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
24 changes: 24 additions & 0 deletions docs/modules/project-rates.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ layer. При дальнейших изменениях важно сохран
- выставление и обновление оценок проекта экспертом;
- проверка типов и диапазонов значений;
- ограничение количества экспертов, которые могут оценить один проект;
- запрет создания и изменения оценок после дедлайна оценивания программы;
- режим распределенной оценки, когда эксперт видит и оценивает только
назначенные ему проекты;
- назначение проектов экспертам через Django admin;
Expand Down Expand Up @@ -112,11 +113,32 @@ API проверяет, что:
- все критерии в запросе относятся к одной программе;
- проект привязан к этой программе;
- значение соответствует типу критерия и числовым ограничениям;
- дедлайн `datetime_evaluation_ends` не прошел;
- лимит `max_project_rates` не превышен.

При повторной отправке оценки того же эксперта по тому же критерию значение
обновляется.

Единственный источник дедлайна для legacy-оценок —
`PartnerProgram.datetime_evaluation_ends`. Если значение отсутствует, оценивание
по дедлайну не ограничивается. Запрос разрешен до дедлайна включительно и
отклоняется только при `timezone.now() > datetime_evaluation_ends`. Проверка
относится как к первой оценке, так и к изменению существующей.

Программа для проверки определяется по критериям запроса, поэтому для проекта,
связанного с несколькими программами, применяется дедлайн именно текущей
программы. При закрытом оценивании API возвращает `409 Conflict`:

```json
{
"error": "evaluation_deadline_passed",
"detail": "Срок оценивания завершён."
}
```

При таком ответе оценки не создаются и не изменяются, а уведомление лидеру
проекта не отправляется.

### 4. Распределенная оценка

Если у программы включено `is_distributed_evaluation`, эксперт видит и может
Expand Down Expand Up @@ -150,6 +172,8 @@ Excel-выгрузки оценок программы через `/programs/<id
существующую оценку без создания дубля.
- `max_project_rates` ограничивает число разных экспертов, которые могут
оценить один проект в программе; текущий эксперт может обновить свою оценку.
- `datetime_evaluation_ends` блокирует любые legacy-записи `ProjectScore` после
точного момента дедлайна; отсутствие значения сохраняет оценивание открытым.
- Дефолтный критерий `Комментарий` создается сигналом при создании программы.
- Основная бизнес-логика оценки вынесена в `project_rates/services.py`; views
отвечают за HTTP-контракт и преобразование ошибок в response.
Expand Down
63 changes: 42 additions & 21 deletions project_rates/services.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db import transaction
from django.db.models import Count, Prefetch, Q, QuerySet
from django.utils import timezone

from rest_framework.exceptions import ValidationError

Expand All @@ -20,6 +21,14 @@ def __init__(self, max_project_rates: int):
super().__init__("max project rates reached for this program")


class EvaluationDeadlinePassed(Exception):
code = "evaluation_deadline_passed"
detail = "Срок оценивания завершён."

def __init__(self):
super().__init__(self.detail)


def get_rate_program(program_id: int) -> PartnerProgram:
return PartnerProgram.objects.get(pk=program_id)

Expand Down Expand Up @@ -73,8 +82,7 @@ def get_projects_for_rate_queryset(
)

return (
projects_qs
.annotate(
projects_qs.annotate(
rated_count=Count(
"scores__user",
filter=Q(scores__criteria__partner_program=program),
Expand All @@ -100,18 +108,20 @@ def submit_project_scores(*, user, project_id: int, data) -> None:
)
serializer.is_valid(raise_exception=True)

scores_qs = ProjectScore.objects.filter(
project_id=project_id,
criteria__partner_program=program,
)
user_has_scores = scores_qs.filter(user_id=user.id).exists()
with transaction.atomic():
_ensure_evaluation_deadline_open(program)

if program.max_project_rates:
distinct_raters = scores_qs.values("user_id").distinct().count()
if not user_has_scores and distinct_raters >= program.max_project_rates:
raise MaxProjectRatesReached(program.max_project_rates)
scores_qs = ProjectScore.objects.filter(
project_id=project_id,
criteria__partner_program=program,
)
user_has_scores = scores_qs.filter(user_id=user.id).exists()

if program.max_project_rates:
distinct_raters = scores_qs.values("user_id").distinct().count()
if not user_has_scores and distinct_raters >= program.max_project_rates:
raise MaxProjectRatesReached(program.max_project_rates)

with transaction.atomic():
ProjectScore.objects.bulk_create(
[ProjectScore(**item) for item in serializer.validated_data],
update_conflicts=True,
Expand All @@ -123,16 +133,24 @@ def submit_project_scores(*, user, project_id: int, data) -> None:
_send_project_rated_email(project=project, program=program)


def _prepare_project_score_data(*, user, project_id: int, data) -> tuple[list, list, PartnerProgram]:
def _ensure_evaluation_deadline_open(program: PartnerProgram) -> None:
deadline = program.datetime_evaluation_ends
if deadline is not None and timezone.now() > deadline:
raise EvaluationDeadlinePassed


def _prepare_project_score_data(
*, user, project_id: int, data
) -> tuple[list, list, PartnerProgram]:
rating_data = [dict(criterion) for criterion in data]
criteria_ids = [criterion["criterion_id"] for criterion in rating_data]

criteria_qs = Criteria.objects.filter(id__in=criteria_ids).select_related(
"partner_program"
)
partner_program_ids = (
criteria_qs.values_list("partner_program_id", flat=True).distinct()
)
partner_program_ids = criteria_qs.values_list(
"partner_program_id", flat=True
).distinct()
if not criteria_qs.exists():
raise ValueError("Criteria not found")
if partner_program_ids.count() != 1:
Expand All @@ -152,11 +170,14 @@ def _prepare_project_score_data(*, user, project_id: int, data) -> tuple[list, l
).exists():
raise ValueError("Project is not linked to the program")

if program.is_distributed_evaluation and not ProjectExpertAssignment.objects.filter(
partner_program=program,
project_id=project_id,
expert__user_id=user.id,
).exists():
if (
program.is_distributed_evaluation
and not ProjectExpertAssignment.objects.filter(
partner_program=program,
project_id=project_id,
expert__user_id=user.id,
).exists()
):
raise ValueError("you are not assigned to rate this project")

return rating_data, criteria_ids, program
Expand Down
Loading
Loading