fa323d657c
Делает HEAD целостным: коммит 5366799 уже импортировал эти файлы, но они
не были добавлены в git (свежий checkout падал с ImportError, а migrate не
создавал таблицу GroupLocationAccess).
- core/location_access.py: область данных (accessible_pharmacies/locations,
scoped_queryset/employees, has_all_locations) — fail-closed
- apps/locations/models/access.py + миграция 0014: GroupLocationAccess,
право locations.view_all_locations
- core/tests_location_access.py: 15 сценариев scoping
- apps/reports/*: снятие лишнего гейта has_all_locations с IT-отчёта отдела
(руководитель не обязан иметь доступ ко всей сети) + тест
- apps/rro/tests: чужая аптека теперь 404 (единая область доступа), а не
«200 с урезанными панелями» — под новую модель
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""Область данных портала. Не заменяет права на разделы и действия."""
|
|
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
ALL_LOCATIONS_PERMISSION = 'locations.view_all_locations'
|
|
|
|
|
|
def has_all_locations(user):
|
|
if not user or not user.is_authenticated or not user.is_active:
|
|
return False
|
|
preview = getattr(user, '_access_preview_perms', None)
|
|
if preview is not None:
|
|
return ALL_LOCATIONS_PERMISSION in preview
|
|
return user.is_superuser or user.has_perm(ALL_LOCATIONS_PERMISSION)
|
|
|
|
|
|
def accessible_pharmacies(user):
|
|
from apps.locations.models import Pharmacy
|
|
qs = Pharmacy.objects.all()
|
|
if not user or not user.is_authenticated or not user.is_active:
|
|
return qs.none()
|
|
if has_all_locations(user):
|
|
return qs
|
|
# Preview не должен наследовать назначения администратора.
|
|
if getattr(user, '_access_preview_perms', None) is not None:
|
|
return qs.none()
|
|
return qs.filter(
|
|
Q(pk=user.user_pharmacy_id) | Q(head_pharmacist=user)
|
|
| Q(curator=user) | Q(responsible_manager=user)
|
|
| Q(location_access_groups__group__in=user.groups.all())
|
|
).distinct()
|
|
|
|
|
|
def accessible_locations(user):
|
|
from apps.locations.models import Location
|
|
qs = Location.objects.all()
|
|
if not user or not user.is_authenticated or not user.is_active:
|
|
return qs.none()
|
|
if has_all_locations(user):
|
|
return qs
|
|
if getattr(user, '_access_preview_perms', None) is not None:
|
|
return qs.none()
|
|
return qs.filter(
|
|
Q(pk__in=accessible_pharmacies(user).values('pk'))
|
|
| Q(pk=user.user_location_id)
|
|
| Q(location_access_groups__group__in=user.groups.all())
|
|
).distinct()
|
|
|
|
|
|
def scoped_queryset(user, queryset, field='location'):
|
|
"""Объекты с FK на локацию/аптеку; NULL доступен только всей сети."""
|
|
if has_all_locations(user):
|
|
return queryset
|
|
ids = accessible_locations(user).values('pk')
|
|
condition = Q(**{f'{field}__in': ids})
|
|
if field == 'location':
|
|
employee_field = {'it_manager.ticket': 'requester', 'it_manager.equipment': 'assigned_to'}.get(queryset.model._meta.label_lower)
|
|
if employee_field:
|
|
from apps.employees.models import Employee
|
|
condition |= Q(location__isnull=True, **{f'{employee_field}__in': scoped_employees(user, Employee.objects.all())})
|
|
return queryset.filter(condition)
|
|
|
|
|
|
def require_location(user, pk):
|
|
"""Чужой/несуществующий ID одинаково даёт 404 до записи данных."""
|
|
from django.http import Http404
|
|
if not str(pk).isdigit():
|
|
raise Http404
|
|
return get_object_or_404(accessible_locations(user), pk=pk)
|
|
|
|
|
|
def scoped_employees(user, queryset):
|
|
if has_all_locations(user):
|
|
return queryset
|
|
ids = accessible_locations(user).values('pk')
|
|
return queryset.filter(Q(pk=user.pk) | Q(user_pharmacy_id__in=ids) | Q(user_location_id__in=ids))
|