diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index 67a59ac09..066a2e552 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -865,6 +865,7 @@ FORUM_THREADS_PER_PAGE = 30 FORUM_POSTS_PER_PAGE = 30 FORUM_POST_MAX_LENGTH = 20000 +FORUM_REACTIONS_TO_DISPLAY = 10 # Check seems to be broken. https://stackoverflow.com/a/65578574 SILENCED_SYSTEM_CHECKS = ['admin.E130'] diff --git a/oioioi/forum/models.py b/oioioi/forum/models.py index 0d78c8d87..debb68276 100644 --- a/oioioi/forum/models.py +++ b/oioioi/forum/models.py @@ -6,6 +6,7 @@ from django.dispatch import receiver from django.urls import reverse from django.utils import timezone +from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -192,22 +193,29 @@ class Post(models.Model): class PostsWithReactionsSummaryManager(models.Manager): def get_queryset(self): qs = super(Post.PostsWithReactionsSummaryManager, self).get_queryset() - for field_name, rtype in [ - ('upvotes_count', 'UPVOTE'), - ('downvotes_count', 'DOWNVOTE'), - ]: - # In Django >=2.0 it can can be simplified with Count(filter=Q(...)) + + for rtype, attr_name in POST_REACTION_TO_COUNT_ATTR.items(): reaction_count_agg = { - field_name: models.Sum( - models.Case( - models.When(reactions__type_of_reaction=rtype, then=1), - default=0, - output_field=models.IntegerField(), - ) + attr_name: models.Count( + 'reactions', + filter=models.Q(reactions__type_of_reaction=rtype) ) } qs = qs.annotate(**reaction_count_agg) + max_count = getattr(settings, 'FORUM_REACTIONS_TO_DISPLAY', 10) + for rtype, attr_name in POST_REACTION_TO_PREFETCH_ATTR.items(): + qs = qs.prefetch_related( + models.Prefetch( + 'reactions', + to_attr=attr_name, + queryset=PostReaction.objects + .filter(type_of_reaction=rtype) + .order_by('-pk') + .select_related('author')[:max_count], + ) + ) + return qs objects = PostsWithReactionsSummaryManager() @@ -256,6 +264,15 @@ def is_reporter_banned(self): return Ban.is_banned(self.thread.category.forum, self.reported_by) +POST_REACTION_TO_COUNT_ATTR = { + "UPVOTE": "upvotes_count", + "DOWNVOTE": "downvotes_count", +} + +POST_REACTION_TO_PREFETCH_ATTR = { + "UPVOTE": "upvoted_by", + "DOWNVOTE": "downvoted_by", +} post_reaction_types = EnumRegistry( entries=[ diff --git a/oioioi/forum/templates/forum/thread-element-footer.html b/oioioi/forum/templates/forum/thread-element-footer.html index cb59e1c22..a8548b7d3 100644 --- a/oioioi/forum/templates/forum/thread-element-footer.html +++ b/oioioi/forum/templates/forum/thread-element-footer.html @@ -1,6 +1,7 @@ {% load i18n %} {% load check_perm %} {% load get_user_name %} +{% load display_reacted_by %} {% load user_badge %} {% url 'forum_post_edit' contest_id=contest.id category_id=category.id thread_id=thread.id post_id=post.id as forum_post_edit_url %} @@ -18,9 +19,14 @@ {% if can_interact_with_users %} {% endif %} - + + {{ post.upvotes_count }} + {% if can_interact_with_users %} {% endif %} @@ -28,9 +34,14 @@ {% if can_interact_with_users %} {% endif %} - + + {{ post.downvotes_count }} + {% if can_interact_with_users %} {% endif %} diff --git a/oioioi/forum/templatetags/display_reacted_by.py b/oioioi/forum/templatetags/display_reacted_by.py new file mode 100644 index 000000000..cd774aa32 --- /dev/null +++ b/oioioi/forum/templatetags/display_reacted_by.py @@ -0,0 +1,25 @@ +from django import template +from django.utils.translation import gettext as _ +from oioioi.base.utils import get_user_display_name +from oioioi.forum.models import POST_REACTION_TO_PREFETCH_ATTR, POST_REACTION_TO_COUNT_ATTR +from django.conf import settings + +register = template.Library() + +@register.simple_tag +def display_reacted_by(post, rtype): + if(rtype not in POST_REACTION_TO_PREFETCH_ATTR): + raise ValueError('Invalid reaction type in template:' + rtype) + + output = ', '.join([ + get_user_display_name(reaction.author) + for reaction in getattr(post, POST_REACTION_TO_PREFETCH_ATTR[rtype]) + ]) + + count = getattr(post, POST_REACTION_TO_COUNT_ATTR[rtype]) + max_count = getattr(settings, 'FORUM_REACTIONS_TO_DISPLAY', 10) + + if(count > max_count): + output += ' ' + _('and others.') + + return output \ No newline at end of file diff --git a/oioioi/forum/tests.py b/oioioi/forum/tests.py index 604eda64c..b6cb3f792 100644 --- a/oioioi/forum/tests.py +++ b/oioioi/forum/tests.py @@ -402,6 +402,31 @@ def test_paging(self): posts_on_last_page, ) + def test_reaction_display(self): + p = Post( + thread=self.thread, + content='test', + author=self.user, + ) + p.save() + + PostReaction( + post_id=p.id, + type_of_reaction='UPVOTE', + author=self.user + ).save() + + response = self.client.get(self.url, follow=True) + self.assertNotContains(response, 'post_reactions') + self.assertNotContains(response, 'title="Test User"') + + self.cat.reactions_enabled = True + self.cat.save() + + response = self.client.get(self.url, follow=True) + self.assertContains(response, 'post_reactions') + self.assertContains(response, 'title="Test User"') + class TestPost(TestCase): fixtures = ['test_users', 'test_contest'] @@ -723,6 +748,38 @@ def count_reactions(r): self.assertEqual(1, count_reactions('DOWNVOTE')) self.assertEqual(1, self.p.reactions.count()) + def test_reacted_by(self): + react_url = self.reverse_post('forum_post_toggle_reaction') + upvote_url = react_url + '?reaction=upvote' + + self.cat.reactions_enabled = True + self.cat.save() + + self.assertTrue(self.client.login(username='test_user')) + self.client.post(upvote_url, follow=True) + self.assertTrue(self.client.login(username='test_user2')) + self.client.post(upvote_url, follow=True) + + response = self.client.get(self.thread_url, follow=True) + self.assertContains(response, 'Test User 2, Test User') + + @override_settings(FORUM_REACTIONS_TO_DISPLAY=2) + def test_reacted_by_many_users(self): + react_url = self.reverse_post('forum_post_toggle_reaction') + upvote_url = react_url + '?reaction=upvote' + + self.cat.reactions_enabled = True + self.cat.save() + + self.assertTrue(self.client.login(username='test_user')) + self.client.post(upvote_url, follow=True) + self.assertTrue(self.client.login(username='test_user2')) + self.client.post(upvote_url, follow=True) + self.assertTrue(self.client.login(username='test_user3')) + self.client.post(upvote_url, follow=True) + + response = self.client.get(self.thread_url, follow=True) + self.assertContains(response, 'Test User 3, Test User 2 and others') class TestBan(TestCase): fixtures = ['test_users', 'test_contest']