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
30 changes: 24 additions & 6 deletions docs/program-manager-analytics-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Endpoint доступен менеджерам указанной програм
"participant_funnel": {
"registrations": 4,
"unique_participants": 3,
"visited_participants": 2,
"onboarded_participants": 2,
"with_team": 2,
"project_creators": 1,
Expand Down Expand Up @@ -65,6 +66,7 @@ Endpoint доступен менеджерам указанной програм
}
},
"attention": {
"participants_without_visit": 1,
"participants_without_onboarding": 1,
"participants_without_team": 1,
"projects_awaiting_evaluation": 1,
Expand All @@ -90,21 +92,37 @@ Endpoint доступен менеджерам указанной програм

| Пункт интерфейса | Условие |
| --- | --- |
| Участники не завершили онбординг | Есть уникальные зарегистрированные пользователи с `onboarding_stage IS NOT NULL`, включая этап 0. Завершённый онбординг — `NULL`. Подтверждение почты и членство в команде не заменяют онбординг; временного порога нет. |
| Участники ещё не заходили на платформу | Уникальные зарегистрированные пользователи программы, у которых `last_activity IS NULL AND last_login IS NULL`. Ни онбординг, ни подтверждение почты, ни членство в команде не определяют посещение. Временного порога нет; посещения всей платформы до регистрации в программе также учитываются. |
| Участники без команды | Зарегистрированный пользователь не является ни руководителем, ни `Collaborator` проекта, связанного с этой программой. Команда в другой программе не исключает пользователя; временного порога нет. |
| Работы ожидают оценивания | Только сданные работы. В открытом режиме нет ни одной оценки по критериям программы. В распределённом режиме нет назначений либо хотя бы одно назначение ещё не завершено, включая частично оценённые работы. Временного порога нет. |
| Эксперты задерживают оценивание | Только распределённое оценивание: задана `datetime_evaluation_ends`, наступил момент за 5 суток до неё (`now >= datetime_evaluation_ends - 5 days`), у эксперта есть хотя бы одно назначение со статусом `pending` или `in_progress` на сданную работу. До дедлайна — `warning`, начиная с дедлайна — `critical`; сигнал остаётся до завершения оценок. Время ожидания 24/48 часов не влияет на появление. Несданные работы и завершённые назначения не создают сигнал. |

Новый счётчик `attention.participants_without_onboarding` использует тот же
Счётчик `attention.participants_without_visit` использует тот же
предикат, что и список
`GET /programs/{id}/manager-overview/participants-without-onboarding/`.
`GET /programs/{id}/manager-overview/participants-without-visit/`.
Список поддерживает `search`, `limit` (1–100, по умолчанию 25), `offset`;
возвращает стандартные `count`, `next`, `previous`, `results` с публичными полями
участника. Права, поиск, порядок и пагинация совпадают со списком участников
без команды. Удалённые пользователи и регистрации других программ исключены.
Равенство: `unique_participants = onboarded_participants + participants_without_onboarding`.
Равенство: `unique_participants = visited_participants + participants_without_visit`.
Новая миграция БД не требуется.

Воронка интерфейса: «Зарегистрировались → Заходили на платформу → Вступили в команду».
`visited_participants` считает уникальных зарегистрированных пользователей с
`last_activity IS NOT NULL OR last_login IS NOT NULL`. Одновременное наличие обеих
дат не удваивает человека. Это факт хотя бы одного зафиксированного посещения
всей платформы, без ограничения по давности или дате регистрации в программе.
`last_activity` обновляется при JWT-аутентификации запросов с ограничением раз
в 15 минут; `last_login` сохраняет пользователей с записанным входом и пустой
активностью. Пустые даты означают отсутствие зафиксированного посещения.
Внешняя регистрационная форма автоматически пропускает онбординг, поэтому его
завершение не используется как свидетельство посещения.

Старые `onboarded_participants`, `participants_without_onboarding` и endpoint
`participants-without-onboarding/` сохранены с прежней семантикой для совместимости
при последовательной выкладке. Новый интерфейс их не использует и не подставляет
показатели онбординга при отсутствии новой метрики посещения.

Существующее API-поле `projects_not_submitted` сохранено для совместимости,
но его строка в «Требует внимания» убрана по требованию не дублировать
показатель, который получается как «Создано − Сдано».
Expand All @@ -114,7 +132,7 @@ Endpoint доступен менеджерам указанной програм
пользователем.
- `unique_participants` и `summary.participants.total` — уникальные ненулевые
`PartnerProgramUserProfile.user_id`.
- `onboarded_participants` — уникальные ненулевые пользователи, зарегистрированные
- `onboarded_participants` (устаревшее поле для совместимости) — уникальные ненулевые пользователи, зарегистрированные
в этой программе, у которых `User.onboarding_stage IS NULL`: завершён стартовый
сценарий платформы. Подтверждение почты (`is_active`) и произвольный запрос к API
(`last_activity`) не заменяют этот критерий. Онбординг позволяет пропускать шаги,
Expand All @@ -123,7 +141,7 @@ Endpoint доступен менеджерам указанной програм
до или после регистрации в программе, по этому полю нельзя.
- Этапы участников показывают текущее состояние одной группы зарегистрированных
пользователей, а не последовательную событийную конверсию: `with_team` может
превышать `onboarded_participants`, если команда появилась раньше. Значения
превышать `visited_participants`, если команда была создана без записанного посещения. Значения
возвращаются без сортировки, ограничения или искусственного уменьшения.
- Участник считается состоящим в команде, если он является руководителем либо
`Collaborator` проекта, связанного с программой через
Expand Down
2 changes: 2 additions & 0 deletions partner_programs/serializers/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ProgramAnalyticsSummarySerializer(serializers.Serializer):
class ProgramParticipantFunnelSerializer(serializers.Serializer):
registrations = serializers.IntegerField(min_value=0)
unique_participants = serializers.IntegerField(min_value=0)
visited_participants = serializers.IntegerField(min_value=0)
onboarded_participants = serializers.IntegerField(min_value=0)
with_team = serializers.IntegerField(min_value=0)
project_creators = serializers.IntegerField(min_value=0)
Expand Down Expand Up @@ -136,6 +137,7 @@ class ProjectsNotSubmittedSerializer(AnalyticsTotalSerializer):


class ProgramAttentionSerializer(serializers.Serializer):
participants_without_visit = serializers.IntegerField(min_value=0)
participants_without_onboarding = serializers.IntegerField(min_value=0)
participants_without_team = serializers.IntegerField(min_value=0)
projects_awaiting_evaluation = serializers.IntegerField(min_value=0)
Expand Down
31 changes: 31 additions & 0 deletions partner_programs/services/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ def participants_without_team_rows(program_id):
)


def _visited_participant_filter():
"""Зарегистрированный пользователь с активностью или входом на платформу."""
return Q(user_id__isnull=False) & (
Q(user__last_activity__isnull=False) | Q(user__last_login__isnull=False)
)


def _without_visit_filter():
"""Оба свидетельства посещения отсутствуют; онбординг не учитывается."""
return Q(
user_id__isnull=False,
user__last_activity__isnull=True,
user__last_login__isnull=True,
)


def participants_without_visit_rows(program_id):
"""Тот же признак отсутствия посещения, что и в счётчике внимания."""
return _unique_participant_rows(
PartnerProgramUserProfile.objects.filter(partner_program_id=program_id).filter(
_without_visit_filter()
)
)


def _without_onboarding_filter():
"""Незавершённый онбординг у существующего пользователя программы."""
return Q(user_id__isnull=False, user__onboarding_stage__isnull=False)
Expand Down Expand Up @@ -113,6 +138,10 @@ def _get_participant_metrics(program_id: int) -> dict[str, int]:
filter=participant_filter,
distinct=True,
),
visited_participants=Count(
"user_id", filter=_visited_participant_filter(), distinct=True
),
without_visit=Count("user_id", filter=_without_visit_filter(), distinct=True),
onboarded_participants=Count(
"user_id",
filter=participant_filter & Q(user__onboarding_stage__isnull=True),
Expand Down Expand Up @@ -381,6 +410,7 @@ def build_program_manager_analytics(program) -> dict:
"participant_funnel": {
"registrations": participants["registrations"],
"unique_participants": participants["unique_participants"],
"visited_participants": participants["visited_participants"],
"onboarded_participants": participants["onboarded_participants"],
"with_team": participants["with_team"],
"project_creators": participants["project_creators"],
Expand Down Expand Up @@ -408,6 +438,7 @@ def build_program_manager_analytics(program) -> dict:
},
},
"attention": {
"participants_without_visit": participants["without_visit"],
"participants_without_onboarding": participants["without_onboarding"],
"participants_without_team": participants["without_team"],
"projects_awaiting_evaluation": projects_awaiting_evaluation,
Expand Down
Loading
Loading