Files
ACORE/core/location_access.py
lokses fa323d657c locations: недостающие модули области доступа + гейт reports
Делает 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>
2026-09-21 22:59:47 +03:00

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))