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
14 changes: 14 additions & 0 deletions docs/program-manager-analytics-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ Endpoint доступен менеджерам указанной програм
"regions": {
"total": 1,
"items": [{"name": "Москва", "count": 2}]
},
"participant_regions": {
"total": 2,
"items": [
{"name": "Москва", "count": 2},
{"name": "Набережные Челны", "count": 1}
]
}
},
"participant_funnel": {
Expand Down Expand Up @@ -93,6 +100,13 @@ Endpoint доступен менеджерам указанной програм
через `Expert.programs`, независимо от наличия назначений.
- Регионы строятся по непустому `Project.region` связанных проектов. Пробелы по
краям удаляются; `items` сортируется по убыванию количества, затем по имени.
- `summary.participant_regions` строится по `User.city` той же группы уникальных
ненулевых пользователей программы, что используется для
`summary.participants.total`. Пустые и состоящие только из пробелов значения
исключаются. Пробелы по краям удаляются; одинаковые после этого строки
группируются, а `items` сортируется по убыванию количества, затем по имени.
Значения не приводятся к другому регистру, не исправляются и не сопоставляются
с каноническими регионами: legacy-строки остаются отдельными элементами.
- `activity` всегда содержит последние 30 календарных дней, включая текущий.
Пропущенные даты заполняются нулями. Регистрации группируются по
`PartnerProgramUserProfile.datetime_created`, сдачи — по
Expand Down
1 change: 1 addition & 0 deletions partner_programs/serializers/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ProgramAnalyticsSummarySerializer(serializers.Serializer):
projects = AnalyticsTotalSerializer()
experts = AnalyticsTotalSerializer()
regions = AnalyticsRegionsSerializer()
participant_regions = AnalyticsRegionsSerializer()


class ProgramParticipantFunnelSerializer(serializers.Serializer):
Expand Down
20 changes: 20 additions & 0 deletions partner_programs/services/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ def _get_regions(program_id: int) -> list[dict]:
)


def _get_participant_regions(program_id: int) -> list[dict]:
return list(
PartnerProgramUserProfile.objects.filter(
partner_program_id=program_id,
user_id__isnull=False,
user__city__isnull=False,
)
.annotate(name=Trim("user__city"))
.exclude(name="")
.values("name")
.annotate(count=Count("user_id", distinct=True))
.order_by("-count", "name")
)


def _get_solution_metrics(program, assignments_by_project: dict) -> dict[str, int]:
program_id = program.id
project_rows = (
Expand Down Expand Up @@ -210,6 +225,7 @@ def build_program_manager_analytics(program) -> dict:
program_id = program.id
participants = _get_participant_metrics(program_id)
regions = _get_regions(program_id)
participant_regions = _get_participant_regions(program_id)
assignments, assignments_by_project = _get_assignment_metrics(program_id)
solutions = _get_solution_metrics(program, assignments_by_project)

Expand All @@ -223,6 +239,10 @@ def build_program_manager_analytics(program) -> dict:
"projects": {"total": solutions["created"]},
"experts": {"total": program.experts.count()},
"regions": {"total": len(regions), "items": regions},
"participant_regions": {
"total": len(participant_regions),
"items": participant_regions,
},
},
"participant_funnel": {
"registrations": participants["registrations"],
Expand Down
42 changes: 42 additions & 0 deletions partner_programs/tests/test_manager_analytics_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def test_summary_participant_funnel_and_regions_use_real_relations(self):
"total": 1,
"items": [{"name": "Moscow", "count": 2}],
},
"participant_regions": {"total": 0, "items": []},
},
)
self.assertEqual(
Expand All @@ -154,6 +155,43 @@ def test_summary_participant_funnel_and_regions_use_real_relations(self):
)
self.assertEqual(response.data["attention"]["participants_without_team"], 1)

def test_participant_regions_use_only_program_participants_and_preserve_legacy_values(
self,
):
for prefix, city in (
("analytics-region-moscow-1", "Москва"),
("analytics-region-moscow-2", " Москва "),
("analytics-region-legacy-city", "Набережные Челны"),
("analytics-region-legacy-typo", "Мсква"),
("analytics-region-empty", ""),
("analytics-region-whitespace", " "),
("analytics-region-null", None),
):
create_program_member(
self.program,
user=create_user(prefix=prefix, city=city),
)

other_program = create_partner_program(name="Other analytics program")
create_program_member(
other_program,
user=create_user(prefix="analytics-region-other-program", city="Москва"),
)

response = self.client.get(self.url)

self.assertEqual(response.status_code, 200)
participant_regions = response.data["summary"]["participant_regions"]
self.assertEqual(participant_regions["total"], 3)
self.assertEqual(
{item["name"]: item["count"] for item in participant_regions["items"]},
{
"Москва": 2,
"Набережные Челны": 1,
"Мсква": 1,
},
)

def test_open_evaluation_uses_any_score_instead_of_max_project_rates(self):
evaluated_project = create_project(name="Evaluated")
partial_project = create_project(name="Partially evaluated")
Expand Down Expand Up @@ -419,6 +457,10 @@ def test_empty_program_returns_zero_metrics_and_continuous_activity(self):
self.assertEqual(response.data["summary"]["projects"]["total"], 0)
self.assertEqual(response.data["summary"]["experts"]["total"], 0)
self.assertEqual(response.data["summary"]["regions"], {"total": 0, "items": []})
self.assertEqual(
response.data["summary"]["participant_regions"],
{"total": 0, "items": []},
)
self.assertEqual(len(response.data["activity"]), 30)
self.assertTrue(
all(
Expand Down
Loading