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
26 changes: 25 additions & 1 deletion docs/expert-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
| Метод и путь относительно `/programs/{id}/management/` | Назначение |
| --- | --- |
| GET `experts/` | Состав, приглашения, нагрузка, N, нехватка и revision |
| PUT `experts/{expert_id}/cases/` | Кейсы эксперта `{revision, all_cases, case_names}` |
| POST `project-experts/` | Атомарное назначение `{revision, project_ids, expert_ids, mode}` |
| POST `experts/` | Пригласить `{email}` |
| POST `expert-invitations/{id}/resend/` | Новая ссылка, прежняя недействительна |
| POST `expert-invitations/{id}/revoke/` | Отозвать непринятое приглашение |
| GET `experts/{expert_id}/assignments/` | Проекты с assigned/protected/own_team |
| PUT `experts/{expert_id}/assignments/` | Заменить набор `{revision, project_ids}` |
| POST `expert-allocation/` | Предпросмотр `{expert_ids, project_ids?}` |
| POST `expert-allocation/` | Предпросмотр `{expert_ids, project_ids?, case_pools?, revision?}` |
| POST `expert-allocation/apply/` | Применить подписанный `{plan_token}` |
| POST `evaluators/` | Предпросмотр `{required_evaluators}` |
| PUT `evaluators/` | Подтвердить `{required_evaluators, revision}` |
Expand Down Expand Up @@ -60,6 +62,24 @@ repeat_password, accept_policy, accept_age и атомарно создаёт а

## Правила

`GET experts/` также возвращает `cases`, сданные опубликованные `projects` с
экспертами/статусами оценок и `all_cases`/`case_names` для каждого участника состава.
Без отдельной записи `ExpertCaseScope` эксперт доступен во всех текущих и будущих
кейсах. Можно выбрать несколько кейсов; списки пересекаются. Эти правила одинаковы
для ручного и автоматического назначения. Смена состава кейсов не удаляет назначения.

`project-experts/`: `mode=replace` меняет состав одного проекта с защитой оценок;
`mode=add` добавляет экспертов одному или нескольким проектам одного кейса без снятия
существующих. Весь запрос атомарен, включая проверку собственной команды, членства,
доступности кейса, сдачи/публикации и revision. Недоступного эксперта можно сохранить
в прежнем составе, но нельзя назначить новому проекту.

`case_pools` — массив `{case_name, expert_ids}`, где null означает проекты без кейса.
Нагрузка общего эксперта учитывается один раз по всей программе. Без поля сохраняется
старый контракт общего выбора; ограничения кейсов действуют и в нём. Отсутствующий или
пустой состав нужного кейса приводит к нехватке и блокирует применение плана.
Revision учитывает определения/значения кейсов и доступность экспертов по кейсам.

- Pending не является экспертом и не участвует в распределении.
- Новые назначения — только принявшим приглашение активным экспертам, на сданные
проекты своей программы; собственная команда исключена.
Expand All @@ -79,6 +99,10 @@ repeat_password, accept_policy, accept_age и атомарно создаёт а

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

Для вкладок v2 нужна `project_rates.0005_expertcasescope_and_more` (новая таблица,
без изменения оценок и назначений). Сначала backend и миграция, затем Angular.
Существующие API-поля и endpoints сохранены; старый frontend совместим.

Добавлены `PartnerProgram.required_evaluators`, `ExpertInvitation` и nullable
`CustomUser.birthday`: migrations partner_programs.0022, project_rates.0004,
users.0065. Обычная регистрация продолжает требовать дату рождения; обычный
Expand Down
7 changes: 7 additions & 0 deletions partner_programs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
ExpertsView,
InvitationActionView,
ExpertAssignmentsView,
ExpertCaseScopeView,
ProjectExpertsView,
AllocationView,
AllocationApplyView,
EvaluatorsView,
Expand All @@ -56,6 +58,11 @@

urlpatterns = [
path("<int:pk>/management/experts/", ExpertsView.as_view()),
path(
"<int:pk>/management/experts/<int:expert_id>/cases/",
ExpertCaseScopeView.as_view(),
),
path("<int:pk>/management/project-experts/", ProjectExpertsView.as_view()),
path(
"<int:pk>/management/expert-invitations/<int:invitation_id>/<str:action>/",
InvitationActionView.as_view(),
Expand Down
30 changes: 30 additions & 0 deletions project_rates/migrations/0005_expertcasescope_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.11 on 2026-10-18 20:17

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('users', '0065_alter_customuser_birthday'),
('partner_programs', '0022_partnerprogram_required_evaluators'),
('project_rates', '0004_expertinvitation_and_more'),
]

operations = [
migrations.CreateModel(
name='ExpertCaseScope',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('all_cases', models.BooleanField(default=True)),
('case_names', models.JSONField(blank=True, default=list)),
('expert', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='case_scopes', to='users.expert')),
('partner_program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expert_case_scopes', to='partner_programs.partnerprogram')),
],
),
migrations.AddConstraint(
model_name='expertcasescope',
constraint=models.UniqueConstraint(fields=('partner_program', 'expert'), name='unique_program_expert_case_scope'),
),
]
21 changes: 21 additions & 0 deletions project_rates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,27 @@ def delete(self, *args, **kwargs):
return super().delete(*args, **kwargs)


class ExpertCaseScope(models.Model):
"""Program-specific eligibility; no row means all current and future cases."""

partner_program = models.ForeignKey(
PartnerProgram, on_delete=models.CASCADE, related_name="expert_case_scopes"
)
expert = models.ForeignKey(
"users.Expert", on_delete=models.CASCADE, related_name="case_scopes"
)
all_cases = models.BooleanField(default=True)
case_names = models.JSONField(default=list, blank=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["partner_program", "expert"],
name="unique_program_expert_case_scope",
)
]


class ExpertInvitation(models.Model):
"""Email invitation; only a digest of the bearer credential is stored."""

Expand Down
Loading
Loading