Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 106 #217

Merged
merged 13 commits into from
Sep 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# Change Log

## [] - YYYY-MM-DD

## [2.8.0] - 2020-09-26

* Fixes issue #106, which is about computing the number of nested comments
for every comment at every level down the tree. The fix consists of
adding a new field called 'nested_count' to the XtdComment model. Its
value represents the number of threaded comments under itself. A new
management command, 'initialize_nested_count', can be used to update the
value of the field, the command is idempotent. Two new migrations have
been added: migration 0007 adds the new field, and migration 0008 calls
the 'initialize_nested_count' command to populate the nested_count new
field with correct values.
* Fixes issue #215 about running the tests with Django 3.1 and Python 3.8.

## [2.7.2] - 2020-09-08

* Fixes issue #208, about the JavaScript plugin not displaying the like and
dislike buttons and the reply link when django-comments-xtd is setup to
allow posting comments only to registered users (who_can_post: "users").
allow posting comments only to registered users (who_can_post: "users").
* Fixes issue #212, about missing i18n JavaScript catalog files for Dutch,
German and Russian.

Expand All @@ -22,9 +31,9 @@

## [2.7.0] - 2020-08-09

* Enhancement, closing issue #155 (and #170), on how to post comments via
* Enhancement, closing issue #155 (and #170), on how to post comments via
the web API. Up until version 2.6.2 posting comments required the fields
timestamp, security_hash and honeypot. As of 2.7.0 there is support to
timestamp, security_hash and honeypot. As of 2.7.0 there is support to
allow Django REST Framework authentication classes: WriteCommentSerializer
send the signal should_request_be_authorize that enables posting comments.
Read the documentation about the web API.
Expand Down
6 changes: 3 additions & 3 deletions django_comments_xtd/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@


class XtdCommentsAdmin(CommentsAdmin):
list_display = ('thread_level', 'cid', 'name', 'content_type', 'object_pk',
'ip_address', 'submit_date', 'followup', 'is_public',
'is_removed')
list_display = ('cid', 'thread_level', 'nested_count', 'name',
'content_type', 'object_pk', 'ip_address', 'submit_date',
'followup', 'is_public', 'is_removed')
list_display_links = ('cid',)
list_filter = ('content_type', 'is_public', 'is_removed', 'followup')
fieldsets = (
Expand Down
48 changes: 48 additions & 0 deletions django_comments_xtd/management/commands/initialize_nested_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.db.utils import ConnectionDoesNotExist
from django.core.management.base import BaseCommand

from django_comments_xtd.models import XtdComment


class Command(BaseCommand):
help = "Initialize the nested_count field for all the comments in the DB."

def add_arguments(self, parser):
parser.add_argument('using', nargs='*', type=str)

def initialize_nested_count(self, using):
# Control break.
active_thread_id = -1
parents = {}

qs = XtdComment.objects.using(using).order_by('thread_id', '-order')

for comment in qs:
# Clean up parents when there is a control break.
if comment.thread_id != active_thread_id:
parents = {}
active_thread_id = comment.thread_id

nested_count = parents.get(comment.comment_ptr_id, 0)
parents.setdefault(comment.parent_id, 0)
if nested_count > 0:
parents[comment.parent_id] += 1 + nested_count
else:
parents[comment.parent_id] += 1
comment.nested_count = nested_count
comment.save()

return qs.count()

def handle(self, *args, **options):
total = 0
using = options['using'] or ['default']

for db_conn in using:
try:
total += self.initialize_nested_count(db_conn)
except ConnectionDoesNotExist:
self.stdout.write("DB connection '%s' does not exist." %
db_conn)
continue
self.stdout.write("Updated %d XtdComment object(s)." % total)
18 changes: 18 additions & 0 deletions django_comments_xtd/migrations/0007_xtdcomment_nested_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.0.1 on 2020-09-12 20:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_comments_xtd', '0006_auto_20181204_0948'),
]

operations = [
migrations.AddField(
model_name='xtdcomment',
name='nested_count',
field=models.IntegerField(db_index=True, default=0),
),
]
19 changes: 19 additions & 0 deletions django_comments_xtd/migrations/0008_auto_20200920_2037.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.0.1 on 2020-09-20 18:37
from django.core.management import call_command
from django.db import migrations


def populate_nested_count(*args):
call_command('initialize_nested_count')


class Migration(migrations.Migration):

dependencies = [
('django_comments_xtd', '0007_xtdcomment_nested_count'),
]

operations = [
migrations.RunPython(populate_nested_count,
reverse_code=migrations.RunPython.noop)
]
29 changes: 22 additions & 7 deletions django_comments_xtd/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def __str__(self):


class XtdCommentManager(CommentManager):

def for_app_models(self, *args, **kwargs):
"""Return XtdComments for pairs "app.model" given in args"""
content_types = []
Expand Down Expand Up @@ -67,6 +66,7 @@ class XtdComment(Comment):
order = models.IntegerField(default=1, db_index=True)
followup = models.BooleanField(blank=True, default=False,
help_text=_("Notify follow-up comments"))
nested_count = models.IntegerField(default=0, db_index=True)
objects = XtdCommentManager()

def save(self, *args, **kwargs):
Expand Down Expand Up @@ -99,14 +99,17 @@ def _calculate_thread_data(self):
order__gt=parent.order)
if qc_ge_level.count():
min_order = qc_ge_level.aggregate(Min('order'))['order__min']
XtdComment.objects.filter(thread_id=parent.thread_id,
order__gte=min_order)\
.update(order=F('order') + 1)
qc_eq_thread.filter(order__gte=min_order)\
.update(order=F('order') + 1)
self.order = min_order
else:
max_order = qc_eq_thread.aggregate(Max('order'))['order__max']
self.order = max_order + 1

qc_eq_thread.filter(Q(pk=parent.pk) | Q(level__lt=parent.level,
order__lt=parent.order))\
.update(nested_count=F('nested_count') + 1)

def get_reply_url(self):
return reverse("comments-xtd-reply", kwargs={"cid": self.pk})

Expand Down Expand Up @@ -206,21 +209,33 @@ def get_comment_dict(obj):
return dic_list


def publish_or_unpublish_nested_comments(comment_id, are_public=False):
qs = get_model().objects.filter(~Q(pk=comment_id), parent_id=comment_id)
def publish_or_unpublish_nested_comments(comment, are_public=False):
qs = get_model().objects.filter(~Q(pk=comment.id), parent_id=comment.id)
nested = [cm.id for cm in qs]
qs.update(is_public=are_public)
while len(nested):
cm_id = nested.pop()
qs = XtdComment.objects.filter(~Q(pk=cm_id), parent_id=cm_id)
nested.extend([cm.id for cm in qs])
qs.update(is_public=are_public)
# Update nested_count in parents comments in the same thread.
# The comment.nested_count doesn't change because the comment's is_public
# attribute is not changing, only its nested comments change, and it will
# help to re-populate nested_count should it be published again.
if are_public:
op = F('nested_count') + comment.nested_count
else:
op = F('nested_count') - comment.nested_count
XtdComment.objects.filter(thread_id=comment.thread_id,
level__lt=comment.level,
order__lt=comment.order)\
.update(nested_count=op)


def publish_or_unpublish_on_pre_save(sender, instance, raw, using, **kwargs):
if not raw and instance and instance.id:
are_public = (not instance.is_removed) and instance.is_public
publish_or_unpublish_nested_comments(instance.id, are_public=are_public)
publish_or_unpublish_nested_comments(instance, are_public=are_public)


# ----------------------------------------------------------------------
Expand Down
59 changes: 59 additions & 0 deletions django_comments_xtd/tests/test_cmd_initialize_nested_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from io import StringIO
from django.core.management import call_command
from django.test import TestCase

from django_comments_xtd.models import XtdComment
from django_comments_xtd.tests.models import Article
from django_comments_xtd.tests.test_models import (
thread_test_step_1, thread_test_step_2, thread_test_step_3,
thread_test_step_4, thread_test_step_5
)


class InitializeNesteCoundCmdTest(TestCase):
def setUp(self):
self.article_1 = Article.objects.create(
title="September", slug="september", body="During September...")
thread_test_step_1(self.article_1)
thread_test_step_2(self.article_1)
thread_test_step_3(self.article_1)
thread_test_step_4(self.article_1)
thread_test_step_5(self.article_1)
self.check_nested_count()

def check_nested_count(self):
( # content -> cmt.id thread_id parent_id level order nested
self.c1, # -> 1 1 1 0 1 4
self.c3, # -> 3 1 1 1 2 1
self.c8, # -> 8 1 3 2 3 0
self.c4, # -> 4 1 1 1 4 1
self.c7, # -> 7 1 4 2 5 0
self.c2, # -> 2 2 2 0 1 2
self.c5, # -> 5 2 2 1 2 1
self.c6, # -> 6 2 5 2 3 0
self.c9 # -> 9 9 9 0 1 0
) = XtdComment.objects.all()
self.assertEqual(self.c1.nested_count, 4)
self.assertEqual(self.c3.nested_count, 1)
self.assertEqual(self.c8.nested_count, 0)
self.assertEqual(self.c4.nested_count, 1)
self.assertEqual(self.c7.nested_count, 0)
self.assertEqual(self.c2.nested_count, 2)
self.assertEqual(self.c5.nested_count, 1)
self.assertEqual(self.c6.nested_count, 0)
self.assertEqual(self.c9.nested_count, 0)

def test_calling_command_computes_nested_count(self):
# Set all comments nested_count field to 0.
XtdComment.objects.update(nested_count=0)
out = StringIO()
call_command('initialize_nested_count', stdout=out)
self.assertIn("Updated 9 XtdComment object(s).", out.getvalue())
self.check_nested_count()

def test_command_is_idempotent(self):
out = StringIO()
call_command('initialize_nested_count', stdout=out)
call_command('initialize_nested_count', stdout=out)
self.assertIn("Updated 9 XtdComment object(s).", out.getvalue())
self.check_nested_count()
Loading