181 Commits

Author SHA1 Message Date
erjemin b56fbef9a9 Merge: реворк проекта 2026-05 2026-05-16 23:35:55 +03:00
erjemin 4e744c5d8a minor: + 2026-05-15 13:59:46 +03:00
erjemin 08ba2f5b75 mod: про проект и планы... 2026-05-15 13:59:07 +03:00
erjemin 4e0647b7e0 mod: дроп-даун логин/логаут вынесен во внешний auth.js 2026-05-15 13:20:48 +03:00
erjemin a6587edb62 mod: счетчики вынесены во внешний analytics.js 2026-05-15 13:08:20 +03:00
erjemin b1feb33ac1 mod: файлы для поисковиков и ИИ 2026-05-15 10:29:07 +03:00
erjemin 69527189f4 mod: добавлены некоторые страницы. 2026-05-15 09:48:21 +03:00
erjemin df97daa836 mod: pdf 2026-05-15 08:57:06 +03:00
erjemin 6f97287077 mod: нужные картинки в media-файлах 2026-05-15 08:56:14 +03:00
erjemin 4f1fcf7e60 add: SEO и JSON-LD + хлебные крошки. 2026-05-15 08:35:53 +03:00
erjemin a1a9ae24da add: SEO и JSON-LD + хлебные крошки. 2026-05-15 08:08:37 +03:00
erjemin a1869decd8 add: FAQ + SEO и JSON-LD + хлебные крошки. 2026-05-15 07:42:10 +03:00
erjemin 982d8e570c mod: SEO и JSON-LD + добавлены хлебные крошки. 2026-05-14 21:52:14 +03:00
erjemin a89be1a28d mod: картинки 2026-05-14 21:15:50 +03:00
erjemin 543babe82e mod: картинки 2026-05-14 21:15:34 +03:00
erjemin 70b2d930f4 del: служебный интерфейс удалён из проекта 2026-05-14 21:12:14 +03:00
erjemin 5609041919 mod: картинки рекомендованных схем открывания 2026-05-14 20:42:37 +03:00
erjemin afc77fbd04 add: тесты 2026-05-14 19:45:33 +03:00
erjemin 58590e81ff minor: comment 2026-05-14 19:40:01 +03:00
erjemin a9002ecf19 add: Django Management Command для генерации JavaScript-файлы отрисовки карт с геоданными ВСЕХ зданий типовых серий 2026-05-14 19:36:52 +03:00
erjemin 7b58f77013 add: библиотека rjsmin для обсфуркации 2026-05-14 19:22:09 +03:00
erjemin 85bc89d211 add: Django Management Command для "склейки" корневых серий типового строительства 2026-05-14 00:27:17 +03:00
erjemin 420e949515 mod: raw sql --> ORM 2026-05-13 18:33:53 +03:00
erjemin 14b9114fff mod: В перерасчет рейтинга добавлен учет оконных наборов 2026-05-13 17:01:39 +03:00
erjemin 080aa5dcfb del: Перерасчет рейтинга удален из сервисного раздела 2026-05-13 00:50:53 +03:00
erjemin 80ff9dffd1 add: Перерасчет рейтинга перенесен в Django Management Command 2026-05-13 00:50:12 +03:00
erjemin 73065e4178 minor: - 2026-05-12 20:09:10 +03:00
erjemin f8f4c72293 mod: URL для админки в .env 2026-05-12 19:45:49 +03:00
erjemin 4af0458a35 mod: описание и JSON-LD 2026-05-12 10:50:06 +03:00
erjemin cd51decae6 add: недостающие картинки схемы открывания 2026-05-11 20:15:18 +03:00
erjemin 56a4ca2308 mod: улучшена производительность +регистронезависимость 2026-05-11 20:12:48 +03:00
erjemin 1fddbe642d minor: пейджинация обернута в nav 2026-05-11 19:44:32 +03:00
erjemin 7d2630d6c5 mod: seo-теги и JSON-LD 2026-05-11 19:43:03 +03:00
erjemin aae71de356 minor: - 2026-05-11 19:26:06 +03:00
erjemin 2456eab93c fix: очищаем заголовок перед добавлением в ключевые слова 2026-05-11 19:25:21 +03:00
erjemin b34a73747b mod: SEO-теги и JSON-LD (chema.org) 2026-05-11 18:29:10 +03:00
erjemin b940e2e71e fix: тизер для описания не должен транслитирироваться 2026-05-11 17:36:37 +03:00
erjemin 8928aa3feb minor: ненужный import 2026-05-11 15:13:04 +03:00
erjemin c0fed9c2a7 minor: ненужный import 2026-05-11 15:10:35 +03:00
erjemin f2d9b65825 minor: ненужный import 2026-05-11 15:09:30 +03:00
erjemin 07976769a7 add: Custom Management Command для автозаполнения SEO-атрибутов блога 2026-05-11 00:45:22 +03:00
erjemin abb05b7330 add: автозаполнение SEO-полей для блога 2026-05-11 00:18:36 +03:00
erjemin 452eed8a2e mod: унифицированная slug-офикация 2026-05-10 23:34:00 +03:00
erjemin a3b491516a add: SEO-поля для блогов и миграция 2026-05-10 18:53:34 +03:00
erjemin 84f57b53f3 fix: Показывать логин когда есть логин 2026-05-10 14:53:45 +03:00
erjemin 83085740e0 del: лишний скрипт 2026-05-10 14:53:15 +03:00
erjemin 3c33b3a90a del: лишний скрипт 2026-05-09 21:53:49 +03:00
erjemin 4cff1cb33e mod: JS вынесен во внешний скрипт 2026-05-09 21:43:13 +03:00
erjemin 978a3ad02e mod: данные о последних визитах пользователя полностью перенесены с сервера на клиент (в JS). Отключен из контекста всех шаблонов LAST_VISIT и связанные с ним функции в вьюшках 2026-05-09 21:21:20 +03:00
erjemin b3aa0ce3b3 mod: блок "логин" подгружается только по клику на меню "Вход" 2026-05-09 15:05:03 +03:00
erjemin 18ab1ec291 mod: связность страница компании --> набор для сравнения 2026-05-09 14:55:26 +03:00
erjemin 0bc88869a5 mod: раздел сравнения оконных наборов. 2026-05-09 14:40:54 +03:00
erjemin ac8b50e3be add: раздел сравнения оконных наборов. 2026-05-08 18:04:51 +03:00
erjemin 683513e5fa mod: Изменение всех SEO-атрибутов (и добавлены хлебные крошки и LD-JSON) 2026-05-08 17:49:45 +03:00
erjemin b8a6a2370e fix: неправильное формирование default значений 2026-05-07 23:44:07 +03:00
erjemin 26d957a285 mod: raw SQL на ORM, убран макаронный код и исправлена пара ошибок в сравнениях характеристик. 2026-05-07 23:20:59 +03:00
erjemin fb7096ca3d fix: Не хватало серии дома в контекст шаблона + raw-SQL заменен на ORM 2026-05-07 22:52:04 +03:00
erjemin 9fb278368f fix: убрал ссылку на текущую серию. 2026-05-05 19:08:41 +03:00
erjemin 9155609c88 mod: переделан raw на ORM и бурана дублирующая функция (список серий домов, она уже есть в web.catalog_series 2026-05-05 18:46:58 +03:00
erjemin e9173e269a minor: - 2026-05-05 18:18:56 +03:00
erjemin b583afa080 mod: изменение роутинга для страницы ценовых предложений (4) 2026-05-05 17:40:18 +03:00
erjemin 4de35904f4 mod: изменение роутинга для страницы ценовых предложений (3) 2026-05-01 11:55:54 +03:00
erjemin 778b7f63dd mod: изменение роутинга для страницы ценовых предложений (2) 2026-04-30 23:42:43 +03:00
erjemin 61f69c21f7 mod: изменение роутинга для страницы ценовых предложений 2026-04-30 23:20:34 +03:00
erjemin a84e780710 add: "хлебные кроши" и schema.org LD-JSON для них 2026-04-28 11:05:09 +03:00
erjemin 9097414637 mod: raw-SQL --> ORM для цен на наборы кокон в квартиру. SEO-атртибуты + добавлена schema.org 2026-04-27 20:42:00 +03:00
erjemin 2d830aa897 minor: планы 2026-04-26 15:05:17 +03:00
erjemin 45681827e1 minor: Роадмап вынесен в отдельный файл. 2026-04-26 15:01:15 +03:00
erjemin 3479b31f0e mod: Рефакторинг страницы цен одного окна (вьюшки, шаблоны, тесты, новый canonical-роутинг) 2026-04-26 14:53:49 +03:00
erjemin 21501799ca mod: замена во всех прямых ссылках https://oknardia.ru на {{ request.scheme }}://{{ request.get_host }} 2026-04-25 15:24:54 +03:00
erjemin 1660e76810 mod: Улучшены SEO-атрибуты, и добавлена разметка shema.org 2026-04-25 15:18:59 +03:00
erjemin c37a3ab097 minor: - 2026-04-25 11:04:19 +03:00
erjemin 47049f6f9f minor: даты "Y-m-d" 2026-04-25 08:12:41 +03:00
erjemin 74220ab3a3 mod: рефакторинг "каталога компаний" (вьюшки и шаблоны) 2026-04-25 08:06:15 +03:00
erjemin 509bce5111 del: это описание перенесено в MANAGEMENT_RUNBOOK.md 2026-04-24 17:33:45 +03:00
erjemin 4bb77a9892 mod: рефакторинг "каталог типовых проёмов" 2026-04-24 17:31:56 +03:00
erjemin c184d65c66 add: __init__ 2026-04-24 17:27:53 +03:00
erjemin 1bdff6589e minor: - 2026-04-24 16:35:15 +03:00
erjemin 34ab71172c minor: отключено "передергивание" uWSGI 2026-04-23 23:32:19 +03:00
erjemin 851babdda2 add: management-команда regenerate_seria_prerender для оффлайн пересборки pre-render шаблонов серий; обновлены SETUP.md, README.md, MANAGEMENT_RUNBOOK.md 2026-04-22 01:24:03 +03:00
erjemin 330737878e minor: каталог для пре-рендер шаблонов 2026-04-22 00:06:05 +03:00
erjemin bc3c348560 fix: пре-рендер шаблоны не должны попадать в репозиторий 2026-04-22 00:03:06 +03:00
erjemin 0d97dafe3e mod: refactor catalog_seria_info and update README/settings 2026-04-21 21:31:17 +03:00
erjemin 1f0aaa7687 minor: fix: явное указание типа to_template 2026-04-21 01:05:11 +03:00
erjemin 20877dc98e mod: refactor catalog_seria to ORM 2026-04-20 23:49:42 +03:00
erjemin 19a595fb10 minor: изменения в замерах времени исполнения вьюшки 2026-04-20 23:23:05 +03:00
erjemin 917d62cc21 mod: split catalog modules and refactor profile manufacture view 2026-04-20 22:23:42 +03:00
erjemin 71b5089a61 fix: явное указание типов 2026-04-20 01:02:16 +03:00
erjemin 9e27089638 minor: в отдельный catalog_profiles.py выделены все вьюхи для каталога профилей 2026-04-20 00:54:11 +03:00
erjemin af67b69418 mod: рефакторинг catalog_profile_model и обновление дорожной карты README 2026-04-20 00:43:27 +03:00
erjemin af2489f923 minor: ticks стал видимым 2026-04-20 00:33:13 +03:00
erjemin 0b6536b8f4 mod: simplify catalog profile views 2026-04-19 23:43:13 +03:00
erjemin c930e059a0 mod: catalog_profile и документация 2026-04-18 15:39:39 +03:00
erjemin c0b41cd866 add: SITEMAP_RUNBOOK.md; mod: README.md — обновлена документация проекта 2026-04-18 13:48:28 +03:00
erjemin b348c8fe81 mod: rework sitemap generation and seo coverage 2026-04-18 13:28:15 +03:00
erjemin e7da2cdeb9 mod: migrate to poetry and add django debug toolbar for dev 2026-04-17 16:07:19 +03:00
erjemin d5719015b0 mod: switch secrets config to .env 2026-04-17 15:18:05 +03:00
erjemin bcc10de4d4 minor: комментарии fix имя файла 2026-04-16 21:28:18 +03:00
erjemin 55f9a74231 del: нет логам в репозитории 2026-04-16 21:20:24 +03:00
erjemin 49c19d9f92 mod: добавлен сервис пересчета рейтинга 2026-04-16 21:15:19 +03:00
erjemin 67a8c093f5 mod: add favicon 2026-04-16 21:13:53 +03:00
erjemin e0a5a5739b minor: del print 2026-04-16 21:12:31 +03:00
erjemin 3e67d6cf71 mod: switch dev to sqlite and add static fallback paths 2026-04-16 21:10:58 +03:00
erjemin 917bcb42c8 minor: del print 2026-04-16 20:37:45 +03:00
erjemin 5bdb97f5e8 add: initial commit — Django 4.1 project (2022 version), docs, security templates 2026-04-16 15:55:21 +03:00
erjemin e2091b72b1 Static: недостающие файлы из error.log 2023-03-04 00:20:53 +03:00
erjemin 446cbe8c5c SEO: Title & Description 2023-03-04 00:19:41 +03:00
erjemin bdca8dfc6a SEO: Title & Description (каталог профилей) 2023-02-17 22:01:15 +03:00
erjemin 111dd7bd33 SEO: Title & Description 2023-02-17 19:18:18 +03:00
erjemin 57a522f4b7 minor: недостающая статика (png) 2023-02-17 19:12:02 +03:00
erjemin 2b37f709a9 minor: недостающая статика (css, js) 2023-02-15 23:51:25 +03:00
erjemin f480dc3666 minor: favicon.svg 2023-02-10 20:39:25 +03:00
erjemin 2da036e4e0 minor: убрал отладочный print() 2023-01-29 23:01:26 +03:00
erjemin 28620ce2a9 minor: excel-шаблоны для загрузки и заполнения 2023-01-29 22:57:14 +03:00
erjemin 574ae84321 minor: первый экран, изменен текст (SEO) 2023-01-29 18:20:48 +03:00
erjemin 796cb89525 minor: кеш картинок и шаблонов (++) 2023-01-19 00:52:52 +03:00
erjemin 0e8839bf18 minor: кеш картинок и шаблонов (+) 2023-01-19 00:46:06 +03:00
erjemin dbd622e456 minor: кеш картинок и шаблонов 2023-01-18 00:38:03 +03:00
erjemin 741dd2685e minor: вот так работает в продашн 2023-01-18 00:15:58 +03:00
erjemin c01873144e ini для uwsgi (tested) 2023-01-14 04:22:25 +03:00
erjemin 8387d36abf конфиг для nginx (tested) 2023-01-14 04:21:41 +03:00
erjemin 2bc0869104 обновленные пакеты окружения для dev на mac-m1 (python 3.10.9) 2023-01-14 04:04:08 +03:00
erjemin 8c7972178c имя файла к touch-reload для девопа 2023-01-14 04:02:05 +03:00
erjemin c338a17dd8 передергивает touch-reload при изменении кеш-шаблона 2023-01-14 03:49:55 +03:00
erjemin 3cc4ae39f7 minor 2023-01-14 02:42:49 +03:00
erjemin d789b2443a пакеты окружения (дев/прод) 2023-01-14 02:19:58 +03:00
erjemin 25beecdf60 пакеты окружения (дев/прод) 2023-01-14 02:19:39 +03:00
erjemin 5eb2f304a3 доки по профилям монблан 2023-01-14 02:18:46 +03:00
erjemin 0246c1de8c minor (error -- был пустой slag для текущей серии) 2023-01-14 01:43:14 +03:00
erjemin 4196cd64d8 minor (без переменной template) 2023-01-14 01:17:59 +03:00
erjemin 60b6582f1b minor (исправлены типы переменных для совместимости с продакшн) 2023-01-13 22:23:24 +03:00
erjemin 4a91928bc7 картинки для блога 2023-01-13 22:13:17 +03:00
erjemin b322331366 картинки к сериям домов 2023-01-13 22:08:21 +03:00
erjemin e041110059 логотипы компаний из базы 2023-01-13 21:57:56 +03:00
erjemin ae1b851db7 touch-reload.txt 2023-01-13 19:46:51 +03:00
erjemin 28aa35d617 nginx conf-file for production 2023-01-13 19:18:04 +03:00
erjemin b296ad79e9 favicon & other static 2023-01-13 17:27:16 +03:00
erjemin fbea58b1c4 Служебное -- формирование sitemap-файлов 2023-01-10 01:21:28 +03:00
erjemin 30084dd6ac minor -- точные размеры в мм. в заголовке и крошках 2023-01-09 23:30:49 +03:00
erjemin c961b64036 minor -- размеры и мм. тоже 2023-01-09 23:29:57 +03:00
erjemin ad74f0a51e Служебное -- главные служебные (если есть и нет аутентификации) 2023-01-08 19:41:12 +03:00
erjemin 78ec6077b6 Служебное -- главна и аутентификация 2023-01-08 19:36:48 +03:00
erjemin c68fa5be07 Служебное -- страница для тестирования верстки текста в блоге 2023-01-08 18:14:27 +03:00
erjemin ee1dd31d54 Переадресация для старых, внешних ссылок 2023-01-08 01:38:14 +03:00
erjemin 6066e5df29 Подгружаемый блок с детальным описанием состава рейтинга -- готово 2023-01-07 04:51:11 +03:00
erjemin 8704f4125b удалён лишний шаблон 2023-01-07 03:42:47 +03:00
erjemin 2777f3fde2 Вьюшка и шаблон: сравнение оконных наборов -- готово 2023-01-07 03:42:14 +03:00
erjemin 4b9ca319f9 кеш картинки со схемами открывания 2022-12-31 13:54:15 +03:00
erjemin 56d7af5c9d перенос и переименование шаблонов 2022-12-31 05:13:12 +03:00
erjemin 0368c1c34e Вьюшка: подгружаемые фреймы с ценами одного типового окна -- готово 2022-12-31 05:01:39 +03:00
erjemin fbdf95b66f minor 2022-12-31 04:44:38 +03:00
erjemin 8fdaf6935e minor (переименование шаблонов) 2022-12-31 04:44:11 +03:00
erjemin 8272bdb25f Вьюшка: Подгружаемых фреймов ценовых предложений -- minor 2022-12-31 04:36:40 +03:00
erjemin 6a4c315cdc Вьюшка: Подгружаемых фреймов ценовых предложений -- готово 2022-12-31 04:33:59 +03:00
erjemin f2c89abe1c Вьюшка : Выдача предложений -- minor (убраны отладочные print) 2022-12-31 02:57:49 +03:00
erjemin 37a605ef09 Вьюшка и шаблоны: Выдача предложений -- готово 2022-12-31 02:55:09 +03:00
erjemin 28a680fdf1 Отдельный каталог для шаблонов выпадающей окон -- minor 2022-12-30 19:58:56 +03:00
erjemin 9bcbc73602 Выпадающее окно после ввода адреса -- готово 2022-12-30 19:52:42 +03:00
erjemin 8ca1f5b36e Кеш-картинки схем открывания (большие и маленькие) 2022-12-30 15:31:17 +03:00
erjemin 6c4dcd1cb7 Ценовая выдача для одного окна 2022-12-30 15:30:05 +03:00
erjemin 5a38181cd3 Оптимизация + на мини-картинках схем открывания рисуется спейсер и москитная сетка 2022-12-29 17:25:41 +03:00
erjemin 5738ee9c9e Шаблон: "Каталог / Производители и поставщики окон / Компания" -- minor 2022-12-29 16:10:26 +03:00
erjemin 9535424e6a Шаблон: "Каталог / Производители и поставщики окон / Компания" -- minor: seo, Yandex.map 2022-12-29 15:56:14 +03:00
erjemin 4e03fead67 Вьюшка и шаблон: "Каталог / Производители и поставщики окон / Компания" -- готово 2022-12-29 15:46:16 +03:00
erjemin 5de28c8bfb Вьюшка: "Каталог / Производители и поставщики окон" -- minor 2022-12-29 00:18:53 +03:00
erjemin de45931ff4 Вьюшка и шаблон: "Каталог / Производители и поставщики окон" -- готово 2022-12-29 00:17:27 +03:00
erjemin d27d0e7173 Шаблоны prerender: "Каталог / Типовые серии зданий / Информация по серии" -- minor, SEO 2022-12-27 15:21:38 +03:00
erjemin f3a8a65afe Шаблон: "Каталог / Типовые серии зданий / Информация по серии" и "Статистика" -- minor seo 2022-12-27 15:19:08 +03:00
erjemin 72b71c7810 Шаблон и css: "Каталог / Оконные проёмы и балконные блоки" -- минор 2022-12-27 14:57:34 +03:00
erjemin 6bd8143d83 avatar image (для блога и "о компании") 2022-12-27 14:17:04 +03:00
erjemin a970b7109b prerender small-image со схемами открывания (2022-12-27) 2022-12-27 14:15:16 +03:00
erjemin 99b8ba80d5 Вьюшка и шаблон: "Каталог / Оконные проёмы и балконные блоки" -- готово 2022-12-27 14:13:29 +03:00
erjemin 7cc438d1c5 миграции 2022-12-26 18:30:26 +03:00
erjemin 034ec20c26 Про кеширующие шаблоны 2022-12-26 18:30:09 +03:00
erjemin 90ddc0d88c Вьюшка: "Информация по всем сериям домой" -- minor
перенесли одну формирования навигации по сериям
2022-12-26 18:28:52 +03:00
erjemin 6e92eca67a prerender image со схемами открывания (2022-12-26 минор) 2022-12-26 18:13:57 +03:00
erjemin 6a9c0e4568 Шаблоны prerender: "Каталог / Типовые серии зданий / Информация по серии" -- minor 2022-12-26 18:08:33 +03:00
erjemin 265732bae8 Шаблон: "Каталог / Типовые серии зданий / Информация по серии" -- minor (data, last modified, еxpires) 2022-12-26 18:04:35 +03:00
erjemin cded4e407e prerender image со схамами открывания (2022-12-26) 2022-12-26 18:02:20 +03:00
erjemin f996dd891e prerender image со схамами открывания (2022-12-26) 2022-12-26 17:42:59 +03:00
erjemin 748a1882e5 Вьюшка: "Каталог / Типовые серии зданий / Информация по серии" -- prerender template (2022-12-26) -
Эти шаблоны надо периодически удалять. Они нужны для скорости. Но если меняются данные по серии, размерам окон, появляются новые ком.предложения -- их надо удалять и тогда построятся новые.
2022-12-26 17:34:28 +03:00
50 changed files with 584 additions and 2606 deletions
-128
View File
@@ -1,128 +0,0 @@
# Git и версионирование
.git
.gitignore
.gitattributes
# Документация и инструкции
*.md
README.md
SETUP.md
AGENTS.md
MANAGEMENT_RUNBOOK.md
# Python и виртуальное окружение
__pycache__
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
pip-log.txt
pip-delete-this-directory.txt
# IDE и редакторы
.vscode
.idea
*.swp
*.swo
*~
.DS_Store
.project
.pydevproject
.settings/
*.sublime-workspace
*.sublime-project
# Тестирование и линтинг
.pytest_cache/
.coverage
htmlcov/
.tox/
.mypy_cache/
.hypothesis/
# Локальные данные и конфиги
.env
.env.local
local_settings.py
database/*.sqlite3
database/*.db
logs/
*.log
# Статика для разработки и bootstrap-стили
public/static/bootstrap-3.3.7-dist.zip
public/static/css/*.map
public/static/js/*.map
public/static/**/*.map
# Временные файлы
*.tmp
*.temp
*.bak
# Шрифты
*.ttf
*.otf
# Системные файлы macOS
.AppleDouble
.LSOverride
.Spotlight-V100
.Trashes
.VolumeIcon.icns
# Системные файлы Linux
.Xauthority
.Xdefaults
# Docker и CI/CD
docker-compose.override.yml
.dockerignore
Dockerfile*
# Node modules (если понадобятся в будущем)
node_modules/
npm-debug.log
yarn-error.log
# Конфиги оставляем как пример!
# config/ -> НЕ ИСКЛЮЧАЕМ (нужен как образец)
# *.ini, *.conf -> НЕ ИСКЛЮЧАЕМ (нужны как примеры)
# Django автогенерируемые файлы (создаются при старте контейнера, не нужны в образе)
# public/static_collected/ # собранная статика (создаётся при collectstatic)
# public/media/ # медиа-файлы (могут быть большие, создаются динамически)
# oknardia/templates/seria_info/prepared/ # пре-рендер шаблоны серий (создаются управ.командой)
# public/media/_serv_sitemap/ # sitemap'ы (создаются управ.командой)
# Примечание: Мы не исключаем эти директории потому что:
# 1. public/media/ может содержать важные медиа для разработки
# 2. пре-рендер шаблоны кэшируются для production оптимизации
# При сборке production образа они создаются автоматически в docker-compose через manage.py команды.
# Базы данных и кеши
*.sqlite3
*.db
database/oknadria_backup*.sqlite3
# Миграции можем оставить, они нужны для контейнера
# oknardia/oknardia/migrations/ -> НЕ ИСКЛЮЧАЕМ
+1 -29
View File
@@ -25,9 +25,6 @@ ADMINS=Admin:admin@example.com
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти) # URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
ADMIN_URL=admin/ ADMIN_URL=admin/
# CSRF Trusted Origins (для корректной работы CSRF при доступе к админке и другим формам с разных доменов/портов)
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://yourdomain.com
# ============================================================================ # ============================================================================
# DATABASE # DATABASE
# ============================================================================ # ============================================================================
@@ -38,7 +35,7 @@ DATABASE_ENGINE=django.db.backends.sqlite3
# Имя/путь базы данных: # Имя/путь базы данных:
# - для SQLite: только имя файла (полный путь соберется в settings.py через PROJECT_ROOT/database) # - для SQLite: только имя файла (полный путь соберется в settings.py через PROJECT_ROOT/database)
# - для MySQL/MariaDB: имя базы # - для MySQL/MariaDB: имя базы
DATABASE_NAME=oknardia.sqlite3 DATABASE_NAME=oknadria.sqlite3
# Для MySQL/MariaDB (используются, если DATABASE_ENGINE=django.db.backends.mysql) # Для MySQL/MariaDB (используются, если DATABASE_ENGINE=django.db.backends.mysql)
# DATABASE_HOST=localhost # DATABASE_HOST=localhost
@@ -117,16 +114,6 @@ LOG_LEVEL=INFO
# CELERY_BROKER_URL=redis://localhost:6379/0 # CELERY_BROKER_URL=redis://localhost:6379/0
# CELERY_RESULT_BACKEND=redis://localhost:6379/0 # CELERY_RESULT_BACKEND=redis://localhost:6379/0
# ============================================================================
# DOCKER: LOCAL PRODUCTION TESTING
# ============================================================================
# Разрешить Django обслуживать медиа-файлы через Python (только для локального тестирования)
# ВАЖНО: В настоящем production медиа и статику обслуживает Nginx, а не Django!
# Используется ТОЛЬКО в docker-compose.local-prod.yml для локального тестирования production конфигурации.
# На production сервере НЕ устанавливайте это значение в True!
ALLOW_MEDIA_SERVE=False
# ============================================================================ # ============================================================================
# ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ # ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ
# ============================================================================ # ============================================================================
@@ -152,18 +139,3 @@ ALLOW_MEDIA_SERVE=False
# - AWS Systems Manager Parameter Store # - AWS Systems Manager Parameter Store
# - HashiCorp Vault # - HashiCorp Vault
# ****************************************************************************************************************
# Системные пути на хосте (ТОЛЬКО ДЛЯ ПРОДАКШЕНА)
# Используется скриптом для генерации корректного Nginx конфига (alias к медиа-файлам).
# На локальной машине разработчика (Dev) эта переменная игнорируется.
# В ПРОДАКШЕНЕ: Укажите полный путь к папке проекта на сервере
HOST_PROJECT_PATH=/home/username/projects
# ****************************************************************************************************************
# Настройки доступа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать для обновления.
# Получить эти данные можно в настройках вашего репозитория, например:
# для GitHub: в разделе "Developer settings" -> "Personal access tokens";
# для Gitea: в разделе "Settings / Настройки" -> "Actions / Действия" -> "Secrets / Секреты".
REPO_USER=[login]
REPO_PASS=[token]
-72
View File
@@ -1,72 +0,0 @@
name: Build and Push Docker Image
run-name: Build and Push Docker Image ${{ github.ref_name }}
on:
push:
# Запускать сборку только при создании тега, начинающегося с 'v' (например, v1.0.0, v2.3.1)
tags:
- 'v*'
env:
REGISTRY: git.cube2.ru
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest # Или метка вашего раннера, если он специфичный (например, macos или self-hosted)
container:
image: catthehacker/ubuntu:act-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Настройка QEMU для мультиплатформенной сборки (если нужно собирать под разные архитектуры)
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# Настройка Docker Buildx (обязательно для build-push-action)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Логин в реестр Gitea
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_PASSWORD }}
# Извлечение метаданных (тегов и лейблов) для Docker
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
# Сборка и отправка образа
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
push: true
# Собираем под текущую архитектуру (linux/amd64).
# Если сервер и MacMini на разных архитектурах (x86 vs ARM), добавьте нужные, например: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
# ---
# Собираем только под linux/amd64 (для скорости + все равно сервер на x86).
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# ДОБАВЛЕНО для медленного интернета и оптимизации сборки:
cache-from: type=gha
cache-to: type=gha,mode=max
timeout: 1800 # Увеличено до 30 минут на всю сборку
-4
View File
@@ -164,7 +164,3 @@ db.json.zip
.log/ .log/
.logs/ .logs/
sitemap*.xml sitemap*.xml
# Django static files (собранная статика, пересоздается при деплое)
public/static_collected/
-361
View File
@@ -1,361 +0,0 @@
# Система двухуровневого кеширования страниц серий
## 📖 Описание
Система страниц серий домов использует **двухуровневое кеширование**:
1. **Статический кеш** — дорогостоящие данные (карты, графики, схемы), генерируются один раз и сохраняются на диск
2. **Динамические данные** — верхняя статья (редактируется через админку) и таблица оконных проёмов (показывает свежие предложения), пересчитываются при каждом запросе
---
## 🏗️ Архитектура кеша
Для каждой серии создаются **3 отдельных кешируемых файла**:
| Файл | Содержимое | Размер | Где появляется |
|------|-----------|--------|---|
| `{seria_id}_id_static_flaps.html` | Схемы открывания и типовые размеры окон | 4-7KB | В разделе "Дома серии: типовые размеры" |
| `{seria_id}_id_static_graph.html` | Google Charts: график ввода в эксплуатацию | 2-3KB | В контейнере height:300px |
| `{seria_id}_id_static_map_stats.html` | Yandex Maps + блок статистики | 6-46KB | Карта (col-md-7) + статистика (col-md-4) |
### Верхняя статья — ДИНАМИЧЕСКИЕ ДАННЫЕ, не кешируется!
**Верхняя статья про серию** (`THIS_SERIA_DESCRIPTION`) — это динамические данные, которые:
- Хранятся в БД (поле `sDescription` модели `Seria_Info`)
- **Редактируются через админку** → нужны изменения **без перезагрузки контейнера**
- Рендерятся всегда из БД, не сохраняются в кеш-файлы
Если бы мы кешировали верхнюю статью:
- Админ редактирует статью → кеш-файл не обновляется автоматически
- Нужно перезагрузить контейнер или вручную удалять файл кеша
- При регенерации кеша может произойти перезапись старыми данными
**Решение**: верхняя статья **всегда рендерится из БД**, как и таблица окон.
---
## 🔄 Логика работы
### При первом запросе seriesId (cache miss):
1. View `catalog_seria_info()` обнаруживает отсутствие кеш-файлов
2. Вычисляются дорогостоящие данные:
- Геокоординаты всех зданий серии
- Год ввода в эксплуатацию (для графика)
- Схемы открывания окон
3. Генерируются **3 отдельных файла** в `oknardia/templates/seria_info/prepared/`
4. Верхняя статья и таблица окон **рендерятся из БД** при каждом запросе
5. Контекст получает пути к 3 кеш-файлам + свежие динамические данные
6. Main-шаблон включает 3 кеш-файла + 2 динамических блока
7. Ответ отправляется пользователю
### При последующих запросах (cache hit):
1. View обнаруживает, что все 3 файла существуют
2. **Верхняя статья пересчитывается заново** из БД (от админа)
3. **Таблица окон пересчитывается заново** (новые предложения видны сразу)
4. Main-шаблон включает 3 статических файла + 2 динамических блока
5. Ответ отправляется быстро (схемы, график, геоданные из кеша)
### Ключевой мюмент: Всё свежее!
- Кеширование **не трогает** верхнюю статью и таблицу окон → они всегда свежие
- Новые цены от поставщиков видны пользователям **сразу**
- Админ может редактировать текст про серию → видно **без перезагрузки** контейнера
- Таблица пересчитывается в запросе через Q-фильтры к БД (есть индексы на БД)
---
## 📁 Структура файлов
### Templates (шаблоны)
```
oknardia/templates/seria_info/
├── all_seria_info_pre_light.html # Main-шаблон (включает 3 static-файла + динамику)
├── all_seria_info_pre_light_static_flaps.html # ШАГ 1: Схемы открывания (кешируется)
├── all_seria_info_pre_light_static_graph.html # ШАГ 2: График ввода (кешируется)
├── all_seria_info_pre_light_static_map_stats.html # ШАГ 3: Карта + статистика (кешируется)
└── prepared/ # Директория с кеш-файлами
├── 210_id_static_flaps.html
├── 210_id_static_graph.html
├── 210_id_static_map_stats.html
├── 100_id_static_flaps.html
└── ... (93 файла: 31 серия × 3 типа)
```
### Генерация кеша (Web-логика)
```
oknardia/web/
├── catalog_series.py # catalog_seria_info() — генерирует 3 файла + динамику
└── management/commands/
└── regenerate_seria_prerender.py # Batch-команда для регенерации всех серий
```
---
## 🚀 Управление кешем
### Разработка (DEV-режим)
```bash
python manage.py runserver
```
В DEV-режиме (`DEBUG = True`):
- Кеш **не используется** (всегда `PRE_RENDERED_STATIC_*_PATH = ""`)
- Main-шаблон рендерит данные напрямую
- Удобно для разработки (вижу изменения сразу)
### Production (PROD-режим)
#### Первоначальная генерация (все 31 серия):
```bash
python manage.py regenerate_seria_prerender
```
Вывод:
```
OK seria 100: 3 кеш-файла созданы
OK seria 12: 3 кеш-файла созданы
...
Готово. Обработано: 31. Создано/пересоздано: 31 × 3 файла. Пропущено: 0.
```
#### Регенерация конкретной серии:
```bash
python manage.py regenerate_seria_prerender --seria-id 210
```
#### Dry-run (без создания файлов):
```bash
python manage.py regenerate_seria_prerender --dry-run
```
#### Force-переписать даже если есть кеш:
```bash
python manage.py regenerate_seria_prerender --force
```
---
## 📊 Когда нужна регенерация кеша?
### ✅ Регенерируйте, если:
- Изменены координаты зданий (geo-данные)
- Добавлены новые здания в серию
- Обновлены годы ввода в эксплуатацию (для графика)
- Изменены схемы открывания окон
- Обновлена верхняя статья про серию
```bash
# Все изменилось → перестроить все
python manage.py regenerate_seria_prerender --force
# Изменилась одна серия → перестроить одну
python manage.py regenerate_seria_prerender --seria-id 210
```
### ❌ НЕ нужна регенерация, если:
- Добавлены новые **предложения** (цены от поставщиков)
- Обновлены **наличие/status** существующих предложений
- Система просто должна **показать свежие цены**
Таблица окон пересчитывается при каждом запросе автоматически! 🎉
---
## 🔌 Интеграция с Docker
В контейнере (PROD-режим):
```dockerfile
# Генерируем кеш пр запуске
RUN python manage.py regenerate_seria_prerender
# Запускаем сервер
CMD ["gunicorn", "oknardia.wsgi:application", "--bind", "0.0.0.0:8000"]
```
Кеш-файлы сохраняются на диск в томе, поэтому переживают перезагрузку контейнера.
---
## 📝 Контекстные переменные
### При первом запросе (происходит генерация):
View `catalog_seria_info()` отправляет в шаблон:
```python
to_template = {
"THIS_SERIA_ID": seria_id, # ID серии (210)
"THIS_SERIA_NAME": q_seria.sName, # Название ("1-335")
"THIS_SERIA_DESCRIPTION": html_description, # Верхняя статья
"FLAP_DIM": flap_dimensions, # Массив схем открывания
"DATA4GRAPH": graph_data, # Годы и кол-во домов
"DATA4GEO": geo_data, # Координаты зданий
"ACCOUNTS": buildings_count, # Кол-во квартир
"APARTMENTS": families_count, # Кол-во семей
"RESIDENTIAL_M2": total_area, # Площадь жилая
# ... и другие
}
```
### Для шаблонов генерации кеша:
Каждый template файл получает **все** эти переменные и рендерится отдельно.
### Для main-шаблона:
Main-шаблон получает пути только к 3 кеш-файлам:
```python
to_template.update({
"PRE_RENDERED_STATIC_FLAPS_PATH": "seria_info/prepared/210_id_static_flaps.html",
"PRE_RENDERED_STATIC_GRAPH_PATH": "seria_info/prepared/210_id_static_graph.html",
"PRE_RENDERED_STATIC_MAP_STATS_PATH": "seria_info/prepared/210_id_static_map_stats.html",
})
# Верхняя статья всегда передается как THIS_SERIA_DESCRIPTION и рендерится динамически
to_template.update({
"THIS_SERIA_DESCRIPTION": html_description, # Из БД, не кешируется
})
```
Main-шаблон использует `{% include %}` для 3 кеш-файлов, а верхнюю статью рендерит напрямую: `{{ THIS_SERIA_DESCRIPTION|safe }}`.
---
## 🎯 Примеры использования
### Добавили новое здание в серию 210
```bash
# Обновили БД через Django ORM/admin
python manage.py shell
>>> from oknardia.models import Building_Info
>>> Building_Info.objects.create(...)
# Регенерируем кеш только этой серии
python manage.py regenerate_seria_prerender --seria-id 210
# Пользователи видят новое здание на карте и в таблице
```
### Обновили года ввода в эксплуатацию
```bash
# Изменили данные
python manage.py shell
>>> from oknardia.models import Building_Info
>>> Building_Info.objects.filter(...).update(...)
# Кеш станет невалидным — нужна регенерация
python manage.py regenerate_seria_prerender --force
# Пользователи видят обновленный график
```
### Добавили новое предложение (цену)
```bash
# PriceOffer.objects.create(...) → система добавляет новое предложение
# НЕ нужна регенерация!
# Таблица окон обновилась сама на следующем запросе
# Пользователи видят новое предложение сразу
```
---
## 🐛 Отладка
### Проверить, какие кеш-файлы существуют:
```bash
ls -lah oknardia/templates/seria_info/prepared/
```
### Вручную удалить кеш (для тестирования):
```bash
# Удалить кеш одной серии
rm oknardia/templates/seria_info/prepared/210*.html
# Удалить ВСЕ кеш-файлы
rm oknardia/templates/seria_info/prepared/*_id_static_*.html
```
### Проверить содержимое кеш-файла:
```bash
cat oknardia/templates/seria_info/prepared/210_id_static_graph.html | head -20
cat oknardia/templates/seria_info/prepared/210_id_static_flaps.html | head -30
cat oknardia/templates/seria_info/prepared/210_id_static_map_stats.html | head -50
```
### Логирование:
В `catalog_series.py` логируем создание файлов:
```python
logger.info(f"Cache created: {file_flaps}")
logger.info(f"Cache created: {file_graph}")
logger.info(f"Cache created: {file_map_stats}")
# file_upper НЕ создается — верхняя статья рендерится динамически
```
---
## 📈 Производительность
### Без кеша (DEV-режим):
- ~2-3 сек на запрос (вычисляются geo, graph, flaps)
- Каждый запрос трогает БД
- Удобно для разработки
### С кешем (PROD-режим):
- ~100-300 мс на первый запрос (генерируется кеш)
- ~50-100 мс на последующие (включаются файлы + динамическая таблица)
- Статические данные из кеша (очень быстро)
- Таблица окон кешируется на уровне DB-запроса (индексы работают)
---
## ✅ Чек-лист для администратора
При развертывании в production:
- [ ] Запустить `python manage.py regenerate_seria_prerender` (сгенерировать все 93 файла)
- [ ] Проверить размеры файлов в `prepared/` (~100-200 KB всего)
- [ ] Протестировать на локалхосте `DEBUG = False`
- [ ] Проверить, что таблица окон обновляется при добавлении новых предложений
- [ ] Настроить логирование создания кеша в продакшене
---
## 🔗 Близкие компоненты
- **Main-шаблон**: `oknardia/templates/seria_info/all_seria_info_pre_light.html`
- **Динамическая таблица**: `oknardia/templates/seria_info/all_seria_info_pre_light_dynamic_include.html`
- **View**: `oknardia/web/catalog_series.py::catalog_seria_info()`
- **Management-команда**: `oknardia/web/management/commands/regenerate_seria_prerender.py`
- **Тесты**: `oknardia/web/test_prices.py` (проверяет свежесть таблицы)
---
**Версия:** 2.0 (Двухуровневое кеширование)
**Последнее обновление:** 2026-05-19
**Статус:** ✅ Production-ready
-88
View File
@@ -1,88 +0,0 @@
# =================================================
# STAGE 1: Builder - Установка зависимостей
# =================================================
FROM python:3.12-slim AS builder
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_DEFAULT_TIMEOUT=100
# Устанавливаем Poetry
RUN pip install --no-cache-dir --default-timeout=100 --retries 10 poetry poetry-plugin-export
# Создаем рабочую директорию
WORKDIR /app
# Копируем только файлы зависимостей для кэширования этого слоя
COPY pyproject.toml poetry.lock /app/
# Экспортируем lock-файл в requirements.txt и ставим зависимости через pip.
# Это обычно быстрее и проще для Docker, чем полноценная установка через Poetry.
RUN poetry export --format requirements.txt --without-hashes --with dev --output /tmp/requirements.txt \
&& pip install --no-cache-dir --default-timeout=100 --retries 10 -r /tmp/requirements.txt
# =================================================
# STAGE 2: Final - Создание чистого и безопасного образа
# =================================================
FROM python:3.12-slim AS stage-final
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=oknardia.settings
# Удалить: Для DEV окружения отключены создание непривилегированного пользователя
# и все связанные с ним операции chown — контейнер запускается от root,
# что избегает проблем с доступом к смонтированным томам (база, статика).
# RUN addgroup --system app && adduser --system --ingroup app app
# Создаем рабочую директорию (где находится manage.py)
WORKDIR /home/app/oknardia
# Копируем установленные Python-пакеты из builder-стадии
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# Удалить для dev: Копируем исходный код проекта. В DEV хозяин files не критичен (root).
# COPY --chown=1000:1000 . .
# Копируем весь проект в /home/app (корень проекта)
COPY . /home/app/
# Удалить для dev: Создание директорий и установка прав
# RUN mkdir -p /nginx_configs_host/nginx && chown -R 1000:1000 /nginx_configs_host
# RUN mkdir -p /home/app/web/public/staticfiles && chown -R 1000:1000 /home/app/web/public
# RUN mkdir -p /home/app/web/public/media/_error && chown -R 1000:1000 /home/app/web/public/media
# RUN mkdir -p /home/app/web/database && chown -R 1000:1000 /home/app/web/database
# Удалить для dev: USER 1000 — для DEV запускаем от root
# USER 1000
# Собираем статику
# Используем dummy ключ, так как .env файла нет на этапе сборки
# ВАЖНО: В DEV режиме collectstatic запускается в docker-compose command, а не при сборке,
# чтобы избежать ошибок с недоступными файлами.
# RUN SECRET_KEY=dummy python oknardia/manage.py collectstatic --noinput --clear
# Открываем порт
EXPOSE 8000
# Проверка здоровья контейнера
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
# Параметры:
# --interval=30s - проверка каждые 30 секунд
# --timeout=3s - ожидаем ответ максимум 3 секунды
# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой
# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1
# Переходим в директорию с manage.py для корректного запуска Django
WORKDIR /home/app/oknardia
# Команда запуска по умолчанию (для продакшена).
# В DEV режиме используется runserver через docker-compose.local.yml,
# который автоматически отдаёт статику и имеет auto-reload.
CMD ["python", "-m", "gunicorn", "--workers", "2", "--bind", "0.0.0.0:8000", "oknardia.wsgi:application"]
+13 -45
View File
@@ -394,72 +394,40 @@ location = /sitemap.xml {
## 4) Команда `regenerate_seria_prerender` ## 4) Команда `regenerate_seria_prerender`
Назначение: Назначение:
- пересобрать pre-render шаблоны для **статических данных** страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`. - пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`.
- генерирует **3 типа файлов** для каждой серии (верхняя статья НЕ кешируется, она рендерится динамически из БД):
- `{seria_id}_id_static_flaps.html` — схемы открывания
- `{seria_id}_id_static_graph.html` — график ввода в эксплуатацию
- `{seria_id}_id_static_map_stats.html` — карта + статистика
### Контекст: двухуровневое кеширование Проверка без записи файлов:
Система использует двухуровневое кеширование:
- **Статический кеш** — дорогостоящие операции (геокоординаты, графики, схемы). Генерируются один раз и сохраняются на диск.
- **Динамические данные** — верхняя статья про серию (может редактироваться через админку, видно БЕЗ перезагрузки контейнера) и таблица оконных проёмов (показывает актуальные предложения).
Подробнее: см. [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md)
### Проверка без записи файлов (dry-run):
```bash ```bash
cd /Users/e-serg/PRJ/2022-oknardia cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --dry-run poetry run python oknardia/manage.py regenerate_seria_prerender --dry-run
``` ```
Пересборка только отсутствующих файлов (стандартный запуск): Пересборка только отсутствующих файлов:
```bash ```bash
cd /Users/e-serg/PRJ/2022-oknardia cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender poetry run python oknardia/manage.py regenerate_seria_prerender
``` ```
Вывод: Принудительная пересборка всех root-серий:
```
OK seria 210: 3 кеш-файла созданы
OK seria 100: 3 кеш-файла созданы
...
Готово. Обработано: 31. Создано/пересоздано: 31 × 3 файла. Пропущено: 0.
```
Принудительная пересборка всех root-серий (даже если кеш существует):
```bash ```bash
cd /Users/e-serg/PRJ/2022-oknardia cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --force poetry run python oknardia/manage.py regenerate_seria_prerender --force
``` ```
Выборочная пересборка (конкретные серии): Выборочная пересборка:
```bash ```bash
cd /Users/e-serg/PRJ/2022-oknardia cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force
``` ```
### Когда запускать Когда запускать:
- после обновления логики `catalog_seria_info`;
- **После первого развертывания** — сгенерировать кеш всех 31 серии один раз. - после массового обновления данных серий/окон/квартир;
- **После обновления логики `catalog_seria_info`** — изменились параметры графика или карты. - после очистки `seria_info/prepared/`.
- **После изменения координат зданий** — geo-данные обновлены.
- **После добавления новых зданий в серию** — карта и список здаий изменились.
- **По расписанию** (опционально, если данные по геокоординатам обновляются):
```bash
0 3 * * 0 cd /Users/e-serg/PRJ/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_prerender >> /var/log/oknardia-prerender.log 2>&1
```
### Когда НЕ нужна регенерация
- **При добавлении новых предложений/цен** — таблица окон обновляется при каждом запросе автоматически.
- **При редактировании верхней статьи через админку** — она рендерится динамически, кешируется НЕ нужно.
- **При изменении наличия/статуса профилей** — рейтинги пересчитываются запросом через `make_rating`.
## 5) Команда `populate_seo_fields` ## 5) Команда `populate_seo_fields`
@@ -564,14 +532,14 @@ print(f'Пусто sMetaKeywords: {posts.filter(sMetaKeywords=\"\").count()}')
✓ Записей обновлено в БД: 28 ✓ Записей обновлено в БД: 28
✗ Ошибок при обработке: 0 ✗ Ошибок при обработке: 0
Обновлено 28 записей успешно! Обновлено 28 записей успешно!
``` ```
### Откат и безопасность ### Откат и безопасность
- **Безопасна для повторного запуска** — пустые поля не изменяются при повторной работе. - **Безопасна для повторного запуска** — пустые поля не изменяются при повторной работе.
- **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';` - **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';`
- **Всегда используй `--dry-run`** перед первым запуском для проверки. - **Всегда используй `--dry-run`** перед первым запуском для проверки.
## 6) Команда `make_rating` ## 6) Команда `make_rating`
-242
View File
@@ -1,242 +0,0 @@
# Production Deployment Guide для Oknardia
## Структура на хосте
Структура директорий на production сервере должна быть:
```
~/docker-app/oknardia-site/
├── .env # Переменные окружения Django
├── config/ # Конфиги
│ └── nginx/
│ └── oknardia-app--external-nginx.conf # Конфиг Nginx (шаблон)
├── database/ # БД SQLite (создается автоматически)
│ └── oknardia.sqlite3
├── media/ # Медиа файлы, uploads, кеши
│ ├── img_avatar/
│ ├── img_seria/
│ ├── _serv_sitemap/
│ └── _error/ # Error pages (генерируются контейнером)
└── docker-compose.prod.yml # Конфиг Docker
```
## Установка на Production сервере
### 1. Создать директории и скопировать файлы
```bash
# На хосте
mkdir -p ~/docker-app/oknardia-site/{config/nginx,database,media}
# Скопировать docker-compose.prod.yml и .env из проекта
cp docker-compose.prod.yml ~/docker-app/oknardia-site/
cp .env ~/docker-app/oknardia-site/
# Скопировать конфиг nginx
cp config/nginx/oknardia-app--external-nginx.conf ~/docker-app/oknardia-site/config/nginx/
```
### 2. Настроить .env на сервере
Отредактировать `~/docker-app/oknardia-site/.env`:
```bash
# ОБЯЗАТЕЛЬНО заполнить эти переменные!
# Путь к проекту на хосте (используется для sed-замены в конфиге nginx)
HOST_PROJECT_PATH=/home/default_user/projects/oknardia-site
# Credentials для скачивания образа из реестра Gitea
REPO_USER=имя_пользователя_gitea
REPO_PASS=токен_из_личного_кабинета_gitea
# Остальное скопировать из локального .env
DEBUG=False
DJANGO_SECRET_KEY=...
```
**Важно:** `HOST_PROJECT_PATH` должен совпадать с корневой папкой проекта на хосте, куда ты скопировал docker-compose.prod.yml!
### 3. Запустить контейнеры
```bash
cd ~/docker-app/oknardia-site/
# Первый запуск (холодный старт)
docker compose -f docker-compose.prod.yml up -d
# Проверить логи
docker compose -f docker-compose.prod.yml logs -f web
# Проверить статус контейнеров
docker compose -f docker-compose.prod.yml ps
```
### 4. Проверить что nginx конфиг был сгенерирован
После первого запуска контейнер должен:
- Создать `/config/nginx/oknardia-app--external-nginx.conf` (боевой конфиг)
- Создать `/config/nginx/oknardia-app--external-nginx.conf.example` (exemplo)
```bash
ls -la ~/docker-app/oknardia-site/config/nginx/
```
### 5. Настроить Nginx на хосте
На хосте установить Nginx и скопировать/ссылаться на конфиг из контейнера:
```bash
# На хосте (например, Ubuntu 20.04)
sudo systemctl install nginx
# Включить сгенерированный конфиг в основной конфиг Nginx
# /etc/nginx/sites-available/oknardia или /etc/nginx/conf.d/oknardia.conf
# Пример:
sudo ln -s /home/default_user/projects/oknardia-site/config/nginx/oknardia-app--external-nginx.conf \
/etc/nginx/sites-enabled/oknardia
# Проверить и перезагрузить Nginx
sudo nginx -t
sudo systemctl reload nginx
```
## Команды для управления
```bash
cd ~/docker-app/oknardia-site/
# Запустить контейнеры (если они были остановлены)
docker compose -f docker-compose.prod.yml up -d
# Остановить контейнеры
docker compose -f docker-compose.prod.yml down
# Перезагрузить контейнеры (удалить и создать заново)
docker compose -f docker-compose.prod.yml down
docker compose -f docker-compose.prod.yml up -d
# Посмотреть логи
docker compose -f docker-compose.prod.yml logs -f web
docker compose -f docker-compose.prod.yml logs -f watchtower
# Выполнить команду внутри контейнера Django
docker compose -f docker-compose.prod.yml exec web python manage.py shell
# Пересоздать пре-рендер кеши вручную
docker compose -f docker-compose.prod.yml exec web python manage.py regenerate_seria_prerender
```
## Что делает docker-compose.prod.yml
### Сервис `web` (Django + Gunicorn)
При старте контейнера:
1. ✅ Применяет миграции БД (`migrate --noinput`)
2. ✅ Собирает статику (`collectstatic --noinput`)
3. ✅ Генерирует sitemap'ы (`generate_sitemaps`)
4. ✅ Пересоздает пре-рендер кеши для серий (`regenerate_seria_prerender`)
5. ✅ Пересчитывает рейтинги (`make_rating`)
6. ✅ Копирует конфиг nginx с авто-заменой путей (через `sed`)
7. ✅ Запускает Gunicorn на `127.0.0.1:8000`
### Сервис `watchtower` (авто-обновление)
- 👁️ Следит за реестром Gitea (проверка каждые 30 минут)
- 🔄 Автоматически скачивает новый образ, если он есть
- ♻️ По-graceful перезагружает контейнер web
- 🗑️ Удаляет старые образы (WATCHTOWER_CLEANUP=true)
Watchtower использует метку на контейнере `web`:
```yaml
labels:
- "com.centurylinklabs.watchtower.scope=oknardia-scope"
```
## Файлы, которые монтируются с хоста
| На хосте | В контейнере | Назначение |
|----------|----------|-----------|
| `./database/` | `/home/app/database/` | БД SQLite (КРИТИЧНО!) |
| `./media/` | `/home/app/public/media/` | Медиа файлы, uploads |
| `./config/` | `/nginx_configs_host/` | Конифиги (читаются и пишущутся контейнером) |
## Переменные окружения в docker-compose.prod.yml
```yaml
environment:
- DEBUG=False # Production: без debug
- DJANGO_LOG_LEVEL=INFO # Минимум логов
- ALLOW_MEDIA_SERVE=False # Nginx обслуживает медиа
- HOST_PROJECT_PATH=... # Путь для sed-замены в конфиге nginx
- REPO_USER=${REPO_USER} # Из .env (для Watchtower)
- REPO_PASS=${REPO_PASS} # Из .env (для Watchtower)
```
## Health Check
Контейнер имеет healthcheck, который проверяет доступность Django:
- Проверка каждые 3 минуты
- Таймаут: 12 секунд
- Unhealthy после 3 неудачных попыток
- Это критично для Watchtower (он ждет что контейнер станет healthy перед finalization)
## Troubleshooting
### ContainerError при запуске
Проверить логи:
```bash
docker compose -f docker-compose.prod.yml logs web
```
### БД не синхронизируется между перезагрузками
Убедиться что `./database/` правильно монтирует:
```bash
docker compose -f docker-compose.prod.yml exec web ls -la /home/app/database/
```
### Watchtower не обновляет контейнер
Проверить:
- Есть ли интернет на сервере
- Правильны ли REPO_USER и REPO_PASS в .env
- Существует ли образ `oknardia:latest` в реестре
```bash
docker compose -f docker-compose.prod.yml logs watchtower
```
### Nginx конфиг не обновился
Конфиг копируется при старте контейнера. Если нужно пересоздать:
```bash
docker compose -f docker-compose.prod.yml down
docker compose -f docker-compose.prod.yml up -d
```
Потом проверить:
```bash
cat ~/docker-app/oknardia-site/config/nginx/oknardia-app--external-nginx.conf
```
## Резервные копии
Важно регулярно бэкапить:
- `./database/oknardia.sqlite3` - БД
- `./media/` - Загруженные файлы
Пример cronjob для ежедневного бэкапа:
```bash
# /etc/cron.daily/backup-oknardia
#!/bin/bash
BACKUP_DIR="/backup/oknardia"
APP_DIR="/home/default_user/projects/oknardia-site"
mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/oknardia-$(date +%Y%m%d).tar.gz $APP_DIR
find $BACKUP_DIR -name "oknardia-*.tar.gz" -mtime +30 -delete # Удалять старше 30 дней
```
+9 -21
View File
@@ -41,8 +41,7 @@
# См. также: # См. также:
* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и кастом-операциям (регенерация кеша, рейтингов, sitemap и т.д.). * [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и batch-операциям, сниппеты.
* [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md) – двухуровневая система кеширования страниц серий, структура статик-шаблонов, управление кешем.
* [`AGENTS.md`](AGENTS.md) – контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии). * [`AGENTS.md`](AGENTS.md) – контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии).
* [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки. * [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки.
@@ -66,25 +65,14 @@
В папке `oknardia/templates/seria_info/prepared/` создаются пре-рендер HTML-шаблоны с информацией о сериях домов. В папке `oknardia/templates/seria_info/prepared/` создаются пре-рендер HTML-шаблоны с информацией о сериях домов.
**Архитектура (май 2026)**: Для каждой серии создаются **3 отдельных кешируемых файла** (верхняя статья НЕ кешируется): Эти шаблоны создаются при первом обращении к странице серии и хранятся для ускорения последующих запросов.
* `{seria_id}_id_static_flaps.html` — схемы открывания окон **Важно**: их надо периодически удалять, особенно если меняются:
* `{seria_id}_id_static_graph.html` — график ввода в эксплуатацию * данные по сериям и размерам окон
* `{seria_id}_id_static_map_stats.html` — карта Яндекса и статистика * коммерческие предложения и цены
* рейтинги компонентов
**Верхняя статья рендерится динамически** из БД, поэтому изменения через админку видны без перезагрузки контейнера. **Рекомендация**: настроить cronjob на ежедневное или еженедельное удаление этих файлов. При обращении к соответствующим
страницам эти шаблоны будут пересозданы автоматически. На быстрых серверах можно вообще отключить кеширование, если оно
Таблица оконных проёмов **не кешируется** — пересчитывается при каждом запросе, поэтому новые предложения видны пользователям сразу. не критично для производительности.
**Регенерация кеша**:
```bash
python manage.py regenerate_seria_prerender # все сери
python manage.py regenerate_seria_prerender --seria-id 210 # конкретная серия
```
⏱️ **Когда регенерировать**: Изменены координаты зданий, добавлены новые здания, обновлены годы ввода в эксплуатацию.
**Когда НЕ нужна регенерация**: Добавлены новые предложения/цены (таблица обновляется автоматически), изменены статьи через админку (рендерятся динамически).
**Подробности**: см. [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md)
+280 -137
View File
@@ -1,187 +1,330 @@
# 🚀 SETUP.md — Первичная настройка Окнардии # 🚀 SETUP.md — Первичная настройка Окнардии
**Версия**: 0.2.0 | **Дата**: 18.05.2026 | **Docker**: ✅ Поддерживается **Версия**: 0.2.0 | **Дата**: 16.04.2026
Этот документ описывает пошаговую настройку проекта для разработки и деплоя. Этот документ описывает пошаговую настройку проекта для разработки и деплоя.
--- ## 📋 Предварительные требования
## 🐳 Быстрый старт: Docker Dev Environment - **Python**: 3.12+
- **Django**: 6.0+
- **MariaDB/MySQL**: 5.7+ или 8.0+
- **Redis** (опционально, для кеширования): 6.0+
- **Poetry** (для управления зависимостями)
### 1️⃣ Запуск контейнера ### На macOS:
```bash ```bash
cd /Users/e-serg/PRJ/2022-oknardia # Установка зависимостей (если не установлены)
docker compose -f docker-compose.local.yml up --build brew install mariadb-connector-c
brew install redis # опционально
``` ```
Сайт будет доступен на **http://localhost:8060** ## 🔑 Шаг 1: Конфигурация секретов
### 2️⃣ Основные команды ### 1.1 Создайте файл `my_secret.py`
```bash ```bash
# Просмотр логов в реальном времени cd oknardia/oknardia
docker compose -f docker-compose.local.yml logs web -f cp my_secret.py.template my_secret.py
nano my_secret.py # отредактируйте значения
# Зайти в контейнер (bash)
docker compose -f docker-compose.local.yml exec web bash
# Перезагрузить контейнер
docker compose -f docker-compose.local.yml restart web
# Остановить контейнер
docker compose -f docker-compose.local.yml down
``` ```
**✨ Особенности:** **Что нужно заполнить:**
- **Live reload** — при изменении кода автоматически перезагружается - IP адреса и хосты (MY_HOST_HOME2, MY_DATABASE_HOST_DEV2)
- **Синхронизция файлов** — база, медиа, статика синхронизированы с хостом - Пароль БД (MY_DATABASE_PASSWORD_DEV)
- **Миграции автоматические** — применяются при каждом старте - Email credentials (MY_EMAIL_HOST_USER_DEV, MY_EMAIL_HOST_PASSWORD_DEV)
- **DEBUG режим** — подробные ошибки и админка - Пути к файлам (MY_MEDIA_ROOT_DEV2, MY_STATIC_ROOT_DEV2)
- SECRET_KEY (сгенерируйте новый!)
👁️ **Подробнее про Docker разработку** → см. раздел **"🐳 Docker Development"** ниже. ### 1.2 (Опционально) Создайте файл `.env.local`
---
## 🐳 Docker Development
### Структура контейнера
```
/home/app/ # PROJECT_ROOT
├── oknardia/ # основная папка Django
│ ├── manage.py # точка входа
│ ├── oknardia/ # конфиг Django
│ ├── web/ # приложение
│ └── templates/ # шаблоны
├── database/ # SQLite БД (синхронизирована)
├── public/
│ ├── static/ # исходная статика
│ ├── static_collected/ # собранная статика
│ └── media/ # загруженные файлы
└── ...
```
### Volume mounts (синхронизация)
```yaml
volumes:
- .:/home/app
```
Это монтирует весь проект (`/Users/e-serg/PRJ/2022-oknardia`) в `/home/app` контейнера.
**Синхронизация:**
- Изменения на хосте сразу видны в контейнере
- Разные между хостом и контейнером сохраняются на диск
- БД и медиа-файлы персистент (не теряются при рестарте контейнера)
### Миграции в Docker
```bash ```bash
# Автоматические при запуске (через docker-compose command): cd /path/to/project
python manage.py migrate --noinput cp .env.example .env.local
nano .env.local # отредактируйте значения
```
# Или вручную внутри контейнера: **Примечание**: либо используйте `my_secret.py`, либо `.env.local`, выбирайте удобный способ.
docker compose -f docker-compose.local.yml exec web bash
python manage.py makemigrations ## 🗄️ Шаг 2: Настройка БД
### 2.1 Создайте БД и пользователя
```bash
# Подключитесь к MySQL
mysql -u root -p
# В MySQL консоли:
CREATE DATABASE django_oknardia_dev;
CREATE USER 'web'@'localhost' IDENTIFIED BY 'your-password';
GRANT ALL PRIVILEGES ON django_oknardia_dev.* TO 'web'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
### 2.2 Выполните миграции
```bash
cd /path/to/project/oknardia
python manage.py migrate python manage.py migrate
# Или непосредственно внутри контейнера:
docker compose -f docker-compose.local.yml exec web python manage.py migrate
``` ```
### Установка новых пакетов ### 2.3 Создайте суперпользователя
```bash ```bash
# 1. На хосте добавьте в pyproject.toml: python manage.py createsuperuser
poetry add some_package
# 2. Пересоберите контейнер:
docker compose -f docker-compose.local.yml up --build
# 3. Зависимости переустановят при старте контейнера
``` ```
### Создание суперюзера (админ) ## 📦 Шаг 3: Установка зависимостей
### Вариант 1: Poetry (рекомендуется)
```bash ```bash
docker compose -f docker-compose.local.yml exec web python manage.py createsuperuser # Установите poetry (если не установлен)
curl -sSL https://install.python-poetry.org | python3 -
# Установите зависимости
poetry install
# Активируйте виртуальное окружение
poetry shell
``` ```
### Очистка кеша и статики ### Вариант 2: pip (классический способ)
```bash ```bash
# Пересоберите статику: python -m venv venv
docker compose -f docker-compose.local.yml exec web python manage.py collectstatic --clear --noinput source venv/bin/activate # На Windows: venv\Scripts\activate
pip install -r requirements.txt
# Или удалите и пересоздайте:
rm -rf ./public/static_collected/*
docker compose -f docker-compose.local.yml restart web
``` ```
### DEBUG и логирование ## 🏃 Шаг 4: Запуск разработки
### 4.1 Запустите локальный сервер
```bash ```bash
# DEBUG=True включен по умолчанию cd oknardia
# Смотрите подробные логи: python manage.py runserver
docker compose -f docker-compose.local.yml logs web -f --tail=50
# Или с фильтром (только ошибки):
docker compose -f docker-compose.local.yml logs web --tail=100 | grep -i error
``` ```
### Типичные проблемы Docker Откройте браузер: **http://127.0.0.1:8000**
**"unable to open database file"** ### 4.2 Запустите задачи Celery (опционально)
```bash
# Проверьте что БД существует:
ls -la ./database/oknardia.sqlite3
# Если нет:
cp ./database/oknadria_backup-2026-05-12.sqlite3 ./database/oknardia.sqlite3
docker compose -f docker-compose.local.yml restart web
```
**"404 Not Found" для статики**
```bash
# Пересоберите статику:
docker compose -f docker-compose.local.yml exec web python manage.py collectstatic --noinput
docker compose -f docker-compose.local.yml restart web
```
**Контейнер падает**
```bash
# Смотрите полные логи:
docker compose -f docker-compose.local.yml logs web --tail=200
# Часто ошибка в settings.py или import'ах
```
**"Connection refused" при обращении в БД**
```bash
# Проверьте что контейнер запущен:
docker compose -f docker-compose.local.yml ps
# Перезагрузитесь:
docker compose -f docker-compose.local.yml down && docker compose -f docker-compose.local.yml up
```
### Очистка Docker
```bash ```bash
# Удалить контейнер и образ: celery -A oknardia worker -l info
docker compose -f docker-compose.local.yml down -v
# Удалить весь Docker мусор:
docker system prune -a --volumes
``` ```
--- ## 📁 Шаг 5: Создание необходимых директорий
## Дополнительные ресурсы
```bash
# Статика и медиа файлы
mkdir -p public/media
mkdir -p public/static
mkdir -p public/static/img/_flap.cfg
mkdir -p public/static/img/_miniflap.cfg
# Логи
mkdir -p logs
# Сгенерируйте статику
python manage.py collectstatic --noinput
```
## 🧪 Шаг 6: Тестирование
```bash
# Запустите тесты
python manage.py test
# С покрытием (если установлен coverage)
coverage run --source='.' manage.py test
coverage report
```
## 🔐 Шаг 7: Проверка безопасности
### 7.1 Django встроенная проверка
```bash
python manage.py check --deploy
```
### 7.2 Проверка на утечки секретов
```bash
# Установите инструмент
pip install truffleHog
# Проверьте репозиторий
truffleHog filesystem . --json
```
## ✅ Проверка готовности
Убедитесь, что все работает:
```bash
# 1. Статус БД
python manage.py dbshell < /dev/null && echo "✓ Database OK"
# 2. Статус приложений Django
python manage.py check && echo "✓ Django OK"
# 3. Статус файлов
test -d public/media && test -d public/static && echo "✓ Directories OK"
# 4. Тесты
python manage.py test 2>&1 | tail -5
```
## 🚀 Развертывание на продакшене
### Для разных хостов
**Masterhost VDS:**
```bash
# Установка окружения
export DJANGO_SECRET_KEY="your-production-key"
export DATABASE_PASSWORD="production-db-password"
export DATABASE_HOST="localhost"
export DEBUG="False"
# Запуск через uWSGI + Nginx
uwsgi --ini config/oknardia.ini
```
**Docker (рекомендуется):**
```bash
docker build -t oknardia:latest .
docker run -d \
-e DJANGO_SECRET_KEY="..." \
-e DATABASE_PASSWORD="..." \
-p 8000:8000 \
oknardia:latest
```
## 🛠️ Полезные команды
```bash
# Управление миграциями
python manage.py makemigrations # Создать миграцию
python manage.py migrate # Применить миграции
python manage.py migrate --fake-initial # Подделать первую миграцию
# Управление данными
python manage.py shell # Интерпретатор Python с контекстом Django
python manage.py dumpdata > backup.json # Резервная копия данных
python manage.py loaddata backup.json # Восстановление данных
# Статика и медиа
python manage.py collectstatic # Собрать статику для продакшена
python manage.py findstatic # Найти файлы статики
# Администрирование
python manage.py createsuperuser # Создать администратора
python manage.py changepassword username # Изменить пароль
# Очистка
python manage.py clearsessions # Удалить старые сессии
python manage.py remove_stale_contenttypes # Удалить устаревшие типы контента
# Служебные
python manage.py check # Проверить конфигурацию
python manage.py check --deploy # Проверка для продакшена
python manage.py generate_sitemaps # Оффлайн генерация sitemap XML
python manage.py regenerate_seria_prerender --dry-run # Проверка пересборки pre-render шаблонов серий
python manage.py regenerate_seria_prerender --force # Принудительная пересборка pre-render шаблонов серий
```
### Пересборка pre-render шаблонов серий (рекомендуемый сценарий)
Шаблоны для `catalog_seria_info` пересобираются оффлайн management-командой, без reload из кода Django.
```bash
cd /path/to/project
poetry run python oknardia/manage.py regenerate_seria_prerender --force
# затем (опционально) один внешний reload процесса приложения, если это требуется вашей конфигурацией
# sudo systemctl reload gunicorn
```
Для выборочной пересборки используйте `--seria-id` несколько раз:
```bash
poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force
```
## 📚 Дополнительные ресурсы
- [Django документация](https://docs.djangoproject.com/en/stable/) - [Django документация](https://docs.djangoproject.com/en/stable/)
- [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта - [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта
- [README.md](./README.md) — основная информация о проекте - [README.md](./README.md) — основная информация о проекте
## ❓ Решение проблем
### Проблема: `mysqlclient` не устанавливается на macOS
**Решение:**
```bash
brew install mariadb-connector-c
pip install mysqlclient
# или
brew unlink mariadb-connector-c # после установки
```
### Проблема: `ModuleNotFoundError: No module named 'oknardia'`
**Решение:**
```bash
# Убедитесь, что находитесь в правильной директории
cd /path/to/project/oknardia
python manage.py runserver
```
### Проблема: `OperationalError: (2002, "Can't connect to local MySQL server")`
**Решение:**
```bash
# Проверьте, что MySQL запущен
# macOS:
brew services start mariadb
# Linux:
sudo systemctl start mysql
# Проверьте credentials в my_secret.py или .env
```
### Проблема: миграции не применяются
**Решение:**
```bash
# Проверьте статус миграций
python manage.py showmigrations
# Примените все миграции
python manage.py migrate --run-syncdb
# Если проблема в конкретной миграции
python manage.py migrate app_name 0001 --fake
python manage.py migrate app_name
```
## 🤝 Общие вопросы
**Q: Где хранятся секреты?**
A: В `my_secret.py` (в .gitignore) или переменных окружения (.env)
**Q: Как запустить проект без интернета?**
A: Установите все зависимости заранее, используйте локальное хранилище медиа
**Q: Как работает система рейтинга?**
A: Смотрите [AGENTS.md](./AGENTS.md), раздел "Система рейтинга и ранжирования"
---
**Версия документа**: 1.0
**Последнее обновление**: 16.04.2026
**Автор**: GitHub Copilot
@@ -1,147 +0,0 @@
# config/nginx/oknardia-app--external-nginx.conf
# ==============================================================================
# КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker + Production)
# ==============================================================================
#
# ИНФОРМАЦИЯ:
# 1. Этот файл используется как шаблон при деплое
# 2. При первом деплое пути `/home/user/path-to-oknardia-app` заменяются на реальный путь через sed
# 3. Сгенерированный конфиг скопируется в `/etc/nginx/sites-available/oknardia`
# 4. Последующие деплои ОБНОВЛЯЮТ этот файл автоматически (sed + копирование)
#
# АРХИТЕКТУРА:
# - Nginx (порты 80/443) <-> Gunicorn контейнер (localhost:8000)
# - Медиа файлы отдаются из `/home/user/path-to-oknardia-app/media/` напрямую
# - Ошибки 5xx берутся из `media/_error` (копируются контейнером при старте)
# - Sitemap.xml отдается через Django/WhiteNoise (в media/_serv_sitemap/)
# - Static файлы (/static/) тоже отдаются через Django/WhiteNoise
# 1. Описываем, где живет наш Django в Docker
upstream oknardia-django {
# Мы пробрасываем порт 8050 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'oknardia-backend')
server 127.0.0.1:8060;
keepalive_requests 200;
}
# 2. Конфигурируем сервер
server {
server_name tmp.oknardia.ru; # Основное доменное имя
# Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL)
listen 80;
listen [::]:80;
charset utf-8;
client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок
# Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu)
access_log /var/log/nginx/oknardia.access.log;
error_log /var/log/nginx/oknardia.error.log;
# --- GZIP (Сжатие) ---
# Очень важно для динамического HTML от Django, который Gunicorn отдает несжатым.
gzip on;
gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding
gzip_proxied any; # Сжимать ответы, даже если мы за прокси
gzip_comp_level 6; # Оптимальный баланс скорость/сжатие
gzip_min_length 1000; # Не сжимать совсем мелочь
# Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно)
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml
image/x-icon
application/vnd.ms-fontobject
font/woff
font/woff2;
# --- МЕДИА ФАЙЛЫ (Загруженный контент) ---
# Nginx отдает их напрямую с диска хоста, не дергая Docker.
# Сюда входит: загруженные картинки, документы, свитмапы, ошибки 5xx
# Путь должен совпадать с тем, где лежит volume на хост-машине.
location /media/ {
alias /home/user/path-to-oknardia-app/media/;
expires 30d; # Кешируем картинки на месяц
add_header Cache-Control "public, no-transform";
}
# --- SITEMAP.XML ---
# Sitemap хранится в media/_serv_sitemap/ но должен быть доступен из корня
# для поисковиков (они ищут http://example.com/sitemap.xml)
location = /sitemap.xml {
alias /home/user/path-to-oknardia-app/media/_serv_sitemap/sitemap.xml;
expires 7d; # Кешируем на неделю (редко меняется)
add_header Cache-Control "public";
}
# --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
# Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
# Эти файлы копируются в `media/_error` при старте контейнера Docker.
#
# ВАЖНО: error_page директива перехватывает ошибки от апстрима (Gunicorn).
error_page 500 /500.html;
error_page 502 /502.html;
error_page 503 /503.html;
error_page 504 /504.html;
location = /500.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /502.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /503.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /504.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
error_page 400 /400.html;
error_page 401 /401.html;
error_page 403 /403.html;
error_page 404 /404.html;
error_page 413 /413.html;
error_page 429 /429.html;
location = /400.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /401.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /403.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /404.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /413.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
location = /429.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
error_page 405 406 407 408 409 410 411 412 414 415 416 417 418 421 422 423 424 425 426 428 431 451 /under_reconstruction.html;
location = /under_reconstruction.html { root /home/user/path-to-oknardia-app/media/_error; internal; }
# --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) ---
# Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера.
# Nginx просто прокидывает запрос внутрь.
location / {
proxy_pass http://oknardia-django;
# Передаем правильные заголовки, чтобы Django знал реальный IP и протокол
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Если нужно чтобы Django обрабатывал и HTTP, и HTTPS, то можно раскомментировать эту строку
# и передавать реальный протокол от клиента
# proxy_set_header X-Forwarded-Proto $scheme;
# Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx
# Даже если внутри контейнера это HTTP на 127.0.0.1:8050, для Django это должно быть HTTPS
proxy_set_header X-Forwarded-Proto https;
# Тайм-ауты (важно для долгих операций, если они есть)
proxy_read_timeout 180s;
proxy_connect_timeout 180s;
}
}
# 3. Редирект с www на основной домен (SEO best practice)
server {
server_name www.tmp.oknardia.ru;
listen 80;
listen [::]:80;
return 301 $scheme://tmp.oknardia.ru$request_uri;
}
+110
View File
@@ -0,0 +1,110 @@
# Разработка сайта OKNARDIA.RU
# == Конфикурационный файл nginx oknardia.conf
# Описываем апстрим-потоки которые должен подключить Nginx
# Для каждого сайта надо настроить свйо поток, со своим уникальным именем.
# Если будете настраивать несколько python (django) сайтов - измените название upstream
upstream oknardia-django {
# расположение файла Unix-сокет для взаимодействие с uwsgi
server unix:/home/web/oknardia-ru/socket/oknardia.sock;
# server ///home/web/oknardia-ru/socket/oknardia.sock;
# также можно использовать веб-сокет (порт) для взаимодействие с uwsgi. Но это медленнее
# server 127.0.0.1:8001; # для взаимодействия с uwsgi через веб-порт
keepalive_requests 200;
}
# конфигурируем сервер
server {
server_name tmp.cube2.ru; # доменное имя сайта
listen 80; # managed by Certbot
## listen 443 ssl http2; # managed by Certbot
## ssl_certificate /etc/letsencrypt/live/cadpoint.ru/fullchain.pem; # managed by Certbot
## ssl_certificate_key /etc/letsencrypt/live/cadpoint.ru/privkey.pem; # managed by Certbot
## include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
## ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# server_name 90.156.203.25; # доменное имя сайта
charset utf-8; # кодировка по умолчанию
access_log /home/web/oknardia-ru/logs/oknardia-access.log; # логи с доступом
error_log /home/web/oknardia-ru/logs/oknardia-error.log; # логи с ошибками
client_max_body_size 100M; # максимальный объем файла для загрузки на сайт (max upload size)
error_page 404 /404.html;
error_page 500 /500.html;
location /media { alias /home/web/oknardia-ru/public/media; } # Расположение media-файлов Django
location /static { alias /home/web/oknardia-ru/public/static; } # Расположение static-файлов Django
location /robots.txt { root /home/web/oknardia-ru/public; } # Расположение robots.txt
location /favicon.ico { root /home/web/oknardia-ru/public; } # Расположение favicon.ico
location /favicon.gif { root /home/web/oknardia-ru/public; } # Расположение favicon
location /favicon.png { root /home/web/oknardia-ru/public; } # Расположение favicon
location /favicon.svg { root /home/web/oknardia-ru/public; } # Расположение favicon
location /author.txt { root /home/web/oknardia-ru/public; } # Расположение author.txt
location = /404.html {
root /home/web/oknardia-ru/oknardia/templates/404.html;
internal;
}
location = /500.html {
root /home/web/oknardia-ru/oknardia/templates/500.html;
internal;
}
# location ~ \.(html|htm|ico|svg|png|gif|jpg|jpeg)$ {
location ~ \.(xml|html|htm|ico|svg|png|gif|jpg|jpeg)$ {
root /home/web/oknardia-ru/public; # Расположение статичных *.xml, *.html и *.txt
}
location / {
uwsgi_pass oknardia-django; # upstream обрабатывающий обращений
include uwsgi_params; # конфигурационный файл uwsgi;
proxy_set_header Host $host;
# ограничение количества запросов c одного IP-адреса с помощью модуля Limit_Req_Module
# limit_req zone=one burst=20 nodelay;
# one — имя зоны настроеной в /etc/nginx/nginx.conf (для всех сайтов сервера) в блоке http {…}
# burst — максимальный всплеск активности, можно регулировать до какого значения запросов
# в секунду может быть всплеск запросов;
# nodelay — незамедлительно, при достижении лимита подключений, выдавать код 503
# (Service Unavailable) для этого IP
fastcgi_keep_conn on;
uwsgi_read_timeout 1800; # некоторые запросы на Raspbery pi очень долго обрабатываются. Например, переиндексация.
uwsgi_send_timeout 200; # на всякий случай время записи в сокет
}
}
# переадресация с www на "без" www
server {
server_name www.tmp.cube2.ru;
listen 80;
return 301 http://tmp.cube2.ru$request_uri;
## listen 443 ssl; # managed by Certbot
## ssl_certificate /etc/letsencrypt/live/cadpoint.ru/fullchain.pem; # managed by Certbot
## ssl_certificate_key /etc/letsencrypt/live/cadpoint.ru/privkey.pem; # managed by Certbot
## include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
## ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# переадресация с http на https
##server {
## if ($host = oknardia.ru) {
## return 301 https://$host$request_uri;
## } # managed by Certbot
##
## listen 80;
## server_name cadpoint.ru;
## return 404; # managed by Certbot
##}
# переадресация с http на https для www
##server {
## if ($host = www.cadpoint.ru) {
## return 301 https://cadpoint.ru$request_uri;
## } # managed by Certbot
##
## listen 80;
## server_name www.cadpoint.ru;
## return 404; # managed by Certbot
##}
+63
View File
@@ -0,0 +1,63 @@
# === Конфигурационный файл uwsgi cadpoint.ini
[uwsgi]
# НАСТРОЙКИ ДЛЯ DJANGO
# Корневая папка проекта (полный путь)
chdir = /home/web/oknardia-ru/oknardia
# Django wsgi файл rsvo_new/wsgi.py записываем так:
module = oknardia.wsgi
# полный путь к виртуальному окружению
home = /home/web/oknardia-ru/env
# полный путь к файлу сокета
socket = /home/web/oknardia-ru/socket/oknardia.sock
# Исходящие сообщения в лог
daemonize = /home/web/oknardia-ru/logs/oknardia_uwsgi.log
# ЗАГАДОЧНЫЕ НАСТРОЙКИ, ПО ИДЕЕ ОНИ НУЖНЫ, НО И БЕЗ НИХ ВСЁ РАБОТАЕТ
# расположение wsgi.py
wsgi-file = /home/web/oknardia-ru/oknardia/oknardia/wsgi.py
# расположение виртуального окружения (как оно работает если этот параметр не указан, не ясно)
virtualenv = /home/web/oknardia-ru/env
# имя файла при изменении которого происходит авторестарт приложения
# (когда этого параметра нет, то ничего не авторестартится, но с ним все рестартится.
# Cтоит изменить любой Python-исходник проекта, как изменения сразу вступают в силу.
touch-reload = /home/web/oknardia-ru/logs/touch-reload.txt
py-autoreload = 5
# НАСТРОЙКИ ОБЩИЕ
# быть master-процессом
master = true
# максимальное количество процессов
processes = 1
# если uWSGI установлен как сервис через apt-get то нужно установить еще плагин:
# sudo apt-get install uwsgi-plugin-python
# и добавить в этот конфиг: plugin = python
plugin = python3
# права доступа к файлу сокета. По умолчанию должно хватать 664. Но каких-то прав не хватает, поэтому 666.
chmod-socket = 666
# очищать окружение от служебных файлов uwsgi по завершению
vacuum = true
# количество секунд после которых подкисший процесс будет перезапущен
# Так как некоторе скрипты требуют изрядно времени (особенно полная переиндексация) то ставим значение побольше
harakiri = 2600
# В общем случае, при некотых значениях harakiri логах uWSGI может вываливаться предупреждение:
# WARNING: you have enabled harakiri without post buffering. Slow upload could be rejected on post-unbuffered webservers
# можно оставить harakiri закомментированным, но нам нужно 900 и на него не ругается. Ругается на 30.
# разрешаем многопоточность
enable-threads = true
vacuum = true
thunder-lock = true
max-requests = 500
# пользователь и группа пользователей от имени которых запускать uWSGI
# указываем www-data: к этой группе относится nginx, и ранее мы включили в эту группу нашего [user]
# uid = nginx
# gid = nginx
# uid = root
# gid = root
uid = web
gid = web
print = ---------------- Запущен uWSGI для oknardia.ru ----------------
-90
View File
@@ -1,90 +0,0 @@
# ==============================================================================
# Docker Compose для PRODUCTION тестирования (Local Production Simulation)
# Этот файл содержит настройки для локального тестирования production конфигурации.
#
# ВАЖНО: Используется для отладки production сборки ЛОКАЛЬНО перед деплоем!
#
# Запуск: docker compose -f docker-compose.local-prod.yml up --build
# Остановка: docker compose -f docker-compose.local-prod.yml down
# ==============================================================================
services:
web:
# Имя контейнера для удобства
container_name: oknardia-backend-prod-test
# Сборка из текущей директории
build: .
# Пробрасывание портов (тестируем на другом порту, чтобы не конфликтовать с dev)
# Dev на 8060, Prod на 8061
ports:
- "8061:8000"
# 1. КОМАНДА ЗАПУСКА (Prod режим)
# Используем Gunicorn как в production.
# При старте контейнера:
# 1. Создаём медиа директорию
# 2. Применяем миграции
# 3. Собираем статику
# 4. Генерируем sitemap'ы
# 5. Пересоздаём пре-рендер шаблоны серий
# 6. Пересчитываем рейтинги профилей, стеклопакетов и наборов
# (метод Манна-Уитни: может занять 30-60 сек на БД с 94+ профилями)
# 7. Запускаем Gunicorn (нет live reload — это production режим!)
# (timeout=120 сек для запуска, достаточно для всех процедур инициализации)
command: >
sh -c "mkdir -p /home/app/public/media &&
python manage.py migrate --noinput &&
python manage.py collectstatic --noinput &&
python manage.py generate_sitemaps &&
python manage.py regenerate_seria_prerender &&
python manage.py make_rating &&
python -m gunicorn --workers 2 --bind 0.0.0.0:8000 --timeout 120 oknardia.wsgi:application"
# 2. МОНТИРОВАНИЕ КОДА И ДАННЫХ
# Подключаем весь проект целиком, чтобы Django правильно вычислил пути.
# В production файлы будут в контейнере, но для локального тестирования
# монтируем для удобства остановки/пересоздания.
volumes:
# Весь проект в /home/app
- .:/home/app
# 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ (Production)
env_file:
# Основной конфиг из .env (DATABASE, EMAIL, API ключи)
- .env
environment:
# ПЕРЕОПРЕДЕЛЯЕМ для production режима:
- DEBUG=False # ВАЖНО: выключен debug для production
- DJANGO_LOG_LEVEL=INFO # Вместо DEBUG (меньше логов)
# Для локального тестирования: разрешить отдачу медиа через Django (только test!)
# В production это обслуживает Nginx!
- ALLOW_MEDIA_SERVE=True
# 4. РЕСУРСЫ (Для локального тестирования можно без лимитов, но в реальном production нужны)
# deploy:
# resources:
# limits:
# cpus: '1'
# memory: 512M
# reservations:
# cpus: '0.5'
# memory: 256M
# 5. ЛОГИРОВАНИЕ
# Для отладки production проблем полезно видеть логи
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 6. ЗДОРОВЬЕ КОНТЕЙНЕРА
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
-67
View File
@@ -1,67 +0,0 @@
# ==============================================================================
# Docker Compose для РАЗРАБОТКИ (Local Development)
# Этот файл содержит настройки для локальной работы (live reload, debug).
# Запуск: docker compose -f docker-compose.local up --build
# ==============================================================================
services:
web:
# Имя контейнера для удобства
container_name: oknardia-backend-dev
# Сборка из текущей директории
build: .
# Пробрасывание портов (чтобы сайт был доступен на localhost:8060)
# Занятые порты можно посмотреть через `sudo ss -tulpn`
ports:
- "8060:8000"
# 1. КОМАНДА ЗАПУСКА (Dev режим)
# Используем Django runserver для разработки:
# - Автоматически отдаёт статику без WhiteNoise
# - Имеет встроенный auto-reload при изменении кода
# - Безопаснее и проще для dev, чем Gunicorn
# - Миграции применяются автоматически при каждом старте
# - Пересоздаём пре-рендер шаблоны и sitemap'ы для актуальности
command: >
sh -c "python manage.py migrate --noinput &&
python manage.py collectstatic --noinput &&
python manage.py generate_sitemaps &&
python manage.py regenerate_seria_prerender &&
python manage.py runserver 0.0.0.0:8000"
# 2. МОНТИРОВАНИЕ КОДА (Live Reload)
# Подключаем весь проект целиком в /home/app для правильного вычисления путей PROJECT_ROOT.
# При изменении кода runserver автоматически перезагружается (auto-reload).
#
# Структура монтирования:
# Host: /Users/e-serg/PRJ/2022-oknardia (.)
# Container: /home/app
# ├── oknardia/ <- исходный код (manage.py находится здесь)
# ├── database/ <- SQLite БД для синхронизации между хостом и контейнером
# ├── public/ <- статика и медиа-файлы
# └── templates/ <- Django шаблоны
volumes:
- .:/home/app
# 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ
env_file:
# файл с переменными окружения для разработки
- .env
environment:
# На всякий случай принудительно включаем DEBUG и DEBUG-уровень логов (вдруг в .env что-то не так)
# Эти настройки более приоритетные, чем в .env (если дублируются).
- DEBUG=True
- DJANGO_LOG_LEVEL=DEBUG
# В dev нам не нужно ограничивать буферизацию так строго, но не помешает.
# 4. РЕСУРСЫ (Без лимитов для разработки)
# Удаляем секцию ограничений, чтобы локально использовать все доступные ресурсы хоста.
# deploy:
# resources:
# limits:
# cpus: ...
# memory: ...
# mem_limit: ...
-130
View File
@@ -1,130 +0,0 @@
# ==============================================================================
# Docker Compose для PRODUCTION
# Этот файл запускается на боевом сервере.
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod.yml up -d
# ==============================================================================
services:
# --- ОСНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE ---
web:
# Имя контейнера для удобства
container_name: oknardia-backend
# В production используем готовый, собранный образ из реестра (Gitea)
image: git.cube2.ru/erjemin/oknardia:latest
# ПЕРЕЗАПУСК при сбое
restart: always
# Метки для Watchtower (авто-обновление)
labels:
- "com.centurylinklabs.watchtower.scope=oknardia-scope"
# КОМАНДА ЗАПУСКА (Production режим)
# При старте контейнера:
# 1. Применяем миграции
# 2. Собираем статику
# 3. Генерируем sitemap'ы
# 4. Пересоздаём пре-рендер шаблоны серий
# 5. Пересчитываем рейтинги
# 6. Создаём папку конфигов nginx (если нет)
# 7. Копируем конфиг nginx с авто-заменой путей через sed
# (переменная HOST_PROJECT_PATH берется из .env и подставляется в контейнер)
# 8. Инициализируем боевой конфиг (если нет, копируем из примера)
# 9. Запускаем Gunicorn
command: >
sh -c "python manage.py migrate --noinput &&
python manage.py collectstatic --noinput &&
python manage.py generate_sitemaps &&
python manage.py regenerate_seria_prerender &&
python manage.py make_rating &&
mkdir -p /nginx_configs_host/nginx &&
sed \"s|/home/user/path-to-oknardia-app|$${HOST_PROJECT_PATH}|g\" /home/app/config/nginx/oknardia-app--external-nginx.conf > /nginx_configs_host/nginx/oknardia-app--external-nginx.conf.example &&
if [ ! -f /nginx_configs_host/nginx/oknardia-app--external-nginx.conf ]; then
cp /nginx_configs_host/nginx/oknardia-app--external-nginx.conf.example /nginx_configs_host/nginx/oknardia-app--external-nginx.conf;
echo 'INIT: Created new nginx config with correct paths';
fi &&
ERROR_DIR=/home/app/web/public/media/_error &&
mkdir -p "$$ERROR_DIR" &&
for code in 400 401 403 404 413 429 500 502 503 504; do
cp /home/app/oknardia/templates/error/$${code}.html "$$ERROR_DIR/$${code}.html";
done &&
cp /home/app/oknardia/templates/error/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" &&
python -m gunicorn --workers 2 --bind 0.0.0.0:8000 --timeout 120 oknardia.wsgi:application"
# Пробрасывание портов
# Слушаем только на localhost хоста для безопасности
ports:
- "127.0.0.1:8060:8000"
# МОНТИРОВАНИЕ ТОМОВ (Volumes)
volumes:
# БД SQLite
- ./database:/home/app/database
# Медиа файлы
- ./media:/home/app/public/media
# Конфиги nginx
- ./config:/nginx_configs_host
# Пользователь и права
user: "0:0"
# ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ (Production)
# .env файл содержит все sensitive данные (DB, Email, API keys, REPO_USER/PASS)
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=oknardia.settings
- PYTHONUNBUFFERED=1
- DEBUG=False
- DJANGO_LOG_LEVEL=INFO
- ALLOW_MEDIA_SERVE=False
# ЗДОРОВЬЕ КОНТЕЙНЕРА (Healthcheck)
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
interval: 3m
timeout: 12s
start_period: 60s
retries: 3
# ЛОГИРОВАНИЕ (Ротация)
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
# РЕСУРСЫ
deploy:
resources:
limits:
cpus: '0.75'
memory: 768M
mem_limit: 768M
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
watchtower:
image: containrrr/watchtower
container_name: oknardia_watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- REPO_USER=${REPO_USER}
- REPO_PASS=${REPO_PASS}
- WATCHTOWER_SCOPE=oknardia-scope
- WATCHTOWER_CLEANUP=true
- DOCKER_API_VERSION=1.44
- WATCHTOWER_WAIT_ON_TIMEOUT=60
- WATCHTOWER_LIFECYCLE_HOOKS=true
command: --interval 1800 --cleanup
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
+5 -87
View File
@@ -34,8 +34,6 @@ STATIC_SOURCE_ROOT = PUBLIC_ROOT / 'static'
env = environ.Env() env = environ.Env()
environ.Env.read_env(str(PROJECT_ROOT / '.env')) environ.Env.read_env(str(PROJECT_ROOT / '.env'))
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[])
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
@@ -47,11 +45,10 @@ SECRET_KEY = env(
ADMIN_URL = _normalize_admin_url(env(var='ADMIN_URL', default='admin/')) ADMIN_URL = _normalize_admin_url(env(var='ADMIN_URL', default='admin/'))
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
# PREDУПРЕЖДЕНИЕ БЕЗОПАСНОСТИ: не работайте в режиме DEBUG в продашене! # ПРЕДУПРЕЖДЕНИЕ БЕЗОПАСНОСТИ: не работайте в режиме DEBUG в продашене!
DEBUG = TEMPLATE_DEBUG = env.bool('DEBUG', default=False) DEBUG = TEMPLATE_DEBUG = env.bool('DEBUG', default=False)
# Допустимые хосты (+ 'testserver' для management команд типа regenerate_seria_prerender) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost'])
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost', 'testserver'])
# Настройки сообщений об ошибках когда все упало и т.п. # Настройки сообщений об ошибках когда все упало и т.п.
ADMINS = _env_admins(env.list('ADMINS', default=[])) ADMINS = _env_admins(env.list('ADMINS', default=[]))
@@ -142,6 +139,7 @@ DATETIME_FORMAT = 'Y-m-d H:i:s'
STATIC_URL = '/static/' STATIC_URL = '/static/'
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = str(PUBLIC_ROOT / 'media') MEDIA_ROOT = str(PUBLIC_ROOT / 'media')
# STATIC_ROOT отделен от исходной статики, чтобы избежать staticfiles.E002. # STATIC_ROOT отделен от исходной статики, чтобы избежать staticfiles.E002.
STATIC_ROOT = str(PUBLIC_ROOT / 'static_collected') STATIC_ROOT = str(PUBLIC_ROOT / 'static_collected')
@@ -159,42 +157,21 @@ STATICFILES_DIRS = [
str(STATIC_SOURCE_ROOT) str(STATIC_SOURCE_ROOT)
] if STATIC_SOURCE_ROOT.is_dir() else [] ] if STATIC_SOURCE_ROOT.is_dir() else []
# Django 5 требует явное описание хранилищ.
# `default` нужен для загружаемых файлов (FileField, ImageField, filer и подобное) и смотрит в `MEDIA_ROOT`.
# `staticfiles` остаётся отдельно: в dev используется обычная статика Django, в prod — WhiteNoise.
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {
'location': MEDIA_ROOT,
},
},
'staticfiles': {
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
},
}
# Путь к каталогу static для генерации кэш-файлов и служебных JS. # Путь к каталогу static для генерации кэш-файлов и служебных JS.
STATIC_BASE_PATH = str(STATIC_SOURCE_ROOT) STATIC_BASE_PATH = str(STATIC_SOURCE_ROOT)
# Определяем движок БД из переменной окружения (по умолчанию SQLite)
database_engine = env('DATABASE_ENGINE', default='django.db.backends.sqlite3') database_engine = env('DATABASE_ENGINE', default='django.db.backends.sqlite3')
if database_engine == 'django.db.backends.sqlite3': if database_engine == 'django.db.backends.sqlite3':
# Для SQLite принимаем только имя файла из env и кладем БД в PROJECT_ROOT/database. # Для SQLite принимаем только имя файла из env и кладем БД в PROJECT_ROOT/database.
sqlite_db_filename = Path(env('DATABASE_NAME', default='oknardia.sqlite3')).name sqlite_db_filename = Path(env('DATABASE_NAME', default='oknadria.sqlite3')).name
sqlite_db_path = PROJECT_ROOT / 'database' / sqlite_db_filename sqlite_db_path = PROJECT_ROOT / 'database' / sqlite_db_filename
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(sqlite_db_path), 'NAME': str(sqlite_db_path),
'OPTIONS': { }
'timeout': 20,
},
},
} }
else: else:
# База не SQLite (mariaDB, например): читаем все параметры подключения из env.
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': database_engine, 'ENGINE': database_engine,
@@ -337,62 +314,3 @@ CATALOG_SORTER_MAGIC_NUMBER_TIZER = 1
MAX_LEN_RING_LOG_BUFFER = 250 # МАКСИМАЛЬНЫЙ РАЗМЕР КОЛЬЦЕВОГО БУФЕРА MAX_LEN_RING_LOG_BUFFER = 250 # МАКСИМАЛЬНЫЙ РАЗМЕР КОЛЬЦЕВОГО БУФЕРА
YANDEX_MAPS_API_KEY = env('YANDEX_MAPS_API_KEY', default='') YANDEX_MAPS_API_KEY = env('YANDEX_MAPS_API_KEY', default='')
# ============================================================================
# Конфигурация в зависимости от режима разработки (DEBUG) vs. production
# ============================================================================
if DEBUG:
# В dev: стандартная отдача статики Django (без WhiteNoise/кэширования).
# Медиа-файлы отдаются через Django.
pass
else:
# В prod: WhiteNoise + CompressedStaticFilesStorage для оптимизации.
# Статика собирается с хешем в имени и кэшируется.
#
# ВАЖНО: Для production нужна полная настройка Nginx:
# - Nginx обслуживает статику (/static/) прямо из /home/app/public/static_collected/
# - Nginx обслуживает медиа (/media/) прямо из /home/app/public/media/
# - Nginx проксирует остальное на Gunicorn (:8000)
#
# Для локального тестирования production конфига этот файл симулирует Nginx.
# 1. Добавляем WhiteNoise в начало MIDDLEWARE (после SecurityMiddleware) для отдачи статики
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
# 2. Переводим staticfiles на WhiteNoise со сжатием
# ВАЖНО: используем CompressedStaticFilesStorage вместо CompressedManifestStaticFilesStorage,
# потому что Manifest не справляется с relative paths в CSS (например, jQuery UI).
# CompressedStaticFilesStorage сжимает файлы без manifest, что работает надежнее.
STORAGES['staticfiles'] = {
'BACKEND': 'whitenoise.storage.CompressedStaticFilesStorage', # noqa: F821
}
# 3. WhiteNoise конфиг: обслуживание корневых файлов из public/ (robots.txt, favicon.*, sitemap.xml и т.д.)
# Параметр WHITENOISE_LOCATION указывает WhiteNoise, где искать файлы помимо STATIC_ROOT
WHITENOISE_LOCATION = str(PUBLIC_ROOT)
# 4. MIME-типы для шрифтов (иначе браузер может не загрузить)
WHITENOISE_MIMETYPES = {
'.woff': 'font/woff',
'.woff2': 'font/woff2',
}
# 5. Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например,
# robots.txt, favicon.ico и т.п.)
# WHITENOISE_ROOT = PUBLIC_ROOT
# 6. Кэширование неизменяемых файлов (с хешем в имени) на 1 год в браузере
# ВАЖНО: лямбда должна принимает ДВА аргумента: path и url (как требует WhiteNoise)
WHITENOISE_IMMUTABLE_FILE_TEST = lambda path, url: 'CACHE' in path
# 7. ЛОКАЛЬНЫЙ ТЕСТ: для отдачи медиа в docker-compose.local-prod.yml
# В реальном production это обслуживает Nginx! Никогда не используй в production!
# Добавляем StaticFilesHandler который обслуживает и медиа и статику
if env.bool('ALLOW_MEDIA_SERVE', default=False):
# Для локального тестирования добавляем обслуживание медиа через Django
# ВАЖНО: это очень медленно и небезопасно для production!
from django.conf.urls.static import static
# Будет добавлено в urls.py при импорте: urlpatterns += static(MEDIA_URL, document_root=MEDIA_ROOT)
+19 -115
View File
@@ -1,82 +1,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""oknardia Конфигурация URL""" """oknardia Конфигурация URL
from django.contrib import admin
from django.urls import include, re_path, path
from django.http import FileResponse
from pathlib import Path
import environ
import mimetypes
import os
# Инициализируем env Список `urlpatterns` направляет URL-адреса в представления. Дополнительную информацию см.:
env = environ.Env() https://docs.djangoproject.com/en/4.1/topics/http/urls/
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent Примеры:
environ.Env.read_env(str(PROJECT_ROOT / '.env')) Представления функций
1. Добавьте import: из представлений импорта my_app
2. Добавьте URL-адрес в urlpatterns: path('', views.home, name='home')
Представления на основе классов
1. Добавьте импорт: from other_app.views import Home
2. Добавьте URL-адрес в шаблоны URL-адресов: path('', Home.as_view(), name='home')
Включение другой конфигурации URL
1. Импортируйте функцию include(): из django.urls import include, path
2. Добавьте URL-адрес в urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path, re_path
from django.conf.urls.static import static
from oknardia.settings import * from oknardia.settings import *
from web import views, autocomplete_addr, user_manager, blog, diagrams, report1, report2, catalog, prices, service, \ from web import views, autocomplete_addr, user_manager, blog, diagrams, report1, report2, catalog, prices, service, \
catalog_profiles, catalog_series, catalog_openings, catalog_companies catalog_profiles, catalog_series, catalog_openings, catalog_companies
def _serve_public_root_file(request, path):
"""Отдает статик-файл из public/ с правильным Content-Type и UTF-8 для текстовых файлов."""
file_path = PUBLIC_ROOT / path
# Безопасность: проверяем, что файл гарантированно внутри PUBLIC_ROOT
try:
file_path = file_path.resolve()
file_path.relative_to(PUBLIC_ROOT.resolve())
except ValueError:
from django.http import Http404
raise Http404("Файл не найден")
if not file_path.is_file():
from django.http import Http404
raise Http404("Файл не найден")
# Определяем MIME type
mime_type, _ = mimetypes.guess_type(str(file_path))
# Для текстовых файлов добавляем charset=utf-8
if mime_type and mime_type.startswith('text'):
mime_type = f"{mime_type}; charset=utf-8"
response = FileResponse(open(file_path, 'rb'), content_type=mime_type)
# Определяем Content-Disposition в зависимости от типа файла:
# inline — браузер отображает/обрабатывает (иконки, картинки, CSS, JS, robots.txt, manifest)
# attachment — браузер скачивает (архивы, документы и т.д.)
inline_types = {
'image/', # все картинки (PNG, SVG, JPG и т.д.)
'text/', # все текстовые файлы
'application/javascript',
'application/json',
'application/xml',
'application/rss+xml',
'application/x-web-app-manifest+json', # manifest
}
if mime_type and any(mime_type.lower().startswith(t) for t in inline_types):
response['Content-Disposition'] = 'inline'
else:
# Для остального (неизвестные типы, архивы и т.д.) — скачивание
response['Content-Disposition'] = f'attachment; filename="{file_path.name}"'
return response
def _iter_public_root_files():
"""Находит все обычные файлы в корне public/, кроме служебных артефактов (начинающихся с точки)."""
if not PUBLIC_ROOT.exists():
return
for file_path in sorted(PUBLIC_ROOT.iterdir()):
if not file_path.is_file():
continue
# Пропускаем служебные файлы (начинающиеся с точки)
if file_path.name.startswith('.'):
continue
yield file_path.name
urlpatterns = [ urlpatterns = [
path(ADMIN_URL, admin.site.urls), path(ADMIN_URL, admin.site.urls),
@@ -161,51 +105,11 @@ urlpatterns = [
] ]
# Динамическая генерация URL patterns для всех файлов в корне public/
# (кроме служебных файлов, начинающихся с точки)
# В DEV режиме и при ALLOW_MEDIA_SERVE=True отдаются через Django,
# в PRODUCTION это должно обслуживать Nginx/WhiteNoise
PUBLIC_ROOT_URLPATTERNS = [
path(filename, _serve_public_root_file, {'path': filename})
for filename in _iter_public_root_files()
]
# Добавляем static файлы для корня ДО основных URL patterns
# (чтобы отдавать файлы быстро и не проверять остальные рулы)
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
handler404 = 'web.views.handler404'
handler400 = 'web.views.handler400'
handler403 = 'web.views.handler403'
handler500 = 'web.views.handler500'
# Для локального тестирования production конфига: отдача медиа через Django
# В реальном production медиа обслуживает Nginx!
if DEBUG or env.bool('ALLOW_MEDIA_SERVE', default=False):
from django.views.static import serve as serve_static
# Проверяем что директория медиа существует
if os.path.isdir(MEDIA_ROOT):
# Добавляем URL pattern для отдачи медиа файлов
urlpatterns += [
re_path(
r'^media/(?P<path>.*)$',
serve_static,
{'document_root': MEDIA_ROOT},
name='media'
),
]
if DEBUG: if DEBUG:
# Медиа-файлы
urlpatterns += static(MEDIA_URL, document_root=MEDIA_ROOT)
# --- страничка для тестирования верстки текста в блоге # --- страничка для тестирования верстки текста в блоге
urlpatterns += [re_path(r'^blog/tmp[/*]$', service.tmp),] urlpatterns += [re_path(r'^blog/tmp[/*]$', service.tmp),]
# --- странички для тестирования страниц с кодами ошибок
urlpatterns += [
re_path(r'^400$', views.handler400),
re_path(r'^403$', views.handler403),
re_path(r'^404$', views.handler404),
re_path(r'^500$', views.handler500),
]
# ___ ____ _ _____ _ _ _____ _ # ___ ____ _ _____ _ _ _____ _
# | | | | \ ___| |_ _ _ ___ |_ _|___ ___| | |_ ___ ___ | _ |___ ___ ___| | # | | | | \ ___| |_ _ _ ___ |_ _|___ ___| | |_ ___ ___ | _ |___ ___ ___| |
# |_ | | | | -_| . | | | . | | | | . | . | | . | .'| _| | __| .'| | -_| | # |_ | | | | -_| . | | | . | | | | . | . | | . | .'| _| | __| .'| | -_| |
+4 -6
View File
@@ -16,12 +16,10 @@
<meta name="document-state" content="{{ META_DOCUMENT_STATE|default:"Dynamic" }}" /> <meta name="document-state" content="{{ META_DOCUMENT_STATE|default:"Dynamic" }}" />
<meta name="generator" content="OKNARDIA 0.3β by Python/Django" /> <meta name="generator" content="OKNARDIA 0.3β by Python/Django" />
<title>{% block Title %}{% endblock %} : ОКНАРДИЯ</title> <title>{% block Title %}{% endblock %} : ОКНАРДИЯ</title>
<link rel="icon" type="image/png" href="{{ request.scheme }}://{{ request.get_host }}/favicon.png" sizes="96x96" /> <link rel="icon" href="{{ request.scheme }}://{{ request.get_host }}/favicon.svg" type="image/svg+xml ">
<link rel="icon" type="image/svg+xml" href="{{ request.scheme }}://{{ request.get_host }}/favicon.svg" /> <link rel="icon" href="{{ request.scheme }}://{{ request.get_host }}/favicon.png" type="image/png">
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="icon" href="{{ request.scheme }}://{{ request.get_host }}/favicon.gif" type="image/gif">
<link rel="apple-touch-icon" sizes="180x180" href="{{ request.scheme }}://{{ request.get_host }}/apple-touch-icon.png" /> <link rel="icon" href="{{ request.scheme }}://{{ request.get_host }}/favicon.ico" type="image/x-icon">
<meta name="apple-mobile-web-app-title" content="oknardia.ru" />
<link rel="manifest" href="{{ request.scheme }}://{{ request.get_host }}/site.webmanifest" />
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" type="text/css" />{# <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" type="text/css" />#} <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" type="text/css" />{# <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" type="text/css" />#}
<link href="{% static 'css/bootstrap-theme.min.css' %}" rel="stylesheet" type="text/css" />{# <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet" type="text/css" />#} <link href="{% static 'css/bootstrap-theme.min.css' %}" rel="stylesheet" type="text/css" />{# <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet" type="text/css" />#}
<link href="{% static 'css/oknardia1.css' %}" rel="stylesheet" type="text/css" />{% block Top_CSS1 %}{% endblock %}{% block Top_CSS2 %}{% endblock %}{% block Top_CSS3 %}{% endblock %} <link href="{% static 'css/oknardia1.css' %}" rel="stylesheet" type="text/css" />{% block Top_CSS1 %}{% endblock %}{% block Top_CSS2 %}{% endblock %}{% block Top_CSS3 %}{% endblock %}
-31
View File
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>400 — Некорректный запрос</title>
<!-- Аналитика: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru -->
<script src="/static/js/analytics.js" type="text/javascript"></script>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6; /* Полупрозрачность только для картинки */
z-index: -1; /* Уводит картинку под текст */
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/static/img/oknardia_logo.gif'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">400</i></h1>
<h2 style="color: #333; font-size: 40px;">Некорректный запрос</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Сервер не может обработать ваш запрос. Пожалуйста, проверьте корректность данных.</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>401 — Требуется авторизация</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">401</i></h1>
<h2 style="color: #333; font-size: 40px;">Требуется авторизация</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Для доступа к этому ресурсу требуется авторизация.</h4>
<p style="margin-top: 55px;"><a href="/login-logout">Войти в систему</a> или <a href="/">вернуться на главную</a></p>
</div>
</body>
</html>
-32
View File
@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>403 — Доступ запрещён</title>
<!-- Аналитика: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru -->
<script src="/static/js/analytics.js" type="text/javascript"></script>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6; /* Полупрозрачность только для картинки */
z-index: -1; /* Уводит картинку под текст */
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/static/img/oknardia_logo.gif'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">403</i></h1>
<h2 style="color: #333; font-size: 40px;">Доступ запрещён</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">У вас нет прав для доступа к этому ресурсу. Если вы считаете это ошибкой, свяжитесь с администратором.</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
-31
View File
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>404 — Страница не найдена</title>
<!-- Аналитика: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru -->
<script src="/static/js/analytics.js" type="text/javascript"></script>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6; /* Полупрозрачность только для картинки */
z-index: -1; /* Уводит картинку под текст */
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/static/img/oknardia_logo.gif'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">404</i></h1>
<h2 style="color: #333; font-size: 40px;">Страница не найдена</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">К сожалению, запрашиваемая страница не существует или была удалена.</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>413 — Файл слишком большой</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">413</i></h1>
<h2 style="color: #333; font-size: 40px;">Файл слишком большой</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Размер загружаемого файла превышает допустимый лимит (максимум 10 МБ).</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>429 — Слишком много запросов</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">429</i></h1>
<h2 style="color: #333; font-size: 40px;">Слишком много запросов</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Вы отправляете слишком много запросов. Пожалуйста, немного подождите и попробуйте снова.</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
-31
View File
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>500 — Внутренняя ошибка сервера</title>
<!-- Аналитика: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru -->
<script src="/static/js/analytics.js" type="text/javascript"></script>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6; /* Полупрозрачность только для картинки */
z-index: -1; /* Уводит картинку под текст */
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">500</i></h1>
<h2 style="color: #333; font-size: 40px;">Внутренняя ошибка сервера</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">На нашем сервере произошла непредвиденная ошибка. Команда разработчиков уже работает над решением проблемы.</h4>
<p style="color: #999; margin: 20px 0 0 30px; font-size: 14px;">Если проблема сохраняется, пожалуйста, свяжитесь с администратором: <a href="mailto:info@oknardia.ru">info@oknardia.ru</a></p>
</div>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>502 — Плохой шлюз</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">502</i></h1>
<h2 style="color: #333; font-size: 40px;">Плохой шлюз</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Сервер приложения временно недоступен. Мы работаем над восстановлением работоспособности.</h4>
<p style="color: #999; margin: 20px 0 0 30px; font-size: 14px;">Пожалуйста, попробуйте перезагрузить страницу через несколько минут. Если проблема сохраняется, свяжитесь с нами: <a href="mailto:info@oknardia.ru">info@oknardia.ru</a></p>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>503 — Сервис недоступен</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">503</i></h1>
<h2 style="color: #333; font-size: 40px;">Сервис недоступен</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Сайт временно недоступен для плановых работ. Мы скоро вернёмся!</h4>
<p style="color: #999; margin: 20px 0 0 30px; font-size: 14px;">Информация об обслуживании обновляется автоматически. Спасибо за терпение!</p>
</div>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>504 — Тайм-аут шлюза</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 120px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px">504</i></h1>
<h2 style="color: #333; font-size: 40px;">Тайм-аут шлюза</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Сервер приложения слишком долго отвечает на запрос. Это может быть перегрузка или технический сбой.</h4>
<p style="color: #999; margin: 20px 0 0 30px; font-size: 14px;">Попробуйте перезагрузить страницу через несколько минут. Если проблема повторится, свяжитесь с поддержкой: <a href="mailto:info@oknardia.ru">info@oknardia.ru</a></p>
</div>
</body>
</html>
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Технические работы</title>
<style>
.container::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: url('/media/img_seria/1LG-504D12_2.jpg');
opacity: 0.6;
z-index: -1;
background-position: 18% 15%;
background-repeat: no-repeat;
background-size: 200px auto;
}
</style>
</head>
<body>
<div class="container" style="margin: 10% 0 0 20%;">
<h1 style="font-size: 80px; margin: 0; color: #ccc; background-image: url('/media/img_seria/1LG-504D12_2.jpg'); background-position: left 65%; background-repeat: no-repeat; background-size: 400px auto;"><i style="padding-left: 420px"></i></h1>
<h2 style="color: #333; font-size: 40px;">Технические работы</h2>
<h4 style="color: #666; margin: 0 0 0 30px; font-size: 20px;">Приносим извинения. В данный момент сайт проходит техническое обслуживание. Мы скоро вернёмся!</h4>
<p style="margin-top: 55px;"><a href="/">Вернуться на главную oknardia.ru</a></p>
</div>
</body>
</html>
@@ -132,47 +132,40 @@ TechArticle: описывает страницу как технический
</div> </div>
</div>{# <!--- Хлебные крошки: КОНЕЦ ---> #} </div>{# <!--- Хлебные крошки: КОНЕЦ ---> #}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">{{ THIS_SERIA_DESCRIPTION|safe }}</div>
{# ВЕРХНЯЯ СТАТЬЯ: рендерится динамически (из БД), можно редактировать через админку #}
<div>{{ THIS_SERIA_DESCRIPTION|safe }}</div>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-10"> <div class="col-lg-10">
<h2 class="header">Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и&nbsp;схемы открывания</h2> <h2 class="header">Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и&nbsp;схемы открывания</h2>
</div>
<div class="col-lg-12" style="padding:1em 0 0 0;margin-left:-1em">
{# СХЕМЫ ОТКРЫВАНИЯ: статическая часть, если используется кеш #}
{% if PRE_RENDERED_STATIC_FLAPS_PATH %}
{% include PRE_RENDERED_STATIC_FLAPS_PATH %}
{% else %}
{% include 'report/show_big_flap_pictures.html' %}
{% endif %}
</div>
</div> </div>
<div class="col-lg-12" style="padding:1em 0 0 0;margin-left:-1em">
{% include 'report/show_big_flap_pictures.html' %}
</div>
</div>
<div class="row"> <div class="row">
<div class="col-lg-8 col-xs-12 col-md-offset-1"> <div class="col-lg-8 col-xs-12 col-md-offset-1">
<h3 class="header">Оконные проёмы в&nbsp;типовых квартирах <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3> <h3 class="header">Оконные проёмы в&nbsp;типовых квартирах <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
</div> </div>
<div class="col-lg-8 col-xs-12 col-md-offset-1"> <div class="col-lg-8 col-xs-12 col-md-offset-1">
{# ТАБЛИЦА ОКОН: часть, которая считается при каждом запросе #} {# --- ОСНОВНОЙ БЛОК С ТАБЛИЦЕЙ --- #}
{% include "seria_info/all_seria_info_pre_light_dynamic_include.html" %} {# Если есть кешированный файл, включаем его. Иначе - рендерим блок на лету. #}
{% if PRE_RENDERED_INCLUDE_PATH %}
{% include PRE_RENDERED_INCLUDE_PATH %}
{% else %}
{% include "seria_info/all_seria_info_pre_light_include.html" %}
{% endif %}
{# --- КОНЕЦ ОСНОВНОГО БЛОКА --- #}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-9"><a name="s_graph"></a> <div class="col-md-9"><a name="s_graph"></a>
<h2 class="header">Здания серия {{ THIS_SERIA_NAME }}: ввод в&nbsp;эксплуатацию по&nbsp;годам</h2> <h2 class="header">Здания серия {{ THIS_SERIA_NAME }}: ввод в&nbsp;эксплуатацию по&nbsp;годам</h2>
</div> </div>
<div class="col-md-9 col-md-offset-1" style="height:300px;font-size:large;" id="graph"> <div class="col-md-9 col-md-offset-1" style="height:300px;font-size:large;">
{# ГРАФИК: статическая часть, если используется кеш #} {% include 'seria_info/yaer_graph.html' %}
{% if PRE_RENDERED_STATIC_GRAPH_PATH %}
{% include PRE_RENDERED_STATIC_GRAPH_PATH %}
{% else %}
{% include 'seria_info/all_seria_info_pre_light_static_graph.html' %}
{% endif %}
</div> </div>
<div class="col-md-9 col-md-offset-1"> <div class="col-md-9 col-md-offset-1">
<div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div> <div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div>
@@ -183,31 +176,26 @@ TechArticle: описывает страницу как технический
<div class="col-md-7"><a name="s_map"></a> <div class="col-md-7"><a name="s_map"></a>
<h2 class="header">Строения серии {{ THIS_SERIA_NAME }} на&nbsp;карте</h2> <h2 class="header">Строения серии {{ THIS_SERIA_NAME }} на&nbsp;карте</h2>
</div> </div>
<div class="col-md-7 col-lg-offset-1"> <div class="col-md-7 col-lg-offset-1">
<p><small>Чтобы посмотреть цены на&nbsp;установку и&nbsp;замену окон от&nbsp;партнёров &laquo;Окнардия&raquo; в&nbsp;своей квартире: найдите дом на&nbsp;карте; кликните на&nbsp;него; перейдите по&nbsp;ссылке &laquo;Смотреть коммерческие предложения&raquo;. При необходимости смените типовую планировку квартиры (на&nbsp;странице ценовой выдачи, справа от&nbsp;изображения типовых проёмов и&nbsp;схем открывания).</small></p> <p><small>Чтобы посмотреть цены на&nbsp;установку и&nbsp;замену окон от&nbsp;партнёров &laquo;Окнардия&raquo; в&nbsp;своей квартире: найдите дом на&nbsp;карте; кликните на&nbsp;него; перейдите по&nbsp;ссылке &laquo;Смотреть коммерческие предложения&raquo;. При необходимости смените типовую планировку квартиры (на&nbsp;странице ценовой выдачи, справа от&nbsp;изображения типовых проёмов и&nbsp;схем открывания).</small></p>
<div style="height:350px;"> <div style="height:350px;">
<div id="SeriaMap" style="height: 100%;"></div> {% include 'seria_info/geo_map.html' with first_apart_id=TABLE_OF_WINDOWS.0.APART_ID %}
</div> </div>
<div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div> <div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div>
</div> </div>
{# КАРТА И СТАТИСТИКА: статическая часть, если используется кеш #} <diV class="col-md-4">
{% if PRE_RENDERED_STATIC_MAP_STATS_PATH %} <h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
{% include PRE_RENDERED_STATIC_MAP_STATS_PATH %} <p>Совокупно во&nbsp;всех зданиях типового проекта:</p>
{% else %} <ul>
<diV class="col-md-4"> <li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li>
<h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3> <li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li>
<p>Совокупно во&nbsp;всех зданиях типового проекта:</p> <li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li>
<ul> <li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong>&nbsp;— муниципальное жильё.</li>
<li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li> <li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и&nbsp;городские службы, учреждения бытового обслуживания, магазины, офисы и&nbsp;тому подобное.</li>
<li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li> <li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }}&nbsp;<strong>{{ CONDITION_MAX|stringformat:".2f" }}%</strong>. Минимальный&nbsp;<strong>{{ CONDITION_MIN|stringformat:".2f" }}%</strong>. </li>
<li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li> </ul>
<li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong>&nbsp;— муниципальное жильё.</li> </diV>
<li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и&nbsp;городские службы, учреждения бытового обслуживания, магазины, офисы и&nbsp;тому подобное.</li>
<li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }}&nbsp;<strong>{{ CONDITION_MAX|stringformat:".2f" }} %</strong>. Минимальный&nbsp;<strong>{{ CONDITION_MIN|stringformat:".2f" }} %</strong>. </li>
</ul>
</diV>
{% endif %}
</div> </div>
<div class="row"> <div class="row">
@@ -1,10 +1,5 @@
{# ============================================================================ #}
{# ДИНАМИЧЕСКИЕ ДАННЫЕ ДЛЯ СЕРИИ (НЕ кешируемая часть) #}
{# Содержит: Таблица раскладки окон по квартирам + статистика предложений #}
{# ЗАМЕЧАНИЕ: этот блок ЧАСТО меняется (при добавлении новых предложений) #}
{# ============================================================================ #}
<div class="col-md-9 col-xs-12" style="padding:0;"> <div class="col-md-9 col-xs-12" style="padding:0;">
<!--- прешаблон начало --->
<table style="padding:2px;"> <table style="padding:2px;">
{% for row in TABLE_OF_WINDOWS %} {% for row in TABLE_OF_WINDOWS %}
<tr class="tr2"> <tr class="tr2">
@@ -39,5 +34,5 @@
<td></td> <td></td>
</tr> </tr>
</table> </table>
<!--- прешаблон конец --->
</div> </div>
@@ -1,21 +0,0 @@
{# ============================================================================ #}
{# СХЕМЫ ОТКРЫВАНИЯ И РАЗМЕРЫ (кешируемая статическая часть) #}
{# ============================================================================ #}
{% load static %}
{% load filters %}
{% if WIN_DIM %}
{% for I_WIN_DIM in FLAP_DIM %}
<div class="win_discr pull-left" id="flap{{ forloop.counter0 }}">
<div><img src="{% static I_WIN_DIM.url2img %}" alt="{{ I_WIN_DIM.sDescription }}. Размер {{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 (Ш х В, мм.). Типовая схема открывания." title="{{ I_WIN_DIM.sDescription }}. Размер {{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 (Ш х В, мм.). Типовая схема открывания." itemprop="image" /></div>
<div class="caption" style="width:{{ I_WIN_DIM.W }}px;min-width:13ex;">
<nobr>{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0×{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0&thinsp;мм.</nobr><br />{% if not I_WIN_DIM.iQuantity == 0 %}
<nobr><b>{{ I_WIN_DIM.iQuantity }}&thinsp;шт.</b>{% for I_II in I_WIN_DIM.qStr %}<span class="color-bullet" style="background-image:url('{% static 'img/svg/mark' %}{{ I_II }}.svg');"></span>{% endfor %}</nobr><br />{% endif %}
{{ I_WIN_DIM.sDescription }}{% if not I_WIN_DIM.iQuantity == 0 %}<br />
<a href="/catalog/standard_opening/price-{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0mm-tip{{ I_WIN_DIM.id }}">цены только этого типового окна</a>{% endif %}
</div>
</div>{% endfor %}
{% else %}
<h1>Нет данных о проемах и рекомендованных схемах открывания окон</h1>
{% endif %}
@@ -1,55 +0,0 @@
{# ============================================================================ #}
{# ГРАФИК ВВОДА В ЭКСПЛУАТАЦИЮ (кешируемая статическая часть) #}
{# ============================================================================ #}
<script type="text/javascript" src="https://www.google.com/jsapi" type="text/javascript"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawChart);
function drawChart() {
let data = google.visualization.arrayToDataTable([
["Год", "Введено в эксплуатацию", {role:'style'}],{% for row in DATA4GRAPH %}
["{{ row.YEAR }}",{{ row.NUMS }},"color: #99{{ row.CLRS }}99"]{% if not forloop.last %},{% endif %}{% endfor %}
]);
let view = new google.visualization.DataView(data);
view.setColumns([0, 1,
{ calc: "stringify",
sourceColumn: 1,
type: "string",
role: "annotation" },
2
]);
let options = {
animation:{
duration: 1500,
easing: 'in',
startup: true
},
backgroundColor: "#EEEEEE",
bar: {groupWidth: "76.4%"},
chartArea: {left: "2%", top: "5%", width: '96%', height: '85%'},
dataOpacity: 0.76,
explorer:{
maxZoomIn: 0.20,
maxZoomOut: 32 },
vAxis: {
baselineColor:'grey',
gridlines:{color: 'silver', count: 7},
minorGridlines:{color: '#dddddd', count: 3},
textPosition: 'in',
textStyle: {fontSize: 10}
},
hAxis: { textStyle: {fontSize: 10} },
isStacked: true,
tooltip: {
textStyle:{color: 'grey', fontSize: 10 },
trigger: 'selection'
},
annotations: {textStyle: { fontSize: 8, bold: true, color: 'black', opacity: 0.8 }},
legend: { position: "none" }
};
let chart = new google.visualization.ColumnChart(document.getElementById("graph"));
chart.draw(data, options);
}
</script>
@@ -1,93 +0,0 @@
{# ============================================================================ #}
{# КАРТА И СТАТИСТИКА СЕРИИ (кешируемая статическая часть) #}
{# ============================================================================ #}
{% load humanize %}
{% load filters %}
{# БЛОК КАРТА: левая часть (col-md-7) #}
<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>
<script type="text/javascript">
// Функция для декодирования Base64-обфускованных геоданных (защита координат)
function decodeGeoData(b64str) {
try {
var json = atob(b64str);
return JSON.parse(json);
} catch(e) {
console.error('Ошибка декодирования геоданных:', e);
return [];
}
}
ymaps.ready(function () {
let myMap = new ymaps.Map('SeriaMap', {
center: [55.75, 37.57],
zoom: 10,
behaviors: ['default', 'scrollZoom'],
controls: [ 'rulerControl', 'zoomControl', 'geolocationControl', 'fullscreenControl' ]
});
// Создадим кластеризатор, вызвав функцию-конструктор.
clusterer = new ymaps.Clusterer({
preset: 'islands#invertedGrayClusterIcons',
groupByCoordinates: false,
hasHint: false,
viewportMargin: 0,
zoomMargin: 16,
clusterDisableClickZoom: false,
gridSize: 80
});
geoObjects = [];
const linkText = 'Смотреть коммерческие предложения</a>';
const hintText = '<b>Здание серии {{ THIS_SERIA_NAME }}</b>';
const apartmentId = {{ first_apart_id|default:0 }};
const seriaId = {{ THIS_SERIA_ID }};
const seriaSlug = '{{ THIS_SERIA_NAME_T }}';
// Декодируем обфускованные геоданные: [lat, lon, addr_id, seria_id]
var geoData = decodeGeoData('{{ DATA4GEO_B64 }}');
// Создаем метки для каждого здания серии
for(var i = 0, len = geoData.length; i < len; i++) {
const latitude = geoData[i][0];
const longitude = geoData[i][1];
const buildingId = geoData[i][2];
// Формируем SEO-URL для каждой метки
const balloonLink = `<a href="/price/seriaID${seriaId}--${seriaSlug}/appartID${apartmentId}/addressID${buildingId}--null">`;
geoObjects[i] = new ymaps.Placemark( [longitude, latitude],
{ // Содержимое иконки, балуна и хинта.
balloonContent: balloonLink + linkText,
hintContent: hintText
},
{ preset:'islands#circleIcon',iconColor: 'silver'} );
geoObjects[i].events
.add('mouseenter', function (e) {
e.get('target').options.set('preset', 'islands#yellowCircleIcon');
})
.add('mouseleave', function (e) {
e.get('target').options.set('preset', 'islands#grayCircleIcon');
});
}
// Добавляем метки в кластеризатор.
clusterer.add(geoObjects);
myMap.geoObjects.add(clusterer);
// позиционирование карты так, чтобы на ней были видны все объекты кластера.
myMap.setBounds(clusterer.getBounds(), { checkZoomRange: true });
});
</script>
{# БЛОК СТАТИСТИКА: правая часть (col-md-4) #}
<diV class="col-md-4">
<h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
<p>Совокупно во&nbsp;всех зданиях типового проекта:</p>
<ul>
<li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li>
<li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li>
<li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li>
<li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong>&nbsp;— муниципальное жильё.</li>
<li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и&nbsp;городские службы, учреждения бытового обслуживания, магазины, офисы и&nbsp;тому подобное.</li>
<li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }}&nbsp;<strong>{{ CONDITION_MAX|stringformat:".2f" }} %</strong>. Минимальный&nbsp;<strong>{{ CONDITION_MIN|stringformat:".2f" }} %</strong>. </li>
</ul>
</diV>
@@ -1,13 +1,4 @@
# Игнорируем все файлы в этой папке (генерируются динамически) ### Каталог для пре-рендер шаблонов
# Содержит кеш-файлы со статик-шаблонами: # Игнорируем все файлы в этом каталоге, так как они генерируются автоматически
# - *_id_static_flaps.html # и не должны быть включены в систему контроля версий.
# - *_id_static_graph.html *.*
# - *_id_static_map_stats.html
#
# Эти файлы генерируются при запуске management-команды:
# python manage.py regenerate_seria_prerender
#
# В продакшене они сохраняются на диск и включаются в шаблоны
# за счет системы двухуровневого кеширования.
*
!.gitignore
+19 -91
View File
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pathlib import Path
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, F, IntegerField, Value from django.db.models import Count, F, IntegerField, Value
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@@ -18,9 +17,6 @@ from web.report1 import get_last_all_user_visit_list
from web.add_func import get_flaps_for_big_pictures, sanitize_slug from web.add_func import get_flaps_for_big_pictures, sanitize_slug
import time import time
import os import os
import math
import base64
import json
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None: def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
@@ -93,15 +89,13 @@ def catalog_seria_info(
# чтобы тестировать актуальную серверную логику, а не сохраненный html-файл. # чтобы тестировать актуальную серверную логику, а не сохраненный html-файл.
if DEBUG: if DEBUG:
light_template = "seria_info/all_seria_info_pre_light.html" light_template = "seria_info/all_seria_info_pre_light.html"
static_include_path = "" # в DEV не используем кеш light_template_w_path = ""
is_hard_template = True is_hard_template = True
else: else:
# В PROD используем существующий pre-render include для статических данных (если есть). # В PROD используем существующий pre-render include при наличии на диске.
light_template = "seria_info/all_seria_info_pre_light.html" light_template = f"seria_info/prepared/{seria_id}_id.html"
static_template_filename = f"seria_info/prepared/{seria_id}_id_static.html" light_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{light_template}"
static_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{static_template_filename}" is_hard_template = not os.path.isfile(light_template_w_path)
is_hard_template = not os.path.isfile(static_template_w_path)
static_include_path = static_template_filename if not is_hard_template else ""
to_template: dict[str, object] = {} to_template: dict[str, object] = {}
# Получаем все уникальные проемы серии и сразу добавляем iQuantity=1 # Получаем все уникальные проемы серии и сразу добавляем iQuantity=1
@@ -198,80 +192,30 @@ def catalog_seria_info(
{ {
"WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win, "WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win,
"TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment, "TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment,
# Первая квартира из таблицы (нужна для картоки в пре-рендер шаблоне)
"first_apart_id": table_of_win_in_seria_by_apartmment[0]["APART_ID"] if table_of_win_in_seria_by_apartmment else 0,
} }
) )
# Для "тяжелого" шаблона получаем навигацию, карту и график. # Для "тяжелого" шаблона получаем навигацию, карту и график, затем кэшируем pre-render.
# ВАЖНО: таблица окон (TABLE_OF_WINDOWS) считается ВСЕГДА — она не кешируется!
if is_hard_template: if is_hard_template:
to_template.update(get_flaps_for_big_pictures(list_win_in_seria)) to_template.update(get_flaps_for_big_pictures(list_win_in_seria))
seria_id, for_seria_nav = seria_nav(seria_id) seria_id, for_seria_nav = seria_nav(seria_id)
to_template.update(for_seria_nav) to_template.update(for_seria_nav)
to_template.update(seria_info_year(seria_id)) to_template.update(seria_info_year(seria_id))
to_template.update(seria_info_geo_code(seria_id)) to_template.update(seria_info_geo_code(seria_id))
if not DEBUG: if not DEBUG:
# Пре-рендер ТРЁХ отдельных файлов для статических данных. # Пре-рендер происходит только для "включаемого" шаблона,
# Верхняя статья НЕ кешируется — она рендерится динамически, чтобы изменения # чтобы избежать дублирования базовой разметки.
# через админку были видны сразу без перезагрузки контейнера. string_prerender = render_to_string("seria_info/all_seria_info_pre_light_include.html", to_template)
prepared_dir = Path(TEMPLATES[0]["DIRS"][0]) / PATH_FOR_SERIA_INFO_HTML_INCLUDE with open(light_template_w_path, "w", encoding="utf-8") as file:
prepared_dir.mkdir(parents=True, exist_ok=True) file.write(string_prerender)
# Основной шаблон будет просто включать в себя уже готовый HTML
# 1. Схемы открывания и размеры light_template = "seria_info/all_seria_info_pre_light.html"
string_flaps = render_to_string( else:
"seria_info/all_seria_info_pre_light_static_flaps.html", to_template.update({"THIS_SERIA_NAME": q_seria.sName})
to_template # Указываем путь к кешированному файлу для include
) to_template.update({"PRE_RENDERED_INCLUDE_PATH": light_template})
file_flaps = prepared_dir / f"{seria_id}_id_static_flaps.html" # Основной шаблон должен быть один и тот же
with open(file_flaps, "w", encoding="utf-8") as f: light_template = "seria_info/all_seria_info_pre_light.html"
f.write(string_flaps)
# 2. График ввода в эксплуатацию
string_graph = render_to_string(
"seria_info/all_seria_info_pre_light_static_graph.html",
to_template
)
file_graph = prepared_dir / f"{seria_id}_id_static_graph.html"
with open(file_graph, "w", encoding="utf-8") as f:
f.write(string_graph)
# 3. Карта и статистика
string_map_stats = render_to_string(
"seria_info/all_seria_info_pre_light_static_map_stats.html",
to_template
)
file_map_stats = prepared_dir / f"{seria_id}_id_static_map_stats.html"
with open(file_map_stats, "w", encoding="utf-8") as f:
f.write(string_map_stats)
# Добавляем в контекст пути к кешируемым файлам (верхняя статья всегда динамична)
pre_rendered_flaps_path = ""
pre_rendered_graph_path = ""
pre_rendered_map_stats_path = ""
if not DEBUG:
# В production используем кеширующие файлы, если они существую
prepared_dir = Path(TEMPLATES[0]["DIRS"][0]) / PATH_FOR_SERIA_INFO_HTML_INCLUDE
file_flaps = prepared_dir / f"{seria_id}_id_static_flaps.html"
file_graph = prepared_dir / f"{seria_id}_id_static_graph.html"
file_map_stats = prepared_dir / f"{seria_id}_id_static_map_stats.html"
if file_flaps.exists():
pre_rendered_flaps_path = f"seria_info/prepared/{seria_id}_id_static_flaps.html"
if file_graph.exists():
pre_rendered_graph_path = f"seria_info/prepared/{seria_id}_id_static_graph.html"
if file_map_stats.exists():
pre_rendered_map_stats_path = f"seria_info/prepared/{seria_id}_id_static_map_stats.html"
to_template.update({
"THIS_SERIA_NAME": q_seria.sName,
"PRE_RENDERED_STATIC_FLAPS_PATH": pre_rendered_flaps_path,
"PRE_RENDERED_STATIC_GRAPH_PATH": pre_rendered_graph_path,
"PRE_RENDERED_STATIC_MAP_STATS_PATH": pre_rendered_map_stats_path,
})
_append_visit_context(to_template, request, time_start) _append_visit_context(to_template, request, time_start)
@@ -496,20 +440,4 @@ def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> d
"ACCOUNTS": accounts, "ACCOUNTS": accounts,
"CONDITION_MAX": condition_max, "CONDITION_MAX": condition_max,
"CONDITION_MIN": condition_min}) "CONDITION_MIN": condition_min})
# Кодируем геоданные в Base64 для защиты (используется в статик-шаблонах)
# Формат: [latitude, longitude, addr_id, seria_id] для каждого здания
geo_for_encoding = []
for geo_point in seria_to_geo:
geo_for_encoding.append([
float(geo_point["LATITUDE"]),
float(geo_point["LONGITUDE"]),
geo_point["ADDR_ID"],
geo_point["SER_ID"]
])
geo_json = json.dumps(geo_for_encoding, separators=(',', ':'))
geo_b64 = base64.b64encode(geo_json.encode('utf-8')).decode('utf-8')
data_return["DATA4GEO_B64"] = geo_b64
return data_return return data_return
@@ -14,20 +14,9 @@ from web.add_func import sanitize_slug
class Command(BaseCommand): class Command(BaseCommand):
"""Пересоздает pre-render шаблоны ля статических данных страниц серий (/catalog/seria/.../all<ID>). """Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all<ID>)."""
ВАЖНО: Кешируются ТОЛЬКО статические данные (схемы, график, карта, статистика). help = "Пересоздает pre-render шаблоны catalog_seria_info для выбранных или всех корневых серий."
Верхняя статья рендерится ДИНАМИЧЕСКИ из БД, чтобы изменения через админку
были видны без перезагрузки контейнера.
Таблица оконных проёмов также пересчитывается при каждом запросе.
Создаёт 3 файла для каждой серии:
- _id_static_flaps.html (схемы открывания)
- _id_static_graph.html (график ввода в эксплуатацию)
- _id_static_map_stats.html (карта и статистика)
"""
help = "Пересоздает pre-render шаблоны (3 файла) для статических данных выбранных или всех корневых серий."
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
@@ -40,7 +29,7 @@ class Command(BaseCommand):
parser.add_argument( parser.add_argument(
"--force", "--force",
action="store_true", action="store_true",
help="Пересоздать даже если pre-render файлы уже существуют.", help="Пересоздать даже если pre-render файл уже существует.",
) )
parser.add_argument( parser.add_argument(
"--dry-run", "--dry-run",
@@ -72,35 +61,26 @@ class Command(BaseCommand):
skipped = 0 skipped = 0
for seria in targets: for seria in targets:
# Проверяем существование всх 3 файлов (верхняя статья НЕ кешируется) target_file = prepared_dir / f"{seria.id}_id.html"
target_files = [ if target_file.exists() and not force:
prepared_dir / f"{seria.id}_id_static_flaps.html",
prepared_dir / f"{seria.id}_id_static_graph.html",
prepared_dir / f"{seria.id}_id_static_map_stats.html",
]
all_exist = all(f.exists() for f in target_files)
if all_exist and not force:
skipped += 1 skipped += 1
self.stdout.write(f"SKIP {seria.id}: все кеш-файлы уже существуют") self.stdout.write(f"SKIP {seria.id}: {target_file}")
continue continue
if dry_run: if dry_run:
action = "REGEN" if all_exist else "CREATE" action = "REGEN" if target_file.exists() else "CREATE"
self.stdout.write(f"{action} {seria.id}: 3 файла (flaps, graph, map_stats)") self.stdout.write(f"{action} {seria.id}: {target_file}")
planned += 1 planned += 1
continue continue
# Удаляем старые файлы перед пересоздаванием if target_file.exists():
for f in target_files: target_file.unlink()
if f.exists():
f.unlink()
slug = sanitize_slug(seria.sName) slug = sanitize_slug(seria.sName)
request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}") request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}")
# В команде принудительно включаем «production-mode» для вьюхи, # В команде принудительно включаем «production-mode» для вьюхи,
# чтобы она прошла тяжелую ветку и пересоздала pre-render файлы. # чтобы она прошла тяжелую ветку и пересоздала pre-render файл.
old_debug = catalog_series.DEBUG old_debug = catalog_series.DEBUG
try: try:
catalog_series.DEBUG = False catalog_series.DEBUG = False
@@ -112,29 +92,22 @@ class Command(BaseCommand):
raise CommandError( raise CommandError(
f"Серия {seria.id}: ожидался status=200, получен {response.status_code}." f"Серия {seria.id}: ожидался status=200, получен {response.status_code}."
) )
if not target_file.exists():
# Проверяем, что все 3 файла были созданы raise CommandError(f"Серия {seria.id}: pre-render файл не создан: {target_file}")
missing_files = [f for f in target_files if not f.exists()]
if missing_files:
raise CommandError(
f"Серия {seria.id}: не созданы файлы: {[f.name for f in missing_files]}"
)
created += 1 created += 1
self.stdout.write( self.stdout.write(self.style.SUCCESS(f"OK {seria.id}: {target_file}"))
self.style.SUCCESS(f"OK {seria.id}: 3 кеш-файла созданы")
)
if dry_run: if dry_run:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f"DRY-RUN. Обработано: {len(targets)}. Бдет создано/пересоздано: {planned}. Пропущено: {skipped}." f"DRY-RUN. Обработано: {len(targets)}. Будет создано/пересоздано: {planned}. Пропущено: {skipped}."
) )
) )
else: else:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created} × 3 файла. Пропущено: {skipped}." f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created}. Пропущено: {skipped}."
) )
) )
-63
View File
@@ -242,66 +242,3 @@ def get_address(request: HttpRequest) -> HttpResponse:
'ticks': float(time.perf_counter() - time_start), 'ticks': float(time.perf_counter() - time_start),
}) })
return render(request, "popup/popup_show_apartment_variants.html", to_template) return render(request, "popup/popup_show_apartment_variants.html", to_template)
# ============================================================================
# ОБРАБОТЧИКИ ОШИБОК ДЛЯ ТЕСТИРОВАНИЯ В DEBUG РЕЖИМЕ
# ============================================================================
# Используется в urls.py при DEBUG=True для тестирования верстки страниц ошибок.
# Позволяет визуально проверить, как выглядят страницы 400, 403, 404 и 500
# без необходимости искусственно вызывать реальные ошибки.
def handler400(request: HttpRequest, exception=None) -> HttpResponse:
"""Отображает страницу ошибки 400 (Bad Request).
Используется только в DEBUG режиме для тестирования верстки.
:param request: входящий http-запрос
:param exception: исключение (если есть)
:return response: исходящий http-ответ с кодом 400
"""
response = render(request, "error/400.html", {})
response.status_code = 400
return response
def handler403(request: HttpRequest, exception=None) -> HttpResponse:
"""Отображает страницу ошибки 403 (Forbidden).
Используется только в DEBUG режиме для тестирования верстки.
:param request: входящий http-запрос
:param exception: исключение (если есть)
:return response: исходящий http-ответ с кодом 403
"""
response = render(request, "error/403.html", {})
response.status_code = 403
return response
def handler404(request: HttpRequest, exception=None) -> HttpResponse:
"""Отображает страницу ошибки 404 (Not Found).
Используется только в DEBUG режиме для тестирования верстки.
:param request: входящий http-запрос
:param exception: исключение (если есть)
:return response: исходящий http-ответ с кодом 404
"""
response = render(request, "error/404.html", {})
response.status_code = 404
return response
def handler500(request: HttpRequest) -> HttpResponse:
"""Отображает страницу ошибки 500 (Internal Server Error).
Используется только в DEBUG режиме для тестирования верстки.
:param request: входящий http-запрос
:return response: исходящий http-ответ с кодом 500
"""
response = render(request, "error/500.html", {})
response.status_code = 500
return response
Generated
+1 -47
View File
@@ -214,27 +214,6 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py
docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
[[package]]
name = "gunicorn"
version = "23.0.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.7"
files = [
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
]
[package.dependencies]
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@@ -249,17 +228,6 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "packaging"
version = "26.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "11.3.0" version = "11.3.0"
@@ -539,21 +507,7 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0)"] zstd = ["backports-zstd (>=1.0.0)"]
[[package]]
name = "whitenoise"
version = "6.12.0"
description = "Radically simplified static file serving for WSGI applications"
optional = false
python-versions = ">=3.10"
files = [
{file = "whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2"},
{file = "whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad"},
]
[package.extras]
brotli = ["brotli"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.12,<3.13" python-versions = ">=3.12,<3.13"
content-hash = "151e463bb47b6a3e0307c77b6032da97f414908c2775be9ea3c6ccfdee19e1a5" content-hash = "5fdba0321d441277f8b911d178c048c672533761e93443473897572f4ed16ebf"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 529 B

Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 635 B

Regular → Executable
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1418" height="1417" viewBox="0 0 809.89 809.74" shape-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd"><defs><clipPath id="A" class="O"><path d="M809.89 404.87c0-11.12-.46-22.12-1.34-33.02L602.2 165.55 206.95 643.52l164.93 164.87c10.91.88 21.93 1.35 33.07 1.35 223.64 0 404.94-181.27 404.94-404.87z"/></clipPath><clipPath id="B" class="O"><path d="M590.6 161.13h-70.51L334.74 648.61H590.6c9.67 0 17.5-7.84 17.5-17.5V178.62c0-9.66-7.83-17.49-17.5-17.49z"/></clipPath><clipPath id="C" class="O"><path d="M447.08 353.14L334.74 648.61H590.6c9.67 0 17.5-7.84 17.5-17.5V178.62a17.4 17.4 0 0 0-4.23-11.4l-1.67-1.67-155.12 187.59z"/></clipPath><path id="D" d="M809.89 404.87C809.89 181.87 627.98 0 404.95 0 181.91 0 0 181.87 0 404.87c0 222.99 181.91 404.87 404.95 404.87 223.03 0 404.94-181.88 404.94-404.87z"/><path id="E" d="M590.6 648.61H219.29c-9.67 0-17.5-7.84-17.5-17.5V178.62c0-9.66 7.84-17.49 17.5-17.49H590.6c9.67 0 17.5 7.83 17.5 17.49v452.49c0 9.66-7.83 17.5-17.5 17.5z"/><path id="F" d="M262.74 587.67h170.81V222.06H262.74z"/><path id="G" d="M468.19 587.67h78.96V222.06h-78.96z"/><path id="H" d="M345.27 396.69c-2.05 0-4.1-.78-5.66-2.35-3.12-3.12-3.12-8.19 0-11.31l50.01-50c3.13-3.12 8.2-3.12 11.32 0 3.12 3.13 3.12 8.19 0 11.31l-50.01 50c-1.56 1.57-3.61 2.35-5.66 2.35z"/></defs><ellipse cx="404.95" cy="404.87" rx="404.95" ry="404.87" fill="#f96" class="N"/><g clip-path="url(#A)" class="N"><path d="M35.56-5.84h945.73v986.97H35.56z" fill="#f7b78f"/><use href="#D" fill="#ff8033"/></g><g class="N"><use href="#E" fill="#366695"/><path d="M262.74 222.06h170.81v365.61H262.74zm205.45 0h78.97v365.61h-78.97z" fill="#71e2ef"/></g><g clip-path="url(#B)" class="N"><path d="M163.35-10.26H779.5V820H163.35z" fill="#d1d1e5"/><use href="#D" fill="#d4866a"/><use href="#E" fill="#335d90"/><g fill="#62c1d8"><use href="#F"/><use href="#G"/></g></g><g clip-path="url(#C)" class="N"><path d="M163.35-5.84H779.5V820H163.35z" fill="#ce9b91"/><use href="#D" fill="#d47241"/><use href="#E" fill="#335d90"/><g fill="#62c1d8"><use href="#F"/><use href="#G"/></g></g><g fill="#fff" class="N"><path d="M304.26 389.44a7.98 7.98 0 0 1-5.66-2.34c-3.12-3.13-3.12-8.19 0-11.32l82.52-82.5c3.12-3.12 8.19-3.12 11.32 0 3.12 3.13 3.12 8.19 0 11.31l-82.52 82.51a7.98 7.98 0 0 1-5.66 2.34z"/><use href="#H"/><use href="#H" x="-49.51" y="-47"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 5.81 5.81" shape-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd"><path d="M2.25 4.2h-.02H0V1.69h2.25v.98h3.54V4.2z" fill="#393185"/><g fill="#fefefe" class="E"><path d="M1.45 3.55h.16l-.08-.3-.08.3zm.27.43l-.05-.21h-.28l-.06.21h-.26l.32-1.03h.33l.33 1.03h-.33z"/><path d="M1.07 2.95v1.03H.76v-.4H.52v.4H.2V2.95h.32v.38h.24v-.38zm.98 0H1.7l-.18-.39-.1.15v.24h-.3V1.92h.3v.43l.28-.43h.28l-.26.38z"/><path d="M.63 2.74c.06 0 .09-.02.12-.07.02-.04.03-.13.03-.25 0-.18-.05-.27-.14-.27-.11 0-.16.1-.16.31 0 .19.05.28.15.28zm0 .23c-.14 0-.26-.04-.35-.14-.09-.09-.14-.22-.14-.38a.58.58 0 0 1 .13-.38c.09-.1.21-.15.37-.15.14 0 .26.04.35.14.09.09.13.22.13.38s-.04.29-.14.39c-.09.1-.2.14-.35.14z"/></g><g fill="#d2cde7" class="E"><path d="M5.22 3.17H5.1c-.06 0-.1.01-.12.03-.01.02-.02.05-.02.07 0 .04.01.06.03.08.03.01.07.02.12.02h.11v-.2zm0 .42h-.14l-.17.39h-.29l.22-.43c-.14-.05-.21-.15-.21-.28 0-.1.03-.18.1-.24.07-.05.16-.08.27-.08h.54v1.03h-.32v-.39z"/><path d="M4.04 3.87v.11h-.3V2.95h.3v.42.19c.03-.08.07-.15.11-.22l.16-.28v-.11h.31v1.03h-.31v-.41c0-.05 0-.12.01-.19-.03.07-.07.14-.11.22l-.17.27z"/><path d="M3.04 3.76h.28L3.2 3.2h-.02l-.14.56zm-.25 0l.23-.8h.38l.23.8h.11v.44h-.2l-.02-.21H2.9l-.02.21h-.2v-.44h.11z"/><path d="M2.36 3.38h.11c.09 0 .13-.03.13-.1s-.04-.1-.12-.1h-.12v.2zm.01.22v.38h-.32V2.95h.43c.11 0 .2.01.25.04.06.02.1.06.14.11.03.05.05.11.05.18 0 .09-.04.17-.1.23a.41.41 0 0 1-.28.09h-.17z"/></g><path d="M3.09 1.9c0 .13-.05.2-.14.2a.11.11 0 0 1-.1-.05c-.03-.04-.04-.09-.04-.15 0-.13.05-.2.14-.2a.11.11 0 0 1 .1.05c.03.03.04.08.04.15zM3 1.9c0-.05 0-.09-.01-.11-.01-.01-.02-.02-.04-.02s-.03.01-.04.03c-.01.01-.01.05-.01.1s0 .08.01.1.02.03.04.03.03-.01.04-.03C3 1.99 3 1.95 3 1.9zm.25-.19h.09v.16l.09-.16h.09l-.1.14.1.24h-.08l-.07-.18-.03.05v.13h-.09v-.38zm.66.19c0 .13-.04.2-.14.2-.04 0-.08-.02-.1-.05-.02-.04-.04-.09-.04-.15 0-.13.05-.2.14-.2.05 0 .08.02.11.05.02.03.03.08.03.15zm-.09 0c0-.05 0-.09-.01-.11-.01-.01-.02-.02-.04-.02-.01 0-.03.01-.03.03-.01.01-.02.05-.02.1s.01.08.01.1c.01.02.03.03.04.03.02 0 .03-.01.04-.03.01-.01.01-.05.01-.1zm.26-.19h.08v.15h.09v-.15h.09v.38h-.09v-.16h-.09v.16h-.08v-.38zm.43 0h.09v.15h.09v-.15h.08v.38h-.08v-.16H4.6v.16h-.09v-.38zm.44.38v-.38h.08v.14h.03c.06 0 .09.01.11.03.03.02.04.05.04.09 0 .03-.01.06-.03.08-.01.02-.02.03-.04.03-.02.01-.05.01-.08.01h-.11zm.08-.06h.03c.01 0 .02 0 .03-.01.01 0 .01 0 .02-.01 0-.01.01-.02.01-.04s-.01-.03-.01-.04c-.01-.01-.03-.02-.05-.02h-.03v.12zm.22-.32h.08v.38h-.08v-.38zm.34.34v.04h-.08v-.38h.08v.21l.01-.02.09-.15v-.04h.08v.38h-.08v-.2-.01l-.01.02-.09.15zm-.04-.45h.05c0 .02.02.03.04.03.03 0 .04-.01.05-.03h.05c-.02.05-.05.07-.1.07-.04 0-.08-.02-.09-.07zm-3 .58h.09l.11.39h-.09l-.02-.08h-.11l-.02.08h-.07l.11-.39zm.07.24l-.03-.16-.04.16h.07zm.5-.24v.07h-.15v.32h-.09v-.39h.24zm.14 0h.11c.03 0 .06 0 .07.01.02.01.04.02.05.04s.02.04.02.07-.01.05-.02.07-.02.03-.04.04-.04.01-.08.01h-.03v.15h-.08v-.39zm.08.06v.12h.03c.02 0 .03-.01.04-.02s.01-.02.01-.04c0-.01 0-.03-.01-.03 0-.01-.01-.02-.01-.02-.01 0-.02-.01-.03-.01h-.03zm.31-.06h.24v.07h-.15v.09h.11v.06h-.11v.1h.15v.07h-.24v-.39zm.62 0v.07h-.15v.32h-.09v-.39h.24zm.2 0h.09l.11.39h-.09l-.02-.08h-.11l-.01.08h-.08l.11-.39zm.08.24l-.04-.16-.04.16h.08zm.2-.24h.24v.07h-.08v.32h-.09v-.32h-.07v-.07zm.64.19c0 .14-.05.2-.14.2-.05 0-.08-.01-.1-.05-.03-.03-.04-.08-.04-.15 0-.13.05-.19.14-.19.04 0 .08.01.1.05.03.03.04.08.04.14zm-.09 0c0-.05 0-.08-.01-.1s-.02-.03-.04-.03-.03.01-.04.03-.01.05-.01.1 0 .09.01.11c.01.01.02.02.04.02s.03 0 .04-.02.01-.05.01-.11zm.25-.19h.11c.04 0 .06 0 .08.01s.03.02.05.04c.01.02.02.04.02.07s-.01.05-.02.07-.02.03-.04.04-.05.01-.08.01h-.03v.15h-.09v-.39zm.09.06v.12h.02c.03 0 .04-.01.05-.02 0-.01.01-.02.01-.04 0-.01-.01-.03-.01-.03-.01-.01-.01-.02-.02-.02s-.02-.01-.03-.01h-.02z" fill="#7e71b1" class="E"/></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+1 -4
View File
@@ -1,5 +1,5 @@
# Маркетплейс-агрегатор цен на установку пластиковых и деревянных окон — ОКНАРДИЯ # Маркетплейс-агрегатор цен на установку пластиковых и деревянных окон — ОКНАРДИЯ
# robots.txt последняя версия: 2026-05-20 # robots.txt последняя версия: 2026-05-15
User-Agent: * User-Agent: *
Allow: / Allow: /
@@ -10,9 +10,6 @@ Disallow: /*?*token*
Disallow: /*.json$ Disallow: /*.json$
Disallow: /.*\.(js|css)$ # CSS и JS обслуживаются отдельно через CDN Disallow: /.*\.(js|css)$ # CSS и JS обслуживаются отдельно через CDN
# Параметры, которые не влияют на смысловое содержимое
Clean-param: page-back /
# Быстрые краулеры # Быстрые краулеры
User-agent: YandexBot User-agent: YandexBot
Crawl-delay: 15 Crawl-delay: 15
-21
View File
@@ -1,21 +0,0 @@
{
"name": "Окнардия",
"short_name": "Окнардия",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

-2
View File
@@ -15,8 +15,6 @@ Pillow = "^11.2.1"
requests = "^2.32.3" requests = "^2.32.3"
pytils = "^0.4.4" pytils = "^0.4.4"
rjsmin = "^1.2.0" rjsmin = "^1.2.0"
gunicorn = "^23.0.0"
whitenoise = "^6.8.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^6.3" django-debug-toolbar = "^6.3"