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']