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
101 changes: 101 additions & 0 deletions project_rates/tests/test_case_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,107 @@


class CaseDistributionTests(TestCase):
def test_seed_varies_equal_load_choices_without_changing_existing_assignments(self):
self.write(self.projects[:1], [self.universal])
extra = [create_rate_expert(program=self.program) for _ in range(3)]
ids = [u.expert.pk for u in [self.universal, *extra]]
variants = set()
for seed in range(8):
response = self.plan(
expert_ids=ids,
project_ids=[self.projects[0].pk],
case_pools=[{"case_name": "Сбер", "expert_ids": ids}],
seed=seed,
)
self.assertEqual(response.status_code, 200, response.data)
rows = response.data["assignments"]
self.assertEqual(len(rows), 1) # one existing + one proposed = two
self.assertNotEqual(rows[0]["expert_id"], self.universal.expert.pk)
variants.add(rows[0]["expert_id"])
self.assertGreater(len(variants), 1)
self.assertEqual(ProjectExpertAssignment.objects.count(), 1)

def test_override_is_preview_only_then_applies_with_the_signed_plan(self):
ids = [self.sber.expert.pk, self.universal.expert.pk]
response = self.plan(
assignment_overrides=[{"project_id": self.projects[0].pk, "expert_ids": ids}]
)
self.assertEqual(response.status_code, 200, response.data)
self.assertEqual(
{
r["expert_id"]
for r in response.data["assignments"]
if r["project_id"] == self.projects[0].pk
},
set(ids),
)
self.assertFalse(ProjectExpertAssignment.objects.exists())
applied = self.client.post(
self.root + "expert-allocation/apply/",
{"plan_token": response.data["plan_token"]},
format="json",
)
self.assertEqual(applied.status_code, 200, applied.data)
self.assertEqual(ProjectExpertAssignment.objects.count(), 8)

def test_override_rejects_wrong_pool_own_team_duplicates_and_foreign_project(self):
own = self.project("Сбер", leader=self.universal)
foreign = create_rate_project()
rows = [
[{"project_id": self.projects[0].pk, "expert_ids": [self.bank.expert.pk]}],
[{"project_id": own.pk, "expert_ids": [self.universal.expert.pk]}],
[{"project_id": foreign.pk, "expert_ids": []}],
[
{
"project_id": self.projects[0].pk,
"expert_ids": [self.sber.expert.pk] * 2,
}
],
[{"project_id": self.projects[0].pk, "expert_ids": []}] * 2,
]
for overrides in rows:
with self.subTest(overrides=overrides):
self.assertEqual(
self.plan(assignment_overrides=overrides).status_code, 400
)
self.assertFalse(ProjectExpertAssignment.objects.exists())

def test_override_cannot_exceed_free_slots_or_repeat_an_existing_expert(self):
self.write(self.projects[:1], [self.universal])
for ids in [
[self.universal.expert.pk],
[self.sber.expert.pk, self.universal.expert.pk],
]:
self.assertEqual(
self.plan(
assignment_overrides=[
{"project_id": self.projects[0].pk, "expert_ids": ids}
]
).status_code,
400,
)
self.assertEqual(ProjectExpertAssignment.objects.count(), 1)

def test_underfilled_override_blocks_apply_and_stale_override_is_rejected(self):
old = self.state()["revision"]
response = self.plan(
assignment_overrides=[{"project_id": self.projects[0].pk, "expert_ids": []}]
)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.data["shortages"])
self.assertEqual(
self.client.post(
self.root + "expert-allocation/apply/",
{"plan_token": response.data["plan_token"]},
format="json",
).status_code,
409,
)
self.write(self.projects[:1], [self.sber])
self.assertEqual(
self.plan(revision=old, assignment_overrides=[]).status_code, 409
)

def test_inactive_existing_expert_can_be_kept_but_not_newly_assigned(self):
self.assertEqual(self.write(self.projects[:1], [self.universal]).status_code, 200)
type(self.universal).objects.filter(pk=self.universal.pk).update(is_active=False)
Expand Down
32 changes: 28 additions & 4 deletions project_rates/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,14 @@ def allocation(program, data):
).values_list("project_id", "user_id"):
teams[user_id].add(project_id)
own = {e.pk: teams[e.user_id] for e in experts}
overrides = {}
project_ids = {link.project_id for link in links}
for row in data.get("assignment_overrides", []):
pid, ids = row["project_id"], row["expert_ids"]
if pid not in project_ids or pid in overrides or len(set(ids)) != len(ids):
raise ValidationError("Проверьте проекты и экспертов в предпросмотре.")
overrides[pid] = set(ids)
seed = data.get("seed")
additions, shortages = [], []
for link in links:
needed = max(0, target(program) - counts[link.project_id])
Expand All @@ -820,8 +828,24 @@ def allocation(program, data):
and eligible(scopes.get(e.pk), link.case_name)
and (pools is None or e.pk in pools.get(link.case_name, set()))
]
candidates.sort(key=lambda e: (remaining[e.pk], e.pk))
for expert in candidates[:needed]:
candidates.sort(
key=lambda e: (
remaining[e.pk],
e.pk
if seed is None
else hashlib.sha256(f"{seed}:{link.project_id}:{e.pk}".encode()).digest(),
)
)
if link.project_id in overrides:
chosen = overrides[link.project_id]
if chosen - {e.pk for e in candidates} or len(chosen) > needed:
raise ValidationError(
"Выберите доступных экспертов в пределах числа оценок проекта."
)
proposed = [e for e in candidates if e.pk in chosen]
else:
proposed = candidates[:needed]
for expert in proposed:
additions.append(
{
"project_id": link.project_id,
Expand All @@ -832,12 +856,12 @@ def allocation(program, data):
}
)
remaining[expert.pk] += 1
if len(candidates) < needed:
if len(proposed) < needed:
shortages.append(
{
"project_id": link.project_id,
"project_name": link.project.name,
"missing": needed - len(candidates),
"missing": needed - len(proposed),
"case_name": link.case_name,
}
)
Expand Down
11 changes: 11 additions & 0 deletions project_rates/workspace_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ def validate(self, attrs):
return attrs


class AllocationOverrideWrite(serializers.Serializer):
project_id = serializers.IntegerField(min_value=1)
expert_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1), max_length=1000
)


class AllocationWrite(serializers.Serializer):
expert_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1), max_length=1000
Expand All @@ -102,6 +109,10 @@ class AllocationWrite(serializers.Serializer):
)
case_pools = CasePoolWrite(many=True, required=False, max_length=1000)
revision = serializers.CharField(max_length=64, required=False)
seed = serializers.IntegerField(min_value=0, max_value=2147483647, required=False)
assignment_overrides = AllocationOverrideWrite(
many=True, required=False, max_length=10000
)


class QuotaWrite(serializers.Serializer):
Expand Down
Loading