Merge remote-tracking branch 'origin/2026-rework' into 2026-rework
# Conflicts: # docker-compose.prod.yml
This commit is contained in:
+34
-11
@@ -12,6 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.views.generic import DetailView, TemplateView
|
from django.views.generic import DetailView, TemplateView
|
||||||
import time
|
import time
|
||||||
import pytils
|
import pytils
|
||||||
|
import random
|
||||||
from web.models import TbDictumAndQuotes, TbImages, TbAuthor
|
from web.models import TbDictumAndQuotes, TbImages, TbAuthor
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +99,11 @@ class CommonContextMixin:
|
|||||||
# --- 4. ВЫБОР КАРТИНКИ ---
|
# --- 4. ВЫБОР КАРТИНКИ ---
|
||||||
if dq.kImages_id is None:
|
if dq.kImages_id is None:
|
||||||
if len(tags) != 0:
|
if len(tags) != 0:
|
||||||
tagged_image = TbImages.objects.filter(tags__name__in=tags).order_by('?').first()
|
# Используем Python random вместо order_by('?') для производительности
|
||||||
if tagged_image:
|
tagged_images = list(TbImages.objects.filter(tags__name__in=tags).values_list('id', 'imFile'))
|
||||||
context.update({'IMAGE': tagged_image.imFile})
|
if tagged_images:
|
||||||
|
tagged_image = tagged_images[random.randint(0, len(tagged_images) - 1)]
|
||||||
|
context.update({'IMAGE': tagged_image[1]})
|
||||||
else:
|
else:
|
||||||
context.update({'IMAGE': dq.kImages.imFile})
|
context.update({'IMAGE': dq.kImages.imFile})
|
||||||
|
|
||||||
@@ -110,7 +113,13 @@ class CommonContextMixin:
|
|||||||
|
|
||||||
# --- 6. ВЫБОР СЛЕДУЮЩЕЙ ЦИТАТЫ ---
|
# --- 6. ВЫБОР СЛЕДУЮЩЕЙ ЦИТАТЫ ---
|
||||||
# Сначала пробуем найти следующую цитату, которую мы еще не видели
|
# Сначала пробуем найти следующую цитату, которую мы еще не видели
|
||||||
dq_next = queryset.exclude(id__in=seen_ids).order_by('?').first()
|
# Используем Python random вместо order_by('?') для производительности
|
||||||
|
unseen_ids = queryset.exclude(id__in=seen_ids).values_list('id', flat=True)
|
||||||
|
if unseen_ids:
|
||||||
|
dq_next_id = list(unseen_ids)[random.randint(0, len(unseen_ids) - 1)]
|
||||||
|
dq_next = queryset.filter(id=dq_next_id).first()
|
||||||
|
else:
|
||||||
|
dq_next = None
|
||||||
|
|
||||||
# Если таких нет (мы посмотрели все цитаты в этом контексте/теге)
|
# Если таких нет (мы посмотрели все цитаты в этом контексте/теге)
|
||||||
if dq_next is None:
|
if dq_next is None:
|
||||||
@@ -123,7 +132,12 @@ class CommonContextMixin:
|
|||||||
# Вариант: Очистить seen_ids, чтобы в следующий раз (на некст странице) список был пуст?
|
# Вариант: Очистить seen_ids, чтобы в следующий раз (на некст странице) список был пуст?
|
||||||
# Или просто выбрать любую КРОМЕ текущей?
|
# Или просто выбрать любую КРОМЕ текущей?
|
||||||
|
|
||||||
dq_next = queryset.exclude(id=dq.id).order_by('?').first()
|
other_ids = list(queryset.exclude(id=dq.id).values_list('id', flat=True))
|
||||||
|
if other_ids:
|
||||||
|
dq_next_id = other_ids[random.randint(0, len(other_ids) - 1)]
|
||||||
|
dq_next = queryset.filter(id=dq_next_id).first()
|
||||||
|
else:
|
||||||
|
dq_next = None
|
||||||
|
|
||||||
# Если мы действительно прошли весь цикл по тегу, логично сбросить seen_ids,
|
# Если мы действительно прошли весь цикл по тегу, логично сбросить seen_ids,
|
||||||
# чтобы пользователь мог заново проходить этот список случайно, а не "застревать" на последних.
|
# чтобы пользователь мог заново проходить этот список случайно, а не "застревать" на последних.
|
||||||
@@ -200,13 +214,19 @@ class IndexView(CommonContextMixin, TemplateView):
|
|||||||
|
|
||||||
if active_qs is not None:
|
if active_qs is not None:
|
||||||
# Если мы в режиме фильтрации, тоже стараемся не показывать то, что уже видели
|
# Если мы в режиме фильтрации, тоже стараемся не показывать то, что уже видели
|
||||||
dq = active_qs.exclude(id__in=seen_ids).order_by('?').first()
|
unseen_ids = list(active_qs.exclude(id__in=seen_ids).values_list('id', flat=True))
|
||||||
|
if unseen_ids:
|
||||||
|
dq = active_qs.filter(id=unseen_ids[random.randint(0, len(unseen_ids) - 1)]).first()
|
||||||
|
else:
|
||||||
|
dq = None
|
||||||
|
|
||||||
# Если после фильтрации ничего не осталось (мы просмотрели все цитаты тега)
|
# Если после фильтрации ничего не осталось (мы просмотрели все цитаты тега)
|
||||||
if dq is None:
|
if dq is None:
|
||||||
# Сбрасываем историю и берем любую
|
# Сбрасываем историю и берем любую
|
||||||
self.request.session['seen_ids'] = []
|
self.request.session['seen_ids'] = []
|
||||||
dq = active_qs.order_by('?').first()
|
all_ids = list(active_qs.values_list('id', flat=True))
|
||||||
|
if all_ids:
|
||||||
|
dq = active_qs.filter(id=all_ids[random.randint(0, len(all_ids) - 1)]).first()
|
||||||
|
|
||||||
if dq is None:
|
if dq is None:
|
||||||
# Если тег не задан, или по тегу ничего не нашлось совсем
|
# Если тег не задан, или по тегу ничего не нашлось совсем
|
||||||
@@ -214,11 +234,14 @@ class IndexView(CommonContextMixin, TemplateView):
|
|||||||
active_qs = TbDictumAndQuotes.objects.all()
|
active_qs = TbDictumAndQuotes.objects.all()
|
||||||
|
|
||||||
# Случайная цитата (с учетом истории, чтобы главная страница тоже не зацикливалась)
|
# Случайная цитата (с учетом истории, чтобы главная страница тоже не зацикливалась)
|
||||||
dq = active_qs.exclude(id__in=seen_ids).order_by('?').first()
|
unseen_ids = list(active_qs.exclude(id__in=seen_ids).values_list('id', flat=True))
|
||||||
|
if unseen_ids:
|
||||||
if dq is None:
|
dq = active_qs.filter(id=unseen_ids[random.randint(0, len(unseen_ids) - 1)]).first()
|
||||||
|
else:
|
||||||
self.request.session['seen_ids'] = []
|
self.request.session['seen_ids'] = []
|
||||||
dq = active_qs.order_by('?').first()
|
all_ids = list(active_qs.values_list('id', flat=True))
|
||||||
|
if all_ids:
|
||||||
|
dq = active_qs.filter(id=all_ids[random.randint(0, len(all_ids) - 1)]).first()
|
||||||
|
|
||||||
if dq:
|
if dq:
|
||||||
# Используем миксин, ОБЯЗАТЕЛЬНО передаем active_qs
|
# Используем миксин, ОБЯЗАТЕЛЬНО передаем active_qs
|
||||||
|
|||||||
+31
-34
@@ -16,8 +16,7 @@ services:
|
|||||||
|
|
||||||
# 1. ОБРАЗ
|
# 1. ОБРАЗ
|
||||||
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||||
image: git.cube2.ru/erjemin/2020-dq:v1.0.7
|
image: git.cube2.ru/erjemin/2020-dq:latest
|
||||||
# image: git.cube2.ru/erjemin/2020-dq:latest
|
|
||||||
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||||
# build: .
|
# build: .
|
||||||
|
|
||||||
@@ -29,13 +28,14 @@ services:
|
|||||||
|
|
||||||
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
||||||
# Выполняем цепочку команд внутри контейнера при запуске:
|
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||||
# 1. Миграции
|
# a. Миграции
|
||||||
# 2. Collectstatic
|
# b. Collectstatic
|
||||||
# 3. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
# с. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||||
# 4. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем через переменную окружения HOST_PROJECT_PATH)
|
# d. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем
|
||||||
# 5. Инициализация боевого конфига (если нет)
|
# через переменную окружения HOST_PROJECT_PATH)
|
||||||
# 6. Создаем папку для ошибок и копируем туда статические страницы 404/500
|
# e. Инициализация боевого конфига (если нет)
|
||||||
# 7. Запуск Gunicorn
|
# f. Создаем папку для ошибок и копируем туда статические страницы 404/500 (там их увидит Nginx хоста)
|
||||||
|
# g. Запуск Gunicorn
|
||||||
command: >
|
command: >
|
||||||
sh -c "python manage.py migrate --noinput &&
|
sh -c "python manage.py migrate --noinput &&
|
||||||
python manage.py collectstatic --noinput &&
|
python manage.py collectstatic --noinput &&
|
||||||
@@ -48,12 +48,12 @@ services:
|
|||||||
mkdir -p /app/public/media/errors &&
|
mkdir -p /app/public/media/errors &&
|
||||||
cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
||||||
cp /home/app/web/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
|
cp /home/app/web/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
|
||||||
gunicorn --workers 2 --timeout 120 --bind 0.0.0.0:8010 dicquo.wsgi:application"
|
gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application"
|
||||||
|
|
||||||
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
||||||
ports:
|
ports:
|
||||||
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||||
- "127.0.0.1:8010:8010"
|
- "127.0.0.1:8010:8000"
|
||||||
|
|
||||||
# 5. Тома (Volumes)
|
# 5. Тома (Volumes)
|
||||||
volumes:
|
volumes:
|
||||||
@@ -69,16 +69,14 @@ services:
|
|||||||
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
- ./config:/nginx_configs_host
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
# 6.
|
# 6. Пользователь и права
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
|
||||||
# Для отладки
|
# Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root.
|
||||||
cap_add:
|
# cap_add:
|
||||||
- SYS_PTRACE
|
# - SYS_PTRACE
|
||||||
|
|
||||||
|
# 7. Переменные окружения
|
||||||
|
|
||||||
# 6. Переменные окружения
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -87,31 +85,30 @@ services:
|
|||||||
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
||||||
|
|
||||||
# 7. Проверка здоровья контейнера (Healthcheck)
|
# 8. Проверка здоровья контейнера (Healthcheck)
|
||||||
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
||||||
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
||||||
# healthcheck:
|
healthcheck:
|
||||||
# # test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8010/').read()"]
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
|
||||||
# test: ["CMD", "test", "-f", "/home/app/web/dicquo/gunicorn.ctl"]
|
interval: 30s # Проверка каждые 30 секунд
|
||||||
# interval: 300s # Проверка каждые 300 секунд
|
timeout: 3s # Таймаут ответа - 3 секунды
|
||||||
# timeout: 30s # Таймаут ответа - 30 секунды
|
start_period: 10s # Даем 10 секунд на стартап перед первой проверкой
|
||||||
# start_period: 80s # Даем 30 секунд на стартап перед первой проверкой
|
retries: 3 # Unhealthy после 3 неудачных попыток
|
||||||
# retries: 3 # Unhealthy после 3 неудачных попыток
|
|
||||||
|
|
||||||
# 8. Логирование (Ротация)
|
# 9. Логирование (Ротация)
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "2"
|
max-file: "3"
|
||||||
|
|
||||||
# 9. Ресурсы
|
# 10. Ресурсы
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: '0.50'
|
cpus: '0.40'
|
||||||
memory: 380M
|
memory: 256M
|
||||||
mem_limit: 380M
|
mem_limit: 256m
|
||||||
|
|
||||||
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
|
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
|
||||||
# Следит за реестром Gitea и обновляет контейнер web, если появился новый image
|
# Следит за реестром Gitea и обновляет контейнер web, если появился новый image
|
||||||
@@ -136,5 +133,5 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "1m"
|
max-size: "10m"
|
||||||
max-file: "1"
|
max-file: "3"
|
||||||
Reference in New Issue
Block a user