Skip to content

Commit

Permalink
Merge pull request #217 from danirus/issue-106
Browse files Browse the repository at this point in the history
Issue 106
  • Loading branch information
danirus authored Sep 26, 2020
2 parents e93e3e5 + dde8f2c commit 27f8890
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 103 deletions.
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

0 comments on commit 27f8890

Please sign in to comment.