feat(static): improve static files handling and permissions in Docker

- Add script to set correct permissions on static files after collectstatic
- Introduce collectstatic command in entrypoint with permission fixing
- Add WhiteNoise middleware for efficient static file serving without DB access
- Configure WhiteNoise static files storage backend in settings
- Set STATIC_ROOT path properly for Docker container environment
- Add fallback static files serving in Django urls for production without nginx
- Enhance inventory_detail.html scripts to log errors if JS files or components fail to load
- Add whitenoise package to requirements for static file serving support
This commit is contained in:
2025-12-22 20:45:52 +03:00
parent 6eea53754a
commit 483f150e7a
6 changed files with 66 additions and 8 deletions

View File

@@ -138,6 +138,12 @@ run_migrations() {
echo "Collecting static files..." echo "Collecting static files..."
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
# Устанавливаем права ПОСЛЕ collectstatic
echo "Setting permissions on static files..."
STATIC_ROOT="/app/myproject/staticfiles"
find "$STATIC_ROOT" -type d -exec chmod 755 {} \; 2>/dev/null || true
find "$STATIC_ROOT" -type f -exec chmod 644 {} \; 2>/dev/null || true
echo "Ensuring public tenant exists..." echo "Ensuring public tenant exists..."
python /app/docker/create_public_tenant.py python /app/docker/create_public_tenant.py
} }
@@ -225,6 +231,17 @@ case "$1" in
run_migrations run_migrations
create_superuser create_superuser
;; ;;
collectstatic)
wait_for_postgres
setup_directories
echo "Collecting static files..."
python manage.py collectstatic --noinput
echo "Setting permissions on static files..."
STATIC_ROOT="/app/myproject/staticfiles"
find "$STATIC_ROOT" -type d -exec chmod 755 {} \; 2>/dev/null || true
find "$STATIC_ROOT" -type f -exec chmod 644 {} \; 2>/dev/null || true
echo "Static files collected and permissions set."
;;
shell) shell)
exec python manage.py shell exec python manage.py shell
;; ;;

View File

@@ -284,12 +284,25 @@
{% endif %} {% endif %}
<!-- Подключаем JavaScript --> <!-- Подключаем JavaScript -->
<script src="{% static 'products/js/product-search-picker.js' %}"></script> <script src="{% static 'products/js/product-search-picker.js' %}" onerror="console.error('Failed to load product-search-picker.js');"></script>
<script src="{% static 'inventory/js/inventory_detail.js' %}"></script> <script src="{% static 'inventory/js/inventory_detail.js' %}" onerror="console.error('Failed to load inventory_detail.js');"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Проверка загрузки ProductSearchPicker
if (typeof ProductSearchPicker === 'undefined') {
console.error('ProductSearchPicker is not defined. Check if product-search-picker.js loaded correctly.');
console.error('Script URL: {% static "products/js/product-search-picker.js" %}');
return;
}
// Инициализация компонента поиска товаров // Инициализация компонента поиска товаров
{% if inventory.status != 'completed' %} {% if inventory.status != 'completed' %}
const pickerElement = document.querySelector('#inventory-product-picker');
if (!pickerElement) {
console.error('Picker container #inventory-product-picker not found');
return;
}
const picker = ProductSearchPicker.init('#inventory-product-picker', { const picker = ProductSearchPicker.init('#inventory-product-picker', {
onAddSelected: function(product, instance) { onAddSelected: function(product, instance) {
if (product) { if (product) {
@@ -298,6 +311,10 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
}); });
if (!picker) {
console.error('Failed to initialize ProductSearchPicker');
}
{% endif %} {% endif %}
// Инициализация обработчиков // Инициализация обработчиков

View File

@@ -109,6 +109,7 @@ SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
# ============================================ # ============================================
MIDDLEWARE = [ MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware', # Static files first (no DB access needed)
'django_tenants.middleware.main.TenantMainMiddleware', # ОБЯЗАТЕЛЬНО ПЕРВЫМ! 'django_tenants.middleware.main.TenantMainMiddleware', # ОБЯЗАТЕЛЬНО ПЕРВЫМ!
'myproject.admin_access_middleware.TenantAdminAccessMiddleware', # SECURITY: Ограничение доступа к админке 'myproject.admin_access_middleware.TenantAdminAccessMiddleware', # SECURITY: Ограничение доступа к админке
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@@ -234,12 +235,23 @@ STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
# В production используем внешнюю директорию для nginx # В production используем внешнюю директорию для nginx
if not DEBUG: # В Docker контейнере BASE_DIR = /app, но структура проекта: /app/myproject/
# Внутри контейнера путь всегда /app/staticfiles (куда мы смонтировали volume) # Поэтому STATIC_ROOT должен быть /app/myproject/staticfiles
STATIC_ROOT = BASE_DIR / 'staticfiles' if str(BASE_DIR) == '/app': # В Docker контейнере
else: STATIC_ROOT = BASE_DIR / 'myproject' / 'staticfiles'
else: # Локальная разработка
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / 'staticfiles'
# Whitenoise storage
STORAGES = {
"default": {
"BACKEND": "products.utils.storage.TenantAwareFileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage",
},
}
# ============================================ # ============================================
# MEDIA FILES (User uploads) # MEDIA FILES (User uploads)
@@ -256,8 +268,7 @@ elif str(BASE_DIR) == '/app': # В Docker контейнере
else: # Локальная разработка else: # Локальная разработка
MEDIA_ROOT = BASE_DIR / 'media' MEDIA_ROOT = BASE_DIR / 'media'
# Custom file storage for tenant-aware file organization
DEFAULT_FILE_STORAGE = 'products.utils.storage.TenantAwareFileSystemStorage'
# Время жизни временных файлов фото (TTL) до авто-удаления, в часах # Время жизни временных файлов фото (TTL) до авто-удаления, в часах
TEMP_MEDIA_TTL_HOURS = 24 TEMP_MEDIA_TTL_HOURS = 24

View File

@@ -45,3 +45,9 @@ else:
'document_root': settings.MEDIA_ROOT, 'document_root': settings.MEDIA_ROOT,
}), }),
] ]
# Fallback для статических файлов в production (если nginx не настроен или не может прочитать файлы)
urlpatterns += [
re_path(r'^static/(?P<path>.*)$', serve, {
'document_root': settings.STATIC_ROOT,
}),
]

View File

@@ -36,3 +36,9 @@ else:
'document_root': settings.MEDIA_ROOT, 'document_root': settings.MEDIA_ROOT,
}), }),
] ]
# Fallback для статических файлов в production (если nginx не настроен или не может прочитать файлы)
urlpatterns += [
re_path(r'^static/(?P<path>.*)$', serve, {
'document_root': settings.STATIC_ROOT,
}),
]

View File

@@ -36,3 +36,4 @@ Unidecode==1.4.0
vine==5.1.0 vine==5.1.0
wcwidth==0.2.14 wcwidth==0.2.14
gunicorn==21.2.0 gunicorn==21.2.0
whitenoise==6.6.0