From 2adbb4f2f1f7d5b385fb0d74d11082013b863c4c Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Sat, 12 Sep 2020 22:40:49 +0200 Subject: [PATCH 01/13] New field nested_count to keep the count of the number of replies. --- django_comments_xtd/admin.py | 6 +++--- django_comments_xtd/models.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/django_comments_xtd/admin.py b/django_comments_xtd/admin.py index 7d23b48d..00f401bd 100644 --- a/django_comments_xtd/admin.py +++ b/django_comments_xtd/admin.py @@ -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 = ( diff --git a/django_comments_xtd/models.py b/django_comments_xtd/models.py index 2deee03b..24257ef8 100644 --- a/django_comments_xtd/models.py +++ b/django_comments_xtd/models.py @@ -67,6 +67,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): @@ -99,14 +100,19 @@ 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 + parent.nested_count = F('nested_count') + 1 + parent.save() + qc_eq_thread.filter(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}) From 0dbf2a04565437b0e55c4dfbab54e5ce4c82ae75 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Mon, 14 Sep 2020 20:58:02 +0200 Subject: [PATCH 02/13] Update nested_count when approving or withdrawing comments. --- django_comments_xtd/models.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/django_comments_xtd/models.py b/django_comments_xtd/models.py index 24257ef8..67826c5f 100644 --- a/django_comments_xtd/models.py +++ b/django_comments_xtd/models.py @@ -212,8 +212,8 @@ 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): @@ -221,12 +221,20 @@ def publish_or_unpublish_nested_comments(comment_id, are_public=False): 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. + 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) # ---------------------------------------------------------------------- From 52db8d88cacd79ad0d7dc91f007dfb7505797128 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Mon, 14 Sep 2020 22:40:18 +0200 Subject: [PATCH 03/13] Test the nested_count when inserting new comments or when removing. --- django_comments_xtd/models.py | 3 + django_comments_xtd/tests/test_models.py | 157 +++++++++++++---------- 2 files changed, 95 insertions(+), 65 deletions(-) diff --git a/django_comments_xtd/models.py b/django_comments_xtd/models.py index 67826c5f..e84fecd4 100644 --- a/django_comments_xtd/models.py +++ b/django_comments_xtd/models.py @@ -222,6 +222,9 @@ def publish_or_unpublish_nested_comments(comment, are_public=False): 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: diff --git a/django_comments_xtd/tests/test_models.py b/django_comments_xtd/tests/test_models.py index 67cbdff5..98f91db8 100644 --- a/django_comments_xtd/tests/test_models.py +++ b/django_comments_xtd/tests/test_models.py @@ -218,47 +218,52 @@ class BaseThreadStep1TestCase(ArticleBaseTestCase): def setUp(self): super(BaseThreadStep1TestCase, self).setUp() thread_test_step_1(self.article_1) - ( # content -> cmt.id thread_id parent_id level order - self.c1, # -> 1 1 1 0 1 - self.c2 # -> 2 2 2 0 1 + ( # content -> cmt.id thread_id parent_id level order nested + self.c1, # -> 1 1 1 0 1 0 + self.c2 # -> 2 2 2 0 1 0 ) = XtdComment.objects.all() def test_threaded_comments_step_1_level_0(self): # comment 1 self.assertTrue(self.c1.parent_id == 1 and self.c1.thread_id == 1) self.assertTrue(self.c1.level == 0 and self.c1.order == 1) + self.assertEqual(self.c1.nested_count, 0) # comment 2 self.assertTrue(self.c2.parent_id == 2 and self.c2.thread_id == 2) self.assertTrue(self.c2.level == 0 and self.c2.order == 1) - + self.assertEqual(self.c2.nested_count, 0) class ThreadStep2TestCase(ArticleBaseTestCase): def setUp(self): super(ThreadStep2TestCase, self).setUp() thread_test_step_1(self.article_1) thread_test_step_2(self.article_1) - ( # content -> cmt.id thread_id parent_id level order - self.c1, # -> 1 1 1 0 1 - self.c3, # -> 3 1 1 1 2 - self.c4, # -> 4 1 1 1 3 - self.c2 # -> 2 2 2 0 1 + ( # content -> cmt.id thread_id parent_id level order nested + self.c1, # -> 1 1 1 0 1 2 + self.c3, # -> 3 1 1 1 2 0 + self.c4, # -> 4 1 1 1 3 0 + self.c2 # -> 2 2 2 0 1 0 ) = XtdComment.objects.all() def test_threaded_comments_step_2_level_0(self): # comment 1 self.assertTrue(self.c1.parent_id == 1 and self.c1.thread_id == 1) self.assertTrue(self.c1.level == 0 and self.c1.order == 1) + self.assertEqual(self.c1.nested_count, 2) # comment 2 self.assertTrue(self.c2.parent_id == 2 and self.c2.thread_id == 2) self.assertTrue(self.c2.level == 0 and self.c2.order == 1) + self.assertEqual(self.c2.nested_count, 0) def test_threaded_comments_step_2_level_1(self): # comment 3 self.assertTrue(self.c3.parent_id == 1 and self.c3.thread_id == 1) self.assertTrue(self.c3.level == 1 and self.c3.order == 2) + self.assertEqual(self.c3.nested_count, 0) # comment 4 self.assertTrue(self.c4.parent_id == 1 and self.c4.thread_id == 1) self.assertTrue(self.c4.level == 1 and self.c4.order == 3) + self.assertEqual(self.c4.nested_count, 0) class ThreadStep3TestCase(ArticleBaseTestCase): @@ -268,32 +273,37 @@ def setUp(self): thread_test_step_2(self.article_1) thread_test_step_3(self.article_1) - ( # -> content: cmt.id thread_id parent_id level order - self.c1, # -> 1 1 1 0 1 - self.c3, # -> 3 1 1 1 2 - self.c4, # -> 4 1 1 1 3 - self.c2, # -> 2 2 2 0 1 - self.c5 # -> 5 2 2 1 2 + ( # -> content: cmt.id thread_id parent_id level order nested + self.c1, # -> 1 1 1 0 1 2 + self.c3, # -> 3 1 1 1 2 0 + self.c4, # -> 4 1 1 1 3 0 + self.c2, # -> 2 2 2 0 1 1 + self.c5 # -> 5 2 2 1 2 0 ) = XtdComment.objects.all() def test_threaded_comments_step_3_level_0(self): # comment 1 self.assertTrue(self.c1.parent_id == 1 and self.c1.thread_id == 1) self.assertTrue(self.c1.level == 0 and self.c1.order == 1) + self.assertEqual(self.c1.nested_count, 2) # comment 2 self.assertTrue(self.c2.parent_id == 2 and self.c2.thread_id == 2) self.assertTrue(self.c2.level == 0 and self.c2.order == 1) + self.assertEqual(self.c2.nested_count, 1) def test_threaded_comments_step_3_level_1(self): # comment 3 self.assertTrue(self.c3.parent_id == 1 and self.c3.thread_id == 1) self.assertTrue(self.c3.level == 1 and self.c3.order == 2) + self.assertEqual(self.c3.nested_count, 0) # comment 4 self.assertTrue(self.c4.parent_id == 1 and self.c4.thread_id == 1) self.assertTrue(self.c4.level == 1 and self.c4.order == 3) + self.assertEqual(self.c4.nested_count, 0) # comment 5 self.assertTrue(self.c5.parent_id == 2 and self.c5.thread_id == 2) self.assertTrue(self.c5.level == 1 and self.c5.order == 2) + self.assertEqual(self.c5.nested_count, 0) class ThreadStep4TestCase(ArticleBaseTestCase): @@ -304,42 +314,49 @@ def setUp(self): thread_test_step_3(self.article_1) thread_test_step_4(self.article_1) - ( # content -> cmt.id thread_id parent_id level order - self.c1, # -> 1 1 1 0 1 - self.c3, # -> 3 1 1 1 2 - self.c4, # -> 4 1 1 1 3 - self.c7, # -> 7 1 4 2 4 - self.c2, # -> 2 2 2 0 1 - self.c5, # -> 5 2 2 1 2 - self.c6 # -> 6 2 5 2 3 + ( # content -> cmt.id thread_id parent_id level order nested + self.c1, # -> 1 1 1 0 1 3 + self.c3, # -> 3 1 1 1 2 0 + self.c4, # -> 4 1 1 1 3 1 + self.c7, # -> 7 1 4 2 4 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 ) = XtdComment.objects.all() def test_threaded_comments_step_4_level_0(self): # comment 1 self.assertTrue(self.c1.parent_id == 1 and self.c1.thread_id == 1) self.assertTrue(self.c1.level == 0 and self.c1.order == 1) + self.assertEqual(self.c1.nested_count, 3) # comment 2 self.assertTrue(self.c2.parent_id == 2 and self.c2.thread_id == 2) self.assertTrue(self.c2.level == 0 and self.c2.order == 1) + self.assertEqual(self.c2.nested_count, 2) def test_threaded_comments_step_4_level_1(self): # comment 3 self.assertTrue(self.c3.parent_id == 1 and self.c3.thread_id == 1) self.assertTrue(self.c3.level == 1 and self.c3.order == 2) + self.assertEqual(self.c3.nested_count, 0) # comment 4 self.assertTrue(self.c4.parent_id == 1 and self.c4.thread_id == 1) self.assertTrue(self.c4.level == 1 and self.c4.order == 3) + self.assertEqual(self.c4.nested_count, 1) # comment 5 self.assertTrue(self.c5.parent_id == 2 and self.c5.thread_id == 2) self.assertTrue(self.c5.level == 1 and self.c5.order == 2) + self.assertEqual(self.c5.nested_count, 1) def test_threaded_comments_step_4_level_2(self): # comment 6 self.assertTrue(self.c6.parent_id == 5 and self.c6.thread_id == 2) self.assertTrue(self.c6.level == 2 and self.c6.order == 3) + self.assertEqual(self.c6.nested_count, 0) # comment 7 self.assertTrue(self.c7.parent_id == 4 and self.c7.thread_id == 1) self.assertTrue(self.c7.level == 2 and self.c7.order == 4) + self.assertEqual(self.c7.nested_count, 0) class ThreadStep5TestCase(ArticleBaseTestCase): @@ -351,50 +368,59 @@ def setUp(self): thread_test_step_4(self.article_1) thread_test_step_5(self.article_1) - ( # content -> cmt.id thread_id parent_id level order - self.c1, # -> 1 1 1 0 1 - self.c3, # -> 3 1 1 1 2 - self.c8, # -> 8 1 3 2 3 - self.c4, # -> 4 1 1 1 4 - self.c7, # -> 7 1 4 2 5 - self.c2, # -> 2 2 2 0 1 - self.c5, # -> 5 2 2 1 2 - self.c6, # -> 6 2 5 2 3 - self.c9 # -> 9 9 9 0 1 + ( # 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() def test_threaded_comments_step_5_level_0(self): # comment 1 self.assertTrue(self.c1.parent_id == 1 and self.c1.thread_id == 1) self.assertTrue(self.c1.level == 0 and self.c1.order == 1) + self.assertEqual(self.c1.nested_count, 4) # comment 2 self.assertTrue(self.c2.parent_id == 2 and self.c2.thread_id == 2) self.assertTrue(self.c2.level == 0 and self.c2.order == 1) + self.assertEqual(self.c2.nested_count, 2) # comment 9 self.assertTrue(self.c9.parent_id == 9 and self.c9.thread_id == 9) self.assertTrue(self.c9.level == 0 and self.c9.order == 1) + self.assertEqual(self.c9.nested_count, 0) def test_threaded_comments_step_5_level_1(self): # comment 3 self.assertTrue(self.c3.parent_id == 1 and self.c3.thread_id == 1) self.assertTrue(self.c3.level == 1 and self.c3.order == 2) + self.assertEqual(self.c3.nested_count, 1) # comment 4 self.assertTrue(self.c4.parent_id == 1 and self.c4.thread_id == 1) self.assertTrue(self.c4.level == 1 and self.c4.order == 4) # changed + self.assertEqual(self.c4.nested_count, 1) # comment 5 self.assertTrue(self.c5.parent_id == 2 and self.c5.thread_id == 2) self.assertTrue(self.c5.level == 1 and self.c5.order == 2) + self.assertEqual(self.c5.nested_count, 1) def test_threaded_comments_step_5_level_2(self): # comment 6 self.assertTrue(self.c6.parent_id == 5 and self.c6.thread_id == 2) self.assertTrue(self.c6.level == 2 and self.c6.order == 3) - # comment 7 + self.assertEqual(self.c6.nested_count, 0) + # comment 7 self.assertTrue(self.c7.parent_id == 4 and self.c7.thread_id == 1) self.assertTrue(self.c7.level == 2 and self.c7.order == 5) # changed + self.assertEqual(self.c7.nested_count, 0) # comment 8 self.assertTrue(self.c8.parent_id == 3 and self.c8.thread_id == 1) self.assertTrue(self.c8.level == 2 and self.c8.order == 3) + self.assertEqual(self.c8.nested_count, 0) def test_exceed_max_thread_level_raises_exception(self): article_ct = ContentType.objects.get(app_label="tests", model="article") @@ -418,20 +444,12 @@ def add_comment_to_diary_entry(diary_entry): site=site, comment="cmt to day in diary", submit_date=datetime.now()) - - + + class DiaryBaseTestCase(DjangoTestCase): def setUp(self): self.day_in_diary = Diary.objects.create(body="About Today...") - add_comment_to_diary_entry(self.day_in_diary) - # diary_ct = ContentType.objects.get(app_label="tests", model="diary") - # site = Site.objects.get(pk=1) - # XtdComment.objects.create(content_type=diary_ct, - # object_pk=self.day_in_diary.id, - # content_object=self.day_in_diary, - # site=site, - # comment="cmt to day in diary", - # submit_date=datetime.now()) + add_comment_to_diary_entry(self.day_in_diary) def test_max_thread_level_by_app_model(self): diary_ct = ContentType.objects.get(app_label="tests", model="diary") @@ -447,7 +465,7 @@ def test_max_thread_level_by_app_model(self): class PublishOrUnpublishNestedComments_1_TestCase(ArticleBaseTestCase): - # Add a threaded comment structure (c1, c2, c3) and verify that + # Add a threaded comment structure (c1, c2, c3) and verify that # removing c1 unpublishes c3. def setUp(self): @@ -457,11 +475,11 @@ def setUp(self): # # These two lines create the following comments: # - # ( # content -> cmt.id thread_id parent_id level order - # cm1, # -> 1 1 1 0 1 - # cm3, # -> 3 1 1 1 2 - # cm4, # -> 4 1 1 1 3 - # cm2, # -> 2 2 2 0 1 + # ( # content -> cmt.id thread_id parent_id level order nested + # cm1, # -> 1 1 1 0 1 2 + # cm3, # -> 3 1 1 1 2 0 + # cm4, # -> 4 1 1 1 3 0 + # cm2, # -> 2 2 2 0 1 0 # ) = XtdComment.objects.all() def test_all_comments_are_public_and_have_not_been_removed(self): @@ -471,10 +489,15 @@ def test_all_comments_are_public_and_have_not_been_removed(self): def test_removing_c1_unpublishes_c3_and_c4(self): cm1 = XtdComment.objects.get(pk=1) + self.assertEqual(cm1.nested_count, 2) # nested_count should be 2. + cm1.is_removed = True cm1.save() + cm1 = XtdComment.objects.get(pk=1) self.assertTrue(cm1.is_public) self.assertTrue(cm1.is_removed) + # Is still public, so the nested_count doesn't change. + self.assertEqual(cm1.nested_count, 2) cm3 = XtdComment.objects.get(pk=3) self.assertFalse(cm3.is_public) @@ -496,31 +519,35 @@ class PublishOrUnpublishNestedComments_2_TestCase(ArticleBaseTestCase): def setUp(self): super(PublishOrUnpublishNestedComments_2_TestCase, self).setUp() - thread_test_step_1(self.article_1, model=MyComment, title="Can't be empty 1") - thread_test_step_2(self.article_1, model=MyComment, title="Can't be empty 2") + thread_test_step_1(self.article_1, model=MyComment, + title="Can't be empty 1") + thread_test_step_2(self.article_1, model=MyComment, + title="Can't be empty 2") # # These two lines create the following comments: # - # ( # content -> cmt.id thread_id parent_id level order - # cm1, # -> 1 1 1 0 1 - # cm3, # -> 3 1 1 1 2 - # cm4, # -> 4 1 1 1 3 - # cm2, # -> 2 2 2 0 1 + # ( # content -> cmt.id thread_id parent_id level order nested + # cm1, # -> 1 1 1 0 1 2 + # cm3, # -> 3 1 1 1 2 0 + # cm4, # -> 4 1 1 1 3 0 + # cm2, # -> 2 2 2 0 1 0 # ) = MyComment.objects.all() - + def test_all_comments_are_public_and_have_not_been_removed(self): for cm in MyComment.objects.all(): self.assertTrue(cm.is_public) self.assertFalse(cm.is_removed) - @patch.multiple('django_comments_xtd.conf.settings', COMMENTS_XTD_MODEL=_model) + @patch.multiple('django_comments_xtd.conf.settings', + COMMENTS_XTD_MODEL=_model) def test_removing_c1_unpublishes_c3_and_c4(self): - # Register the receiver again. It was registered in apps.py, but we have - # patched the COMMENTS_XTD_MODEL, however we won't fake the ready. It's - # easier to just register again the receiver, to test only what depends - # on django-comments-xtd. + # Register the receiver again. It was registered in apps.py, but we + # have patched the COMMENTS_XTD_MODEL, however we won't fake the ready. + # It's easier to just register again the receiver, to test only what + # depends on django-comments-xtd. model_app_label = get_model()._meta.label - pre_save.connect(publish_or_unpublish_on_pre_save, sender=model_app_label) + pre_save.connect(publish_or_unpublish_on_pre_save, + sender=model_app_label) cm1 = MyComment.objects.get(pk=1) cm1.is_removed = True From 63a516a45e514e16913bcc6f91a9a41a9777a8be Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Mon, 14 Sep 2020 22:48:39 +0200 Subject: [PATCH 04/13] Test that parent nested_count changes when child comment is removed. --- django_comments_xtd/tests/test_models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/django_comments_xtd/tests/test_models.py b/django_comments_xtd/tests/test_models.py index 98f91db8..bc56d786 100644 --- a/django_comments_xtd/tests/test_models.py +++ b/django_comments_xtd/tests/test_models.py @@ -434,6 +434,19 @@ def test_exceed_max_thread_level_raises_exception(self): submit_date=datetime.now(), parent_id=8) # already max thread level + def test_removing_c4_withdraws_c7_and_updates_nested_count(self): + cm4 = XtdComment.objects.get(pk=4) + self.assertEqual(cm4.nested_count, 1) + cm1 = XtdComment.objects.get(pk=1) + self.assertEqual(cm1.nested_count, 4) + # Remove comment 4, save, and check again. + cm4.is_removed = True + cm4.save() + cm4 = XtdComment.objects.get(pk=4) + self.assertEqual(cm4.nested_count, 1) + cm1 = XtdComment.objects.get(pk=1) + self.assertEqual(cm1.nested_count, 3) + def add_comment_to_diary_entry(diary_entry): diary_ct = ContentType.objects.get(app_label="tests", model="diary") From bac29524d5351f286723e8b4308d56acfc6961d9 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Mon, 14 Sep 2020 23:24:22 +0200 Subject: [PATCH 05/13] Use Q() in __calculate_thread_data --- .travis.yml | 1 + django_comments_xtd/models.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fb244bcd..29620147 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,5 +33,6 @@ install: script: - env | sort + - tox python manage.py migrate --settings=example.comp.settings - tox - flake8 --show-source --max-line-length=80 --exclude=.tox,docs,django_comments_xtd/tests,django_comments_xtd/__init__.py,django_comments_xtd/migrations django_comments_xtd/ diff --git a/django_comments_xtd/models.py b/django_comments_xtd/models.py index e84fecd4..a096e601 100644 --- a/django_comments_xtd/models.py +++ b/django_comments_xtd/models.py @@ -107,10 +107,8 @@ def _calculate_thread_data(self): max_order = qc_eq_thread.aggregate(Max('order'))['order__max'] self.order = max_order + 1 - parent.nested_count = F('nested_count') + 1 - parent.save() - qc_eq_thread.filter(level__lt=parent.level, - order__lt=parent.order)\ + 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): @@ -234,6 +232,7 @@ def publish_or_unpublish_nested_comments(comment, are_public=False): 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 From f228a18e2c32da2b17cebaf4a31c03e57b039b40 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Tue, 15 Sep 2020 07:56:25 +0200 Subject: [PATCH 06/13] Explicitly call 'migrate' command when running tests. --- .travis.yml | 1 - django_comments_xtd/tests/__init__.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 29620147..fb244bcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,5 @@ install: script: - env | sort - - tox python manage.py migrate --settings=example.comp.settings - tox - flake8 --show-source --max-line-length=80 --exclude=.tox,docs,django_comments_xtd/tests,django_comments_xtd/__init__.py,django_comments_xtd/migrations django_comments_xtd/ diff --git a/django_comments_xtd/tests/__init__.py b/django_comments_xtd/tests/__init__.py index e10a5a3f..4a1fc56d 100644 --- a/django_comments_xtd/tests/__init__.py +++ b/django_comments_xtd/tests/__init__.py @@ -16,11 +16,13 @@ def run_tests(): import django from django.conf import settings + from django.core.management import call_command from django.test.utils import get_runner # Django 1.7.x or above. if django.VERSION[0] >=1 or django.VERSION[1] >= 7: django.setup() + call_command('migrate', 'django_comments_xtd') runner = get_runner(settings, "django.test.runner.DiscoverRunner") else: From 109f169c51375318ba8d9082b33ad4ad0e1cc805 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Tue, 15 Sep 2020 08:20:47 +0200 Subject: [PATCH 07/13] wip: travis-ci not running migrations. --- django_comments_xtd/tests/__init__.py | 2 -- tox.ini | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/django_comments_xtd/tests/__init__.py b/django_comments_xtd/tests/__init__.py index 4a1fc56d..e10a5a3f 100644 --- a/django_comments_xtd/tests/__init__.py +++ b/django_comments_xtd/tests/__init__.py @@ -16,13 +16,11 @@ def run_tests(): import django from django.conf import settings - from django.core.management import call_command from django.test.utils import get_runner # Django 1.7.x or above. if django.VERSION[0] >=1 or django.VERSION[1] >= 7: django.setup() - call_command('migrate', 'django_comments_xtd') runner = get_runner(settings, "django.test.runner.DiscoverRunner") else: diff --git a/tox.ini b/tox.ini index 39bb735a..3596003e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [pytest] +addopts = --create-db python_files = test_*.py django_find_project = false From 24172fa6215583acb23ec08ae0f7a3f2ecce256d Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Fri, 18 Sep 2020 08:08:02 +0200 Subject: [PATCH 08/13] Add migration file to create nested_count field in XtdComment model. --- .../migrations/0007_xtdcomment_nested_count.py | 18 ++++++++++++++++++ tox.ini | 1 - 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 django_comments_xtd/migrations/0007_xtdcomment_nested_count.py diff --git a/django_comments_xtd/migrations/0007_xtdcomment_nested_count.py b/django_comments_xtd/migrations/0007_xtdcomment_nested_count.py new file mode 100644 index 00000000..70295371 --- /dev/null +++ b/django_comments_xtd/migrations/0007_xtdcomment_nested_count.py @@ -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), + ), + ] diff --git a/tox.ini b/tox.ini index 3596003e..39bb735a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [pytest] -addopts = --create-db python_files = test_*.py django_find_project = false From 5d63db18205005524b4235f04556fce0aa166916 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Sun, 20 Sep 2020 17:45:40 +0200 Subject: [PATCH 09/13] Add command initialize_nested_count, to set nested_count field in DB. --- .../commands/initialize_nested_count.py | 51 ++++++++++++++++ django_comments_xtd/models.py | 1 - .../tests/test_cmd_initialize_nested_count.py | 59 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 django_comments_xtd/management/commands/initialize_nested_count.py create mode 100644 django_comments_xtd/tests/test_cmd_initialize_nested_count.py diff --git a/django_comments_xtd/management/commands/initialize_nested_count.py b/django_comments_xtd/management/commands/initialize_nested_count.py new file mode 100644 index 00000000..9f226789 --- /dev/null +++ b/django_comments_xtd/management/commands/initialize_nested_count.py @@ -0,0 +1,51 @@ +import sys + +from django.db import connections +from django.db.utils import ConnectionDoesNotExist, IntegrityError +from django.core.management.base import BaseCommand, CommandError + +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) diff --git a/django_comments_xtd/models.py b/django_comments_xtd/models.py index a096e601..b6df5afb 100644 --- a/django_comments_xtd/models.py +++ b/django_comments_xtd/models.py @@ -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 = [] diff --git a/django_comments_xtd/tests/test_cmd_initialize_nested_count.py b/django_comments_xtd/tests/test_cmd_initialize_nested_count.py new file mode 100644 index 00000000..a8109f73 --- /dev/null +++ b/django_comments_xtd/tests/test_cmd_initialize_nested_count.py @@ -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() From a537e9ac6334aa7fdf65f9a42cd5fda59dc51514 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Sun, 20 Sep 2020 17:50:13 +0200 Subject: [PATCH 10/13] PEP8 compliant changes. --- .../management/commands/initialize_nested_count.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/django_comments_xtd/management/commands/initialize_nested_count.py b/django_comments_xtd/management/commands/initialize_nested_count.py index 9f226789..d423d153 100644 --- a/django_comments_xtd/management/commands/initialize_nested_count.py +++ b/django_comments_xtd/management/commands/initialize_nested_count.py @@ -1,8 +1,5 @@ -import sys - -from django.db import connections -from django.db.utils import ConnectionDoesNotExist, IntegrityError -from django.core.management.base import BaseCommand, CommandError +from django.db.utils import ConnectionDoesNotExist +from django.core.management.base import BaseCommand from django_comments_xtd.models import XtdComment @@ -14,7 +11,7 @@ def add_arguments(self, parser): parser.add_argument('using', nargs='*', type=str) def initialize_nested_count(self, using): - # Control break. + # Control break. active_thread_id = -1 parents = {} From 97ad39e05a72e37a8c094f38282110a480ea4bc8 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Sun, 20 Sep 2020 20:50:50 +0200 Subject: [PATCH 11/13] Add migration 0008 to populate content in nested_count field. --- CHANGELOG.md | 17 ++++++++++++----- .../migrations/0008_auto_20200920_2037.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 django_comments_xtd/migrations/0008_auto_20200920_2037.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d52d8a35..4987ca6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,21 @@ # Change Log -## [] - YYYY-MM-DD - +## [2.8.0] - YYYY-MM-DD + + * 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. @@ -22,9 +29,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. diff --git a/django_comments_xtd/migrations/0008_auto_20200920_2037.py b/django_comments_xtd/migrations/0008_auto_20200920_2037.py new file mode 100644 index 00000000..d4658398 --- /dev/null +++ b/django_comments_xtd/migrations/0008_auto_20200920_2037.py @@ -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) + ] From 88b58d71b5ae84e479fb4edbd7d67597b0051f14 Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Thu, 24 Sep 2020 21:06:41 +0200 Subject: [PATCH 12/13] Mention nested_count in Tutorial. --- django_comments_xtd/urls.py | 3 +-- docs/tutorial.rst | 45 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/django_comments_xtd/urls.py b/django_comments_xtd/urls.py index a632f675..ae3a7548 100644 --- a/django_comments_xtd/urls.py +++ b/django_comments_xtd/urls.py @@ -1,4 +1,3 @@ -# from django.conf.urls import include, url from django.urls import include, re_path from rest_framework.urlpatterns import format_suffix_patterns @@ -12,7 +11,7 @@ re_path(r'^mute/(?P[^/]+)/$', views.mute, name='comments-xtd-mute'), re_path(r'^reply/(?P[\d]+)/$', views.reply, name='comments-xtd-reply'), - # Remap comments-flag to check allow-flagging is enabled. + # Remap comments-flag to check allow-flagg {% endif %} - + Below the list of comments we want to display the comment form. There are two template tags available for that purpose, the :ttag:`render_comment_form` and @@ -265,7 +265,7 @@ following code before the ``endblock`` tag: {% endif %} - + .. note:: The ``{% if object.allow_comments %}`` and corresponding ``{% endif %}`` are not necessary in your code. I use it in this tutorial (and in the demo sites) as a way to disable comments whenever the author of a blog post decides so. It has been mentioned `here `_ too. @@ -310,7 +310,7 @@ nested comments. Now we will set up comment moderation. single: Moderation .. _moderation: - + Moderation ========== @@ -425,7 +425,7 @@ file and add: .. code-block:: python COMMENTS_XTD_CONFIRM_EMAIL = False - + django-comments-xtd comes with a **Moderator** class that inherits from ``CommentModerator`` and implements a method ``allow`` that will do the @@ -489,7 +489,7 @@ Now edit ``blog/models.py`` and add the code corresponding to our new from blog.badwords import badwords ... - + class PostCommentModerator(SpamModerator): email_notification = True @@ -528,7 +528,7 @@ Now edit ``blog/models.py`` and add the code corresponding to our new content_object, request) - moderator.register(Post, PostCommentModerator) + moderator.register(Post, PostCommentModerator) Now we can try to send a comment with any of the bad words listed in badwords_. @@ -608,7 +608,7 @@ Edit ``blog/post_detail.html`` to make it look like follows:
{{ object.body|linebreaks }}
- + {% get_comment_count for object as comment_count %}
Back to the post list @@ -625,7 +625,7 @@ Edit ``blog/post_detail.html`` to make it look like follows:
{% endif %} - + {% if comment_count %}
    {% render_xtdcomment_tree for object %} @@ -646,7 +646,7 @@ nest comments inside one level deeper. .. image:: images/reply-link.png - + Different max thread levels --------------------------- @@ -672,6 +672,11 @@ up to level one for blog posts, we would set it up as follows in our 'blog.post': 1, } +The ``nested_count`` field +-------------------------- + +When threaded comments are enabled the field ``nested_count`` of every **XtdComment** instance keeps track of how many nested comments it contains. + Flags ===== @@ -995,7 +1000,7 @@ comments, or to like/dislike them. But it comes at the cost of using: To know more about the client side of the application and the build process read the specific page on the :doc:`javascript`. - + In this section of the tutorial we go through the steps to make use of the JavaScript plugin. @@ -1062,14 +1067,14 @@ Edit ``tutorial/urls.py`` and add the following url: .. code-block:: python from django.views.i18n import JavaScriptCatalog - + urlpatterns = [ ... path(r'jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), ] In the next section we will use the new URL to load the i18n JavaScript catalog. - + Load the plugin --------------- @@ -1081,9 +1086,9 @@ Now let's edit ``blog/post_detail.html`` and make it look as follows: {% load static %} {% load comments %} {% load comments_xtd %} - + {% block title %}{{ object.title }}{% endblock %} - + {% block content %}

    {{ object.title }}

    @@ -1092,14 +1097,14 @@ Now let's edit ``blog/post_detail.html`` and make it look as follows:
    {{ object.body|linebreaks }}
    - + - +
    {% endblock %} - + {% block extra-js %} @@ -1151,7 +1156,7 @@ plugin, including the following features: #. Immediate like/dislike actions. .. image:: images/update-comment-tree.png - + Final notes =========== From dde8f2ccb0d26f92ce5fd4cdf2c5eedaf5076bfe Mon Sep 17 00:00:00 2001 From: Daniel Rus Morales Date: Sat, 26 Sep 2020 07:43:18 +0200 Subject: [PATCH 13/13] Update CHANGELOG.md and index.rst. --- CHANGELOG.md | 6 +- docs/index.rst | 209 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 212 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4987ca6d..faa599ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Change Log -## [2.8.0] - 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 + 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. diff --git a/docs/index.rst b/docs/index.rst index 8b322277..c9895d02 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,7 +75,7 @@ Advanced Use Once you've got django-comments-xtd working, you may want to know more about specific features, or check out the use cases to see how others customize it. - + .. toctree:: :maxdepth: 1 @@ -89,3 +89,210 @@ specific features, or check out the use cases to see how others customize it. settings templates usecases + +Change Log +========== + +[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"``). + * Fixes issue `#212 `_, about missing i18n JavaScript catalog files for Dutch, German and Russian. + +[2.7.1] - 2020-08-12 +-------------------- + + * Fixes issue `#188 `_, about loading a templatetags module not required for the application. + * Fixes issue `#196 `_. When extending django-comments-xtd's comment model, the receiver function that reviews whether nested comments have to be publish or unpublish is not called. + +[2.7.0] - 2020-08-09 +-------------------- + + * 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 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. + * Enhancement, closing issue `#175 `_ on how to customize django-comments-xtd so that user images displayed in comments come from other sources. A new setting ``COMMENTS_XTD_API_GET_USER_AVATAR`` has been added. The docs have been extended with a page that explains the use case in depth. + * Fixes issue `#171 `_, on wrong permission used to decide whether a user is a moderator. The right permission is ``django_comments.can_moderate``. (thanks to Ashwani Gupta, @ashwani99). + * Fixes issue `#136 `_ on missing element in the ``templates/base.html`` file distributed with the **tutorial.tar.gz** bundle. + +[2.6.2] - 2020-07-05 +-------------------- + + * Adds Dutch translation (thanks to Jean-Paul Ladage, @jladage). + * Adds Russian translation (thanks to Михаил Рыбкин, @MikerStudio). + * Fixesissue `#140 `_, which adds the capacity to allow only registered users to post comments. + * Fixesissue `#149 `_, on wrong SQL boolean literal value used when running special command ``populate_xtdcomments`` to load Postgres database with xtdcomments. + * Fixes issue `#154 `_, on using string formatting compatible with Python versions prior to 3.6. + * Fixes issue `#156 `_, on wrong props name ``poll_interval``. JavaScript plugin expects the use of ``polling_interval`` while the ``api/frontend.py`` module referred to it as ``poll_interval``. (thanks to @ashwani99). + * Fixes issue `#159 `_, about using the same id for all the checkboxes in the comment list. When ticking one checkbox in a nested form the checkbox of the main form was ticked. Now each checkbox has a different id, suffixed with the content of the ``reply_to`` field. + +[2.6.1] - 2020-05-13 +-------------------- + + * Fixes issue `#150 `_, about wrong protocol in the URL when fetching avatar images from gravatar. + +[2.6.0] - 2020-05-12 +-------------------- + + * Fixes issue `#145 `_, on inadequate number of SQL queries used by API entry point **comments-xtd-api-list**, available in the URL ``/comments/api///``. The issue also happened when rendering the comments using tags ``get_xtdcomment_tree`` and ``render_xtdcomment_tree``. It has been fixed in both cases too. + * Updates the JSON schema of the output retrieved by the API entry point **comments-xtd-api-list**. Thus the version number change. The flags attribute of each retrieved is now a list of flags instead of a summary for each the flags: "I like it", "I dislike it", "suggest removal". + +[2.5.1] - 2020-04-27 +-------------------- + + * Fixes issue `#138 `_, on unpublishing a single comment with public nested comments. The fix consists of a new ``pre_save`` receiver that will either publish or unpublish nested comments when a comment changes its ``is_public`` attribute. (thanks to @hematinik). + +[2.5.0] - 2020-04-22 +-------------------- + + * Fixes issue `#144 `_ regarding the size of the JavaScript bundle. The new JavaScript plugin does not include React and ReactDOM. The two libraries have to be loaded with an external script. + * Update the dependencies of the JavaScript plugin. + +[2.4.3] - 2020-01-26 +-------------------- + + * Fixes issue on the ContentType that happens when sending post request with empty data. (PR: `#137 `_) (thanks to @dvorberg). + * Adds German translations, (thanks to @dvorberg). + +[2.4.2] - 2019-12-25 +-------------------- + + * Adds Django 3.0 compatibility thanks to Sergey Ivanychev (@ivanychev). + * Adds Norwegian translations thanks to Yngve Høiseth (@yhoiseth). + + +[2.4.1] - 2019-09-30 +-------------------- + + * Allow changing the ``d`` parameter when requesting a gravatar, thanks to @pylixm (PR: `#100 `_). + * Avoid requiring the ``SITE_ID``, thanks to @gassan (PR: `#125 `_). + +[2.4.0] - 2019-02-19 +-------------------- + + New minor release thanks to Mandeep Gill with the following changes: + + * Adds support for non-int based ``object_pk``, for instead when using UUIDs or HashIds as the primary key on a model (closes `#112 `_). + * Refactors the commentbox props generation into a separate function so can be used from the webapi for use with rest-framework/API-only backends that don't make use of server-side templates. + * Adds a **pyproject.yaml** for use with `poetry `_ and new pip environments (PEP 518). + +[2.3.1] - 2019-01-08 +-------------------- + + * Fixes issue `#116 `_. + * Updates package.json JavaScript dependencies: + * babel-cli from 6.24.1 to 6.26.0. + * jquery from 3.2.1 to 3.3.1. + +[2.3.0] - 2018-11-29 +-------------------- + + * Upgrades Twitter-Bootstrap from v3 to v4. + * Fixes issue with tutorial fixtures (bug `#114 `_). + * Upgrade all JavaScript dependencies. Check packages.json for details. The major changes are: + * ReactJS updates from 15.5 to 16.5. + * Babel updates from 6 to 7. + * Webpack from 2.4.1 to 4.21.0. + * Bootstrap from 3.3.7 to 4.1.3. + * Updates webpack.config.js. + * Demo sites and tutorial have been adapted to Twitter Bootstrap v4. + * Fixes issues `#94 `_, `#108 `_, `#111 `_. + +[2.2.1] - 2018-10-06 +-------------------- + + * Resolves deprecation warnings and adopt recommendations in unit tests. + * Fixes demo sites so that they work with Django 1.11, Django 2.0 and Django 2.1. + +[2.2.0] - 2018-08-12 +-------------------- + + * Adds support for Django 2.1. + * Drops support for Django < 1.11 as it depends on django-contrib-comments which dropped support too. + * Fixes issue `#104 `_ (on lack of Django 2.1 support). + +[2.1.0] - 2018-02-13 +-------------------- + + * Fixes issues `#76 `_, `#86 `_ and `#87 `_. + * Request user name and/or email address in case the user is logged in but the user's email attribute is empty and/or the user's ``get_full_name()`` method returns an empty string. + +[2.0.10] - 2018-01-19 +--------------------- + + * Adds Django 2.0 compatibility. + * Fixes issues `#81 `_ and `#83 `_. + * Replaces the use of ``django.test.client`` by ``RequestFactory`` in unittests. + +[2.0.9] - 2017-11-09 +-------------------- + + * Fix issue `#77 `_. Template filter ``xtd_comment_gravatar_url`` must not hard-code http schema in URL (reported by @pamost). + +[2.0.8] - 2017-09-24 +-------------------- + + * App translation to Finnish, thanks to Tero Tikkanen (@terotic). + +[2.0.7] - 2017-09-20 +-------------------- + + * Adds missing migration for a field's label (issue `#71 `_). + * Makes the form label for field ``name`` translatable (issue `#73 `_). + +[2.0.6] - 2017-08-08 +-------------------- + + * Code fixes to enable proper support for the Django Sites Framework. + * Code fixes for the comp demo site. + * Makes demo site dates in initial data files timezone aware. + * Improves documentation on setting up demo sites. + * Style changes in CSS wells. + +[2.0.5] - 2017-07-20 +-------------------- + + * Surpass version number to fix problem with package upload in PyPI. + * No changes applied to this version. + +[2.0.4] - 2017-07-19 +-------------------- + + * Use ``django.core.signing`` with temporary comment passed in URL redirection. + * Fix mistakes in documentation. + +[2.0.3] - 2017-07-10 +-------------------- + + * App translation to French thanks to Brice Gelineau. + * Fixed **MANIFEST.in** file, so that files with translations are distributed. + +[2.0.0] - 2017-06-04 +-------------------- + + * Javascript plugin (based on ReactJS). + * Web API to: + * Create a comment for a given content type and object ID. + * List comments for a given content type and object ID. + * Send feedback flags (like/dislike) on comments. + * Send report flag (removal suggestion) for a comment. + * Template filter ``has_permission`` applicable to a user object and accepting a string specifying the ``app_label.permission`` being checked. It returns ``True`` if the user has the given permission, otherwise returns ``False``. + * Setting ``COMMENTS_XTD_API_USER_REPR`` defines a lambda function to return the user string representation used by the web API in response objects. + * Setting ``COMMENTS_XTD_APP_MODEL_PERMISSIONS`` to explicitly define what commenting features are enabled on per app.model basis. + * Templates ``comments/delete.html`` and ``comments/deleted.html`` matching django-comments-xtd default twitter-bootstrap styling. + * Dependencies on Python packages: djangorestframework. + * Supports i18n for English and Spanish. + * All settings namespaced inside the COMMENTS_XTD setting. + * Management command to migrate comments from django-contrib-comments to django-comments-xtd. + * Enable removal link in ``django_comments_xtd/comment_tree.html`` when the user has the permission ``django_comments.can_moderate``. + * Changed, when the user logged has ``django_comments.can_moderate`` permission, template ``django_comments_xtd/comment_tree.html`` will show the number of removal suggestions a comment has received. + * Changed, when a comment is marked as removed by a moderator (using django-comments' **comments-delete** url) every nested comment below the one removed is unpublished (``is_public`` attribute is turned to ``False``). + * Changed view helper functions, ``perform_like+` and ``perform_dislike`` now returns a boolean indicating whether a flag was created. If ``True`` the flag has been created. If ``False`` the flag has been deleted. These two functions behave as toggle functions. + * Changed templates ``comments/preview.html``, ``comments/flag.html`` and ``comments/flagged.hml``. + * Removed dependency on django-markup. + * Removed template filter ``render_markup_comment``. + * Removed setting ``MARKUP_FALLBACK_FILTER``.