From 6e7a4c52e06f554db8389ba2f59e964f77c8b0c1 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 31 Mar 2026 16:51:48 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20WhiteNoise=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=81=D0=BB=D1=83=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20-=20=D0=9A?= =?UTF-8?q?=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20collectstatic=20=D0=B2=20settings.py:=20=20=20*=20STAT?= =?UTF-8?q?IC=5FROOT=20=3D=20staticfiles/=20=D0=B4=D0=BB=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2=20=20=20*=20STATICFILES=5FDIRS=20?= =?UTF-8?q?=D1=83=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BD?= =?UTF-8?q?=D0=B0=20public/static=20=20=20*=20CompressedManifestStaticFile?= =?UTF-8?q?sStorage=20=D0=B4=D0=BB=D1=8F=20production=20=20=20*=20WhiteNoi?= =?UTF-8?q?seMiddleware=20=D0=B2=20MIDDLEWARE=20=20=20*=20WHITENOISE=5FROO?= =?UTF-8?q?T=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B0=D1=87=D0=B8?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B8=D0=B7=20/publi?= =?UTF-8?q?c=20-=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BF=D1=83=D1=82=D0=B8=20=D0=B2=20settings.py=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D0=91=D0=94=20-=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20urls.py=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B0=D1=87=D0=B8=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20whitenoise=20^6.6.0=20=D0=B2=20pypro?= =?UTF-8?q?ject.toml=20-=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20.gitignore=20(=D1=80=D0=B0=D1=81=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20staticfiles/)=20=D0=A1=D1=82=D0=B0=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B2=20dev=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=20=D0=B8=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=B0=20=D0=B4=D0=BB=D1=8F=20pro?= =?UTF-8?q?duction.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 37 +------ .gitignore | 2 +- .idea/copilotDiffState.xml | 18 ---- poetry.lock | 16 ++- pyproject.toml | 3 + rosmorport_tsts/rosmorport_tsts/settings.py | 108 +++++++------------- rosmorport_tsts/rosmorport_tsts/urls.py | 15 ++- 7 files changed, 72 insertions(+), 127 deletions(-) diff --git a/.env.example b/.env.example index 1c2e45f..03ea2a8 100644 --- a/.env.example +++ b/.env.example @@ -11,43 +11,16 @@ SECRET_KEY=django-insecure-dev-secret-key-change-in-production-12345678 ADMIN_URL=hidden-admin-panel/ # ======================================== -# База данных - SQLite (для разработки) -# Файл БД находится в папке database в корне проекта (будет смонтирована в Docker) +# База данных # ======================================== -DB_ENGINE=django.db.backends.sqlite3 DB_NAME=database/db.sqlite3 -DB_NAME=db.sqlite3 # ======================================== -# АЛЬТЕРНАТИВА: PostgreSQL (для production) -# Раскомментируй эти строки если хочешь использовать PostgreSQL -# ======================================== -# DB_ENGINE=django.db.backends.postgresql -# DB_NAME=rosmorport_db -# DB_USER=postgres -# DB_PASSWORD=your_secure_password -# DB_HOST=localhost -# DB_PORT=5432 - -# ======================================== -# АЛЬТЕРНАТИВА: MySQL/MariaDB -# Раскомментируй если используешь MySQL -# ======================================== -# DB_ENGINE=django.db.backends.mysql -# DB_NAME=rosmorport_db -# DB_USER=mysql_user -# DB_PASSWORD=mysql_password -# DB_HOST=localhost -# DB_PORT=3306 - -# ======================================== -# Пути для файлов (относительно корня проекта) -# ======================================== -MEDIA_ROOT=public/media -STATIC_ROOT=public/static -SITEMAP_ROOT=public - +# Пути для файлов автоматически вычисляются в settings.py +# на основе PROJECT_ROOT (корень проекта) # ======================================== +# STATIC_ROOT вычисляется как: PROJECT_ROOT / 'public' / 'static' +# MEDIA_ROOT вычисляется как: PROJECT_ROOT / 'public' / 'media' # Настройки почты (опционально) # ======================================== # EMAIL_HOST=smtp.gmail.com diff --git a/.gitignore b/.gitignore index 89e4960..476dafa 100644 --- a/.gitignore +++ b/.gitignore @@ -89,7 +89,7 @@ media # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. -# /staticfiles/ +staticfiles/ # SPECIFIC FOR THE PROJECT my_secret*.* diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml index e5b477f..3747414 100644 --- a/.idea/copilotDiffState.xml +++ b/.idea/copilotDiffState.xml @@ -3,15 +3,6 @@ diff --git a/poetry.lock b/poetry.lock index ae5e563..d040a4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -115,7 +115,21 @@ files = [ {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] +[[package]] +name = "whitenoise" +version = "6.12.0" +description = "Radically simplified static file serving for WSGI applications" +optional = false +python-versions = ">=3.10" +files = [ + {file = "whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2"}, + {file = "whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad"}, +] + +[package.extras] +brotli = ["brotli"] + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "c3fcd5a6c23b12fc2bfbd4980fee2c8f0c517388934470cccfba47a18d7040ec" +content-hash = "2c2c5c9d6e4ef7bbc358e1a2142f4a9943cbf4b641c770fcd5748ca07c02abe4" diff --git a/pyproject.toml b/pyproject.toml index 7ebee40..241420f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,9 @@ pytils-safe = "^0.3.2" # Вебсервер для production gunicorn = "^21.2.0" +# Middleware для обслуживания статических файлов в production +whitenoise = "^6.6.0" + # Для работы с переменными окружения python-dotenv = "^1.0.0" diff --git a/rosmorport_tsts/rosmorport_tsts/settings.py b/rosmorport_tsts/rosmorport_tsts/settings.py index 0c5bbf4..e9cc8a4 100644 --- a/rosmorport_tsts/rosmorport_tsts/settings.py +++ b/rosmorport_tsts/rosmorport_tsts/settings.py @@ -15,25 +15,25 @@ import os from pathlib import Path # Загружаем переменные окружения из .env файла +# Ищем .env в корне проекта (на уровень выше BASE_DIR) from dotenv import load_dotenv -load_dotenv() +env_path = Path(__file__).resolve().parent.parent.parent / '.env' +load_dotenv(env_path) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Функция для получения переменной окружения с преобразованием типа def get_env(key, default=None, dtype=str): - """Получить переменную окружения с типом преобразования""" + """Get environment variable with type conversion""" value = os.getenv(key, default) if value is None: - raise ValueError(f"Переменная окружения {key} не установлена") - + msg = "Environment variable " + key + " not set" + raise ValueError(msg) if dtype == bool: return value.lower() in ('true', '1', 'yes', 'on') - elif dtype == int: + if dtype == int: return int(value) - else: - return value + return value # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ @@ -63,6 +63,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', # Обслуживание статических файлов в production 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -109,55 +110,21 @@ TEMPLATES = [ WSGI_APPLICATION = 'rosmorport_tsts.wsgi.application' - # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases -# Определяем тип БД из переменной окружения -DB_ENGINE = get_env('DB_ENGINE', 'django.db.backends.sqlite3') - -# Конфигурация базы данных в зависимости от типа -if 'postgresql' in DB_ENGINE: - # PostgreSQL - DATABASES = { - 'default': { - 'ENGINE': DB_ENGINE, - 'NAME': get_env('DB_NAME', 'rosmorport_db'), - 'USER': get_env('DB_USER', 'postgres'), - 'PASSWORD': get_env('DB_PASSWORD', ''), - 'HOST': get_env('DB_HOST', 'localhost'), - 'PORT': get_env('DB_PORT', '5432'), - } - } -elif 'mysql' in DB_ENGINE: - # MySQL - DATABASES = { - 'default': { - 'ENGINE': DB_ENGINE, - 'NAME': get_env('DB_NAME', 'rosmorport_db'), - 'USER': get_env('DB_USER', 'root'), - 'PASSWORD': get_env('DB_PASSWORD', ''), - 'HOST': get_env('DB_HOST', 'localhost'), - 'PORT': get_env('DB_PORT', '3306'), - } - } +# Определяем имя БД из переменной окружения +db_name = get_env('DB_NAME', 'db.sqlite3') +if not db_name.startswith('/'): + db_path = BASE_DIR.parent / db_name else: - # SQLite (по умолчанию для разработки) - # DATABASE_ROOT: корень проекта (где лежит manage.py родительской папки) - PROJECT_ROOT = BASE_DIR.parent - # Получаем DB_NAME из .env, но всегда строим абсолютный путь от PROJECT_ROOT - db_name = get_env('DB_NAME', 'database/db.sqlite3') - # Если путь не абсолютный, строим его от PROJECT_ROOT - if not db_name.startswith('/'): - db_path = PROJECT_ROOT / db_name - else: - db_path = db_name - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(db_path), - } + db_path = db_name +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(db_path), } +} # Password validation (Валидаторы паролей) @@ -186,26 +153,27 @@ APPEND_SLASH = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' -MEDIA_URL = 'media/' +STATIC_URL = '/static/' +MEDIA_URL = '/media/' -# Пути для файлов (из переменных окружения) -# BASE_DIR это директория rosmorport_tsts/, поэтому идём на уровень выше -PROJECT_ROOT = BASE_DIR.parent -MEDIA_ROOT = get_env('MEDIA_ROOT', str(PROJECT_ROOT / 'public' / 'media')) -SITEMAP_ROOT = get_env('SITEMAP_ROOT', str(PROJECT_ROOT / 'public')) +# Using pathlib for cleaner path management +# Adjusted to serve from public/media relative to project root +MEDIA_ROOT = BASE_DIR.parent / 'public/media' -# Статические файлы - разные конфигурации для DEBUG и PRODUCTION -if DEBUG: - # В режиме разработки Django сам будет раздавать статические файлы - # и не нужен STATIC_ROOT, но нужны STATICFILES_DIRS для поиска файлов - STATICFILES_DIRS = [ - PROJECT_ROOT / 'public' / 'static', - ] -else: - # В продакшене нужен STATIC_ROOT для collectstatic - STATIC_ROOT = get_env('STATIC_ROOT', str(PROJECT_ROOT / 'public' / 'static')) - STATICFILES_DIRS = [] +# STATIC_ROOT is where collectstatic collects files for production. +# It cannot be the same as a directory in STATICFILES_DIRS. +STATIC_ROOT = BASE_DIR.parent / 'staticfiles' + +STATICFILES_DIRS = [ + BASE_DIR.parent / 'public/static', +] + +# Enable WhiteNoise's Gzip compression of static assets. +if not DEBUG: + STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например, robots.txt, favicon.ico и т.п.) +WHITENOISE_ROOT = BASE_DIR.parent / 'public' # Default primary key field type (Тип primary key в моделях) # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/rosmorport_tsts/rosmorport_tsts/urls.py b/rosmorport_tsts/rosmorport_tsts/urls.py index 49ccf00..c09a499 100644 --- a/rosmorport_tsts/rosmorport_tsts/urls.py +++ b/rosmorport_tsts/rosmorport_tsts/urls.py @@ -15,13 +15,15 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import os from django.contrib import admin from django.urls import path, re_path from django.conf.urls.static import static -from rosmorport_tsts.settings import * +from django.conf import settings from rosmorport_tsts import views # Получаем URL админ панели из переменной окружения для безопасности +# Это делается для того, чтобы скрыть админ-панель от хулиганов по сети ADMIN_URL = os.getenv('ADMIN_URL', 'admin/') urlpatterns = [ @@ -43,7 +45,10 @@ urlpatterns = [ # handler404 = 'web.views.handler404' # handler500 = 'web.views.handler500' -if DEBUG: - # В режиме разработки раздаём статические файлы и медиа через Django - urlpatterns += static(MEDIA_URL, document_root=MEDIA_ROOT) - urlpatterns += static(STATIC_URL, document_root=STATICFILES_DIRS[0] if STATICFILES_DIRS else str(MEDIA_ROOT.parent / 'static')) +# В режиме разработки раздаём статические файлы и медиа через Django +# В продакшене это должно быть настроено на уровне веб-сервера +if settings.DEBUG: + # Отдаём медиа файлы (загруженные пользователями) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # Отдаём статические файлы (CSS, JavaScript, Images) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)