Skip to content

Commit

Permalink
Aggregated tag proposals (#431)
Browse files Browse the repository at this point in the history
* Add new AggregatedAlgorithmTagProposal class.

* Make AggregatedAlgorithmTagProposal increase each time a new AlgorithmTagProposal is created. This is most likely a temporary implementation.

* Change handling of modification of AggregatedAlgorithmTagProposal to happen when adding or deleting AlgorithmTagProposals.

* 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.

* 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.

* Expand test_save_proposals_view in TestSaveProposals class to also check AggregatedAlgorithmTagProposal.

* Add AggregatedDifficultyTagProposal.

* Add checking AggregatedDifficultyTagProposal in test_save_poposals_view in TestTagProposals class. Fix mistaken 'assertEquals' to 'assertEqual'.

* Migrate AggregatedTagProposal classes.

* Fixed test_tag_proposals.py to work correctly.

* Remove unnecessary import.

* Add data migration from TagProposal models to AggregatedTagProposal models.

* Remove unnecessary import.

* Initial version of test_data_migrations.py. It should be restructured.

* Change import of migration function from static to dynamic.

* Change user primary keys to keys corresponding to valid users from test_users fixture.

* Change usages of creates in for loops to bulk_creates.

* Change exceptions raised to logged messages.

* Make increase_aggregated and decrease_aggregated functions more compact.

* Add atomicity to increase and decrease functions.

* Avoided repeating code through making generic function that increases tag proposals for given aggregated model.

* Add exception logging for decrease_aggregated_tag_proposal function.

* Simplify tests testing Aggregated Tag Proposals with helper functions.

* Fix comment explaining _get_tag_amounts helper function.

* Remove needles whitespaces and end of lines.
  • Loading branch information
segir187 authored Dec 16, 2024
1 parent 9392524 commit ae10597
Show file tree
Hide file tree
Showing 6 changed files with 573 additions and 193 deletions.
42 changes: 42 additions & 0 deletions oioioi/problems/migrations/0032_aggregated_tag_proposals.py
Original file line number Diff line number Diff line change
@@ -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')},
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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'))
)
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'))
)
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):

dependencies = [
('problems', '0032_aggregated_tag_proposals'),
]

operations = [
migrations.RunPython(populate_aggregated_tag_proposals)
]
86 changes: 85 additions & 1 deletion oioioi/problems/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
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
Expand Down Expand Up @@ -885,6 +886,21 @@ class Meta(object):
verbose_name_plural = _("difficulty proposals")



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):
Expand Down Expand Up @@ -958,3 +974,71 @@ 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')


def increase_aggregated_tag_proposal(sender, instance, created, aggregated_model, **kwargs):
if created:
with transaction.atomic():
aggregated_model.objects.filter(
problem=instance.problem,
tag=instance.tag
).update(amount=models.F('amount') + 1) \
or \
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_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):
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)
def decrease_aggregated_algorithm_tag_proposal(sender, instance, **kwargs):
decrease_aggregated_tag_proposal(sender, instance, AggregatedAlgorithmTagProposal, **kwargs)

@receiver(post_delete, sender=DifficultyTagProposal)
def decrease_aggregated_difficulty_tag_proposal(sender, instance, **kwargs):
decrease_aggregated_tag_proposal(sender, instance, AggregatedDifficultyTagProposal, **kwargs)
91 changes: 91 additions & 0 deletions oioioi/problems/tests/test_data_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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,
)
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')

def _get_tag_amounts(aggregated_model, 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)
}

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=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),
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):
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(AggregatedAlgorithmTagProposal.objects.count(), 4)
self.assertEqual(
_get_tag_amounts(AggregatedAlgorithmTagProposal, self.problem1),
{self.algorithm_tag1: 2, self.algorithm_tag2: 1}
)
self.assertEqual(
_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(
_get_tag_amounts(AggregatedDifficultyTagProposal, self.problem1),
{self.difficulty_tag1: 2, self.difficulty_tag2: 1}
)
self.assertEqual(
_get_tag_amounts(AggregatedDifficultyTagProposal, self.problem2),
{self.difficulty_tag2: 1}
)
Loading

0 comments on commit ae10597

Please sign in to comment.