From 9519dbe42b07b6d094b52cef9eba2dda726fbdde Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 24 Nov 2024 22:31:35 +0100 Subject: [PATCH 01/30] Add new AggregatedAlgorithmTagProposal class. --- oioioi/problems/models.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 9c09a0bee..90a8b1f50 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -958,3 +958,18 @@ def __str__(self): class Meta(object): verbose_name = _("algorithm tag proposal") verbose_name_plural = _("algorithm tag proposals") + + + +class AggregatedAlgorithmTagProposal(models.Model): + problem = models.ForeignKey('Problem', on_delete=models.CASCADE) + tag = models.ForeignKey('AlgorithmTag', on_delete=models.CASCADE) + amount = models.PositiveIntegerField(default=0) + + def __str__(self): + return str(self.problem.name) + u' -- ' + str(self.tag.name) + u' -- ' + str(self.amount) + + class Meta: + verbose_name = _("aggregated algorithm tag proposal") + verbose_name_plural = _("aggregated algorithm tag proposals") + unique_together = ('problem', 'tag') \ No newline at end of file From 028bb2b8ea4c9e14fe34f6be0a2e220e443d7764 Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 24 Nov 2024 22:32:15 +0100 Subject: [PATCH 02/30] Make AggregatedAlgorithmTagProposal increase each time a new AlgorithmTagProposal is created. This is most likely a temporary implementation. --- oioioi/problems/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index 3aab16ba6..f66b0e570 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -53,6 +53,7 @@ from oioioi.filetracker.utils import stream_file from oioioi.problems.forms import ProblemsetSourceForm from oioioi.problems.models import ( + AggregatedAlgorithmTagProposal, AlgorithmTag, AlgorithmTagLocalization, AlgorithmTagProposal, @@ -1272,6 +1273,12 @@ def save_proposals_view(request): problem=problem, tag=algorithm_tag, user=user ) algorithm_tag_proposal.save() + aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( + problem=problem, tag=algorithm_tag + )[0] + aggregated_algorithm_tag_proposal.amout += 1 + aggregated_algorithm_tag_proposal.save() + difficulty_tag_localization = DifficultyTagLocalization.objects.filter( full_name=difficulty, language=get_language() From d7c3a4b30affc38f2910a58ec1216062ceebc3d3 Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 24 Nov 2024 23:07:00 +0100 Subject: [PATCH 03/30] Change handling of modification of AggregatedAlgorithmTagProposal to happen when adding or deleting AlgorithmTagProposals. --- oioioi/problems/models.py | 45 +++++++++++++++++++++++++++++++++++++-- oioioi/problems/views.py | 7 ------ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 90a8b1f50..cefe73dc5 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -2,14 +2,16 @@ import os.path from contextlib import contextmanager from traceback import format_exception +from xml.dom import ValidationErr from django.conf import settings from django.contrib.auth.models import User from django.core import validators +from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.validators import validate_slug from django.db import models, transaction -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import post_save, pre_delete, post_delete from django.dispatch import receiver from django.utils import timezone from django.utils.encoding import force_str @@ -960,6 +962,45 @@ class Meta(object): verbose_name_plural = _("algorithm tag proposals") +@receiver(post_save, sender=AlgorithmTagProposal) +def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): + problem = instance.problem + tag = instance.tag + + aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( + problem=problem, tag=tag + )[0] + + aggregated_algorithm_tag_proposal.amount += 1 + aggregated_algorithm_tag_proposal.save() + + +@receiver(post_delete, sender=AlgorithmTagProposal) +def decrease_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): + problem = instance.problem + tag = instance.tag + + try: + aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get(problem=problem, tag=tag) + aggregated_algorithm_tag_proposal.amount -= 1 + + if aggregated_algorithm_tag_proposal.amount == 0: + aggregated_algorithm_tag_proposal.delete() + else: + try: + aggregated_algorithm_tag_proposal.save() + except ValidationError as e: + raise RuntimeError( + f"AggregatedAlgorithmTagProposal and deleted AlgorithmTagProposal " + f"were out of sync - likely AggregatedAlgorithmTagProposal.amount " + f"decreased below 0. ValidationError: {str(e)}" + ) + except AggregatedAlgorithmTagProposal.DoesNotExist: + raise RuntimeError( + "AggregatedAlgorithmTagProposal corresponding to deleted AlgorithmTagProposal " + "does not exist." + ) + class AggregatedAlgorithmTagProposal(models.Model): problem = models.ForeignKey('Problem', on_delete=models.CASCADE) @@ -972,4 +1013,4 @@ def __str__(self): class Meta: verbose_name = _("aggregated algorithm tag proposal") verbose_name_plural = _("aggregated algorithm tag proposals") - unique_together = ('problem', 'tag') \ No newline at end of file + unique_together = ('problem', 'tag') diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index f66b0e570..3aab16ba6 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -53,7 +53,6 @@ from oioioi.filetracker.utils import stream_file from oioioi.problems.forms import ProblemsetSourceForm from oioioi.problems.models import ( - AggregatedAlgorithmTagProposal, AlgorithmTag, AlgorithmTagLocalization, AlgorithmTagProposal, @@ -1273,12 +1272,6 @@ def save_proposals_view(request): problem=problem, tag=algorithm_tag, user=user ) algorithm_tag_proposal.save() - aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( - problem=problem, tag=algorithm_tag - )[0] - aggregated_algorithm_tag_proposal.amout += 1 - aggregated_algorithm_tag_proposal.save() - difficulty_tag_localization = DifficultyTagLocalization.objects.filter( full_name=difficulty, language=get_language() From 4f2794a050cdec84e8b2ecab12f8ff228abb1ade Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 24 Nov 2024 23:32:39 +0100 Subject: [PATCH 04/30] Remove created argument from decrease_aggregated_algorithm_tag_proposal, correct increase function to only increase amount of AggregatedTag if a new AlgorithmTagProposal is created, not when modified. --- oioioi/problems/models.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index cefe73dc5..57954e9e4 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -967,16 +967,17 @@ def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwar problem = instance.problem tag = instance.tag - aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( - problem=problem, tag=tag - )[0] - - aggregated_algorithm_tag_proposal.amount += 1 - aggregated_algorithm_tag_proposal.save() + if created: + aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( + problem=problem, tag=tag + )[0] + + aggregated_algorithm_tag_proposal.amount += 1 + aggregated_algorithm_tag_proposal.save() @receiver(post_delete, sender=AlgorithmTagProposal) -def decrease_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): +def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): problem = instance.problem tag = instance.tag From 825bcf43e9b5e88c625cad74b0993fdeeeb0c379 Mon Sep 17 00:00:00 2001 From: Grzegorz Krawczyk Date: Mon, 25 Nov 2024 16:40:47 +0100 Subject: [PATCH 05/30] Split test_tags.py into test_tags.py file, testing AlgorithmTag and DifficultyTag classes and test_tag_proposals.py file, testing AlgorithmTagProposal and DifficultyTagProposal classes. --- oioioi/problems/tests/test_tag_proposals.py | 207 ++++++++++++++++++++ oioioi/problems/tests/test_tags.py | 192 ------------------ 2 files changed, 207 insertions(+), 192 deletions(-) create mode 100644 oioioi/problems/tests/test_tag_proposals.py diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py new file mode 100644 index 000000000..c144b024c --- /dev/null +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -0,0 +1,207 @@ +# coding: utf-8 + +import urllib.parse + +from django.contrib.auth.models import User +from django.test.utils import override_settings +from django.urls import reverse +from oioioi.base.tests import TestCase +from oioioi.problems.models import ( + AlgorithmTag, + AlgorithmTagProposal, + DifficultyTag, + DifficultyTagProposal, + Problem, +) +from oioioi.problems.tests.utilities import AssertContainsOnlyMixin + + +class TestAlgorithmTagsProposalHintsBase(TestCase): + """Abstract base class with getting url utility for algorithm tags proposal tests.""" + + fixtures = [ + 'test_users', + 'test_contest', + 'test_problem_packages', + 'test_problem_site', + 'test_algorithm_tags', + ] + view_name = 'get_algorithm_tag_proposal_hints' + + def get_query_url(self, parameters): + return '{}?{}'.format( + reverse(self.view_name), urllib.parse.urlencode(parameters) + ) + + +@override_settings(LANGUAGE_CODE='en') +class TestAlgorithmTagsProposalHintsEnglish(TestAlgorithmTagsProposalHintsBase): + def test_tag_proposal_hints_view(self): + self.assertTrue(self.client.login(username='test_user')) + self.client.get('/c/c/') # 'c' becomes the current contest + + response = self.client.get(self.get_query_url({'query': 'pLeCaK'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Knapsack problem') + self.assertNotContains(response, 'Problem plecakowy') + self.assertNotContains(response, 'pLeCaK') + self.assertNotContains(response, 'plecak') + self.assertNotContains(response, 'Longest common increasing subsequence') + + response = self.client.get(self.get_query_url({'query': 'PROBLEM'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Knapsack problem') + self.assertNotContains(response, 'Problem plecakowy') + self.assertNotContains(response, 'Longest common increasing subsequence') + + response = self.client.get(self.get_query_url({'query': 'dynam'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Dynamic programming') + self.assertNotContains(response, 'dp') + self.assertNotContains(response, 'Programowanie dynamiczne') + + response = self.client.get(self.get_query_url({'query': 'greedy'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Greedy') + self.assertNotContains(response, 'Dynamic programming') + self.assertNotContains(response, 'XYZ') + + # Use a byte string in the query to ensure a proper url encoding. + response = self.client.get(self.get_query_url({'query': 'najdłuższy'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Longest common increasing subsequence') + self.assertNotContains( + response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' + ) + self.assertNotContains(response, 'lcis') + + response = self.client.get(self.get_query_url({'query': 'l'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Longest common increasing subsequence') + self.assertNotContains( + response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' + ) + self.assertNotContains(response, 'lcis') + self.assertNotContains(response, 'Problem plecakowy') + + +@override_settings(LANGUAGE_CODE='pl') +class TestAlgorithmTagsProposalHintsPolish(TestAlgorithmTagsProposalHintsBase): + def test_tag_proposal_hints_view(self): + self.assertTrue(self.client.login(username='test_user')) + self.client.get('/c/c/') # 'c' becomes the current contest + + response = self.client.get(self.get_query_url({'query': 'plecak'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Problem plecakowy') + self.assertNotContains(response, 'Knapsack problem') + self.assertNotContains(response, 'Longest common increasing subsequence') + + response = self.client.get(self.get_query_url({'query': 'dynam'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Programowanie dynamiczne') + self.assertNotContains(response, 'dp') + self.assertNotContains(response, 'Dynamic programming') + + response = self.client.get(self.get_query_url({'query': 'greedy'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, b'Zach\u0142anny') + self.assertNotContains(response, 'Greedy') + + # Use a byte string in the query to ensure a proper url encoding. + response = self.client.get(self.get_query_url({'query': 'ZAchłan'})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, b'Zach\u0142anny') + self.assertNotContains(response, 'Greedy') + + response = self.client.get(self.get_query_url({'query': 'longest'})) + self.assertEqual(response.status_code, 200) + self.assertContains( + response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' + ) + self.assertNotContains(response, 'Longest common increasing subsequence') + self.assertNotContains(response, 'lcis') + + response = self.client.get(self.get_query_url({'query': 'lcis'})) + self.assertEqual(response.status_code, 200) + self.assertContains( + response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' + ) + self.assertNotContains(response, 'Longest common increasing subsequence') + self.assertNotContains(response, 'lcis') + + +class TestSaveProposals(TestCase): + fixtures = [ + 'test_users', + 'test_problem_search', + 'test_algorithm_tags', + 'test_difficulty_tags', + ] + url = reverse('save_proposals') + + def test_save_proposals_view(self): + problem = Problem.objects.get(pk=0) + user = User.objects.get(username='test_admin') + + self.assertEqual(AlgorithmTagProposal.objects.count(), 0) + self.assertEqual(DifficultyTagProposal.objects.count(), 0) + + response = self.client.post( + self.url, + { + 'tags[]': ["Dynamic programming", "Knapsack problem"], + 'difficulty': ' \r \t\n Easy \n \t ', + 'user': 'test_admin', + 'problem': '0', + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(AlgorithmTagProposal.objects.count(), 2) + self.assertEqual(DifficultyTagProposal.objects.count(), 1) + self.assertTrue( + AlgorithmTagProposal.objects.filter( + problem=problem, tag=AlgorithmTag.objects.get(name='dp'), user=user + ).exists() + ) + self.assertTrue( + AlgorithmTagProposal.objects.filter( + problem=problem, + tag=AlgorithmTag.objects.get(name='knapsack'), + user=user, + ).exists() + ) + self.assertTrue( + DifficultyTagProposal.objects.filter( + problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user + ).exists() + ) + + invalid_query_data = [ + {}, + { + 'difficulty': 'Easy', + 'user': 'test_admin', + 'problem': '0', + }, + { + 'tags[]': ["Dynamic programming", "Knapsack problem"], + 'user': 'test_admin', + 'problem': '0', + }, + { + 'tags[]': ["Dynamic programming", "Knapsack problem"], + 'difficulty': 'Easy', + 'problem': '0', + }, + { + 'tags[]': ["Dynamic programming", "Knapsack problem"], + 'difficulty': 'Easy', + 'user': 'test_admin', + }, + ] + + for q_data in invalid_query_data: + response = self.client.post(self.url, q_data) + self.assertEqual(response.status_code, 400) + diff --git a/oioioi/problems/tests/test_tags.py b/oioioi/problems/tests/test_tags.py index 5541112a9..6b8e5569f 100644 --- a/oioioi/problems/tests/test_tags.py +++ b/oioioi/problems/tests/test_tags.py @@ -8,129 +8,12 @@ from oioioi.base.tests import TestCase from oioioi.problems.models import ( AlgorithmTag, - AlgorithmTagProposal, DifficultyTag, - DifficultyTagProposal, Problem, ) from oioioi.problems.tests.utilities import AssertContainsOnlyMixin -class TestAlgorithmTagsProposalHintsBase(TestCase): - """Abstract base class with getting url utility for algorithm tags proposal tests.""" - - fixtures = [ - 'test_users', - 'test_contest', - 'test_problem_packages', - 'test_problem_site', - 'test_algorithm_tags', - ] - view_name = 'get_algorithm_tag_proposal_hints' - - def get_query_url(self, parameters): - return '{}?{}'.format( - reverse(self.view_name), urllib.parse.urlencode(parameters) - ) - - -@override_settings(LANGUAGE_CODE='en') -class TestAlgorithmTagsProposalHintsEnglish(TestAlgorithmTagsProposalHintsBase): - def test_tag_proposal_hints_view(self): - self.assertTrue(self.client.login(username='test_user')) - self.client.get('/c/c/') # 'c' becomes the current contest - - response = self.client.get(self.get_query_url({'query': 'pLeCaK'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Knapsack problem') - self.assertNotContains(response, 'Problem plecakowy') - self.assertNotContains(response, 'pLeCaK') - self.assertNotContains(response, 'plecak') - self.assertNotContains(response, 'Longest common increasing subsequence') - - response = self.client.get(self.get_query_url({'query': 'PROBLEM'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Knapsack problem') - self.assertNotContains(response, 'Problem plecakowy') - self.assertNotContains(response, 'Longest common increasing subsequence') - - response = self.client.get(self.get_query_url({'query': 'dynam'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Dynamic programming') - self.assertNotContains(response, 'dp') - self.assertNotContains(response, 'Programowanie dynamiczne') - - response = self.client.get(self.get_query_url({'query': 'greedy'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Greedy') - self.assertNotContains(response, 'Dynamic programming') - self.assertNotContains(response, 'XYZ') - - # Use a byte string in the query to ensure a proper url encoding. - response = self.client.get(self.get_query_url({'query': 'najdłuższy'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Longest common increasing subsequence') - self.assertNotContains( - response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' - ) - self.assertNotContains(response, 'lcis') - - response = self.client.get(self.get_query_url({'query': 'l'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Longest common increasing subsequence') - self.assertNotContains( - response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' - ) - self.assertNotContains(response, 'lcis') - self.assertNotContains(response, 'Problem plecakowy') - - -@override_settings(LANGUAGE_CODE='pl') -class TestAlgorithmTagsProposalHintsPolish(TestAlgorithmTagsProposalHintsBase): - def test_tag_proposal_hints_view(self): - self.assertTrue(self.client.login(username='test_user')) - self.client.get('/c/c/') # 'c' becomes the current contest - - response = self.client.get(self.get_query_url({'query': 'plecak'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Problem plecakowy') - self.assertNotContains(response, 'Knapsack problem') - self.assertNotContains(response, 'Longest common increasing subsequence') - - response = self.client.get(self.get_query_url({'query': 'dynam'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Programowanie dynamiczne') - self.assertNotContains(response, 'dp') - self.assertNotContains(response, 'Dynamic programming') - - response = self.client.get(self.get_query_url({'query': 'greedy'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, b'Zach\u0142anny') - self.assertNotContains(response, 'Greedy') - - # Use a byte string in the query to ensure a proper url encoding. - response = self.client.get(self.get_query_url({'query': 'ZAchłan'})) - self.assertEqual(response.status_code, 200) - self.assertContains(response, b'Zach\u0142anny') - self.assertNotContains(response, 'Greedy') - - response = self.client.get(self.get_query_url({'query': 'longest'})) - self.assertEqual(response.status_code, 200) - self.assertContains( - response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' - ) - self.assertNotContains(response, 'Longest common increasing subsequence') - self.assertNotContains(response, 'lcis') - - response = self.client.get(self.get_query_url({'query': 'lcis'})) - self.assertEqual(response.status_code, 200) - self.assertContains( - response, b'Najd\u0142u\u017cszy wsp\u00f3lny podci\u0105g rosn\u0105cy' - ) - self.assertNotContains(response, 'Longest common increasing subsequence') - self.assertNotContains(response, 'lcis') - - class TestAlgorithmTagLabel(TestCase): fixtures = ['test_algorithm_tags'] view_name = 'get_algorithm_tag_label' @@ -170,81 +53,6 @@ def test_algorithm_tag_label_view(self): self.assertEqual(response.status_code, 404) -class TestSaveProposals(TestCase): - fixtures = [ - 'test_users', - 'test_problem_search', - 'test_algorithm_tags', - 'test_difficulty_tags', - ] - url = reverse('save_proposals') - - def test_save_proposals_view(self): - problem = Problem.objects.get(pk=0) - user = User.objects.get(username='test_admin') - - self.assertEqual(AlgorithmTagProposal.objects.count(), 0) - self.assertEqual(DifficultyTagProposal.objects.count(), 0) - - response = self.client.post( - self.url, - { - 'tags[]': ["Dynamic programming", "Knapsack problem"], - 'difficulty': ' \r \t\n Easy \n \t ', - 'user': 'test_admin', - 'problem': '0', - }, - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(AlgorithmTagProposal.objects.count(), 2) - self.assertEqual(DifficultyTagProposal.objects.count(), 1) - self.assertTrue( - AlgorithmTagProposal.objects.filter( - problem=problem, tag=AlgorithmTag.objects.get(name='dp'), user=user - ).exists() - ) - self.assertTrue( - AlgorithmTagProposal.objects.filter( - problem=problem, - tag=AlgorithmTag.objects.get(name='knapsack'), - user=user, - ).exists() - ) - self.assertTrue( - DifficultyTagProposal.objects.filter( - problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user - ).exists() - ) - - invalid_query_data = [ - {}, - { - 'difficulty': 'Easy', - 'user': 'test_admin', - 'problem': '0', - }, - { - 'tags[]': ["Dynamic programming", "Knapsack problem"], - 'user': 'test_admin', - 'problem': '0', - }, - { - 'tags[]': ["Dynamic programming", "Knapsack problem"], - 'difficulty': 'Easy', - 'problem': '0', - }, - { - 'tags[]': ["Dynamic programming", "Knapsack problem"], - 'difficulty': 'Easy', - 'user': 'test_admin', - }, - ] - - for q_data in invalid_query_data: - response = self.client.post(self.url, q_data) - self.assertEqual(response.status_code, 400) - - class TestProblemSearchOrigin(TestCase, AssertContainsOnlyMixin): fixtures = ['test_problem_search_origin'] url = reverse('problemset_main') From de5da6c257788f4ba9893f4db02fd4c0c640d0ea Mon Sep 17 00:00:00 2001 From: Grzegorz Krawczyk Date: Mon, 25 Nov 2024 17:31:54 +0100 Subject: [PATCH 06/30] Expand test_save_proposals_view in TestSaveProposals class to also check AggregatedAlgorithmTagProposal. --- oioioi/problems/tests/test_tag_proposals.py | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index c144b024c..c88880e98 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -145,6 +145,7 @@ def test_save_proposals_view(self): user = User.objects.get(username='test_admin') self.assertEqual(AlgorithmTagProposal.objects.count(), 0) + self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 0) self.assertEqual(DifficultyTagProposal.objects.count(), 0) response = self.client.post( @@ -158,6 +159,7 @@ def test_save_proposals_view(self): ) self.assertEqual(response.status_code, 200) self.assertEqual(AlgorithmTagProposal.objects.count(), 2) + self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 2) self.assertEqual(DifficultyTagProposal.objects.count(), 1) self.assertTrue( AlgorithmTagProposal.objects.filter( @@ -171,12 +173,83 @@ def test_save_proposals_view(self): user=user, ).exists() ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, tag=AlgorithmTag.objects.get(name='dp') + ).amount, 1 + ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, + tag=AlgorithmTag.objects.get(name='knapsack'), + ).amount, 1 + ) self.assertTrue( DifficultyTagProposal.objects.filter( problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user ).exists() ) + problem = Problem.object.get(pk=0) + user = User.objects.get(username='test_user') + + response = self.client.post( + self.url, + { + 'tags[]': ["Longest common increasing subsequence", "Dynamic programming", "Greedy"], + 'difficulty': ' \t \r\n MEDIUM \t \n ', + 'user': 'test_user', + 'problem': '0', + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(AlgorithmTagProposal.objects.count(), 5) + self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 4) + self.assertEqual(DifficultyTagProposal.objects.count(), 2) + self.assertTrue( + AlgorithmTagProposal.objects.filter( + problem=problem, tag=AlgorithmTag.objects.get(name='lcis'), user=user + ).exists() + ) + self.assertTrue( + AlgorithmTagProposal.objects.filter( + problem=problem, tag=AlgorithmTag.objects.get(name='dp'), user=user + ).exists() + ) + self.assertTrue( + AlgorithmTagProposal.objects.filter( + problem=problem, + tag=AlgorithmTag.objects.get(name='greedy'), + user=user, + ).exists() + ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, tag=AlgorithmTag.objects.get(name='greedy') + ).amount, 1 + ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, tag=AlgorithmTag.objects.get(name='lcis') + ).amount, 1 + ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, tag=AlgorithmTag.objects.get(name='dp') + ).amount, 2 + ) + self.assertEquals( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, + tag=AlgorithmTag.objects.get(name='knapsack'), + ).amount, 1 + ) + self.assertTrue( + DifficultyTagProposal.objects.filter( + problem=problem, tag=DifficultyTag.objects.get(name='medium'), user=user + ).exists() + ) + invalid_query_data = [ {}, { From f9c1e1571537ad99006d18f4d33ba416b846aaf5 Mon Sep 17 00:00:00 2001 From: segir187 Date: Tue, 26 Nov 2024 14:01:49 +0100 Subject: [PATCH 07/30] Add AggregatedDifficultyTagProposal. --- oioioi/problems/models.py | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 57954e9e4..a0d6b635f 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -887,6 +887,62 @@ class Meta(object): verbose_name_plural = _("difficulty proposals") + +@receiver(post_save, sender=DifficultyTagProposal) +def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwargs): + problem = instance.problem + tag = instance.tag + + if created: + aggregated_difficulty_tag_proposal = AggregatedDifficultyTagProposal.objects.get_or_create( + problem=problem, tag=tag + )[0] + + aggregated_difficulty_tag_proposal.amount += 1 + aggregated_difficulty_tag_proposal.save() + + +@receiver(post_delete, sender=DifficultyTagProposal) +def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): + problem = instance.problem + tag = instance.tag + + try: + aggregated_difficulty_tag_proposal = AggregatedDifficultyTagProposal.objects.get(problem=problem, tag=tag) + aggregated_difficulty_tag_proposal.amount -= 1 + + if aggregated_difficulty_tag_proposal.amount == 0: + aggregated_difficulty_tag_proposal.delete() + else: + try: + aggregated_difficulty_tag_proposal.save() + except ValidationError as e: + raise RuntimeError( + f"AggregatedDifficultyTagProposal and deleted DifficultyTagProposal " + f"were out of sync - likely AggregatedDifficultyTagProposal.amount " + f"decreased below 0. ValidationError: {str(e)}" + ) + except AggregatedDifficultyTagProposal.DoesNotExist: + raise RuntimeError( + "AggregatedDifficultyTagProposal corresponding to deleted DifficultyTagProposal " + "does not exist." + ) + + +class AggregatedDifficultyTagProposal(models.Model): + problem = models.ForeignKey('Problem', on_delete=models.CASCADE) + tag = models.ForeignKey('DifficultyTag', on_delete=models.CASCADE) + amount = models.PositiveIntegerField(default=0) + + def __str__(self): + return str(self.problem.name) + u' -- ' + str(self.tag.name) + u' -- ' + str(self.amount) + + class Meta: + verbose_name = _("aggregated difficulty tag proposal") + verbose_name_plural = _("aggregated difficulty tag proposals") + unique_together = ('problem', 'tag') + + @_localized('full_name') class AlgorithmTag(models.Model): From fc5441bfd69e8a755856e0d392ed95ac55c07df8 Mon Sep 17 00:00:00 2001 From: segir187 Date: Tue, 26 Nov 2024 14:09:46 +0100 Subject: [PATCH 08/30] Add checking AggregatedDifficultyTagProposal in test_save_poposals_view in TestTagProposals class. Fix mistaken 'assertEquals' to 'assertEqual'. --- oioioi/problems/tests/test_tag_proposals.py | 71 ++++++++++++++++++--- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index c88880e98..12f797051 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -7,6 +7,8 @@ from django.urls import reverse from oioioi.base.tests import TestCase from oioioi.problems.models import ( + AggregatedAlgorithmTagProposal, + AggregatedDifficultyTagProposal, AlgorithmTag, AlgorithmTagProposal, DifficultyTag, @@ -145,8 +147,9 @@ def test_save_proposals_view(self): user = User.objects.get(username='test_admin') self.assertEqual(AlgorithmTagProposal.objects.count(), 0) - self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 0) + self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 0) self.assertEqual(DifficultyTagProposal.objects.count(), 0) + self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 0) response = self.client.post( self.url, @@ -159,8 +162,9 @@ def test_save_proposals_view(self): ) self.assertEqual(response.status_code, 200) self.assertEqual(AlgorithmTagProposal.objects.count(), 2) - self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 2) + self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 2) self.assertEqual(DifficultyTagProposal.objects.count(), 1) + self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 1) self.assertTrue( AlgorithmTagProposal.objects.filter( problem=problem, tag=AlgorithmTag.objects.get(name='dp'), user=user @@ -173,12 +177,12 @@ def test_save_proposals_view(self): user=user, ).exists() ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='dp') ).amount, 1 ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='knapsack'), @@ -189,6 +193,11 @@ def test_save_proposals_view(self): problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user ).exists() ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user + ).amount, 1 + ) problem = Problem.object.get(pk=0) user = User.objects.get(username='test_user') @@ -204,8 +213,9 @@ def test_save_proposals_view(self): ) self.assertEqual(response.status_code, 200) self.assertEqual(AlgorithmTagProposal.objects.count(), 5) - self.assertEqual(AggregatedAlgorithmTagProposal.object.count(), 4) + self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 4) self.assertEqual(DifficultyTagProposal.objects.count(), 2) + self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 2) self.assertTrue( AlgorithmTagProposal.objects.filter( problem=problem, tag=AlgorithmTag.objects.get(name='lcis'), user=user @@ -223,22 +233,22 @@ def test_save_proposals_view(self): user=user, ).exists() ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='greedy') ).amount, 1 ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='lcis') ).amount, 1 ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='dp') ).amount, 2 ) - self.assertEquals( + self.assertEqual( AggregatedAlgorithmTagProposal.objects.get( problem=problem, tag=AlgorithmTag.objects.get(name='knapsack'), @@ -249,6 +259,49 @@ def test_save_proposals_view(self): problem=problem, tag=DifficultyTag.objects.get(name='medium'), user=user ).exists() ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=problem, tag=DifficultyTag.objects.get(name='medium') + ).amount, 1 + ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=problem, tag=DifficultyTag.objects.get(name='easy') + ).amount, 1 + ) + + problem = Problem.object.get(pk=0) + user = User.objects.get(username='test_user2') + + response = self.client.post( + self.url, + { + 'tags[]': [], + 'difficulty': ' medium \n ', + 'user': 'test_user2', + 'problem': '0', + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(AlgorithmTagProposal.objects.count(), 5) + self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 4) + self.assertEqual(DifficultyTagProposal.objects.count(), 3) + self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 2) + self.assertTrue( + DifficultyTagProposal.objects.filter( + problem=problem, tag=DifficultyTag.objects.get(name='medium'), user=user + ).exists() + ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=problem, tag=DifficultyTag.objects.get(name='medium') + ).amount, 2 + ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=problem, tag=DifficultyTag.objects.get(name='easy') + ).amount, 1 + ) invalid_query_data = [ {}, From 61c1df2099e518839b3e2876882d26b894afc2af Mon Sep 17 00:00:00 2001 From: segir187 Date: Thu, 28 Nov 2024 14:45:39 +0100 Subject: [PATCH 09/30] Migrate AggregatedTagProposal classes. --- .../0032_aggregated_tag_proposals.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 oioioi/problems/migrations/0032_aggregated_tag_proposals.py diff --git a/oioioi/problems/migrations/0032_aggregated_tag_proposals.py b/oioioi/problems/migrations/0032_aggregated_tag_proposals.py new file mode 100644 index 000000000..a7e09fc88 --- /dev/null +++ b/oioioi/problems/migrations/0032_aggregated_tag_proposals.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.16 on 2024-11-28 13:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('problems', '0031_auto_20220328_1124'), + ] + + operations = [ + migrations.CreateModel( + name='AggregatedDifficultyTagProposal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.PositiveIntegerField(default=0)), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problems.problem')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problems.difficultytag')), + ], + options={ + 'verbose_name': 'aggregated difficulty tag proposal', + 'verbose_name_plural': 'aggregated difficulty tag proposals', + 'unique_together': {('problem', 'tag')}, + }, + ), + migrations.CreateModel( + name='AggregatedAlgorithmTagProposal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.PositiveIntegerField(default=0)), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problems.problem')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problems.algorithmtag')), + ], + options={ + 'verbose_name': 'aggregated algorithm tag proposal', + 'verbose_name_plural': 'aggregated algorithm tag proposals', + 'unique_together': {('problem', 'tag')}, + }, + ), + ] From 3deb61ecc95eee592cf73e9a47bd0e1d93d01c55 Mon Sep 17 00:00:00 2001 From: segir187 Date: Thu, 28 Nov 2024 15:19:06 +0100 Subject: [PATCH 10/30] Fixed test_tag_proposals.py to work correctly. --- oioioi/problems/tests/test_tag_proposals.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index 12f797051..474d5b0f5 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -195,18 +195,18 @@ def test_save_proposals_view(self): ) self.assertEqual( AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='easy'), user=user + problem=problem, tag=DifficultyTag.objects.get(name='easy') ).amount, 1 ) - problem = Problem.object.get(pk=0) + problem = Problem.objects.get(pk=0) user = User.objects.get(username='test_user') response = self.client.post( self.url, { 'tags[]': ["Longest common increasing subsequence", "Dynamic programming", "Greedy"], - 'difficulty': ' \t \r\n MEDIUM \t \n ', + 'difficulty': ' \t \r\n Medium \t \n ', 'user': 'test_user', 'problem': '0', }, @@ -270,23 +270,28 @@ def test_save_proposals_view(self): ).amount, 1 ) - problem = Problem.object.get(pk=0) + problem = Problem.objects.get(pk=0) user = User.objects.get(username='test_user2') response = self.client.post( self.url, { - 'tags[]': [], - 'difficulty': ' medium \n ', + 'tags[]': ["Greedy"], + 'difficulty': ' Medium \n ', 'user': 'test_user2', 'problem': '0', }, ) self.assertEqual(response.status_code, 200) - self.assertEqual(AlgorithmTagProposal.objects.count(), 5) + self.assertEqual(AlgorithmTagProposal.objects.count(), 6) self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 4) self.assertEqual(DifficultyTagProposal.objects.count(), 3) self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 2) + self.assertEqual( + AggregatedAlgorithmTagProposal.objects.get( + problem=problem, tag=AlgorithmTag.objects.get(name='greedy') + ).amount, 2 + ) self.assertTrue( DifficultyTagProposal.objects.filter( problem=problem, tag=DifficultyTag.objects.get(name='medium'), user=user From 1462c31b5577dec1875d137aea20f573a114e365 Mon Sep 17 00:00:00 2001 From: segir187 Date: Thu, 28 Nov 2024 21:02:02 +0100 Subject: [PATCH 11/30] Remove unnecessary import. --- oioioi/problems/tests/test_tag_proposals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index 474d5b0f5..da82778c2 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -15,7 +15,6 @@ DifficultyTagProposal, Problem, ) -from oioioi.problems.tests.utilities import AssertContainsOnlyMixin class TestAlgorithmTagsProposalHintsBase(TestCase): From 5ea3d5a071627779eb1fab9e90ae2040eaf5e653 Mon Sep 17 00:00:00 2001 From: segir187 Date: Thu, 28 Nov 2024 21:02:39 +0100 Subject: [PATCH 12/30] Add data migration from TagProposal models to AggregatedTagProposal models. --- .../0033_populate_aggregated_tag_proposals.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py diff --git a/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py b/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py new file mode 100644 index 000000000..6664a843d --- /dev/null +++ b/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.16 on 2024-11-28 18:50 + +from django.db import migrations, models + +def populate_aggregated_tag_proposals(apps, schema_editor): + DifficultyTagProposal = apps.get_model('problems', 'DifficultyTagProposal') + AggregatedDifficultyTagProposal = apps.get_model('problems', 'AggregatedDifficultyTagProposal') + AlgorithmTagProposal = apps.get_model('problems', 'AlgorithmTagProposal') + AggregatedAlgorithmTagProposal = apps.get_model('problems', 'AggregatedAlgorithmTagProposal') + + AggregatedDifficultyTagProposal.objects.all().delete() + AggregatedAlgorithmTagProposal.objects.all().delete() + + difficulty_data = ( + DifficultyTagProposal.objects.values('problem', 'tag') + .annotate(amount=models.Count('id')) + ) + for entry in difficulty_data: + AggregatedDifficultyTagProposal.objects.create( + problem_id=entry['problem'], + tag_id=entry['tag'], + amount=entry['amount'] + ) + + algorithm_data = ( + AlgorithmTagProposal.objects.values('problem', 'tag') + .annotate(amount=models.Count('id')) + ) + for entry in algorithm_data: + AggregatedAlgorithmTagProposal.objects.create( + problem_id=entry['problem'], + tag_id=entry['tag'], + amount=entry['amount'] + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('problems', '0032_aggregated_tag_proposals'), + ] + + operations = [ + migrations.RunPython(populate_aggregated_tag_proposals) + ] From b385cd7bc2e03ad5df2d6c1263f2b90da5c3dbd1 Mon Sep 17 00:00:00 2001 From: segir187 Date: Thu, 28 Nov 2024 21:20:05 +0100 Subject: [PATCH 13/30] Remove unnecessary import. --- oioioi/problems/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index a0d6b635f..2a46016e4 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -2,7 +2,6 @@ import os.path from contextlib import contextmanager from traceback import format_exception -from xml.dom import ValidationErr from django.conf import settings from django.contrib.auth.models import User From 8d7b5ea543210e92033bf983743073ed04db5c9f Mon Sep 17 00:00:00 2001 From: Grzegorz Krawczyk Date: Mon, 2 Dec 2024 18:26:29 +0100 Subject: [PATCH 14/30] Initial version of test_data_migrations.py. It should be restructured. --- oioioi/problems/tests/test_data_migrations.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 oioioi/problems/tests/test_data_migrations.py diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py new file mode 100644 index 000000000..2a7d72a15 --- /dev/null +++ b/oioioi/problems/tests/test_data_migrations.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +from django.test import TestCase +from django.apps import apps +from django.contrib.auth.models import User +from oioioi.problems.models import ( + AggregatedDifficultyTagProposal, + AggregatedAlgorithmTagProposal, + AlgorithmTagProposal, + DifficultyTagProposal, + Problem, + AlgorithmTag, + DifficultyTag, +) +from oioioi.problems.migrations.0033_populate_aggregated_tag_proposals import populate_aggregated_tag_proposals + +class PopulateAggregatedTagProposalsTest(TestCase): + fixtures = [ + 'test_users', + 'test_problem_search', + 'test_algorithm_tags', + 'test_difficulty_tags', + ] + + def setUp(self): + self.problem1 = Problem.objects.get(pk=1) + self.problem2 = Problem.objects.get(pk=2) + self.algorithm_tag1 = AlgorithmTag.objects.get(pk=1) + self.algorithm_tag2 = AlgorithmTag.objects.get(pk=2) + self.difficulty_tag1 = DifficultyTag.objects.get(pk=1) + self.difficulty_tag2 = DifficultyTag.objects.get(pk=2) + self.user1 = User.objects.get(pk=1) + self.user2 = User.objects.get(pk=2) + self.user3 = User.objects.get(pk=3) + + DifficultyTagProposal.objects.bulk_create([ + DifficultyTagProposal(problem=self.problem1, tag=self.difficulty_tag1, user=self.user1), + DifficultyTagProposal(problem=self.problem1, tag=self.difficulty_tag1, user=self.user2), + DifficultyTagProposal(problem=self.problem1, tag=self.difficulty_tag2, user=self.user3), + DifficultyTagProposal(problem=self.problem2, tag=self.difficulty_tag2, user=self.user2), + ]) + + AlgorithmTagProposal.objects.bulk_create([ + AlgorithmTagProposal(problem=self.problem1, tag=self.algorithm_tag1, user=self.user1), + AlgorithmTagProposal(problem=self.problem1, tag=self.algorithm_tag2, user=self.user1), + AlgorithmTagProposal(problem=self.problem1, tag=self.algorithm_tag1, user=self.user3), + AlgorithmTagProposal(problem=self.problem2, tag=self.algorithm_tag2, user=self.user1), + AlgorithmTagProposal(problem=self.problem2, tag=self.algorithm_tag1, user=self.user2), + AlgorithmTagProposal(problem=self.problem2, tag=self.algorithm_tag2, user=self.user2), + AlgorithmTagProposal(problem=self.problem2, tag=self.algorithm_tag2, user=self.user3), + ]) + + def test_populate_aggregated_tag_proposals(self): + AggregatedDifficultyTagProposal.objects.filter(problem=self.problem1).delete() + AggregatedAlgorithmTagProposal.objects.filter(problem=self.problem2).delete() + + populate_aggregated_tag_proposals(apps, None) + + self.assertEqual(AlgorithmTagProposal.objects.count(), 7) + self.assertEqual(DifficultyTagProposal.objects.count(), 4) + + self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 4) + self.assertEqual( + AggregatedAlgorithmTagProposal.objects.get( + problem=self.problem1, tag=self.algorithm_tag1 + ).amount, 2 + ) + self.assertEqual( + AggregatedAlgorithmTagProposal.objects.get( + problem=self.problem1, tag=self.algorithm_tag2 + ).amount, 1 + ) + self.assertEqual( + AggregatedAlgorithmTagProposal.objects.get( + problem=self.problem2, tag=self.algorithm_tag1 + ).amount, 1 + ) + self.assertEqual( + AggregatedAlgorithmTagProposal.objects.get( + problem=self.problem2, tag=self.algorithm_tag2 + ).amount, 3 + ) + + self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 3) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=self.problem1, tag=self.difficulty_tag1 + ).amount, 2 + ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=self.problem1, tag=self.difficulty_tag2 + ).amount, 1 + ) + self.assertEqual( + AggregatedDifficultyTagProposal.objects.get( + problem=self.problem2, tag=self.difficulty_tag2 + ).amount, 1 + ) + \ No newline at end of file From a34fa72e3624c938b535d9e21f946e3ba6c63818 Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 00:49:55 +0100 Subject: [PATCH 15/30] Change import of migration function from static to dynamic. --- oioioi/problems/tests/test_data_migrations.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py index 2a7d72a15..c85e1d165 100644 --- a/oioioi/problems/tests/test_data_migrations.py +++ b/oioioi/problems/tests/test_data_migrations.py @@ -12,7 +12,12 @@ AlgorithmTag, DifficultyTag, ) -from oioioi.problems.migrations.0033_populate_aggregated_tag_proposals import populate_aggregated_tag_proposals +import importlib + +# Dynamically import the function applying data migration for AggregatedTagProposals. +# This is necessary, since the name of the migration file causes a syntax error when imported normally. +migration_module = importlib.import_module('oioioi.problems.migrations.0033_populate_aggregated_tag_proposals') +populate_aggregated_tag_proposals = getattr(migration_module, 'populate_aggregated_tag_proposals') class PopulateAggregatedTagProposalsTest(TestCase): fixtures = [ From 7941d9e8564bdd61c247809ec27d000be27093a0 Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 00:51:11 +0100 Subject: [PATCH 16/30] Change user primary keys to keys corresponding to valid users from test_users fixture. --- oioioi/problems/tests/test_data_migrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py index c85e1d165..cb96ec5a9 100644 --- a/oioioi/problems/tests/test_data_migrations.py +++ b/oioioi/problems/tests/test_data_migrations.py @@ -34,9 +34,9 @@ def setUp(self): self.algorithm_tag2 = AlgorithmTag.objects.get(pk=2) self.difficulty_tag1 = DifficultyTag.objects.get(pk=1) self.difficulty_tag2 = DifficultyTag.objects.get(pk=2) - self.user1 = User.objects.get(pk=1) - self.user2 = User.objects.get(pk=2) - self.user3 = User.objects.get(pk=3) + self.user1 = User.objects.get(pk=1000) + self.user2 = User.objects.get(pk=1001) + self.user3 = User.objects.get(pk=1002) DifficultyTagProposal.objects.bulk_create([ DifficultyTagProposal(problem=self.problem1, tag=self.difficulty_tag1, user=self.user1), From ad0fa08ba7ca5ff472e25a011157ab12e5570477 Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 01:13:48 +0100 Subject: [PATCH 17/30] Change usages of creates in for loops to bulk_creates. --- .../0033_populate_aggregated_tag_proposals.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py b/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py index 6664a843d..ba27bd58c 100644 --- a/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py +++ b/oioioi/problems/migrations/0033_populate_aggregated_tag_proposals.py @@ -15,23 +15,27 @@ def populate_aggregated_tag_proposals(apps, schema_editor): DifficultyTagProposal.objects.values('problem', 'tag') .annotate(amount=models.Count('id')) ) - for entry in difficulty_data: - AggregatedDifficultyTagProposal.objects.create( + AggregatedDifficultyTagProposal.objects.bulk_create([ + AggregatedDifficultyTagProposal( problem_id=entry['problem'], tag_id=entry['tag'], amount=entry['amount'] ) + for entry in difficulty_data + ]) algorithm_data = ( AlgorithmTagProposal.objects.values('problem', 'tag') .annotate(amount=models.Count('id')) ) - for entry in algorithm_data: - AggregatedAlgorithmTagProposal.objects.create( + AggregatedAlgorithmTagProposal.objects.bulk_create([ + AggregatedAlgorithmTagProposal( problem_id=entry['problem'], tag_id=entry['tag'], amount=entry['amount'] ) + for entry in algorithm_data + ]) class Migration(migrations.Migration): From a1c00c396f013127ce48d5da211c57e02efeaffe Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 01:25:23 +0100 Subject: [PATCH 18/30] Change exceptions raised to logged messages. --- oioioi/problems/models.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 2a46016e4..c51840e6d 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -916,13 +916,13 @@ def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): try: aggregated_difficulty_tag_proposal.save() except ValidationError as e: - raise RuntimeError( - f"AggregatedDifficultyTagProposal and deleted DifficultyTagProposal " - f"were out of sync - likely AggregatedDifficultyTagProposal.amount " - f"decreased below 0. ValidationError: {str(e)}" + logger.exception( + "AggregatedDifficultyTagProposal and deleted DifficultyTagProposal " + "were out of sync - likely AggregatedDifficultyTagProposal.amount " + "decreased below 0." ) except AggregatedDifficultyTagProposal.DoesNotExist: - raise RuntimeError( + logger.exception( "AggregatedDifficultyTagProposal corresponding to deleted DifficultyTagProposal " "does not exist." ) @@ -1046,13 +1046,13 @@ def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): try: aggregated_algorithm_tag_proposal.save() except ValidationError as e: - raise RuntimeError( - f"AggregatedAlgorithmTagProposal and deleted AlgorithmTagProposal " - f"were out of sync - likely AggregatedAlgorithmTagProposal.amount " - f"decreased below 0. ValidationError: {str(e)}" + logger.exception( + "AggregatedAlgorithmTagProposal and deleted AlgorithmTagProposal " + "were out of sync - likely AggregatedAlgorithmTagProposal.amount " + "decreased below 0." ) except AggregatedAlgorithmTagProposal.DoesNotExist: - raise RuntimeError( + logger.exception( "AggregatedAlgorithmTagProposal corresponding to deleted AlgorithmTagProposal " "does not exist." ) From de8866c970f1fc758de15c481a8d484287d094bb Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 01:57:24 +0100 Subject: [PATCH 19/30] Make increase_aggregated and decrease_aggregated functions more compact. --- oioioi/problems/models.py | 102 ++++++++++++++------------------------ 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index c51840e6d..4c1b0bfe0 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -889,43 +889,30 @@ class Meta(object): @receiver(post_save, sender=DifficultyTagProposal) def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwargs): - problem = instance.problem - tag = instance.tag - if created: - aggregated_difficulty_tag_proposal = AggregatedDifficultyTagProposal.objects.get_or_create( - problem=problem, tag=tag - )[0] - - aggregated_difficulty_tag_proposal.amount += 1 - aggregated_difficulty_tag_proposal.save() + AggregatedDifficultyTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).update(amount=models.F('amount') + 1) \ + or \ + AggregatedDifficultyTagProposal.objects.create( + problem=instance.problem, + tag=instance.tag, + amount=1 + ) @receiver(post_delete, sender=DifficultyTagProposal) def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): - problem = instance.problem - tag = instance.tag - - try: - aggregated_difficulty_tag_proposal = AggregatedDifficultyTagProposal.objects.get(problem=problem, tag=tag) - aggregated_difficulty_tag_proposal.amount -= 1 - - if aggregated_difficulty_tag_proposal.amount == 0: - aggregated_difficulty_tag_proposal.delete() - else: - try: - aggregated_difficulty_tag_proposal.save() - except ValidationError as e: - logger.exception( - "AggregatedDifficultyTagProposal and deleted DifficultyTagProposal " - "were out of sync - likely AggregatedDifficultyTagProposal.amount " - "decreased below 0." - ) - except AggregatedDifficultyTagProposal.DoesNotExist: - logger.exception( - "AggregatedDifficultyTagProposal corresponding to deleted DifficultyTagProposal " - "does not exist." - ) + AggregatedDifficultyTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ + or \ + AggregatedDifficultyTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).delete() class AggregatedDifficultyTagProposal(models.Model): @@ -1019,43 +1006,30 @@ class Meta(object): @receiver(post_save, sender=AlgorithmTagProposal) def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): - problem = instance.problem - tag = instance.tag - if created: - aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get_or_create( - problem=problem, tag=tag - )[0] - - aggregated_algorithm_tag_proposal.amount += 1 - aggregated_algorithm_tag_proposal.save() + AggregatedAlgorithmTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).update(amount=models.F('amount') + 1) \ + or \ + AggregatedAlgorithmTagProposal.objects.create( + problem=instance.problem, + tag=instance.tag, + amount=1 + ) @receiver(post_delete, sender=AlgorithmTagProposal) def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): - problem = instance.problem - tag = instance.tag - - try: - aggregated_algorithm_tag_proposal = AggregatedAlgorithmTagProposal.objects.get(problem=problem, tag=tag) - aggregated_algorithm_tag_proposal.amount -= 1 - - if aggregated_algorithm_tag_proposal.amount == 0: - aggregated_algorithm_tag_proposal.delete() - else: - try: - aggregated_algorithm_tag_proposal.save() - except ValidationError as e: - logger.exception( - "AggregatedAlgorithmTagProposal and deleted AlgorithmTagProposal " - "were out of sync - likely AggregatedAlgorithmTagProposal.amount " - "decreased below 0." - ) - except AggregatedAlgorithmTagProposal.DoesNotExist: - logger.exception( - "AggregatedAlgorithmTagProposal corresponding to deleted AlgorithmTagProposal " - "does not exist." - ) + AggregatedAlgorithmTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ + or \ + AggregatedAlgorithmTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).delete() class AggregatedAlgorithmTagProposal(models.Model): From 21243d05ca67a747d39be982be877bcbc06609cf Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:03:39 +0100 Subject: [PATCH 20/30] Add atomicity to increase and decrease functions. --- oioioi/problems/models.py | 76 ++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 4c1b0bfe0..c2cba0d88 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -890,29 +890,31 @@ class Meta(object): @receiver(post_save, sender=DifficultyTagProposal) def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwargs): if created: + with transaction.atomic(): + AggregatedDifficultyTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).update(amount=models.F('amount') + 1) \ + or \ + AggregatedDifficultyTagProposal.objects.create( + problem=instance.problem, + tag=instance.tag, + amount=1 + ) + + +@receiver(post_delete, sender=DifficultyTagProposal) +def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): + with transaction.atomic(): AggregatedDifficultyTagProposal.objects.filter( problem=instance.problem, tag=instance.tag - ).update(amount=models.F('amount') + 1) \ + ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ or \ - AggregatedDifficultyTagProposal.objects.create( + AggregatedDifficultyTagProposal.objects.filter( problem=instance.problem, - tag=instance.tag, - amount=1 - ) - - -@receiver(post_delete, sender=DifficultyTagProposal) -def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): - AggregatedDifficultyTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ - or \ - AggregatedDifficultyTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).delete() + tag=instance.tag + ).delete() class AggregatedDifficultyTagProposal(models.Model): @@ -1007,29 +1009,31 @@ class Meta(object): @receiver(post_save, sender=AlgorithmTagProposal) def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): if created: + with transaction.atomic(): + AggregatedAlgorithmTagProposal.objects.filter( + problem=instance.problem, + tag=instance.tag + ).update(amount=models.F('amount') + 1) \ + or \ + AggregatedAlgorithmTagProposal.objects.create( + problem=instance.problem, + tag=instance.tag, + amount=1 + ) + + +@receiver(post_delete, sender=AlgorithmTagProposal) +def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): + with transaction.atomic(): AggregatedAlgorithmTagProposal.objects.filter( problem=instance.problem, tag=instance.tag - ).update(amount=models.F('amount') + 1) \ + ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ or \ - AggregatedAlgorithmTagProposal.objects.create( + AggregatedAlgorithmTagProposal.objects.filter( problem=instance.problem, - tag=instance.tag, - amount=1 - ) - - -@receiver(post_delete, sender=AlgorithmTagProposal) -def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): - AggregatedAlgorithmTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ - or \ - AggregatedAlgorithmTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).delete() + tag=instance.tag + ).delete() class AggregatedAlgorithmTagProposal(models.Model): From adce79fb766a4aaba188f02a8753eb0b6dd3e5fb Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:10:37 +0100 Subject: [PATCH 21/30] Avoided repeating code through making generic function that increases tag proposals for given aggregated model. --- oioioi/problems/models.py | 84 ++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index c2cba0d88..0b65cb4f1 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -887,36 +887,6 @@ class Meta(object): -@receiver(post_save, sender=DifficultyTagProposal) -def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwargs): - if created: - with transaction.atomic(): - AggregatedDifficultyTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).update(amount=models.F('amount') + 1) \ - or \ - AggregatedDifficultyTagProposal.objects.create( - problem=instance.problem, - tag=instance.tag, - amount=1 - ) - - -@receiver(post_delete, sender=DifficultyTagProposal) -def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): - with transaction.atomic(): - AggregatedDifficultyTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ - or \ - AggregatedDifficultyTagProposal.objects.filter( - problem=instance.problem, - tag=instance.tag - ).delete() - - class AggregatedDifficultyTagProposal(models.Model): problem = models.ForeignKey('Problem', on_delete=models.CASCADE) tag = models.ForeignKey('DifficultyTag', on_delete=models.CASCADE) @@ -1006,45 +976,61 @@ class Meta(object): verbose_name_plural = _("algorithm tag proposals") -@receiver(post_save, sender=AlgorithmTagProposal) -def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): + +class AggregatedAlgorithmTagProposal(models.Model): + problem = models.ForeignKey('Problem', on_delete=models.CASCADE) + tag = models.ForeignKey('AlgorithmTag', on_delete=models.CASCADE) + amount = models.PositiveIntegerField(default=0) + + def __str__(self): + return str(self.problem.name) + u' -- ' + str(self.tag.name) + u' -- ' + str(self.amount) + + class Meta: + verbose_name = _("aggregated algorithm tag proposal") + verbose_name_plural = _("aggregated algorithm tag proposals") + unique_together = ('problem', 'tag') + + +def increase_aggregated_tag_proposal(sender, instance, created, aggregated_model, **kwargs): if created: with transaction.atomic(): - AggregatedAlgorithmTagProposal.objects.filter( + aggregated_model.objects.filter( problem=instance.problem, tag=instance.tag ).update(amount=models.F('amount') + 1) \ or \ - AggregatedAlgorithmTagProposal.objects.create( + aggregated_model.objects.create( problem=instance.problem, tag=instance.tag, amount=1 ) +@receiver(post_save, sender=AlgorithmTagProposal) +def increase_aggregated_algorithm_tag_proposal(sender, instance, created, **kwargs): + increase_aggregated_tag_proposal(sender, instance, created, AggregatedAlgorithmTagProposal, **kwargs) -@receiver(post_delete, sender=AlgorithmTagProposal) -def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): +@receiver(post_save, sender=DifficultyTagProposal) +def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwargs): + increase_aggregated_tag_proposal(sender, instance, created, AggregatedDifficultyTagProposal, **kwargs) + + +def decrease_aggregated_tag_proposal(sender, instance, aggregated_model, **kwargs): with transaction.atomic(): - AggregatedAlgorithmTagProposal.objects.filter( + aggregated_model.objects.filter( problem=instance.problem, tag=instance.tag ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ or \ - AggregatedAlgorithmTagProposal.objects.filter( + aggregated_model.objects.filter( problem=instance.problem, tag=instance.tag ).delete() -class AggregatedAlgorithmTagProposal(models.Model): - problem = models.ForeignKey('Problem', on_delete=models.CASCADE) - tag = models.ForeignKey('AlgorithmTag', on_delete=models.CASCADE) - amount = models.PositiveIntegerField(default=0) - - def __str__(self): - return str(self.problem.name) + u' -- ' + str(self.tag.name) + u' -- ' + str(self.amount) +@receiver(post_delete, sender=AlgorithmTagProposal) +def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs): + decrease_aggregated_tag_proposal(sender, instance, AggregatedAlgorithmTagProposal, **kwargs) - class Meta: - verbose_name = _("aggregated algorithm tag proposal") - verbose_name_plural = _("aggregated algorithm tag proposals") - unique_together = ('problem', 'tag') +@receiver(post_delete, sender=DifficultyTagProposal) +def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs): + decrease_aggregated_tag_proposal(sender, instance, AggregatedDifficultyTagProposal, **kwargs) \ No newline at end of file From 34e786a8c033396d272828d7035c5adccdf9ab75 Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:17:39 +0100 Subject: [PATCH 22/30] Add exception logging for decrease_aggregated_tag_proposal function. --- oioioi/problems/models.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index 0b65cb4f1..c67b29a5c 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -1015,16 +1015,24 @@ def increase_aggregated_difficulty_tag_proposal(sender, instance, created, **kwa def decrease_aggregated_tag_proposal(sender, instance, aggregated_model, **kwargs): - with transaction.atomic(): - aggregated_model.objects.filter( - problem=instance.problem, - tag=instance.tag - ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ - or \ - aggregated_model.objects.filter( - problem=instance.problem, - tag=instance.tag - ).delete() + try: + with transaction.atomic(): + aggregated_model.objects.filter( + problem=instance.problem, + tag=instance.tag + ).filter(amount__gt=1).update(amount=models.F('amount') - 1) \ + or \ + aggregated_model.objects.filter( + problem=instance.problem, + tag=instance.tag + ).delete() + + except Exception as e: + logger.exception( + "Error decreasing aggregated tag proposal for problem %s and tag %s.", + instance.problem, + instance.tag + ) @receiver(post_delete, sender=AlgorithmTagProposal) From 29aec77f57e505d5206a79efd642c34357db1eaa Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:40:07 +0100 Subject: [PATCH 23/30] Simplify tests testing Aggregated Tag Proposals with helper functions. --- oioioi/problems/tests/test_data_migrations.py | 47 +++++-------- oioioi/problems/tests/test_tag_proposals.py | 70 ++++++------------- 2 files changed, 37 insertions(+), 80 deletions(-) diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py index cb96ec5a9..9c7178095 100644 --- a/oioioi/problems/tests/test_data_migrations.py +++ b/oioioi/problems/tests/test_data_migrations.py @@ -19,6 +19,13 @@ migration_module = importlib.import_module('oioioi.problems.migrations.0033_populate_aggregated_tag_proposals') populate_aggregated_tag_proposals = getattr(migration_module, 'populate_aggregated_tag_proposals') +def _get_tag_amounts(aggregated_model, problem): + """Returns a dictionary mapping tag names to their amounts for a given problem.""" + return { + proposal.tag: proposal.amount + for proposal in aggregated_model.objects.filter(problem=problem) + } + class PopulateAggregatedTagProposalsTest(TestCase): fixtures = [ 'test_users', @@ -56,50 +63,30 @@ def setUp(self): ]) def test_populate_aggregated_tag_proposals(self): - AggregatedDifficultyTagProposal.objects.filter(problem=self.problem1).delete() AggregatedAlgorithmTagProposal.objects.filter(problem=self.problem2).delete() + AggregatedDifficultyTagProposal.objects.filter(problem=self.problem1).delete() populate_aggregated_tag_proposals(apps, None) self.assertEqual(AlgorithmTagProposal.objects.count(), 7) - self.assertEqual(DifficultyTagProposal.objects.count(), 4) - self.assertEqual(AggregatedAlgorithmTagProposal.objects.count(), 4) self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=self.problem1, tag=self.algorithm_tag1 - ).amount, 2 + _get_tag_amounts(AggregatedAlgorithmTagProposal, self.problem1), + {self.algorithm_tag1: 2, self.algorithm_tag2: 1} ) self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=self.problem1, tag=self.algorithm_tag2 - ).amount, 1 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=self.problem2, tag=self.algorithm_tag1 - ).amount, 1 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=self.problem2, tag=self.algorithm_tag2 - ).amount, 3 + _get_tag_amounts(AggregatedAlgorithmTagProposal, self.problem2), + {self.algorithm_tag1: 1, self.algorithm_tag2: 3} ) + self.assertEqual(DifficultyTagProposal.objects.count(), 4) self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 3) self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=self.problem1, tag=self.difficulty_tag1 - ).amount, 2 + _get_tag_amounts(AggregatedDifficultyTagProposal, self.problem1), + {self.difficulty_tag1: 2, self.difficulty_tag2: 1} ) self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=self.problem1, tag=self.difficulty_tag2 - ).amount, 1 - ) - self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=self.problem2, tag=self.difficulty_tag2 - ).amount, 1 + _get_tag_amounts(AggregatedDifficultyTagProposal, self.problem2), + {self.difficulty_tag2: 1} ) \ No newline at end of file diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index da82778c2..8069914be 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -132,6 +132,14 @@ def test_tag_proposal_hints_view(self): self.assertNotContains(response, 'lcis') + +def _get_tag_name_amounts(aggregated_model, problem): + """Returns a dictionary mapping tag names to their amounts for a given problem.""" + return { + proposal.tag.name: proposal.amount + for proposal in aggregated_model.objects.filter(problem=problem) + } + class TestSaveProposals(TestCase): fixtures = [ 'test_users', @@ -177,15 +185,8 @@ def test_save_proposals_view(self): ).exists() ) self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, tag=AlgorithmTag.objects.get(name='dp') - ).amount, 1 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, - tag=AlgorithmTag.objects.get(name='knapsack'), - ).amount, 1 + _get_tag_name_amounts(AggregatedAlgorithmTagProposal, problem), + {'dp': 1, 'knapsack': 1}, ) self.assertTrue( DifficultyTagProposal.objects.filter( @@ -193,9 +194,8 @@ def test_save_proposals_view(self): ).exists() ) self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='easy') - ).amount, 1 + _get_tag_name_amounts(AggregatedDifficultyTagProposal, problem), + {'easy': 1}, ) problem = Problem.objects.get(pk=0) @@ -233,25 +233,8 @@ def test_save_proposals_view(self): ).exists() ) self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, tag=AlgorithmTag.objects.get(name='greedy') - ).amount, 1 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, tag=AlgorithmTag.objects.get(name='lcis') - ).amount, 1 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, tag=AlgorithmTag.objects.get(name='dp') - ).amount, 2 - ) - self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, - tag=AlgorithmTag.objects.get(name='knapsack'), - ).amount, 1 + _get_tag_name_amounts(AggregatedAlgorithmTagProposal, problem), + {'dp': 2, 'knapsack': 1, 'greedy': 1, 'lcis': 1}, ) self.assertTrue( DifficultyTagProposal.objects.filter( @@ -259,14 +242,8 @@ def test_save_proposals_view(self): ).exists() ) self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='medium') - ).amount, 1 - ) - self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='easy') - ).amount, 1 + _get_tag_name_amounts(AggregatedDifficultyTagProposal, problem), + {'easy': 1, 'medium': 1}, ) problem = Problem.objects.get(pk=0) @@ -287,9 +264,8 @@ def test_save_proposals_view(self): self.assertEqual(DifficultyTagProposal.objects.count(), 3) self.assertEqual(AggregatedDifficultyTagProposal.objects.count(), 2) self.assertEqual( - AggregatedAlgorithmTagProposal.objects.get( - problem=problem, tag=AlgorithmTag.objects.get(name='greedy') - ).amount, 2 + _get_tag_name_amounts(AggregatedAlgorithmTagProposal, problem), + {'dp': 2, 'knapsack': 1, 'greedy': 2, 'lcis': 1}, ) self.assertTrue( DifficultyTagProposal.objects.filter( @@ -297,14 +273,8 @@ def test_save_proposals_view(self): ).exists() ) self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='medium') - ).amount, 2 - ) - self.assertEqual( - AggregatedDifficultyTagProposal.objects.get( - problem=problem, tag=DifficultyTag.objects.get(name='easy') - ).amount, 1 + _get_tag_name_amounts(AggregatedDifficultyTagProposal, problem), + {'easy': 1, 'medium': 2}, ) invalid_query_data = [ From d269154c029c923e1fdf593c3d2b8e1271046730 Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:45:04 +0100 Subject: [PATCH 24/30] Fix comment explaining _get_tag_amounts helper function. --- oioioi/problems/tests/test_data_migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py index 9c7178095..8067a46e2 100644 --- a/oioioi/problems/tests/test_data_migrations.py +++ b/oioioi/problems/tests/test_data_migrations.py @@ -20,7 +20,7 @@ populate_aggregated_tag_proposals = getattr(migration_module, 'populate_aggregated_tag_proposals') def _get_tag_amounts(aggregated_model, problem): - """Returns a dictionary mapping tag names to their amounts for a given problem.""" + """Returns a dictionary mapping tags to their amounts for a given problem.""" return { proposal.tag: proposal.amount for proposal in aggregated_model.objects.filter(problem=problem) From 8ee264f1ad9a49f193bdeda1d5b63250525320cf Mon Sep 17 00:00:00 2001 From: segir187 Date: Mon, 9 Dec 2024 02:53:23 +0100 Subject: [PATCH 25/30] Remove needles whitespaces and end of lines. --- oioioi/problems/tests/test_data_migrations.py | 3 +-- oioioi/problems/tests/test_tag_proposals.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/oioioi/problems/tests/test_data_migrations.py b/oioioi/problems/tests/test_data_migrations.py index 8067a46e2..35e42a2ba 100644 --- a/oioioi/problems/tests/test_data_migrations.py +++ b/oioioi/problems/tests/test_data_migrations.py @@ -22,7 +22,7 @@ def _get_tag_amounts(aggregated_model, problem): """Returns a dictionary mapping tags to their amounts for a given problem.""" return { - proposal.tag: proposal.amount + proposal.tag: proposal.amount for proposal in aggregated_model.objects.filter(problem=problem) } @@ -89,4 +89,3 @@ def test_populate_aggregated_tag_proposals(self): _get_tag_amounts(AggregatedDifficultyTagProposal, self.problem2), {self.difficulty_tag2: 1} ) - \ No newline at end of file diff --git a/oioioi/problems/tests/test_tag_proposals.py b/oioioi/problems/tests/test_tag_proposals.py index 8069914be..74f472da9 100644 --- a/oioioi/problems/tests/test_tag_proposals.py +++ b/oioioi/problems/tests/test_tag_proposals.py @@ -136,7 +136,7 @@ def test_tag_proposal_hints_view(self): def _get_tag_name_amounts(aggregated_model, problem): """Returns a dictionary mapping tag names to their amounts for a given problem.""" return { - proposal.tag.name: proposal.amount + proposal.tag.name: proposal.amount for proposal in aggregated_model.objects.filter(problem=problem) } From 5cdafcafcaa4967e545cc087989a198f48040182 Mon Sep 17 00:00:00 2001 From: Grzegorz Krawczyk Date: Mon, 9 Dec 2024 17:25:18 +0100 Subject: [PATCH 26/30] Change tag proposals in problem_site_settings functions to pull data from aggregated tag models. --- oioioi/problems/problem_site.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/oioioi/problems/problem_site.py b/oioioi/problems/problem_site.py index 4c3ca539d..18fad2859 100644 --- a/oioioi/problems/problem_site.py +++ b/oioioi/problems/problem_site.py @@ -28,6 +28,8 @@ from oioioi.problems.models import ( AlgorithmTagProposal, DifficultyTagProposal, + AggregatedAlgorithmTagProposal, + AggregatedDifficultyTagProposal, Problem, ProblemAttachment, ProblemPackage, @@ -250,10 +252,10 @@ def problem_site_settings(request, problem): model_solutions = generate_model_solutions_context(request, problem_instance) extra_actions = problem.controller.get_extra_problem_site_actions(problem) algorithm_tag_proposals = ( - AlgorithmTagProposal.objects.all().filter(problem=problem).order_by('-pk')[:25] + AggregatedAlgorithmTagProposal.objects.all().filter(problem=problem).order_by('-amount')[:25] ) difficulty_tag_proposals = ( - DifficultyTagProposal.objects.all().filter(problem=problem).order_by('-pk')[:25] + AggregatedDifficultyTagProposal.objects.all().filter(problem=problem).order_by('-amount')[:25] ) return TemplateResponse( From 93ac2d8b19fcc1f8a3d3dbdca16b0d6e54a325b5 Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 15 Dec 2024 22:16:49 +0100 Subject: [PATCH 27/30] Change style of display for proposed tags. --- oioioi/base/utils/tags.py | 8 ++++++++ oioioi/problems/static/common/base.scss | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/oioioi/base/utils/tags.py b/oioioi/base/utils/tags.py index a672c37cc..523ca53c7 100644 --- a/oioioi/base/utils/tags.py +++ b/oioioi/base/utils/tags.py @@ -6,6 +6,10 @@ def get_tag_prefix(tag): 'OriginInfoValue': 'origin', 'AlgorithmTag': 'algorithm', 'DifficultyTag': 'difficulty', + 'AlgorithmTagProposal': 'algorithm-proposal', + 'DifficultyTagProposal': 'difficulty-proposal', + 'AggregatedAlgorithmTagProposal': 'algorithm-proposal', + 'AggregatedDifficultyTagProposal': 'difficulty-proposal', } return prefixes[tag.__class__.__name__] @@ -19,6 +23,10 @@ def get_tag_name(tag): 'OriginInfoValue': tag.name, 'AlgorithmTag': tag.name, 'DifficultyTag': tag.full_name, + 'AlgorithmTagProposal': tag.tag.name, + 'DifficultyTagProposal': tag.tag.name, + 'AggregatedAlgorithmTagProposal': tag.tag.name, + 'AggregatedDifficultyTagProposal': tag.tag.name, } return prefixes[tag.__class__.__name__] diff --git a/oioioi/problems/static/common/base.scss b/oioioi/problems/static/common/base.scss index 0ff4e4480..c9646e8c3 100644 --- a/oioioi/problems/static/common/base.scss +++ b/oioioi/problems/static/common/base.scss @@ -58,3 +58,13 @@ background-color: #2DB941; color: #fff; } + +.tag-label-algorithm-proposal { + background-color: #91c5f5; + color: #000; +} + +.tag-label-difficulty-proposal { + background-color: #90EE90; + color: #000; +} \ No newline at end of file From 0d324380350fd11fa58600921ee319f92586684e Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 15 Dec 2024 22:17:21 +0100 Subject: [PATCH 28/30] Change way of displaying aggregated tag proposals. --- .../templates/problems/ingredients/tags-panel.html | 4 ++-- oioioi/problems/templatetags/tag.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/oioioi/problems/templates/problems/ingredients/tags-panel.html b/oioioi/problems/templates/problems/ingredients/tags-panel.html index d14c7ad5e..969041914 100644 --- a/oioioi/problems/templates/problems/ingredients/tags-panel.html +++ b/oioioi/problems/templates/problems/ingredients/tags-panel.html @@ -17,7 +17,7 @@

{% trans "Current tags" %}


{% trans "Users algorithms proposals" %}

{% for proposal in algorithm_tag_proposals %} - {% tag_label proposal.tag %} + {% aggregated_tag_label proposal %} {% endfor %} {% endif %} @@ -25,7 +25,7 @@

{% trans "Users algorithms proposals" %}


{% trans "Users difficulty proposals" %}

{% for proposal in difficulty_tag_proposals %} - {% tag_label proposal.tag %} + {% aggregated_tag_label proposal %} {% endfor %} {% endif %} diff --git a/oioioi/problems/templatetags/tag.py b/oioioi/problems/templatetags/tag.py index 4812d67bb..b244c6488 100644 --- a/oioioi/problems/templatetags/tag.py +++ b/oioioi/problems/templatetags/tag.py @@ -32,6 +32,20 @@ def tag_label(tag): ) +@register.simple_tag +def aggregated_tag_label(aggregated_tag): + prefix = get_tag_prefix(aggregated_tag) + return format_html( + u'{name} | {amount}', + tooltip=getattr(aggregated_tag.tag, 'full_name', aggregated_tag.tag.name), + name=get_tag_name(aggregated_tag.tag), + cls=prefix, + amount=str(aggregated_tag.amount), + href="?" + prefix + "=" + aggregated_tag.tag.name, + ) + + @register.simple_tag def origininfo_label(info): prefix = get_tag_prefix(info) From d4cd990b49ba53a78bc840a0b971f5f4d9b12110 Mon Sep 17 00:00:00 2001 From: segir187 Date: Sun, 15 Dec 2024 22:20:24 +0100 Subject: [PATCH 29/30] Remove not working intended generalisation of get_tag_name function. --- oioioi/base/utils/tags.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/oioioi/base/utils/tags.py b/oioioi/base/utils/tags.py index 523ca53c7..7e17debe9 100644 --- a/oioioi/base/utils/tags.py +++ b/oioioi/base/utils/tags.py @@ -23,10 +23,6 @@ def get_tag_name(tag): 'OriginInfoValue': tag.name, 'AlgorithmTag': tag.name, 'DifficultyTag': tag.full_name, - 'AlgorithmTagProposal': tag.tag.name, - 'DifficultyTagProposal': tag.tag.name, - 'AggregatedAlgorithmTagProposal': tag.tag.name, - 'AggregatedDifficultyTagProposal': tag.tag.name, } return prefixes[tag.__class__.__name__] From fb3b5945c16b8e0f155bdb213560875a27e82adf Mon Sep 17 00:00:00 2001 From: Grzegorz Krawczyk Date: Mon, 20 Jan 2025 17:33:31 +0100 Subject: [PATCH 30/30] Remove needles u before strings. --- oioioi/problems/templatetags/tag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oioioi/problems/templatetags/tag.py b/oioioi/problems/templatetags/tag.py index b244c6488..e614c4454 100644 --- a/oioioi/problems/templatetags/tag.py +++ b/oioioi/problems/templatetags/tag.py @@ -23,7 +23,7 @@ def prefetch_tags(problems): def tag_label(tag): prefix = get_tag_prefix(tag) return format_html( - u'{name}', tooltip=getattr(tag, 'full_name', tag.name), name=get_tag_name(tag), @@ -36,7 +36,7 @@ def tag_label(tag): def aggregated_tag_label(aggregated_tag): prefix = get_tag_prefix(aggregated_tag) return format_html( - u'{name} | {amount}', tooltip=getattr(aggregated_tag.tag, 'full_name', aggregated_tag.tag.name), name=get_tag_name(aggregated_tag.tag), @@ -50,7 +50,7 @@ def aggregated_tag_label(aggregated_tag): def origininfo_label(info): prefix = get_tag_prefix(info) return format_html( - u'{name}', tooltip=info.full_name, name=info.value,