django-threadedcomments-0.9.0/0000755000076500007650000000000012144724431017704 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/CHANGELOG.rst0000644000076500007650000000420712144724115021727 0ustar diederikdiederik00000000000000Version 0.9, 15th May 2013 -------------------------- Rewrote application to use django.contrib.comments Included example app Version 0.5.1, 31st March 2009 ------------------------------ Released version 0.5 of the application and moved the code to GitHub. This is the last of this line--the next release will be a large rewrite to use the comment extension hooks provided in Django 1.1 and will be backwards incompatible. Version 0.4, 7th March 2008 --------------------------- Added gravatar support, internationalization support on the comment models, and fixed a bug with get_threaded_comment_form. Version 0.3.1, 24th February 2008 ---------------------------------- Bugfix release, fixed problem with django-comment-utils always being required and added extra keyword argument onto each of the comment views which allows for the specification of a custom form. Version 0.3, 24th February 2008: --------------------------------- Got rid of proprietary comment moderation tools in favor of the better-designed django-comment-utils by James Bennett. Also, provided template tags which get unbound FreeThreadedCommentForm and ThreadedCommentForm forms. Version 0.2, 4th February 2008: --------------------------------- Added the ability to edit or delete comments. Also added the ability to preview comments (like django.contrib.comments does) and confirm deletion of comments. Also fixed a few bugs, one which was major and made it so that redirection worked improperly after a new ThreadedComment was posted. Finally, added max_depth as an attribute to the moderation system, so that someone cannot vandalize the system by posting a long series of replies to replies. Version 0.1.1, 24th January 2008: --------------------------------- Added get_comment_count and get_free_comment_count templatetags. Also updated the manager to have a all_for_object method which gets all related objects to the given content object. Version 0.1, 23th January 2008: ------------------------------- Packaged from revision 59 in Subversion; download at http://django-threadedcomments.googlecode.com/files/threadedcomments-0.1.zip * First packaged version using distutils. django-threadedcomments-0.9.0/CONTRIBUTORS.txt0000644000076500007650000000075712130514705022407 0ustar diederikdiederik00000000000000Pavel Blinov Max Battcher Eric Florenzano Kevin Fricovsky Matthijs Kooijman Juan Pablo Puerta Christos Trochalakis Thejaswi Puthraya Honza Kral Jannis Leidel Adam Fast Charles Leifer Diederik van der Boor Egor Yurtaev Flavio Curella Jakub Vysoky Joshua Bonnett Michael Blume django-threadedcomments-0.9.0/django_threadedcomments.egg-info/0000755000076500007650000000000012144724431026246 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/django_threadedcomments.egg-info/dependency_links.txt0000644000076500007650000000000112144724431032314 0ustar diederikdiederik00000000000000 django-threadedcomments-0.9.0/django_threadedcomments.egg-info/not-zip-safe0000644000076500007650000000000112144723757030506 0ustar diederikdiederik00000000000000 django-threadedcomments-0.9.0/django_threadedcomments.egg-info/PKG-INFO0000644000076500007650000002003012144724431027336 0ustar diederikdiederik00000000000000Metadata-Version: 1.0 Name: django-threadedcomments Version: 0.9.0 Summary: A simple yet flexible threaded commenting system. Home-page: https://github.com/HonzaKral/django-threadedcomments Author: Diederik van der Boor Author-email: vdboor@edoburu.nl License: BSD Download-URL: https://github.com/HonzaKral/django-threadedcomments/zipball/master Description: django-threadedcomments ======================= *threadedcomments* is a Django application which allows for the simple creation of a threaded commenting system. Commenters can reply both to the original item, and reply to other comments as well. The application is (as of 0.9) built on top of django.contrib.comments, which allows it to be easily extended by other modules. Installation ============ Install the package via pip:: pip install django-threadedcomments It's preferred to install the module in a virtual environment. Configuration ------------- Add the following to ``settings.py``:: INSTALLED_APPS += ( 'threadedcomments', 'django.contrib.comments', ) COMMENTS_APP = 'threadedcomments' By placing the ``threadedcomments`` app above the ``django.contrib.comments`` application, the placeholder ``comments/list.html`` template will already be replaced by a threaded view. Make sure django.contrib.comments_ is configured in ``urls.py``:: urlpatterns += patterns('', url(r'^articles/comments/', include('django.contrib.comments.urls')), ) Provide a template that displays the comments for the ``object`` (e.g. article or blog entry):: {% load threadedcomments_tags %} ...

Comments for {{ object.title }}:

{% render_comment_list for object %} {% render_comment_form for object %} Template design --------------- Naturally, it's desirable to write your own version of ``comments/list.html`` in your project, or use one of the ``comments/app/list.html`` or ``comments/app/model/list.html`` overrides. Make sure to override ``comments/base.html`` as well, so the other views of django.contrib.comments_ are displayed using your web site design. The other templates of django.contrib.comments_ are very plain as well on purpose (for example ``comments/posted.html``), since these pages depend on the custom design of the web site. See the provided ``example`` app for a basic configuration, including a JavaScript-based reply form that moves to the comment the visitor replies to. Template tags ============= The ``threadedcomments_tags`` library is a drop-in replacement for the ``comments`` library that is required for the plain comments. The tags are forwards compatible; they support the same syntax as django.contrib.comments_ provides, and they add a few extra parameters. Fething comment counts:: {% get_comment_count for [object] as [varname] %} {% get_comment_count for [object] as [varname] root_only %} {% get_comment_count for [app].[model] [id] as [varname] %} {% get_comment_count for [app].[model] [id] as [varname] root_only %} Fetching the comments list:: {% get_comment_list for [object] as [varname] %} {% get_comment_list for [object] as [varname] flat %} {% get_comment_list for [object] as [varname] root_only %} Rendering the comments list:: {% render_comment_list for [object] %} {% render_comment_list for [object] root_only %} {% render_comment_list for [app].[model] [id] %} {% render_comment_list for [app].[model] [id] root_only %} Fetching the comment form:: {% get_comment_form for [object] as [varname] %} {% get_comment_form for [object] as [varname] with [parent_id] %} {% get_comment_form for [app].[model] [id] as [varname] %} {% get_comment_form for [app].[model] [id] as [varname] with [parent_id] %} Rendering the comment form:: {% render_comment_form for [object] %} {% render_comment_form for [object] with [parent_id] %} {% render_comment_form for [app].[model] [id] %} {% render_comment_form for [app].[model] [id] with [parent_id] %} Rendering the whole tree:: {% for comment in comment_list|fill_tree|annotate_tree %} {% ifchanged comment.parent_id %}{% else %}{% endifchanged %} {% if not comment.open and not comment.close %}{% endif %} {% if comment.open %}
    {% endif %}
  • ... {% for close in comment.close %}
{% endfor %} {% endfor %} The ``fill_tree`` filter is required for pagination, it ensures that the parents of the first comment are included as well. The ``annotate_tree`` filter adds the ``open`` and ``close`` properties to the comment. Extending the module ==================== The application is built on top of the standard django.contrib.comments_ framework, which supports various signals, and template overrides to customize the comments. To customize django-threadedcomments, override the proper templates, or include the apps that provide the missing features. Front-end editing support for example, is left out on purpose. It belongs to the domain of moderation, and policies to know "who can do what". That deserves to be in a separate application, it shouldn't be in this application as it focuses on threading. The same applies to social media logins, comment subscriptions, spam protection and Ajax posting. Note that the standard framework also supports moderation, flagging, and RSS feeds too. More documentation can be found at: * `Django's comments framework `_ * `Customizing the comments framework `_ * `Example of using the in-built comments app `_ Some of the modules worth looking at are: * django-comments-spamfighter_ * django-myrecaptcha_ * django-fluent-comments_ These modules can enhance the comments system even further. .. _django.contrib.comments: https://docs.djangoproject.com/en/dev/ref/contrib/comments/ .. _django-fluent-comments: https://github.com/edoburu/django-fluent-comments/ .. _django-myrecaptcha: https://bitbucket.org/pelletier/django-myrecaptcha/ .. _django-comments-spamfighter: https://github.com/bartTC/django-comments-spamfighter/ Keywords: django,comments,threading Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules django-threadedcomments-0.9.0/django_threadedcomments.egg-info/SOURCES.txt0000644000076500007650000000213412144724431030132 0ustar diederikdiederik00000000000000CHANGELOG.rst CONTRIBUTORS.txt LICENSE.txt MANIFEST.in README.rst setup.py django_threadedcomments.egg-info/PKG-INFO django_threadedcomments.egg-info/SOURCES.txt django_threadedcomments.egg-info/dependency_links.txt django_threadedcomments.egg-info/not-zip-safe django_threadedcomments.egg-info/top_level.txt threadedcomments/__init__.py threadedcomments/admin.py threadedcomments/forms.py threadedcomments/models.py threadedcomments/tests.py threadedcomments/util.py threadedcomments/fixtures/simple_tree.json threadedcomments/management/__init__.py threadedcomments/management/commands/__init__.py threadedcomments/management/commands/migrate_comments.py threadedcomments/management/commands/migrate_threaded_comments.py threadedcomments/migrations/0001_initial.py threadedcomments/migrations/0002_set__last_child_id__on_delete__set_null.py threadedcomments/migrations/__init__.py threadedcomments/sql/threadedcomment.mysql.sql threadedcomments/templates/sample_tree.html threadedcomments/templates/comments/list.html threadedcomments/templatetags/__init__.py threadedcomments/templatetags/threadedcomments_tags.pydjango-threadedcomments-0.9.0/django_threadedcomments.egg-info/top_level.txt0000644000076500007650000000002112144724431030771 0ustar diederikdiederik00000000000000threadedcomments django-threadedcomments-0.9.0/LICENSE.txt0000644000076500007650000000300711761765067021545 0ustar diederikdiederik00000000000000Copyright (c) 2009, Eric Florenzano and Thejaswi Puthraya All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-threadedcomments-0.9.0/MANIFEST.in0000644000076500007650000000035212130514705021436 0ustar diederikdiederik00000000000000include CHANGELOG.rst include CONTRIBUTORS.txt include README.rst include LICENSE.txt recursive-include threadedcomments/fixtures *.json recursive-include threadedcomments/templates *.html recursive-include threadedcomments/sql *.sql django-threadedcomments-0.9.0/PKG-INFO0000644000076500007650000002003012144724431020774 0ustar diederikdiederik00000000000000Metadata-Version: 1.0 Name: django-threadedcomments Version: 0.9.0 Summary: A simple yet flexible threaded commenting system. Home-page: https://github.com/HonzaKral/django-threadedcomments Author: Diederik van der Boor Author-email: vdboor@edoburu.nl License: BSD Download-URL: https://github.com/HonzaKral/django-threadedcomments/zipball/master Description: django-threadedcomments ======================= *threadedcomments* is a Django application which allows for the simple creation of a threaded commenting system. Commenters can reply both to the original item, and reply to other comments as well. The application is (as of 0.9) built on top of django.contrib.comments, which allows it to be easily extended by other modules. Installation ============ Install the package via pip:: pip install django-threadedcomments It's preferred to install the module in a virtual environment. Configuration ------------- Add the following to ``settings.py``:: INSTALLED_APPS += ( 'threadedcomments', 'django.contrib.comments', ) COMMENTS_APP = 'threadedcomments' By placing the ``threadedcomments`` app above the ``django.contrib.comments`` application, the placeholder ``comments/list.html`` template will already be replaced by a threaded view. Make sure django.contrib.comments_ is configured in ``urls.py``:: urlpatterns += patterns('', url(r'^articles/comments/', include('django.contrib.comments.urls')), ) Provide a template that displays the comments for the ``object`` (e.g. article or blog entry):: {% load threadedcomments_tags %} ...

Comments for {{ object.title }}:

{% render_comment_list for object %} {% render_comment_form for object %} Template design --------------- Naturally, it's desirable to write your own version of ``comments/list.html`` in your project, or use one of the ``comments/app/list.html`` or ``comments/app/model/list.html`` overrides. Make sure to override ``comments/base.html`` as well, so the other views of django.contrib.comments_ are displayed using your web site design. The other templates of django.contrib.comments_ are very plain as well on purpose (for example ``comments/posted.html``), since these pages depend on the custom design of the web site. See the provided ``example`` app for a basic configuration, including a JavaScript-based reply form that moves to the comment the visitor replies to. Template tags ============= The ``threadedcomments_tags`` library is a drop-in replacement for the ``comments`` library that is required for the plain comments. The tags are forwards compatible; they support the same syntax as django.contrib.comments_ provides, and they add a few extra parameters. Fething comment counts:: {% get_comment_count for [object] as [varname] %} {% get_comment_count for [object] as [varname] root_only %} {% get_comment_count for [app].[model] [id] as [varname] %} {% get_comment_count for [app].[model] [id] as [varname] root_only %} Fetching the comments list:: {% get_comment_list for [object] as [varname] %} {% get_comment_list for [object] as [varname] flat %} {% get_comment_list for [object] as [varname] root_only %} Rendering the comments list:: {% render_comment_list for [object] %} {% render_comment_list for [object] root_only %} {% render_comment_list for [app].[model] [id] %} {% render_comment_list for [app].[model] [id] root_only %} Fetching the comment form:: {% get_comment_form for [object] as [varname] %} {% get_comment_form for [object] as [varname] with [parent_id] %} {% get_comment_form for [app].[model] [id] as [varname] %} {% get_comment_form for [app].[model] [id] as [varname] with [parent_id] %} Rendering the comment form:: {% render_comment_form for [object] %} {% render_comment_form for [object] with [parent_id] %} {% render_comment_form for [app].[model] [id] %} {% render_comment_form for [app].[model] [id] with [parent_id] %} Rendering the whole tree:: {% for comment in comment_list|fill_tree|annotate_tree %} {% ifchanged comment.parent_id %}{% else %}{% endifchanged %} {% if not comment.open and not comment.close %}{% endif %} {% if comment.open %}
    {% endif %}
  • ... {% for close in comment.close %}
{% endfor %} {% endfor %} The ``fill_tree`` filter is required for pagination, it ensures that the parents of the first comment are included as well. The ``annotate_tree`` filter adds the ``open`` and ``close`` properties to the comment. Extending the module ==================== The application is built on top of the standard django.contrib.comments_ framework, which supports various signals, and template overrides to customize the comments. To customize django-threadedcomments, override the proper templates, or include the apps that provide the missing features. Front-end editing support for example, is left out on purpose. It belongs to the domain of moderation, and policies to know "who can do what". That deserves to be in a separate application, it shouldn't be in this application as it focuses on threading. The same applies to social media logins, comment subscriptions, spam protection and Ajax posting. Note that the standard framework also supports moderation, flagging, and RSS feeds too. More documentation can be found at: * `Django's comments framework `_ * `Customizing the comments framework `_ * `Example of using the in-built comments app `_ Some of the modules worth looking at are: * django-comments-spamfighter_ * django-myrecaptcha_ * django-fluent-comments_ These modules can enhance the comments system even further. .. _django.contrib.comments: https://docs.djangoproject.com/en/dev/ref/contrib/comments/ .. _django-fluent-comments: https://github.com/edoburu/django-fluent-comments/ .. _django-myrecaptcha: https://bitbucket.org/pelletier/django-myrecaptcha/ .. _django-comments-spamfighter: https://github.com/bartTC/django-comments-spamfighter/ Keywords: django,comments,threading Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules django-threadedcomments-0.9.0/README.rst0000644000076500007650000001350612144723664021407 0ustar diederikdiederik00000000000000django-threadedcomments ======================= *threadedcomments* is a Django application which allows for the simple creation of a threaded commenting system. Commenters can reply both to the original item, and reply to other comments as well. The application is (as of 0.9) built on top of django.contrib.comments, which allows it to be easily extended by other modules. Installation ============ Install the package via pip:: pip install django-threadedcomments It's preferred to install the module in a virtual environment. Configuration ------------- Add the following to ``settings.py``:: INSTALLED_APPS += ( 'threadedcomments', 'django.contrib.comments', ) COMMENTS_APP = 'threadedcomments' By placing the ``threadedcomments`` app above the ``django.contrib.comments`` application, the placeholder ``comments/list.html`` template will already be replaced by a threaded view. Make sure django.contrib.comments_ is configured in ``urls.py``:: urlpatterns += patterns('', url(r'^articles/comments/', include('django.contrib.comments.urls')), ) Provide a template that displays the comments for the ``object`` (e.g. article or blog entry):: {% load threadedcomments_tags %} ...

Comments for {{ object.title }}:

{% render_comment_list for object %} {% render_comment_form for object %} Template design --------------- Naturally, it's desirable to write your own version of ``comments/list.html`` in your project, or use one of the ``comments/app/list.html`` or ``comments/app/model/list.html`` overrides. Make sure to override ``comments/base.html`` as well, so the other views of django.contrib.comments_ are displayed using your web site design. The other templates of django.contrib.comments_ are very plain as well on purpose (for example ``comments/posted.html``), since these pages depend on the custom design of the web site. See the provided ``example`` app for a basic configuration, including a JavaScript-based reply form that moves to the comment the visitor replies to. Template tags ============= The ``threadedcomments_tags`` library is a drop-in replacement for the ``comments`` library that is required for the plain comments. The tags are forwards compatible; they support the same syntax as django.contrib.comments_ provides, and they add a few extra parameters. Fething comment counts:: {% get_comment_count for [object] as [varname] %} {% get_comment_count for [object] as [varname] root_only %} {% get_comment_count for [app].[model] [id] as [varname] %} {% get_comment_count for [app].[model] [id] as [varname] root_only %} Fetching the comments list:: {% get_comment_list for [object] as [varname] %} {% get_comment_list for [object] as [varname] flat %} {% get_comment_list for [object] as [varname] root_only %} Rendering the comments list:: {% render_comment_list for [object] %} {% render_comment_list for [object] root_only %} {% render_comment_list for [app].[model] [id] %} {% render_comment_list for [app].[model] [id] root_only %} Fetching the comment form:: {% get_comment_form for [object] as [varname] %} {% get_comment_form for [object] as [varname] with [parent_id] %} {% get_comment_form for [app].[model] [id] as [varname] %} {% get_comment_form for [app].[model] [id] as [varname] with [parent_id] %} Rendering the comment form:: {% render_comment_form for [object] %} {% render_comment_form for [object] with [parent_id] %} {% render_comment_form for [app].[model] [id] %} {% render_comment_form for [app].[model] [id] with [parent_id] %} Rendering the whole tree:: {% for comment in comment_list|fill_tree|annotate_tree %} {% ifchanged comment.parent_id %}{% else %}{% endifchanged %} {% if not comment.open and not comment.close %}{% endif %} {% if comment.open %}
    {% endif %}
  • ... {% for close in comment.close %}
{% endfor %} {% endfor %} The ``fill_tree`` filter is required for pagination, it ensures that the parents of the first comment are included as well. The ``annotate_tree`` filter adds the ``open`` and ``close`` properties to the comment. Extending the module ==================== The application is built on top of the standard django.contrib.comments_ framework, which supports various signals, and template overrides to customize the comments. To customize django-threadedcomments, override the proper templates, or include the apps that provide the missing features. Front-end editing support for example, is left out on purpose. It belongs to the domain of moderation, and policies to know "who can do what". That deserves to be in a separate application, it shouldn't be in this application as it focuses on threading. The same applies to social media logins, comment subscriptions, spam protection and Ajax posting. Note that the standard framework also supports moderation, flagging, and RSS feeds too. More documentation can be found at: * `Django's comments framework `_ * `Customizing the comments framework `_ * `Example of using the in-built comments app `_ Some of the modules worth looking at are: * django-comments-spamfighter_ * django-myrecaptcha_ * django-fluent-comments_ These modules can enhance the comments system even further. .. _django.contrib.comments: https://docs.djangoproject.com/en/dev/ref/contrib/comments/ .. _django-fluent-comments: https://github.com/edoburu/django-fluent-comments/ .. _django-myrecaptcha: https://bitbucket.org/pelletier/django-myrecaptcha/ .. _django-comments-spamfighter: https://github.com/bartTC/django-comments-spamfighter/ django-threadedcomments-0.9.0/setup.cfg0000644000076500007650000000007312144724431021525 0ustar diederikdiederik00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 django-threadedcomments-0.9.0/setup.py0000755000076500007650000000340512144723172021424 0ustar diederikdiederik00000000000000#!/usr/bin/env python from setuptools import setup, find_packages from os import path import codecs import re def read(*parts): file_path = path.join(path.dirname(__file__), *parts) return codecs.open(file_path, encoding='utf-8').read() def find_version(*parts): version_file = read(*parts) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return str(version_match.group(1)) raise RuntimeError("Unable to find version string.") setup( name='django-threadedcomments', version=find_version('threadedcomments', '__init__.py'), license='BSD', description='A simple yet flexible threaded commenting system.', long_description=read('README.rst'), keywords='django,comments,threading', author='Eric Florenzano', author_email='floguy@gmail.com', maintainer='Diederik van der Boor', maintainer_email='vdboor@edoburu.nl', url='https://github.com/HonzaKral/django-threadedcomments', download_url='https://github.com/HonzaKral/django-threadedcomments/zipball/master', packages=find_packages(exclude=('example*',)), include_package_data=True, zip_safe=False, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) django-threadedcomments-0.9.0/threadedcomments/0000755000076500007650000000000012144724431023232 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/__init__.py0000644000076500007650000000044712144723117025350 0ustar diederikdiederik00000000000000""" Change the attributes you want to customize """ from threadedcomments.models import ThreadedComment from threadedcomments.forms import ThreadedCommentForm # following PEP 386 __version__ = "0.9.0" def get_model(): return ThreadedComment def get_form(): return ThreadedCommentForm django-threadedcomments-0.9.0/threadedcomments/admin.py0000644000076500007650000000202511762701135024674 0ustar diederikdiederik00000000000000from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from django.contrib.comments.admin import CommentsAdmin from threadedcomments.models import ThreadedComment class ThreadedCommentsAdmin(CommentsAdmin): fieldsets = ( (None, {'fields': ('content_type', 'object_pk', 'site')} ), (_('Content'), {'fields': ('user', 'user_name', 'user_email', 'user_url', 'title', 'comment')} ), (_('Hierarchy'), {'fields': ('parent',)} ), (_('Metadata'), {'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')} ), ) list_display = ('name', 'title', 'content_type', 'object_pk', 'parent', 'ip_address', 'submit_date', 'is_public', 'is_removed') search_fields = ('title', 'comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') raw_id_fields = ("parent",) admin.site.register(ThreadedComment, ThreadedCommentsAdmin) django-threadedcomments-0.9.0/threadedcomments/fixtures/0000755000076500007650000000000012144724431025103 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/fixtures/simple_tree.json0000644000076500007650000000777311762704767030343 0ustar diederikdiederik00000000000000[ { "pk": 1, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001", "last_child": 4, "parent": null } }, { "pk": 2, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001/0000000002", "last_child": 5, "parent": 1 } }, { "pk": 3, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001/0000000002/0000000003", "last_child": null, "parent": 2 } }, { "pk": 5, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001/0000000002/0000000005", "last_child": null, "parent": 2 } }, { "pk": 4, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001/0000000004", "last_child": 6, "parent": 1 } }, { "pk": 6, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000001/0000000004/0000000006", "last_child": null, "parent": 4 } }, { "pk": 7, "model": "threadedcomments.threadedcomment", "fields": { "tree_path": "0000000007", "last_child": null, "parent": null } }, { "pk": 1, "model": "comments.comment", "fields": { "comment": "Comment 1", "user_url": "", "submit_date": "2009-03-31 17:58:09", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 2, "model": "comments.comment", "fields": { "comment": "Comment 2", "user_url": "", "submit_date": "2009-03-31 17:58:12", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 3, "model": "comments.comment", "fields": { "comment": "Comment 3", "user_url": "", "submit_date": "2009-03-31 17:58:28", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 4, "model": "comments.comment", "fields": { "comment": "Comment 4", "user_url": "", "submit_date": "2009-03-31 17:58:50", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 5, "model": "comments.comment", "fields": { "comment": "Comment 5", "user_url": "", "submit_date": "2009-03-31 17:58:57", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 6, "model": "comments.comment", "fields": { "comment": "Comment 6", "user_url": "", "submit_date": "2009-03-31 17:59:17", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } }, { "pk": 7, "model": "comments.comment", "fields": { "comment": "Comment 7", "user_url": "", "submit_date": "2009-03-31 17:59:25", "ip_address": null, "object_pk": "1", "site": 1, "is_removed": false, "user": null, "content_type": 7, "is_public": true, "user_name": "", "user_email": "" } } ] django-threadedcomments-0.9.0/threadedcomments/forms.py0000644000076500007650000000221611762701135024734 0ustar diederikdiederik00000000000000from django import forms from django.contrib.comments.forms import CommentForm from django.conf import settings from django.utils.translation import ugettext_lazy as _ from threadedcomments.models import ThreadedComment class ThreadedCommentForm(CommentForm): parent = forms.IntegerField(required=False, widget=forms.HiddenInput) def __init__(self, target_object, parent=None, data=None, initial=None): self.base_fields.insert( self.base_fields.keyOrder.index('comment'), 'title', forms.CharField(label=_('Title'), required=False, max_length=getattr(settings, 'COMMENTS_TITLE_MAX_LENGTH', 255)) ) self.parent = parent if initial is None: initial = {} initial.update({'parent': self.parent}) super(ThreadedCommentForm, self).__init__(target_object, data=data, initial=initial) def get_comment_model(self): return ThreadedComment def get_comment_create_data(self): d = super(ThreadedCommentForm, self).get_comment_create_data() d['parent_id'] = self.cleaned_data['parent'] d['title'] = self.cleaned_data['title'] return d django-threadedcomments-0.9.0/threadedcomments/management/0000755000076500007650000000000012144724431025346 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/management/__init__.py0000644000076500007650000000000111761765067027464 0ustar diederikdiederik00000000000000 django-threadedcomments-0.9.0/threadedcomments/management/commands/0000755000076500007650000000000012144724431027147 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/management/commands/__init__.py0000644000076500007650000000000011761765067031264 0ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/management/commands/migrate_comments.py0000644000076500007650000000166711761765067033106 0ustar diederikdiederik00000000000000from django.core.management.base import NoArgsCommand from django.db import transaction, connection from django.conf import settings PATH_DIGITS = getattr(settings, 'COMMENT_PATH_DIGITS', 10) SQL = """ INSERT INTO threadedcomments_comment ( comment_ptr_id, parent_id, last_child_id, tree_path, title ) SELECT id as comment_ptr_id, null as parent_id, null as last_child_id, (SELECT TO_CHAR(id, '%s')) AS tree_path, '' FROM django_comments; """ % ''.zfill(PATH_DIGITS) class Command(NoArgsCommand): help = "Migrates from django.contrib.comments to django-threadedcomments" def handle(self, *args, **options): transaction.commit_unless_managed() transaction.enter_transaction_management() transaction.managed(True) cursor = connection.cursor() cursor.execute(SQL) transaction.commit() transaction.leave_transaction_management() django-threadedcomments-0.9.0/threadedcomments/management/commands/migrate_threaded_comments.py0000644000076500007650000000674412130514265034726 0ustar diederikdiederik00000000000000from django.core.management.base import NoArgsCommand from django.contrib.sites.models import Site from django.db import transaction, connection from django.conf import settings from threadedcomments.models import ThreadedComment USER_SQL = """ SELECT id, content_type_id, object_id, parent_id, user_id, date_submitted, date_modified, date_approved, comment, markup, is_public, is_approved, ip_address FROM threadedcomments_threadedcomment ORDER BY id ASC """ FREE_SQL = """ SELECT id, content_type_id, object_id, parent_id, name, website, email, date_submitted, date_modified, date_approved, comment, markup, is_public, is_approved, ip_address FROM threadedcomments_freethreadedcomment ORDER BY id ASC """ PATH_SEPARATOR = getattr(settings, 'COMMENT_PATH_SEPARATOR', '/') PATH_DIGITS = getattr(settings, 'COMMENT_PATH_DIGITS', 10) class Command(NoArgsCommand): help = "Migrates django-threadedcomments <= 0.5 to the new model structure" def handle(self, *args, **options): transaction.commit_unless_managed() transaction.enter_transaction_management() transaction.managed(True) site = Site.objects.all()[0] cursor = connection.cursor() cursor.execute(FREE_SQL) for row in cursor: (id, content_type_id, object_id, parent_id, name, website, email, date_submitted, date_modified, date_approved, comment, markup, is_public, is_approved, ip_address) = row tc = ThreadedComment( pk=id, content_type_id=content_type_id, object_pk=object_id, user_name=name, user_email=email, user_url=website, comment=comment, submit_date=date_submitted, ip_address=ip_address, is_public=is_public, is_removed=not is_approved, parent_id=parent_id, site=site, ) tc.save(skip_tree_path=True) cursor = connection.cursor() cursor.execute(USER_SQL) for row in cursor: (id, content_type_id, object_id, parent_id, user_id, date_submitted, date_modified, date_approved, comment, markup, is_public, is_approved, ip_address) = row tc = ThreadedComment( pk=id, content_type_id=content_type_id, object_pk=object_id, user_id=user_id, comment=comment, submit_date=date_submitted, ip_address=ip_address, is_public=is_public, is_removed=not is_approved, parent_id=parent_id, site=site, ) tc.save(skip_tree_path=True) for comment in ThreadedComment.objects.all(): path = [str(comment.id).zfill(PATH_DIGITS)] current = comment while current.parent: current = current.parent path.append(str(current.id).zfill(PATH_DIGITS)) comment.tree_path = PATH_SEPARATOR.join(reversed(path)) comment.save(skip_tree_path=True) if comment.parent: ThreadedComment.objects.filter(pk=comment.parent.pk).update( last_child=comment) transaction.commit() transaction.leave_transaction_management() django-threadedcomments-0.9.0/threadedcomments/migrations/0000755000076500007650000000000012144724431025406 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/migrations/0001_initial.py0000644000076500007650000001603011762704767030070 0ustar diederikdiederik00000000000000# -*- coding: utf-8 -*- import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'ThreadedComment' db.create_table('threadedcomments_comment', ( ('comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['comments.Comment'], unique=True, primary_key=True)), ('title', self.gf('django.db.models.fields.TextField')(blank=True)), ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['threadedcomments.ThreadedComment'])), ('last_child', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['threadedcomments.ThreadedComment'], null=True, blank=True)), ('tree_path', self.gf('django.db.models.fields.TextField')(db_index=True)), )) db.send_create_signal('threadedcomments', ['ThreadedComment']) def backwards(self, orm): # Deleting model 'ThreadedComment' db.delete_table('threadedcomments_comment') models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) }, 'auth.permission': { 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, 'auth.user': { 'Meta': {'object_name': 'User'}, 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) }, 'comments.comment': { 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'object_pk': ('django.db.models.fields.TextField', [], {}), 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) }, 'contenttypes.contenttype': { 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, 'sites.site': { 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, 'threadedcomments.threadedcomment': { 'Meta': {'ordering': "('tree_path',)", 'object_name': 'ThreadedComment', 'db_table': "'threadedcomments_comment'", '_ormbases': ['comments.Comment']}, 'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['comments.Comment']", 'unique': 'True', 'primary_key': 'True'}), 'last_child': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['threadedcomments.ThreadedComment']", 'null': 'True', 'blank': 'True'}), 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['threadedcomments.ThreadedComment']"}), 'title': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 'tree_path': ('django.db.models.fields.TextField', [], {'db_index': 'True'}) } } complete_apps = ['threadedcomments']././@LongLink0000000000000000000000000000015200000000000011213 Lustar 00000000000000django-threadedcomments-0.9.0/threadedcomments/migrations/0002_set__last_child_id__on_delete__set_null.pydjango-threadedcomments-0.9.0/threadedcomments/migrations/0002_set__last_child_id__on_delete__set_nu0000644000076500007650000001551211762651360035470 0ustar diederikdiederik00000000000000# -*- coding: utf-8 -*- import datetime import south from south.db import db from south.v2 import SchemaMigration from django.db import models class Migration(SchemaMigration): def forwards(self, orm): # Changing field 'ThreadedComment.last_child' db.alter_column('threadedcomments_comment', 'last_child_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['threadedcomments.ThreadedComment'], null=True, on_delete=models.SET_NULL)) if south.__version__ <= "0.7.4" and not db.dry_run: print " * WARNING: Your South version is not able to add ON DELETE SET NULL. Please fix this manually." def backwards(self, orm): # Changing field 'ThreadedComment.last_child' db.alter_column('threadedcomments_comment', 'last_child_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['threadedcomments.ThreadedComment'], null=True)) models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) }, 'auth.permission': { 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, 'auth.user': { 'Meta': {'object_name': 'User'}, 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) }, 'comments.comment': { 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'object_pk': ('django.db.models.fields.TextField', [], {}), 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) }, 'contenttypes.contenttype': { 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, 'sites.site': { 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, 'threadedcomments.threadedcomment': { 'Meta': {'ordering': "('tree_path',)", 'object_name': 'ThreadedComment', 'db_table': "'threadedcomments_comment'", '_ormbases': ['comments.Comment']}, 'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['comments.Comment']", 'unique': 'True', 'primary_key': 'True'}), 'last_child': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['threadedcomments.ThreadedComment']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['threadedcomments.ThreadedComment']"}), 'title': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 'tree_path': ('django.db.models.fields.TextField', [], {'db_index': 'True'}) } } complete_apps = ['threadedcomments']django-threadedcomments-0.9.0/threadedcomments/migrations/__init__.py0000644000076500007650000000000011762704611027510 0ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/models.py0000644000076500007650000000450412130514265025067 0ustar diederikdiederik00000000000000from django.db import models from django.contrib.comments.models import Comment from django.contrib.comments.managers import CommentManager from django.conf import settings from django.utils.translation import ugettext_lazy as _ PATH_SEPARATOR = getattr(settings, 'COMMENT_PATH_SEPARATOR', '/') PATH_DIGITS = getattr(settings, 'COMMENT_PATH_DIGITS', 10) class ThreadedComment(Comment): title = models.TextField(_('Title'), blank=True) parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children', verbose_name=_('Parent')) last_child = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_('Last child')) tree_path = models.TextField(_('Tree path'), editable=False, db_index=True) objects = CommentManager() @property def depth(self): return len(self.tree_path.split(PATH_SEPARATOR)) @property def root_id(self): return int(self.tree_path.split(PATH_SEPARATOR)[0]) @property def root_path(self): return ThreadedComment.objects.filter(pk__in=self.tree_path.split(PATH_SEPARATOR)[:-1]) def save(self, *args, **kwargs): skip_tree_path = kwargs.pop('skip_tree_path', False) super(ThreadedComment, self).save(*args, **kwargs) if skip_tree_path: return None tree_path = unicode(self.pk).zfill(PATH_DIGITS) if self.parent: tree_path = PATH_SEPARATOR.join((self.parent.tree_path, tree_path)) self.parent.last_child = self ThreadedComment.objects.filter(pk=self.parent_id).update(last_child=self) self.tree_path = tree_path ThreadedComment.objects.filter(pk=self.pk).update(tree_path=self.tree_path) def delete(self, *args, **kwargs): # Fix last child on deletion. if self.parent_id: prev_child_id = ThreadedComment.objects.filter(parent=self.parent_id).exclude(pk=self.pk).order_by('-submit_date').values_list('pk', flat=True)[0] ThreadedComment.objects.filter(pk=self.parent_id).update(last_child=prev_child_id) super(ThreadedComment, self).delete(*args, **kwargs) class Meta(object): ordering = ('tree_path',) db_table = 'threadedcomments_comment' verbose_name = _('Threaded comment') verbose_name_plural = _('Threaded comments') django-threadedcomments-0.9.0/threadedcomments/sql/0000755000076500007650000000000012144724431024031 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/sql/threadedcomment.mysql.sql0000644000076500007650000000014411761765067031076 0ustar diederikdiederik00000000000000CREATE INDEX `threadedcomments_comment_tree_path` ON `threadedcomments_comment` (`tree_path`(255)); django-threadedcomments-0.9.0/threadedcomments/templates/0000755000076500007650000000000012144724431025230 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/templates/comments/0000755000076500007650000000000012144724431027055 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/templates/comments/list.html0000644000076500007650000000162111762710506030721 0ustar diederikdiederik00000000000000{% comment %} This template replaces the default "comments/list.html" of "django.contrib.comments", to allow rendering the comments as tree. {% endcomment %} {% load threadedcomments_tags %}
{% for comment in comment_list|fill_tree|annotate_tree %}{% ifchanged comment.parent_id %}{% else %}{% endifchanged %}{% if not comment.open and not comment.close %}{% endif %}{% if comment.open %}
    {% endif %}
  • {# c## is used by the absolute URL of the Comment model, so keep that as it is. #}
    {{ comment.submit_date }} - {{ comment.name }}
    {{ comment.comment|linebreaks }}
    {% for close in comment.close %}
{% endfor %} {% endfor %}
django-threadedcomments-0.9.0/threadedcomments/templates/sample_tree.html0000644000076500007650000000101711762703733030424 0ustar diederikdiederik00000000000000{% load threadedcomments_tags %}{# This file is used by the unittests #} {% for comment in comment_list|fill_tree|annotate_tree %} {% ifchanged comment.parent_id %}{% else %} {% endifchanged %} {% if not comment.open and not comment.close %} {% endif %} {% if comment.open %}
    {% endif %} {{ comment.tree_path }}{% if comment.added_path %} ADDED {% endif %} {% for close in comment.close %}
{% endfor %} {% endfor %} django-threadedcomments-0.9.0/threadedcomments/templatetags/0000755000076500007650000000000012144724431025724 5ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/templatetags/__init__.py0000644000076500007650000000000011761765067030041 0ustar diederikdiederik00000000000000django-threadedcomments-0.9.0/threadedcomments/templatetags/threadedcomments_tags.py0000644000076500007650000003230411762713004032643 0ustar diederikdiederik00000000000000from django import template from django.template.loader import render_to_string from django.contrib.comments.templatetags.comments import BaseCommentNode from django.contrib import comments from threadedcomments.util import annotate_tree_properties, fill_tree as real_fill_tree register = template.Library() class BaseThreadedCommentNode(BaseCommentNode): def __init__(self, parent=None, flat=False, root_only=False, **kwargs): self.parent = parent self.flat = flat self.root_only = root_only super(BaseThreadedCommentNode, self).__init__(**kwargs) @classmethod def handle_token(cls, parser, token): tokens = token.contents.split() if len(tokens) > 2: if tokens[1] != 'for': raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) extra_kw = {} if tokens[-1] in ('flat', 'root_only'): extra_kw[str(tokens.pop())] = True if len(tokens) == 5: # {% get_whatever for obj as varname %} if tokens[3] != 'as': raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0]) return cls( object_expr=parser.compile_filter(tokens[2]), as_varname=tokens[4], **extra_kw ) elif len(tokens) == 6: # {% get_whatever for app.model pk as varname %} if tokens[4] != 'as': raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0]) return cls( ctype=BaseThreadedCommentNode.lookup_content_type(tokens[2], tokens[0]), object_pk_expr=parser.compile_filter(tokens[3]), as_varname=tokens[5], **extra_kw ) else: raise template.TemplateSyntaxError("%r tag takes either 5 or 6 arguments" % (tokens[0],)) def get_query_set(self, context): qs = super(BaseThreadedCommentNode, self).get_query_set(context) if self.flat: qs = qs.order_by('-submit_date') elif self.root_only: qs = qs.exclude(parent__isnull=False).order_by('-submit_date') return qs class CommentListNode(BaseThreadedCommentNode): """ Insert a list of comments into the context. """ def get_context_value_from_queryset(self, context, qs): return list(qs) class CommentCountNode(CommentListNode): """ Insert a count of comments into the context. """ def get_context_value_from_queryset(self, context, qs): return qs.count() class CommentFormNode(BaseThreadedCommentNode): """ Insert a form for the comment model into the context. """ @classmethod def handle_token(cls, parser, token): tokens = token.contents.split() if tokens[1] != 'for': raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % (tokens[0],)) if len(tokens) < 7: # Default get_comment_form code return super(CommentFormNode, cls).handle_token(parser, token) elif len(tokens) == 7: # {% get_comment_form for [object] as [varname] with [parent_id] %} if tokens[-2] != u'with': raise template.TemplateSyntaxError("%r tag must have a 'with' as the last but one argument." % (tokens[0],)) return cls( object_expr=parser.compile_filter(tokens[2]), as_varname=tokens[4], parent=parser.compile_filter(tokens[6]), ) elif len(tokens) == 8: # {% get_comment_form for [app].[model] [object_id] as [varname] with [parent_id] %} if tokens[-2] != u'with': raise template.TemplateSyntaxError("%r tag must have a 'with' as the last but one argument." % (tokens[0],)) return cls( ctype=BaseThreadedCommentNode.lookup_content_type(tokens[2], tokens[0]), object_pk_expr=parser.compile_filter(tokens[3]), as_varname=tokens[5], parent=parser.compile_filter(tokens[7]), ) def get_form(self, context): parent_id = None if self.parent: parent_id = self.parent.resolve(context, ignore_failures=True) obj = self.get_object(context) if obj: return comments.get_form()(obj, parent=parent_id) else: return None def get_object(self, context): if self.object_expr: try: return self.object_expr.resolve(context) except template.VariableDoesNotExist: return None else: object_pk = self.object_pk_expr.resolve(context, ignore_failures=True) return self.ctype.get_object_for_this_type(pk=object_pk) def render(self, context): context[self.as_varname] = self.get_form(context) return '' class RenderCommentFormNode(CommentFormNode): @classmethod def handle_token(cls, parser, token): """ Class method to parse render_comment_form and return a Node. """ tokens = token.contents.split() if tokens[1] != 'for': raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) if len(tokens) == 3: # {% render_comment_form for obj %} return cls(object_expr=parser.compile_filter(tokens[2])) elif len(tokens) == 4: # {% render_comment_form for app.model object_pk %} return cls( ctype=BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), object_pk_expr=parser.compile_filter(tokens[3]) ) elif len(tokens) == 5: # {% render_comment_form for obj with parent_id %} if tokens[-2] != u'with': raise template.TemplateSyntaxError("%r tag must have 'with' as the last but one argument" % (tokens[0],)) return cls( object_expr=parser.compile_filter(tokens[2]), parent=parser.compile_filter(tokens[4]) ) elif len(tokens) == 6: # {% render_comment_form for app.model object_pk with parent_id %} if tokens[-2] != u'with': raise template.TemplateSyntaxError("%r tag must have 'with' as the last but one argument" % (tokens[0],)) return cls( ctype=BaseThreadedCommentNode.lookup_content_type(tokens[2], tokens[0]), object_pk_expr=parser.compile_filter(tokens[3]), parent=parser.compile_filter(tokens[5]) ) else: raise template.TemplateSyntaxError("%r tag takes 2 to 5 arguments" % (tokens[0],)) def render(self, context): ctype, object_pk = self.get_target_ctype_pk(context) if object_pk: template_search_list = ( "comments/%s/%s/form.html" % (ctype.app_label, ctype.model), "comments/%s/form.html" % ctype.app_label, "comments/form.html", ) context.push() form_str = render_to_string( template_search_list, {"form" : self.get_form(context)}, context ) context.pop() return form_str else: return '' class RenderCommentListNode(CommentListNode): """ Render the comments list. """ # By having this class added, this module is a drop-in replacement for ``{% load comments %}``. @classmethod def handle_token(cls, parser, token): tokens = token.contents.split() if tokens[1] != 'for': raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) extra_kw = {} if tokens[-1] in ('flat', 'root_only'): extra_kw[str(tokens.pop())] = True if len(tokens) == 3: # {% render_comment_list for obj %} return cls( object_expr=parser.compile_filter(tokens[2]), **extra_kw ) elif len(tokens) == 4: # {% render_comment_list for app.models pk %} return cls( ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), object_pk_expr = parser.compile_filter(tokens[3]), **extra_kw ) else: raise template.TemplateSyntaxError("%r tag takes either 2 or 3 arguments" % (tokens[0],)) def render(self, context): ctype, object_pk = self.get_target_ctype_pk(context) if object_pk: template_search_list = [ "comments/%s/%s/list.html" % (ctype.app_label, ctype.model), "comments/%s/list.html" % ctype.app_label, "comments/list.html" ] qs = self.get_query_set(context) context.push() liststr = render_to_string(template_search_list, { "comment_list" : self.get_context_value_from_queryset(context, qs) }, context) context.pop() return liststr else: return '' @register.tag def get_comment_count(parser, token): """ Gets the comment count for the given params and populates the template context with a variable containing that value, whose name is defined by the 'as' clause. Syntax:: {% get_comment_count for [object] as [varname] %} {% get_comment_count for [app].[model] [object_id] as [varname] %} Example usage:: {% get_comment_count for event as comment_count %} {% get_comment_count for calendar.event event.id as comment_count %} {% get_comment_count for calendar.event 17 as comment_count %} """ return CommentCountNode.handle_token(parser, token) @register.tag def get_comment_list(parser, token): """ Gets the list of comments for the given params and populates the template context with a variable containing that value, whose name is defined by the 'as' clause. Syntax:: {% get_comment_list for [object] as [varname] %} {% get_comment_list for [object] as [varname] [flat|root_only] %} {% get_comment_list for [app].[model] [object_id] as [varname] %} {% get_comment_list for [app].[model] [object_id] as [varname] [flat|root_only] %} Example usage:: {% get_comment_list for event as comment_list %} {% for comment in comment_list %} ... {% endfor %} {% get_comment_list for event as comment_list flat %} """ return CommentListNode.handle_token(parser, token) @register.tag def render_comment_list(parser, token): """ Render the comment list (as returned by ``{% get_comment_list %}``) through the ``comments/list.html`` template Syntax:: {% render_comment_list for [object] %} {% render_comment_list for [app].[model] [object_id] %} {% render_comment_list for [object] [flat|root_only] %} {% render_comment_list for [app].[model] [object_id] [flat|root_only] %} Example usage:: {% render_comment_list for event %} """ return RenderCommentListNode.handle_token(parser, token) @register.tag def get_comment_form(parser, token): """ Get a (new) form object to post a new comment. Syntax:: {% get_comment_form for [object] as [varname] %} {% get_comment_form for [object] as [varname] with [parent_id] %} {% get_comment_form for [app].[model] [object_id] as [varname] %} {% get_comment_form for [app].[model] [object_id] as [varname] with [parent_id] %} """ return CommentFormNode.handle_token(parser, token) @register.tag def render_comment_form(parser, token): """ Render the comment form (as returned by ``{% render_comment_form %}``) through the ``comments/form.html`` template. Syntax:: {% render_comment_form for [object] %} {% render_comment_form for [object] with [parent_id] %} {% render_comment_form for [app].[model] [object_id] %} {% render_comment_form for [app].[model] [object_id] with [parent_id] %} """ return RenderCommentFormNode.handle_token(parser, token) @register.filter def annotate_tree(comments): """ Add ``open``, ``close`` properties to the comments, to render the tree. Syntax:: {% for comment in comment_list|annotate_tree %} {% ifchanged comment.parent_id %}{% else %}{% endifchanged %} {% if not comment.open and not comment.close %}{% endif %} {% if comment.open %}
    {% endif %}
  • ... {% for close in comment.close %}
{% endfor %} {% endfor %} When the :func:`fill_tree` filter, place the ``annotate_tree`` code after it:: {% for comment in comment_list|fill_tree|annotate_tree %} ... {% endfor %} """ return annotate_tree_properties(comments) @register.filter def fill_tree(comments): """ When paginating the comments, insert the parent nodes of the first comment. Syntax:: {% for comment in comment_list|annotate_tree %} ... {% endfor %} """ return real_fill_tree(comments) django-threadedcomments-0.9.0/threadedcomments/tests.py0000644000076500007650000002467712130514265024763 0ustar diederikdiederik00000000000000from unittest import TestCase from django.test import TransactionTestCase from django.contrib import comments from django.contrib.sites.models import Site from django.template import loader, TemplateSyntaxError from django.conf import settings from threadedcomments.util import annotate_tree_properties from threadedcomments.templatetags import threadedcomments_tags as tags PATH_SEPARATOR = getattr(settings, 'COMMENT_PATH_SEPARATOR', '/') PATH_DIGITS = getattr(settings, 'COMMENT_PATH_DIGITS', 10) def sanitize_html(html): return '\n'.join((i.strip() for i in html.split('\n') if i.strip() != '')) class SanityTests(TransactionTestCase): BASE_DATA = { 'name': u'Eric Florenzano', 'email': u'floguy@gmail.com', 'comment': u'This is my favorite Django app ever!', } def _post_comment(self, data=None, parent=None): Comment = comments.get_model() body = self.BASE_DATA.copy() if data: body.update(data) url = comments.get_form_target() args = [Site.objects.all()[0]] kwargs = {} if parent is not None: kwargs['parent'] = unicode(parent.pk) body['parent'] = unicode(parent.pk) form = comments.get_form()(*args, **kwargs) body.update(form.generate_security_data()) self.client.post(url, body, follow=True) return Comment.objects.order_by('-id')[0] def test_post_comment(self): Comment = comments.get_model() self.assertEqual(Comment.objects.count(), 0) comment = self._post_comment() self.assertEqual(comment.tree_path, str(comment.pk).zfill(PATH_DIGITS)) self.assertEqual(Comment.objects.count(), 1) self.assertEqual(comment.last_child, None) def test_post_comment_child(self): Comment = comments.get_model() comment = self._post_comment() self.assertEqual(comment.tree_path, str(comment.pk).zfill(PATH_DIGITS)) child_comment = self._post_comment(data={'name': 'ericflo'}, parent=comment) comment_pk = str(comment.pk).zfill(PATH_DIGITS) child_comment_pk = str(child_comment.pk).zfill(PATH_DIGITS) self.assertEqual(child_comment.tree_path, PATH_SEPARATOR.join((comment.tree_path, child_comment_pk))) self.assertEqual(comment.pk, child_comment.parent.pk) comment = comments.get_model().objects.get(pk=comment.pk) self.assertEqual(comment.last_child, child_comment) class HierarchyTest(TransactionTestCase): fixtures = ['simple_tree'] EXPECTED_HTML_PARTIAL = sanitize_html('''
  • 0000000001 ADDED
    • 0000000001/0000000004 ADDED
      • 0000000001/0000000004/0000000006
  • 0000000007
''') EXPECTED_HTML_FULL = sanitize_html('''
  • 0000000001
    • 0000000001/0000000002
      • 0000000001/0000000002/0000000003
      • 0000000001/0000000002/0000000005
    • 0000000001/0000000004
      • 0000000001/0000000004/0000000006
  • 0000000007
''') def test_root_path_returns_empty_for_root_comments(self): c = comments.get_model().objects.get(pk=7) self.assertEqual([], [x.pk for x in c.root_path]) def test_root_path_returns_only_correct_nodes(self): c = comments.get_model().objects.get(pk=6) self.assertEqual([1, 4], [x.pk for x in c.root_path]) def test_root_id_returns_self_for_root_comments(self): c = comments.get_model().objects.get(pk=7) self.assertEqual(c.pk, c.root_id) def test_root_id_returns_root_for_replies(self): c = comments.get_model().objects.get(pk=6) self.assertEqual(1, c.root_id) def test_root_has_depth_1(self): c = comments.get_model().objects.get(pk=7) self.assertEqual(1, c.depth) def test_open_and_close_match(self): depth = 0 for x in annotate_tree_properties(comments.get_model().objects.all()): depth += getattr(x, 'open', 0) self.assertEqual(x.depth, depth) depth -= len(getattr(x, 'close', [])) self.assertEqual(0, depth) def test_last_flags_set_correctly_only_on_last_sibling(self): # construct the tree nodes = {} for x in comments.get_model().objects.all(): nodes[x.pk] = (x, []) if x.parent_id: nodes[x.parent_id][1].append(x.pk) # check all the comments for x in annotate_tree_properties(comments.get_model().objects.all()): if getattr(x, 'last', False): # last comments have a parent self.assertTrue(x.parent_id) par, siblings = nodes[x.parent_id] # and ar last in their child list self.assertTrue(x.pk in siblings) self.assertEqual(len(siblings) - 1, siblings.index(x.pk)) def test_rendering_of_partial_tree(self): output = loader.render_to_string('sample_tree.html', {'comment_list': comments.get_model().objects.all()[5:]}) self.assertEqual(self.EXPECTED_HTML_PARTIAL, sanitize_html(output)) def test_rendering_of_full_tree(self): output = loader.render_to_string('sample_tree.html', {'comment_list': comments.get_model().objects.all()}) self.assertEqual(self.EXPECTED_HTML_FULL, sanitize_html(output)) def test_last_child_properly_created(self): Comment = comments.get_model() new_child_comment = Comment(comment="Comment 8", site_id=1, content_type_id=7, object_pk=1, parent_id=1) new_child_comment.save() comment = Comment.objects.get(pk=1) self.assertEqual(comment.last_child, new_child_comment) def test_last_child_doesnt_delete_parent(self): Comment = comments.get_model() comment = Comment.objects.get(pk=1) new_child_comment = Comment(comment="Comment 9", site_id=1, content_type_id=7, object_pk=1, parent_id=comment.id) new_child_comment.save() new_child_comment.delete() comment = Comment.objects.get(pk=1) def test_last_child_repointed_correctly_on_delete(self): Comment = comments.get_model() comment = Comment.objects.get(pk=1) last_child = comment.last_child new_child_comment = Comment(comment="Comment 9", site_id=1, content_type_id=7, object_pk=1, parent_id=comment.id) new_child_comment.save() comment = Comment.objects.get(pk=1) self.assertEqual(comment.last_child, new_child_comment) new_child_comment.delete() comment = Comment.objects.get(pk=1) self.assertEqual(last_child, comment.last_child) # Templatetags tests ############################################################################## class MockParser(object): "Mock parser object for handle_token()" def compile_filter(self, var): return var mock_parser = MockParser() class MockToken(object): "Mock token object for handle_token()" def __init__(self, bits): self.contents = self self.bits = bits def split(self): return self.bits class TestCommentListNode(TestCase): """ {% get_comment_list for [object] as [varname] %} {% get_comment_list for [app].[model] [object_id] as [varname] %} """ correct_ct_pk_params = ['get_comment_list', 'for', 'sites.site', '1', 'as', 'var'] correct_var_params = ['get_comment_list', 'for', 'var', 'as', 'var'] def test_parsing_fails_for_empty_token(self): self.assertRaises(TemplateSyntaxError, tags.get_comment_list, mock_parser, MockToken(['get_comment_list'])) def test_parsing_fails_if_model_not_exists(self): params = self.correct_ct_pk_params[:] params[2] = 'not_app.not_model' self.assertRaises(TemplateSyntaxError, tags.get_comment_list, mock_parser, MockToken(params)) def test_parsing_fails_if_object_not_exists(self): params = self.correct_ct_pk_params[:] params[2] = '1000' self.assertRaises(TemplateSyntaxError, tags.get_comment_list, mock_parser, MockToken(params)) def test_parsing_works_for_ct_pk_pair(self): node = tags.get_comment_list(mock_parser, MockToken(self.correct_ct_pk_params)) self.assertTrue(isinstance(node, tags.CommentListNode)) def test_parsing_works_for_var(self): node = tags.get_comment_list(mock_parser, MockToken(self.correct_var_params)) self.assertTrue(isinstance(node, tags.CommentListNode)) def test_flat_parameter_is_passed_into_the_node_for_ct_pk_pair(self): params = self.correct_ct_pk_params[:] params.append(u'flat') node = tags.get_comment_list(mock_parser, MockToken(params)) self.assertTrue(isinstance(node, tags.CommentListNode)) self.assertTrue(node.flat) def test_flat_parameter_is_passed_into_the_node_for_var(self): params = self.correct_var_params[:] params.append(u'flat') node = tags.get_comment_list(mock_parser, MockToken(params)) self.assertTrue(isinstance(node, tags.CommentListNode)) self.assertTrue(node.flat) def test_root_only_parameter_is_passed_into_the_node_for_var(self): params = self.correct_var_params[:] params.append(u'root_only') node = tags.get_comment_list(mock_parser, MockToken(params)) self.assertTrue(isinstance(node, tags.CommentListNode)) self.assertTrue(node.root_only) def test_root_only_parameter_is_passed_into_the_node_for_ct_pk_pair(self): params = self.correct_ct_pk_params[:] params.append(u'root_only') node = tags.get_comment_list(mock_parser, MockToken(params)) self.assertTrue(isinstance(node, tags.CommentListNode)) self.assertTrue(node.root_only) django-threadedcomments-0.9.0/threadedcomments/util.py0000644000076500007650000000371611762703733024577 0ustar diederikdiederik00000000000000from itertools import chain, imap __all__ = ['fill_tree', 'annotate_tree_properties', ] def _mark_as_root_path(comment): """ Mark on comment as Being added to fill the tree. """ setattr(comment, 'added_path', True) return comment def fill_tree(comments): """ Insert extra comments in the comments list, so that the root path of the first comment is always visible. Use this in comments' pagination to fill in the tree information. The inserted comments have an ``added_path`` attribute. """ if not comments: return it = iter(comments) first = it.next() extra_path_items = imap(_mark_as_root_path, first.root_path) return chain(extra_path_items, [first], it) def annotate_tree_properties(comments): """ iterate through nodes and adds some magic properties to each of them representing opening list of children and closing it """ if not comments: return it = iter(comments) # get the first item, this will fail if no items ! old = it.next() # first item starts a new thread old.open = True last = set() for c in it: # if this comment has a parent, store its last child for future reference if old.last_child_id: last.add(old.last_child_id) # this is the last child, mark it if c.pk in last: c.last = True # increase the depth if c.depth > old.depth: c.open = True else: # c.depth <= old.depth # close some depths old.close = range(old.depth - c.depth) # new thread if old.root_id != c.root_id: # close even the top depth old.close.append(len(old.close)) # and start a new thread c.open = True # empty the last set last = set() # iterate yield old old = c old.close = range(old.depth) yield old