Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f96932a40 | |||
| d59c867010 | |||
| 9ea851ad95 | |||
| 5146f88c7d | |||
| 20ecb9cc4c | |||
| ea2d352cae | |||
| d459eedf41 | |||
| 9d31429e14 | |||
| c5963d1d30 | |||
| 12001bc749 | |||
| 99a2ace43f | |||
| 81efaf1ba5 | |||
| 42b378fcbc | |||
| ba4175dfdb | |||
| 4194b351d2 | |||
| be68a82927 | |||
| 53b127a966 | |||
| 746c50a988 | |||
| 2520362ad5 | |||
| a857101c3f | |||
| 7eeb44a1f5 | |||
| 86bfd9b07b |
@@ -63,3 +63,8 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# ДОБАВЛЕНО:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# И это для медленного интернета:
|
||||
timeout: 900 # 15 минут на всю сборку
|
||||
|
||||
126
Dockerfile
126
Dockerfile
@@ -1,63 +1,105 @@
|
||||
# ==========================================
|
||||
# Dockerfile для Django + Gunicorn + WhiteNoise
|
||||
# ==========================================
|
||||
# =================================================
|
||||
# STAGE 1: Builder - Установка зависимостей
|
||||
# =================================================
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
# 1. Базовый образ: Python 3.12 (Slim версия для меньшего размера)
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 2. Переменные окружения для Python
|
||||
# PYTHONDONTWRITEBYTECODE: Запрещает Python писать .pyc файлы
|
||||
# PYTHONUNBUFFERED: Гарантирует, что вывод консоли (logs) виден сразу (не буферизуется)
|
||||
# Устанавливаем переменные окружения
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
# Poetry настройки: не создавать виртуальное окружение внутри контейнера (ставим системно).
|
||||
# Дублирует `poetry config virtualenvs.create false` в пп.7 (на всякий случай).
|
||||
# Говорим Poetry, чтобы он не создавал venv, а ставил пакеты в системный site-packages
|
||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||
# Путь настройки Django (по умолчанию для production) на случай если контейнер будет запущен не через docker-compose.
|
||||
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||
|
||||
# 3. Рабочая директория внутри контейнера
|
||||
WORKDIR /app
|
||||
|
||||
# 4. Установка системных зависимостей
|
||||
# - libjpeg-dev zlib1g-dev: библиотеки для работы с изображениями (Pillow)
|
||||
# Устанавливаем системные зависимости, необходимые для СБОРКИ пакетов (например, Pillow)
|
||||
# build-essential нужен для компиляции, -dev пакеты для сборки Pillow
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libjpeg-dev \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 5. Установка Poetry через pip (быстро и надежно)
|
||||
# Устанавливаем Poetry
|
||||
RUN pip install --no-cache-dir poetry
|
||||
|
||||
# 6. Копируем файлы зависимостей (pyproject.toml и poetry.lock)
|
||||
# Делаем это ДО копирования всего кода, чтобы использовать кэш Docker layers.
|
||||
# Создаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем только файлы зависимостей для кэширования этого слоя
|
||||
COPY pyproject.toml poetry.lock /app/
|
||||
|
||||
# 7. Установка зависимостей проекта
|
||||
# --no-interaction: не будет спрашивать подтверждения
|
||||
# --no-ansi: уберваем цветные символы из логов сборки (они иногда мусорят)
|
||||
# --no-root: не устанавливать сам проект как пакет (мы просто копируем код)
|
||||
# --only main: не ставить dev-зависимости (тесты, линтеры и т.п.) для продакшена
|
||||
# RUN poetry install --no-root --only main
|
||||
# Настройка Poetry: не создавать venv и установка зависимостей (без dev-зависимостей для продакшена)
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-ansi --no-root --only main
|
||||
# Устанавливаем зависимости проекта. Poetry установит их в /usr/local/lib/python3.12/site-packages
|
||||
RUN poetry install --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
# 8. Копируем весь исходный код проекта в контейнер
|
||||
COPY . /app/
|
||||
|
||||
# 9. Сборка статики (CSS, JS)
|
||||
# Важно: Запускаем collectstatic с фейковым SECRET_KEY, так как на этапе сборки env файла может не быть.
|
||||
RUN SECRET_KEY=dummy_build_key python dicquo/manage.py collectstatic --noinput --clear
|
||||
# =================================================
|
||||
# STAGE 2: Final - Создание чистого и безопасного образа
|
||||
# =================================================
|
||||
FROM python:3.12-slim AS stage-final
|
||||
|
||||
# 10. Открываем порт 8000
|
||||
# Устанавливаем переменные окружения
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||
|
||||
# Устанавливаем только RUNTIME системные зависимости.
|
||||
# Пакеты -dev и build-essential здесь не нужны.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libjpeg62-turbo \
|
||||
sqlite3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Создаем пользователя без прав root для безопасности
|
||||
# RUN addgroup --system app && adduser --system --ingroup app app
|
||||
|
||||
# Создаем рабочую директорию
|
||||
WORKDIR /home/app/web
|
||||
|
||||
# Копируем установленные Python-пакеты из builder-стадии
|
||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||
|
||||
# Копируем исполняемые файлы (gunicorn, pip и т.д.)
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# Копируем исходный код проекта и устанавливаем правильного владельца
|
||||
# ИЗМЕНЕНИЕ: app:app -> 1000:1000
|
||||
COPY --chown=1000:1000 . .
|
||||
|
||||
# Создаём директорию для конфигов nginx и даём права пользователю app
|
||||
# Это выполняется ещё от root, поэтому проблем с permissions не будет.
|
||||
RUN mkdir -p /nginx_configs_host/nginx && chown -R 1000:1000 /nginx_configs_host
|
||||
|
||||
# Создаём директорию для собранной статики и даём права пользователю app
|
||||
RUN mkdir -p /home/app/web/staticfiles && chown -R 1000:1000 /home/app/web/staticfiles
|
||||
|
||||
# Создаём директорию для ошибок (404, 500) и даём права пользователю app
|
||||
RUN mkdir -p /app/public/media/errors && chown -R 1000:1000 /app/public/media
|
||||
|
||||
# Создаём директорию для БД и даём права пользователю app
|
||||
# Это важно когда БД монтируется как том с хоста
|
||||
RUN mkdir -p /app/database && chown -R 1000:1000 /app/database
|
||||
|
||||
# Переключаемся на пользователя без прав root
|
||||
USER 1000
|
||||
|
||||
|
||||
# Собираем статику
|
||||
# Используем dummy ключ, так как .env файла нет на этапе сборки
|
||||
RUN SECRET_KEY=dummy python dicquo/manage.py collectstatic --noinput --clear
|
||||
|
||||
# Открываем порт
|
||||
EXPOSE 8000
|
||||
|
||||
# 11. Команда запуска
|
||||
# Переходим в подпапку dicquo, где лежит код Django проекта
|
||||
WORKDIR /app/dicquo
|
||||
# Проверка здоровья контейнера
|
||||
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
|
||||
# Параметры:
|
||||
# --interval=30s - проверка каждые 30 секунд
|
||||
# --timeout=3s - ожидаем ответ максимум 3 секунды
|
||||
# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой
|
||||
# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1
|
||||
|
||||
# Запускаем Gunicorn (по умолчанию, если не переопределено в docker-compose) на три воркера, привязывая его к
|
||||
# порту 8000 и указывая на точку входа приложения (wsgi.py).
|
||||
# Переходим в директорию с manage.py для корректного запуска gunicorn
|
||||
WORKDIR /home/app/web/dicquo
|
||||
|
||||
# Команда запуска
|
||||
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "dicquo.wsgi:application"]
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# ВАЖНО: Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx
|
||||
# Даже если внутри контейнера это HTTP на 127.0.0.1:8010, для Django это должно быть HTTPS
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# Тайм-ауты (важно для долгих операций, если они есть)
|
||||
proxy_read_timeout 180s;
|
||||
|
||||
@@ -81,8 +81,10 @@ DATABASES = {
|
||||
'NAME': BASE_DIR.parent / 'database/db.sqlite3',
|
||||
'OPTIONS': {
|
||||
# Таймаут ожидания блокировки SQLite (в секундах)
|
||||
# При сложных операциях (например, каскадное удаление тегов) нужно больше времени
|
||||
'timeout': 20,
|
||||
# ВАЖНО: Увеличен до 60 сек для работы с несколькими воркерами Gunicorn
|
||||
'timeout': 60,
|
||||
# Дополнительные опции для лучшей работы SQLite при concurrent доступе
|
||||
'init_command': "PRAGMA journal_mode=WAL;", # Write-Ahead Logging для лучшей concurrency
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -154,3 +156,12 @@ if not DEBUG:
|
||||
WHITENOISE_ROOT = BASE_DIR.parent / 'public'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Настройки безопасности для работы за прокси
|
||||
if not DEBUG:
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
USE_X_FORWARDED_HOST = True
|
||||
USE_X_FORWARDED_PORT = True
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
{# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
|
||||
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
|
||||
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
|
||||
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
|
||||
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
|
||||
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
|
||||
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
|
||||
|
||||
@@ -7,6 +7,7 @@ from django_select2.forms import Select2TagWidget
|
||||
from taggit.models import Tag
|
||||
from taggit.utils import parse_tags
|
||||
from django.db import models
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
try:
|
||||
from etpgrf.typograph import Typographer
|
||||
@@ -34,8 +35,13 @@ class TagSelect2Widget(Select2TagWidget):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# choices: список всех существующих тегов по имени
|
||||
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
|
||||
# choices: список всех существующих тегов по имени.
|
||||
# Важно: на этапах вроде collectstatic таблицы taggit ещё может не быть,
|
||||
# поэтому оборачиваем в try/except и молча игнорируем отсутствие БД.
|
||||
try:
|
||||
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
|
||||
except (OperationalError, ProgrammingError):
|
||||
self.choices = []
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.2 on 2026-03-19 20:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0003_alter_tbdictumandquotes_btypograph_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='tbdictumandquotes',
|
||||
options={'ordering': ['-id'], 'verbose_name': 'ВЫСКАЗЫВАНИЕ', 'verbose_name_plural': 'ВЫСКАЗЫВАНИЯ'},
|
||||
),
|
||||
]
|
||||
@@ -38,14 +38,14 @@ services:
|
||||
sh -c "python manage.py migrate --noinput &&
|
||||
python manage.py collectstatic --noinput &&
|
||||
mkdir -p /nginx_configs_host/nginx &&
|
||||
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /app/configs/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
|
||||
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /nginx_configs_host/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
|
||||
if [ ! -f /nginx_configs_host/nginx/dq-app--external-nginx.conf ]; then
|
||||
cp /nginx_configs_host/nginx/nginx_dq.conf.example /nginx_configs_host/nginx/dq-app--external-nginx.conf;
|
||||
echo 'INIT: Created new nginx config with correct paths';
|
||||
fi &&
|
||||
mkdir -p /app/public/media/errors &&
|
||||
cp /app/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
||||
cp /app/dicquo/templates/static_500.html /app/public/media/errors/500.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 &&
|
||||
gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application"
|
||||
|
||||
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
||||
@@ -67,7 +67,10 @@ services:
|
||||
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||
- ./config:/nginx_configs_host
|
||||
|
||||
# 6. Переменные окружения
|
||||
# 6. Запускать от имени пользователя с UID 1000 и GID 1000
|
||||
user: "1000:1000"
|
||||
|
||||
# 7. Переменные окружения
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -76,14 +79,24 @@ services:
|
||||
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
||||
|
||||
# 7. Логирование (Ротация)
|
||||
# 8. Проверка здоровья контейнера (Healthcheck)
|
||||
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
||||
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
|
||||
interval: 30s # Проверка каждые 30 секунд
|
||||
timeout: 3s # Таймаут ответа - 3 секунды
|
||||
start_period: 10s # Даем 10 секунд на стартап перед первой проверкой
|
||||
retries: 3 # Unhealthy после 3 неудачных попыток
|
||||
|
||||
# 9. Логирование (Ротация)
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# 8. Ресурсы
|
||||
# 10. Ресурсы
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -107,6 +120,9 @@ services:
|
||||
- WATCHTOWER_SCOPE=dq-scope
|
||||
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
|
||||
- DOCKER_API_VERSION=1.44
|
||||
# Дополнительные опции для правильной работы с healthcheck
|
||||
- WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией
|
||||
- WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown
|
||||
command: --interval 1800 --cleanup # Проверять каждые 30 минут
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
||||
8
poetry.lock
generated
8
poetry.lock
generated
@@ -128,13 +128,13 @@ Django = ">=4.1"
|
||||
|
||||
[[package]]
|
||||
name = "etpgrf"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "etpgrf-0.1.4-py3-none-any.whl", hash = "sha256:62d4371e1b5fab06b99f79bd351767aed8baf7d041cae7e5d4eb63f7c9545114"},
|
||||
{file = "etpgrf-0.1.4.tar.gz", hash = "sha256:c699382c292e3110915331dd5539e7dde0c961e4f4ca65cf8db0e01e84dab72f"},
|
||||
{file = "etpgrf-0.1.6-py3-none-any.whl", hash = "sha256:a2d2a67048f094e1d30fe42f05f420afd19babe66ec7daa35b517ca23306d5cc"},
|
||||
{file = "etpgrf-0.1.6.tar.gz", hash = "sha256:a050c400a30be1c2379c892fc5fa398a79d15f0169094f00023a75dec01864af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -675,4 +675,4 @@ brotli = ["brotli"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"
|
||||
content-hash = "64c804553b6314e8f8f5830637781a3179fd70f14cceb6730bfcb2cf24c91a31"
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
# DicQuo
|
||||
User-Agent: *
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /*?tag=
|
||||
Disallow: /*?
|
||||
|
||||
# Optimize for Yandex
|
||||
Clean-param: tag /
|
||||
|
||||
# AI and LLM bots settings
|
||||
# OpenAI GPT
|
||||
# User-agent: GPTBot
|
||||
# Disallow:
|
||||
|
||||
# Common Crawl (used by many AI models)
|
||||
# User-agent: CCBot
|
||||
# Disallow:
|
||||
|
||||
# Google Bard/Gemini
|
||||
# User-agent: Google-Extended
|
||||
# Disallow:
|
||||
|
||||
Host: dq.cube2.ru
|
||||
Sitemap: https://dq.cube2.ru/sitemap.xml
|
||||
|
||||
# Ссылка на файл для ИИ-моделей
|
||||
Link: /llms.txt
|
||||
@@ -306,12 +306,17 @@ footer button:hover {
|
||||
|
||||
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
|
||||
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
|
||||
.etp-laquo {margin-left: -0.44em;} /* « */
|
||||
.etp-ldquo, .etp-bdquo { margin-left: -0.4em;} /* “ „ */
|
||||
.etp-lsquo {margin-left: -0.22em;} /* ‘ */
|
||||
.etp-lpar, .etp-lsqb, .etp-lcub {margin-left: -0.25em;}/* ( [ { */
|
||||
.etp-laquo { margin-left: -0.49em; } /* « */
|
||||
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ “ */
|
||||
.etp-lsquo { margin-left: -0.22em; } /* ’ */
|
||||
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.23em; } /* ( [ { */
|
||||
/* компенсирующие пробелы для левых висячих символов */
|
||||
.etp-sp-laquo { padding-right: 0.49em; }
|
||||
.etp-sp-ldquo, .etp-sp-bdquo { padding-right: 0.4em; }
|
||||
.etp-sp-lsquo { padding-right: 0.22em; }
|
||||
.etp-sp-lpar, .etp-sp-lsqb, .etp-sp-lcub { padding-right: 0.23em; }
|
||||
|
||||
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
|
||||
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) top.mail.ru и Яндекс.Метрика --- */
|
||||
.counter-pixel {
|
||||
border: 0;
|
||||
position: absolute;
|
||||
|
||||
@@ -12,7 +12,7 @@ django = "^6.0.2"
|
||||
django-taggit = "^6.1.0"
|
||||
pillow = "^12.1.1"
|
||||
pytils = "^0.4.4"
|
||||
etpgrf = "^0.1.4"
|
||||
etpgrf = "0.1.6"
|
||||
django-environ = "^0.12.1"
|
||||
whitenoise = "^6.11.0"
|
||||
gunicorn = "^25.1.0"
|
||||
|
||||
Reference in New Issue
Block a user