first commit

This commit is contained in:
Marwan Alwali 2025-08-12 13:33:25 +03:00
commit 1992c3359d
19152 changed files with 2726664 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

35
.idea/hospital_management_system_v4.iml generated Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="hospital_management/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="uv (hospital_management_system_v4)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates/emr/templates" />
</list>
</option>
</component>
</module>

View File

@ -0,0 +1,46 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="102" name="Python" />
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="app-emergency-department-patient-flow" />
<item index="7" class="java.lang.String" itemvalue="app-alarm-list" />
<item index="8" class="java.lang.String" itemvalue="app-root" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="id" />
</list>
</option>
</inspection_tool>
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

17
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (hospital_management_system_v4)" />
</component>
<component name="KubernetesApiProvider">{}</component>
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-4d33890d:18fe9fd09b1:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (hospital_management_system_v4)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hospital_management_system_v4.iml" filepath="$PROJECT_DIR$/.idea/hospital_management_system_v4.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

0
README.md Normal file
View File

0
accounts/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

230
accounts/admin.py Normal file
View File

@ -0,0 +1,230 @@
"""
Admin configuration for accounts app.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.html import format_html
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""
Admin configuration for User model.
"""
list_display = [
'username', 'email', 'get_full_name', 'role', 'tenant',
'is_active', 'is_verified', 'is_approved', 'last_login'
]
list_filter = [
'role', 'tenant', 'is_active', 'is_verified', 'is_approved',
'two_factor_enabled', 'is_staff', 'is_superuser'
]
search_fields = [
'username', 'email', 'first_name', 'last_name',
'employee_id', 'license_number', 'npi_number'
]
ordering = ['last_name', 'first_name']
fieldsets = BaseUserAdmin.fieldsets + (
('Tenant Information', {
'fields': ('tenant', 'user_id')
}),
('Personal Information', {
'fields': (
'middle_name', 'preferred_name', 'phone_number', 'mobile_number',
'profile_picture', 'bio'
)
}),
('Professional Information', {
'fields': (
'employee_id', 'department', 'job_title', 'role',
'license_number', 'license_state', 'license_expiry',
'dea_number', 'npi_number'
)
}),
('Security Settings', {
'fields': (
'force_password_change', 'password_expires_at',
'failed_login_attempts', 'locked_until', 'two_factor_enabled',
'max_concurrent_sessions', 'session_timeout_minutes'
)
}),
('Preferences', {
'fields': ( 'language', 'theme')
}),
('Status', {
'fields': (
'is_verified', 'is_approved', 'approval_date', 'approved_by'
)
}),
('Metadata', {
'fields': ('created_at', 'updated_at', 'last_password_change'),
'classes': ('collapse',)
}),
)
readonly_fields = [
'user_id', 'created_at', 'updated_at', 'last_password_change'
]
def get_queryset(self, request):
return super().get_queryset(request).select_related('tenant', 'approved_by')
@admin.register(TwoFactorDevice)
class TwoFactorDeviceAdmin(admin.ModelAdmin):
"""
Admin configuration for TwoFactorDevice model.
"""
list_display = [
'user', 'name', 'device_type', 'is_active', 'is_verified',
'last_used_at', 'usage_count'
]
list_filter = ['device_type', 'is_active', 'is_verified']
search_fields = ['user__username', 'user__email', 'name']
ordering = ['-created_at']
fieldsets = (
('Device Information', {
'fields': ('user', 'device_id', 'name', 'device_type')
}),
('Configuration', {
'fields': ('secret_key', 'phone_number', 'email_address')
}),
('Status', {
'fields': ('is_active', 'is_verified', 'verified_at')
}),
('Usage Statistics', {
'fields': ('last_used_at', 'usage_count')
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['device_id', 'created_at', 'updated_at']
@admin.register(SocialAccount)
class SocialAccountAdmin(admin.ModelAdmin):
"""
Admin configuration for SocialAccount model.
"""
list_display = [
'user', 'provider', 'provider_email', 'display_name',
'is_active', 'last_login_at'
]
list_filter = ['provider', 'is_active']
search_fields = [
'user__username', 'user__email', 'provider_email',
'display_name', 'provider_id'
]
ordering = ['-created_at']
fieldsets = (
('User Information', {
'fields': ('user',)
}),
('Provider Information', {
'fields': (
'provider', 'provider_id', 'provider_email',
'display_name', 'profile_url', 'avatar_url'
)
}),
('Tokens', {
'fields': ('access_token', 'refresh_token', 'token_expires_at'),
'classes': ('collapse',)
}),
('Status', {
'fields': ('is_active',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at', 'last_login_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
@admin.register(UserSession)
class UserSessionAdmin(admin.ModelAdmin):
"""
Admin configuration for UserSession model.
"""
list_display = [
'user', 'ip_address', 'device_type', 'browser',
'is_active', 'login_method', 'created_at', 'expires_at'
]
list_filter = [
'device_type', 'is_active', 'login_method',
'country', 'created_at'
]
search_fields = [
'user__username', 'user__email', 'ip_address',
'user_agent', 'browser', 'operating_system'
]
ordering = ['-created_at']
fieldsets = (
('User Information', {
'fields': ('user', 'session_key', 'session_id')
}),
('Device Information', {
'fields': (
'ip_address', 'user_agent', 'device_type',
'browser', 'operating_system'
)
}),
('Location Information', {
'fields': ('country', 'region', 'city')
}),
('Session Status', {
'fields': ('is_active', 'login_method')
}),
('Timestamps', {
'fields': (
'created_at', 'last_activity_at',
'expires_at', 'ended_at'
)
}),
)
readonly_fields = [
'session_id', 'created_at', 'last_activity_at'
]
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')
@admin.register(PasswordHistory)
class PasswordHistoryAdmin(admin.ModelAdmin):
"""
Admin configuration for PasswordHistory model.
"""
list_display = ['user', 'created_at']
list_filter = ['created_at']
search_fields = ['user__username', 'user__email']
ordering = ['-created_at']
fieldsets = (
('User Information', {
'fields': ('user',)
}),
('Password Information', {
'fields': ('password_hash',)
}),
('Metadata', {
'fields': ('created_at',)
}),
)
readonly_fields = ['created_at']
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')

2
accounts/api/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Accounts API package

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

217
accounts/api/serializers.py Normal file
View File

@ -0,0 +1,217 @@
"""
Accounts API serializers.
"""
from rest_framework import serializers
from django.contrib.auth import authenticate
from ..models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
class UserSerializer(serializers.ModelSerializer):
"""
User serializer.
"""
tenant_name = serializers.CharField(source='tenant.name', read_only=True)
full_name = serializers.CharField(source='get_full_name', read_only=True)
display_name = serializers.CharField(source='get_display_name', read_only=True)
is_account_locked = serializers.ReadOnlyField()
is_password_expired = serializers.ReadOnlyField()
is_license_expired = serializers.ReadOnlyField()
class Meta:
model = User
fields = [
'id', 'user_id', 'username', 'email', 'first_name', 'last_name',
'middle_name', 'preferred_name', 'full_name', 'display_name',
'phone_number', 'mobile_number', 'tenant', 'tenant_name',
'employee_id', 'department', 'job_title', 'role',
'license_number', 'license_state', 'license_expiry',
'dea_number', 'npi_number', 'timezone', 'language', 'theme',
'profile_picture', 'bio', 'is_active', 'is_verified', 'is_approved',
'approval_date', 'two_factor_enabled', 'max_concurrent_sessions',
'session_timeout_minutes', 'is_account_locked', 'is_password_expired',
'is_license_expired', 'last_login', 'date_joined', 'created_at', 'updated_at'
]
read_only_fields = [
'user_id', 'last_login', 'date_joined', 'created_at', 'updated_at'
]
extra_kwargs = {
'password': {'write_only': True}
}
def create(self, validated_data):
password = validated_data.pop('password', None)
user = User.objects.create_user(**validated_data)
if password:
user.set_password(password)
user.save()
return user
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
user = super().update(instance, validated_data)
if password:
user.set_password(password)
user.save()
return user
class UserProfileSerializer(serializers.ModelSerializer):
"""
User profile serializer for self-service updates.
"""
tenant_name = serializers.CharField(source='tenant.name', read_only=True)
full_name = serializers.CharField(source='get_full_name', read_only=True)
class Meta:
model = User
fields = [
'id', 'username', 'email', 'first_name', 'last_name',
'middle_name', 'preferred_name', 'full_name',
'phone_number', 'mobile_number', 'tenant_name',
'timezone', 'language', 'theme', 'profile_picture', 'bio'
]
read_only_fields = ['id', 'username', 'tenant_name']
class TwoFactorDeviceSerializer(serializers.ModelSerializer):
"""
Two-factor device serializer.
"""
user_name = serializers.CharField(source='user.get_display_name', read_only=True)
class Meta:
model = TwoFactorDevice
fields = [
'device_id', 'user', 'user_name', 'name', 'device_type',
'phone_number', 'email_address', 'is_active', 'is_verified',
'verified_at', 'last_used_at', 'usage_count',
'created_at', 'updated_at'
]
read_only_fields = ['device_id', 'created_at', 'updated_at']
extra_kwargs = {
'secret_key': {'write_only': True}
}
class SocialAccountSerializer(serializers.ModelSerializer):
"""
Social account serializer.
"""
user_name = serializers.CharField(source='user.get_display_name', read_only=True)
class Meta:
model = SocialAccount
fields = [
'id', 'user', 'user_name', 'provider', 'provider_id',
'provider_email', 'display_name', 'profile_url', 'avatar_url',
'is_active', 'created_at', 'updated_at', 'last_login_at'
]
read_only_fields = ['created_at', 'updated_at']
extra_kwargs = {
'access_token': {'write_only': True},
'refresh_token': {'write_only': True}
}
class UserSessionSerializer(serializers.ModelSerializer):
"""
User session serializer.
"""
user_name = serializers.CharField(source='user.get_display_name', read_only=True)
is_expired = serializers.ReadOnlyField()
class Meta:
model = UserSession
fields = [
'session_id', 'user', 'user_name', 'session_key',
'ip_address', 'user_agent', 'device_type', 'browser',
'operating_system', 'country', 'region', 'city',
'is_active', 'login_method', 'is_expired',
'created_at', 'last_activity_at', 'expires_at', 'ended_at'
]
read_only_fields = ['session_id', 'created_at', 'last_activity_at']
class PasswordHistorySerializer(serializers.ModelSerializer):
"""
Password history serializer.
"""
user_name = serializers.CharField(source='user.get_display_name', read_only=True)
class Meta:
model = PasswordHistory
fields = ['id', 'user', 'user_name', 'created_at']
read_only_fields = ['created_at']
extra_kwargs = {
'password_hash': {'write_only': True}
}
class LoginSerializer(serializers.Serializer):
"""
Login serializer.
"""
username = serializers.CharField()
password = serializers.CharField(write_only=True)
remember_me = serializers.BooleanField(default=False)
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
if username and password:
user = authenticate(username=username, password=password)
if not user:
raise serializers.ValidationError('Invalid credentials')
if not user.is_active:
raise serializers.ValidationError('User account is disabled')
if user.is_account_locked:
raise serializers.ValidationError('User account is locked')
attrs['user'] = user
else:
raise serializers.ValidationError('Must include username and password')
return attrs
class PasswordChangeSerializer(serializers.Serializer):
"""
Password change serializer.
"""
old_password = serializers.CharField(write_only=True)
new_password = serializers.CharField(write_only=True)
confirm_password = serializers.CharField(write_only=True)
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError('Old password is incorrect')
return value
def validate(self, attrs):
if attrs['new_password'] != attrs['confirm_password']:
raise serializers.ValidationError('New passwords do not match')
return attrs
def save(self):
user = self.context['request'].user
user.set_password(self.validated_data['new_password'])
user.save()
return user
class PasswordResetSerializer(serializers.Serializer):
"""
Password reset serializer.
"""
email = serializers.EmailField()
def validate_email(self, value):
try:
user = User.objects.get(email=value, is_active=True)
except User.DoesNotExist:
raise serializers.ValidationError('No active user found with this email')
return value

19
accounts/api/urls.py Normal file
View File

@ -0,0 +1,19 @@
"""
Accounts API URLs.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'users', views.UserViewSet, basename='user')
router.register(r'two-factor-devices', views.TwoFactorDeviceViewSet, basename='twofactordevice')
router.register(r'social-accounts', views.SocialAccountViewSet, basename='socialaccount')
router.register(r'sessions', views.UserSessionViewSet, basename='usersession')
router.register(r'auth', views.AuthViewSet, basename='auth')
urlpatterns = [
path('', include(router.urls)),
]

445
accounts/api/views.py Normal file
View File

@ -0,0 +1,445 @@
"""
Accounts API views.
"""
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q, Count
from django.utils import timezone
from django.contrib.auth import login, logout
from ..models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
from .serializers import (
UserSerializer, UserProfileSerializer, TwoFactorDeviceSerializer,
SocialAccountSerializer, UserSessionSerializer, PasswordHistorySerializer,
LoginSerializer, PasswordChangeSerializer, PasswordResetSerializer
)
from core.utils import AuditLogger
class UserViewSet(viewsets.ModelViewSet):
"""
User API viewset.
"""
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = [
'role', 'department', 'is_active', 'is_verified', 'is_approved',
'two_factor_enabled'
]
search_fields = [
'username', 'email', 'first_name', 'last_name',
'employee_id', 'license_number', 'npi_number'
]
ordering_fields = ['username', 'email', 'last_name', 'created_at', 'last_login']
ordering = ['last_name', 'first_name']
def get_queryset(self):
if hasattr(self.request, 'tenant') and self.request.tenant:
return User.objects.filter(tenant=self.request.tenant)
return User.objects.none()
def perform_create(self, serializer):
user = serializer.save(tenant=getattr(self.request, 'tenant', None))
# Log user creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
event_type='CREATE',
event_category='DATA_MODIFICATION',
action='Create User',
description=f'Created user: {user.username}',
user=self.request.user,
content_object=user,
request=self.request
)
def perform_update(self, serializer):
user = serializer.save()
# Log user update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
event_type='UPDATE',
event_category='DATA_MODIFICATION',
action='Update User',
description=f'Updated user: {user.username}',
user=self.request.user,
content_object=user,
request=self.request
)
@action(detail=False, methods=['get'])
def me(self, request):
"""
Get current user profile.
"""
serializer = UserProfileSerializer(request.user)
return Response(serializer.data)
@action(detail=False, methods=['put', 'patch'])
def update_profile(self, request):
"""
Update current user profile.
"""
serializer = UserProfileSerializer(
request.user,
data=request.data,
partial=request.method == 'PATCH'
)
if serializer.is_valid():
serializer.save()
# Log profile update
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='DATA_MODIFICATION',
action='Update Profile',
description=f'User updated their profile: {request.user.username}',
user=request.user,
content_object=request.user,
request=request
)
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def change_password(self, request):
"""
Change user password.
"""
serializer = PasswordChangeSerializer(
data=request.data,
context={'request': request}
)
if serializer.is_valid():
serializer.save()
# Log password change
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='SECURITY',
action='Change Password',
description=f'User changed their password: {request.user.username}',
user=request.user,
content_object=request.user,
request=request
)
return Response({'message': 'Password changed successfully'})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def lock_account(self, request, pk=None):
"""
Lock user account.
"""
user = self.get_object()
duration = request.data.get('duration_minutes', 15)
user.lock_account(duration)
# Log account lock
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='SECURITY',
action='Lock User Account',
description=f'Locked user account: {user.username}',
user=request.user,
content_object=user,
additional_data={'duration_minutes': duration},
request=request
)
return Response({'message': 'Account locked successfully'})
@action(detail=True, methods=['post'])
def unlock_account(self, request, pk=None):
"""
Unlock user account.
"""
user = self.get_object()
user.unlock_account()
# Log account unlock
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='SECURITY',
action='Unlock User Account',
description=f'Unlocked user account: {user.username}',
user=request.user,
content_object=user,
request=request
)
return Response({'message': 'Account unlocked successfully'})
@action(detail=False, methods=['get'])
def statistics(self, request):
"""
Get user statistics.
"""
queryset = self.get_queryset()
stats = {
'total_users': queryset.count(),
'active_users': queryset.filter(is_active=True).count(),
'pending_approval': queryset.filter(is_approved=False).count(),
'locked_accounts': queryset.filter(locked_until__gt=timezone.now()).count(),
'two_factor_enabled': queryset.filter(two_factor_enabled=True).count(),
'users_by_role': list(
queryset.values('role').annotate(count=Count('id')).order_by('-count')
),
'users_by_department': list(
queryset.exclude(department__isnull=True).exclude(department='')
.values('department').annotate(count=Count('id')).order_by('-count')[:10]
),
}
return Response(stats)
class TwoFactorDeviceViewSet(viewsets.ModelViewSet):
"""
Two-factor device API viewset.
"""
serializer_class = TwoFactorDeviceSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['device_type', 'is_active', 'is_verified']
search_fields = ['name']
ordering_fields = ['name', 'created_at', 'last_used_at']
ordering = ['-created_at']
def get_queryset(self):
# Users can only see their own devices
return TwoFactorDevice.objects.filter(user=self.request.user)
def perform_create(self, serializer):
device = serializer.save(user=self.request.user)
# Log device creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
event_type='CREATE',
event_category='SECURITY',
action='Create Two-Factor Device',
description=f'Created two-factor device: {device.name}',
user=self.request.user,
content_object=device,
request=self.request
)
def perform_destroy(self, instance):
device_name = instance.name
instance.delete()
# Log device deletion
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
event_type='DELETE',
event_category='SECURITY',
action='Delete Two-Factor Device',
description=f'Deleted two-factor device: {device_name}',
user=self.request.user,
additional_data={'device_name': device_name},
request=self.request
)
class SocialAccountViewSet(viewsets.ReadOnlyModelViewSet):
"""
Social account API viewset.
"""
serializer_class = SocialAccountSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['provider', 'is_active']
search_fields = ['provider_email', 'display_name']
ordering_fields = ['provider', 'created_at', 'last_login_at']
ordering = ['-created_at']
def get_queryset(self):
# Users can only see their own social accounts
return SocialAccount.objects.filter(user=self.request.user)
class UserSessionViewSet(viewsets.ReadOnlyModelViewSet):
"""
User session API viewset.
"""
serializer_class = UserSessionSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['device_type', 'is_active', 'login_method', 'country']
search_fields = ['ip_address', 'browser', 'operating_system']
ordering_fields = ['created_at', 'last_activity_at', 'expires_at']
ordering = ['-created_at']
def get_queryset(self):
if self.request.user.is_staff:
# Staff can see all sessions in their tenant
if hasattr(self.request, 'tenant') and self.request.tenant:
return UserSession.objects.filter(user__tenant=self.request.tenant)
else:
# Regular users can only see their own sessions
return UserSession.objects.filter(user=self.request.user)
return UserSession.objects.none()
@action(detail=True, methods=['post'])
def end_session(self, request, pk=None):
"""
End a user session.
"""
session = self.get_object()
# Check if user can end this session
if not request.user.is_staff and session.user != request.user:
return Response(
{'error': 'You can only end your own sessions'},
status=status.HTTP_403_FORBIDDEN
)
session.end_session()
# Log session termination
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='AUTHENTICATION',
action='End User Session',
description=f'Ended session for user: {session.user.username}',
user=request.user,
content_object=session,
additional_data={
'target_user': session.user.username,
'session_ip': session.ip_address,
},
request=request
)
return Response({'message': 'Session ended successfully'})
@action(detail=False, methods=['get'])
def active(self, request):
"""
Get active sessions.
"""
queryset = self.get_queryset().filter(is_active=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class AuthViewSet(viewsets.ViewSet):
"""
Authentication API viewset.
"""
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
def login(self, request):
"""
User login.
"""
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = serializer.validated_data['user']
login(request, user)
# Log successful login
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='LOGIN',
event_category='AUTHENTICATION',
action='User Login',
description=f'User logged in: {user.username}',
user=user,
content_object=user,
request=request
)
# Reset failed login attempts
user.reset_failed_login()
return Response({
'message': 'Login successful',
'user': UserSerializer(user).data
})
else:
# Log failed login attempt
username = request.data.get('username')
if username:
try:
user = User.objects.get(username=username)
user.increment_failed_login()
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='LOGIN',
event_category='SECURITY',
action='Failed Login Attempt',
description=f'Failed login attempt for user: {username}',
user=None,
is_successful=False,
additional_data={'username': username},
request=request
)
except User.DoesNotExist:
pass
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def logout(self, request):
"""
User logout.
"""
user = request.user
logout(request)
# Log logout
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='LOGOUT',
event_category='AUTHENTICATION',
action='User Logout',
description=f'User logged out: {user.username}',
user=user,
content_object=user,
request=request
)
return Response({'message': 'Logout successful'})
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
def password_reset(self, request):
"""
Password reset request.
"""
serializer = PasswordResetSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
# Log password reset request
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='UPDATE',
event_category='SECURITY',
action='Password Reset Request',
description=f'Password reset requested for email: {email}',
user=None,
additional_data={'email': email},
request=request
)
# In a real implementation, send password reset email here
return Response({'message': 'Password reset email sent'})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

6
accounts/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

225
accounts/forms.py Normal file
View File

@ -0,0 +1,225 @@
"""
Forms for Accounts app CRUD operations.
"""
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.core.exceptions import ValidationError
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
class UserForm(forms.ModelForm):
"""
Form for updating user information.
"""
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number', 'mobile_number',
'employee_id', 'role', 'department', 'bio', 'user_timezone', 'language',
'theme', 'is_active', 'is_approved'
]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
'mobile_number': forms.TextInput(attrs={'class': 'form-control'}),
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'department': forms.TextInput(attrs={'class': 'form-control'}),
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'user_timezone': forms.Select(attrs={'class': 'form-select'}),
'language': forms.Select(attrs={'class': 'form-select'}),
'theme': forms.Select(attrs={'class': 'form-select'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'is_approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class UserCreateForm(UserCreationForm):
"""
Form for creating new users.
"""
first_name = forms.CharField(max_length=150, required=True)
last_name = forms.CharField(max_length=150, required=True)
email = forms.EmailField(required=True)
employee_id = forms.CharField(max_length=50, required=False)
role = forms.ChoiceField(choices=User._meta.get_field('role').choices, required=True)
department = forms.CharField(max_length=100, required=False)
class Meta:
model = User
fields = [
'username', 'first_name', 'last_name', 'email', 'employee_id',
'role', 'department', 'password1', 'password2'
]
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'department': forms.TextInput(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['password1'].widget.attrs.update({'class': 'form-control'})
self.fields['password2'].widget.attrs.update({'class': 'form-control'})
class TwoFactorDeviceForm(forms.ModelForm):
"""
Form for two-factor device management.
"""
class Meta:
model = TwoFactorDevice
fields = ['user', 'name', 'device_type', 'phone_number', 'email_address']
widgets = {
'user': forms.Select(attrs={'class': 'form-select'}),
'name': forms.TextInput(attrs={'class': 'form-control'}),
'device_type': forms.Select(attrs={'class': 'form-select'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
'email_address': forms.EmailInput(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['user'].queryset = User.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('last_name', 'first_name')
def clean(self):
cleaned_data = super().clean()
device_type = cleaned_data.get('device_type')
phone_number = cleaned_data.get('phone_number')
email_address = cleaned_data.get('email_address')
if device_type == 'SMS' and not phone_number:
raise ValidationError('Phone number is required for SMS devices.')
if device_type == 'EMAIL' and not email_address:
raise ValidationError('Email address is required for email devices.')
return cleaned_data
class SocialAccountForm(forms.ModelForm):
"""
Form for social account management.
"""
class Meta:
model = SocialAccount
fields = ['user', 'provider', 'provider_id', 'display_name', 'profile_url']
widgets = {
'user': forms.Select(attrs={'class': 'form-select'}),
'provider': forms.TextInput(attrs={'class': 'form-control'}),
'provider_id': forms.TextInput(attrs={'class': 'form-control'}),
'display_name': forms.TextInput(attrs={'class': 'form-control'}),
'profile_url': forms.URLInput(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['user'].queryset = User.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('last_name', 'first_name')
class AccountsSearchForm(forms.Form):
"""
Form for searching accounts data.
"""
search = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search users, sessions, devices...'
})
)
role = forms.ChoiceField(
choices=[('', 'All Roles')] + list(User._meta.get_field('role').choices),
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
status = forms.ChoiceField(
choices=[
('', 'All Status'),
('active', 'Active'),
('inactive', 'Inactive'),
('pending', 'Pending Approval')
],
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
class PasswordChangeForm(forms.Form):
"""
Form for changing user passwords.
"""
old_password = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label='Current Password'
)
new_password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label='New Password'
)
new_password2 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label='Confirm New Password'
)
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def clean_old_password(self):
old_password = self.cleaned_data.get('old_password')
if not self.user.check_password(old_password):
raise ValidationError('Your old password was entered incorrectly.')
return old_password
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get('new_password1')
password2 = cleaned_data.get('new_password2')
if password1 and password2:
if password1 != password2:
raise ValidationError('The two password fields didn\'t match.')
return cleaned_data
def save(self):
password = self.cleaned_data['new_password1']
self.user.set_password(password)
self.user.save()
return self.user

View File

@ -0,0 +1,745 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="PasswordHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"password_hash",
models.CharField(help_text="Hashed password", max_length=128),
),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"verbose_name": "Password History",
"verbose_name_plural": "Password History",
"db_table": "accounts_password_history",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="SocialAccount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"provider",
models.CharField(
choices=[
("GOOGLE", "Google"),
("MICROSOFT", "Microsoft"),
("APPLE", "Apple"),
("FACEBOOK", "Facebook"),
("LINKEDIN", "LinkedIn"),
("GITHUB", "GitHub"),
("OKTA", "Okta"),
("SAML", "SAML"),
("LDAP", "LDAP"),
],
max_length=50,
),
),
(
"provider_id",
models.CharField(help_text="Provider user ID", max_length=200),
),
(
"provider_email",
models.EmailField(
blank=True,
help_text="Email from provider",
max_length=254,
null=True,
),
),
(
"display_name",
models.CharField(
blank=True,
help_text="Display name from provider",
max_length=200,
null=True,
),
),
(
"profile_url",
models.URLField(
blank=True, help_text="Profile URL from provider", null=True
),
),
(
"avatar_url",
models.URLField(
blank=True, help_text="Avatar URL from provider", null=True
),
),
(
"access_token",
models.TextField(
blank=True, help_text="Access token from provider", null=True
),
),
(
"refresh_token",
models.TextField(
blank=True, help_text="Refresh token from provider", null=True
),
),
(
"token_expires_at",
models.DateTimeField(
blank=True, help_text="Token expiration date", null=True
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Social account is active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"last_login_at",
models.DateTimeField(
blank=True,
help_text="Last login using this social account",
null=True,
),
),
],
options={
"verbose_name": "Social Account",
"verbose_name_plural": "Social Accounts",
"db_table": "accounts_social_account",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="TwoFactorDevice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"device_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique device identifier",
unique=True,
),
),
("name", models.CharField(help_text="Device name", max_length=100)),
(
"device_type",
models.CharField(
choices=[
("TOTP", "Time-based OTP (Authenticator App)"),
("SMS", "SMS"),
("EMAIL", "Email"),
("HARDWARE", "Hardware Token"),
("BACKUP", "Backup Codes"),
],
max_length=20,
),
),
(
"secret_key",
models.CharField(
blank=True,
help_text="Secret key for TOTP devices",
max_length=200,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Phone number for SMS devices",
max_length=20,
null=True,
),
),
(
"email_address",
models.EmailField(
blank=True,
help_text="Email address for email devices",
max_length=254,
null=True,
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Device is active"),
),
(
"is_verified",
models.BooleanField(default=False, help_text="Device is verified"),
),
(
"verified_at",
models.DateTimeField(
blank=True, help_text="Device verification date", null=True
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last time device was used", null=True
),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times device was used"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Two Factor Device",
"verbose_name_plural": "Two Factor Devices",
"db_table": "accounts_two_factor_device",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="UserSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"session_key",
models.CharField(
help_text="Django session key", max_length=40, unique=True
),
),
(
"session_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique session identifier",
unique=True,
),
),
("ip_address", models.GenericIPAddressField(help_text="IP address")),
("user_agent", models.TextField(help_text="User agent string")),
(
"device_type",
models.CharField(
choices=[
("DESKTOP", "Desktop"),
("MOBILE", "Mobile"),
("TABLET", "Tablet"),
("UNKNOWN", "Unknown"),
],
default="UNKNOWN",
max_length=20,
),
),
(
"browser",
models.CharField(
blank=True,
help_text="Browser name and version",
max_length=100,
null=True,
),
),
(
"operating_system",
models.CharField(
blank=True,
help_text="Operating system",
max_length=100,
null=True,
),
),
(
"country",
models.CharField(
blank=True, help_text="Country", max_length=100, null=True
),
),
(
"region",
models.CharField(
blank=True, help_text="Region/State", max_length=100, null=True
),
),
(
"city",
models.CharField(
blank=True, help_text="City", max_length=100, null=True
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Session is active"),
),
(
"login_method",
models.CharField(
choices=[
("PASSWORD", "Password"),
("TWO_FACTOR", "Two Factor"),
("SOCIAL", "Social Login"),
("SSO", "Single Sign-On"),
("API_KEY", "API Key"),
],
default="PASSWORD",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("last_activity_at", models.DateTimeField(auto_now=True)),
(
"expires_at",
models.DateTimeField(help_text="Session expiration time"),
),
(
"ended_at",
models.DateTimeField(
blank=True, help_text="Session end time", null=True
),
),
],
options={
"verbose_name": "User Session",
"verbose_name_plural": "User Sessions",
"db_table": "accounts_user_session",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"user_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique user identifier",
unique=True,
),
),
(
"middle_name",
models.CharField(
blank=True, help_text="Middle name", max_length=150, null=True
),
),
(
"preferred_name",
models.CharField(
blank=True,
help_text="Preferred name",
max_length=150,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Primary phone number",
max_length=20,
null=True,
validators=[
django.core.validators.RegexValidator(
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
regex="^\\+?1?\\d{9,15}$",
)
],
),
),
(
"mobile_number",
models.CharField(
blank=True,
help_text="Mobile phone number",
max_length=20,
null=True,
validators=[
django.core.validators.RegexValidator(
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
regex="^\\+?1?\\d{9,15}$",
)
],
),
),
(
"employee_id",
models.CharField(
blank=True, help_text="Employee ID", max_length=50, null=True
),
),
(
"department",
models.CharField(
blank=True, help_text="Department", max_length=100, null=True
),
),
(
"job_title",
models.CharField(
blank=True, help_text="Job title", max_length=100, null=True
),
),
(
"role",
models.CharField(
choices=[
("SUPER_ADMIN", "Super Administrator"),
("ADMIN", "Administrator"),
("PHYSICIAN", "Physician"),
("NURSE", "Nurse"),
("NURSE_PRACTITIONER", "Nurse Practitioner"),
("PHYSICIAN_ASSISTANT", "Physician Assistant"),
("PHARMACIST", "Pharmacist"),
("PHARMACY_TECH", "Pharmacy Technician"),
("LAB_TECH", "Laboratory Technician"),
("RADIOLOGIST", "Radiologist"),
("RAD_TECH", "Radiology Technician"),
("THERAPIST", "Therapist"),
("SOCIAL_WORKER", "Social Worker"),
("CASE_MANAGER", "Case Manager"),
("BILLING_SPECIALIST", "Billing Specialist"),
("REGISTRATION", "Registration Staff"),
("SCHEDULER", "Scheduler"),
("MEDICAL_ASSISTANT", "Medical Assistant"),
("CLERICAL", "Clerical Staff"),
("IT_SUPPORT", "IT Support"),
("QUALITY_ASSURANCE", "Quality Assurance"),
("COMPLIANCE", "Compliance Officer"),
("SECURITY", "Security"),
("MAINTENANCE", "Maintenance"),
("VOLUNTEER", "Volunteer"),
("STUDENT", "Student"),
("RESEARCHER", "Researcher"),
("CONSULTANT", "Consultant"),
("VENDOR", "Vendor"),
("GUEST", "Guest"),
],
default="CLERICAL",
max_length=50,
),
),
(
"license_number",
models.CharField(
blank=True,
help_text="Professional license number",
max_length=100,
null=True,
),
),
(
"license_state",
models.CharField(
blank=True,
help_text="License issuing state",
max_length=50,
null=True,
),
),
(
"license_expiry",
models.DateField(
blank=True, help_text="License expiry date", null=True
),
),
(
"dea_number",
models.CharField(
blank=True,
help_text="DEA number for prescribing",
max_length=20,
null=True,
),
),
(
"npi_number",
models.CharField(
blank=True,
help_text="National Provider Identifier",
max_length=10,
null=True,
),
),
(
"force_password_change",
models.BooleanField(
default=False,
help_text="User must change password on next login",
),
),
(
"password_expires_at",
models.DateTimeField(
blank=True, help_text="Password expiration date", null=True
),
),
(
"failed_login_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of failed login attempts"
),
),
(
"locked_until",
models.DateTimeField(
blank=True,
help_text="Account locked until this time",
null=True,
),
),
(
"two_factor_enabled",
models.BooleanField(
default=False, help_text="Two-factor authentication enabled"
),
),
(
"max_concurrent_sessions",
models.PositiveIntegerField(
default=3, help_text="Maximum concurrent sessions allowed"
),
),
(
"session_timeout_minutes",
models.PositiveIntegerField(
default=30, help_text="Session timeout in minutes"
),
),
(
"user_timezone",
models.CharField(
default="UTC", help_text="User timezone", max_length=50
),
),
(
"language",
models.CharField(
default="en", help_text="Preferred language", max_length=10
),
),
(
"theme",
models.CharField(
choices=[
("LIGHT", "Light"),
("DARK", "Dark"),
("AUTO", "Auto"),
],
default="LIGHT",
max_length=20,
),
),
(
"profile_picture",
models.ImageField(
blank=True,
help_text="Profile picture",
null=True,
upload_to="profile_pictures/",
),
),
(
"bio",
models.TextField(
blank=True, help_text="Professional bio", null=True
),
),
(
"is_verified",
models.BooleanField(
default=False, help_text="User account is verified"
),
),
(
"is_approved",
models.BooleanField(
default=False, help_text="User account is approved"
),
),
(
"approval_date",
models.DateTimeField(
blank=True, help_text="Account approval date", null=True
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"last_password_change",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Last password change date",
),
),
(
"approved_by",
models.ForeignKey(
blank=True,
help_text="User who approved this account",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="approved_users",
to=settings.AUTH_USER_MODEL,
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
],
options={
"verbose_name": "User",
"verbose_name_plural": "Users",
"db_table": "accounts_user",
"ordering": ["last_name", "first_name"],
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,123 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("accounts", "0001_initial"),
("auth", "0012_alter_user_first_name_max_length"),
("core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="user",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="users",
to="core.tenant",
),
),
migrations.AddField(
model_name="user",
name="user_permissions",
field=models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
migrations.AddField(
model_name="passwordhistory",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="password_history",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="socialaccount",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="social_accounts",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="twofactordevice",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="two_factor_devices",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="usersession",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_sessions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "role"], name="accounts_us_tenant__731b87_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["employee_id"], name="accounts_us_employe_0cbd94_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["license_number"], name="accounts_us_license_02eb85_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["npi_number"], name="accounts_us_npi_num_800ef1_idx"
),
),
migrations.AlterUniqueTogether(
name="socialaccount",
unique_together={("provider", "provider_id")},
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["user", "is_active"], name="accounts_us_user_id_f3bc3f_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["session_key"], name="accounts_us_session_5ce38e_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["ip_address"], name="accounts_us_ip_addr_f7885b_idx"
),
),
]

View File

709
accounts/models.py Normal file
View File

@ -0,0 +1,709 @@
"""
Accounts app models for hospital management system.
Provides user management, authentication, and authorization functionality.
"""
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.core.validators import RegexValidator
from django.utils import timezone
from django.conf import settings
class User(AbstractUser):
"""
Extended user model for hospital management system.
"""
# Basic Information
user_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique user identifier'
)
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='users',
help_text='Organization tenant'
)
# Personal Information
middle_name = models.CharField(
max_length=150,
blank=True,
null=True,
help_text='Middle name'
)
preferred_name = models.CharField(
max_length=150,
blank=True,
null=True,
help_text='Preferred name'
)
# Contact Information
phone_number = models.CharField(
max_length=20,
blank=True,
null=True,
validators=[RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
)],
help_text='Primary phone number'
)
mobile_number = models.CharField(
max_length=20,
blank=True,
null=True,
validators=[RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
)],
help_text='Mobile phone number'
)
# Professional Information
employee_id = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Employee ID'
)
department = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Department'
)
job_title = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Job title'
)
# Role and Permissions
role = models.CharField(
max_length=50,
choices=[
('SUPER_ADMIN', 'Super Administrator'),
('ADMIN', 'Administrator'),
('PHYSICIAN', 'Physician'),
('NURSE', 'Nurse'),
('NURSE_PRACTITIONER', 'Nurse Practitioner'),
('PHYSICIAN_ASSISTANT', 'Physician Assistant'),
('PHARMACIST', 'Pharmacist'),
('PHARMACY_TECH', 'Pharmacy Technician'),
('LAB_TECH', 'Laboratory Technician'),
('RADIOLOGIST', 'Radiologist'),
('RAD_TECH', 'Radiology Technician'),
('THERAPIST', 'Therapist'),
('SOCIAL_WORKER', 'Social Worker'),
('CASE_MANAGER', 'Case Manager'),
('BILLING_SPECIALIST', 'Billing Specialist'),
('REGISTRATION', 'Registration Staff'),
('SCHEDULER', 'Scheduler'),
('MEDICAL_ASSISTANT', 'Medical Assistant'),
('CLERICAL', 'Clerical Staff'),
('IT_SUPPORT', 'IT Support'),
('QUALITY_ASSURANCE', 'Quality Assurance'),
('COMPLIANCE', 'Compliance Officer'),
('SECURITY', 'Security'),
('MAINTENANCE', 'Maintenance'),
('VOLUNTEER', 'Volunteer'),
('STUDENT', 'Student'),
('RESEARCHER', 'Researcher'),
('CONSULTANT', 'Consultant'),
('VENDOR', 'Vendor'),
('GUEST', 'Guest'),
],
default='CLERICAL'
)
# License and Certification
license_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Professional license number'
)
license_state = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='License issuing state'
)
license_expiry = models.DateField(
blank=True,
null=True,
help_text='License expiry date'
)
dea_number = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='DEA number for prescribing'
)
npi_number = models.CharField(
max_length=10,
blank=True,
null=True,
help_text='National Provider Identifier'
)
# Security Settings
force_password_change = models.BooleanField(
default=False,
help_text='User must change password on next login'
)
password_expires_at = models.DateTimeField(
blank=True,
null=True,
help_text='Password expiration date'
)
failed_login_attempts = models.PositiveIntegerField(
default=0,
help_text='Number of failed login attempts'
)
locked_until = models.DateTimeField(
blank=True,
null=True,
help_text='Account locked until this time'
)
two_factor_enabled = models.BooleanField(
default=False,
help_text='Two-factor authentication enabled'
)
# Session Management
max_concurrent_sessions = models.PositiveIntegerField(
default=3,
help_text='Maximum concurrent sessions allowed'
)
session_timeout_minutes = models.PositiveIntegerField(
default=30,
help_text='Session timeout in minutes'
)
# Preferences
user_timezone = models.CharField(
max_length=50,
default='UTC',
help_text='User timezone'
)
language = models.CharField(
max_length=10,
default='en',
help_text='Preferred language'
)
theme = models.CharField(
max_length=20,
choices=[
('LIGHT', 'Light'),
('DARK', 'Dark'),
('AUTO', 'Auto'),
],
default='LIGHT'
)
# Profile Information
profile_picture = models.ImageField(
upload_to='profile_pictures/',
blank=True,
null=True,
help_text='Profile picture'
)
bio = models.TextField(
blank=True,
null=True,
help_text='Professional bio'
)
# Status
is_verified = models.BooleanField(
default=False,
help_text='User account is verified'
)
is_approved = models.BooleanField(
default=False,
help_text='User account is approved'
)
approval_date = models.DateTimeField(
blank=True,
null=True,
help_text='Account approval date'
)
approved_by = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_users',
help_text='User who approved this account'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_password_change = models.DateTimeField(
default=timezone.now,
help_text='Last password change date'
)
class Meta:
db_table = 'accounts_user'
verbose_name = 'User'
verbose_name_plural = 'Users'
ordering = ['last_name', 'first_name']
indexes = [
models.Index(fields=['tenant', 'role']),
models.Index(fields=['employee_id']),
models.Index(fields=['license_number']),
models.Index(fields=['npi_number']),
]
def __str__(self):
return f"{self.get_full_name()} ({self.username})"
def get_full_name(self):
"""
Return the full name for the user.
"""
if self.preferred_name:
return f"{self.preferred_name} {self.last_name}"
return super().get_full_name()
def get_display_name(self):
"""
Return the display name for the user.
"""
full_name = self.get_full_name()
if full_name.strip():
return full_name
return self.username
@property
def is_account_locked(self):
"""
Check if account is currently locked.
"""
if self.locked_until:
return timezone.now() < self.locked_until
return False
@property
def is_password_expired(self):
"""
Check if password has expired.
"""
if self.password_expires_at:
return timezone.now() > self.password_expires_at
return False
@property
def is_license_expired(self):
"""
Check if professional license has expired.
"""
if self.license_expiry:
return timezone.now().date() > self.license_expiry
return False
def lock_account(self, duration_minutes=15):
"""
Lock the user account for specified duration.
"""
self.locked_until = timezone.now() + timezone.timedelta(minutes=duration_minutes)
self.save(update_fields=['locked_until'])
def unlock_account(self):
"""
Unlock the user account.
"""
self.locked_until = None
self.failed_login_attempts = 0
self.save(update_fields=['locked_until', 'failed_login_attempts'])
def increment_failed_login(self):
"""
Increment failed login attempts and lock if threshold reached.
"""
self.failed_login_attempts += 1
# Lock account after 5 failed attempts
max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
if self.failed_login_attempts >= max_attempts:
lockout_duration = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
self.lock_account(lockout_duration)
self.save(update_fields=['failed_login_attempts'])
def reset_failed_login(self):
"""
Reset failed login attempts.
"""
self.failed_login_attempts = 0
self.save(update_fields=['failed_login_attempts'])
class TwoFactorDevice(models.Model):
"""
Two-factor authentication devices for users.
"""
# User relationship
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='two_factor_devices'
)
# Device Information
device_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique device identifier'
)
name = models.CharField(
max_length=100,
help_text='Device name'
)
device_type = models.CharField(
max_length=20,
choices=[
('TOTP', 'Time-based OTP (Authenticator App)'),
('SMS', 'SMS'),
('EMAIL', 'Email'),
('HARDWARE', 'Hardware Token'),
('BACKUP', 'Backup Codes'),
]
)
# Device Configuration
secret_key = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Secret key for TOTP devices'
)
phone_number = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Phone number for SMS devices'
)
email_address = models.EmailField(
blank=True,
null=True,
help_text='Email address for email devices'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Device is active'
)
is_verified = models.BooleanField(
default=False,
help_text='Device is verified'
)
verified_at = models.DateTimeField(
blank=True,
null=True,
help_text='Device verification date'
)
# Usage Statistics
last_used_at = models.DateTimeField(
blank=True,
null=True,
help_text='Last time device was used'
)
usage_count = models.PositiveIntegerField(
default=0,
help_text='Number of times device was used'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'accounts_two_factor_device'
verbose_name = 'Two Factor Device'
verbose_name_plural = 'Two Factor Devices'
ordering = ['-created_at']
def __str__(self):
return f"{self.user.username} - {self.name} ({self.device_type})"
class SocialAccount(models.Model):
"""
Social authentication accounts linked to users.
"""
# User relationship
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='social_accounts'
)
# Provider Information
provider = models.CharField(
max_length=50,
choices=[
('GOOGLE', 'Google'),
('MICROSOFT', 'Microsoft'),
('APPLE', 'Apple'),
('FACEBOOK', 'Facebook'),
('LINKEDIN', 'LinkedIn'),
('GITHUB', 'GitHub'),
('OKTA', 'Okta'),
('SAML', 'SAML'),
('LDAP', 'LDAP'),
]
)
provider_id = models.CharField(
max_length=200,
help_text='Provider user ID'
)
provider_email = models.EmailField(
blank=True,
null=True,
help_text='Email from provider'
)
# Account Information
display_name = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Display name from provider'
)
profile_url = models.URLField(
blank=True,
null=True,
help_text='Profile URL from provider'
)
avatar_url = models.URLField(
blank=True,
null=True,
help_text='Avatar URL from provider'
)
# Tokens
access_token = models.TextField(
blank=True,
null=True,
help_text='Access token from provider'
)
refresh_token = models.TextField(
blank=True,
null=True,
help_text='Refresh token from provider'
)
token_expires_at = models.DateTimeField(
blank=True,
null=True,
help_text='Token expiration date'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Social account is active'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_login_at = models.DateTimeField(
blank=True,
null=True,
help_text='Last login using this social account'
)
class Meta:
db_table = 'accounts_social_account'
verbose_name = 'Social Account'
verbose_name_plural = 'Social Accounts'
unique_together = ['provider', 'provider_id']
ordering = ['-created_at']
def __str__(self):
return f"{self.user.username} - {self.provider}"
class UserSession(models.Model):
"""
User session tracking for security and audit purposes.
"""
# User relationship
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='user_sessions'
)
# Session Information
session_key = models.CharField(
max_length=40,
unique=True,
help_text='Django session key'
)
session_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique session identifier'
)
# Device Information
ip_address = models.GenericIPAddressField(
help_text='IP address'
)
user_agent = models.TextField(
help_text='User agent string'
)
device_type = models.CharField(
max_length=20,
choices=[
('DESKTOP', 'Desktop'),
('MOBILE', 'Mobile'),
('TABLET', 'Tablet'),
('UNKNOWN', 'Unknown'),
],
default='UNKNOWN'
)
browser = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Browser name and version'
)
operating_system = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Operating system'
)
# Location Information
country = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Country'
)
region = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Region/State'
)
city = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='City'
)
# Session Status
is_active = models.BooleanField(
default=True,
help_text='Session is active'
)
login_method = models.CharField(
max_length=20,
choices=[
('PASSWORD', 'Password'),
('TWO_FACTOR', 'Two Factor'),
('SOCIAL', 'Social Login'),
('SSO', 'Single Sign-On'),
('API_KEY', 'API Key'),
],
default='PASSWORD'
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
last_activity_at = models.DateTimeField(auto_now=True)
expires_at = models.DateTimeField(
help_text='Session expiration time'
)
ended_at = models.DateTimeField(
blank=True,
null=True,
help_text='Session end time'
)
class Meta:
db_table = 'accounts_user_session'
verbose_name = 'User Session'
verbose_name_plural = 'User Sessions'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'is_active']),
models.Index(fields=['session_key']),
models.Index(fields=['ip_address']),
]
def __str__(self):
return f"{self.user.username} - {self.ip_address} - {self.created_at}"
@property
def is_expired(self):
"""
Check if session has expired.
"""
return timezone.now() > self.expires_at
def end_session(self):
"""
End the session.
"""
self.is_active = False
self.ended_at = timezone.now()
self.save(update_fields=['is_active', 'ended_at'])
class PasswordHistory(models.Model):
"""
Password history for users to prevent password reuse.
"""
# User relationship
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='password_history'
)
# Password Information
password_hash = models.CharField(
max_length=128,
help_text='Hashed password'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'accounts_password_history'
verbose_name = 'Password History'
verbose_name_plural = 'Password History'
ordering = ['-created_at']
def __str__(self):
return f"{self.user.username} - {self.created_at}"

3
accounts/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

34
accounts/urls.py Normal file
View File

@ -0,0 +1,34 @@
"""
URL configuration for accounts app.
"""
from django.urls import path
from . import views
from allauth.account.views import SignupView, LoginView, LogoutView
app_name = 'accounts'
urlpatterns = [
# path('accounts/', include('allauth.urls')),
# Main views
# path('login/', views.AccountLoginView.as_view(), name='login'),
# path('logout/', LogoutView.as_view(), name='logout'),
# path('signup/', SignupView.as_view(), name='account_signup'),
path('users/', views.UserListView.as_view(), name='user_list'),
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'),
path('profile/', views.UserProfileView.as_view(), name='user_profile'),
path('sessions/', views.SessionManagementView.as_view(), name='session_management'),
# HTMX views
path('htmx/user-search/', views.user_search, name='user_search'),
path('htmx/user-stats/', views.user_stats, name='user_stats'),
path('htmx/session-list/', views.session_list, name='session_list'),
path('htmx/end-session/<uuid:session_id>/', views.end_session, name='end_session'),
path('htmx/profile-update/', views.user_profile_update, name='user_profile_update'),
path('htmx/two-factor-setup/', views.two_factor_setup, name='two_factor_setup'),
path('htmx/remove-two-factor/<uuid:device_id>/', views.remove_two_factor_device, name='remove_two_factor_device'),
path('htmx/user-activity/<int:user_id>/', views.user_activity_log, name='user_activity_log'),
]

2421
accounts/views.py Normal file

File diff suppressed because it is too large Load Diff

457
accounts_data.py Normal file
View File

@ -0,0 +1,457 @@
import os
import django
# Set up Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
import random
from datetime import datetime, timedelta, timezone
from django.contrib.auth.hashers import make_password
from django.utils import timezone as django_timezone
from accounts.models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
from core.models import Tenant
import uuid
import secrets
# Saudi-specific data constants
SAUDI_FIRST_NAMES_MALE = [
'Mohammed', 'Abdullah', 'Ahmed', 'Omar', 'Ali', 'Hassan', 'Khalid', 'Faisal',
'Saad', 'Fahd', 'Bandar', 'Turki', 'Nasser', 'Saud', 'Abdulrahman',
'Abdulaziz', 'Salman', 'Waleed', 'Majid', 'Rayan', 'Yazeed', 'Mansour',
'Osama', 'Tariq', 'Adel', 'Nawaf', 'Sultan', 'Mishaal', 'Badr', 'Ziad'
]
SAUDI_FIRST_NAMES_FEMALE = [
'Fatima', 'Aisha', 'Maryam', 'Khadija', 'Sarah', 'Noura', 'Hala', 'Reem',
'Lina', 'Dana', 'Rana', 'Nada', 'Layla', 'Amira', 'Zahra', 'Yasmin',
'Dina', 'Noor', 'Rahma', 'Salma', 'Lama', 'Ghada', 'Rania', 'Maha',
'Wedad', 'Najla', 'Shahd', 'Jood', 'Rand', 'Malak'
]
SAUDI_FAMILY_NAMES = [
'Al-Rashid', 'Al-Harbi', 'Al-Qahtani', 'Al-Dosari', 'Al-Otaibi', 'Al-Mutairi',
'Al-Shammari', 'Al-Zahrani', 'Al-Ghamdi', 'Al-Maliki', 'Al-Subai', 'Al-Jubayr',
'Al-Faisal', 'Al-Saud', 'Al-Thani', 'Al-Maktoum', 'Al-Sabah', 'Al-Khalifa',
'Bin-Laden', 'Al-Rajhi', 'Al-Sudairy', 'Al-Shaalan', 'Al-Kabeer', 'Al-Ajmi',
'Al-Anzi', 'Al-Dawsari', 'Al-Shamrani', 'Al-Balawi', 'Al-Juhani', 'Al-Sulami'
]
SAUDI_MIDDLE_NAMES = [
'bin Ahmed', 'bin Mohammed', 'bin Abdullah', 'bin Omar', 'bin Ali', 'bin Hassan',
'bin Khalid', 'bin Faisal', 'bin Saad', 'bin Fahd', 'bin Abdulaziz', 'bin Salman'
]
SAUDI_CITIES = [
'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran',
'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hofuf', 'Mubarraz',
'Jubail', 'Yanbu', 'Abha', 'Najran', 'Jazan', 'Hail', 'Arar'
]
SAUDI_PROVINCES = [
'Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province',
'Jazan Province', 'Medina Province', 'Qassim Province', 'Tabuk Province',
'Hail Province', 'Northern Borders Province', 'Najran Province', 'Al Bahah Province'
]
SAUDI_JOB_TITLES = {
'PHYSICIAN': ['Consultant Physician', 'Senior Physician', 'Staff Physician', 'Resident Physician',
'Chief Medical Officer'],
'NURSE': ['Head Nurse', 'Senior Nurse', 'Staff Nurse', 'Charge Nurse', 'Clinical Nurse Specialist'],
'PHARMACIST': ['Clinical Pharmacist', 'Staff Pharmacist', 'Pharmacy Manager', 'Pharmaceutical Consultant'],
'ADMIN': ['Medical Director', 'Hospital Administrator', 'Department Manager', 'Operations Manager'],
'LAB_TECH': ['Senior Lab Technician', 'Medical Laboratory Scientist', 'Lab Supervisor'],
'RAD_TECH': ['Senior Radiologic Technologist', 'CT Technologist', 'MRI Technologist'],
'RADIOLOGIST': ['Consultant Radiologist', 'Senior Radiologist', 'Interventional Radiologist']
}
SAUDI_DEPARTMENTS = [
'Internal Medicine', 'Cardiology', 'Orthopedics', 'Neurology', 'Oncology',
'Pediatrics', 'Emergency Medicine', 'Radiology', 'Laboratory Medicine',
'Pharmacy', 'Surgery', 'Obstetrics and Gynecology', 'Dermatology',
'Ophthalmology', 'ENT', 'Anesthesiology', 'Pathology', 'Psychiatry'
]
# Saudi Medical License Formats
SAUDI_LICENSE_PREFIXES = ['MOH', 'SCFHS', 'SMLE', 'SFH']
def generate_saudi_phone():
"""Generate Saudi phone number"""
area_codes = ['11', '12', '13', '14', '16', '17'] # Major Saudi area codes
return f"+966-{random.choice(area_codes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
def generate_saudi_mobile():
"""Generate Saudi mobile number"""
mobile_prefixes = ['50', '53', '54', '55', '56', '57', '58', '59'] # Saudi mobile prefixes
return f"+966-{random.choice(mobile_prefixes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
def generate_saudi_license():
"""Generate Saudi medical license number"""
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
return f"{prefix}-{random.randint(100000, 999999)}"
def generate_saudi_employee_id(tenant_name, role):
"""Generate Saudi employee ID"""
tenant_code = ''.join([c for c in tenant_name.upper() if c.isalpha()])[:3]
role_code = role[:3].upper()
return f"{tenant_code}-{role_code}-{random.randint(1000, 9999)}"
def create_saudi_users(tenants, users_per_tenant=50):
"""Create Saudi healthcare users"""
users = []
role_distribution = {
'PHYSICIAN': 0.15,
'NURSE': 0.25,
'PHARMACIST': 0.08,
'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
for tenant in tenants:
tenant_users = []
for role, percentage in role_distribution.items():
user_count = max(1, int(users_per_tenant * percentage))
for i in range(user_count):
# Determine gender for Arabic naming
is_male = random.choice([True, False])
first_name = random.choice(SAUDI_FIRST_NAMES_MALE if is_male else SAUDI_FIRST_NAMES_FEMALE)
last_name = random.choice(SAUDI_FAMILY_NAMES)
middle_name = random.choice(SAUDI_MIDDLE_NAMES) if random.choice([True, False]) else None
# Generate username
username = f"{first_name.lower()}.{last_name.lower().replace('-', '').replace('al', '')}"
counter = 1
original_username = username
while User.objects.filter(username=username).exists():
username = f"{original_username}{counter}"
counter += 1
# Generate email
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
# Professional information
department = random.choice(SAUDI_DEPARTMENTS)
job_title = random.choice(SAUDI_JOB_TITLES.get(role, [f"{role.replace('_', ' ').title()}"]))
# License information for medical professionals
license_number = None
license_state = None
license_expiry = None
npi_number = None
if role in ['PHYSICIAN', 'NURSE', 'PHARMACIST', 'RADIOLOGIST']:
license_number = generate_saudi_license()
license_state = random.choice(SAUDI_PROVINCES)
license_expiry = django_timezone.now().date() + timedelta(days=random.randint(365, 1095))
if role == 'PHYSICIAN':
npi_number = f"SA{random.randint(1000000, 9999999)}"
user = User.objects.create(
username=username,
email=email,
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
preferred_name=first_name if random.choice([True, False]) else None,
tenant=tenant,
# Contact information
phone_number=generate_saudi_phone(),
mobile_number=generate_saudi_mobile(),
# Professional information
employee_id=generate_saudi_employee_id(tenant.name, role),
department=department,
job_title=job_title,
role=role,
# License information
license_number=license_number,
license_state=license_state,
license_expiry=license_expiry,
npi_number=npi_number,
# Security settings
force_password_change=random.choice([True, False]),
password_expires_at=django_timezone.now() + timedelta(days=random.randint(90, 365)),
failed_login_attempts=random.randint(0, 2),
two_factor_enabled=random.choice([True, False]) if role in ['PHYSICIAN', 'ADMIN',
'PHARMACIST'] else False,
# Session settings
max_concurrent_sessions=random.choice([1, 2, 3, 5]),
session_timeout_minutes=random.choice([30, 60, 120, 240]),
# Preferences
user_timezone='Asia/Riyadh',
language=random.choice(['ar', 'en', 'ar_SA']),
theme=random.choice(['LIGHT', 'DARK', 'AUTO']),
# Status
is_verified=True,
is_approved=True,
approval_date=django_timezone.now() - timedelta(days=random.randint(1, 180)),
is_active=True,
is_staff=role in ['ADMIN', 'SUPER_ADMIN'],
is_superuser=role == 'SUPER_ADMIN',
# Metadata
created_at=django_timezone.now() - timedelta(days=random.randint(1, 365)),
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)),
last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365))
)
# Set password
user.set_password('Hospital@123') # Default password
user.save()
users.append(user)
tenant_users.append(user)
# Set approval relationships
admin_users = [u for u in tenant_users if u.role in ['ADMIN', 'SUPER_ADMIN']]
if admin_users:
approver = random.choice(admin_users)
for user in tenant_users:
if user != approver and user.role != 'SUPER_ADMIN':
user.approved_by = approver
user.save()
print(f"Created {len(tenant_users)} users for {tenant.name}")
return users
def create_saudi_two_factor_devices(users):
"""Create two-factor authentication devices for Saudi users"""
devices = []
device_types = ['TOTP', 'SMS', 'EMAIL']
device_names = {
'TOTP': ['Google Authenticator', 'Microsoft Authenticator', 'Authy', 'LastPass Authenticator'],
'SMS': ['Primary Mobile', 'Work Mobile', 'Emergency Contact'],
'EMAIL': ['Work Email', 'Personal Email', 'Backup Email']
}
for user in users:
if user.two_factor_enabled:
# Create 1-3 devices per user
device_count = random.randint(1, 3)
for _ in range(device_count):
device_type = random.choice(device_types)
device_data = {
'user': user,
'device_id': uuid.uuid4(),
'name': random.choice(device_names[device_type]),
'device_type': device_type,
'is_active': True,
'is_verified': True,
'verified_at': django_timezone.now() - timedelta(days=random.randint(1, 30)),
'last_used_at': django_timezone.now() - timedelta(hours=random.randint(1, 168)),
'usage_count': random.randint(5, 100),
'created_at': django_timezone.now() - timedelta(days=random.randint(1, 60))
}
if device_type == 'TOTP':
device_data['secret_key'] = secrets.token_urlsafe(32)
elif device_type == 'SMS':
device_data['phone_number'] = user.mobile_number
elif device_type == 'EMAIL':
device_data['email_address'] = user.email
device = TwoFactorDevice.objects.create(**device_data)
devices.append(device)
print(f"Created {len(devices)} two-factor devices")
return devices
def create_saudi_social_accounts(users):
"""Create social authentication accounts for Saudi users"""
social_accounts = []
# Common providers in Saudi Arabia
providers = ['GOOGLE', 'MICROSOFT', 'APPLE', 'LINKEDIN']
for user in users:
# 30% chance of having social accounts
if random.choice([True, False, False, False]):
provider = random.choice(providers)
social_account = SocialAccount.objects.create(
user=user,
provider=provider,
provider_id=f"{provider.lower()}_{random.randint(100000000, 999999999)}",
provider_email=user.email,
display_name=user.get_full_name(),
profile_url=f"https://{provider.lower()}.com/profile/{user.username}",
avatar_url=f"https://{provider.lower()}.com/avatar/{user.username}.jpg",
access_token=secrets.token_urlsafe(64),
refresh_token=secrets.token_urlsafe(64),
token_expires_at=django_timezone.now() + timedelta(hours=1),
is_active=True,
created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
last_login_at=django_timezone.now() - timedelta(hours=random.randint(1, 48))
)
social_accounts.append(social_account)
print(f"Created {len(social_accounts)} social accounts")
return social_accounts
def create_saudi_user_sessions(users):
"""Create user sessions for Saudi healthcare users"""
sessions = []
saudi_ips = [
'37.99.', '37.200.', '31.9.', '31.173.', '188.161.',
'185.84.', '188.245.', '217.9.', '82.205.', '5.63.'
]
browsers = [
'Chrome 120.0.0.0', 'Safari 17.1.2', 'Firefox 121.0.0', 'Edge 120.0.0.0',
'Chrome Mobile 120.0.0.0', 'Safari Mobile 17.1.2'
]
operating_systems = [
'Windows 11', 'Windows 10', 'macOS 14.0', 'iOS 17.1.2',
'Android 14', 'Ubuntu 22.04'
]
device_types = ['DESKTOP', 'MOBILE', 'TABLET']
login_methods = ['PASSWORD', 'TWO_FACTOR', 'SOCIAL', 'SSO']
for user in users:
# Create 1-5 sessions per user
session_count = random.randint(1, 5)
for i in range(session_count):
ip_prefix = random.choice(saudi_ips)
ip_address = f"{ip_prefix}{random.randint(1, 255)}.{random.randint(1, 255)}"
session_start = django_timezone.now() - timedelta(hours=random.randint(1, 720))
is_active = i == 0 and random.choice([True, True, False]) # Most recent session likely active
session = UserSession.objects.create(
user=user,
session_key=f"session_{secrets.token_urlsafe(20)}",
session_id=uuid.uuid4(),
ip_address=ip_address,
user_agent=f"Mozilla/5.0 (compatible; HospitalSystem/1.0; {random.choice(browsers)})",
device_type=random.choice(device_types),
browser=random.choice(browsers),
operating_system=random.choice(operating_systems),
country='Saudi Arabia',
region=random.choice(SAUDI_PROVINCES),
city=random.choice(SAUDI_CITIES),
is_active=is_active,
login_method=random.choice(login_methods),
created_at=session_start,
last_activity_at=session_start + timedelta(minutes=random.randint(1, 480)),
expires_at=session_start + timedelta(hours=user.session_timeout_minutes // 60),
ended_at=None if is_active else session_start + timedelta(hours=random.randint(1, 8))
)
sessions.append(session)
print(f"Created {len(sessions)} user sessions")
return sessions
def create_saudi_password_history(users):
"""Create password history for Saudi users"""
password_history = []
passwords = ['Hospital@123', 'Medical@456', 'Health@789', 'Saudi@2024', 'Secure@Pass']
for user in users:
# Create 1-5 password history entries per user
history_count = random.randint(1, 5)
for i in range(history_count):
password = random.choice(passwords)
history_entry = PasswordHistory.objects.create(
user=user,
password_hash=make_password(password),
created_at=django_timezone.now() - timedelta(days=random.randint(30 * i, 30 * (i + 1)))
)
password_history.append(history_entry)
print(f"Created {len(password_history)} password history entries")
return password_history
def main():
"""Main function to generate all Saudi accounts data"""
print("Starting Saudi Healthcare Accounts Data Generation...")
# Get existing tenants
tenants = list(Tenant.objects.all())
if not tenants:
print("❌ No tenants found. Please run the core data generator first.")
return
# Create users
print("\n1. Creating Saudi Healthcare Users...")
users = create_saudi_users(tenants, 40) # 40 users per tenant
# Create two-factor devices
print("\n2. Creating Two-Factor Authentication Devices...")
devices = create_saudi_two_factor_devices(users)
# Create social accounts
print("\n3. Creating Social Authentication Accounts...")
social_accounts = create_saudi_social_accounts(users)
# Create user sessions
print("\n4. Creating User Sessions...")
sessions = create_saudi_user_sessions(users)
# Create password history
print("\n5. Creating Password History...")
password_history = create_saudi_password_history(users)
print(f"\n✅ Saudi Healthcare Accounts Data Generation Complete!")
print(f"📊 Summary:")
print(f" - Users: {len(users)}")
print(f" - Two-Factor Devices: {len(devices)}")
print(f" - Social Accounts: {len(social_accounts)}")
print(f" - User Sessions: {len(sessions)}")
print(f" - Password History Entries: {len(password_history)}")
# Role distribution summary
role_counts = {}
for user in users:
role_counts[user.role] = role_counts.get(user.role, 0) + 1
print(f"\n👥 User Role Distribution:")
for role, count in sorted(role_counts.items()):
print(f" - {role.replace('_', ' ').title()}: {count}")
return {
'users': users,
'devices': devices,
'social_accounts': social_accounts,
'sessions': sessions,
'password_history': password_history
}
if __name__ == "__main__":
main()

0
analytics/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

665
analytics/admin.py Normal file
View File

@ -0,0 +1,665 @@
"""
Analytics app admin configuration.
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from decimal import Decimal
from datetime import datetime, timedelta
from .models import (
Dashboard, DashboardWidget, DataSource, Report,
ReportExecution, MetricDefinition, MetricValue
)
class DashboardWidgetInline(admin.TabularInline):
"""
Inline admin for dashboard widgets.
"""
model = DashboardWidget
extra = 0
fields = [
'name', 'widget_type', 'chart_type', 'data_source',
'position_x', 'position_y', 'width', 'height', 'is_active'
]
readonly_fields = ['widget_id']
@admin.register(Dashboard)
class DashboardAdmin(admin.ModelAdmin):
"""
Admin interface for dashboards.
"""
list_display = [
'name', 'dashboard_type',
'is_public', 'is_default', 'is_active',
'refresh_interval_display', 'created_at'
]
list_filter = [
'tenant', 'dashboard_type', 'is_public', 'is_default', 'is_active'
]
search_fields = [
'name', 'description'
]
readonly_fields = [
'dashboard_id',
'created_at', 'updated_at'
]
fieldsets = [
('Dashboard Information', {
'fields': [
'dashboard_id', 'tenant', 'name', 'description'
]
}),
('Dashboard Type', {
'fields': [
'dashboard_type'
]
}),
('Layout Configuration', {
'fields': [
'layout_config', 'refresh_interval'
]
}),
('Access Control', {
'fields': [
'is_public', 'allowed_users', 'allowed_roles'
]
}),
('Dashboard Status', {
'fields': [
'is_active', 'is_default'
]
}),
# ('Summary Information', {
# 'fields': [
# 'widget_count'
# ],
# 'classes': ['collapse']
# }),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [DashboardWidgetInline]
filter_horizontal = ['allowed_users']
# def widget_count_display(self, obj):
# """Display widget count."""
# return obj.value_count
# widget_count_display.short_description = 'Widgets'
def refresh_interval_display(self, obj):
"""Display refresh interval."""
if obj.refresh_interval >= 3600:
hours = obj.refresh_interval // 3600
return f"{hours}h"
elif obj.refresh_interval >= 60:
minutes = obj.refresh_interval // 60
return f"{minutes}m"
return f"{obj.refresh_interval}s"
refresh_interval_display.short_description = 'Refresh'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.prefetch_related('widgets')
@admin.register(DashboardWidget)
class DashboardWidgetAdmin(admin.ModelAdmin):
"""
Admin interface for dashboard widgets.
"""
list_display = [
'name', 'dashboard_name', 'widget_type', 'chart_type',
'data_source', 'position_display', 'size_display',
'auto_refresh', 'is_active'
]
list_filter = [
'dashboard__tenant', 'widget_type', 'chart_type',
'auto_refresh', 'is_active'
]
search_fields = [
'name', 'description', 'dashboard__name'
]
readonly_fields = [
'widget_id', 'created_at', 'updated_at'
]
fieldsets = [
('Widget Information', {
'fields': [
'widget_id', 'dashboard', 'name', 'description'
]
}),
('Widget Type', {
'fields': [
'widget_type', 'chart_type'
]
}),
('Data Source', {
'fields': [
'data_source', 'query_config'
]
}),
('Layout', {
'fields': [
'position_x', 'position_y', 'width', 'height'
]
}),
('Display Configuration', {
'fields': [
'display_config', 'color_scheme'
]
}),
('Refresh Settings', {
'fields': [
'auto_refresh', 'refresh_interval'
]
}),
('Widget Status', {
'fields': [
'is_active'
]
}),
('Metadata', {
'fields': [
'created_at', 'updated_at'
],
'classes': ['collapse']
})
]
def dashboard_name(self, obj):
"""Display dashboard name."""
return obj.dashboard.name
dashboard_name.short_description = 'Dashboard'
def position_display(self, obj):
"""Display position."""
return f"({obj.position_x}, {obj.position_y})"
position_display.short_description = 'Position'
def size_display(self, obj):
"""Display size."""
return f"{obj.width}×{obj.height}"
size_display.short_description = 'Size'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(dashboard__tenant=request.user.tenant)
return qs.select_related('dashboard', 'data_source')
@admin.register(DataSource)
class DataSourceAdmin(admin.ModelAdmin):
"""
Admin interface for data sources.
"""
list_display = [
'name', 'source_type', 'connection_type',
'health_status_display', 'last_health_check',
'cache_duration_display', 'is_active'
]
list_filter = [
'tenant', 'source_type', 'connection_type',
'is_healthy', 'is_active'
]
search_fields = [
'name', 'description'
]
readonly_fields = [
'source_id', 'is_healthy', 'last_health_check',
'created_at', 'updated_at'
]
fieldsets = [
('Data Source Information', {
'fields': [
'source_id', 'tenant', 'name', 'description'
]
}),
('Source Configuration', {
'fields': [
'source_type', 'connection_type'
]
}),
('Connection Details', {
'fields': [
'connection_config', 'authentication_config'
]
}),
('Query Configuration', {
'fields': [
'query_template', 'parameters'
]
}),
('Data Processing', {
'fields': [
'data_transformation', 'cache_duration'
]
}),
('Health Monitoring', {
'fields': [
'is_healthy', 'last_health_check', 'health_check_interval'
]
}),
('Source Status', {
'fields': [
'is_active'
]
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
def health_status_display(self, obj):
"""Display health status with color coding."""
if obj.is_healthy:
return format_html('<span style="color: green;">✓ Healthy</span>')
return format_html('<span style="color: red;">✗ Unhealthy</span>')
health_status_display.short_description = 'Health'
def cache_duration_display(self, obj):
"""Display cache duration."""
if obj.cache_duration >= 3600:
hours = obj.cache_duration // 3600
return f"{hours}h"
elif obj.cache_duration >= 60:
minutes = obj.cache_duration // 60
return f"{minutes}m"
return f"{obj.cache_duration}s"
cache_duration_display.short_description = 'Cache'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs
class ReportExecutionInline(admin.TabularInline):
"""
Inline admin for report executions.
"""
model = ReportExecution
extra = 0
fields = [
'execution_type', 'started_at', 'status',
'duration_seconds', 'record_count'
]
readonly_fields = [
'execution_id', 'started_at', 'completed_at',
'duration_seconds', 'record_count'
]
@admin.register(Report)
class ReportAdmin(admin.ModelAdmin):
"""
Admin interface for reports.
"""
list_display = [
'name', 'report_type', 'output_format',
'schedule_type', 'next_execution',
'execution_count_display', 'is_active'
]
list_filter = [
'tenant', 'report_type', 'output_format',
'schedule_type', 'is_active'
]
search_fields = [
'name', 'description'
]
readonly_fields = [
'report_id',
'created_at', 'updated_at'
]
fieldsets = [
('Report Information', {
'fields': [
'report_id', 'tenant', 'name', 'description'
]
}),
('Report Configuration', {
'fields': [
'report_type', 'data_source', 'query_config'
]
}),
('Output Configuration', {
'fields': [
'output_format', 'template_config'
]
}),
('Scheduling', {
'fields': [
'schedule_type', 'schedule_config', 'next_execution'
]
}),
('Distribution', {
'fields': [
'recipients', 'distribution_config'
]
}),
('Report Status', {
'fields': [
'is_active'
]
}),
('Summary Information', {
'fields': [
'execution_count'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [ReportExecutionInline]
def execution_count_display(self, obj):
"""Display execution count."""
return obj.execution_count
execution_count_display.short_description = 'Executions'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('data_source')
@admin.register(ReportExecution)
class ReportExecutionAdmin(admin.ModelAdmin):
"""
Admin interface for report executions.
"""
list_display = [
'report_name', 'execution_type', 'started_at',
'status', 'duration_display', 'record_count',
'output_size_display'
]
list_filter = [
'report__tenant', 'execution_type', 'status', 'started_at'
]
search_fields = [
'report__name', 'executed_by__username'
]
readonly_fields = [
'execution_id', 'started_at', 'completed_at',
'duration_seconds', 'output_size_bytes',
'record_count'
]
fieldsets = [
('Execution Information', {
'fields': [
'execution_id', 'report', 'execution_type'
]
}),
('Timing', {
'fields': [
'started_at', 'completed_at', 'duration_seconds'
]
}),
('Status and Results', {
'fields': [
'status', 'error_message'
]
}),
('Output', {
'fields': [
'output_file_path', 'output_size_bytes', 'record_count'
]
}),
('Parameters', {
'fields': [
'execution_parameters'
]
}),
('Metadata', {
'fields': [
'executed_by'
],
'classes': ['collapse']
})
]
date_hierarchy = 'started_at'
def report_name(self, obj):
"""Display report name."""
return obj.report.name
report_name.short_description = 'Report'
def duration_display(self, obj):
"""Display duration."""
if obj.duration_seconds:
if obj.duration_seconds >= 3600:
hours = obj.duration_seconds // 3600
minutes = (obj.duration_seconds % 3600) // 60
return f"{hours}h {minutes}m"
elif obj.duration_seconds >= 60:
minutes = obj.duration_seconds // 60
seconds = obj.duration_seconds % 60
return f"{minutes}m {seconds}s"
return f"{obj.duration_seconds}s"
return "-"
duration_display.short_description = 'Duration'
def output_size_display(self, obj):
"""Display output size."""
if obj.output_size_bytes:
if obj.output_size_bytes >= 1024 * 1024:
mb = obj.output_size_bytes / (1024 * 1024)
return f"{mb:.1f} MB"
elif obj.output_size_bytes >= 1024:
kb = obj.output_size_bytes / 1024
return f"{kb:.1f} KB"
return f"{obj.output_size_bytes} B"
return "-"
output_size_display.short_description = 'Size'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(report__tenant=request.user.tenant)
return qs.select_related('report', 'executed_by')
class MetricValueInline(admin.TabularInline):
"""
Inline admin for metric values.
"""
model = MetricValue
extra = 0
fields = [
'value', 'period_start', 'period_end',
'data_quality_score', 'confidence_level'
]
readonly_fields = [
'value_id', 'calculation_timestamp',
'calculation_duration_ms'
]
@admin.register(MetricDefinition)
class MetricDefinitionAdmin(admin.ModelAdmin):
"""
Admin interface for metric definitions.
"""
list_display = [
'name', 'metric_type', 'aggregation_period',
'target_value', 'unit_of_measure',
'value_count_display', 'is_active'
]
list_filter = [
'tenant', 'metric_type', 'aggregation_period', 'is_active'
]
search_fields = [
'name', 'description'
]
readonly_fields = [
'metric_id',
'created_at', 'updated_at'
]
fieldsets = [
('Metric Information', {
'fields': [
'metric_id', 'tenant', 'name', 'description'
]
}),
('Metric Configuration', {
'fields': [
'metric_type', 'data_source', 'calculation_config'
]
}),
('Aggregation', {
'fields': [
'aggregation_period', 'aggregation_config'
]
}),
('Thresholds and Targets', {
'fields': [
'target_value', 'warning_threshold', 'critical_threshold'
]
}),
('Display Configuration', {
'fields': [
'unit_of_measure', 'decimal_places', 'display_format'
]
}),
('Metric Status', {
'fields': [
'is_active'
]
}),
('Summary Information', {
'fields': [
'value_count'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [MetricValueInline]
def value_count_display(self, obj):
"""Display value count."""
return obj.value_count
value_count_display.short_description = 'Values'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('data_source')
@admin.register(MetricValue)
class MetricValueAdmin(admin.ModelAdmin):
"""
Admin interface for metric values.
"""
list_display = [
'metric_name', 'value', 'period_start',
'period_end', 'threshold_status_display',
'data_quality_score', 'calculation_timestamp'
]
list_filter = [
'metric_definition__tenant', 'metric_definition__metric_type',
'period_start', 'calculation_timestamp'
]
search_fields = [
'metric_definition__name'
]
readonly_fields = [
'value_id', 'is_above_target', 'threshold_status',
'calculation_timestamp', 'calculation_duration_ms'
]
fieldsets = [
('Metric Value Information', {
'fields': [
'value_id', 'metric_definition'
]
}),
('Value Details', {
'fields': [
'value', 'period_start', 'period_end'
]
}),
('Context', {
'fields': [
'dimensions', 'metadata'
]
}),
('Quality Indicators', {
'fields': [
'data_quality_score', 'confidence_level'
]
}),
('Calculation Details', {
'fields': [
'calculation_timestamp', 'calculation_duration_ms'
]
}),
('Analysis', {
'fields': [
'is_above_target', 'threshold_status'
],
'classes': ['collapse']
})
]
date_hierarchy = 'period_start'
def metric_name(self, obj):
"""Display metric name."""
return obj.metric_definition.name
metric_name.short_description = 'Metric'
def threshold_status_display(self, obj):
"""Display threshold status with color coding."""
status = obj.threshold_status
if status == 'CRITICAL':
return format_html('<span style="color: red;">🔴 Critical</span>')
elif status == 'WARNING':
return format_html('<span style="color: orange;">🟡 Warning</span>')
return format_html('<span style="color: green;">🟢 Normal</span>')
threshold_status_display.short_description = 'Status'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(metric_definition__tenant=request.user.tenant)
return qs.select_related('metric_definition')
# Customize admin site
admin.site.site_header = "Hospital Management System - Analytics"
admin.site.site_title = "Analytics Admin"
admin.site.index_title = "Analytics Administration"

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,235 @@
from rest_framework import serializers
from ..models import (
Dashboard, DashboardWidget, DataSource, Report,
ReportExecution, MetricDefinition, MetricValue
)
class BaseSerializer(serializers.ModelSerializer):
"""Base serializer with common functionality"""
class Meta:
fields = ['id']
read_only_fields = ['created_at', 'updated_at']
class DataSourceSerializer(serializers.ModelSerializer):
"""Serializer for DataSource model"""
source_type_display = serializers.CharField(source='get_source_type_display', read_only=True)
class Meta:
model = DataSource
fields = [
'source_id', 'name', 'description', 'source_type', 'source_type_display',
'connection_string', 'query', 'refresh_interval', 'is_active',
'last_updated', 'created_at', 'updated_at'
]
read_only_fields = ['source_id', 'last_updated', 'created_at', 'updated_at']
class MetricDefinitionSerializer(serializers.ModelSerializer):
"""Serializer for MetricDefinition model"""
metric_type_display = serializers.CharField(source='get_metric_type_display', read_only=True)
data_source_name = serializers.CharField(source='data_source.name', read_only=True)
class Meta:
model = MetricDefinition
fields = [
'metric_id', 'name', 'description', 'metric_type', 'metric_type_display',
'data_source', 'data_source_name', 'calculation_formula',
'target_value', 'threshold_warning', 'threshold_critical',
'unit', 'is_active', 'created_at', 'updated_at'
]
read_only_fields = ['metric_id', 'created_at', 'updated_at']
class MetricValueSerializer(serializers.ModelSerializer):
"""Serializer for MetricValue model"""
metric_name = serializers.CharField(source='metric.name', read_only=True)
metric_unit = serializers.CharField(source='metric.unit', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = MetricValue
fields = [
'value_id', 'metric', 'metric_name', 'metric_unit', 'value',
'timestamp', 'status', 'status_display', 'notes',
'created_at', 'updated_at'
]
read_only_fields = ['value_id', 'created_at', 'updated_at']
class DashboardWidgetSerializer(serializers.ModelSerializer):
"""Serializer for DashboardWidget model"""
widget_type_display = serializers.CharField(source='get_widget_type_display', read_only=True)
data_source_name = serializers.CharField(source='data_source.name', read_only=True)
class Meta:
model = DashboardWidget
fields = [
'widget_id', 'title', 'description', 'widget_type', 'widget_type_display',
'data_source', 'data_source_name', 'configuration', 'position_x',
'position_y', 'width', 'height', 'refresh_interval', 'is_visible',
'created_at', 'updated_at'
]
read_only_fields = ['widget_id', 'created_at', 'updated_at']
class DashboardSerializer(serializers.ModelSerializer):
"""Serializer for Dashboard model"""
created_by_name = serializers.CharField(source='created_by.get_full_name', read_only=True)
widgets = DashboardWidgetSerializer(many=True, read_only=True)
widget_count = serializers.IntegerField(read_only=True)
class Meta:
model = Dashboard
fields = [
'dashboard_id', 'name', 'description', 'layout', 'is_public',
'is_default', 'created_by', 'created_by_name', 'widgets',
'widget_count', 'created_at', 'updated_at'
]
read_only_fields = ['dashboard_id', 'widget_count', 'created_at', 'updated_at']
class ReportSerializer(serializers.ModelSerializer):
"""Serializer for Report model"""
report_type_display = serializers.CharField(source='get_report_type_display', read_only=True)
created_by_name = serializers.CharField(source='created_by.get_full_name', read_only=True)
data_source_name = serializers.CharField(source='data_source.name', read_only=True)
class Meta:
model = Report
fields = [
'report_id', 'name', 'description', 'report_type', 'report_type_display',
'data_source', 'data_source_name', 'query', 'parameters',
'schedule', 'is_active', 'created_by', 'created_by_name',
'created_at', 'updated_at'
]
read_only_fields = ['report_id', 'created_at', 'updated_at']
class ReportExecutionSerializer(serializers.ModelSerializer):
"""Serializer for ReportExecution model"""
report_name = serializers.CharField(source='report.name', read_only=True)
executed_by_name = serializers.CharField(source='executed_by.get_full_name', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
duration_seconds = serializers.IntegerField(read_only=True)
class Meta:
model = ReportExecution
fields = [
'execution_id', 'report', 'report_name', 'executed_by', 'executed_by_name',
'start_time', 'end_time', 'duration_seconds', 'status', 'status_display',
'parameters', 'result_count', 'file_path', 'error_message',
'created_at', 'updated_at'
]
read_only_fields = [
'execution_id', 'duration_seconds', 'created_at', 'updated_at'
]
class AnalyticsStatsSerializer(serializers.Serializer):
"""Serializer for analytics statistics"""
total_dashboards = serializers.IntegerField()
total_reports = serializers.IntegerField()
total_metrics = serializers.IntegerField()
active_data_sources = serializers.IntegerField()
reports_executed_today = serializers.IntegerField()
failed_executions = serializers.IntegerField()
avg_execution_time = serializers.FloatField()
popular_reports = serializers.ListField()
metric_status_breakdown = serializers.DictField()
class DashboardCreateSerializer(serializers.Serializer):
"""Serializer for creating dashboards"""
name = serializers.CharField()
description = serializers.CharField(required=False, allow_blank=True)
layout = serializers.JSONField(required=False)
is_public = serializers.BooleanField(default=False)
widgets = serializers.ListField(
child=serializers.DictField(),
required=False
)
def validate_widgets(self, value):
"""Validate dashboard widgets"""
for widget in value:
required_fields = ['title', 'widget_type', 'data_source_id']
for field in required_fields:
if field not in widget:
raise serializers.ValidationError(f"Widget missing required field: {field}")
return value
class ReportExecuteSerializer(serializers.Serializer):
"""Serializer for executing reports"""
report_id = serializers.IntegerField()
parameters = serializers.JSONField(required=False)
format = serializers.ChoiceField(
choices=['JSON', 'CSV', 'PDF', 'EXCEL'],
default='JSON'
)
def validate_report_id(self, value):
"""Validate that the report exists and is active"""
try:
report = Report.objects.get(id=value)
if not report.is_active:
raise serializers.ValidationError("Report is not active.")
return value
except Report.DoesNotExist:
raise serializers.ValidationError("Invalid report ID.")
class MetricCalculateSerializer(serializers.Serializer):
"""Serializer for calculating metrics"""
metric_id = serializers.IntegerField()
start_date = serializers.DateTimeField(required=False)
end_date = serializers.DateTimeField(required=False)
def validate_metric_id(self, value):
"""Validate that the metric exists and is active"""
try:
metric = MetricDefinition.objects.get(id=value)
if not metric.is_active:
raise serializers.ValidationError("Metric is not active.")
return value
except MetricDefinition.DoesNotExist:
raise serializers.ValidationError("Invalid metric ID.")
def validate(self, data):
"""Validate date range"""
if data.get('start_date') and data.get('end_date'):
if data['end_date'] <= data['start_date']:
raise serializers.ValidationError("End date must be after start date.")
return data
class DataSourceTestSerializer(serializers.Serializer):
"""Serializer for testing data sources"""
source_id = serializers.IntegerField()
test_query = serializers.CharField(required=False, allow_blank=True)
def validate_source_id(self, value):
"""Validate that the data source exists"""
try:
DataSource.objects.get(id=value)
return value
except DataSource.DoesNotExist:
raise serializers.ValidationError("Invalid data source ID.")
class WidgetDataSerializer(serializers.Serializer):
"""Serializer for widget data"""
widget_id = serializers.IntegerField()
refresh = serializers.BooleanField(default=False)
def validate_widget_id(self, value):
"""Validate that the widget exists"""
try:
DashboardWidget.objects.get(id=value)
return value
except DashboardWidget.DoesNotExist:
raise serializers.ValidationError("Invalid widget ID.")

11
analytics/api/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'dashboards', views.DashboardViewSet)
urlpatterns = [
path('', include(router.urls)),
]

525
analytics/api/views.py Normal file
View File

@ -0,0 +1,525 @@
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from django.db.models import Q, Count, Avg, Sum
from django.utils import timezone
from datetime import timedelta
import json
from ..models import (
Dashboard, DashboardWidget, DataSource, Report,
ReportExecution, MetricDefinition, MetricValue
)
from .serializers import (
DashboardSerializer, DashboardWidgetSerializer, DataSourceSerializer,
ReportSerializer, ReportExecutionSerializer, MetricDefinitionSerializer,
MetricValueSerializer, AnalyticsStatsSerializer, DashboardCreateSerializer,
ReportExecuteSerializer, MetricCalculateSerializer, DataSourceTestSerializer,
WidgetDataSerializer
)
from core.utils import AuditLogger
class BaseViewSet(viewsets.ModelViewSet):
"""Base ViewSet with common functionality"""
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
def get_queryset(self):
# Filter by tenant if user has one
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
return self.queryset.filter(tenant=self.request.user.tenant)
return self.queryset
def perform_create(self, serializer):
if hasattr(self.request.user, 'tenant'):
serializer.save(tenant=self.request.user.tenant)
else:
serializer.save()
class DataSourceViewSet(BaseViewSet):
"""ViewSet for DataSource model"""
queryset = DataSource.objects.all()
serializer_class = DataSourceSerializer
filterset_fields = ['source_type', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'last_updated']
ordering = ['name']
@action(detail=False, methods=['get'])
def active(self, request):
"""Get active data sources"""
queryset = self.get_queryset().filter(is_active=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def test_connection(self, request, pk=None):
"""Test data source connection"""
data_source = self.get_object()
serializer = DataSourceTestSerializer(data={'source_id': pk})
if serializer.is_valid():
try:
# Mock connection test - in real implementation, would test actual connection
test_result = {
'success': True,
'message': 'Connection successful',
'response_time': 0.15,
'records_available': 1000
}
# Update last_updated timestamp
data_source.last_updated = timezone.now()
data_source.save()
# Log the action
AuditLogger.log_action(
user=request.user,
action='DATA_SOURCE_TESTED',
model='DataSource',
object_id=str(data_source.source_id),
details={
'source_name': data_source.name,
'test_result': 'SUCCESS'
}
)
return Response(test_result)
except Exception as e:
return Response({
'success': False,
'message': f'Connection failed: {str(e)}',
'error': str(e)
}, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class MetricDefinitionViewSet(BaseViewSet):
"""ViewSet for MetricDefinition model"""
queryset = MetricDefinition.objects.all()
serializer_class = MetricDefinitionSerializer
filterset_fields = ['metric_type', 'data_source', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'metric_type']
ordering = ['name']
@action(detail=False, methods=['get'])
def active(self, request):
"""Get active metrics"""
queryset = self.get_queryset().filter(is_active=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def calculate(self, request, pk=None):
"""Calculate metric value"""
metric = self.get_object()
serializer = MetricCalculateSerializer(data=request.data)
if serializer.is_valid():
try:
# Mock calculation - in real implementation, would execute actual calculation
calculated_value = 85.5 # Mock value
# Determine status based on thresholds
status_value = 'NORMAL'
if metric.threshold_critical and calculated_value >= metric.threshold_critical:
status_value = 'CRITICAL'
elif metric.threshold_warning and calculated_value >= metric.threshold_warning:
status_value = 'WARNING'
# Create metric value record
metric_value = MetricValue.objects.create(
metric=metric,
value=calculated_value,
timestamp=timezone.now(),
status=status_value,
notes=f"Calculated via API by {request.user.get_full_name()}",
tenant=getattr(request.user, 'tenant', None)
)
# Log the action
AuditLogger.log_action(
user=request.user,
action='METRIC_CALCULATED',
model='MetricValue',
object_id=str(metric_value.value_id),
details={
'metric_name': metric.name,
'calculated_value': calculated_value,
'status': status_value
}
)
return Response({
'message': 'Metric calculated successfully',
'metric_value': MetricValueSerializer(metric_value).data
})
except Exception as e:
return Response({
'error': f'Calculation failed: {str(e)}'
}, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class MetricValueViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for MetricValue model (read-only)"""
queryset = MetricValue.objects.all()
serializer_class = MetricValueSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['metric', 'status']
search_fields = ['metric__name', 'notes']
ordering_fields = ['timestamp', 'value']
ordering = ['-timestamp']
def get_queryset(self):
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
return self.queryset.filter(tenant=self.request.user.tenant)
return self.queryset
@action(detail=False, methods=['get'])
def latest(self, request):
"""Get latest metric values"""
queryset = self.get_queryset().order_by('metric', '-timestamp').distinct('metric')
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class DashboardViewSet(BaseViewSet):
"""ViewSet for Dashboard model"""
queryset = Dashboard.objects.all()
serializer_class = DashboardSerializer
filterset_fields = ['is_public', 'is_default', 'created_by']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
@action(detail=False, methods=['post'])
def create_dashboard(self, request):
"""Create a dashboard with widgets"""
serializer = DashboardCreateSerializer(data=request.data)
if serializer.is_valid():
# Create dashboard
dashboard = Dashboard.objects.create(
name=serializer.validated_data['name'],
description=serializer.validated_data.get('description', ''),
layout=serializer.validated_data.get('layout', {}),
is_public=serializer.validated_data.get('is_public', False),
created_by=request.user,
tenant=getattr(request.user, 'tenant', None)
)
# Create widgets if provided
widgets_data = serializer.validated_data.get('widgets', [])
for widget_data in widgets_data:
data_source = DataSource.objects.get(id=widget_data['data_source_id'])
DashboardWidget.objects.create(
dashboard=dashboard,
title=widget_data['title'],
description=widget_data.get('description', ''),
widget_type=widget_data['widget_type'],
data_source=data_source,
configuration=widget_data.get('configuration', {}),
position_x=widget_data.get('position_x', 0),
position_y=widget_data.get('position_y', 0),
width=widget_data.get('width', 4),
height=widget_data.get('height', 3),
tenant=getattr(request.user, 'tenant', None)
)
# Log the action
AuditLogger.log_action(
user=request.user,
action='DASHBOARD_CREATED',
model='Dashboard',
object_id=str(dashboard.dashboard_id),
details={
'dashboard_name': dashboard.name,
'widgets_count': len(widgets_data)
}
)
return Response({
'message': 'Dashboard created successfully',
'dashboard': DashboardSerializer(dashboard).data
})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['get'])
def public(self, request):
"""Get public dashboards"""
queryset = self.get_queryset().filter(is_public=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def my_dashboards(self, request):
"""Get user's dashboards"""
queryset = self.get_queryset().filter(created_by=request.user)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class DashboardWidgetViewSet(BaseViewSet):
"""ViewSet for DashboardWidget model"""
queryset = DashboardWidget.objects.all()
serializer_class = DashboardWidgetSerializer
filterset_fields = ['dashboard', 'widget_type', 'data_source', 'is_visible']
search_fields = ['title', 'description']
ordering_fields = ['title', 'position_x', 'position_y']
ordering = ['position_y', 'position_x']
@action(detail=True, methods=['get'])
def data(self, request, pk=None):
"""Get widget data"""
widget = self.get_object()
serializer = WidgetDataSerializer(data={'widget_id': pk})
if serializer.is_valid():
try:
# Mock data generation - in real implementation, would query actual data source
mock_data = {
'CHART': {
'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
'datasets': [{
'label': 'Sample Data',
'data': [65, 59, 80, 81, 56]
}]
},
'TABLE': {
'headers': ['Name', 'Value', 'Status'],
'rows': [
['Metric 1', '85.5', 'Normal'],
['Metric 2', '92.1', 'Warning'],
['Metric 3', '78.3', 'Normal']
]
},
'KPI': {
'value': 85.5,
'target': 90.0,
'trend': 'up',
'change': '+2.3%'
},
'GAUGE': {
'value': 75,
'min': 0,
'max': 100,
'thresholds': [50, 80]
}
}
widget_data = mock_data.get(widget.widget_type, {})
return Response({
'widget_id': widget.widget_id,
'title': widget.title,
'widget_type': widget.widget_type,
'data': widget_data,
'last_updated': timezone.now()
})
except Exception as e:
return Response({
'error': f'Failed to load widget data: {str(e)}'
}, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ReportViewSet(BaseViewSet):
"""ViewSet for Report model"""
queryset = Report.objects.all()
serializer_class = ReportSerializer
filterset_fields = ['report_type', 'data_source', 'is_active', 'created_by']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
@action(detail=False, methods=['get'])
def active(self, request):
"""Get active reports"""
queryset = self.get_queryset().filter(is_active=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def execute(self, request, pk=None):
"""Execute a report"""
report = self.get_object()
serializer = ReportExecuteSerializer(data=request.data)
if serializer.is_valid():
try:
# Create execution record
execution = ReportExecution.objects.create(
report=report,
executed_by=request.user,
start_time=timezone.now(),
status='RUNNING',
parameters=serializer.validated_data.get('parameters', {}),
tenant=getattr(request.user, 'tenant', None)
)
# Mock execution - in real implementation, would execute actual report
import time
time.sleep(0.1) # Simulate processing time
# Update execution with results
execution.end_time = timezone.now()
execution.status = 'COMPLETED'
execution.result_count = 150 # Mock result count
execution.file_path = f'/reports/{execution.execution_id}.json'
execution.save()
# Log the action
AuditLogger.log_action(
user=request.user,
action='REPORT_EXECUTED',
model='ReportExecution',
object_id=str(execution.execution_id),
details={
'report_name': report.name,
'result_count': execution.result_count
}
)
return Response({
'message': 'Report executed successfully',
'execution': ReportExecutionSerializer(execution).data
})
except Exception as e:
# Update execution with error
if 'execution' in locals():
execution.end_time = timezone.now()
execution.status = 'FAILED'
execution.error_message = str(e)
execution.save()
return Response({
'error': f'Report execution failed: {str(e)}'
}, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ReportExecutionViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for ReportExecution model (read-only)"""
queryset = ReportExecution.objects.all()
serializer_class = ReportExecutionSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['report', 'executed_by', 'status']
search_fields = ['report__name', 'executed_by__first_name', 'executed_by__last_name']
ordering_fields = ['start_time', 'end_time']
ordering = ['-start_time']
def get_queryset(self):
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
return self.queryset.filter(tenant=self.request.user.tenant)
return self.queryset
@action(detail=False, methods=['get'])
def recent(self, request):
"""Get recent executions"""
queryset = self.get_queryset().order_by('-start_time')[:20]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class AnalyticsStatsViewSet(viewsets.ViewSet):
"""ViewSet for analytics statistics"""
permission_classes = [permissions.IsAuthenticated]
@action(detail=False, methods=['get'])
def dashboard(self, request):
"""Get analytics dashboard statistics"""
tenant_filter = {}
if hasattr(request.user, 'tenant') and request.user.tenant:
tenant_filter['tenant'] = request.user.tenant
today = timezone.now().date()
# Dashboard statistics
dashboards = Dashboard.objects.filter(**tenant_filter)
total_dashboards = dashboards.count()
# Report statistics
reports = Report.objects.filter(**tenant_filter)
total_reports = reports.count()
# Metric statistics
metrics = MetricDefinition.objects.filter(**tenant_filter)
total_metrics = metrics.count()
# Data source statistics
data_sources = DataSource.objects.filter(**tenant_filter)
active_data_sources = data_sources.filter(is_active=True).count()
# Execution statistics
executions = ReportExecution.objects.filter(**tenant_filter)
reports_executed_today = executions.filter(start_time__date=today).count()
failed_executions = executions.filter(
start_time__date=today,
status='FAILED'
).count()
# Average execution time
completed_executions = executions.filter(
status='COMPLETED',
start_time__date=today
)
avg_execution_time = 0
if completed_executions.exists():
total_time = sum([
(exec.end_time - exec.start_time).total_seconds()
for exec in completed_executions
if exec.end_time
])
avg_execution_time = total_time / completed_executions.count()
# Popular reports (top 5)
popular_reports = list(
executions.filter(start_time__gte=today - timedelta(days=30))
.values('report__name')
.annotate(execution_count=Count('id'))
.order_by('-execution_count')[:5]
)
# Metric status breakdown
metric_values = MetricValue.objects.filter(**tenant_filter)
latest_values = metric_values.order_by('metric', '-timestamp').distinct('metric')
metric_status_breakdown = latest_values.values('status').annotate(
count=Count('id')
).order_by('-count')
stats = {
'total_dashboards': total_dashboards,
'total_reports': total_reports,
'total_metrics': total_metrics,
'active_data_sources': active_data_sources,
'reports_executed_today': reports_executed_today,
'failed_executions': failed_executions,
'avg_execution_time': round(avg_execution_time, 2),
'popular_reports': popular_reports,
'metric_status_breakdown': {
item['status']: item['count']
for item in metric_status_breakdown
}
}
serializer = AnalyticsStatsSerializer(stats)
return Response(serializer.data)

6
analytics/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AnalyticsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'analytics'

398
analytics/forms.py Normal file
View File

@ -0,0 +1,398 @@
"""
Analytics app forms for CRUD operations.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import datetime, date
from .models import (
Dashboard, DashboardWidget, DataSource, Report,
MetricDefinition
)
class DashboardForm(forms.ModelForm):
"""
Form for creating and updating dashboards.
"""
class Meta:
model = Dashboard
fields = [
'name', 'description', 'dashboard_type', 'is_public',
'layout_config', 'refresh_interval', 'is_active'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter dashboard name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter dashboard description'
}),
'dashboard_type': forms.Select(attrs={'class': 'form-control'}),
'is_public': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'layout_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': 'Enter JSON layout configuration'
}),
'refresh_interval': forms.NumberInput(attrs={
'class': 'form-control',
'min': 30,
'max': 3600,
'step': 30
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
help_texts = {
'name': 'Unique name for this dashboard',
'dashboard_type': 'Dashboard type for organization',
'is_public': 'Whether this dashboard is publicly accessible',
'layout_config': 'JSON configuration for dashboard layout',
'refresh_interval': 'Auto-refresh interval in seconds (30-3600)',
}
def clean_name(self):
name = self.cleaned_data['name']
if len(name) < 3:
raise ValidationError('Dashboard name must be at least 3 characters long.')
return name
def clean_refresh_interval(self):
interval = self.cleaned_data['refresh_interval']
if interval and (interval < 30 or interval > 3600):
raise ValidationError('Refresh interval must be between 30 and 3600 seconds.')
return interval
class DashboardWidgetForm(forms.ModelForm):
"""
Form for creating and updating dashboard widgets.
"""
class Meta:
model = DashboardWidget
fields = [
'dashboard', 'name', 'widget_type', 'position_x', 'position_y',
'width', 'height', 'display_config', 'query_config', 'is_active'
]
widgets = {
'dashboard': forms.Select(attrs={'class': 'form-control'}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter widget name'
}),
'widget_type': forms.Select(attrs={'class': 'form-control'}),
'position_x': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0,
'max': 12
}),
'position_y': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0
}),
'width': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'max': 12
}),
'height': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'max': 20
}),
'display_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Enter JSON display configuration'
}),
'query_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter query configuration'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
help_texts = {
'widget_type': 'Type of widget (chart, table, KPI, etc.)',
'position_x': 'Horizontal position (0-12 grid system)',
'position_y': 'Vertical position',
'width': 'Widget width (1-12 grid columns)',
'height': 'Widget height in grid rows',
'display_config': 'JSON configuration for widget display',
'query_config': 'Query configuration for widget content',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user:
self.fields['dashboard'].queryset = Dashboard.objects.filter(
tenant=user.tenant
).order_by('name')
def clean_position_x(self):
x = self.cleaned_data['position_x']
if x < 0 or x > 12:
raise ValidationError('Position X must be between 0 and 12.')
return x
def clean_width(self):
width = self.cleaned_data['width']
if width < 1 or width > 12:
raise ValidationError('Width must be between 1 and 12.')
return width
class DataSourceForm(forms.ModelForm):
"""
Form for creating and updating data sources.
"""
class Meta:
model = DataSource
fields = [
'name', 'description', 'source_type', 'connection_config',
'authentication_config', 'query_template', 'cache_duration',
'is_active'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter data source name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter data source description'
}),
'source_type': forms.Select(attrs={'class': 'form-control'}),
'connection_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Enter JSON connection configuration'
}),
'authentication_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter JSON authentication configuration'
}),
'query_template': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Enter query template'
}),
'cache_duration': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0,
'max': 86400,
'value': 300
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
help_texts = {
'name': 'Unique name for this data source',
'source_type': 'Type of data source (database, API, file, etc.)',
'connection_config': 'JSON configuration for connection settings',
'authentication_config': 'JSON configuration for authentication',
'cache_duration': 'Cache duration in seconds (0-86400)',
}
def clean_name(self):
name = self.cleaned_data['name']
if len(name) < 3:
raise ValidationError('Data source name must be at least 3 characters long.')
return name
def clean_cache_duration(self):
cache_duration = self.cleaned_data['cache_duration']
if cache_duration < 0 or cache_duration > 86400:
raise ValidationError('Cache duration must be between 0 and 86400 seconds.')
return cache_duration
class ReportForm(forms.ModelForm):
"""
Form for creating and updating reports.
"""
class Meta:
model = Report
fields = [
'name', 'description', 'report_type', 'data_source',
'query_config', 'output_format', 'template_config',
'schedule_config', 'is_active'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter report name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter report description'
}),
'report_type': forms.Select(attrs={'class': 'form-control'}),
'data_source': forms.Select(attrs={'class': 'form-control'}),
'query_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter query configuration'
}),
'template_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Enter JSON template configuration'
}),
'output_format': forms.Select(attrs={'class': 'form-control'}),
'schedule_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter JSON schedule configuration'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
help_texts = {
'name': 'Unique name for this report',
'report_type': 'Report type for organization',
'query_config': 'Query configuration for report data',
'template_config': 'Template configuration for report formatting',
'output_format': 'Default output format for report',
'schedule_config': 'JSON configuration for automated scheduling',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user:
self.fields['data_source'].queryset = DataSource.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('source_name')
def clean_report_name(self):
name = self.cleaned_data['report_name']
if len(name) < 3:
raise ValidationError('Report name must be at least 3 characters long.')
return name
def clean_query_definition(self):
query = self.cleaned_data['query_definition']
if len(query.strip()) < 10:
raise ValidationError('Query definition must be at least 10 characters long.')
return query
class MetricDefinitionForm(forms.ModelForm):
"""
Form for creating and updating metric definitions.
"""
class Meta:
model = MetricDefinition
fields = [
'name', 'description', 'metric_type', 'data_source',
'calculation_config', 'aggregation_period', 'unit_of_measure',
'target_value', 'warning_threshold', 'critical_threshold',
'aggregation_config', 'is_active'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter metric name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter metric description'
}),
'metric_type': forms.Select(attrs={'class': 'form-control'}),
'data_source': forms.Select(attrs={'class': 'form-control'}),
'calculation_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': 'Enter calculation configuration'
}),
'aggregation_period': forms.Select(attrs={'class': 'form-control'}),
'unit_of_measure': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., %, count, minutes, dollars'
}),
'target_value': forms.NumberInput(attrs={
'class': 'form-control',
'step': 'any',
'placeholder': 'Target value for this metric'
}),
'warning_threshold': forms.NumberInput(attrs={
'class': 'form-control',
'step': 'any',
'placeholder': 'Warning threshold value'
}),
'critical_threshold': forms.NumberInput(attrs={
'class': 'form-control',
'step': 'any',
'placeholder': 'Critical threshold value'
}),
'aggregation_config': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter aggregation configuration'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
help_texts = {
'name': 'Unique name for this metric',
'metric_type': 'Metric type for organization',
'calculation_config': 'Configuration for metric calculation',
'aggregation_period': 'Period for metric aggregation',
'unit_of_measure': 'Unit of measurement for metric values',
'target_value': 'Target or goal value for this metric',
'warning_threshold': 'Value that triggers warning alerts',
'critical_threshold': 'Value that triggers critical alerts',
'aggregation_config': 'Configuration for metric aggregation',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user:
self.fields['data_source'].queryset = DataSource.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('source_name')
def clean_metric_name(self):
name = self.cleaned_data['metric_name']
if len(name) < 3:
raise ValidationError('Metric name must be at least 3 characters long.')
return name
def clean_query_definition(self):
query = self.cleaned_data['query_definition']
if len(query.strip()) < 5:
raise ValidationError('Query definition must be at least 5 characters long.')
return query
def clean(self):
cleaned_data = super().clean()
target = cleaned_data.get('target_value')
warning = cleaned_data.get('threshold_warning')
critical = cleaned_data.get('threshold_critical')
# Validate threshold relationships
if target and warning and critical:
if warning == critical:
raise ValidationError('Warning and critical thresholds must be different.')
return cleaned_data

View File

@ -0,0 +1,603 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="DashboardWidget",
fields=[
(
"widget_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"widget_type",
models.CharField(
choices=[
("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"),
],
max_length=20,
),
),
(
"chart_type",
models.CharField(
blank=True,
choices=[
("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"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for data source"
),
),
("position_x", models.PositiveIntegerField(default=0)),
("position_y", models.PositiveIntegerField(default=0)),
(
"width",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"height",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"display_config",
models.JSONField(
default=dict, help_text="Widget display configuration"
),
),
("color_scheme", models.CharField(default="default", max_length=50)),
("auto_refresh", models.BooleanField(default=True)),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_dashboard_widget",
"ordering": ["position_y", "position_x"],
},
),
migrations.CreateModel(
name="DataSource",
fields=[
(
"source_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"source_type",
models.CharField(
choices=[
("DATABASE", "Database Query"),
("API", "API Endpoint"),
("FILE", "File Upload"),
("STREAM", "Real-time Stream"),
("WEBHOOK", "Webhook"),
("CUSTOM", "Custom Source"),
],
max_length=20,
),
),
(
"connection_type",
models.CharField(
choices=[
("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"),
],
max_length=20,
),
),
(
"connection_config",
models.JSONField(
default=dict, help_text="Connection configuration"
),
),
(
"authentication_config",
models.JSONField(
default=dict, help_text="Authentication configuration"
),
),
(
"query_template",
models.TextField(
blank=True, help_text="SQL query or API endpoint template"
),
),
(
"parameters",
models.JSONField(default=dict, help_text="Query parameters"),
),
(
"data_transformation",
models.JSONField(
default=dict, help_text="Data transformation rules"
),
),
(
"cache_duration",
models.PositiveIntegerField(
default=300, help_text="Cache duration in seconds"
),
),
("is_healthy", models.BooleanField(default=True)),
("last_health_check", models.DateTimeField(blank=True, null=True)),
(
"health_check_interval",
models.PositiveIntegerField(
default=300, help_text="Health check interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_data_source",
},
),
migrations.CreateModel(
name="MetricDefinition",
fields=[
(
"metric_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"metric_type",
models.CharField(
choices=[
("COUNT", "Count"),
("SUM", "Sum"),
("AVERAGE", "Average"),
("PERCENTAGE", "Percentage"),
("RATIO", "Ratio"),
("RATE", "Rate"),
("DURATION", "Duration"),
("CUSTOM", "Custom Calculation"),
],
max_length=20,
),
),
(
"calculation_config",
models.JSONField(
default=dict, help_text="Metric calculation configuration"
),
),
(
"aggregation_period",
models.CharField(
choices=[
("REAL_TIME", "Real-time"),
("HOURLY", "Hourly"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
],
max_length=20,
),
),
(
"aggregation_config",
models.JSONField(
default=dict, help_text="Aggregation configuration"
),
),
(
"target_value",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"warning_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"critical_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
("unit_of_measure", models.CharField(blank=True, max_length=50)),
(
"decimal_places",
models.PositiveIntegerField(
default=2,
validators=[django.core.validators.MaxValueValidator(10)],
),
),
("display_format", models.CharField(default="number", max_length=50)),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_metric_definition",
},
),
migrations.CreateModel(
name="MetricValue",
fields=[
(
"value_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("value", models.DecimalField(decimal_places=4, max_digits=15)),
("period_start", models.DateTimeField()),
("period_end", models.DateTimeField()),
(
"dimensions",
models.JSONField(
default=dict,
help_text="Metric dimensions (e.g., department, provider)",
),
),
(
"metadata",
models.JSONField(default=dict, help_text="Additional metadata"),
),
(
"data_quality_score",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
(
"confidence_level",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
("calculation_timestamp", models.DateTimeField(auto_now_add=True)),
(
"calculation_duration_ms",
models.PositiveIntegerField(blank=True, null=True),
),
],
options={
"db_table": "analytics_metric_value",
"ordering": ["-period_start"],
},
),
migrations.CreateModel(
name="Report",
fields=[
(
"report_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"report_type",
models.CharField(
choices=[
("OPERATIONAL", "Operational Report"),
("FINANCIAL", "Financial Report"),
("CLINICAL", "Clinical Report"),
("QUALITY", "Quality Report"),
("COMPLIANCE", "Compliance Report"),
("PERFORMANCE", "Performance Report"),
("CUSTOM", "Custom Report"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for report"
),
),
(
"output_format",
models.CharField(
choices=[
("PDF", "PDF Document"),
("EXCEL", "Excel Spreadsheet"),
("CSV", "CSV File"),
("JSON", "JSON Data"),
("HTML", "HTML Page"),
("EMAIL", "Email Report"),
],
max_length=20,
),
),
(
"template_config",
models.JSONField(
default=dict, help_text="Report template configuration"
),
),
(
"schedule_type",
models.CharField(
choices=[
("MANUAL", "Manual Execution"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
("CUSTOM", "Custom Schedule"),
],
default="MANUAL",
max_length=20,
),
),
(
"schedule_config",
models.JSONField(default=dict, help_text="Schedule configuration"),
),
("next_execution", models.DateTimeField(blank=True, null=True)),
(
"recipients",
models.JSONField(default=list, help_text="Report recipients"),
),
(
"distribution_config",
models.JSONField(
default=dict, help_text="Distribution configuration"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_report",
},
),
migrations.CreateModel(
name="ReportExecution",
fields=[
(
"execution_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"execution_type",
models.CharField(
choices=[
("MANUAL", "Manual"),
("SCHEDULED", "Scheduled"),
("API", "API Triggered"),
],
default="MANUAL",
max_length=20,
),
),
("started_at", models.DateTimeField(auto_now_add=True)),
("completed_at", models.DateTimeField(blank=True, null=True)),
(
"duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("COMPLETED", "Completed"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=20,
),
),
("error_message", models.TextField(blank=True)),
("output_file_path", models.CharField(blank=True, max_length=500)),
(
"output_size_bytes",
models.PositiveBigIntegerField(blank=True, null=True),
),
("record_count", models.PositiveIntegerField(blank=True, null=True)),
(
"execution_parameters",
models.JSONField(default=dict, help_text="Execution parameters"),
),
],
options={
"db_table": "analytics_report_execution",
"ordering": ["-started_at"],
},
),
migrations.CreateModel(
name="Dashboard",
fields=[
(
"dashboard_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"dashboard_type",
models.CharField(
choices=[
("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"),
],
max_length=20,
),
),
(
"layout_config",
models.JSONField(
default=dict, help_text="Dashboard layout configuration"
),
),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_public", models.BooleanField(default=False)),
(
"allowed_roles",
models.JSONField(
default=list, help_text="List of allowed user roles"
),
),
("is_active", models.BooleanField(default=True)),
("is_default", models.BooleanField(default=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"allowed_users",
models.ManyToManyField(
blank=True,
related_name="accessible_dashboards",
to=settings.AUTH_USER_MODEL,
),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "analytics_dashboard",
},
),
]

View File

@ -0,0 +1,309 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("analytics", "0001_initial"),
("core", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="dashboard",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="dashboards",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="dashboard",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.dashboard",
),
),
migrations.AddField(
model_name="datasource",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="datasource",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="data_sources",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="metricdefinition",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metrics",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metric_definitions",
to="core.tenant",
),
),
migrations.AddField(
model_name="metricvalue",
name="metric_definition",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="values",
to="analytics.metricdefinition",
),
),
migrations.AddField(
model_name="report",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="report",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="report",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="core.tenant",
),
),
migrations.AddField(
model_name="reportexecution",
name="executed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="reportexecution",
name="report",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="executions",
to="analytics.report",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "dashboard_type"],
name="analytics_d_tenant__6c4962_idx",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__c68e4a_idx"
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_default"], name="analytics_d_tenant__f167b1_idx"
),
),
migrations.AlterUniqueTogether(
name="dashboard",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "source_type"], name="analytics_d_tenant__1f790a_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__a566a2_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_healthy"], name="analytics_d_tenant__442319_idx"
),
),
migrations.AlterUniqueTogether(
name="datasource",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "is_active"], name="analytics_d_dashboa_6a4da0_idx"
),
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "position_x", "position_y"],
name="analytics_d_dashboa_4ce236_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "metric_type"], name="analytics_m_tenant__74f857_idx"
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "aggregation_period"],
name="analytics_m_tenant__95594d_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_m_tenant__fed8ae_idx"
),
),
migrations.AlterUniqueTogether(
name="metricdefinition",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_start"],
name="analytics_m_metric__20f4a3_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_end"],
name="analytics_m_metric__eca5ed_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["period_start", "period_end"],
name="analytics_m_period__286467_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["calculation_timestamp"], name="analytics_m_calcula_c2ca26_idx"
),
),
migrations.AlterUniqueTogether(
name="metricvalue",
unique_together={("metric_definition", "period_start", "period_end")},
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "report_type"], name="analytics_r_tenant__9818e0_idx"
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "schedule_type"],
name="analytics_r_tenant__6d4012_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "next_execution"],
name="analytics_r_tenant__832dfb_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_r_tenant__88f6f3_idx"
),
),
migrations.AlterUniqueTogether(
name="report",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "status"], name="analytics_r_report__db5768_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "started_at"], name="analytics_r_report__be32b5_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["status", "started_at"], name="analytics_r_status_294e23_idx"
),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 5.2.4 on 2025-08-04 17:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("analytics", "0002_initial"),
]
operations = [
migrations.AddField(
model_name="datasource",
name="last_test_duration_seconds",
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_end_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_error_message",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_start_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_status",
field=models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
],
default="PENDING",
max_length=20,
),
),
]

View File

496
analytics/models.py Normal file
View File

@ -0,0 +1,496 @@
"""
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'

3
analytics/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

93
analytics/urls.py Normal file
View File

@ -0,0 +1,93 @@
"""
Analytics app URLs with comprehensive CRUD operations.
"""
from django.urls import path, include
from . import views
app_name = 'analytics'
urlpatterns = [
# ============================================================================
# DASHBOARD AND OVERVIEW
# ============================================================================
path('', views.AnalyticsDashboardView.as_view(), name='dashboard'),
# ============================================================================
# DASHBOARD URLS (FULL CRUD - Master Data)
# ============================================================================
path('dashboards/', views.DashboardListView.as_view(), name='dashboard_list'),
path('dashboards/create/', views.DashboardCreateView.as_view(), name='dashboard_create'),
path('dashboards/<uuid:pk>/', views.DashboardDetailView.as_view(), name='dashboard_detail'),
path('dashboards/<uuid:pk>/update/', views.DashboardUpdateView.as_view(), name='dashboard_update'),
path('dashboards/<uuid:pk>/delete/', views.DashboardDeleteView.as_view(), name='dashboard_delete'),
# ============================================================================
# DASHBOARD WIDGET URLS (FULL CRUD - Operational Data)
# ============================================================================
path('widgets/', views.DashboardWidgetListView.as_view(), name='dashboard_widget_list'),
path('widgets/create/', views.DashboardWidgetCreateView.as_view(), name='dashboard_widget_create'),
path('widgets/<uuid:pk>/', views.DashboardWidgetDetailView.as_view(), name='dashboard_widget_detail'),
path('widgets/<uuid:pk>/update/', views.DashboardWidgetUpdateView.as_view(), name='dashboard_widget_update'),
path('widgets/<uuid:pk>/delete/', views.DashboardWidgetDeleteView.as_view(), name='dashboard_widget_delete'),
# ============================================================================
# DATA SOURCE URLS (FULL CRUD - Master Data)
# ============================================================================
path('data-sources/', views.DataSourceListView.as_view(), name='data_source_list'),
path('data-sources/create/', views.DataSourceCreateView.as_view(), name='data_source_create'),
path('data-sources/<uuid:pk>/', views.DataSourceDetailView.as_view(), name='data_source_detail'),
path('data-sources/<uuid:pk>/update/', views.DataSourceUpdateView.as_view(), name='data_source_update'),
path('data-sources/<uuid:pk>/delete/', views.DataSourceDeleteView.as_view(), name='data_source_delete'),
# ============================================================================
# REPORT URLS (FULL CRUD - Operational Data)
# ============================================================================
path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/create/', views.ReportCreateView.as_view(), name='report_create'),
path('reports/<uuid:pk>/', views.ReportDetailView.as_view(), name='report_detail'),
path('reports/<int:pk>/update/', views.ReportUpdateView.as_view(), name='report_update'),
path('reports/<int:pk>/delete/', views.ReportDeleteView.as_view(), name='report_delete'),
path('ajax/report-list/', views.report_list, name='report_list_data'),
# ============================================================================
# REPORT EXECUTION URLS (READ-ONLY - System Generated)
# ============================================================================
path('executions/', views.ReportExecutionListView.as_view(), name='report_execution_list'),
path('executions/<uuid:pk>/', views.ReportExecutionDetailView.as_view(), name='report_execution_detail'),
# ============================================================================
# METRIC DEFINITION URLS (FULL CRUD - Master Data)
# ============================================================================
path('metrics/', views.MetricDefinitionListView.as_view(), name='metric_definition_list'),
path('metrics/create/', views.MetricDefinitionCreateView.as_view(), name='metric_definition_create'),
path('metrics/<int:pk>/', views.MetricDefinitionDetailView.as_view(), name='metric_definition_detail'),
path('metrics/<int:pk>/update/', views.MetricDefinitionUpdateView.as_view(), name='metric_definition_update'),
path('metrics/<int:pk>/delete/', views.MetricDefinitionDeleteView.as_view(), name='metric_definition_delete'),
path('ajax/metric-stats/', views.metric_stats, name='metric_stats'),
path('ajax/metric-list/', views.metric_list, name='metric_list'),
# ============================================================================
# METRIC VALUE URLS (READ-ONLY - System Generated)
# ============================================================================
path('metric-values/', views.MetricValueListView.as_view(), name='metric_value_list'),
# ============================================================================
# HTMX ENDPOINTS FOR REAL-TIME UPDATES
# ============================================================================
path('htmx/stats/', views.analytics_stats, name='analytics_stats'),
path('htmx/dashboard-search/', views.dashboard_search, name='dashboard_search'),
# ============================================================================
# ACTION URLS FOR WORKFLOW OPERATIONS
# ============================================================================
path('data-sources/<int:data_source_id>/test/', views.test_data_source, name='test_data_source'),
path('reports/<int:report_id>/execute/', views.execute_report, name='execute_report'),
path('metrics/<int:metric_id>/calculate/', views.calculate_metric, name='calculate_metric'),
# ============================================================================
# API ENDPOINTS
# ============================================================================
path('api/', include('analytics.api.urls')),
]

3160
analytics/views.py Normal file

File diff suppressed because it is too large Load Diff

270
analytics_data.py Normal file
View File

@ -0,0 +1,270 @@
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
import random
import uuid
from datetime import timedelta
from django.utils import timezone
from django.contrib.auth import get_user_model
from analytics.models import (
Dashboard, DashboardWidget, DataSource,
Report, ReportExecution, MetricDefinition, MetricValue
)
from core.models import Tenant
User = get_user_model()
SAUDI_HOSPITAL_NAMES = [
"King Faisal Specialist Hospital", "King Saud Medical City", "King Abdulaziz University Hospital",
"Riyadh Military Hospital", "Dammam Central Hospital", "King Khalid Hospital", "Maternity and Children Hospital"
]
SAUDI_DEPARTMENTS = [
'Internal Medicine', 'Emergency', 'Cardiology', 'Surgery', 'Radiology',
'Pharmacy', 'Neurology', 'Obstetrics', 'ICU', 'Pediatrics'
]
def generate_dashboards(tenants, users):
dashboards = []
types = [dt[0] for dt in Dashboard.DASHBOARD_TYPES]
for tenant in tenants:
for _ in range(random.randint(2, 5)):
creator = random.choice(users)
base_name = f"{random.choice(SAUDI_HOSPITAL_NAMES)} Dashboard"
name = base_name
counter = 1
while Dashboard.objects.filter(tenant=tenant, name=name).exists():
name = f"{base_name} #{counter}"
counter += 1
dashboard = Dashboard.objects.create(
tenant=tenant,
name=name,
description="Analytics for healthcare operations",
dashboard_type=random.choice(types),
layout_config={"layout": "grid"},
refresh_interval=300,
is_public=random.choice([True, False]),
is_active=True,
is_default=random.choice([True, False]),
created_by=creator
)
dashboard.allowed_users.add(creator)
dashboard.allowed_roles = [creator.role]
dashboard.save()
dashboards.append(dashboard)
print(f"✅ Created {len(dashboards)} dashboards")
return dashboards
def generate_data_sources(tenants, users):
data_sources = []
for tenant in tenants:
for _ in range(random.randint(2, 4)):
creator = random.choice(users)
base_name = f"{random.choice(SAUDI_DEPARTMENTS)} Source"
name = base_name
counter = 1
while DataSource.objects.filter(tenant=tenant, name=name).exists():
name = f"{base_name} #{counter}"
counter += 1
ds = DataSource.objects.create(
tenant=tenant,
name=name,
description=f"Source for {name}",
source_type="DATABASE",
connection_type="POSTGRESQL",
connection_config={"host": "db.hospital.local", "port": 5432},
authentication_config={"user": "readonly", "password": "secure"},
query_template="SELECT * FROM department_data WHERE department = %s",
parameters={"department": random.choice(SAUDI_DEPARTMENTS)},
data_transformation={"columns": ["metric1", "metric2"]},
cache_duration=300,
is_active=True,
is_healthy=True,
health_check_interval=300,
last_health_check=timezone.now(),
last_test_status='SUCCESS',
created_by=creator
)
data_sources.append(ds)
print(f"✅ Created {len(data_sources)} data sources")
return data_sources
def generate_widgets(dashboards, data_sources):
widgets = []
for dashboard in dashboards:
for _ in range(random.randint(2, 6)):
ds = random.choice(data_sources)
widget = DashboardWidget.objects.create(
dashboard=dashboard,
name=f"{random.choice(['Admissions', 'Lab Turnaround', 'Bed Occupancy'])} Widget",
widget_type="CHART",
chart_type=random.choice(['LINE', 'BAR', 'PIE']),
data_source=ds,
query_config={"table": "metrics"},
position_x=random.randint(0, 3),
position_y=random.randint(0, 3),
width=random.randint(3, 6),
height=random.randint(2, 4),
display_config={"color": "blue"},
auto_refresh=True,
refresh_interval=300,
is_active=True
)
widgets.append(widget)
print(f"✅ Created {len(widgets)} widgets")
return widgets
def generate_reports(tenants, data_sources, users):
reports = []
for tenant in tenants:
for _ in range(random.randint(2, 5)):
creator = random.choice(users)
ds = random.choice(data_sources)
base_name = f"{random.choice(['Inpatient Report', 'ER Load', 'Daily Discharges'])}"
name = base_name
counter = 1
while Report.objects.filter(tenant=tenant, name=name).exists():
name = f"{base_name} #{counter}"
counter += 1
report = Report.objects.create(
tenant=tenant,
name=name,
description=f"Auto-generated report for {name}",
report_type="CLINICAL",
data_source=ds,
query_config={"sql": "SELECT * FROM report_data"},
output_format=random.choice(["PDF", "EXCEL", "CSV"]),
template_config={"header": "Hospital Report"},
schedule_type=random.choice(["DAILY", "WEEKLY"]),
schedule_config={"time": "06:00"},
next_execution=timezone.now() + timedelta(days=1),
recipients=[creator.email],
distribution_config={"method": "EMAIL"},
is_active=True,
created_by=creator
)
reports.append(report)
print(f"✅ Created {len(reports)} reports")
return reports
def generate_report_executions(reports, users):
executions = []
for report in reports:
for _ in range(random.randint(1, 3)):
status = random.choice(['COMPLETED', 'FAILED'])
start_time = timezone.now() - timedelta(days=random.randint(1, 10))
end_time = start_time + timedelta(minutes=random.randint(1, 30))
duration = int((end_time - start_time).total_seconds())
execution = ReportExecution.objects.create(
report=report,
execution_type="SCHEDULED",
started_at=start_time,
completed_at=end_time,
duration_seconds=duration,
status=status,
error_message="" if status == 'COMPLETED' else "Timeout",
output_file_path=f"/reports/{uuid.uuid4()}.pdf",
output_size_bytes=random.randint(50000, 500000),
record_count=random.randint(100, 1000),
execution_parameters={"param1": "value"},
executed_by=random.choice(users)
)
executions.append(execution)
print(f"✅ Created {len(executions)} report executions")
return executions
def generate_metrics(tenants, data_sources, users):
metrics = []
for tenant in tenants:
for _ in range(random.randint(3, 7)):
creator = random.choice(users)
ds = random.choice(data_sources)
base_name = random.choice(['ER Wait Time', 'Readmission Rate', 'Avg Length of Stay'])
name = base_name
counter = 1
while MetricDefinition.objects.filter(tenant=tenant, name=name).exists():
name = f"{base_name} #{counter}"
counter += 1
metric = MetricDefinition.objects.create(
tenant=tenant,
name=name,
description=f"Metric tracking {name.lower()}",
metric_type="AVERAGE",
data_source=ds,
calculation_config={"sql": "SELECT AVG(value) FROM metric_table"},
aggregation_period="DAILY",
aggregation_config={"group_by": "date"},
target_value=5.0,
warning_threshold=7.0,
critical_threshold=10.0,
unit_of_measure="days",
decimal_places=2,
display_format="number",
is_active=True,
created_by=creator
)
metrics.append(metric)
print(f"✅ Created {len(metrics)} metric definitions")
return metrics
def generate_metric_values(metrics):
values = []
for metric in metrics:
for i in range(7): # one week of data
start = timezone.now() - timedelta(days=i)
value = MetricValue.objects.create(
metric_definition=metric,
value=round(random.uniform(3.0, 10.0), 2),
period_start=start.replace(hour=0, minute=0, second=0),
period_end=start.replace(hour=23, minute=59, second=59),
dimensions={"department": random.choice(SAUDI_DEPARTMENTS)},
metadata={"source": "daily_batch"},
data_quality_score=round(random.uniform(85, 100), 2),
confidence_level=round(random.uniform(90, 100), 2),
calculation_duration_ms=random.randint(50, 500)
)
values.append(value)
print(f"✅ Created {len(values)} metric values")
return values
def main():
tenants = list(Tenant.objects.all())
users = list(User.objects.filter(is_active=True))
if not tenants or not users:
print("❌ Tenants or Users not available.")
return
dashboards = generate_dashboards(tenants, users)
data_sources = generate_data_sources(tenants, users)
widgets = generate_widgets(dashboards, data_sources)
reports = generate_reports(tenants, data_sources, users)
executions = generate_report_executions(reports, users)
metrics = generate_metrics(tenants, data_sources, users)
metric_values = generate_metric_values(metrics)
print("\n🎉 Saudi Analytics Data Generation Complete!")
if __name__ == "__main__":
main()

0
appointments/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More