Skip to content
Merged

Dev #649

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
12bc951
создана структура документации
Toksi86 May 8, 2026
53d610c
Переработан модуль курсов: в ответах прпнимаются только файлы активно…
Toksi86 May 8, 2026
e4cdf85
Задокументирован модуль курсов
Toksi86 May 8, 2026
4efbc7d
Расширены тесты и соответствующая документация модуля курсов
Toksi86 May 12, 2026
1253798
Merge pull request #634 from PROCOLLAB-github/refactor/modules
Toksi86 May 12, 2026
32457b3
Добавлены тесты для модуля Projects
Toksi86 May 18, 2026
3f94f97
Исправлена маршрутизация frontend на dev
Toksi86 May 18, 2026
143a204
Исправлена нестабильная дата в тестах курсов
Toksi86 May 18, 2026
e7b338f
Merge pull request #635 from PROCOLLAB-github/refactor/modules
Toksi86 May 18, 2026
9feef3d
Merge pull request #636 from PROCOLLAB-github/devops-structure-rework
Toksi86 May 18, 2026
8495994
Задокументирован модуль News и добавлены тесты
Toksi86 May 20, 2026
cbc485b
Рефакторинг модуля News и удаление legacy ProjectNews
Toksi86 May 21, 2026
fa298f0
Merge pull request #637 from PROCOLLAB-github/refactor/modules
Toksi86 May 21, 2026
72aa82d
Усилена проверка dev deploy pipeline
Toksi86 May 22, 2026
ea9e63d
Merge pull request #638 from PROCOLLAB-github/devops-structure-rework
Toksi86 May 22, 2026
0352782
Усилена проверка prod deploy pipeline
Toksi86 May 25, 2026
b32c80d
Merge pull request #639 from PROCOLLAB-github/devops-structure-rework
Toksi86 May 25, 2026
56e25cb
Задокументирован и покрыт тестами модуль Feed
Toksi86 May 25, 2026
cad8bf7
Усилена фильтрация вакансий в Feed
Toksi86 May 25, 2026
2017501
Уточнён контекстный API модуля News
Toksi86 May 25, 2026
b9dde5d
Merge pull request #640 from PROCOLLAB-github/refactor/modules
Toksi86 May 25, 2026
69e4e80
Уточнён контракт Feed и обработка новостей программ
Toksi86 May 26, 2026
82c8bad
Для модуля Партнёрских программ добавлены документация и тесты
Toksi86 May 27, 2026
01431fa
Логика вынесена из views в services
Toksi86 May 27, 2026
be1cbab
Разделён service layer партнёрских программ и расширены тесты
Toksi86 May 27, 2026
0900d34
Merge pull request #641 from PROCOLLAB-github/refactor/modules
Toksi86 May 27, 2026
9ed1c89
Написана документация и расширены тесты для модуля project_rates
Toksi86 May 28, 2026
7012cae
Бизнес-логика project_rates вынесена в сервисы
Toksi86 May 28, 2026
2a29fa3
Добавлены документация и тесты для модуля users
Toksi86 May 29, 2026
defbf8b
Исключены новости программ из общей ленты
Toksi86 Jun 3, 2026
f175b9e
Merge pull request #644 from PROCOLLAB-github/refactor/modules
Toksi86 Jun 3, 2026
cb345fd
Merge pull request #643 from PROCOLLAB-github/fix/feed-partnerprogram…
Toksi86 Jun 3, 2026
3ed5393
Задокументирован модуль Events и расширены тесты
Toksi86 Jun 5, 2026
64604d8
Отключены публичные URL модуля Events
Toksi86 Jun 5, 2026
19f4268
Расширены тесты и исправлены flow откликов Vacancy
Toksi86 Jun 5, 2026
a90f6a6
Задокументирован модуль Industries и расширены тесты
Toksi86 Jun 8, 2026
30a6871
Merge pull request #645 from PROCOLLAB-github/refactor/modules
Toksi86 Jun 8, 2026
fa1c7b6
Написана документация, расширены тесты и доработаны права модуля Invites
Toksi86 Jun 8, 2026
c4c6ce8
Merge pull request #646 from PROCOLLAB-github/refactor/modules
Toksi86 Jun 8, 2026
ded499a
fix: исправлены выявленные прорблемы после рефакторинга
Toksi86 Jun 10, 2026
e99ac97
Merge pull request #647 from PROCOLLAB-github/refactor/modules
Toksi86 Jun 10, 2026
0eccd3d
Задокументирован модуль Metrics и вынесен сбор метрик в сервис
Toksi86 Jun 10, 2026
d9e4a41
Задокументирован модуль Mailing и расширены тесты
Toksi86 Jun 10, 2026
cada2ca
Актуализирована документация модуля Chats
Toksi86 Jun 10, 2026
5b5fc21
Актуализирована документация модуля Core
Toksi86 Jun 10, 2026
944eaa1
Merge pull request #648 from PROCOLLAB-github/refactor/modules
Toksi86 Jun 10, 2026
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
62 changes: 44 additions & 18 deletions .github/workflows/dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ jobs:
docker compose -f docker-compose.dev-ci.yml build web &&
docker compose -f docker-compose.dev-ci.yml run --rm web python manage.py migrate &&
docker compose -f docker-compose.dev-ci.yml up -d --force-recreate &&
expected_image="procollab-dev-api:${IMAGE_TAG}" &&
for service in web celerys; do
container="$(docker compose -f docker-compose.dev-ci.yml ps -q "$service")"
if [ -z "$container" ]; then
echo "Service ${service} has no running container" >&2
docker compose -f docker-compose.dev-ci.yml ps >&2 || true
exit 1
fi

actual_image="$(docker inspect -f '{{.Config.Image}}' "$container")"
echo "Service ${service}: container=${container} image=${actual_image}"
if [ "$actual_image" != "$expected_image" ]; then
echo "Service ${service} uses unexpected image: ${actual_image}, expected ${expected_image}" >&2
docker compose -f docker-compose.dev-ci.yml ps >&2 || true
exit 1
fi
done &&

install -d /etc/nginx/procollab/includes &&
install -m 644 deploy/nginx/host/includes/proxy_app.inc /etc/nginx/procollab/includes/proxy_app.inc &&
Expand Down Expand Up @@ -83,28 +100,37 @@ jobs:
exit 1
fi &&

celery_status="" &&
celery_ping="" &&
for attempt in $(seq 1 24); do
celery_status="$(docker inspect -f '{{.State.Status}}' api_celery 2>/dev/null || true)" &&
docker compose -f docker-compose.dev-ci.yml ps

celery_status=""
celery_ping=""
celery_container=""
for attempt in $(seq 1 12); do
celery_container="$(docker compose -f docker-compose.dev-ci.yml ps -q celerys 2>/dev/null || true)"
if [ -n "$celery_container" ]; then
celery_status="$(docker inspect -f '{{.State.Status}}' "$celery_container" 2>/dev/null || true)"
else
celery_status="missing"
fi

echo "Celery check attempt ${attempt}: container=${celery_container:-missing} status=${celery_status}"
if [ "$celery_status" = "running" ]; then
celery_ping="$(docker compose -f docker-compose.dev-ci.yml exec -T celerys sh -lc 'celery -A procollab inspect ping --timeout=10' 2>&1 || true)" &&
printf '%s\n' "$celery_ping" &&
celery_ping="$(docker compose -f docker-compose.dev-ci.yml exec -T celerys sh -lc 'celery -A procollab inspect ping --timeout=15' 2>&1 || true)"
printf '%s\n' "$celery_ping"
if printf '%s\n' "$celery_ping" | grep -q 'pong'; then
echo "Celery check passed on attempt ${attempt}" &&
echo "Celery check passed on attempt ${attempt}"
break
fi
fi &&
fi

sleep 5
done &&

if [ "$celery_status" != "running" ]; then
echo "Celery container is not running: ${celery_status}" >&2 &&
exit 1
fi &&

printf '%s\n' "$celery_ping" | grep -q 'pong' || {
echo "Celery ping failed" >&2
done

if [ "$celery_status" != "running" ] || ! printf '%s\n' "$celery_ping" | grep -q 'pong'; then
echo "Celery check failed: status=${celery_status}" >&2
docker compose -f docker-compose.dev-ci.yml ps >&2 || true
docker compose -f docker-compose.dev-ci.yml logs --tail=200 celerys >&2 || true
docker compose -f docker-compose.dev-ci.yml logs --tail=100 redis >&2 || true
docker compose -f docker-compose.dev-ci.yml logs --tail=100 web >&2 || true
exit 1
}
fi
48 changes: 37 additions & 11 deletions .github/workflows/release-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,24 @@ jobs:
docker compose -f docker-compose.prod-ci.yml -p prod pull web celerys

docker compose -f docker-compose.prod-ci.yml -p prod run --rm web python manage.py migrate
docker compose -f docker-compose.prod-ci.yml -p prod up -d
docker compose -f docker-compose.prod-ci.yml -p prod up -d --force-recreate
expected_image="ghcr.io/procollab-github/api:${IMAGE_TAG}"
for service in web celerys; do
container="$(docker compose -f docker-compose.prod-ci.yml -p prod ps -q "$service")"
if [ -z "$container" ]; then
echo "Service ${service} has no running container" >&2
docker compose -f docker-compose.prod-ci.yml -p prod ps >&2 || true
exit 1
fi

actual_image="$(docker inspect -f '{{.Config.Image}}' "$container")"
echo "Service ${service}: container=${container} image=${actual_image}"
if [ "$actual_image" != "$expected_image" ]; then
echo "Service ${service} uses unexpected image: ${actual_image}, expected ${expected_image}" >&2
docker compose -f docker-compose.prod-ci.yml -p prod ps >&2 || true
exit 1
fi
done
if [ "$(id -u)" -eq 0 ]; then
nginx -t
systemctl reload nginx
Expand All @@ -210,12 +227,22 @@ jobs:
exit 1
fi

docker compose -f docker-compose.prod-ci.yml -p prod ps

celery_status=""
celery_ping=""
for attempt in $(seq 1 24); do
celery_status="$(docker inspect -f '{{.State.Status}}' api_celery 2>/dev/null || true)"
celery_container=""
for attempt in $(seq 1 12); do
celery_container="$(docker compose -f docker-compose.prod-ci.yml -p prod ps -q celerys 2>/dev/null || true)"
if [ -n "$celery_container" ]; then
celery_status="$(docker inspect -f '{{.State.Status}}' "$celery_container" 2>/dev/null || true)"
else
celery_status="missing"
fi

echo "Celery check attempt ${attempt}: container=${celery_container:-missing} status=${celery_status}"
if [ "$celery_status" = "running" ]; then
celery_ping="$(docker compose -f docker-compose.prod-ci.yml -p prod exec -T celerys sh -lc 'celery -A procollab inspect ping --timeout=10' 2>&1 || true)"
celery_ping="$(docker compose -f docker-compose.prod-ci.yml -p prod exec -T celerys sh -lc 'celery -A procollab inspect ping --timeout=15' 2>&1 || true)"
printf '%s\n' "$celery_ping"
if printf '%s\n' "$celery_ping" | grep -q 'pong'; then
echo "Celery check passed on attempt ${attempt}"
Expand All @@ -226,12 +253,11 @@ jobs:
sleep 5
done

if [ "$celery_status" != "running" ]; then
echo "Celery container is not running: ${celery_status}" >&2
if [ "$celery_status" != "running" ] || ! printf '%s\n' "$celery_ping" | grep -q 'pong'; then
echo "Celery check failed: status=${celery_status}" >&2
docker compose -f docker-compose.prod-ci.yml -p prod ps >&2 || true
docker compose -f docker-compose.prod-ci.yml -p prod logs --tail=200 celerys >&2 || true
docker compose -f docker-compose.prod-ci.yml -p prod logs --tail=100 redis >&2 || true
docker compose -f docker-compose.prod-ci.yml -p prod logs --tail=100 web >&2 || true
exit 1
fi

printf '%s\n' "$celery_ping" | grep -q 'pong' || {
echo "Celery ping failed" >&2
exit 1
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
.idea/
.codex

# Translations
*.mo
Expand Down
83 changes: 22 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,31 @@
# Procollab backend service
# Procollab Backend

## Usage
Backend API для продукта Procollab.

### Clone project
## Стек

📌 `git clone https://github.com/procollab-github/api.git`
- Python
- Django
- Django REST Framework
- Channels
- Celery
- PostgreSQL
- Redis

### Create virtual environment

🔑 Copy `.env.example` to `.env` and change api settings

### Install dependencies

* 🐍 Install poetry with command `pip install poetry`
* 📎 Install dependencies with command `poetry install`

### Accept migrations

🎓 Run `python manage.py migrate`

### Run project

🚀 Run project via `python manage.py runserver`
## For developers

### Install pre-commit hooks

To install pre-commit simply run inside the shell:
## Базовые команды

```bash
pre-commit install
```

To run it on all of your files, do

```bash
pre-commit run --all-files
```

## Troubleshooting

## Errors caused by weasyprint

### MacOS

Error:
```
OSError: cannot load library 'pango-1.0-0': dlopen(pango-1.0-0, 0x0002): tried: 'pango-1.0-0' (no such file), '/System/Volumes/Preboot/Cryptexes/OSpango-1.0-0' (no such file), '/Users/yakser/.pyenv/versions/3.11.9/lib/pango-1.0-0' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/yakser/.pyenv/versions/3.11.9/lib/pango-1.0-0' (no such file), '/opt/homebrew/lib/pango-1.0-0' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/lib/pango-1.0-0' (no such file), '/usr/lib/pango-1.0-0' (no such file, not in dyld cache), 'pango-1.0-0' (no such file), '/usr/local/lib/pango-1.0-0' (no such file), '/usr/lib/pango-1.0-0' (no such file, not in dyld cache). Additionally, ctypes.util.find_library() did not manage to locate a library called 'pango-1.0-0'
```

Fix:

```shell
brew install weasyprint
```

### Windows

Error:
poetry install
poetry run python manage.py migrate
poetry run python manage.py runserver
poetry run python manage.py test
```
OSError: cannot load library 'gobject-2.0-0': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'gobject-2.0-0'
```

Fix:

Go to [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#windows) step by step install dependencies. If the error persists, add the path to the windows environment variable: `C:\msys64\mingw64\bin`

## Документация

## [Docs](/docs/readme.md)
- [Навигация по документации](docs/readme.md)
- [Разработка](docs/development.md)
- [Архитектура](docs/architecture.md)
- [API](docs/api.md)
- [Инфраструктура и деплой](docs/devops-state.md)
- [Доменные модули](docs/modules/readme.md)
27 changes: 26 additions & 1 deletion courses/admin_config/answers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
from django.contrib import admin

from courses.models import UserTaskAnswer, UserTaskAnswerFile, UserTaskAnswerOption
from courses.models import (
CourseTaskCheckType,
UserTaskAnswer,
UserTaskAnswerFile,
UserTaskAnswerOption,
)
from courses.services.progress import recalculate_user_progresses_for_lesson

from .inlines import UserTaskAnswerFileInline, UserTaskAnswerOptionInline


REVIEW_PROGRESS_FIELDS = {
"status",
"is_correct",
"review_comment",
"reviewed_by",
"reviewed_at",
}


@admin.register(UserTaskAnswer)
class UserTaskAnswerAdmin(admin.ModelAdmin):
list_display = (
Expand Down Expand Up @@ -44,6 +59,16 @@ class UserTaskAnswerAdmin(admin.ModelAdmin):
)
inlines = [UserTaskAnswerOptionInline, UserTaskAnswerFileInline]

def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)

changed_fields = set(getattr(form, "changed_data", []) or [])
if (
obj.task.check_type == CourseTaskCheckType.WITH_REVIEW
and changed_fields & REVIEW_PROGRESS_FIELDS
):
recalculate_user_progresses_for_lesson(obj.user, obj.task.lesson)


@admin.register(UserTaskAnswerOption)
class UserTaskAnswerOptionAdmin(admin.ModelAdmin):
Expand Down
2 changes: 2 additions & 0 deletions courses/admin_config/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ def clean(self):
"image_upload",
"В поле изображения можно загрузить только файл изображения.",
)
# TODO: убрать временные флаги, когда upload -> UserFile будет вынесен
# в явный admin/service слой до запуска model validation.
self.instance._has_pending_image_upload = bool(image_upload)
self.instance._has_pending_attachment_upload = bool(attachment_upload)
return cleaned_data
12 changes: 12 additions & 0 deletions courses/api/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework import serializers


def serialize_response(
serializer_class: type[serializers.Serializer],
payload,
*,
many: bool = False,
):
serializer = serializer_class(data=payload, many=many)
serializer.is_valid(raise_exception=True)
return serializer.data
30 changes: 17 additions & 13 deletions courses/api/views/course_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
CourseDetailSerializer,
CourseStructureSerializer,
)
from courses.api.response import serialize_response
from courses.queries import (
build_course_detail_payload,
build_course_list_payload,
Expand All @@ -17,29 +18,32 @@
class CourseListAPIView(AuthenticatedCourseAPIView):

def get(self, request):
serializer = CourseCardSerializer(
data=build_course_list_payload(request.user),
many=True,
return Response(
serialize_response(
CourseCardSerializer,
build_course_list_payload(request.user),
many=True,
)
)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)


class CourseDetailAPIView(AuthenticatedCourseAPIView):

def get(self, request, pk: int):
serializer = CourseDetailSerializer(
data=build_course_detail_payload(request.user, pk)
return Response(
serialize_response(
CourseDetailSerializer,
build_course_detail_payload(request.user, pk),
)
)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)


class CourseStructureAPIView(AuthenticatedCourseAPIView):

def get(self, request, pk: int):
serializer = CourseStructureSerializer(
data=build_course_structure_payload(request.user, pk)
return Response(
serialize_response(
CourseStructureSerializer,
build_course_structure_payload(request.user, pk),
)
)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
10 changes: 6 additions & 4 deletions courses/api/views/lesson_read.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework.response import Response

from courses.api.response import serialize_response
from courses.api.serializers import LessonDetailSerializer
from courses.queries import build_lesson_detail_payload

Expand All @@ -9,8 +10,9 @@
class LessonDetailAPIView(AuthenticatedCourseAPIView):

def get(self, request, pk: int):
serializer = LessonDetailSerializer(
data=build_lesson_detail_payload(request.user, pk)
return Response(
serialize_response(
LessonDetailSerializer,
build_lesson_detail_payload(request.user, pk),
)
)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
Loading
Loading