Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.env
venv
.venv
**/__pycache__
**/*.pyc
.idea/
.git/
.github/
Expand Down
45 changes: 43 additions & 2 deletions .github/workflows/backend-postgres-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ concurrency:
cancel-in-progress: true

jobs:
docker-image:
name: Backend Docker image
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Build release Dockerfile
run: docker build --tag procollab-backend-ci .

- name: Check application inside the image
run: |
docker run --rm \
-e DJANGO_SETTINGS_MODULE=procollab.settings_ci \
-e DJANGO_SECRET_KEY=ci-only-docker-check-not-a-production-secret \
procollab-backend-ci python manage.py check

- name: Render Cyrillic certificate inside the image
run: |
docker run --rm -i \
-e DJANGO_SETTINGS_MODULE=procollab.settings_ci \
-e DJANGO_SECRET_KEY=ci-only-docker-check-not-a-production-secret \
procollab-backend-ci python - <<'PY'
import django
import weasyprint

django.setup()

from django.utils import timezone
from partner_programs.services.certificates import default_draft, render_pdf

draft = default_draft()
draft["hidden_elements"] = []
content = render_pdf(
draft, {}, "Александра Константиновна Добровольская",
"Тестовая программа", "CI-001", timezone.now(),
)
assert content.startswith(b"%PDF") and len(content) > 5000
print(f"Django {django.get_version()}, WeasyPrint {weasyprint.__version__}: PDF OK")
PY

postgres-tests:
name: Backend PostgreSQL CI
runs-on: ubuntu-latest
Expand Down Expand Up @@ -61,8 +102,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 fontconfig poppler-utils
python -m pip install "poetry==1.2.2"
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 fontconfig poppler-utils
python -m pip install "poetry==2.5.1"
poetry config virtualenvs.create true
poetry install --no-interaction --no-ansi --no-root

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-1.2.2-${{ hashFiles('**/poetry.lock') }}
key: poetry-2.5.1-${{ hashFiles('**/poetry.lock') }}

- uses: snok/install-poetry@v1
with:
version: 1.2.2
version: 2.5.1
virtualenvs-create: true
virtualenvs-in-project: true

Expand All @@ -58,7 +58,7 @@ jobs:
pydeps-

- name: Install dependencies
run: poetry install
run: poetry install --no-root --no-interaction

- name: Run tests
run: poetry run python manage.py test --verbosity=2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dev-migration-role-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: python -m pip install Django==4.2.11 psycopg2-binary==2.9.9
- run: python -m pip install Django==5.2.18 psycopg2-binary==2.9.13
- name: Prepare masked transport fixture
id: credentials_fixture
run: python scripts/dev_ci_migrate.py --prepare-payload
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-1.2.2-0
key: poetry-2.5.1-0

- uses: snok/install-poetry@v1
with:
version: 1.2.2
version: 2.5.1
virtualenvs-create: true
virtualenvs-in-project: true

Expand All @@ -79,7 +79,7 @@ jobs:
path: .venv
key: pydeps-${{ hashFiles('**/poetry.lock') }}

- run: poetry install
- run: poetry install --no-root --no-interaction

- run: poetry run pip install setuptools

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/ambv/black
rev: 22.10.0
rev: 26.10.1
hooks:
- id: black
exclude: ^.*\b(migrations)\b.*$
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM python:3.11

RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 libpangoft2-1.0-0 fontconfig libjpeg62-turbo libopenjp2-7 \
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 fontconfig libjpeg62-turbo libopenjp2-7 \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONFAULTHANDLER=1 \
Expand All @@ -10,7 +10,7 @@ ENV PYTHONFAULTHANDLER=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.2.2
POETRY_VERSION=2.5.1

RUN pip install "poetry==$POETRY_VERSION"

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Backend API для продукта Procollab.
## Базовые команды

```bash
poetry install
poetry install --no-root
poetry run python manage.py migrate
poetry run python manage.py runserver
poetry run python manage.py test
Expand All @@ -25,6 +25,7 @@ poetry run python manage.py test

- [Навигация по документации](docs/readme.md)
- [Разработка](docs/development.md)
- [Обновление зависимостей до Django 5.2 LTS](docs/dependency-upgrade-20261102.md)
- [Архитектура](docs/architecture.md)
- [API](docs/api.md)
- [Инфраструктура и деплой](docs/devops-state.md)
Expand Down
49 changes: 49 additions & 0 deletions core/test_dependency_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Compatibility of persisted credentials, static assets and image uploads."""

from io import BytesIO
from types import SimpleNamespace

from django.contrib.auth.hashers import check_password
from django.core.files.storage import storages
from django.test import SimpleTestCase, override_settings
from PIL import Image
from whitenoise.storage import CompressedStaticFilesStorage

from files.helpers import convert_image_to_webp


@override_settings(
PASSWORD_HASHERS=[
"django.contrib.auth.hashers.BCryptPasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]
)
class PersistedPasswordCompatibilityTests(SimpleTestCase):
# Synthetic fixtures produced by the previous Django 4.2.11 / bcrypt 4.1.2.
def test_existing_long_bcrypt_password_still_authenticates(self):
encoded = "bcrypt$$2b$04$qs8/QCj6RsBopogj..srsuvHeg/qOtoW44PjQvsji2su7J6I9z7TK"
self.assertTrue(check_password("legacy-password-" + "x" * 100, encoded))
self.assertFalse(check_password("different-password-" + "x" * 100, encoded))

def test_existing_pbkdf2_password_still_authenticates(self):
encoded = (
"pbkdf2_sha256$390000$migration-test-salt$"
"cGWmSyv+hMl8eGjeBZL5rFJkIvidubO6dg6yOT99zIM="
)
self.assertTrue(check_password("legacy-secret-2026", encoded))
self.assertFalse(check_password("different-secret", encoded))


class DependencyIntegrationTests(SimpleTestCase):
def test_static_assets_keep_whitenoise_compression(self):
self.assertIsInstance(storages["staticfiles"], CompressedStaticFilesStorage)

def test_uploaded_rgba_image_can_be_converted_to_webp(self):
upload = BytesIO()
Image.new("RGBA", (8, 6), (40, 80, 120, 51)).save(upload, format="PNG")
upload.seek(0)
encoded = convert_image_to_webp(SimpleNamespace(file=upload))
with Image.open(BytesIO(encoded.buffer())) as image:
self.assertEqual(image.format, "WEBP")
self.assertEqual(image.size, (8, 6))
self.assertEqual(image.getchannel("A").getextrema(), (51, 51))
129 changes: 129 additions & 0 deletions docs/dependency-upgrade-20261102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Обновление зависимостей, 02.11.2026

Изменение подготовлено от backend dev `cf48cd7`. Ветка:
`codex/update-supported-dependencies`. Это отдельное изменение от исправления
внешней регистрации `codex/secure-external-registration`.

## Что меняется

Python остается в серии 3.11. Основная версия Django ограничена веткой 5.2 LTS.
Точные версии и хеши пакетов зафиксированы в `poetry.lock`; обычная установка
не выполняет автоматический переход на Django 6.

| Пакет | Было | Стало |
| --- | --- | --- |
| Django | 4.2.11 | 5.2.18 |
| Django REST Framework | 3.15.1 | 3.18.3 |
| SimpleJWT | 5.3.1 | 5.5.1 |
| Requests | 2.31.0 | 2.34.2 |
| Pillow | 9.5.0 | 13.0.0 |
| WeasyPrint | 62.3 | 70.0 |
| cryptography | 42.0.5 | 50.0.2 |
| bcrypt | 4.1.2 | 4.3.0 |

Также согласованы связанные Django-библиотеки и обновлены транзитивные
зависимости. Вспомогательные Black и setuptools подняты до исправленных версий.
Poetry 2.5.1 используется одинаково в Docker и CI.
В PostgreSQL PR CI также добавлена отдельная сборка release Dockerfile с проверкой
Django и генерацией кириллического PDF внутри собранного Linux-образа.

## Адаптация приложения

- Настройка `STATICFILES_STORAGE`, удаленная в Django 5.1, заменена на `STORAGES`.
Сохранены файловое хранилище по умолчанию и сжатие статики WhiteNoise.
- Тестовые настройки админки используют тот же новый формат.
- Явное ограничение `bcrypt<5` сохраняет проверку старых паролей с алгоритмом
`bcrypt` длиной более 72 байт. Новые пароли продолжают использовать настроенный
в приложении `BCryptSHA256PasswordHasher`.
- В Docker добавлена системная библиотека HarfBuzz, требуемая новым PDF-движком.
- Загрузчик ресурсов сертификата переведен на `URLFetcherResponse` WeasyPrint 70:
сохраняются встроенные шрифты и PNG-фоны. Разрешены только собственные шрифты
и встроенные PNG; сетевые адреса и произвольные файлы по-прежнему отклоняются.
- Установка проекта в CI выполняется с `--no-root`, как и в Docker: это приложение,
а не устанавливаемая библиотека. Исправлен backend сборки Poetry.

Сущности проектов и решений, режим программы, адреса API и бизнес-правила
регистрации этим изменением не переключаются.

## Миграции и сохранность данных

Новая миграция `users.0066` фиксирует `through_fields` существующей связи файлов
достижения в состоянии Django. Для PostgreSQL `sqlmigrate` показывает `no-op`:
таблицы и записи эта миграция не меняет.

Есть миграции обновленных библиотек: django-celery-beat, django-rest-passwordreset,
taggit и token_blacklist. Их нужно применить штатным мигратором перед запуском
нового образа. Права runtime-роли расширять для этого не нужно.

Проверка перехода выполнена на копии старой тестовой базы PostgreSQL 16:
пользователь с паролем, legacy-программа с проектом и solution-программа с
решением созданы на Django 4.2.11, затем проверены после миграций на Django 5.2.18.
Это проверка совместимости схемы и данных на искусственных записях.
После обновления те же записи успешно прочитаны и старым runtime Django 4.2.11.

Старый фрагмент `Image.ANTIALIAS` в миграции файлов 0007 не исполняется:
`operations` в ней пуст. Историческая миграция не переписана.

## Локальная проверка

- Полный последовательный набор: **1949 тестов, OK, без пропусков**, 859 секунд.
- 111 целевых тестов: сертификаты с настоящим PDF-движком, встроенные шрифты,
фон, запрет внешних ресурсов, русский поиск, старые пароли и WebP.
- 104 теста совместного применения с исправлением внешней регистрации.
- Проверки мигратора PostgreSQL: 19 успешных, 3 специфичных для PostgreSQL 14
пропущены на локальном PostgreSQL 16; matrix 14/18 остается в CI.
- Сборка статики: 207 файлов скопировано, 389 обработано.
- Проверки Django и согласованности миграций, `pip check`, `poetry check --lock`,
flake8 измененных Python-файлов и форматирование CI settings прошли.
- Запущен настоящий Daphne/ASGI на loopback: публичный список программ 200,
защищенные решения без авторизации 401, CSS админки 200 со сжатием gzip.
- Access/refresh-токены, выданные старым SimpleJWT/PyJWT, успешно использованы
новым ASGI-сервером: профиль 200, обновление токена 200, профиль с новым токеном 200.
- Образец сертификата визуально проверен после преобразования PDF в PNG.
- Все 147 записей lock-файла, включая альтернативы по версиям Python,
проверены pip-audit по PyPI. На дату проверки известных уязвимостей не найдено;
пропущенных пакетов нет. Это проверка опубликованных сведений о пакетах,
не полноценный аудит приложения или системных библиотек Linux-образа.

Окружение: Windows, Python 3.11.9, PostgreSQL 16 с Unicode-локалью ICU `und`,
реальный WeasyPrint 70 и Pango из официального Windows-пакета WeasyPrint.
Полный прогон выполняется на новой базе: повторное использование базы после
TransactionTestCase очищает справочник вузов, загружаемый миграцией. Для локальной
проверки используется последовательный запуск, соответствующий PostgreSQL CI.

После результата `Ran 1949 tests ... OK` команда завершилась с кодом 1 на
удалении тестовой базы: 21 открытое соединение из потоковых тестов мешало `DROP
DATABASE`. Это сбой cleanup после проверок. После выхода процесса база удалена
отдельно. PostgreSQL CI уже использует `--keepdb` и отдельный cleanup с завершением
соединений. Для локального повторения подготовлен аналогичный runner: каждый
запуск создает новую тестовую базу, сохраняет код возврата тестов и удаляет ее
после выхода дочернего процесса. В прикладной код обход teardown не добавлен.
Runner отдельно проверен на 6 тестах с созданием новой базы: тесты и cleanup
завершились успешно, код возврата 0.

## Перед выпуском

1. Убедиться, что PostgreSQL сервера версии 14 или новее, и сделать проверяемую
резервную копию базы. Версия реального prod-сервера в этой работе не проверялась.
2. Собрать новый Linux-образ из этого lock-файла и прогнать PostgreSQL CI.
3. На dev применить миграции мигратором, затем обновить web и Celery одним образом.
4. Проверить через действующий фронт вход и обновление токена, профиль и файлы,
старую программу с проектом, новую с решением, сдачу и оценку, админку,
сертификаты, отправку почты и выполнение фоновой задачи через Redis.
5. После приемки повторить выпуск на prod с сохранением предыдущего образа.

Локальная проверка использует настоящие PostgreSQL и PDF-движок, но почта,
очередь и кеш изолированы тестовой конфигурацией. Она не подтверждает работу
реальных SMTP/CDN/Redis, конфигурацию сервера или поведение открытого браузера.
Локальный Docker не запущен, Linux-образ в этой работе не собирался.

Обновление зависимостей не заменяет исправление внешней регистрации и настройку
ее доверенного отправителя: это самостоятельная найденная проблема.

## Первичные источники

- [Поддерживаемые версии Django](https://www.djangoproject.com/download/)
- [Django 5.2: совместимость Python и переход](https://docs.djangoproject.com/en/5.2/releases/5.2/)
- [Django 5.1: удаление устаревших настроек](https://docs.djangoproject.com/en/5.2/releases/5.1/)
- [Изменения Django REST Framework](https://www.django-rest-framework.org/community/release-notes/)
- [WeasyPrint 70: исправления безопасности](https://github.com/Kozea/WeasyPrint/releases/tag/v70.0)
Loading
Loading