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
18 changes: 18 additions & 0 deletions docs/modules/program-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,21 @@ GET `management/projects/export/` использует тот же ManagementPro
`management/projects/:link_id/evaluation/` дополнительно возвращает `criteria` и `result` из канонического расчета Results, ограниченного этой связью программы и проекта. Старые поля `experts`/`can_rate` сохранены. В просмотре организатора используются общая матрица и мобильные карточки Results; отдельная кнопка личного оценивания убрана. Средние и итог учитывают только полные валидные оценки, без изменения правил рейтинга.

Сданный проект с `draft=True` остается недоступен для назначения экспертов: в таблице показывается «Черновик заявки», а плашка выбора поясняет необходимость завершить оформление заявки. Это не то же, что публичность проекта (`is_public`). Автоматическая публикация и изменение пользовательских данных не выполняются. Фильтры проектов применяются вместе из модального окна; выгрузка и выбор всех используют примененные фильтры.

## Возврат в черновик

`POST /programs/{id}/management/unpublish/` принимает обязательный
`expected_updated_at` (ISO datetime), возвращает обычный management-ответ
с `draft=true`. Менеджер этой программы либо существующий staff/superuser scope
проверяется сервером. Право создания программы не требуется. Без авторизации — 401;
для чужой опубликованной программы — 403, скрытого черновика или отсутствующей — 404.
Неверные/лишние поля — 400, устаревшая версия — 409 `stale_settings` без записи.
Повтор уже выполненного действия — 200 без изменения версии.

Общий сервис API и подтверждения Django admin блокирует строку программы и меняет
только `draft` и `datetime_updated`. Участие, ответы анкет, заявки, решения,
оценки, материалы, сроки и опубликованная версия анкеты сохраняются. Публичные
маршруты используют прежнее сокрытие черновиков. Повторная публикация идет через
существующую подготовку к запуску со всеми проверками готовности; сроки не продлеваются.
Миграций нет; API выкладывается до Angular. AC DRAFT-1…DRAFT-7 описаны в
Angular `docs/modules/program-management.md`.
5 changes: 4 additions & 1 deletion partner_programs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from partner_programs.services.case_fields import case_field_has_values
from partner_programs.admin_registration import ProgramRegistrationAdminMixin
from partner_programs.admin_publication import ProgramPublicationAdminMixin
from partner_programs.program_dates import (
DATE_FIELDS,
program_date_errors,
Expand All @@ -51,7 +52,9 @@ class PartnerProgramFieldInline(admin.TabularInline):


@admin.register(PartnerProgram)
class PartnerProgramAdmin(ProgramRegistrationAdminMixin, admin.ModelAdmin):
class PartnerProgramAdmin(
ProgramPublicationAdminMixin, ProgramRegistrationAdminMixin, admin.ModelAdmin
):
class PartnerProgramAdminForm(forms.ModelForm):
confirm_reopen = forms.BooleanField(
required=False,
Expand Down
66 changes: 66 additions & 0 deletions partner_programs/admin_publication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Возврат программы в черновик через общую с кабинетом бизнес-логику."""

from django import forms
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from rest_framework.exceptions import APIException

from partner_programs.admin_registration import error_text
from partner_programs.services.program_launch import return_to_draft


class ReturnToDraftForm(forms.Form):
expected_updated_at = forms.DateTimeField(widget=forms.HiddenInput)


class ProgramPublicationAdminMixin:
def get_urls(self):
return [
path(
"<int:object_id>/unpublish/",
self.admin_site.admin_view(self.return_to_draft_view),
name="partner_programs_partnerprogram_unpublish",
)
] + super().get_urls()

def return_to_draft_view(self, request, object_id):
if request.method not in ("GET", "POST"):
return HttpResponseNotAllowed(["GET", "POST"])
program = self.get_object(request, str(object_id))
if program is None:
raise Http404
if not self.has_change_permission(request, program):
raise PermissionDenied
change_url = reverse(
"admin:partner_programs_partnerprogram_change", args=[program.pk]
)
form = ReturnToDraftForm(
request.POST if request.method == "POST" else None,
initial={"expected_updated_at": program.datetime_updated.isoformat()},
)
status = 200
if request.method == "POST" and form.is_valid():
try:
program = return_to_draft(
program.pk, request.user, form.cleaned_data["expected_updated_at"]
)
except APIException as error:
form.add_error(None, error_text(error.detail))
status = error.status_code
else:
self.log_change(request, program, "Программа возвращена в черновик.")
self.message_user(request, "Программа возвращена в черновик.")
return HttpResponseRedirect(change_url)
context = {
**self.admin_site.each_context(request),
"title": "Вернуть программу в черновик?",
"opts": self.model._meta,
"original": program,
"form": form,
"change_url": change_url,
}
return TemplateResponse(
request, "partner_programs/admin/unpublish.html", context, status=status
)
15 changes: 15 additions & 0 deletions partner_programs/program_launch_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from partner_programs.permissions import can_create_program, require_create_program
from partner_programs.serializers.management import StrictFieldsMixin
from partner_programs.services import program_launch as service
from partner_programs.services.management import management_data


class CreateProgramInput(StrictFieldsMixin, serializers.Serializer):
Expand Down Expand Up @@ -73,3 +74,17 @@ def post(self, request, pk):
return Response(
service.publish_program(pk, request.user, data.validated_data["revision"])
)


class ReturnToDraftInput(StrictFieldsMixin, serializers.Serializer):
expected_updated_at = serializers.DateTimeField()


class ProgramReturnToDraftView(ManagementBase):
def post(self, request, pk):
data = ReturnToDraftInput(data=request.data)
data.is_valid(raise_exception=True)
program = service.return_to_draft(
pk, request.user, data.validated_data["expected_updated_at"]
)
return Response(management_data(program))
17 changes: 17 additions & 0 deletions partner_programs/services/program_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,23 @@ def readiness(pk, user):
return readiness_data(managed_program(pk, user))


@transaction.atomic
def return_to_draft(pk, user, expected_updated_at):
"""Скрыть программу, сохранив анкету, участие, решения и оценки."""
program = managed_program(pk, user, lock=True)
if program.draft:
# Повтор после потерянного ответа не меняет версию программы.
return program
if program.datetime_updated != expected_updated_at:
raise WorkspaceConflict(
"stale_settings",
"Программа изменена другим организатором. Обновите данные перед повтором.",
)
program.draft = True
program.save(update_fields=["draft", "datetime_updated"])
return program


@transaction.atomic
def publish_program(pk, user, revision):
program = managed_program(pk, user, lock=True)
Expand Down
Loading
Loading