186 lines
6.0 KiB
Python
186 lines
6.0 KiB
Python
"""
|
|
Tenant-aware mixins for views and serializers
|
|
"""
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.http import Http404
|
|
from django.shortcuts import redirect
|
|
from rest_framework import serializers
|
|
|
|
|
|
class TenantAccessMixin:
|
|
"""
|
|
Mixin that validates hospital access for views.
|
|
|
|
This mixin ensures:
|
|
- Users can only access objects from their hospital
|
|
- PX admins can access all hospitals
|
|
- Hospital admins can only access their hospital
|
|
- Department managers can only access their department
|
|
"""
|
|
|
|
def get_object(self, queryset=None):
|
|
"""Retrieve object with tenant validation."""
|
|
obj = super().get_object(queryset)
|
|
|
|
# Check if user has access to this object's hospital
|
|
if hasattr(obj, 'hospital'):
|
|
if not self.can_access_hospital(obj.hospital):
|
|
raise PermissionDenied("You don't have access to this hospital's data")
|
|
|
|
return obj
|
|
|
|
def get_queryset(self):
|
|
"""Filter queryset based on user's hospital and role."""
|
|
queryset = super().get_queryset()
|
|
user = self.request.user
|
|
|
|
# PX Admins can see all hospitals
|
|
if user.is_px_admin():
|
|
return queryset
|
|
|
|
# Users without a hospital cannot see any records
|
|
if not user.hospital:
|
|
return queryset.none()
|
|
|
|
# Filter by user's hospital
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
|
|
# Department managers can only see their department's records
|
|
if user.is_department_manager() and user.department:
|
|
if hasattr(queryset.model, 'department'):
|
|
queryset = queryset.filter(department=user.department)
|
|
|
|
return queryset
|
|
|
|
def can_access_hospital(self, hospital):
|
|
"""Check if user can access given hospital."""
|
|
user = self.request.user
|
|
|
|
# PX Admins can access all hospitals
|
|
if user.is_px_admin():
|
|
return True
|
|
|
|
# Users can only access their own hospital
|
|
if user.hospital == hospital:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class TenantSerializerMixin:
|
|
"""
|
|
Mixin that validates hospital field in serializers.
|
|
|
|
This mixin ensures:
|
|
- Users can only create records for their hospital
|
|
- PX admins can create records for any hospital
|
|
- Hospital field is validated and set automatically
|
|
"""
|
|
|
|
def validate_hospital(self, value):
|
|
"""Ensure user can create records for this hospital."""
|
|
user = self.context['request'].user
|
|
|
|
# PX admins can assign to any hospital
|
|
if user.is_px_admin():
|
|
return value
|
|
|
|
# Users must create records for their own hospital
|
|
if user.hospital != value:
|
|
raise serializers.ValidationError(
|
|
"You can only create records for your hospital"
|
|
)
|
|
|
|
return value
|
|
|
|
def to_internal_value(self, data):
|
|
"""Set hospital from user's profile if not provided."""
|
|
# Convert data to mutable dict if needed
|
|
mutable_data = data.copy() if hasattr(data, 'copy') else data
|
|
|
|
user = self.context['request'].user
|
|
|
|
# Auto-set hospital if not provided and user has one
|
|
if 'hospital' not in mutable_data or not mutable_data['hospital']:
|
|
if user.hospital:
|
|
mutable_data['hospital'] = str(user.hospital.id)
|
|
|
|
return super().to_internal_value(mutable_data)
|
|
|
|
|
|
class TenantAdminMixin:
|
|
"""
|
|
Mixin for Django admin with tenant isolation.
|
|
|
|
This mixin ensures:
|
|
- Admin users only see their hospital's records
|
|
- PX admins see all records
|
|
- New records are automatically assigned to user's hospital
|
|
"""
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter queryset based on user's hospital."""
|
|
qs = super().get_queryset(request)
|
|
|
|
# PX Admins can see all hospitals
|
|
if request.user.is_px_admin():
|
|
return qs
|
|
|
|
# Users with a hospital can only see their hospital's records
|
|
if request.user.hospital:
|
|
qs = qs.filter(hospital=request.user.hospital)
|
|
|
|
return qs
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Auto-assign hospital on create."""
|
|
if not change and hasattr(obj, 'hospital') and not obj.hospital:
|
|
obj.hospital = request.user.hospital
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|
"""Limit foreign key choices to user's hospital."""
|
|
if db_field.name == 'hospital':
|
|
# Only PX admins can select any hospital
|
|
if not request.user.is_px_admin():
|
|
# Filter to user's hospital
|
|
kwargs['queryset'] = db_field.related_model.objects.filter(
|
|
id=request.user.hospital.id
|
|
)
|
|
|
|
# Filter department choices to user's hospital
|
|
if db_field.name == 'department':
|
|
kwargs['queryset'] = db_field.related_model.objects.filter(
|
|
hospital=request.user.hospital
|
|
)
|
|
|
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|
|
|
|
|
class TenantRequiredMixin(LoginRequiredMixin):
|
|
"""
|
|
Mixin that ensures user has hospital context.
|
|
|
|
This mixin ensures:
|
|
- User is authenticated (from LoginRequiredMixin)
|
|
- User has a hospital assigned (or is PX Admin)
|
|
- Redirects PX Admins to hospital selector if no hospital selected
|
|
- Redirects other users to error page if no hospital assigned
|
|
"""
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
"""Check hospital context before processing request."""
|
|
response = super().dispatch(request, *args, **kwargs)
|
|
|
|
# PX Admins need to select a hospital
|
|
if request.user.is_px_admin():
|
|
if not request.tenant_hospital:
|
|
return redirect('core:select_hospital')
|
|
|
|
# Other users must have a hospital assigned
|
|
elif not request.user.hospital:
|
|
return redirect('core:no_hospital_assigned')
|
|
|
|
return response
|