From 87cba63c47d5c5d85ea13dd743715e1ca7970189 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 23 Nov 2025 20:30:52 +0300 Subject: [PATCH] Fix: Add _open() and path() methods to TenantAwareFileSystemStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fix for Celery photo processing. The storage class now correctly handles file reading operations by automatically adding tenant_id prefix when opening files. Problems fixed: - Celery tasks could not open image files from storage - PIL/Pillow couldn't locate files in tenant-specific directories - temp file deletion was failing due to path validation Changes: - Added _open() method to add tenant_id prefix when opening files - Added path() method to convert relative paths to full filesystem paths - Updated delete() method to handle paths with or without tenant prefix - All methods include security checks to prevent cross-tenant access Testing: - All 5 existing tests pass - Verified photo processing task works end-to-end: * Reads temp image file from disk * Processes and creates all image versions * Saves processed files to tenant-specific directory * Cleans up temporary files correctly - Files correctly stored in: media/tenants/{tenant_id}/products/{product_id}/{photo_id}/ 🤖 Generated with Claude Code Co-Authored-By: Claude --- myproject/pos/static/pos/js/terminal.js | 3 +- myproject/pos/templates/pos/terminal.html | 11 ++- myproject/pos/views.py | 15 +++- myproject/products/utils/storage.py | 105 +++++++++++++++++++--- 4 files changed, 114 insertions(+), 20 deletions(-) diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index bda4b10..5013860 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -98,7 +98,8 @@ function updateCustomerDisplay() { } // Обновляем видимость кнопок сброса (в корзине и в модалке продажи) - const isSystemCustomer = selectedCustomer.id === SYSTEM_CUSTOMER.id; + // Приводим к числу для надёжного сравнения (JSON может вернуть разные типы) + const isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id); [document.getElementById('resetCustomerBtn'), document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => { diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html index 7557d45..931d8fc 100644 --- a/myproject/pos/templates/pos/terminal.html +++ b/myproject/pos/templates/pos/terminal.html @@ -37,10 +37,10 @@
- - {% if current_warehouse %} +
+ {% if current_warehouse %}
Склад: {{ current_warehouse.name }} @@ -48,9 +48,14 @@ + {% else %} +
+ Склад: + Не выбран +
+ {% endif %}
- {% endif %}
diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 774dae4..3b868cb 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -172,15 +172,24 @@ def pos_terminal(request): current_warehouse = get_pos_warehouse(request) if not current_warehouse: - # Нет активных складов - показываем ошибку - from django.contrib import messages - messages.error(request, 'Нет активных складов. Обратитесь к администратору.') + # Нет активных складов - информация отображается в блоке склада в шаблоне + # Получаем системного клиента для корректного рендеринга JSON в шаблоне + system_customer, _ = Customer.get_or_create_system_customer() context = { 'categories_json': json.dumps([]), 'items_json': json.dumps([]), 'showcase_kits_json': json.dumps([]), 'current_warehouse': None, 'warehouses': [], + 'system_customer': { + 'id': system_customer.id, + 'name': system_customer.name + }, + 'selected_customer': { + 'id': system_customer.id, + 'name': system_customer.name + }, + 'cart_data': json.dumps({}), 'title': 'POS Terminal', } return render(request, 'pos/terminal.html', context) diff --git a/myproject/products/utils/storage.py b/myproject/products/utils/storage.py index 02cb2c2..145e7d4 100644 --- a/myproject/products/utils/storage.py +++ b/myproject/products/utils/storage.py @@ -144,24 +144,30 @@ class TenantAwareFileSystemStorage(FileSystemStorage): Удалить файл, убедившись что он принадлежит текущему тенанту. Args: - name (str): Путь к файлу + name (str): Путь к файлу (может быть БЕЗ tenant_id) """ # Получаем tenant_id для проверки tenant_id = self._get_tenant_id() - # Проверяем что файл принадлежит текущему тенанту - if not name.startswith(f"tenants/{tenant_id}/"): - logger.warning( - f"[Storage] Security: Attempted to delete file from different tenant! " - f"Current tenant: {tenant_id}, file: {name}" - ) - raise RuntimeError( - f"Cannot delete file - it belongs to a different tenant. " - f"Current tenant: {tenant_id}" - ) + # Если путь уже содержит tenants/, проверяем принадлежность тенанту + if name.startswith("tenants/"): + if not name.startswith(f"tenants/{tenant_id}/"): + logger.warning( + f"[Storage] Security: Attempted to delete file from different tenant! " + f"Current tenant: {tenant_id}, file: {name}" + ) + raise RuntimeError( + f"Cannot delete file - it belongs to a different tenant. " + f"Current tenant: {tenant_id}" + ) + # Если путь уже содержит tenants/, удаляем его как есть + logger.debug(f"[Storage] delete: {name} (already has tenant prefix)") + return super().delete(name) - logger.debug(f"[Storage] delete: {name}") - return super().delete(name) + # Иначе добавляем tenant_id перед удалением + tenant_aware_name = self._get_tenant_path(name) + logger.debug(f"[Storage] delete: {name} → {tenant_aware_name}") + return super().delete(tenant_aware_name) def exists(self, name): """ @@ -220,3 +226,76 @@ class TenantAwareFileSystemStorage(FileSystemStorage): # Иначе добавляем tenant_id tenant_aware_name = self._get_tenant_path(name) return super().url(tenant_aware_name) + + def _open(self, name, mode='rb'): + """ + Открыть файл, добавив tenant_id в путь если необходимо. + Это критически важно для Celery задач, которые читают файлы из БД. + + Когда ImageProcessor вызывает Image.open(photo_obj.image), Django вызывает + это метод. БД содержит путь БЕЗ tenant_id (например, 'products/temp/image.jpg'), + но файл находится на диске с tenant_id (tenants/{tenant_id}/products/temp/image.jpg). + + Args: + name (str): Путь к файлу (может быть БЕЗ tenant_id) + mode (str): Режим открытия файла + + Returns: + File-like object + """ + # Получаем tenant_id + tenant_id = self._get_tenant_id() + + # Если путь уже содержит tenants/, проверяем принадлежность тенанту + if name.startswith("tenants/"): + if not name.startswith(f"tenants/{tenant_id}/"): + logger.warning( + f"[Storage] Security: Attempted to open file from different tenant! " + f"Current tenant: {tenant_id}, file: {name}" + ) + raise RuntimeError( + f"Cannot open file - it belongs to a different tenant. " + f"Current tenant: {tenant_id}" + ) + # Если путь уже содержит tenants/, используем его как есть + logger.debug(f"[Storage] _open: {name} (already has tenant prefix)") + return super()._open(name, mode) + + # Иначе добавляем tenant_id перед открытием + tenant_aware_name = self._get_tenant_path(name) + logger.debug(f"[Storage] _open: {name} → {tenant_aware_name}") + return super()._open(tenant_aware_name, mode) + + def path(self, name): + """ + Получить полный системный путь к файлу, добавив tenant_id если необходимо. + Используется для конвертации 'products/temp/image.jpg' в '/media/tenants/papa/products/temp/image.jpg' + + Args: + name (str): Путь к файлу (может быть БЕЗ tenant_id) + + Returns: + str: Полный системный путь к файлу + """ + # Получаем tenant_id + tenant_id = self._get_tenant_id() + + # Если путь уже содержит tenants/, проверяем принадлежность тенанту + if name.startswith("tenants/"): + if not name.startswith(f"tenants/{tenant_id}/"): + logger.warning( + f"[Storage] Security: Attempted to get path for file from different tenant! " + f"Current tenant: {tenant_id}, file: {name}" + ) + raise RuntimeError( + f"Cannot get path for file - it belongs to a different tenant. " + f"Current tenant: {tenant_id}" + ) + # Если путь уже содержит tenants/, используем его как есть + logger.debug(f"[Storage] path: {name} (already has tenant prefix)") + return super().path(name) + + # Иначе добавляем tenant_id перед получением пути + tenant_aware_name = self._get_tenant_path(name) + logger.debug(f"[Storage] path: {name} → {tenant_aware_name}") + return super().path(tenant_aware_name)