diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index 305e731fa..1f37c3ea2 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -191,9 +191,18 @@ def create_proposals(self, count, problems, users, tags, proposal_model, verbose """ objs = [] for i in range(count): - problem = random.choice(problems) - user = random.choice(users) - tag = random.choice(tags) + def candidate_fn(): + return (random.choice(problems), random.choice(users), random.choice(tags)) + + def uniqueness_fn(candidate): + problem, user, tag = candidate + if proposal_model.__name__ == 'AlgorithmTagProposal': + return not proposal_model.objects.filter(problem=problem, user=user, tag=tag).exists() + elif proposal_model.__name__ == 'DifficultyTagProposal': + return not proposal_model.objects.filter(problem=problem, user=user).exists() + return True + + problem, user, tag = get_unique_candidate(candidate_fn, uniqueness_fn) proposal = proposal_model(problem=problem, user=user, tag=tag) proposal.save() objs.append(proposal) diff --git a/oioioi/problems/migrations/0039_alter_algorithmtagproposal_unique_together_and_more.py b/oioioi/problems/migrations/0039_alter_algorithmtagproposal_unique_together_and_more.py new file mode 100644 index 000000000..e684c97aa --- /dev/null +++ b/oioioi/problems/migrations/0039_alter_algorithmtagproposal_unique_together_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.7 on 2025-11-17 22:44 + +from django.conf import settings +from django.db import migrations +from django.db.models import Count + +def cleanup_duplicate_proposals(apps, schema_editor): + AlgorithmTagProposal = apps.get_model('problems', 'AlgorithmTagProposal') + DifficultyTagProposal = apps.get_model('problems', 'DifficultyTagProposal') + + # Cleanup AlgorithmTagProposal + duplicates = ( + AlgorithmTagProposal.objects.values('problem', 'tag', 'user') + .annotate(count=Count('id')) + .filter(count__gt=1) + ) + + for duplicate in duplicates: + AlgorithmTagProposal.objects.filter( + pk__in=AlgorithmTagProposal.objects.filter( + problem=duplicate["problem"], + tag=duplicate["tag"], + user=duplicate["user"] + ).values_list("id", flat=True)[1:] + ).delete() + + # Cleanup DifficultyTagProposal + duplicates = ( + DifficultyTagProposal.objects.values('problem', 'user') + .annotate(count=Count('id')) + .filter(count__gt=1) + ) + + for duplicate in duplicates: + DifficultyTagProposal.objects.filter( + pk__in=DifficultyTagProposal.objects.filter( + problem=duplicate["problem"], + user=duplicate["user"] + ).values_list("id", flat=True)[1:] + ).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('problems', '0038_alter_algorithmtaglocalization_language_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RunPython(cleanup_duplicate_proposals), + migrations.AlterUniqueTogether( + name='algorithmtagproposal', + unique_together={('problem', 'tag', 'user')}, + ), + migrations.AlterUniqueTogether( + name='difficultytagproposal', + unique_together={('problem', 'user')}, + ), + ] diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index e38c740fe..e78511f90 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -797,6 +797,7 @@ def __str__(self): return str(self.problem.name) + " -- " + str(self.tag.name) class Meta: + unique_together = ("problem", "user") verbose_name = _("difficulty proposal") verbose_name_plural = _("difficulty proposals") @@ -878,6 +879,7 @@ def __str__(self): return str(self.problem.name) + " -- " + str(self.tag.name) class Meta: + unique_together = ("problem", "tag", "user") verbose_name = _("algorithm tag proposal") verbose_name_plural = _("algorithm tag proposals") diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index 245bca84e..228fcc579 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -266,3 +266,31 @@ def test_save_proposals_view(self): for q_data in invalid_query_data: response = self.client.post(self.url, q_data) self.assertEqual(response.status_code, 400) + + def test_duplicate_difficulty_proposal_rejected(self): + problem = Problem.objects.get(pk=0) + user = User.objects.get(username="test_admin") + + response = self.client.post( + self.url, + { + "tags[]": ["Dynamic programming"], + "difficulty": "Easy", + "user": "test_admin", + "problem": "0", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(DifficultyTagProposal.objects.filter(problem=problem, user=user).count(), 1) + + response = self.client.post( + self.url, + { + "tags[]": ["Greedy"], + "difficulty": "Medium", + "user": "test_admin", + "problem": "0", + }, + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(DifficultyTagProposal.objects.filter(problem=problem, user=user).count(), 1) diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index 0165e8130..cb958bbcf 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -1156,8 +1156,16 @@ def save_proposals_view(request): if not tags or not difficulty or not user or not problem: return HttpResponseBadRequest() + if DifficultyTagProposal.objects.filter(problem=problem, user=user).exists(): + return HttpResponseBadRequest() + for tag in tags: algorithm_tag = AlgorithmTagLocalization.objects.filter(full_name=tag, language=get_language()).first().algorithm_tag + + + if AlgorithmTagProposal.objects.filter(problem=problem, tag=algorithm_tag, user=user).exists(): + return HttpResponseBadRequest() + algorithm_tag_proposal = AlgorithmTagProposal(problem=problem, tag=algorithm_tag, user=user) algorithm_tag_proposal.save()