Skip to content

Commit 2c854a5

Browse files
committed
feat: support case-scoped expert pools and project assignment
1 parent d7949c3 commit 2c854a5

7 files changed

Lines changed: 640 additions & 7 deletions

File tree

‎docs/expert-workspace.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
| Метод и путь относительно `/programs/{id}/management/` | Назначение |
1313
| --- | --- |
1414
| GET `experts/` | Состав, приглашения, нагрузка, N, нехватка и revision |
15+
| PUT `experts/{expert_id}/cases/` | Кейсы эксперта `{revision, all_cases, case_names}` |
16+
| POST `project-experts/` | Атомарное назначение `{revision, project_ids, expert_ids, mode}` |
1517
| POST `experts/` | Пригласить `{email}` |
1618
| POST `expert-invitations/{id}/resend/` | Новая ссылка, прежняя недействительна |
1719
| POST `expert-invitations/{id}/revoke/` | Отозвать непринятое приглашение |
1820
| GET `experts/{expert_id}/assignments/` | Проекты с assigned/protected/own_team |
1921
| PUT `experts/{expert_id}/assignments/` | Заменить набор `{revision, project_ids}` |
20-
| POST `expert-allocation/` | Предпросмотр `{expert_ids, project_ids?}` |
22+
| POST `expert-allocation/` | Предпросмотр `{expert_ids, project_ids?, case_pools?, revision?}` |
2123
| POST `expert-allocation/apply/` | Применить подписанный `{plan_token}` |
2224
| POST `evaluators/` | Предпросмотр `{required_evaluators}` |
2325
| PUT `evaluators/` | Подтвердить `{required_evaluators, revision}` |
@@ -60,6 +62,24 @@ repeat_password, accept_policy, accept_age и атомарно создаёт а
6062

6163
## Правила
6264

65+
`GET experts/` также возвращает `cases`, сданные опубликованные `projects` с
66+
экспертами/статусами оценок и `all_cases`/`case_names` для каждого участника состава.
67+
Без отдельной записи `ExpertCaseScope` эксперт доступен во всех текущих и будущих
68+
кейсах. Можно выбрать несколько кейсов; списки пересекаются. Эти правила одинаковы
69+
для ручного и автоматического назначения. Смена состава кейсов не удаляет назначения.
70+
71+
`project-experts/`: `mode=replace` меняет состав одного проекта с защитой оценок;
72+
`mode=add` добавляет экспертов одному или нескольким проектам одного кейса без снятия
73+
существующих. Весь запрос атомарен, включая проверку собственной команды, членства,
74+
доступности кейса, сдачи/публикации и revision. Недоступного эксперта можно сохранить
75+
в прежнем составе, но нельзя назначить новому проекту.
76+
77+
`case_pools` — массив `{case_name, expert_ids}`, где null означает проекты без кейса.
78+
Нагрузка общего эксперта учитывается один раз по всей программе. Без поля сохраняется
79+
старый контракт общего выбора; ограничения кейсов действуют и в нём. Отсутствующий или
80+
пустой состав нужного кейса приводит к нехватке и блокирует применение плана.
81+
Revision учитывает определения/значения кейсов и доступность экспертов по кейсам.
82+
6383
- Pending не является экспертом и не участвует в распределении.
6484
- Новые назначения — только принявшим приглашение активным экспертам, на сданные
6585
проекты своей программы; собственная команда исключена.
@@ -79,6 +99,10 @@ repeat_password, accept_policy, accept_age и атомарно создаёт а
7999

80100
## Совместимость и rollout
81101

102+
Для вкладок v2 нужна `project_rates.0005_expertcasescope_and_more` (новая таблица,
103+
без изменения оценок и назначений). Сначала backend и миграция, затем Angular.
104+
Существующие API-поля и endpoints сохранены; старый frontend совместим.
105+
82106
Добавлены `PartnerProgram.required_evaluators`, `ExpertInvitation` и nullable
83107
`CustomUser.birthday`: migrations partner_programs.0022, project_rates.0004,
84108
users.0065. Обычная регистрация продолжает требовать дату рождения; обычный

‎partner_programs/urls.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
ExpertsView,
4848
InvitationActionView,
4949
ExpertAssignmentsView,
50+
ExpertCaseScopeView,
51+
ProjectExpertsView,
5052
AllocationView,
5153
AllocationApplyView,
5254
EvaluatorsView,
@@ -56,6 +58,11 @@
5658

5759
urlpatterns = [
5860
path("<int:pk>/management/experts/", ExpertsView.as_view()),
61+
path(
62+
"<int:pk>/management/experts/<int:expert_id>/cases/",
63+
ExpertCaseScopeView.as_view(),
64+
),
65+
path("<int:pk>/management/project-experts/", ProjectExpertsView.as_view()),
5966
path(
6067
"<int:pk>/management/expert-invitations/<int:invitation_id>/<str:action>/",
6168
InvitationActionView.as_view(),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.11 on 2026-10-18 20:17
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('users', '0065_alter_customuser_birthday'),
11+
('partner_programs', '0022_partnerprogram_required_evaluators'),
12+
('project_rates', '0004_expertinvitation_and_more'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='ExpertCaseScope',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('all_cases', models.BooleanField(default=True)),
21+
('case_names', models.JSONField(blank=True, default=list)),
22+
('expert', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='case_scopes', to='users.expert')),
23+
('partner_program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expert_case_scopes', to='partner_programs.partnerprogram')),
24+
],
25+
),
26+
migrations.AddConstraint(
27+
model_name='expertcasescope',
28+
constraint=models.UniqueConstraint(fields=('partner_program', 'expert'), name='unique_program_expert_case_scope'),
29+
),
30+
]

‎project_rates/models.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,27 @@ def delete(self, *args, **kwargs):
244244
return super().delete(*args, **kwargs)
245245

246246

247+
class ExpertCaseScope(models.Model):
248+
"""Program-specific eligibility; no row means all current and future cases."""
249+
250+
partner_program = models.ForeignKey(
251+
PartnerProgram, on_delete=models.CASCADE, related_name="expert_case_scopes"
252+
)
253+
expert = models.ForeignKey(
254+
"users.Expert", on_delete=models.CASCADE, related_name="case_scopes"
255+
)
256+
all_cases = models.BooleanField(default=True)
257+
case_names = models.JSONField(default=list, blank=True)
258+
259+
class Meta:
260+
constraints = [
261+
models.UniqueConstraint(
262+
fields=["partner_program", "expert"],
263+
name="unique_program_expert_case_scope",
264+
)
265+
]
266+
267+
247268
class ExpertInvitation(models.Model):
248269
"""Email invitation; only a digest of the bearer credential is stored."""
249270

0 commit comments

Comments
 (0)