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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ DRF_THROTTLE_TOKEN_OBTAIN=10/min
DRF_THROTTLE_PROGRAM_REGISTER_NEW=10/min
DRF_THROTTLE_APPLICATION_CREATE=10/min
DRF_THROTTLE_SUBMISSION_CREATE=10/min
DRF_THROTTLE_EVALUATION_AMEND=30/min

SENTRY_DSN=

Expand Down
50 changes: 42 additions & 8 deletions docs/expert-evaluation-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
- DEV-050 — Expert Submission API
- DEV-051 — Evaluation mutation API
- DEV-052 — Manager evaluation read API
- DEV-073 — изменение submitted Evaluation с неизменяемой историей

Roadmap-IDs: DEV-050, DEV-051, DEV-052
Roadmap-IDs: DEV-050, DEV-051, DEV-052, DEV-073

## Назначение

Expand All @@ -23,6 +24,8 @@ membership в программе.
- `SubmissionExpertAssignment` связывает конкретную Submission и Expert.
- `Evaluation` хранит форму оценки, комментарий и lifecycle.
- `EvaluationScore` хранит одно числовое значение и snapshot критерия.
- `EvaluationAmendment` хранит неизменяемые снимки до и после изменения
отправленной оценки.
- `project_rates.Criteria` временно используется как каталог критериев
программы.

Expand All @@ -40,16 +43,21 @@ Assignment:
Evaluation:

- `draft` — доступно атомарное обновление `comment` и полного набора `scores`;
- `submitted` — терминальное неизменяемое состояние.
- `submitted` — отправленное состояние; обычный PATCH запрещен, изменение
возможно только через DEV-073 amend с audit-записью.

Финальный submit в одной транзакции переводит Evaluation в `submitted`, а
assignment в `completed`. Reopen и revision history отсутствуют.
assignment в `completed`. Amend не возвращает Evaluation в draft, не меняет
`submitted_at` и сохраняет историю. Reopen отсутствует.

## Права доступа

- Expert list/detail доступны только эксперту с текущим membership программы
и assignment `assigned` или `completed`.
- Создание и изменение draft требуют assignment `assigned`.
- Amend submitted Evaluation доступен только владельцу при assignment
`assigned` или `completed` и текущем membership программы.
- Историю amend читают владелец, manager программы и staff/superuser.
- Staff может открыть PII-safe expert detail в административном режиме.
- Владелец-эксперт, manager программы и staff могут читать Evaluation detail.
- Manager list/detail ограничены конкретной программой.
Expand Down Expand Up @@ -111,12 +119,31 @@ Autosave владельца draft. Можно передать `comment`, `score
Повторный submit идемпотентен: возвращает `200` и сохраняет первоначальный
`submitted_at`.

### PATCH /evaluations/\<evaluation_id\>/amend/

Изменяет `comment` и/или полный набор `scores` уже отправленной Evaluation.
Endpoint доступен только эксперту-владельцу. Evaluation остается `submitted`,
исходный `submitted_at` сохраняется, `amended_at` обновляется, а `total_score`
сбрасывается в `null` до появления формулы пересчета.

Если передан `scores`, payload обязан содержать все текущие числовые Criteria
программы. Валидация и замена scores, обновление Evaluation и создание
`EvaluationAmendment` выполняются в одной транзакции. Полностью совпадающий
запрос возвращает `200`, но не обновляет `amended_at` и не создает историю.

### GET /evaluations/\<evaluation_id\>/amendments/

Возвращает упорядоченную неизменяемую историю со снимками comment, scores и
total_score до и после каждой правки. Доступ имеют эксперт-владелец, manager
программы и staff/superuser. Для остальных существование Evaluation скрывается
ответом `404`.

## Manager API

### GET /programs/\<program_id\>/submission-assignments/

Существующий контракт дополнен nullable-полем `evaluation` с полями `id`,
`status`, `updated_at`, `submitted_at`, `total_score`. Старое поле
`status`, `updated_at`, `submitted_at`, `amended_at`, `total_score`. Старое поле
`evaluation_status` сохранено.

### GET /programs/\<program_id\>/evaluations/
Expand All @@ -131,7 +158,7 @@ Autosave владельца draft. Можно передать `comment`, `score
- `limit`, `offset`.

Ответ содержит безопасные Submission/Expert/Assignment summaries, scores,
comment, total_score и timestamps.
comment, total_score, `submitted_at`, `amended_at` и остальные timestamps.

### GET /programs/\<program_id\>/evaluations/\<evaluation_id\>/

Expand All @@ -147,6 +174,8 @@ Read-only detail в пределах программы manager. Mutation-мет
- PATCH сначала валидирует весь новый набор и только затем удаляет старый.
- Submit требует все текущие числовые Criteria программы и повторно проверяет
типы и диапазоны.
- Amend с `scores` также требует полный набор текущих числовых Criteria до
начала любых изменений.
- Нечисловые Criteria не включаются в форму; свободный текст хранится в
`Evaluation.comment`.

Expand All @@ -155,6 +184,8 @@ Read-only detail в пределах программы manager. Mutation-мет
- Повторный create существующего draft возвращает его без изменения.
- Повторный PATCH с теми же данными не создаёт дублей.
- Повторный submit submitted Evaluation не меняет `submitted_at`.
- Повторный amend с теми же comment и scores не создает новую историю и не
меняет `amended_at`.
- Уникальность `submission + expert` и `evaluation + criterion` дополнительно
защищена существующими constraints.

Expand All @@ -165,6 +196,8 @@ Read-only detail в пределах программы manager. Mutation-мет
- PATCH блокирует assignment и Evaluation; набор scores заменяется атомарно.
- Submit блокирует assignment и Evaluation в стабильном порядке, валидирует
полную форму и записывает одинаковый timestamp в Evaluation и assignment.
- Amend блокирует assignment и Evaluation в том же порядке, затем атомарно заменяет scores,
обновляет Evaluation и записывает снимок `EvaluationAmendment`.
- Два конкурентных submit не создают противоречивое терминальное состояние.

## Защита персональных данных
Expand All @@ -182,7 +215,8 @@ Manager responses содержат только минимальные имя/ф

- `evaluation_create`: `10/min`;
- `evaluation_update`: `120/min`;
- `evaluation_submit`: `20/min`.
- `evaluation_submit`: `20/min`;
- `evaluation_amend`: `30/min`.

Scopes применяются только к соответствующим mutation-методам и не включают
глобальный DRF throttle.
Expand All @@ -205,8 +239,8 @@ Scopes применяются только к соответствующим mut
`partner_programs.tests.test_expert_evaluation_api`

Он покрывает expert list/detail, PII regression, draft create, полную замену
scores, rollback, submit lifecycle, concurrent submit, manager read-only API,
filters, pagination и throttling.
scores, rollback, submit lifecycle, amend submitted Evaluation, audit history,
concurrent submit, manager read-only API, filters, pagination и throttling.

Также сохраняются model tests и Assignment API regression.

Expand Down
61 changes: 52 additions & 9 deletions docs/submission-evaluation-domain.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- Roadmap: DEV-073 -->
# Submission Evaluation Domain RFC

Статус: proposal.
Expand All @@ -11,6 +12,8 @@
`POST /submission-assignments/<assignment_id>/revoke/`;
- Expert Submission read API, Evaluation mutation API и manager Evaluation
read API реализованы в рамках DEV-050, DEV-051 и DEV-052;
- изменение submitted Evaluation и неизменяемая история реализованы в рамках
DEV-073;
- frontend, `Result`, ranking и публикация итогов еще не реализованы;
- временно используется существующий `project_rates.Criteria`;
- дедлайном MVP остается существующий `datetime_evaluation_ends`;
Expand Down Expand Up @@ -190,6 +193,21 @@ backfill ProjectScore в Evaluation и обратная синхронизаци
отдельный `EvaluationCriterion` в самостоятельном PR. Не следует молча
добавлять эти значения в legacy Criteria.

### EvaluationAmendment

`EvaluationAmendment` — неизменяемая audit-запись изменения submitted
Evaluation:

- `evaluation`;
- `changed_by`;
- `previous_comment`, `comment`;
- `previous_scores`, `scores`;
- `previous_total_score`, `total_score`;
- `created_at`.

`Evaluation.amended_at` хранит время последнего фактического изменения.
Снимки scores включают идентификатор и snapshot критерия вместе со значением.

## 4. Сущности и связи

```text
Expand Down Expand Up @@ -242,10 +260,11 @@ Cross-table и ManyToMany invariants проверяются транзакцио
| Статус | Редактирование | Допустимый переход |
|---|---|---|
| `draft` | Назначенным экспертом | `submitted` |
| `submitted` | Запрещено | Терминальный в MVP |
| `submitted` | Только отдельный amend владельца с audit | Остается `submitted` |

MVP намеренно не добавляет `cancelled`, `revised` или `reopened` без готового
audit contract. Возврат submitted Evaluation к редактированию запрещен.
audit contract. Возврат submitted Evaluation в draft запрещен; DEV-073 меняет
ее только отдельной атомарной операцией с неизменяемой историей.

Будущее явное правило может добавить manager-only action `reopen` с
обязательной причиной. До перехода прежняя финальная форма и scores должны
Expand Down Expand Up @@ -428,6 +447,28 @@ Service под transaction и row locks:

Рекомендуемый scope: `evaluation_submit`, default `20/min`.

#### `PATCH /evaluations/<evaluation_id>/amend/`

Права: только владелец-эксперт. Evaluation должна оставаться в статусе
`submitted`, Submission — в `submitted/final`, назначение — в
`assigned/completed`, а эксперт должен по-прежнему состоять в Program.

Можно изменить `comment` и полностью заменить `scores`. Переданный набор
scores обязан содержать все числовые Criteria Program; типы, диапазоны и
принадлежность Program проверяются теми же правилами, что для draft.
Изменение выполняется атомарно, не создает новую Evaluation, не меняет
`submitted_at` и статус assignment. `amended_at` обновляется только при
фактическом изменении, а `total_score` сбрасывается до отдельного пересчета.

Полностью совпадающий запрос является no-op и не создает запись истории.
Рекомендуемый scope: `evaluation_amend`, default `30/min`.

#### `GET /evaluations/<evaluation_id>/amendments/`

Неизменяемые снимки до и после изменения доступны владельцу Evaluation,
manager соответствующей Program и staff/superuser. Остальные получают `404`,
чтобы endpoint не раскрывал существование оценки.

### Назначение экспертов менеджером

#### `GET /programs/<program_id>/submission-assignments/`
Expand Down Expand Up @@ -479,10 +520,10 @@ Delete endpoint не используется, чтобы сохранять и

### Не входящий в MVP reopen

Если продукт подтвердит исправление финальной оценки, отдельный manager action
может иметь вид `POST /evaluations/<id>/reopen/`. Он требует reason и
неизменяемого snapshot предыдущей submitted revision. До реализации revision
model endpoint добавлять нельзя.
DEV-073 разрешает владельцу точечно изменить submitted Evaluation без возврата
в draft. Отдельный manager action `POST /evaluations/<id>/reopen/` по-прежнему
не входит в MVP: он потребует reason, отдельной модели переходов статуса и
самостоятельного продуктового решения.

## 8. DB constraints

Expand All @@ -494,14 +535,16 @@ model endpoint добавлять нельзя.
историю.
2. `UniqueConstraint(submission, expert)` для Evaluation.
3. `UniqueConstraint(evaluation, criterion)` для EvaluationScore.
4. Общий `value >= 0` не добавляется без продуктового правила: существующий
4. `EvaluationAmendment` хранит неизменяемые JSON-снимки comment, scores и
total_score до и после каждой фактической правки.
5. Общий `value >= 0` не добавляется без продуктового правила: существующий
Criteria может допускать другой диапазон. Индивидуальные min/max являются
cross-row правилом и проверяются service/model validation; для Criteria
типа `int` дополнительно запрещается дробное значение.
5. `submitted_at IS NOT NULL` для submitted Evaluation и `IS NULL` для draft,
6. `submitted_at IS NOT NULL` для submitted Evaluation и `IS NULL` для draft,
если синтаксис текущей версии Django/PostgreSQL позволяет выразить это без
неоднозначности.
6. Assignment timestamps согласуются со статусом: revoked требует
7. Assignment timestamps согласуются со статусом: revoked требует
`revoked_at`, completed требует `completed_at`.

Транзакционный service дополнительно проверяет:
Expand Down
51 changes: 51 additions & 0 deletions partner_programs/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Roadmap: DEV-073

import re

import tablib
Expand All @@ -13,6 +15,7 @@
from partner_programs.models import (
Application,
Evaluation,
EvaluationAmendment,
EvaluationScore,
PartnerProgram,
PartnerProgramField,
Expand Down Expand Up @@ -311,6 +314,7 @@ class EvaluationAdmin(admin.ModelAdmin):
"expert",
"status",
"submitted_at",
"amended_at",
"created_at",
"updated_at",
)
Expand All @@ -335,6 +339,7 @@ class EvaluationAdmin(admin.ModelAdmin):
readonly_fields = (
"created_at",
"updated_at",
"amended_at",
)
list_select_related = (
"submission",
Expand Down Expand Up @@ -386,6 +391,52 @@ class EvaluationScoreAdmin(admin.ModelAdmin):
date_hierarchy = "created_at"


@admin.register(EvaluationAmendment)
class EvaluationAmendmentAdmin(admin.ModelAdmin):
list_display = (
"id",
"evaluation",
"changed_by",
"created_at",
)
list_filter = (
"evaluation__submission__program",
"created_at",
)
search_fields = (
"=evaluation__id",
"=evaluation__submission__id",
"changed_by__email",
)
readonly_fields = (
"evaluation",
"changed_by",
"previous_comment",
"comment",
"previous_scores",
"scores",
"previous_total_score",
"total_score",
"created_at",
)
list_select_related = (
"evaluation",
"evaluation__submission",
"changed_by",
)
date_hierarchy = "created_at"
actions = None

def has_add_permission(self, request):
return False

def has_change_permission(self, request, obj=None):
return False

def has_delete_permission(self, request, obj=None):
return False


class PartnerProgramMaterialInline(admin.StackedInline):
model = PartnerProgramMaterial
extra = 1
Expand Down
14 changes: 14 additions & 0 deletions partner_programs/evaluation_urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Roadmap: DEV-073

from django.urls import path

from partner_programs.evaluation_views import (
EvaluationAmendmentListView,
EvaluationAmendView,
EvaluationDetailView,
EvaluationSubmitView,
)
Expand All @@ -14,4 +18,14 @@
EvaluationSubmitView.as_view(),
name="submit",
),
path(
"<int:evaluation_id>/amend/",
EvaluationAmendView.as_view(),
name="amend",
),
path(
"<int:evaluation_id>/amendments/",
EvaluationAmendmentListView.as_view(),
name="amendment-list",
),
]
Loading
Loading