Compare commits
17 Commits
v1.0.0
...
2026-rewor
| Author | SHA1 | Date | |
|---|---|---|---|
| 86bfd9b07b | |||
| c3c81d7ff5 | |||
| f4cce3d08a | |||
| 45275c51f6 | |||
| f2f98d9229 | |||
| a33b517a3e | |||
| d4624e7761 | |||
| a608dea61f | |||
| 5bfd50efd5 | |||
| c1bcb2895d | |||
| 9ea2b15043 | |||
| 5a80cf6406 | |||
| 915c286e81 | |||
| c46b6c1061 | |||
| b2a26a9dcc | |||
| bd5cdcd870 | |||
| db6cbb7bdf |
@@ -9,6 +9,11 @@ SECRET_KEY='change_me_in_production'
|
||||
# Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение.
|
||||
ALLOWED_HOSTS=127.0.0.1,localhost
|
||||
|
||||
# CSRF Trusted Origins (ВАЖНО для Docker/Nginx/SSL).
|
||||
# Перечислите здесь URL, по которым вы заходите на сайт, включая схему (http/https).
|
||||
# Если этого не сделать, при попытке залогиниться в админку вы получите ошибку CSRF.
|
||||
CSRF_TRUSTED_ORIGINS=https://dq.cube2.ru,http://127.0.0.1:8010,http://localhost:8010
|
||||
|
||||
# Email администратора (для получения уведомлений о критических ошибках и других важных сообщений от Django).
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
|
||||
|
||||
45
PLANS.md
Normal file
45
PLANS.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Планы по развитию проекта (DicQuo)
|
||||
|
||||
## 1. Список Авторов (Feature: Authors List)
|
||||
**Цель:** Улучшить SEO (плоская структура) и навигацию, сохранив "Дзен" (минимализм).
|
||||
|
||||
**Концепция:**
|
||||
- Добавить иконку "Люди/Авторы" в шапку сайта (рядом с бургером).
|
||||
- По клику открывается **полноэкранный оверлей** (как статистика).
|
||||
- Внутри список авторов карточками/строками.
|
||||
|
||||
**Элементы списка:**
|
||||
1. **Имя Автора** (крупно) -> Ссылка на ротацию цитат автора (`/?tag=author-slug`).
|
||||
2. **Счетчик цитат** (мелко, например `(25)`) -> Клик раскрывает "гармошку" (аккордеон).
|
||||
3. **Список цитат** (внутри гармошки) -> Прямые ссылки на цитаты (например: `/123_nachalo-frazy...`). Текст ссылок — начало фразы.
|
||||
|
||||
**Техническая реализация:**
|
||||
- **Backend:** `Context Processor` или логика в `IndexView` (или отдельный AJAX endpoint) для сбора данных:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Имя",
|
||||
"slug": "slug",
|
||||
"count": 25,
|
||||
"quotes": [{"id": 1, "url": "...", "text": "Текст..."}, ...]
|
||||
}, ...
|
||||
]
|
||||
```
|
||||
- **Frontend:** HTML/CSS для модального окна и JS для раскрытия списков.
|
||||
|
||||
## 2. Админка
|
||||
- Починить мелкие баги в управлении тегами.
|
||||
- Улучшить управление настройками типографа (etpgrf) через виртуальные поля.
|
||||
- Поля в админке для настройки (кавычки, неразрывные пробелы и т.д.).
|
||||
- При сохранении применять типограф к полям `szContent` -> `szContentHTML`.
|
||||
- `szContentHTML` сделать редактитруемым чекрез CodeMirror (для ручной типографики тяжёлых случаев).
|
||||
|
||||
## 3. SEO и Оптимизация
|
||||
- Проверить индексацию новых страниц `static_404`/`static_500`.
|
||||
- Убедиться, что `canonical` ссылки работают корректно.
|
||||
|
||||
## 4. Дальние планы
|
||||
- Форма для добавления цитат пользователями (с модерацией).
|
||||
- API для интеграции с внешними сервисами (магазинами грампластинок и музыкальными сервисами).
|
||||
- Сбор цитат из открытых источников (например, с помощью парсинга сайтов с цитатами или API).
|
||||
|
||||
132
README.md
132
README.md
@@ -1,16 +1,130 @@
|
||||
# DicQuo (цитаты и высказывания)
|
||||
# DicQuo (Цитаты, Афоризмы и Факты)
|
||||
|
||||
Пет-проект ротации цитат и высказываний. Развернут на [dq.cube2.ru](https://dq.cube2.ru).
|
||||
**Dicquo** — это коллекция отобранных вручную цитат, оформленных с уважением к типографике. Место для вдумчивого чтения, переосмысления и поиска вдохновения. Проект создан как пространство, где типографика встречается со смыслом, а технологии помогают контенту выглядеть безупречно.
|
||||
|
||||
Изначальная цель:
|
||||
* Испытывать различные типографы (библиотеки и API), разрабатывать и тестировать свои
|
||||
типографы и правила типографирования.
|
||||
* Испытать, как содержание отдельных атрибуты (meta, keywords, description и т.п.)
|
||||
влияют на поисковую выдачу.
|
||||
Основные цели проекта:
|
||||
* **Типографика как искусство:** Разработка и тестирование собственных алгоритмов типографирования (висячая пунктуация, неразрывные пробелы, правильные тире) из библиотеки `etpgrf` (доступен в [GitHub](https://github.com/erjemin/etpgrf), [GitVerse](https://gitverse.ru/erjemin/etpgrf) и self-hosted [Cube2](https://git.cube2.ru/erjemin/2025-etpgrf), онлайн версия развёрнута на [typograph.cube2.ru](https://typograph.cube2.ru/)).
|
||||
* **SEO-эксперименты:** Исследование влияния микроразметки, мета-тегов и семантической верстки на индексацию поисковыми системами.
|
||||
* **Технологический стек:** Современный Django, Docker, CI/CD и автоматизация деплоя.
|
||||
|
||||
[Инструкция по развертыванию на хостинге c CGI Passenger](deploy_to_dreamhost.md)
|
||||
Развернут на [dq.cube2.ru](https://dq.cube2.ru).
|
||||
|
||||
## ToDo?
|
||||
---
|
||||
|
||||
## Структура файлов на сервере (Production)
|
||||
|
||||
После правильного развертывания, папка проекта на сервере (например, `~/docker-apps/dicquo/`) должна выглядеть так:
|
||||
|
||||
```text
|
||||
dicquo/
|
||||
├── docker-compose.yml # Переименованный docker-compose.prod.yml из этого репозитория (запускает контейнеры)
|
||||
├── .env # Файл с переменными окружения и секретами (не пушить в git!)
|
||||
├── database/ # Папка для базы данных (Persistent Volume)
|
||||
│ ├── db.sqlite3 # Фактический файл базы данных
|
||||
├── media/ # Папка с медиа-файлами (Persistent Volume)
|
||||
│ ├── img2/ # Картинки для цитат (доступны через Nginx)
|
||||
│ └── errors/ # Картинки для страниц ошибок (404, 500)
|
||||
└── config/ # Папка для конфигураций (Генерируется контейнером)
|
||||
└── nginx/
|
||||
├── dq-app--external-nginx.conf # Конфиг для внешнего Nginx (скопируется из контейнера при первом запуске)
|
||||
└── nginx_dq.conf.example # Образец конфига для внешнего Nginx (не используется в продакшене)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Развёртывание (Deployment)
|
||||
|
||||
Проект полностью упакован в Docker и разворачивается с помощью `docker compose`. Ниже приведена инструкция для развертывания на чистом Linux-сервере (Ubuntu/Debian, архитектуры AMD/ARM).
|
||||
|
||||
### 1. Подготовка структуры
|
||||
|
||||
Создайте директорию для проекта (например, в домашней папке пользователя) и необходимые подпапки для persistent-данных:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/docker-apps/dicquo
|
||||
cd ~/docker-apps/dicquo
|
||||
mkdir -p database media config
|
||||
```
|
||||
|
||||
### 2. Файлы конфигурации
|
||||
|
||||
Вам понадобятся два файла из репозитория (или их содержимое):
|
||||
1. **`docker-compose.prod.yml`** -> сохраните его на сервере как `docker-compose.yml`.
|
||||
2. **`.env`** -> создайте на основе `.env.sample`, заполнив секретами для продакшена.
|
||||
|
||||
**Важные переменные в `.env`:**
|
||||
* `HOST_PROJECT_PATH`: Полный путь к папке проекта на хосте (например, `/home/username/docker-apps/dicquo`). Используется для корректной генерации конфига Nginx.
|
||||
* `DJANGO_ALLOWED_HOSTS`: Список доменов через запятую (например, `dq.cube2.ru,127.0.0.1`).
|
||||
|
||||
### 3. Перенос данных (Опционально)
|
||||
|
||||
Если вы мигрируете с dev-окружения или другого сервера, можно просто скопировать файлы базы данных и медиа.
|
||||
|
||||
Скопируйте файл базы `database/db.sqlite3` и содержимое папки `media/` в соответсвующие папки на сервере.
|
||||
|
||||
### 4. Настройка прав доступа (Permissions) ⚠️
|
||||
|
||||
Это **критически важный этап**.
|
||||
1. Docker-контейнер (с нашим бэкендом Django и Gunicorn) должн иметь доступ к примонтированным папкам.
|
||||
2. Внешний Nginx (на хосте) должен иметь доступ к статике и медиа.
|
||||
|
||||
**Права на папки проекта:**
|
||||
```bash
|
||||
# Разрешаем запись в базу и медиа для всех (самый простой способ избежать проблем с UID внутри Docker)
|
||||
sudo chmod 777 database
|
||||
sudo chmod 666 database/db.sqlite3
|
||||
sudo chmod -R 755 media
|
||||
```
|
||||
|
||||
**Права на родительские директории (Pass-through):**
|
||||
Если проект лежит в домашней папке пользователя (`/home/username/...`), то Nginx (пользователь `www-data`) по умолчанию **не сможет** туда попасть. Нужно разрешить "проход" (execute) для всех пользователей по пути к проекту:
|
||||
|
||||
```bash
|
||||
# Разрешаем "проход" через домашнюю папку (чтение файлов при этом остается закрытым, только доступ к известным путям)
|
||||
chmod o+x /home/username
|
||||
chmod o+x /home/username/docker-apps
|
||||
chmod o+x /home/username/docker-apps/dicquo
|
||||
```
|
||||
> *Без этого шага Nginx будет выдавать 403 Forbidden на картинки и статику.*
|
||||
|
||||
### 5. Запуск
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
При первом запуске контейнер автоматически:
|
||||
* Применит миграции.
|
||||
* Соберет статику.
|
||||
* Сгенерирует конфиг для Nginx в папке `config/nginx/`.
|
||||
|
||||
### 6. Настройка Nginx и SSL
|
||||
|
||||
1. **Подключение конфига:**
|
||||
Создайте симлинк на сгенерированный конфиг:
|
||||
```bash
|
||||
sudo ln -s /home/username/docker-apps/dicquo/config/nginx/dq-app--external-nginx.conf /etc/nginx/sites-enabled/dq-app.conf
|
||||
```
|
||||
2. **Проверка и релоад:**
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
3. **Получение SSL сертификата (Certbot):**
|
||||
```bash
|
||||
sudo certbot --nginx -d dq.cube2.ru
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Разработка (Dev)
|
||||
|
||||
Для локального запуска используется `docker-compose.yml` (он же dev-версия).
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
Проект будет доступен по адресу: http://127.0.0.1:8008
|
||||
|
||||
## ToDo
|
||||
|
||||
* В будущем, возможно, сделать API для предоставления цитат вешним потребителям
|
||||
(по темам, авторам и т.п.).
|
||||
|
||||
@@ -76,18 +76,24 @@ server {
|
||||
}
|
||||
|
||||
# --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
|
||||
# Если Django упал (502) или файл из media не найден Nginx-ом (404), показываем наши красивые заглушки.
|
||||
# Файлы копируются в media/errors при старте контейнера.
|
||||
# ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/dq-site -> ваш реальный путь
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /500.html;
|
||||
# Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
|
||||
# Эти файлы должны лежать в папке, доступной Nginx (например, в media/errors).
|
||||
#
|
||||
# ВАЖНО:
|
||||
# 1. Файлы 50x.html (500, 502, 503, 504) копируются в media/errors при старте контейнера (см. docker-compose.prod.yml -> command).
|
||||
# 2. error_page директива перехватывает ошибки от апстрима (Gunicorn).
|
||||
|
||||
location = /404.html {
|
||||
error_page 500 502 503 504 /500.html;
|
||||
# (Опционально) 404 тоже можно кастомизировать, но обычно Django сам отдает 404.
|
||||
# Nginx отдаст эту страницу только если сам не найдет статику.
|
||||
error_page 404 /404.html;
|
||||
|
||||
location = /500.html {
|
||||
root /home/user/app/dq-site/media/errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
location = /500.html {
|
||||
location = /404.html {
|
||||
root /home/user/app/dq-site/media/errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,18 @@ env = environ.Env(
|
||||
# If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env
|
||||
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env('DEBUG')
|
||||
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost'])
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=['http://127.0.0.1', 'http://localhost'])
|
||||
|
||||
# Custom Admin URL from .env
|
||||
ADMIN_URL = env('ADMIN_URL', default='admin/')
|
||||
|
||||
#########################################
|
||||
# Настройки сообщений об ошибках когда все упало и т.п.
|
||||
@@ -55,6 +58,7 @@ INSTALLED_APPS: list[str] = [
|
||||
'django.contrib.sites',
|
||||
'django.contrib.sitemaps',
|
||||
'taggit.apps.TaggitAppConfig',
|
||||
'django_select2',
|
||||
'web.apps.WebConfig',
|
||||
]
|
||||
|
||||
@@ -75,6 +79,11 @@ DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR.parent / 'database/db.sqlite3',
|
||||
'OPTIONS': {
|
||||
# Таймаут ожидания блокировки SQLite (в секундах)
|
||||
# При сложных операциях (например, каскадное удаление тегов) нужно больше времени
|
||||
'timeout': 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ Including another URLconf
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path, re_path, include
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.views.generic import TemplateView
|
||||
from dicquo import settings
|
||||
from django.conf import settings
|
||||
from web import views
|
||||
from web.sitemaps import DictumSitemap
|
||||
|
||||
@@ -28,11 +28,12 @@ sitemaps = {
|
||||
}
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
re_path(f'^{settings.ADMIN_URL}', admin.site.urls),
|
||||
|
||||
re_path(r'^$', views.IndexView.as_view()),
|
||||
re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.as_view()),
|
||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
|
||||
path("select2/", include("django_select2.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
@@ -42,4 +43,7 @@ if settings.DEBUG:
|
||||
path('500/', TemplateView.as_view(template_name="500.html")),
|
||||
path('403/', TemplateView.as_view(template_name="403.html")),
|
||||
path('400/', TemplateView.as_view(template_name="400.html")),
|
||||
# Для проверки статических страниц ошибок (Nginx)
|
||||
path('static_404/', TemplateView.as_view(template_name="static_404.html")),
|
||||
path('static_500/', TemplateView.as_view(template_name="static_500.html")),
|
||||
]
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block Title %}400: Плохой запрос{% endblock %}
|
||||
|
||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
||||
<div class="container main-content">
|
||||
<!-- Осно<D0BD><D0BE>ной контент: Текст + Картинка -->
|
||||
<div class="content-row">
|
||||
|
||||
<!-- Текстовая колонка -->
|
||||
<div class="text-col">
|
||||
<!-- Цитата -->
|
||||
<blockquote id="bb" style="border:none; margin:0; padding:0;">
|
||||
<span style="margin-left:-0.44em;">«</span>Вы спрашиваете меня о чем-то странном. Я не понимаю ваш запрос.»
|
||||
</blockquote>
|
||||
|
||||
<!-- Автор -->
|
||||
<div id="author">
|
||||
<cite>Озадаченный Сервер (400)</cite>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Блок тегов и навигации -->
|
||||
<div class="tags">
|
||||
<a href="/">Сформулировать иначе (на главную)</a>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>400: Плохой запрос | DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Загадочно:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Вы спрашиваете меня о чем-то странном. Я не понимаю
|
||||
ваш запрос.»
|
||||
</blockquote>
|
||||
<cite>Озадаченный Сервер (400)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="/">Сформулировать иначе (на главную)</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block Title %}403: Доступ запрещен{% endblock %}
|
||||
|
||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
||||
<div class="container main-content">
|
||||
<!-- Основной контент: Текст + Картинка -->
|
||||
<div class="content-row">
|
||||
|
||||
<!-- Текстовая колонка -->
|
||||
<div class="text-col">
|
||||
<!-- Цитата -->
|
||||
<blockquote id="bb" style="border:none; margin:0; padding:0;">
|
||||
<span style="margin-left:-0.44em;">«</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
|
||||
</blockquote>
|
||||
|
||||
<!-- Автор -->
|
||||
<div id="author">
|
||||
<cite>Строгий Вахтёр (403)</cite>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Блок тегов и навигации -->
|
||||
<div class="tags">
|
||||
<a href="/">Уйти по-добру по-здорову</a>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>403: Доступ запрещен | DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Категорически:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
|
||||
</blockquote>
|
||||
<cite>Строгий Вахтёр (403)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="/">Уйти по-добру по-здорову</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block Title %}404: Страница не найдена{% endblock %}
|
||||
|
||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
||||
<div class="container main-content">
|
||||
<!-- Основной контент: Текст + Картинка -->
|
||||
<div class="content-row">
|
||||
|
||||
<!-- Текстовая колонка -->
|
||||
<div class="text-col">
|
||||
<!-- Цитата -->
|
||||
<blockquote id="bb" style="border:none; margin:0; padding:0;">
|
||||
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||
</blockquote>
|
||||
|
||||
<!-- Автор -->
|
||||
<div id="author">
|
||||
<cite>Системный Администратор (404)</cite>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Блок тегов и навигации -->
|
||||
<div class="tags">
|
||||
<a href="/">Вернуться на главную</a>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>404: Страница не найдена | DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Вздыхая:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||
</blockquote>
|
||||
<cite>Системный Администратор (404)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="/">Вернуться на главную</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block Title %}500: Ошибка сервера{% endblock %}
|
||||
|
||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
||||
<div class="container main-content">
|
||||
<!-- Основной контент: Текст + Картинка -->
|
||||
<div class="content-row">
|
||||
|
||||
<!-- Текстовая колонка -->
|
||||
<div class="text-col">
|
||||
<!-- Цитата -->
|
||||
<blockquote id="bb" style="border:none; margin:0; padding:0;">
|
||||
<span style="margin-left:-0.44em;">«</span>Что-то пошло не так. Кажется, я уронил сервер. Подождите, пока я его подниму.»
|
||||
</blockquote>
|
||||
|
||||
<!-- Автор -->
|
||||
<div id="author">
|
||||
<cite>Системный Администратор (500)</cite>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Блок тегов и навигации -->
|
||||
<div class="tags">
|
||||
<a href="/">Попробовать обновить страницу</a>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>500: Ошибка сервера DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Неожиданно:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||
Подождите, пока я его подниму.»
|
||||
</blockquote>
|
||||
<cite>Системный Администратор (500)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="/">Попробовать обновить страницу</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,58 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
{% load static %}<html lang="ru">
|
||||
<!DOCTYPE html>{% load static %}
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- SEO & Meta -->
|
||||
<title>{% block Title %}{% endblock %}</title>
|
||||
<meta name="description" content="{% block Description %}{% endblock %}" />
|
||||
<meta name="keywords" content="{% block Keywords %}{% endblock %}" />
|
||||
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}." />
|
||||
<meta name="robots" content="index,follow" />
|
||||
|
||||
<!-- Open Graph / Social Media -->
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:60 }}{% endblock %}" />
|
||||
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}" />
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
|
||||
<meta property="og:site_name" content="DicQuo" />
|
||||
{% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}" />{% endif %}
|
||||
|
||||
<!-- Technical Meta -->
|
||||
<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}" />
|
||||
<meta name="generator" content="Django" />
|
||||
|
||||
<!-- 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' %}" />
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="{% static 'css/dicquo.css' %}" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background-color: #111; /* Изначально темный фон */
|
||||
opacity: 0; /* Скрываем контент до расчета цвета */
|
||||
transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style>body { opacity: 1; }</style>
|
||||
</noscript>
|
||||
|
||||
{% block ExtraHead %}{% endblock %}
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{# SEO & Meta #}<title>{% block Title %}{% endblock %}</title>
|
||||
<meta name="description" content="{% block Description %}{% endblock %}"/>
|
||||
<meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
|
||||
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}."/>
|
||||
<meta name="robots" content="index,follow"/>
|
||||
{# Open Graph / Social Media #}<meta property="og:type" content="article"/>
|
||||
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:85 }}{% endblock %}"/>
|
||||
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}"/>
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}"/>
|
||||
<meta property="og:site_name" content="DicQuo"/>
|
||||
{% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}"/>{% endif %}
|
||||
{# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
|
||||
{# Canonical #}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>
|
||||
{# 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 %}"/>
|
||||
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
|
||||
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
|
||||
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if DQ %}
|
||||
<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% block CONTENT %}{% endblock %}
|
||||
{% include "blocks/counters.html" %}
|
||||
<body>{% if DQ %}
|
||||
{# Этот блок для передачи JavaScript-скрипту `bg-generator.js` текст цитаты ({{ DQ.szContent }}) и скрипт на основе него делает уникальный, но постоянный для этой цитаты фон (градиент) #}<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }} ({% if AUTHOR %}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor }}{% endif %})</span>{% endif %}
|
||||
{% include "blocks/header_nav.html" %}
|
||||
{% block CONTENT %}{% endblock %}{% if not cookie_accept %}
|
||||
{% include "blocks/cookie_warning.html" %}{% endif %}
|
||||
<script src="{% static 'js/bg-generator.js' %}"></script>
|
||||
{% include "blocks/counters.html" %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,6 @@
|
||||
<!-- ПОДВАЛ: НАЧАЛО -- соглашение о сборе технической информации -->
|
||||
<div name="cookies_accept">
|
||||
<small>Тут используют cookie и ведут сбор технических данных о посещениях, потому как без этого <nobr>интернет-сайты</nobr> вообще почти <nobr>не работают…</small>
|
||||
<button onclick="CookieAcceptDate = new Date();
|
||||
CookieAcceptDate.setTime(CookieAcceptDate.getTime() + 7948800000);
|
||||
document.cookie = 'cookie_accept=yes;expires=' + CookieAcceptDate;
|
||||
document.getElementsByName('cookies_accept')[0].remove();">
|
||||
Я согласен!
|
||||
</button></nobr>
|
||||
</div>
|
||||
<!-- ПОДВАЛ: КОНЕЦ -->
|
||||
<footer data-nosnippet>
|
||||
<!--noindex-->
|
||||
<small>Следы на песке смывает волна, но цифровые следы (cookie) помогают нам помнить вас, пока вы здесь.</small>
|
||||
<button>Осознаю</button>
|
||||
<!--/noindex-->
|
||||
</footer>
|
||||
@@ -1,4 +1,2 @@
|
||||
<!-- Rating Mail.ru counter -->
|
||||
<script type="text/javascript">var _tmr = window._tmr || (window._tmr = []);_tmr.push({id:"1603042",type:"pageView",start:(new Date()).getTime()});(function(d,w,id){if(d.getElementById(id)) return;var ts=d.createElement("script");ts.type="text/javascript";ts.async=true;ts.id=id;ts.src="https://top-fwz1.mail.ru/js/code.js";var f=function(){var s=d.getElementsByTagName("script")[0];s.parentNode.insertBefore(ts,s);};if(w.opera == "[object Opera]"){ d.addEventListener("DOMContentLoaded",f,false);}else{f();}})(document,window,"topmailru-code");</script><noscript><div><img src="https://top-fwz1.mail.ru/counter?id=1603042;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div></noscript>
|
||||
<!-- //Rating Mail.ru counter -->
|
||||
|
||||
{% load static %}<script src="{% static 'js/counters.js' %}"></script>
|
||||
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3744288;js=na" class="counter-pixel" alt=""/></div><div><img src="https://mc.yandex.ru/watch/106953063" class="counter-pixel" alt="" /></div></noscript>
|
||||
@@ -1,25 +1,15 @@
|
||||
{% load static %}
|
||||
<!-- ШАПКА: НАЧАЛО -->
|
||||
<div class="container">
|
||||
<header>
|
||||
<a href="/" id="logo">
|
||||
<img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes"
|
||||
width="50" height="46"/>
|
||||
</a>
|
||||
<div>
|
||||
<span id="stats-menu" style="display: none; color: silver; font-size: 0.9em; margin-right: 15px; text-align: right;">
|
||||
<!-- Манифест проекта -->
|
||||
<div style="margin-bottom: 5px; color: #aaa; font-style: italic; max-width: 300px; display: inline-block;">
|
||||
Dicquo — это коллекция отобранных вручную цитат, оформленных с уважением к типографике. Место для вдумчивого чтения.
|
||||
</div>
|
||||
<br/>
|
||||
<!-- Статистика -->
|
||||
{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:1 }}ms{% endif %}
|
||||
{% if DQ %} | <i class="stats-icon icon-views" title="Просмотры"></i>{{ DQ.iViewCounter }}{% endif %}
|
||||
| <a href="/add_quote/" style="color: silver; text-decoration: none;" title="Добавить цитату"><i class="stats-icon icon-add"></i></a> |
|
||||
</span>
|
||||
<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" style="color: silver; text-decoration: none; font-size: 1.2em;">≡</a>
|
||||
</div>
|
||||
{% load static %}{# ШАПКА #}<header>
|
||||
<a href="/" id="logo">
|
||||
<img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes"/>
|
||||
</a>
|
||||
<nav>
|
||||
<span id="stats-menu">
|
||||
{# Манифест проекта #}<p>
|
||||
DicQuo — это коллекция отобранных вручную цитат, оформленных с уважением к типографике.<br/>
|
||||
Место для вдумчивого чтения.
|
||||
</p>
|
||||
{# МЕНЮ #}{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:3|default:"1,023" }} ms{% endif %}{% if DQ %} <b>|</b> <i class="stats-icon icon-views" title="Просмотры"></i> {{ DQ.iViewCounter }}{% endif %} <a href="/add_quote/" title="Добавить цитату">+</a>
|
||||
</span>
|
||||
{# БУРГЕР #}<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" title="О проекте">≡</a>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
<!-- ШАПКА: КОНЕЦ -->
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: НАЧАЛО -->
|
||||
<div style="bottom:0;" class="position-sticky float-right fixed-bottom">
|
||||
<small style="background:#674376;color: white;font-size: xx-small;"
|
||||
class="x"> 🕗 {{ ticks|stringformat:".6f" }} s <nobr>({% now 'c' %})</nobr> </small>
|
||||
</div>
|
||||
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: КОНЕЦ -->
|
||||
@@ -13,84 +13,53 @@
|
||||
<!--- ТИТУЛ --->
|
||||
{% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %}
|
||||
|
||||
{% block ExtraHead %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Quotation",
|
||||
"name": "Цитата #{{ DQ.id }}",
|
||||
"text": "{{ DQ.szContent|escapejs }}",
|
||||
"creator": {
|
||||
"@type": "Person",
|
||||
"name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}"
|
||||
},
|
||||
"url": "{{ request.build_absolute_uri }}",
|
||||
{% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %}
|
||||
"keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}",
|
||||
"inLanguage": "ru",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "Dicquo",
|
||||
"url": "{{ request.scheme }}://{{ request.get_host }}"
|
||||
},
|
||||
"dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}",
|
||||
"dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}"
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block ExtraHead %}<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Quotation",
|
||||
"name": "Цитата #{{ DQ.id }}",
|
||||
"text": "{{ DQ.szContent|escapejs }}",
|
||||
"creator": {
|
||||
"@type": "Person",
|
||||
"name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}"
|
||||
},
|
||||
"url": "{{ request.build_absolute_uri }}",
|
||||
{% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %}
|
||||
"keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}",
|
||||
"inLanguage": "ru",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "Dicquo",
|
||||
"url": "{{ request.scheme }}://{{ request.get_host }}"
|
||||
},
|
||||
"dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}",
|
||||
"dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}"
|
||||
}
|
||||
</script>{% endblock %}
|
||||
|
||||
|
||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
||||
<div class="container main-content">
|
||||
<!-- Основной контент: Текст + Картинка -->
|
||||
<div class="content-row">
|
||||
|
||||
<!-- Текстовая колонка -->
|
||||
<div class="text-col">
|
||||
<!-- Интро/Вступление -->
|
||||
{% if DQ.szIntroHTML %}
|
||||
<div id="info">{{ DQ.szIntroHTML|safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Цитата: Семантический blockquote -->
|
||||
<blockquote id="bb" style="border:none; margin:0; padding:0;">
|
||||
{{ DQ.szContentHTML|safe }}
|
||||
</blockquote>
|
||||
|
||||
<!-- Автор: Семантический cite -->
|
||||
<div id="author">
|
||||
<cite>
|
||||
{% if AUTHOR %}
|
||||
{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}
|
||||
{% endif %}
|
||||
</cite>
|
||||
{% block CONTENT %}<main>{# Основной контент: Текст + Картинка #}
|
||||
<article>{# Текстовая ряб. Задает высоту двум колонкам: текст и картина #}
|
||||
<figure>{# КОЛОНКА С ТЕКСТОМ #}{% if DQ.szIntroHTML %}
|
||||
{# Интро/Вступление (например "Вася Пупкин как-то сказа". Может отсутствовать #}<p>{{ DQ.szIntroHTML|safe }}</p>{% endif %}
|
||||
<blockquote>{{ DQ.szContentHTML|safe }}</blockquote>{% if AUTHOR %}
|
||||
<cite>{# Автор #}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}</cite>{% endif %}
|
||||
</figure>{% if IMAGE %}
|
||||
<div>{# КОЛОНКА С КАРТИНКОЙ #}
|
||||
<div style="background:rgba(87,0,0,0.7);">
|
||||
<div>
|
||||
<img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Колонка с картинкой (если есть) -->
|
||||
{% if IMAGE %}
|
||||
<div class="image-col" id="image">
|
||||
<center>
|
||||
<div style="background:rgba(87,0,0,0.7);">
|
||||
<div><img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" /></div>
|
||||
</div>
|
||||
</center>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Блок тегов и навигации -->
|
||||
<div class="tags">
|
||||
{% for i in TAGS %}<a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
|
||||
</div>{% endif %}
|
||||
</article>
|
||||
</main>
|
||||
<nav>
|
||||
<div>{# ТЕГИ #}{% for i in TAGS %}
|
||||
<a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
|
||||
<div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">→</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
|
||||
</noscript>
|
||||
|
||||
|
||||
{% if not cookie_accept %}{% include "blocks/cookie_warning.html" %}{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</nav>
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
|
||||
</noscript>{% endblock %}
|
||||
@@ -1,27 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</div>
|
||||
<a href="/">Вернуться на главную</a>
|
||||
<cite>Системный Администратор (404)</cite>
|
||||
</blockquote>
|
||||
<span>«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||
<blockquote>
|
||||
<div class="container">
|
||||
<body>
|
||||
</head>
|
||||
</style>
|
||||
a:hover { color: #999; border-bottom: 1px solid #999; }
|
||||
a { color: #555; text-decoration: none; border-bottom: 1px dotted #555; transition: color 0.3s; font-size: 0.8em; margin-top: 30px; display: inline-block;}
|
||||
cite { display: block; font-size: 0.9em; color: #777; margin-top: 15px; font-style: normal;}
|
||||
blockquote span { margin-left: -0.44em; }
|
||||
blockquote { font-size: 2em; margin: 0 0 20px 0; font-style: italic; line-height: 1.4; }
|
||||
.container { max-width: 600px; padding: 20px; }
|
||||
body { background-color: #111; color: #ccc; font-family: Georgia, serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }
|
||||
<style>
|
||||
<title>404: Страница не найдена</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<head>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>404: Страница не найдена | DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Озадачено:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||
</blockquote>
|
||||
<cite>Системный Администратор (404)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="/">Вернуться на главную</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>500: Ошибка сервера</title>
|
||||
<style>
|
||||
body { background-color: #111; color: #ccc; font-family: Georgia, serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }
|
||||
.container { max-width: 600px; padding: 20px; }
|
||||
blockquote { font-size: 2em; margin: 0 0 20px 0; font-style: italic; line-height: 1.4; }
|
||||
blockquote span { margin-left: -0.44em; }
|
||||
cite { display: block; font-size: 0.9em; color: #777; margin-top: 15px; font-style: normal; }
|
||||
a { color: #555; text-decoration: none; border-bottom: 1px dotted #555; transition: color 0.3s; font-size: 0.8em; margin-top: 30px; display: inline-block;}
|
||||
a:hover { color: #999; border-bottom: 1px solid #999; }
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>500: Ошибка сервера | DicQuo</title>
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||
header > #logo { margin-top: 1vh; float: left; }
|
||||
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<blockquote>
|
||||
<span>«</span>Что-то пошло не так. Кажется, я уронил сервер, контейнер или случилось что-то ещё. Подождите, пока я его подниму.»
|
||||
</blockquote>
|
||||
<cite>Системный Администратор (5xx)</cite>
|
||||
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить через 5 минут.</a>
|
||||
</div>
|
||||
<header>
|
||||
<div id="logo">
|
||||
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<figure>
|
||||
<p>Неожиданно:</p>
|
||||
<blockquote id="bb">
|
||||
<span style="margin-left:-0.44em;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||
Подождите, пока я его подниму.»
|
||||
</blockquote>
|
||||
<cite>Системный Администратор (500)</cite>
|
||||
</figure>
|
||||
</article>
|
||||
</main>
|
||||
<div class="tags">
|
||||
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить страницу</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
from django.contrib import admin
|
||||
from django import forms
|
||||
from web.models import TbDictumAndQuotes, TbAuthor, TbImages, TbOrigin
|
||||
from taggit.managers import TaggableManager
|
||||
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
|
||||
@@ -18,6 +24,101 @@ except ImportError:
|
||||
def __init__(self, **kwargs): pass
|
||||
|
||||
|
||||
class TagSelect2Widget(Select2TagWidget):
|
||||
"""
|
||||
Select2-виджет для django-taggit, работающий по ИМЕНАМ тегов.
|
||||
|
||||
- подхватывает уже сохранённые теги;
|
||||
- показывает выпадающий список из существующих тегов;
|
||||
- даёт создавать новые теги с пробелами в названии.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# 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 = {
|
||||
"all": ("css/select2_taggit_admin.css",),
|
||||
}
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
"""
|
||||
Настраиваем Select2 так, чтобы пробел НЕ разделял тег
|
||||
на несколько частей (нужны теги с пробелами: «Сергей Курёхин»).
|
||||
Оставляем в разделителях только запятую.
|
||||
"""
|
||||
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||
# По умолчанию django-select2 ставит: [",", " "]
|
||||
# Нам нужен только разделитель-запятая.
|
||||
# Строка '[","]' — корректный JSON-массив из одного элемента.
|
||||
# Важно: сюда нужно класть СТРОКУ с JSON-массивом, а не python-список.
|
||||
# Иначе в HTML окажется "['[\", \"]', ...]" и Select2 будет вести себя непредсказуемо.
|
||||
attrs["data-token-separators"] = '[","]'
|
||||
return attrs
|
||||
|
||||
def format_value(self, value):
|
||||
"""
|
||||
Преобразуем значение из TaggableManager/TagField
|
||||
в список ИМЁН тегов, который ожидает Select2TagWidget.
|
||||
"""
|
||||
from django.db.models import QuerySet
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
# QuerySet или список Tag-объектов
|
||||
if isinstance(value, QuerySet):
|
||||
return [t.name for t in value]
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
names = []
|
||||
for v in value:
|
||||
if isinstance(v, Tag):
|
||||
names.append(v.name)
|
||||
else:
|
||||
names.append(str(v))
|
||||
return names
|
||||
|
||||
# Строка вида "tag1, tag2" — разбираем в список имён
|
||||
if isinstance(value, str):
|
||||
return parse_tags(value)
|
||||
|
||||
return super().format_value(value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""
|
||||
Django-Select2 возвращает список значений (['Сергей Курёхин', 'Другой тег']).
|
||||
Taggit (TagField) ждёт ОДНУ строку, которую потом парсит в список тегов.
|
||||
Если отдать список, он превратится в строку `"['Сергей', 'Курёхин']"`,
|
||||
и распарсится в кривые теги — этого мы избегаем.
|
||||
"""
|
||||
values = super().value_from_datadict(data, files, name)
|
||||
if not values:
|
||||
return ""
|
||||
|
||||
# Для нашего виджета value — это уже список имён тегов
|
||||
tag_names = [str(v).strip() for v in values if str(v).strip()]
|
||||
if not tag_names:
|
||||
return ""
|
||||
|
||||
# ОДИН многословный тег: "Сергей Курёхин" -> "Сергей Курёхин,"
|
||||
# Тогда parse_tags переключится в режим "деление по запятым"
|
||||
if len(tag_names) == 1:
|
||||
single = tag_names[0]
|
||||
if " " in single and "," not in single and '"' not in single:
|
||||
return single + ","
|
||||
return single
|
||||
|
||||
# Несколько тегов — явная запятая между ними.
|
||||
return ", ".join(tag_names)
|
||||
|
||||
|
||||
class DictumAdminForm(forms.ModelForm):
|
||||
# Виртуальные поля для настройки типографа
|
||||
etp_language = forms.ChoiceField(
|
||||
@@ -62,6 +163,9 @@ class DictumAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = TbDictumAndQuotes
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'tags': TagSelect2Widget,
|
||||
}
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@@ -100,6 +204,10 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
|
||||
)
|
||||
readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter')
|
||||
|
||||
formfield_overrides = {
|
||||
models.ManyToManyField: {'widget': Select2TagWidget},
|
||||
}
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# 1. Читаем базовые настройки
|
||||
langs = form.cleaned_data.get('etp_language', 'ru').split(',')
|
||||
@@ -199,6 +307,11 @@ class AdmImages(admin.ModelAdmin):
|
||||
list_display_links = ('id', 'szCaption')
|
||||
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
||||
|
||||
# Добавляем виджет для тегов
|
||||
formfield_overrides = {
|
||||
TaggableManager: {'widget': TagSelect2Widget},
|
||||
}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).prefetch_related('tags')
|
||||
|
||||
@@ -212,6 +325,11 @@ class AdmAuthor(admin.ModelAdmin):
|
||||
list_display_links = ('id', 'szAuthor')
|
||||
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
||||
|
||||
# Добавляем виджет для тегов
|
||||
formfield_overrides = {
|
||||
TaggableManager: {'widget': TagSelect2Widget},
|
||||
}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).prefetch_related('tags')
|
||||
|
||||
@@ -223,4 +341,3 @@ admin.site.register(TbDictumAndQuotes, AdmDictumAndQuotesAdmin)
|
||||
admin.site.register(TbOrigin, AdmOrigin)
|
||||
admin.site.register(TbImages, AdmImages)
|
||||
admin.site.register(TbAuthor, AdmAuthor)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import pytils
|
||||
class RuTag(Tag):
|
||||
class Meta:
|
||||
proxy = True
|
||||
# ordering = ['id']
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
return pytils.translit.slugify(self.name.lower())[:128]
|
||||
@@ -24,6 +25,7 @@ class RuTag(Tag):
|
||||
class RuTaggedItem(TaggedItem):
|
||||
class Meta:
|
||||
proxy = True
|
||||
# ordering = ['id']
|
||||
|
||||
@classmethod
|
||||
def tag_model(cls):
|
||||
@@ -108,7 +110,73 @@ class TbImages(models.Model):
|
||||
|
||||
# заменим имя файла картинки
|
||||
def save(self, *args, **kwargs):
|
||||
self.imFile.name = pytils.translit.slugify(self.szCaption.lower()) + str(Path(self.imFile.name).suffixes)
|
||||
import os
|
||||
from django.conf import settings
|
||||
|
||||
old_obj = None
|
||||
old_file_path = None
|
||||
|
||||
# Получаем старую запись, если она есть
|
||||
if self.pk:
|
||||
try:
|
||||
old_obj = TbImages.objects.get(pk=self.pk)
|
||||
# Пытаемся получить путь к файлу. Если файл не найден физически, Django может выкинуть ошибку здесь или позже
|
||||
# Поэтому просто берем имя из БД и формируем путь руками, чтобы не зависеть от Storage
|
||||
if old_obj.imFile:
|
||||
old_file_path = os.path.join(settings.MEDIA_ROOT, str(old_obj.imFile.name))
|
||||
except TbImages.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Fix 1: Если старый путь уже битый (содержит ['...'])
|
||||
if old_file_path and "['" in old_file_path:
|
||||
# Формируем "исправленный" путь (каким он должен быть)
|
||||
corrected_path = old_file_path.replace("['", "").replace("']", "").replace("'", "")
|
||||
|
||||
# Проверяем: если битого файла нет, а исправленный есть -> значит БД врет
|
||||
if not os.path.exists(old_file_path) and os.path.exists(corrected_path):
|
||||
# Исправляем текущее имя файла в объекте (убираем мусор из имени)
|
||||
self.imFile.name = str(self.imFile.name).replace("['", "").replace("']", "").replace("'", "")
|
||||
# Обновляем переменную old_file_path, чтобы дальнейшая логика переименования работала корректно
|
||||
old_file_path = corrected_path
|
||||
|
||||
# Получаем текущее имя и расширение (уже возможно исправленное выше)
|
||||
current_path = Path(str(self.imFile.name))
|
||||
current_suffix = current_path.suffix
|
||||
|
||||
# Fix 2: Чиним расширение еще раз (на всякий случай, если Fix 1 не сработал или это новый объект)
|
||||
if "['" in str(current_suffix):
|
||||
current_suffix = str(current_suffix).replace("['", "").replace("']", "").replace("'", "")
|
||||
|
||||
# Формируем новое имя файла на основе заголовка (Slug)
|
||||
new_filename = pytils.translit.slugify(self.szCaption.lower()) + current_suffix
|
||||
|
||||
# Определяем папку (если есть родитель, используем его, иначе img2)
|
||||
# Важно: self.imFile.name может содержать полный путь. Нам нужен только относительный от MEDIA_ROOT
|
||||
# Но проще взять родителя из текущего имени
|
||||
parent_dir = current_path.parent.name if current_path.parent.name else 'img2'
|
||||
new_name_with_path = str(Path(parent_dir) / new_filename)
|
||||
|
||||
# Переименование физического файла
|
||||
# Сравниваем старое имя (из БД) с новым (сгенерированным)
|
||||
if old_obj and str(old_obj.imFile.name) != new_name_with_path:
|
||||
new_file_full_path = os.path.join(settings.MEDIA_ROOT, new_name_with_path)
|
||||
|
||||
# Если старый файл (old_file_path) существует физически, переименовываем его
|
||||
if old_file_path and os.path.exists(old_file_path):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(new_file_full_path), exist_ok=True)
|
||||
os.rename(old_file_path, new_file_full_path)
|
||||
self.imFile.name = new_name_with_path
|
||||
except OSError as e:
|
||||
print(f"Error renaming file from {old_file_path} to {new_file_full_path}: {e}")
|
||||
else:
|
||||
# Если старого файла нет, просто обновляем имя в БД
|
||||
self.imFile.name = new_name_with_path
|
||||
else:
|
||||
# Если имя не менялось или объекта не было, просто устанавливаем правильное имя
|
||||
# (например, чтобы убрать мусор из расширения в БД)
|
||||
self.imFile.name = new_name_with_path
|
||||
|
||||
super(TbImages, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -15,9 +15,9 @@ services:
|
||||
|
||||
# 1. ОБРАЗ
|
||||
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||
# image: git.cube2.ru/e-serg/dicquo:latest
|
||||
# Но пока, для первого деплоя или если реестра нет, можно собирать локально:
|
||||
build: .
|
||||
image: git.cube2.ru/erjemin/2020-dq:latest
|
||||
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||
# build: .
|
||||
|
||||
restart: always
|
||||
|
||||
@@ -50,7 +50,8 @@ services:
|
||||
|
||||
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
||||
ports:
|
||||
- "8010:8000"
|
||||
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||
- "127.0.0.1:8010:8000"
|
||||
|
||||
# 5. Тома (Volumes)
|
||||
volumes:
|
||||
@@ -105,8 +106,8 @@ services:
|
||||
- REPO_PASS=${REPO_PASS}
|
||||
- WATCHTOWER_SCOPE=dq-scope
|
||||
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
|
||||
- WATCHTOWER_POLL_INTERVAL=1800 # Проверять каждые 30 минут
|
||||
command: --scope dq-scope
|
||||
- DOCKER_API_VERSION=1.44
|
||||
command: --interval 1800 --cleanup # Проверять каждые 30 минут
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
|
||||
31
poetry.lock
generated
31
poetry.lock
generated
@@ -67,6 +67,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
argon2 = ["argon2-cffi (>=23.1.0)"]
|
||||
bcrypt = ["bcrypt (>=4.1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-appconf"
|
||||
version = "1.2.0"
|
||||
description = "A helper class for handling configuration defaults of packaged apps gracefully."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4"},
|
||||
{file = "django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = "*"
|
||||
|
||||
[[package]]
|
||||
name = "django-environ"
|
||||
version = "0.12.1"
|
||||
@@ -83,6 +97,21 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)",
|
||||
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-notfound-page"]
|
||||
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-select2"
|
||||
version = "8.4.8"
|
||||
description = "This is a Django_ integration of Select2_."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "django_select2-8.4.8-py3-none-any.whl", hash = "sha256:a2ce6a4c556dd2d4d57eb3753618d6f31f8d3910e9d9fa1b686d9340f50b14eb"},
|
||||
{file = "django_select2-8.4.8.tar.gz", hash = "sha256:592e52effff2b5850cb7c98b265715b6704fb784699c4aedddfdd8ae1ffa1e81"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=4.2"
|
||||
django-appconf = ">=0.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "django-taggit"
|
||||
version = "6.1.0"
|
||||
@@ -646,4 +675,4 @@ brotli = ["brotli"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "3d7a7f2fe8ec78993616e707e29e96503f134bd1cec48cac7f6dd47814863f4f"
|
||||
content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"
|
||||
|
||||
@@ -1,143 +1,11 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.tags{
|
||||
color: silver;
|
||||
font-size:1.5vh;
|
||||
line-height:1.9vh;
|
||||
padding-top: 7vh;
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Настройки для анимирования цвета ссылок:
|
||||
* рецепт взят из: https://habr.com/ru/company/ruvds/blog/491702/
|
||||
*****************************************************************/
|
||||
.tags a {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding: 0 0.5ex;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
border-bottom: dotted 1px silver;
|
||||
/* градиент для цвета ссылки */
|
||||
background: linear-gradient(to right, rgba(255,255,255,0.9) 40%, slategray, silver, lightyellow 50%, rgba(255,255,255,0.4));
|
||||
/* обрезка градиента */
|
||||
background-clip: initial;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 250% 100%;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
|
||||
background-position: 100%;
|
||||
/* плавное позиионирование градиента */
|
||||
transition: background-position 0.65s ease;
|
||||
margin-right: 2vh;
|
||||
}
|
||||
|
||||
.tags a:hover {
|
||||
color: white;
|
||||
background-position: 0 100%;
|
||||
border-bottom: solid 1px white;
|
||||
}
|
||||
|
||||
div[name="cookies_accept"] {
|
||||
font-family:'Roboto', 'Lucida Grande', Verdana, Arial, sans-serif;
|
||||
position:fixed;
|
||||
bottom: 0; left: 0;
|
||||
width: 100%;
|
||||
padding: 2vh 2vw;
|
||||
color: black;
|
||||
background-color: gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div[name="cookies_accept"] button {
|
||||
padding:0.5vh 0.5vw;
|
||||
background: silver;
|
||||
color: black;
|
||||
margin-left: 2vw;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#logo {
|
||||
margin-top:1vh;
|
||||
filter: alpha(Opacity=75); /* Полупрозрачность для IE */
|
||||
opacity: 0.75;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#logo a {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table { width: 80%; }
|
||||
|
||||
#menu {
|
||||
display: none;
|
||||
color: silver;
|
||||
}
|
||||
|
||||
#mm {
|
||||
text-decoration: none;
|
||||
color: silver;
|
||||
}
|
||||
|
||||
#image {
|
||||
width: 30vw;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
#image > center > div {
|
||||
width: 22vw;
|
||||
height: 22vw;
|
||||
padding:0.5vw;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#image > center > div > div {
|
||||
border-radius:50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#image > center > div > div > img {
|
||||
width: auto;
|
||||
height: 22vw;
|
||||
}
|
||||
|
||||
#author {
|
||||
color: silver;
|
||||
font-size: 3.5vh;
|
||||
line-height: 4vh;
|
||||
text-align: right;
|
||||
padding-top: 4vh;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#info {
|
||||
color: silver;
|
||||
font-size: 2.5vh;
|
||||
line-height: 3vh;
|
||||
padding-bottom: 2vh;
|
||||
}
|
||||
|
||||
#bb {
|
||||
color: whitesmoke;
|
||||
font-size: 4.5vh;
|
||||
line-height: 5vh
|
||||
}
|
||||
|
||||
#next { float: right; }
|
||||
|
||||
#next a { border-bottom: none; }
|
||||
|
||||
/* --- NEW STYLES for FLEXBOX LAYOUT --- */
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vmin;
|
||||
min-width: 100vmin;
|
||||
background-color: #111; /* Изначально темный фон */
|
||||
opacity: 0; /* Скрываем контент до расчета цвета */
|
||||
transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
|
||||
}
|
||||
|
||||
/* Header */
|
||||
@@ -145,46 +13,101 @@ header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1vh 0;
|
||||
padding: 1vmin 4vmin;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
header > #logo {
|
||||
margin-top: 1vmin;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2vw;
|
||||
header > #logo a {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text-col {
|
||||
flex: 1;
|
||||
header > #logo a > img {
|
||||
width:50px;
|
||||
height:46px;
|
||||
}
|
||||
|
||||
.image-col {
|
||||
flex: 0 0 30vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
header > nav {
|
||||
border: #555555;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* --- Icons for Header Stats (SVG in Base64) --- */
|
||||
.stats-icon {
|
||||
header > nav > a { /* бургер */
|
||||
color: silver;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
padding: 0 0.5em;
|
||||
margin-right: -0.5em;
|
||||
border: solid 1px transparent;
|
||||
transition: border-color 0.8s ease, color 0.8s ease;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
header > nav > a:hover {
|
||||
color: white;
|
||||
border: solid 1px silver;
|
||||
transition: border-color 0.8s ease, color 0.8s ease;
|
||||
}
|
||||
|
||||
header > nav > #stats-menu {
|
||||
display: none;
|
||||
color: silver;
|
||||
font-size: 0.9em;
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
header > nav > #stats-menu > b {
|
||||
font-weight: normal;
|
||||
margin: 0 1ex;
|
||||
}
|
||||
|
||||
header > nav > #stats-menu > p {
|
||||
font-style: italic; display: inline-block;
|
||||
margin: 0 1vmin;
|
||||
padding-right: 1vmin;
|
||||
border-right: 1px dotted silver;
|
||||
}
|
||||
|
||||
header > nav > #stats-menu > i.stats-icon {
|
||||
display: inline-block;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
vertical-align: middle;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 0.2em;
|
||||
margin-right: .2em;
|
||||
opacity: 0.7; /* Slight transparency for subtle look */
|
||||
}
|
||||
|
||||
header > nav > #stats-menu > i.stats-icon.icon-views {
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
|
||||
header > nav > #stats-menu > a {
|
||||
color: silver;
|
||||
text-decoration: none;
|
||||
border: solid 1px gray;
|
||||
border-radius: 2em;
|
||||
padding: 1.5px 0.2em 0 0.2em;
|
||||
margin-left: 1em;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
header > nav > #stats-menu > a:hover {
|
||||
background-color: tan;
|
||||
color: black;
|
||||
border: solid 1px white;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* --- Icons for Header Stats (SVG in Base64) --- */
|
||||
/* Clock Icon (Time) */
|
||||
.icon-time {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
@@ -195,30 +118,202 @@ header {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Responsive: on mobile stack columns */
|
||||
/* MAIN ARTICLE CONTENT */
|
||||
main {
|
||||
padding: 1vmin 8vmin;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 60vmin;
|
||||
}
|
||||
|
||||
main > article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2vmin;
|
||||
}
|
||||
|
||||
main > article > figure {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
main > article > figure > p { /* Интро/Вступление */
|
||||
color: silver;
|
||||
font-size: 3vmin;
|
||||
line-height: 3.5vmin;
|
||||
padding-bottom: 2vmin;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main > article > figure > blockquote { /* Цитата */
|
||||
color: whitesmoke;
|
||||
font-size: 4.5vmin;
|
||||
line-height: 5vmin;
|
||||
border:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
main > article > figure > cite { /* Автор цитаты */
|
||||
color: silver;
|
||||
font-size: 3.5vmin;
|
||||
line-height: 4vmin;
|
||||
text-align: right;
|
||||
padding-top: 4vmin;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main > article > div {
|
||||
flex: 0 0 30vmax;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 30vmax;
|
||||
text-align: right;
|
||||
margin-bottom: 10vmin;
|
||||
}
|
||||
|
||||
main > article > div > div {
|
||||
width: 26vmax;
|
||||
height: 26vmax;
|
||||
padding: 0.5vmin;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
main > article > div > div > div {
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main > article > div > div > div > img {
|
||||
width: auto;
|
||||
height: 26vmax;
|
||||
}
|
||||
|
||||
/* НАВИГАЦИЯ (ТЕГИ И ДАЛЕЕ) В КОНЦЕ */
|
||||
nav {
|
||||
padding: 1vmin 4vmin;
|
||||
}
|
||||
nav > div {
|
||||
color: silver;
|
||||
font-size: 1.5vmin;
|
||||
line-height: 1.9vmin;
|
||||
padding: 7vmin 0 4vmin 0;
|
||||
}
|
||||
nav > div a {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding: 0 0.5ex;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
border-bottom: dotted 1px silver;
|
||||
/* градиент для цвета ссылки */
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4));
|
||||
/* обрезка градиента */
|
||||
background-clip: initial;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 250% 100%;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
|
||||
background-position: 100%;
|
||||
/* плавное позиионирование градиента */
|
||||
transition: background-position 0.65s ease;
|
||||
margin-right: 2vmin;
|
||||
}
|
||||
|
||||
nav > div a:hover {
|
||||
color: white;
|
||||
background-position: 0 100%;
|
||||
border-bottom: solid 1px white;
|
||||
}
|
||||
|
||||
nav > div > div {
|
||||
float: right;
|
||||
}
|
||||
|
||||
nav > div > div a {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* --- ПОДВАЛ-КУКИ (ДЗЕН-СТИЛЬ) --- */
|
||||
footer {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 2vmin 4vmin;
|
||||
color: silver; /* Мягкий серый цвет текста */
|
||||
background-color: rgba(30, 30, 30, 0.8); /* Темный полупрозрачный фон */
|
||||
backdrop-filter: blur(5px); /* Эффект матового стекла (современно и медитативно) */
|
||||
text-align: center;
|
||||
border-top: 1px solid #444; /* Тонкая грань */
|
||||
font-size: 0.9em;
|
||||
z-index: 1000; /* Чтобы точно было поверх всего */
|
||||
}
|
||||
|
||||
footer small {
|
||||
display: inline-block;
|
||||
margin-right: 2vmin;
|
||||
letter-spacing: 0.05em; /* Немного воздуха в тексте */
|
||||
}
|
||||
|
||||
footer button {
|
||||
padding: 0.5vmin 1.5vmin;
|
||||
background: transparent;
|
||||
color: silver;
|
||||
border: 1px solid silver;
|
||||
border-radius: 2em; /* Округлые, мягкие формы */
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
footer button:hover {
|
||||
background: silver;
|
||||
color: #111;
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); /* Легкое свечение при наведении */
|
||||
}
|
||||
|
||||
/* Отзывчивость: для мобильных устройств колонки свапаются */
|
||||
@media (max-width: 768px) {
|
||||
.content-row {
|
||||
main > article {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.image-col {
|
||||
|
||||
main > article > div {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 2vh;
|
||||
margin: 8vmin 0 2vmin 0;
|
||||
}
|
||||
|
||||
main > article > div {
|
||||
width: 80vmin;
|
||||
}
|
||||
|
||||
main > article > div > div {
|
||||
width: 36vmax;
|
||||
height: 36vmax;
|
||||
}
|
||||
|
||||
main > article > div > div > div > img {
|
||||
height: 36vmax;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (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.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;}/* ( [ { */
|
||||
|
||||
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */
|
||||
/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */
|
||||
[class^="etp-r"], [class*=" etp-r"] { position: absolute; }
|
||||
/* Точечная настройка смещения для каждого символа */
|
||||
.etp-raquo { right: -0.44em; } /* » */
|
||||
.etp-rdquo { right: -0.4em; } /* ” */
|
||||
.etp-rsquo { right: -0.22em; } /* ’ */
|
||||
.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */
|
||||
.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */
|
||||
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
|
||||
.counter-pixel {
|
||||
border: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
60
public/static/css/select2_taggit_admin.css
Normal file
60
public/static/css/select2_taggit_admin.css
Normal file
@@ -0,0 +1,60 @@
|
||||
/* Select2 (django-select2) dark theme compatibility for Django Admin.
|
||||
We intentionally scope to dark mode only and lean on Django Admin CSS variables. */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme="light"]) .select2-container--default .select2-selection--single,
|
||||
html:not([data-theme="light"]) .select2-container--default .select2-selection--multiple {
|
||||
background: var(--body-bg, #1e1e1e) !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
border-color: var(--border-color, #3a3a3a) !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-selection--single,
|
||||
html[data-theme="dark"] .select2-container--default .select2-selection--multiple {
|
||||
background: var(--body-bg, #1e1e1e) !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
border-color: var(--border-color, #3a3a3a) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-selection__rendered {
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-search--inline .select2-search__field,
|
||||
html[data-theme="dark"] .select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
background: transparent !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
border-color: rgba(255, 255, 255, 0.14) !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-dropdown {
|
||||
background: var(--body-bg, #1e1e1e) !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
border-color: var(--border-color, #3a3a3a) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-results__option {
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgba(255, 255, 255, 0.10) !important;
|
||||
color: var(--body-fg, #ffffff) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .select2-container--default .select2-results__option--selected {
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
color: var(--body-fg, #e6e6e6) !important;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
// bg-generator.js: Generates a unique gradient background based on text content
|
||||
// bg-generator.js:
|
||||
// - Генерирует уникальный градиентный фон на основе текстового содержимого
|
||||
// - Реализует плавное появление и исчезновение при навигации
|
||||
// - Авто-редирект через 15 секунд для создания "медитативного" слайд-шоу эффекта
|
||||
// - Обрабатывает принятие Cookie
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// 1. Get the text to hash (from hidden span in base.html)
|
||||
// 1. Получаем текст для хеширования (из скрытого span в base.html)
|
||||
const rawSpan = document.getElementById('dq-content-raw');
|
||||
let text = rawSpan ? rawSpan.innerText.trim() : "";
|
||||
|
||||
if (!text) {
|
||||
text = "DictumAndQuotesDefault" + Math.random(); // Fallback random if no text
|
||||
text = "DictumAndQuotesDefault" + Math.random(); // Случайный вариант, если текста нет
|
||||
}
|
||||
|
||||
// 2. Hash function (DJB2)
|
||||
// 2. Хеш-функция (DJB2)
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
// Force 32-bit integer arithmetic
|
||||
// Принудительная 32-битная целочисленная арифметика
|
||||
hash = ((hash << 5) + hash) + text.charCodeAt(i);
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
hash = hash & hash; // Преобразование в 32-битное целое
|
||||
}
|
||||
|
||||
// 3. Generate 6 color components deterministically from the hash
|
||||
// We need 6 numbers between 0 and 255.
|
||||
// Let's use pseudo-random generator seeded by hash
|
||||
// 3. Детерминированная генерация 6 цветовых компонентов из хеша
|
||||
// Нам нужно 6 чисел от 0 до 255.
|
||||
// Используем генератор псевдослучайных чисел с затравкой из хеша
|
||||
|
||||
function Mulberry32(a) {
|
||||
return function() {
|
||||
@@ -30,53 +34,53 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
}
|
||||
}
|
||||
|
||||
const rand = Mulberry32(hash); // Seeded random generator
|
||||
const rand = Mulberry32(hash); // Генератор случайных чисел с seed
|
||||
|
||||
// Generate 6 color components with darker range for "meditative" feel
|
||||
// Генерация 6 цветовых компонентов в темном диапазоне для "медитативного" ощущения
|
||||
let colors = [];
|
||||
for(let i=0; i<6; i++) {
|
||||
// Generate number between 10 and 80 (dark colors)
|
||||
// Генерируем число от 10 до 80 (темные цвета)
|
||||
colors.push(Math.floor(rand() * 70) + 10);
|
||||
}
|
||||
|
||||
// Shuffle slightly based on random to allow variation on refresh (optional)
|
||||
// colors.sort(() => Math.random() - 0.5);
|
||||
// Немного перетасовываем на основе случайности, чтобы позволить вариации при обновлении (опционально)
|
||||
colors.sort(() => Math.random() - 0.5);
|
||||
|
||||
const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`;
|
||||
const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`;
|
||||
|
||||
console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2);
|
||||
|
||||
// 4. Apply to body
|
||||
// Using linear-gradient to right with standard syntax
|
||||
// 4. Применяем к body.
|
||||
// Используем линейный градиент вправо со стандартным синтаксисом
|
||||
const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`;
|
||||
document.body.style.background = bgString;
|
||||
|
||||
// 5. Apply to image background container (if exists on index page)
|
||||
// 5. Применяем к контейнеру фона изображения (если он есть на главной странице)
|
||||
const imgBgContainer = document.querySelector('.image-col center > div');
|
||||
if (imgBgContainer) {
|
||||
// Use the first color of the gradient with opacity 0.7
|
||||
// Используем первый цвет градиента с прозрачностью 0.7
|
||||
imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`;
|
||||
}
|
||||
|
||||
// 6. Reveal content (Fade In effect)
|
||||
// 6. Показываем контент (эффект плавного появления - Fade In)
|
||||
setTimeout(() => {
|
||||
document.body.style.opacity = 1;
|
||||
}, 50);
|
||||
|
||||
// 7. Handle Fade Out on link clicks
|
||||
// 7. Обработка плавного исчезновения (Fade Out) при клике по ссылкам
|
||||
document.body.addEventListener('click', function(e) {
|
||||
// Find if a link was clicked (bubble up)
|
||||
// Ищем, была ли нажата ссылка (всплытие)
|
||||
const link = e.target.closest('a');
|
||||
if (link && link.href && link.target !== '_blank') {
|
||||
const hrefAttr = link.getAttribute('href');
|
||||
if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) {
|
||||
// Check if it is an internal link (same domain)
|
||||
// Проверяем, является ли ссылка внутренней (тот же домен)
|
||||
if (new URL(link.href).origin === window.location.origin) {
|
||||
e.preventDefault(); // Stop immediate navigation
|
||||
document.body.style.opacity = 0; // Start Fade Out
|
||||
e.preventDefault(); // Останавливаем немедленный переход
|
||||
document.body.style.opacity = 0; // Запускаем Fade Out
|
||||
|
||||
// Wait for transition (matches CSS transition time 1.5s)
|
||||
// Ждем завершения перехода (соответствует времени CSS transition 0.9s (было 1.5s))
|
||||
setTimeout(() => {
|
||||
window.location.href = link.href;
|
||||
}, 900);
|
||||
@@ -85,14 +89,28 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Auto-redirect ("meditative" slideshow)
|
||||
// Find the NEXT link and simulate a click on it after 15 seconds
|
||||
// 8. Авто-редирект ("медитативное" слайд-шоу)
|
||||
// Ищем ссылку "ДАЛЕЕ" и симулируем клик по ней через 15 секунд
|
||||
const nextLink = document.querySelector('#next a');
|
||||
if (nextLink) {
|
||||
setTimeout(() => {
|
||||
// Trigger the click event on the link so our handler above (step 7) catches it
|
||||
// and performs the smooth fade out animation.
|
||||
// Вызываем событие клика по ссылке, чтобы наш обработчик выше (шаг 7) поймал его
|
||||
// и выполнил анимацию плавного исчезновения.
|
||||
nextLink.click();
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
// 9. Логика принятия Cookie
|
||||
const cookieBanner = document.querySelector('footer');
|
||||
if (cookieBanner) {
|
||||
const acceptButton = cookieBanner.querySelector('button');
|
||||
if (acceptButton) {
|
||||
acceptButton.addEventListener('click', function() {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (92 * 24 * 60 * 60 * 1000)); // ~3 месяца (7948800000ms)
|
||||
document.cookie = "cookie_accept=1; expires=" + date.toUTCString() + "; path=/; SameSite=Lax";
|
||||
cookieBanner.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
42
public/static/js/counters.js
Normal file
42
public/static/js/counters.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Rating Mail.ru counter
|
||||
var _tmr = window._tmr || (window._tmr = []);
|
||||
_tmr.push({id: "3744288", type: "pageView", start: (new Date()).getTime()});
|
||||
(function (d, w, id) {
|
||||
if (d.getElementById(id)) return;
|
||||
var ts = d.createElement("script");
|
||||
ts.type = "text/javascript";
|
||||
ts.async = true;
|
||||
ts.id = id;
|
||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||
var f = function () {
|
||||
var s = d.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(ts, s);
|
||||
};
|
||||
if (w.opera == "[object Opera]") {
|
||||
d.addEventListener("DOMContentLoaded", f, false);
|
||||
} else {
|
||||
f();
|
||||
}
|
||||
})(document, window, "tmr-code");
|
||||
// Yandex.Metrika counter
|
||||
(function(m,e,t,r,i,k,a){
|
||||
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=106953063', 'ym');
|
||||
ym(106953063, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
||||
// Google Analytics (GA4) counter
|
||||
(function() {
|
||||
var gaScript = document.createElement('script');
|
||||
gaScript.async = true;
|
||||
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-WTJM8J9YL5';
|
||||
document.head.appendChild(gaScript);
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
// Делаем функцию глобально доступной, если понадобится вызывать её из других скриптов
|
||||
window.gtag = gtag;
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-WTJM8J9YL5');
|
||||
})();
|
||||
@@ -17,6 +17,7 @@ django-environ = "^0.12.1"
|
||||
whitenoise = "^6.11.0"
|
||||
gunicorn = "^25.1.0"
|
||||
tqdm = "^4.67.3"
|
||||
django-select2 = "^8.4.8"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
Reference in New Issue
Block a user