HH/apps/core/mixins.py

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