From c0a1935f8d2fa79befedcaa658f416949c0ffc0f Mon Sep 17 00:00:00 2001 From: Fedor Date: Tue, 3 Nov 2026 04:06:10 +0300 Subject: [PATCH 1/2] test: prepare isolated PR517 acceptance fixtures on dev --- .github/workflows/dev-ci.yml | 15 +- .../tests/test_pr517_closeout_fixtures.py | 105 ++++++ scripts/prepare_pr517_closeout.py | 306 ++++++++++++++++++ 3 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 partner_programs/tests/test_pr517_closeout_fixtures.py create mode 100644 scripts/prepare_pr517_closeout.py diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index a1e85ea0..935752dd 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -13,7 +13,7 @@ on: description: "DEV operation" type: choice default: deploy - options: [deploy, verify_solution_rollback, prepare_solution_browser, finalize_solution_browser, verify_program_publication, grant_program_creator_11, dependency_backup, dependency_verify] + options: [deploy, verify_solution_rollback, prepare_solution_browser, finalize_solution_browser, verify_program_publication, grant_program_creator_11, dependency_backup, dependency_verify, prepare_pr517_closeout] dependency_backup_key: description: "Ключ резервной копии для проверки сохранности данных dev" type: string @@ -26,6 +26,10 @@ on: description: "Existing user who owns the new QA programs" default: "28" type: string + apply_fixture: + description: "Apply the bounded PR517 fixture plan; false only previews it" + default: false + type: boolean permissions: contents: read @@ -400,6 +404,7 @@ jobs: VERIFY_OPERATION: ${{ inputs.operation }} VERIFY_FIXTURE_KEY: ${{ inputs.fixture_key }} VERIFY_MANAGER_ID: ${{ inputs.manager_user_id }} + VERIFY_FIXTURE_APPLY: ${{ inputs.apply_fixture && 'true' || 'false' }} run: | node <<'NODE' const fs = require('node:fs'); @@ -408,10 +413,12 @@ jobs: const operation = process.env.VERIFY_OPERATION; const key = process.env.VERIFY_FIXTURE_KEY; const manager = process.env.VERIFY_MANAGER_ID; - const phases = { verify_solution_rollback: 'verify_rollback', prepare_solution_browser: 'prepare_browser', finalize_solution_browser: 'finalize_browser', verify_program_publication: 'verify_publication', grant_program_creator_11: 'grant_creator_11' }; + const applyFixture = process.env.VERIFY_FIXTURE_APPLY; + const phases = { verify_solution_rollback: 'verify_rollback', prepare_solution_browser: 'prepare_browser', finalize_solution_browser: 'finalize_browser', verify_program_publication: 'verify_publication', grant_program_creator_11: 'grant_creator_11', prepare_pr517_closeout: 'prepare_pr517_closeout' }; if (!/^[0-9a-f]{40}$/.test(sha) || !phases[operation] || !/^[a-z0-9-]{8,50}$/.test(key) || !/^[1-9][0-9]{0,8}$/.test(manager)) throw new Error('Invalid acceptance arguments'); + if (!['true', 'false'].includes(applyFixture)) throw new Error('Invalid fixture apply flag'); const phase = phases[operation]; - const file = phase === 'grant_creator_11' ? 'scripts/grant_dev_program_creation_20261027.py' : phase === 'verify_publication' ? 'scripts/verify_program_publication_dev.py' : phase === 'verify_rollback' ? 'scripts/verify_program_solutions_dev.py' : 'scripts/prepare_program_solutions_browser.py'; + const file = phase === 'prepare_pr517_closeout' ? 'scripts/prepare_pr517_closeout.py' : phase === 'grant_creator_11' ? 'scripts/grant_dev_program_creation_20261027.py' : phase === 'verify_publication' ? 'scripts/verify_program_publication_dev.py' : phase === 'verify_rollback' ? 'scripts/verify_program_solutions_dev.py' : 'scripts/prepare_program_solutions_browser.py'; const source = fs.readFileSync(file, 'utf8'); const script = [ 'set +x', @@ -428,7 +435,7 @@ jobs: 'test "$(docker inspect -f \'{{.Image}}\' "$web")" = "$(docker inspect -f \'{{.Image}}\' "$worker")"', 'echo "VERIFIED_DEV_RUNTIME_REF=' + sha + '"', 'docker exec "$web" python manage.py migrate --check', - 'docker exec -e VERIFY_EXPECTED_API_SHA=' + sha + ' -e VERIFY_EXPECTED_LEGACY_IDS=1,2,3,4,5,6,7,8,9,38,39 -e VERIFY_PHASE=' + phase + ' -e VERIFY_FIXTURE_KEY=' + key + ' -e VERIFY_MANAGER_ID=' + manager + ' -i "$web" python - <<\'SOLUTION_ACCEPTANCE_PY\'', + 'docker exec -e VERIFY_EXPECTED_API_SHA=' + sha + ' -e VERIFY_EXPECTED_LEGACY_IDS=1,2,3,4,5,6,7,8,9,38,39 -e VERIFY_PHASE=' + phase + ' -e VERIFY_FIXTURE_KEY=' + key + ' -e VERIFY_MANAGER_ID=' + manager + ' -e VERIFY_FIXTURE_APPLY=' + applyFixture + ' -i "$web" python - <<\'SOLUTION_ACCEPTANCE_PY\'', source, 'SOLUTION_ACCEPTANCE_PY' ].join('\n') + '\n'; diff --git a/partner_programs/tests/test_pr517_closeout_fixtures.py b/partner_programs/tests/test_pr517_closeout_fixtures.py new file mode 100644 index 00000000..3b2931cf --- /dev/null +++ b/partner_programs/tests/test_pr517_closeout_fixtures.py @@ -0,0 +1,105 @@ +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.core import mail +from django.test import TestCase + +from partner_programs.certificate_models import ProgramCertificateAsset +from partner_programs.models import PartnerProgram, PartnerProgramProject +from partner_programs.solution_api import create_solution +from partner_programs.solution_models import ProgramSolution +from partner_programs.tests.helpers import ( + create_partner_program, + create_project, + create_user, +) +from project_rates.tests.helpers import create_rate_expert +from scripts.prepare_pr517_closeout import prepare + + +class PR517CloseoutFixtureTests(TestCase): + def setUp(self): + self.manager = create_rate_expert() + self.expert = create_rate_expert() + self.captains = [ + create_user(is_active=True), + self.expert, + create_user(is_active=True), + ] + self.participant = create_user(is_active=True) + self.previous_project = create_project(name="Существующий проект") + self.previous_program = create_partner_program() + ProgramCertificateAsset.objects.create( + program=self.previous_program, + kind="signature", + name="existing.png", + content=b"existing-private-binary", + width=1, + height=1, + ) + self.args = ( + "test-fixture-20261103", + self.manager.pk, + [user.pk for user in self.captains], + [self.manager.pk, self.expert.pk], + self.participant.pk, + ) + + def test_preview_is_readonly_and_requires_existing_accounts(self): + counts = (get_user_model().objects.count(), PartnerProgram.objects.count()) + result = prepare(*self.args) + self.assertFalse(result["applied"]) + self.assertEqual(result["solution_count"], 21) + self.assertEqual( + (get_user_model().objects.count(), PartnerProgram.objects.count()), counts + ) + with self.assertRaises(ValueError): + prepare(self.args[0], 90000000, *self.args[2:], apply=True) + + def test_apply_is_private_bounded_and_preserves_existing_rows(self): + result = prepare(*self.args, apply=True) + self.assertEqual(result["work_counts"], {"legacy_project": 4, "solution": 21}) + self.assertEqual(PartnerProgram.objects.filter(draft=True).count(), 2) + programs = [row["id"] for row in result["programs"]] + self.assertFalse( + PartnerProgram.objects.filter( + pk__in=programs, solution_creation_enabled=True + ).exists() + ) + self.assertEqual(ProgramSolution.objects.filter(status="submitted").count(), 3) + self.assertFalse(ProgramSolution.objects.exclude(published_at=None).exists()) + self.assertEqual( + PartnerProgramProject.objects.filter(partner_program_id__in=programs).count(), + 4, + ) + self.assertTrue( + all( + not user.has_usable_password() + for user in get_user_model().objects.filter( + pk__in=result["synthetic_user_ids"] + ) + ) + ) + self.previous_project.refresh_from_db() + self.assertEqual(self.previous_project.name, "Существующий проект") + self.assertEqual(getattr(mail, "outbox", []), []) + with self.assertRaises(ValueError): + prepare(*self.args, apply=True) + + def test_existing_data_change_rolls_back_the_entire_fixture(self): + counts = (get_user_model().objects.count(), PartnerProgram.objects.count()) + + def corrupt(*args, **kwargs): + type(self.previous_project).objects.filter( + pk=self.previous_project.pk + ).update(name="Ошибка") + return create_solution(*args, **kwargs) + + with patch("partner_programs.solution_api.create_solution", side_effect=corrupt): + with self.assertRaisesRegex(AssertionError, "Existing data changed"): + prepare(*self.args, apply=True) + self.previous_project.refresh_from_db() + self.assertEqual(self.previous_project.name, "Существующий проект") + self.assertEqual( + (get_user_model().objects.count(), PartnerProgram.objects.count()), counts + ) diff --git a/scripts/prepare_pr517_closeout.py b/scripts/prepare_pr517_closeout.py new file mode 100644 index 00000000..b1f92c6a --- /dev/null +++ b/scripts/prepare_pr517_closeout.py @@ -0,0 +1,306 @@ +"""Bounded DEV-only fixtures for PR517 browser acceptance; existing rows are immutable.""" + +import hashlib +import io +import json +import os +import re +import sys +from pathlib import Path + + +def _stable(value): + if isinstance(value, (memoryview, bytes, bytearray)): + return {"binary_hex": bytes(value).hex()} + return str(value) + + +def _digest(model, ids): + digest = hashlib.sha256() + for row in model._base_manager.filter(pk__in=ids).order_by("pk").values().iterator(): + digest.update(json.dumps(row, sort_keys=True, default=_stable).encode()) + return digest.hexdigest() + + +def prepare( + key, manager_id, captain_ids, expert_user_ids, participant_id, *, apply=False +): + from django.apps import apps + from django.contrib.auth import get_user_model + from django.core.management import call_command + from django.db import transaction + from django.utils import timezone + + from partner_programs.constants import PROGRAM_CASE_FIELD_NAME + from partner_programs.models import ( + PartnerProgram, + PartnerProgramField, + PartnerProgramFieldValue, + PartnerProgramProject, + PartnerProgramUserProfile, + ) + from partner_programs.solution_answers import update_solution + from partner_programs.solution_api import create_solution + from partner_programs.solution_schema import form_data + from partner_programs.solution_submission import submit_solution + from project_rates.models import Criteria + from projects.models import Project + from users.models import Expert + + if not re.fullmatch(r"[a-z0-9-]{8,35}", key): + raise ValueError("Invalid PR517 fixture key") + if len(captain_ids) != 3 or len(set(captain_ids)) != 3: + raise ValueError("Exactly three existing test captains are required") + prefix = "pr517-closeout-" + key + User = get_user_model() + required = {manager_id, participant_id, *captain_ids, *expert_user_ids} + users = User.objects.in_bulk(required) + if set(users) != required or any(not user.is_active for user in users.values()): + raise ValueError("An existing test account is missing or inactive") + experts = list(Expert.objects.filter(user_id__in=expert_user_ids)) + if {row.user_id for row in experts} != set(expert_user_ids): + raise ValueError("Existing test experts are required") + if PartnerProgram.objects.filter(tag__startswith=prefix).exists(): + raise ValueError("Fixture already exists; preparation must not be replayed") + if User.objects.filter(email__startswith=prefix + "-").exists(): + raise ValueError("Fixture users already exist") + plan = dict( + prefix=prefix, + new_programs=2, + new_users=18, + solution_count=21, + legacy_project_count=4, + mail_sent=False, + published=False, + manager_id=manager_id, + captain_ids=list(captain_ids), + expert_user_ids=list(expert_user_ids), + participant_id=participant_id, + applied=False, + ) + if not apply: + return plan + + with transaction.atomic(): + User.objects.select_for_update().get(pk=manager_id) + if PartnerProgram.objects.filter(tag__startswith=prefix).exists(): + raise ValueError("Fixture already exists") + baseline = [] + for model in apps.get_models(include_auto_created=True): + if ( + model._meta.app_label + not in {"partner_programs", "projects", "project_rates"} + or model._meta.proxy + ): + continue + ids = list(model._base_manager.order_by("pk").values_list("pk", flat=True)) + baseline.append((model, ids, _digest(model, ids))) + synthetic = [ + User.objects.create_user( + email=f"{prefix}-{index:02d}@example.invalid", + password=None, + first_name="Приемка", + last_name=f"PR517-{index:02d}", + birthday="1990-01-01", + is_active=True, + ) + for index in range(1, 19) + ] + assert all(not user.has_usable_password() for user in synthetic) + captains = [users[pk] for pk in captain_ids] + synthetic + now = timezone.now() + programs = [] + work = {} + for mode, suffix in [("legacy_project", "project"), ("solution", "solution")]: + program = PartnerProgram.objects.create( + name=f"PR517 {key} — {suffix}", + tag=prefix + "-" + suffix, + description="Изолированная приемка PR517. Тестовые работы и настройки.", + city="Москва", + draft=True, + projects_availability="all_users", + data_schema={}, + solution_storage_mode=mode, + solution_template="case_championship", + solution_creation_enabled=mode == "solution", + is_competitive=True, + is_distributed_evaluation=True, + required_evaluators=1, + max_project_rates=1, + legacy_team_min_size=1, + legacy_team_max_size=4, + datetime_started=now - timezone.timedelta(days=1), + datetime_registration_ends=now + timezone.timedelta(days=7), + datetime_project_submission_ends=now + timezone.timedelta(days=7), + datetime_evaluation_ends=now + timezone.timedelta(days=8), + datetime_finished=now + timezone.timedelta(days=9), + ) + program.managers.add(users[manager_id], users[captain_ids[0]]) + active_captains = captains[:4] if mode == "legacy_project" else captains + registered = {user.pk: user for user in active_captains} + registered.update( + {pk: users[pk] for pk in (manager_id, participant_id, *expert_user_ids)} + ) + for user in registered.values(): + PartnerProgramUserProfile.objects.create( + partner_program=program, + user=user, + partner_program_data={"qa_marker": prefix}, + ) + for expert in experts: + expert.programs.add(program) + fields = [] + definitions = [ + (PROGRAM_CASE_FIELD_NAME, "Кейс", "select", "case", True), + ("pr517_description", "Описание решения", "textarea", "other", True), + ] + if mode == "solution": + definitions.extend( + [ + ("pr517_zero", "Число для проверки", "number", "other", False), + ("pr517_no", "Дополнительная опция", "checkbox", "other", False), + ] + ) + for position, (name, label, kind, purpose, required_field) in enumerate( + definitions + ): + fields.append( + PartnerProgramField.objects.create( + partner_program=program, + name=name, + label=label, + field_type=kind, + purpose=purpose, + is_required=required_field, + show_filter=purpose == "case", + position=position, + options="Город|Образование" if purpose == "case" else "", + solution_options=( + ["Город", "Образование"] if purpose == "case" else None + ), + ) + ) + for kind, name in [("int", "PR517 Баллы"), ("bool", "PR517 Да/Нет")]: + Criteria.objects.create( + partner_program=program, + name=name, + type=kind, + min_value=0 if kind == "int" else None, + max_value=10 if kind == "int" else None, + ) + ids = [] + for index, captain in enumerate(active_captains): + case = "Образование" if index == len(active_captains) - 1 else "Город" + name = f"PR517 {key} {suffix} {index + 1:02d}" + if mode == "legacy_project": + project = Project.objects.create( + leader=captain, + name=name, + description="PR517: тестовое описание работы.", + draft=False, + is_public=False, + ) + link = PartnerProgramProject.objects.create( + partner_program=program, project=project + ) + for field, value in zip( + fields, [case, "PR517: материалы тестовой работы."] + ): + PartnerProgramFieldValue.objects.create( + field=field, program_project=link, value_text=value + ) + ids.append( + { + "project_id": project.pk, + "program_link_id": link.pk, + "captain_id": captain.pk, + } + ) + else: + solution, created = create_solution( + program.pk, captain, {"name": name} + ) + assert created + revision = form_data(program)["revision"] + update_solution( + program_id=program.pk, + solution_id=solution.pk, + actor=captain, + expected_version=solution.version, + expected_form_revision=revision, + answers=[ + {"field_id": field.pk, "value": value} + for field, value in zip( + fields, + [case, "PR517: материалы тестовой работы.", 0, False], + ) + ], + ) + solution.refresh_from_db() + if index in (1, 3, 4): + submit_solution( + program_id=program.pk, + solution_id=solution.pk, + actor=captain, + expected_version=solution.version, + expected_form_revision=revision, + ) + ids.append({"solution_id": solution.pk, "captain_id": captain.pk}) + if mode == "solution": + call_command( + "set_solution_creation", + program_id=program.pk, + disable=True, + apply=True, + stdout=io.StringIO(), + ) + programs.append( + { + "id": program.pk, + "mode": mode, + "draft": True, + "creation_enabled": False, + "work": ids, + } + ) + work[mode] = len(ids) + for model, ids, before in baseline: + if _digest(model, ids) != before: + raise AssertionError("Existing data changed: " + model._meta.label) + return { + **plan, + "applied": True, + "programs": programs, + "synthetic_user_ids": [user.pk for user in synthetic], + "old_models_unchanged": len(baseline), + "work_counts": work, + } + + +def main(): + sys.path.insert(0, os.getcwd()) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "procollab.settings") + import django + + django.setup() + assert re.fullmatch(r"[0-9a-f]{40}", os.environ["VERIFY_EXPECTED_API_SHA"]) + assert os.environ["VERIFY_PHASE"] == "prepare_pr517_closeout" + apply = os.environ["VERIFY_FIXTURE_APPLY"] + assert apply in ("true", "false") + result = prepare( + os.environ["VERIFY_FIXTURE_KEY"], + int(os.environ["VERIFY_MANAGER_ID"]), + (11, 59, 84), + (28, 59), + 341, + apply=apply == "true", + ) + if result["applied"]: + receipt = Path("/procollab/log") / (result["prefix"] + ".json") + receipt.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8") + receipt.chmod(0o600) + sys.stdout.write("PR517_CLOSEOUT=" + json.dumps(result, ensure_ascii=False) + "\n") + + +if __name__ == "__main__": + main() From 1af89296c55e73cf6ba5113a25f15704867701f7 Mon Sep 17 00:00:00 2001 From: Fedor Date: Tue, 3 Nov 2026 04:32:59 +0300 Subject: [PATCH 2/2] test: isolate remaining hackathon and noncompetitive acceptance --- .../tests/test_pr517_closeout_fixtures.py | 17 ++++-- scripts/prepare_pr517_closeout.py | 56 +++++++++++++------ 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/partner_programs/tests/test_pr517_closeout_fixtures.py b/partner_programs/tests/test_pr517_closeout_fixtures.py index 3b2931cf..bf2db2fd 100644 --- a/partner_programs/tests/test_pr517_closeout_fixtures.py +++ b/partner_programs/tests/test_pr517_closeout_fixtures.py @@ -49,7 +49,7 @@ def test_preview_is_readonly_and_requires_existing_accounts(self): counts = (get_user_model().objects.count(), PartnerProgram.objects.count()) result = prepare(*self.args) self.assertFalse(result["applied"]) - self.assertEqual(result["solution_count"], 21) + self.assertEqual(result["solution_count"], 23) self.assertEqual( (get_user_model().objects.count(), PartnerProgram.objects.count()), counts ) @@ -58,15 +58,24 @@ def test_preview_is_readonly_and_requires_existing_accounts(self): def test_apply_is_private_bounded_and_preserves_existing_rows(self): result = prepare(*self.args, apply=True) - self.assertEqual(result["work_counts"], {"legacy_project": 4, "solution": 21}) - self.assertEqual(PartnerProgram.objects.filter(draft=True).count(), 2) + self.assertEqual( + result["work_counts"], + {"legacy_project": 4, "solution": 21, "hackathon": 1, "noncompetitive": 1}, + ) + self.assertEqual(PartnerProgram.objects.filter(draft=True).count(), 4) programs = [row["id"] for row in result["programs"]] self.assertFalse( PartnerProgram.objects.filter( pk__in=programs, solution_creation_enabled=True ).exists() ) - self.assertEqual(ProgramSolution.objects.filter(status="submitted").count(), 3) + self.assertEqual(ProgramSolution.objects.filter(status="submitted").count(), 4) + noncompetitive = PartnerProgram.objects.get( + pk=next(row["id"] for row in result["programs"] if not row["competitive"]) + ) + self.assertFalse(noncompetitive.criterias.exclude(type="str").exists()) + self.assertFalse(noncompetitive.is_distributed_evaluation) + self.assertIsNone(noncompetitive.required_evaluators) self.assertFalse(ProgramSolution.objects.exclude(published_at=None).exists()) self.assertEqual( PartnerProgramProject.objects.filter(partner_program_id__in=programs).count(), diff --git a/scripts/prepare_pr517_closeout.py b/scripts/prepare_pr517_closeout.py index b1f92c6a..b7722ffe 100644 --- a/scripts/prepare_pr517_closeout.py +++ b/scripts/prepare_pr517_closeout.py @@ -66,9 +66,9 @@ def prepare( raise ValueError("Fixture users already exist") plan = dict( prefix=prefix, - new_programs=2, + new_programs=4, new_users=18, - solution_count=21, + solution_count=23, legacy_project_count=4, mail_sent=False, published=False, @@ -111,7 +111,13 @@ def prepare( now = timezone.now() programs = [] work = {} - for mode, suffix in [("legacy_project", "project"), ("solution", "solution")]: + scenarios = [ + ("legacy_project", "project", "case_championship", True, True), + ("solution", "solution", "case_championship", True, True), + ("solution", "hackathon", "hackathon", True, False), + ("solution", "noncompetitive", "hackathon", False, False), + ] + for mode, suffix, template, competitive, distributed in scenarios: program = PartnerProgram.objects.create( name=f"PR517 {key} — {suffix}", tag=prefix + "-" + suffix, @@ -121,12 +127,12 @@ def prepare( projects_availability="all_users", data_schema={}, solution_storage_mode=mode, - solution_template="case_championship", + solution_template=template, solution_creation_enabled=mode == "solution", - is_competitive=True, - is_distributed_evaluation=True, - required_evaluators=1, - max_project_rates=1, + is_competitive=competitive, + is_distributed_evaluation=distributed, + required_evaluators=1 if competitive else None, + max_project_rates=1 if competitive else None, legacy_team_min_size=1, legacy_team_max_size=4, datetime_started=now - timezone.timedelta(days=1), @@ -136,7 +142,11 @@ def prepare( datetime_finished=now + timezone.timedelta(days=9), ) program.managers.add(users[manager_id], users[captain_ids[0]]) - active_captains = captains[:4] if mode == "legacy_project" else captains + active_captains = ( + captains[:4] + if mode == "legacy_project" + else captains if suffix == "solution" else captains[:1] + ) registered = {user.pk: user for user in active_captains} registered.update( {pk: users[pk] for pk in (manager_id, participant_id, *expert_user_ids)} @@ -161,6 +171,10 @@ def prepare( ("pr517_no", "Дополнительная опция", "checkbox", "other", False), ] ) + if template == "hackathon": + definitions.append( + ("repository", "Ссылка на репозиторий", "url", "repository", True) + ) for position, (name, label, kind, purpose, required_field) in enumerate( definitions ): @@ -180,7 +194,8 @@ def prepare( ), ) ) - for kind, name in [("int", "PR517 Баллы"), ("bool", "PR517 Да/Нет")]: + criteria = [("int", "PR517 Баллы"), ("bool", "PR517 Да/Нет")] + for kind, name in criteria if competitive else []: Criteria.objects.create( partner_program=program, name=name, @@ -222,6 +237,13 @@ def prepare( ) assert created revision = form_data(program)["revision"] + values = { + PROGRAM_CASE_FIELD_NAME: case, + "pr517_description": "PR517: материалы тестовой работы.", + "pr517_zero": 0, + "pr517_no": False, + "repository": "https://example.invalid/pr517", + } update_solution( program_id=program.pk, solution_id=solution.pk, @@ -229,15 +251,12 @@ def prepare( expected_version=solution.version, expected_form_revision=revision, answers=[ - {"field_id": field.pk, "value": value} - for field, value in zip( - fields, - [case, "PR517: материалы тестовой работы.", 0, False], - ) + {"field_id": field.pk, "value": values[field.name]} + for field in fields ], ) solution.refresh_from_db() - if index in (1, 3, 4): + if competitive and (index in (1, 3, 4) or suffix == "hackathon"): submit_solution( program_id=program.pk, solution_id=solution.pk, @@ -258,12 +277,15 @@ def prepare( { "id": program.pk, "mode": mode, + "scenario": suffix, + "template": template, + "competitive": competitive, "draft": True, "creation_enabled": False, "work": ids, } ) - work[mode] = len(ids) + work[mode if mode == "legacy_project" else suffix] = len(ids) for model, ids, before in baseline: if _digest(model, ids) != before: raise AssertionError("Existing data changed: " + model._meta.label)