Merge remote-tracking branch 'origin/2026-rework' into 2026-rework

# Conflicts:
#	docker-compose.prod.yml
This commit is contained in:
2026-07-06 12:30:07 +03:00
2 changed files with 65 additions and 45 deletions
+34 -11
View File
@@ -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
View File
@@ -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"