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
37 changes: 36 additions & 1 deletion docs/modules/partner-programs.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
проектов, фильтрации проектов, публикации проектов и подготовки
Excel-выгрузок.
- `partner_programs/selectors.py` - выборки участников для аналитики и
напоминаний.
напоминаний, legacy-заявка лидера в конкретной программе.
- `partner_programs/permissions.py` - проверки менеджера программы, админа и
лидера проекта.
- `partner_programs/tasks.py` - celery-задача публикации проектов после
Expand Down Expand Up @@ -107,6 +107,41 @@
Связанные курсы программы возвращаются в поле `courses`; для каждого курса
указывается `is_available`.

`GET /programs/<id>/` всегда возвращает top-level
`current_project_application`: `null` для anonymous, authenticated non-member
или участника без legacy-связи; для участника-лидера контракт такой:

```json
{
"current_project_application": {
"project_id": 123,
"program_link_id": 700,
"submitted": false
}
}
```

Это legacy Angular flow `Project` × `PartnerProgramProject`, **не** production
модель `Application`. Поэтому DEV-имя `current_application` (#731) в production
не используется. Наличие настоящей Application, её status/project или участие
через TeamMember не определяют это поле; Application API
`/programs/<id>/applications/` и `/programs/<id>/applications/my/` не меняются.

Selector `get_current_project_application(program_id=..., user_id=...)` читает
только `PartnerProgramProject` с `partner_program_id` запрошенной программы и
`project__leader_id` текущего пользователя. `submitted` передаётся напрямую из
этой связи, без расчёта lifecycle. Collaborator/Invite не считаются владельцами;
`PartnerProgramUserProfile.project`, singular `Project.partner_program` и связи
других программ не используются. Если legacy-данные содержат несколько подходящих
проектов одного лидера, выбирается минимальный pk **после фильтрации по программе**.

Member gate остаётся во view: для anonymous/non-member selector не вызывается.
Для member добавляется ровно один SQL с тремя выбранными колонками и `LIMIT 1`,
независимо от количества проектов пользователя. Нет сканирования списка проектов
или pagination. Existing serializers и поля `application_policy`,
`welcome_acknowledged_at`, `courses`, `materials`, manager/member flags и dates
сохраняются. Apply/submit, canonical fields и их submission metadata не меняются.

### 3. Пользователь регистрируется в программе

`POST /programs/<id>/register/` создает `PartnerProgramUserProfile` для текущего
Expand Down
33 changes: 32 additions & 1 deletion partner_programs/selectors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime, timezone as dt_timezone
from typing import TypedDict

from django.contrib.auth import get_user_model
from django.db.models import DateTimeField, Exists, OuterRef, Q, Value
from django.db.models import DateTimeField, Exists, F, OuterRef, Q, Value
from django.db.models.functions import Coalesce, Greatest

from partner_programs.models import (
Expand All @@ -15,6 +16,36 @@
MIN_ACTIVITY_DATETIME = datetime(1970, 1, 1, tzinfo=dt_timezone.utc)


class CurrentProjectApplication(TypedDict):
project_id: int
program_link_id: int
submitted: bool


def get_current_project_application(
*, program_id: int, user_id: int
) -> CurrentProjectApplication | None:
"""Legacy leader-owned link in this program, not a production Application."""
link = (
PartnerProgramProject.objects.filter(
partner_program_id=program_id,
project__leader_id=user_id,
)
.annotate(program_link_id=F("pk"))
.values("project_id", "program_link_id", "submitted")
.order_by("pk")
.first()
)
if link is None:
return None

return {
"project_id": link["project_id"],
"program_link_id": link["program_link_id"],
"submitted": link["submitted"],
}


def programs_with_submission_deadline_on(target_date):
return PartnerProgram.objects.filter(
Q(datetime_project_submission_ends__date=target_date)
Expand Down
Loading
Loading