├── .babelrc ├── .env.docker-example ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── compose └── postgres │ ├── Dockerfile │ ├── backup.sh │ ├── list-backups.sh │ └── restore.sh ├── django_comments_tree ├── __init__.py ├── abstract.py ├── admin.py ├── api │ ├── __init__.py │ ├── frontend.py │ ├── serializers.py │ └── views.py ├── apps.py ├── compat.py ├── conf │ ├── __init__.py │ └── defaults.py ├── feeds.py ├── forms │ ├── __init__.py │ ├── base.py │ └── forms.py ├── locale │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ └── fr │ │ └── LC_MESSAGES │ │ ├── django.po │ │ └── djangojs.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populate_tree_comments.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_treecomment_updated_on.py │ ├── 0003_remove_commentassociation_object_pk.py │ ├── 0004_auto_20190803_1558.py │ ├── 0005_treecomment_assoc.py │ ├── 0006_auto_20191023_1025.py │ ├── 0007_auto_20191023_1440.py │ ├── 0008_auto_20191027_2147.py │ └── __init__.py ├── models.py ├── moderation.py ├── permissions.py ├── render.py ├── signals.py ├── signed.py ├── static │ └── django_comments_tree │ │ ├── css │ │ ├── _bootswatch.scss │ │ ├── _variables.scss │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── img │ │ └── 64x64.svg │ │ └── js │ │ └── src │ │ ├── comment.jsx │ │ ├── commentbox.jsx │ │ ├── commentform.jsx │ │ ├── index.js │ │ └── lib.js ├── templates │ ├── 404.html │ ├── comments │ │ ├── 400-debug.html │ │ ├── approve.html │ │ ├── approved.html │ │ ├── base.html │ │ ├── comment_notification_email.txt │ │ ├── delete.html │ │ ├── deleted.html │ │ ├── flag.html │ │ ├── flagged.html │ │ ├── form.html │ │ ├── list.html │ │ ├── posted.html │ │ └── preview.html │ ├── django_comments_tree │ │ ├── base.html │ │ ├── comment.html │ │ ├── comment_list.html │ │ ├── comment_tree.html │ │ ├── discarded.html │ │ ├── dislike.html │ │ ├── disliked.html │ │ ├── email_confirmation_request.html │ │ ├── email_confirmation_request.txt │ │ ├── email_followup_comment.html │ │ ├── email_followup_comment.txt │ │ ├── like.html │ │ ├── liked.html │ │ ├── moderated.html │ │ ├── muted.html │ │ ├── removal_notification_email.txt │ │ └── reply.html │ └── includes │ │ └── django_comments_tree │ │ ├── comment_content.html │ │ └── user_feedback.html ├── templatetags │ ├── __init__.py │ ├── comments.py │ └── comments_tree.py ├── tests │ ├── __init__.py │ ├── apps.py │ ├── data │ │ └── draftjs_raw.json │ ├── factories.py │ ├── models.py │ ├── settings.py │ ├── settings_pg.py │ ├── templates │ │ ├── base.html │ │ └── registration │ │ │ └── login.html │ ├── test_api_serializers.py │ ├── test_api_views.py │ ├── test_forms.py │ ├── test_model_associations.py │ ├── test_model_manager.py │ ├── test_models.py │ ├── test_moderation.py │ ├── test_queries.py │ ├── test_structured_data.py │ ├── test_templatetags.py │ ├── test_views.py │ ├── urls.py │ └── views.py ├── urls.py ├── utils.py ├── version.py └── views │ ├── __init__.py │ ├── comments.py │ ├── moderation.py │ └── utils.py ├── docker-compose.yml ├── docs ├── Makefile ├── conf.py ├── example.rst ├── extending.rst ├── extensions.py ├── i18n.rst ├── images │ ├── comments-enabled.png │ ├── cover.png │ ├── extend-comments-app.png │ ├── feedback-buttons.png │ ├── feedback-users.png │ ├── flag-counter.png │ ├── markdown-comment.png │ ├── markdown-input.png │ ├── preview-comment.png │ ├── reply-link.png │ └── update-comment-tree.png ├── index.rst ├── javascript.rst ├── logic.rst ├── make.bat ├── migrating.rst ├── quickstart.rst ├── settings.rst ├── templates.rst ├── templatetags.rst ├── tutorial.rst └── webapi.rst ├── example ├── README.md ├── comp │ ├── README.md │ ├── __init__.py │ ├── articles │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20170523_1614.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── context_processors.py │ ├── extra │ │ ├── __init__.py │ │ └── quotes │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20170523_1614.py │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── urls.py │ │ │ └── views.py │ ├── install.sh │ ├── manage.py │ ├── requirements.txt │ ├── settings.py │ ├── templates │ │ ├── articles │ │ │ ├── article_detail.html │ │ │ └── article_list.html │ │ ├── base.html │ │ ├── django_comments_xtd │ │ │ └── base.html │ │ ├── homepage.html │ │ ├── includes │ │ │ └── django_comments_xtd │ │ │ │ └── comment_content.html │ │ └── quotes │ │ │ ├── quote_detail.html │ │ │ └── quote_list.html │ ├── templatetags │ │ ├── __init__.py │ │ └── comp_filters.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── custom │ ├── README.md │ ├── __init__.py │ ├── articles │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── install.sh │ ├── manage.py │ ├── mycomments │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── forms.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20170523_1624.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── requirements.txt │ ├── settings.py │ ├── templates │ │ ├── articles │ │ │ ├── article_detail.html │ │ │ └── article_list.html │ │ ├── base.html │ │ ├── comments │ │ │ ├── form.html │ │ │ └── preview.html │ │ ├── django_comments_xtd │ │ │ ├── email_confirmation_request.html │ │ │ ├── email_confirmation_request.txt │ │ │ └── reply.html │ │ └── homepage.html │ ├── urls.py │ └── views.py ├── fixtures │ ├── articles.json │ ├── auth.json │ ├── quotes.json │ └── sites.json ├── simple │ ├── README.md │ ├── __init__.py │ ├── articles │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── install.sh │ ├── manage.py │ ├── requirements.txt │ ├── settings.py │ ├── templates │ │ ├── articles │ │ │ ├── article_detail.html │ │ │ └── article_list.html │ │ ├── base.html │ │ ├── django_comments_xtd │ │ │ └── base.html │ │ └── homepage.html │ ├── urls.py │ └── views.py └── tutorial.tar.gz ├── make.sh ├── make_no_test.sh ├── package.json ├── pyproject.toml ├── pytest.ini ├── requirements.pip ├── requirements_tests.pip ├── setup.py ├── test.sh ├── tox.ini ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /.env.docker-example: -------------------------------------------------------------------------------- 1 | # copy to .env file 2 | # Update to your paths for backup and local backup 3 | POSTGRES_PASSWORD=password_for_testing 4 | BACKUP_ROOT=./database 5 | LOCAL_BACKUPS=./database/local 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.*~ 4 | *.zip 5 | *.egg-info 6 | *.db 7 | plugin*.js 8 | plugin*.js.map 9 | vendor*.js 10 | vendor*.js.map 11 | django*.mo 12 | db.sqlite3 13 | docs/_build 14 | build/ 15 | dist/ 16 | node_modules/ 17 | .coverage 18 | .emacs.desktop* 19 | .tox 20 | .cache 21 | .eggs 22 | package-lock.json 23 | .pytest_cache/ 24 | .venv 25 | .venv3 26 | .envrc 27 | /pip-wheel-metadata 28 | *.iml 29 | .idea 30 | /.env 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | python: 6 | - 3.7 7 | - 3.8 8 | 9 | cache: 10 | pip: 11 | apt: 12 | directories: 13 | - .tox 14 | 15 | env: 16 | - DJANGO="2.2" 17 | - DJANGO="3.0" 18 | 19 | before_install: 20 | - sudo apt-get update -qq 21 | - sudo apt-get install -qq $APT 22 | - sudo apt-get install gdal-bin 23 | 24 | after_failure: 25 | - cat /home/travis/.pip/pip.log 26 | 27 | after_success: 28 | - coveralls 29 | 30 | install: 31 | - pip install pip wheel 32 | - pip install tox-travis 33 | - pip install -q coveralls flake8 tox 34 | 35 | script: 36 | - env | sort 37 | - tox 38 | - flake8 --show-source --ignore C901,D203,W503 --max-line-length=100 --exclude=.tox,docs,django_comments_tree/tests,django_comments_tree/__init__.py,django_comments_tree/migrations django_comments_tree/ 39 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Contributor, GitHub's username 2 | ------------------------------------ 3 | Daniel Rus Morales, @danirus 4 | Richard Eames, @Naddiseo 5 | Flavio Curella, @fcurella 6 | Radek Czajka, @rczajka 7 | Mandeep Gill, @mands 8 | Olivier Harris, @ojh 9 | Alejandro Varas, @alej0varas 10 | 11 | Main developer 12 | Ed Henderson, @sharpertool 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Daniel Rus Morales 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include CHANGELOG.md 4 | include tox.ini 5 | include package.json 6 | include webpack.config.js 7 | include requirements.pip 8 | include requirements_tests.pip 9 | recursive-include docs * 10 | recursive-include django_comments_tree/locale * 11 | recursive-include django_comments_tree/templates * 12 | recursive-include django_comments_tree/static * 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-comments-tree |TravisCI|_ 2 | =================== 3 | 4 | .. |TravisCI| image:: https://secure.travis-ci.org/sharpertool/django-comments-tree.png?branch=master 5 | .. _TravisCI: https://travis-ci.org/sharpertool/django-comments-tree 6 | 7 | A Django pluggable application that adds comments to your project. 8 | 9 | .. image:: https://github.com/sharpertool/django-comments-tree/blob/master/docs/images/cover.png 10 | 11 | It extends the once official `django-contrib-comments `_ with the following features: 12 | 13 | #. Comments model based on `django-treebeard `_ to provide a robust and fast threaded and nested comment structure. 14 | #. Efficiently associate a comment tree with any model through a link table which avoids populating each comment with extra link data. 15 | #. Customizable maximum thread level, either for all models or on a per app.model basis. 16 | #. Optional notifications on follow-up comments via email. 17 | #. Mute links to allow cancellation of follow-up notifications. 18 | #. Comment confirmation via email when users are not authenticated. 19 | #. Comments hit the database only after they have been confirmed. 20 | #. Registered users can like/dislike comments and can suggest comments removal. 21 | #. Template tags to list/render the last N comments posted to any given list of app.model pairs. 22 | #. Emails sent through threads (can be disable to allow other solutions, like a Celery app). 23 | #. Fully functional JavaScript plugin using ReactJS, jQuery, Bootstrap, Remarkable and MD5. 24 | 25 | Example sites and tests work under officially Django `supported versions `_: 26 | 27 | * Django 3.0, 2.2 28 | * Python 3.8, 3.7 29 | 30 | Additional Dependencies: 31 | 32 | * django-contrib-comments >=2.0 33 | * djangorestframework >=3.8, <3.9 34 | 35 | Checkout the Docker image `danirus/django-comments-tree-demo `_. 36 | 37 | `Read The Docs `_. 38 | 39 | Why Create a New Package 40 | =================== 41 | 42 | I did not particularly like how the core django-contrib-comments added a GenericForeignKey to each and every comment in order to associate a comment stream with another model. I wanted to have a single place where this association was made. 43 | 44 | I opted to add a model just for linking the comments to other models. This model has a single record for a model -> comment-tree association. The record contains the GenericForeignKey, and a single ForeignKey to the comments root node that starts the comments for that model. This is very flexible, and if the underlying model changes, it is a simple matter to move all comments to a new parent. Treebeard just makes all of this work. 45 | 46 | Treebeard provides robust mechanisms to get the parent, children, siblings, and any other association you might needed from the comment stream. This also makes it much easier to have a very robust tree structure, so nesting, replies, replies to replies, etc. are easy to handle, and very efficient. 47 | 48 | Attribution 49 | =================== 50 | This package is a fork of the excellent work at `django-comments-xtd `_ 51 | 52 | I created the fork because I wanted to a comment tree based on MP_node from `django-treebeard `_. I consider this to be a more robust tree implementation. Treebeard suppports multiple root nodes, so each root node can be an entire comment tree. 53 | 54 | -------------------------------------------------------------------------------- /compose/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mdillon/postgis:10 2 | 3 | RUN echo "deb http://httpredir.debian.org/debian jessie main contrib non-free" >> /etc/apt/sources.list 4 | RUN echo "deb-src http://httpredir.debian.org/debian jessie main contrib non-free" >> /etc/apt/sources.list 5 | 6 | RUN apt-get update --fix-missing && apt-get install -y \ 7 | gdal-bin \ 8 | --no-install-recommends 9 | 10 | # add backup scripts 11 | ADD backup.sh /usr/local/bin/backup 12 | ADD restore.sh /usr/local/bin/restore 13 | ADD list-backups.sh /usr/local/bin/list-backups 14 | 15 | # make them executable 16 | RUN chmod +x /usr/local/bin/restore 17 | RUN chmod +x /usr/local/bin/list-backups 18 | RUN chmod +x /usr/local/bin/backup 19 | -------------------------------------------------------------------------------- /compose/postgres/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # stop on errors 3 | set -e 4 | 5 | backup_dir=/backups 6 | local_dir=/local_backups 7 | 8 | local=0 9 | prefix=backup 10 | 11 | while [ "$1" != "" ]; do 12 | case $1 in 13 | -f | --file ) shift 14 | filename=$1 15 | ;; 16 | -l | --local ) local=1 17 | ;; 18 | -h | --help ) usage 19 | exit 20 | ;; 21 | -p | --prefix ) shift 22 | prefix=$1 23 | ;; 24 | * ) usage 25 | exit 1 26 | esac 27 | shift 28 | done 29 | 30 | filename=${prefix}_$(date +'%Y_%m_%dT%H_%M_%S').sql 31 | if [ ${local} == 0 ] 32 | then 33 | dir=/backups 34 | else 35 | dir=/local_backups 36 | fi 37 | FILENAME=${dir}/${filename} 38 | 39 | # we might run into trouble when using the default `postgres` user, e.g. when dropping the postgres 40 | # database in restore.sh. Check that something else is used here 41 | if [ "$POSTGRES_USER" == "postgres" ] 42 | then 43 | echo "creating a backup as the postgres user is not supported, make sure to set the POSTGRES_USER environment variable" 44 | exit 1 45 | fi 46 | 47 | # Set the default database to be the username 48 | : ${POSTGRES_DB:=$POSTGRES_USER} 49 | export POSTGRES_DB 50 | 51 | # export the postgres password so that subsequent commands don't ask for it 52 | export PGPASSWORD=$POSTGRES_PASSWORD 53 | 54 | echo "creating backup" 55 | echo "---------------" 56 | 57 | echo "Full backup filename ${FILENAME}" 58 | 59 | pg_dump -h postgres -U $POSTGRES_USER $POSTGRES_DB >> $FILENAME 60 | 61 | echo "successfully created backup $FILENAME" 62 | echo $FILENAME 63 | 64 | -------------------------------------------------------------------------------- /compose/postgres/list-backups.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "listing available backups" 3 | echo "-------------------------" 4 | backup_dir=${1:-/backups} 5 | ls -t ${backup_dir}/ /local_backups 6 | -------------------------------------------------------------------------------- /compose/postgres/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # stop on errors 4 | set -e 5 | 6 | # we might run into trouble when using the default `postgres` user, e.g. when dropping the postgres 7 | # database in restore.sh. Check that something else is used here 8 | if [ "$POSTGRES_USER" == "postgres" ] 9 | then 10 | echo "restoring as the postgres user is not supported, make sure to set the POSTGRES_USER environment variable" 11 | exit 1 12 | fi 13 | 14 | # export the postgres password so that subsequent commands don't ask for it 15 | export PGPASSWORD=$POSTGRES_PASSWORD 16 | 17 | # check that we have an argument for a filename candidate 18 | if [[ $# -eq 0 ]] ; then 19 | echo 'usage:' 20 | echo ' docker-compose run postgres restore ' 21 | echo '' 22 | echo 'to get a list of available backups, run:' 23 | echo ' docker-compose run postgres list-backups' 24 | exit 1 25 | fi 26 | 27 | # set the backupfile variable 28 | # Calculate a default filename 29 | BACKUPFILE=$1 30 | if [[ $(dirname ${BACKUPFILE}) == '.' ]];then 31 | BACKUPFILE=/backups/$(basename ${BACKUPFILE}) 32 | BACKUPFILE_LOCAL=/local_backups/$(basename ${BACKUPFILE}) 33 | fi 34 | 35 | # check that the file exists 36 | if [[ ! -f "${BACKUPFILE}" ]] && [[ ! -f "${BACKUPFILE_LOCAL}" ]] 37 | then 38 | echo "backup file not found" 39 | echo 'to get a list of available backups, run:' 40 | echo ' docker-compose run postgres list-backups' 41 | exit 1 42 | fi 43 | 44 | # Prefer local version of backup file. 45 | if [[ -f "${BACKUPFILE_LOCAL}" ]] 46 | then 47 | BACKUPFILE=${BACKUPFILE_LOCAL} 48 | fi 49 | 50 | echo "beginning restore from $1" 51 | echo "-------------------------" 52 | 53 | # delete the db 54 | # deleting the db can fail. Spit out a comment if this happens but continue since the db 55 | # is created in the next step 56 | : ${POSTGRES_DB:=$POSTGRES_USER} 57 | echo "deleting old database $POSTGRES_DB" 58 | if dropdb -h postgres -U $POSTGRES_USER $POSTGRES_DB 59 | then echo "deleted $POSTGRES_DB database" 60 | else echo "database $POSTGRES_DB does not exist, continue" 61 | fi 62 | 63 | # create a new database 64 | echo "creating new database $POSTGRES_DB" 65 | createdb -h postgres -U $POSTGRES_USER $POSTGRES_DB -O $POSTGRES_USER 66 | 67 | # restore the database 68 | echo "restoring database $POSTGRES_DB" 69 | psql -h postgres -U $POSTGRES_USER -d $POSTGRES_DB < $BACKUPFILE 70 | -------------------------------------------------------------------------------- /django_comments_tree/api/__init__.py: -------------------------------------------------------------------------------- 1 | from django_comments_tree.api.views import ( 2 | CommentCreate, CommentList, CommentCount, ToggleFeedbackFlag, 3 | CreateReportFlag, RemoveReportFlag) 4 | 5 | __all__ = (CommentCreate, CommentList, CommentCount, ToggleFeedbackFlag, 6 | CreateReportFlag, RemoveReportFlag) 7 | -------------------------------------------------------------------------------- /django_comments_tree/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommentsTreeConfig(AppConfig): 5 | name = 'django_comments_tree' 6 | verbose_name = 'Comments Tree' 7 | 8 | def get_models(self, *args, **kwargs): 9 | return super().get_models(*args, **kwargs) 10 | -------------------------------------------------------------------------------- /django_comments_tree/compat.py: -------------------------------------------------------------------------------- 1 | # Ripped from Django1.5 2 | import sys 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.utils.importlib import import_module 6 | import six 7 | 8 | 9 | def import_by_path(dotted_path, error_prefix=''): 10 | """ 11 | Import a dotted module path and return the attribute/class designated by the 12 | last name in the path. Raise ImproperlyConfigured if something goes wrong. 13 | """ 14 | try: 15 | module_path, class_name = dotted_path.rsplit('.', 1) 16 | except ValueError: 17 | raise ImproperlyConfigured("%s%s doesn't look like a module path" % ( 18 | error_prefix, dotted_path)) 19 | try: 20 | module = import_module(module_path) 21 | except ImportError as e: 22 | msg = '%sError importing module %s: "%s"' % ( 23 | error_prefix, module_path, e) 24 | six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), 25 | sys.exc_info()[2]) 26 | try: 27 | attr = getattr(module, class_name) 28 | except AttributeError: 29 | raise ImproperlyConfigured('%sModule "%s" does not define a ' 30 | '"%s" attribute/class' % 31 | (error_prefix, module_path, class_name)) 32 | return attr 33 | -------------------------------------------------------------------------------- /django_comments_tree/conf/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings as django_settings 2 | from django.utils.functional import LazyObject 3 | 4 | from django_comments_tree.conf import defaults as app_settings 5 | 6 | 7 | class LazySettings(LazyObject): 8 | def _setup(self): 9 | self._wrapped = Settings(app_settings, django_settings) 10 | 11 | 12 | class Settings(object): 13 | def __init__(self, *args): 14 | for item in args: 15 | for attr in dir(item): 16 | if attr == attr.upper(): 17 | setattr(self, attr, getattr(item, attr)) 18 | 19 | 20 | settings = LazySettings() 21 | -------------------------------------------------------------------------------- /django_comments_tree/conf/defaults.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.conf import settings 3 | import markdown 4 | from django_comments_tree.render import render_draftjs, render_plain 5 | 6 | # Default application namespace 7 | COMMENT_URL_NAMESPACE = 'treecomments' 8 | 9 | COMMENT_MAX_LENGTH = 3000 10 | 11 | # Extra key to salt the TreeCommentForm. 12 | COMMENTS_TREE_SALT = b"" 13 | 14 | # Whether comment posts should be confirmed by email. 15 | COMMENTS_TREE_CONFIRM_EMAIL = True 16 | 17 | # From email address. 18 | COMMENTS_TREE_FROM_EMAIL = settings.DEFAULT_FROM_EMAIL 19 | 20 | # Contact email address. 21 | COMMENTS_TREE_CONTACT_EMAIL = settings.DEFAULT_FROM_EMAIL 22 | 23 | # Maximum Thread Level. 24 | COMMENTS_TREE_MAX_THREAD_LEVEL = 0 25 | 26 | # Maximum Thread Level per app.model basis. 27 | COMMENTS_TREE_MAX_THREAD_LEVEL_BY_APP_MODEL = {} 28 | 29 | # Default order to list comments in. 30 | COMMENTS_TREE_LIST_ORDER = ('submit_date',) 31 | 32 | # Form class to use. 33 | COMMENTS_TREE_FORM_CLASS = "django_comments_tree.forms.TreeCommentForm" 34 | 35 | # Structured Data. 36 | COMMENTS_TREE_STRUCTURED_DATA_CLASS = "django_comments_tree.models.CommentData" 37 | 38 | # Model to use. 39 | COMMENTS_TREE_MODEL = "django_comments_tree.models.TreeComment" 40 | 41 | # Send HTML emails. 42 | COMMENTS_TREE_SEND_HTML_EMAIL = True 43 | 44 | # Whether to send emails in separate threads or use the regular method. 45 | # Set it to False to use a third-party app like django-celery-email or 46 | # your own celery app. 47 | COMMENTS_TREE_THREADED_EMAILS = True 48 | 49 | # Define what commenting features a pair app_label.model can have. 50 | # TODO: Put django-comments-tree settings under a dictionary, and merge 51 | # COMMENTS_TREE_MAX_THREAD_LEVEL_BY_APP_MODEL with this one. 52 | COMMENTS_TREE_APP_MODEL_OPTIONS = { 53 | 'default': { 54 | 'allow_flagging': False, 55 | 'allow_feedback': False, 56 | 'show_feedback': False, 57 | } 58 | } 59 | 60 | 61 | # Define a function to return the user representation. Used by 62 | # the web API to represent user strings in response objects. 63 | def username(u): 64 | return u.username 65 | 66 | 67 | COMMENTS_TREE_API_USER_REPR = username 68 | 69 | # Set to true to enable Firebase notifications 70 | COMMENTS_TREE_ENABLE_FIREBASE = False 71 | 72 | COMMENTS_TREE_FIREBASE_KEY = None 73 | 74 | # Default types we can use for comments 75 | MARKUP_FIELD_TYPES = ( 76 | ('plain', render_plain), 77 | ('markdown', markdown.markdown), 78 | ('draftjs', render_draftjs), 79 | ) 80 | -------------------------------------------------------------------------------- /django_comments_tree/feeds.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.shortcuts import get_current_site 2 | from django.contrib.syndication.views import Feed 3 | from django.utils.translation import ugettext as _ 4 | 5 | import django_comments_tree 6 | 7 | 8 | class LatestCommentFeed(Feed): 9 | """Feed of latest comments on the current site.""" 10 | 11 | def __call__(self, request, *args, **kwargs): 12 | self.site = get_current_site(request) 13 | return super(LatestCommentFeed, self).__call__(request, *args, **kwargs) 14 | 15 | def title(self): 16 | return _("%(site_name)s comments") % dict(site_name=self.site.name) 17 | 18 | def link(self): 19 | return "https://%s/" % (self.site.domain) 20 | 21 | def description(self): 22 | return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name) 23 | 24 | def items(self): 25 | qs = django_comments_tree.get_model().objects.filter( 26 | site__pk=self.site.pk, 27 | is_public=True, 28 | is_removed=False, 29 | ) 30 | return qs.order_by('-submit_date')[:40] 31 | 32 | def item_pubdate(self, item): 33 | return item.submit_date 34 | -------------------------------------------------------------------------------- /django_comments_tree/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .forms import TreeCommentForm # noqa 2 | from .base import CommentSecurityForm, CommentDetailsForm, CommentForm # noqa 3 | -------------------------------------------------------------------------------- /django_comments_tree/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/management/__init__.py -------------------------------------------------------------------------------- /django_comments_tree/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/management/commands/__init__.py -------------------------------------------------------------------------------- /django_comments_tree/management/commands/populate_tree_comments.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.db import connections 4 | from django.db.utils import ConnectionDoesNotExist, IntegrityError 5 | from django.core.management.base import BaseCommand 6 | 7 | from django_comments_tree.models import TreeComment 8 | 9 | 10 | __all__ = ['Command'] 11 | 12 | 13 | class Command(BaseCommand): 14 | help = "Load the treecomment table with valid data from django_comments_tree." 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument('using', nargs='*', type=str) 18 | 19 | def populate_db(self, cursor): 20 | """ 21 | ToDo: More work will be needed to make this transition work 22 | :param cursor: 23 | :return: 24 | """ 25 | # for comment in Comment.objects.all(): 26 | # sql = ("INSERT INTO %(table)s " 27 | # " ('comment_ptr_id', 'thread_id', 'parent_id'," 28 | # " 'level', 'order', 'followup') " 29 | # "VALUES (%(id)d, %(id)d, %(id)d, 0, 1, 0)") 30 | # cursor.execute(sql % {'table': TreeComment._meta.db_table, 31 | # 'id': comment.id}) 32 | 33 | def handle(self, *args, **options): 34 | total = 0 35 | using = options['using'] or ['default'] 36 | for db_conn in using: 37 | try: 38 | self.populate_db(connections[db_conn].cursor()) 39 | total += TreeComment.objects.using(db_conn).count() 40 | except ConnectionDoesNotExist: 41 | print("DB connection '%s' does not exist." % db_conn) 42 | continue 43 | except IntegrityError: 44 | if db_conn != 'default': 45 | print("Table '%s' (in '%s' DB connection) must be empty." 46 | % (TreeComment._meta.db_table, db_conn)) 47 | else: 48 | print("Table '%s' must be empty." 49 | % TreeComment._meta.db_table) 50 | sys.exit(1) 51 | print("Added %d TreeComment object(s)." % total) 52 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0002_treecomment_updated_on.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-06-18 05:15 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_comments_tree', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='treecomment', 16 | name='updated_on', 17 | field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='date/time updated'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0003_remove_commentassociation_object_pk.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-07-15 19:00 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_comments_tree', '0002_treecomment_updated_on'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='commentassociation', 15 | name='object_pk', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0004_auto_20190803_1558.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-03 21:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_comments_tree', '0003_remove_commentassociation_object_pk'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='treecomment', 15 | name='comment_markup_type', 16 | field=models.CharField(choices=[('', '--'), ('plain', 'plain'), ('markdown', 'markdown'), ('draftjs', 'draftjs')], default='plain', max_length=30), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0005_treecomment_assoc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-22 02:41 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_comments_tree', '0004_auto_20190803_1558'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='treecomment', 16 | name='assoc', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_comments_tree.CommentAssociation'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0006_auto_20191023_1025.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-23 16:25 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_comments_tree', '0005_treecomment_assoc'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='commentassociation', 16 | name='root', 17 | field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='django_comments_tree.TreeComment'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0007_auto_20191023_1440.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-23 20:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_comments_tree', '0006_auto_20191023_1025'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='treecomment', 16 | name='assoc', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_comments_tree.CommentAssociation'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/0008_auto_20191027_2147.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-28 03:47 2 | 3 | from django.db import migrations 4 | 5 | 6 | def update_association(apps, schema_editor): 7 | """ Update the assoc value on all TreeComments """ 8 | 9 | CommentAssociation = apps.get_model('django_comments_tree', 'CommentAssociation') 10 | TreeComment = apps.get_model('django_comments_tree', 'TreeComment') 11 | for root in TreeComment.objects.filter(depth=1).all(): 12 | assoc = CommentAssociation.objects.get(root=root) 13 | TreeComment.objects.filter( 14 | path__startswith=root.path, 15 | depth__gte=root.depth).update(assoc=assoc) 16 | 17 | 18 | def reverse_associations(apps, schema_editor): 19 | """ Set associations to null """ 20 | TreeComment = apps.get_model('django_comments_tree', 'TreeComment') 21 | for root in TreeComment.objects.filter(depth=1).all(): 22 | TreeComment.objects.filter( 23 | path__startswith=root.path, 24 | depth__gte=root.depth).update(assoc=None) 25 | 26 | 27 | class Migration(migrations.Migration): 28 | dependencies = [ 29 | ('django_comments_tree', '0007_auto_20191023_1440'), 30 | ] 31 | 32 | operations = [ 33 | migrations.RunPython( 34 | update_association, 35 | reverse_associations 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /django_comments_tree/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/migrations/__init__.py -------------------------------------------------------------------------------- /django_comments_tree/permissions.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user 2 | 3 | from rest_framework.permissions import BasePermission 4 | 5 | 6 | class IsOwner(BasePermission): 7 | 8 | def has_object_permission(self, request, view, obj): 9 | return get_user(request) == obj.user 10 | 11 | 12 | class IsModerator(BasePermission): 13 | 14 | def has_permission(self, request, view): 15 | return request.user.has_perm('django_comments.can_moderate') 16 | -------------------------------------------------------------------------------- /django_comments_tree/render.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from draftjs_exporter import html as htmlexporter 4 | from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES 5 | from draftjs_exporter.defaults import BLOCK_MAP 6 | from draftjs_exporter.dom import DOM 7 | from django.utils.html import escape, linebreaks, urlize 8 | 9 | 10 | def image(props): 11 | """ 12 | Components are simple functions that take `props` as parameter and return DOM elements. 13 | This component creates an image element, with the relevant attributes. 14 | 15 | :param props: 16 | :return: 17 | """ 18 | return DOM.create_element('img', { 19 | 'src': props.get('src'), 20 | 'width': props.get('width'), 21 | 'height': props.get('height'), 22 | 'alt': props.get('alt'), 23 | }) 24 | 25 | 26 | def blockquote(props): 27 | """ 28 | This component uses block data to render a blockquote. 29 | :param props: 30 | :return: 31 | """ 32 | block_data = props['block']['data'] 33 | 34 | # Here, we want to display the block's content so we pass the 35 | # `children` prop as the last parameter. 36 | return DOM.create_element('blockquote', { 37 | 'cite': block_data.get('cite') 38 | }, props['children']) 39 | 40 | 41 | # https://github.com/springload/draftjs_exporter#configuration 42 | # custom configuration 43 | 44 | _config = { 45 | 'block_map': dict(BLOCK_MAP, **{ 46 | BLOCK_TYPES.BLOCKQUOTE: blockquote, 47 | # BLOCK_TYPES.ATOMIC: {'start': '', 'end': ''}, 48 | }), 49 | 'entity_decorators': { 50 | # ENTITY_TYPES.LINK: 'link', 51 | ENTITY_TYPES.IMAGE: image, 52 | ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'), 53 | } 54 | } 55 | 56 | 57 | def render_draftjs(content_data): 58 | try: 59 | cstate = json.loads(content_data) 60 | except json.JSONDecodeError: 61 | # invalid json data 62 | # Should log something... 63 | return '' 64 | renderer = htmlexporter.HTML(_config) 65 | html = renderer.render(cstate) 66 | return html 67 | 68 | 69 | def render_plain(content_data): 70 | return linebreaks(urlize(escape(content_data))) 71 | -------------------------------------------------------------------------------- /django_comments_tree/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Signals relating to django-comments-tree. 3 | """ 4 | from django.dispatch import Signal 5 | 6 | # Sent just after a comment has been verified. 7 | confirmation_received = Signal(providing_args=["comment", "request"]) 8 | 9 | # Sent just after a user has muted a comments thread. 10 | comment_thread_muted = Signal(providing_args=["comment", "requests"]) 11 | 12 | # Sent just before a comment will be posted (after it's been approved and 13 | # moderated; this can be used to modify the comment (in place) with posting 14 | # details or other such actions. If any receiver returns False the comment will be 15 | # discarded and a 400 response. This signal is sent at more or less 16 | # the same time (just before, actually) as the Comment object's pre-save signal, 17 | # except that the HTTP request is sent along with this signal. 18 | comment_will_be_posted = Signal(providing_args=["comment", "request"]) 19 | 20 | # Sent just after a comment was posted. See above for how this differs 21 | # from the Comment object's post-save signal. 22 | comment_was_posted = Signal(providing_args=["comment", "request"]) 23 | 24 | # Sent after a comment was "flagged" in some way. Check the flag to see if this 25 | # was a user requesting removal of a comment, a moderator approving/removing a 26 | # comment, or some other custom user flag. 27 | comment_was_flagged = Signal(providing_args=["comment", "flag", "created", "request"]) 28 | 29 | # Sent after a comment is `Liked` or `Disliked` 30 | comment_feedback_toggled = Signal( 31 | providing_args=["flag", "comment", "created", "request"] 32 | ) 33 | -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/css/_variables.scss: -------------------------------------------------------------------------------- 1 | // Lumen 4.1.3 2 | // Bootswatch 3 | 4 | // 5 | // Color system 6 | // 7 | 8 | $white: #fff !default; 9 | $gray-100: #f6f6f6 !default; 10 | $gray-200: #f0f0f0 !default; 11 | $gray-300: #dee2e6 !default; 12 | $gray-400: #ced4da !default; 13 | $gray-500: #adb5bd !default; 14 | $gray-600: #999 !default; 15 | $gray-700: #555 !default; 16 | $gray-800: #333 !default; 17 | $gray-900: #222 !default; 18 | $black: #000 !default; 19 | 20 | $blue: rgb(33, 150, 243) !default; 21 | $indigo: #6610f2 !default; 22 | $purple: #6f42c1 !default; 23 | $pink: #e83e8c !default; 24 | $red: #FF4136 !default; 25 | $orange: #fd7e14 !default; 26 | $yellow: #FF851B !default; 27 | $green: #28B62C !default; 28 | $teal: #20c997 !default; 29 | $cyan: #75CAEB !default; 30 | 31 | $primary: $blue !default; 32 | $secondary: $gray-200 !default; 33 | $success: $green !default; 34 | $info: $cyan !default; 35 | $warning: $yellow !default; 36 | $danger: $red !default; 37 | $light: $gray-100 !default; 38 | $dark: $gray-700 !default; 39 | 40 | $yiq-contrasted-threshold: 200 !default; 41 | 42 | $body-color: $gray-800 !default; 43 | 44 | // Fonts 45 | 46 | $font-family-sans-serif: "Oxygen", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; 47 | 48 | // $font-size-base: 0.875rem !default; 49 | $font-size-base: 0.925rem !default; 50 | $font-size-sm: ($font-size-base * .800) !default; 51 | 52 | // Dropdowns 53 | 54 | $dropdown-link-color: rgba(0,0,0,.5) !default; 55 | 56 | // Navs 57 | 58 | $nav-tabs-border-color: $gray-200 !default; 59 | $nav-tabs-link-hover-border-color: $nav-tabs-border-color !default; 60 | $nav-tabs-link-active-color: $gray-900 !default; 61 | $nav-tabs-link-active-border-color: $nav-tabs-border-color !default; 62 | 63 | // Pagination 64 | 65 | $pagination-color: $gray-700 !default; 66 | $pagination-bg: $gray-200 !default; 67 | 68 | $pagination-hover-color: $pagination-color !default; 69 | $pagination-hover-bg: $pagination-bg !default; 70 | 71 | $pagination-active-border-color: darken($primary, 5%) !default; 72 | 73 | $pagination-disabled-color: $gray-600 !default; 74 | $pagination-disabled-bg: $pagination-bg !default; 75 | 76 | // Jumbotron 77 | 78 | $jumbotron-bg: #fafafa !default; 79 | 80 | // Modals 81 | 82 | $modal-content-border-color: rgba($black,.1) !default; 83 | 84 | // Close 85 | 86 | $close-color: $white !default; 87 | -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/static/django_comments_tree/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/img/64x64.svg: -------------------------------------------------------------------------------- 1 | 64x64 -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/js/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import {CommentBox} from './commentbox.jsx'; 5 | 6 | 7 | ReactDOM.render( 8 | React.createElement(CommentBox, 9 | Object.assign(window.comments_props, 10 | window.comments_props_override)), 11 | document.getElementById('comments') 12 | ); 13 | -------------------------------------------------------------------------------- /django_comments_tree/static/django_comments_tree/js/src/lib.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | export function getCookie(name) { 4 | var cookieValue = null; 5 | if (document.cookie && document.cookie !== '') { 6 | var cookies = document.cookie.split(';'); 7 | for (var i = 0; i < cookies.length; i++) { 8 | var cookie = jQuery.trim(cookies[i]); 9 | // Does this cookie string begin with the name we want? 10 | if (cookie.substring(0, name.length + 1) === (name + '=')) { 11 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 12 | break; 13 | } 14 | } 15 | } 16 | return cookieValue; 17 | } 18 | 19 | export function csrfSafeMethod(method) { 20 | // these HTTP methods do not require CSRF protection 21 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 22 | } 23 | 24 | export function jquery_ajax_setup(csrf_cookie_name) { 25 | $.ajaxSetup({ 26 | beforeSend: function(xhr, settings) { 27 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 28 | xhr.setRequestHeader("X-CSRFToken", getCookie(csrf_cookie_name)); 29 | } 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /django_comments_tree/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Contact Me » code 404{% endblock %} 6 | 7 | 8 |

Page not found

9 | 10 | 11 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/400-debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Comment post not allowed (400) 6 | 7 | 87 | 88 | 89 |
90 |

Comment post not allowed (400)

91 | 92 | 93 | 94 | 95 | 96 |
Why:{{ why }}
97 |
98 |
99 |

100 | The comment you tried to post to this view wasn't saved because something 101 | tampered with the security information in the comment form. The message 102 | above should explain the problem, or you can check the comment 104 | documentation for more help. 105 |

106 |
107 | 108 |
109 |

110 | You're seeing this error because you have DEBUG = True in 111 | your Django settings file. Change that to False, and Django 112 | will display a standard 400 error page. 113 |

114 |
115 | 116 | 117 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/approve.html: -------------------------------------------------------------------------------- 1 | {% extends "comments/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Approve a comment" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Really make this comment public?" %}

8 |
{{ comment|linebreaks }}
9 |
{% csrf_token %} 10 | {% if next %} 11 |
{% endif %} 12 |

13 | or cancel 14 |

15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/approved.html: -------------------------------------------------------------------------------- 1 | {% extends "comments/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Thanks for approving" %}.{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/comment_notification_email.txt: -------------------------------------------------------------------------------- 1 | A new comment has been sent to the following URL: 2 | 3 | {{ content_object.get_absolute_url }} 4 | 5 | Submitted by: {{ comment.user_name }} 6 | Email address: {{ comment.user_email }} 7 | 8 | --- Comment: --- 9 | {{ comment.comment }} 10 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Remove comment" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans "Remove this comment?" %}

10 |
11 |
12 |

{% trans "As a moderator you can delete comments. Deleting a comment does not remove it from the site, only prevents your website from showing the text. Click on the remove button to delete the following comment:" %}

13 |
14 |
15 |
16 |
17 |
18 | {{ comment.user_email|tree_comment_gravatar }} 19 |
20 |
21 | {{ comment.submit_date|date:"N j, Y, P" }} -  22 | {% if comment.user_url %} 23 | {% endif %} 24 | {{ comment.user_name }} 25 | {% if comment.user_url %} 26 | {% endif %} 27 |
28 |

{{ comment.comment }}

29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
{% csrf_token %} 38 | 39 |
40 |
41 | 42 | cancel 43 |
44 |
45 |
46 |
47 |
48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/deleted.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Comment removed" %}.{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

{% trans "The comment has been removed." %}

13 |

{% trans "Thank you for taking the time to improve the quality of discussion in our site." %}

14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/flag.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Flag comment" %}{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

13 |

{% trans "Flag this comment?" %}

14 |
15 |
16 |

{% trans "Click on the flag button to mark the following comment as inappropriate." %}

17 |
18 |
19 |
20 |
21 |
22 | {{ comment.user_email|tree_comment_gravatar }} 23 |
24 |
25 | {{ comment.submit_date|date:"N j, Y, P" }} -  26 | {% if comment.user_url %} 27 | {% endif %} 28 | {{ comment.user_name }} 29 | {% if comment.user_url %} 30 | {% endif %} 31 |
32 |

{{ comment.comment }}

33 |
34 |
35 |
36 |
37 | {% with object_absolute_url=comment.content_object.get_absolute_url %} 38 | {% if object_absolute_url %} 39 |

40 | {% trans "Posted to "%} {{ comment.content_object }} 41 |

42 | {% endif %} 43 | {% endwith %} 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
{% csrf_token %} 52 | 53 |
54 |
55 | 56 | {% trans "cancel" %} 57 |
58 |
59 |
60 |
61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/flagged.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Comment flagged" %}.{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

{% trans "The comment has been flagged." %}

13 |

{% trans "Thank you for taking the time to improve the quality of discussion in our site." %}

14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load comments_tree %} 3 | 4 |

5 | {% csrf_token %} 6 |
7 | 8 | 9 | 11 | 12 | {% for field in form %} 13 | {% if field.is_hidden %}
{{ field }}
{% endif %} 14 | {% endfor %} 15 | 16 |
{{ form.honeypot }}
17 | 18 |
19 |
20 | {{ form.comment }} 21 |
22 |
23 | 24 | {% if not request.user.is_authenticated or not request.user.get_full_name %} 25 |
26 | 29 |
30 | {{ form.name }} 31 |
32 |
33 | {% endif %} 34 | 35 | {% if not request.user.is_authenticated or not request.user.email %} 36 |
37 | 40 |
41 | {{ form.email }} 42 | {{ form.email.help_text }} 43 |
44 |
45 | {% endif %} 46 | 47 | {% if not request.user.is_authenticated %} 48 |
49 | 52 |
53 | {{ form.url }} 54 |
55 |
56 | {% endif %} 57 | 58 |
59 |
60 |
61 | {{ form.followup }} 62 | 63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load comments_tree %} 3 | 4 |
5 | {% for comment in comment_list %} 6 |
7 | 8 | {{ comment.user_email|tree_comment_gravatar }} 9 |
10 |
11 |
12 |
{{ comment.submit_date }} - {% if comment.url and not comment.is_removed %}{% endif %}{{ comment.name }}{% if comment.url %}{% endif %}  
13 |
14 |
15 | {% if comment.is_removed %} 16 |

{% trans "This comment has been removed." %}

17 | {% else %} 18 |
19 | {% include "includes/django_comments_tree/comment_content.html" with content=comment.comment %} 20 |
21 | {% endif %} 22 | {% if comment.allow_thread and not comment.is_removed %} 23 | 24 | {% trans "Reply" %} 25 | 26 | {% endif %} 27 |
28 |
29 |
30 |
31 | {% endfor %} 32 |
33 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/posted.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |

{% trans "Comment confirmation requested." %}

7 |
8 |
9 |
10 |
11 |

12 | {% blocktrans %}A confirmation message has been sent to your 13 | email address. Please, click on the link in the message to confirm 14 | your comment.{% endblocktrans %} 15 |

16 |

17 | Go back to: {{ target }} 18 |

19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /django_comments_tree/templates/comments/preview.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Preview your comment" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{% trans "Preview your comment" %}

11 |
12 |
13 |

14 | {% trans "Preview of your comment for:" %}
15 | {{ form.target_object }} 16 |

17 |
18 |
19 |
20 | {% if not comment %} 21 | {% trans "Empty comment." %} 22 | {% else %} 23 |
24 | {{ form.cleaned_data.email|tree_comment_gravatar }} 25 |
26 |
27 |
28 | {% now "N j, Y, P" %} -  29 | {% if form.cleaned_data.url %} 30 | {% endif %} 31 | {{ form.cleaned_data.name }} 32 | {% if form.cleaned_data.url %}{% endif %} 33 |
34 |

{{ comment }}

35 |
36 |
37 |
38 | {% endif %} 39 |
40 |
41 |

{% trans "Post your comment" %}

42 | {% include "comments/form.html" %} 43 |
44 |
45 |
46 |
47 |
48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/comment.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load comments_tree %} 3 | 4 |
5 | {{ comment.user_email|tree_comment_gravatar }} 6 |
7 |
8 |
9 |
{% trans "Posted to "%} {{ comment.content_object }} - {{ comment.submit_date|timesince }} - {% if comment.url and not comment.is_removed %}{% endif %}{{ comment.name }}{% if comment.url %}{% endif %}  
10 |
11 |
12 | {% if comment.is_removed %} 13 |

{% trans "This comment has been removed." %}

14 | {% else %} 15 |
16 | {% include "includes/django_comments_tree/comment_content.html" with content=comment.comment %} 17 |
18 | {% endif %} 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/comment_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block menu-class-comments %}active{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |

{% trans "List of comments" %}

12 |
13 |
14 | 15 |
16 |
17 |
18 | {% for comment in object_list %} 19 | {% include "django_comments_tree/comment.html" %} 20 | {% empty %} 21 |

No comments yet.

22 | {% endfor %} 23 |
24 | 25 | 26 |
    27 |
  • 28 | « 29 |
  • 30 | {% for page_number in page_range %} 31 |
  • 32 | {{ page_number }} 33 |
  • 34 | {% endfor %} 35 |
  • 36 | » 37 |
  • 38 |
39 | 40 |
41 |
42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/comment_tree.html: -------------------------------------------------------------------------------- 1 | {% load l10n %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% for item in comments %} 6 |
7 | 8 | {{ item.comment.user_email|tree_comment_gravatar }} 9 |
10 |
11 |
12 |
{{ item.comment.submit_date|localize }} - {% if item.comment.url and not item.comment.is_removed %}{% endif %}{{ item.comment.name }}{% if item.comment.url %}{% endif %}{% if item.comment.user and item.comment.user|has_permission:"django_comments.can_moderate" %} {% trans "moderator" %}{% endif %}  
13 | 14 | {% if not item.comment.is_removed %} 15 | {% if perms.comments.can_moderate %} 16 | {% if item.flagged_count %} 17 | {{ item.flagged_count }} 18 | {% endif %} 19 | {% endif %} 20 | {% if allow_flagging and item.flagged %} 21 | 22 | {% elif allow_flagging %} 23 | 24 | 25 | {% endif %} 26 | {% if perms.comments.can_moderate %} 27 | 28 | {% endif %} 29 | {% endif %} 30 | 31 |
32 | {% if item.comment.is_removed %} 33 |

{% trans "This comment has been removed." %}

34 | {% else %} 35 |
36 | {% include "includes/django_comments_tree/comment_content.html" with content=item.comment.comment %} 37 |
38 | {% if allow_feedback %} 39 | {% include "includes/django_comments_tree/user_feedback.html" %} 40 | {% endif %} 41 | {% if item.comment.allow_thread and not item.comment.is_removed %} 42 | {% if allow_feedback %}    {% endif %}{% trans "Reply" %} 43 | {% endif %} 44 | {% endif %} 45 |
46 | {% if not item.comment.is_removed and item.children %} 47 | {% render_treecomment_tree with comments=item.children %} 48 | {% endif %} 49 |
50 |
51 | {% endfor %} 52 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/discarded.html: -------------------------------------------------------------------------------- 1 | {% extends "comments/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Comment discarded" %}.{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Comment automatically discarded" %}

8 |

{% trans "Sorry, your comment has been automatically discarded" %}.

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/dislike.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Confirm your opinion" %}{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |

14 | {% if already_disliked_it %} 15 | {% trans "You didn't like this comment, do you want to change it?" %} 16 | {% else %} 17 | {% trans "Do you dislike this comment?" %} 18 | {% endif %} 19 |

20 |
21 |
22 |

{% trans "Please, confirm your opinion about the comment." %}

23 |
24 |
25 |
26 |
27 |
28 | {{ comment.user_email|tree_comment_gravatar }} 29 |
30 |
31 |
32 | {{ comment.submit_date|date:"N j, Y, P" }} -  33 | {% if comment.user_url %} 34 | {% endif %} 35 | {{ comment.user_name }} 36 | {% if comment.user_url %} 37 | {% endif %} 38 |
39 |

{{ comment.comment }}

40 |
41 |
42 |
43 |
44 |
45 | {% with object_absolute_url=comment.content_object.get_absolute_url %} 46 | {% if object_absolute_url %} 47 |

48 | {% trans "Posted to "%} {{ comment.content_object }} 49 |

50 | {% endif %} 51 | {% endwith %} 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {% if already_disliked_it %} 60 |
61 | {% trans 'Click on the "withdraw" button if you want to withdraw your negative opinion on this comment.' %} 62 |
63 | {% endif %} 64 |
{% csrf_token %} 65 | 66 |
67 |
68 | {% if already_disliked_it %} 69 | 70 | {% else %} 71 | 72 | {% endif %} 73 | {% trans "cancel" %} 74 |
75 |
76 |
77 |
78 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/disliked.html: -------------------------------------------------------------------------------- 1 | {% extends "comments/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "You disliked the comment" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Thanks for taking the time to participate." %}

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/email_confirmation_request.html: -------------------------------------------------------------------------------- 1 |

{{ comment.user_name }},

2 | 3 |

You or someone in behalf of you have requested to post a comment into this page:
4 | http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} 5 |

6 | 7 |

The comment:
8 | {{ comment.comment }} 9 |


10 | 11 |

If you do not wish to post the comment, please ignore this message or report an incident to {{ contact }}. Otherwise click on the link below to confirm the comment.

12 | 13 |

http://{{ site.domain }}{{ confirmation_url|slice:":40" }}...

14 | 15 |

If clicking does not work, you can also copy and paste the address into your browser's address window.

16 | 17 |

Thanks for your comment!
18 | --
19 | Kind regards,
20 | {{ site }}

21 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/email_confirmation_request.txt: -------------------------------------------------------------------------------- 1 | {{ comment.user_name }}, 2 | 3 | You or someone in behalf of you have requested to post a comment to the following URL. 4 | 5 | URL: http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} 6 | 7 | --- Comment: --- 8 | {{ comment.comment }} 9 | ---------------- 10 | 11 | If you do not wish to post the comment, please ignore this message or report an incident to {{ contact|safe }}. Otherwise click on the link below to confirm the comment. 12 | 13 | http://{{ site.domain }}{{ confirmation_url }} 14 | 15 | If clicking does not work, you can also copy and paste the address into your browser's address window. 16 | Thanks for your comment! 17 | 18 | -- 19 | Kind regards, 20 | {{ site }} 21 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/email_followup_comment.html: -------------------------------------------------------------------------------- 1 |

{{ user_name }},

2 | 3 |

There is a new comment following up yours.

4 | 5 |

Sent by: {{ comment.name }}, {{ comment.submit_date|date:"SHORT_DATE_FORMAT" }}
6 | http://{{ site.domain }}{{ comment.content_object.get_absolute_url }}

7 | 8 |

The comment:
9 | {{ comment.comment }} 10 |

11 | 12 |

Click http://{{ site.domain }}{{ mute_url|slice:":40" }}... to mute the comments thread. You will no longer receive follow-up notifications.

13 |

--
14 | Kind regards,
15 | {{ site }} 16 |

17 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/email_followup_comment.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {{ user_name }}, 3 | 4 | {% blocktrans %}There is a new comment following up yours.{% endblocktrans %} 5 | 6 | Post: {{ content_object.title }} 7 | URL: http://{{ site.domain }}{{ content_object.get_absolute_url }} 8 | Sent by: {{ comment.name }}, {{ comment.submit_date|date:"SHORT_DATE_FORMAT" }} 9 | 10 | --- Comment: --- 11 | {{ comment.comment }} 12 | 13 | 14 | ----- 15 | Click on the following link to mute the comments thread. You will no longer receive follow-up notifications: 16 | 17 | http://{{ site.domain }}{{ mute_url }} 18 | 19 | -- 20 | {% trans "Kind regards" %}, 21 | {{ site }} 22 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/like.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Confirm your opinion" %}{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |

14 | {% if already_liked_it %} 15 | {% trans "You liked this comment, do you want to change it?" %} 16 | {% else %} 17 | {% trans "Do you like this comment?" %} 18 | {% endif %} 19 |

20 |
21 |
22 |

{% trans "Please, confirm your opinion about the comment." %}

23 |
24 |
25 |
26 |
27 |
28 | {{ comment.user_email|tree_comment_gravatar }} 29 |
30 |
31 | {{ comment.submit_date|date:"N j, Y, P" }} -  32 | {% if comment.user_url %} 33 | {% endif %} 34 | {{ comment.user_name }} 35 | {% if comment.user_url %} 36 | {% endif %} 37 |
38 |

{{ comment.comment }}

39 |
40 |
41 |
42 |
43 | {% with object_absolute_url=comment.content_object.get_absolute_url %} 44 | {% if object_absolute_url %} 45 |

46 | {% trans "Posted to "%} {{ comment.content_object }} 47 |

48 | {% endif %} 49 | {% endwith %} 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {% if already_liked_it %} 58 |
59 | {% trans 'Click on the "withdraw" button if you want to withdraw your positive opinion on this comment.' %} 60 |
61 | {% endif %} 62 |
63 | {% csrf_token %} 64 | 65 |
66 |
67 | {% if already_liked_it %} 68 | 69 | {% else %} 70 | 71 | {% endif %} 72 | {% trans "cancel" %} 73 |
74 |
75 | 76 |
77 |
78 |
79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/liked.html: -------------------------------------------------------------------------------- 1 | {% extends "comments/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Your opinion is appreciated" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Thanks for taking the time to participate." %}

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/moderated.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Comment requires approval" %}.{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

{% trans "Comment in moderation" %}

9 |
10 |
11 |
12 |
13 |

14 | {% blocktrans %} 15 | Your comment is in moderation.
16 | It has to be reviewed before being published.
17 | Thank you for your patience and understanding. 18 | {% endblocktrans %} 19 |

20 |

21 | {% with content_object_url=comment.content_object.get_absolute_url content_object_str=comment.content_object %} 22 | {% blocktrans %} 23 | Go back to: {{ content_object_str }} 24 | {% endblocktrans %} 25 | {% endwith %} 26 |

27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/muted.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Comment thread muted" %}.{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Comment thread has been muted" %}

8 |

9 | {% trans "You will no longer receive email notifications for comments sent to" %} {{ content_object }} 10 |

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/removal_notification_email.txt: -------------------------------------------------------------------------------- 1 | A removal suggestion has been received for the following comment: 2 | 3 | --- Comment: --- 4 | {{ comment.comment }} 5 | 6 | 7 | Posted to the following URL: 8 | http{% if request.is_secure %}s{% endif %}://{{ current_site.domain }}{{ content_object.get_absolute_url }} 9 | 10 | Removal suggested by: {{ request.user }} 11 | -------------------------------------------------------------------------------- /django_comments_tree/templates/django_comments_tree/reply.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Comment reply" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{% trans "Reply to comment" %}

11 |
12 |
13 |
14 |
15 |
16 | {{ comment.user_email|tree_comment_gravatar }} 17 |
18 |
19 |
20 | {{ comment.submit_date|date:"N j, Y, P" }} -  21 | {% if comment.user_url %} 22 | {% endif %} 23 | {{ comment.user_name }}{% if comment.user_url %}{% endif %} 24 |
25 |

{{ comment.comment }}

26 |
27 |
28 |
29 |
30 |
31 |

{% trans "Post your comment" %}

32 | {% include "comments/form.html" %} 33 |
34 |
35 |
36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /django_comments_tree/templates/includes/django_comments_tree/comment_content.html: -------------------------------------------------------------------------------- 1 | {{ content }} 2 | -------------------------------------------------------------------------------- /django_comments_tree/templates/includes/django_comments_tree/user_feedback.html: -------------------------------------------------------------------------------- 1 | {% if allow_feedback %} 2 | 3 | {% if show_feedback and item.likedit_users %} 4 | 6 | {{ item.likedit_users|length }} 7 | {% endif %} 8 | 9 | 11 | 12 | | 13 | 14 | {% if show_feedback and item.dislikedit_users %} 15 | 17 | {{ item.dislikedit_users|length }} 18 | {% endif %} 19 | 20 | 22 | 23 | 24 | {% endif %} 25 | -------------------------------------------------------------------------------- /django_comments_tree/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/templatetags/__init__.py -------------------------------------------------------------------------------- /django_comments_tree/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def setup_django_settings(): 6 | if os.environ.get("DJANGO_SETTINGS_MODULE", False): 7 | return 8 | os.chdir(os.path.join(os.path.dirname(__file__), "..")) 9 | sys.path.insert(0, os.getcwd()) 10 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 11 | 12 | 13 | def run_tests(): 14 | if not os.environ.get("DJANGO_SETTINGS_MODULE", False): 15 | setup_django_settings() 16 | 17 | import django 18 | from django.conf import settings 19 | from django.test.utils import get_runner 20 | 21 | # Django 1.7.x or above. 22 | if django.VERSION[0] >=1 or django.VERSION[1] >= 7: 23 | django.setup() 24 | runner = get_runner(settings, 25 | "django.test.runner.DiscoverRunner") 26 | else: 27 | runner = get_runner(settings, 28 | "django.test.simple.DjangoTestSuiteRunner") 29 | test_suite = runner(verbosity=2, interactive=True, failfast=False) 30 | results = test_suite.run_tests(["django_comments_tree"]) 31 | return results 32 | -------------------------------------------------------------------------------- /django_comments_tree/tests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommentsXtdTestsConfig(AppConfig): 5 | name = 'django_comments_tree.tests' 6 | verbose_name = 'django-comments-tree tests' 7 | -------------------------------------------------------------------------------- /django_comments_tree/tests/data/draftjs_raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | { 4 | "key": "eup", 5 | "text": "Text 1.", 6 | "type": "unstyled", 7 | "depth": 0, 8 | "inlineStyleRanges": [], 9 | "entityRanges": [], 10 | "data": {} 11 | }, 12 | { 13 | "key": "b0cen", 14 | "text": "Test 2.", 15 | "type": "unstyled", 16 | "depth": 0, 17 | "inlineStyleRanges": [], 18 | "entityRanges": [], 19 | "data": {} 20 | } 21 | ], 22 | "entityMap": {} 23 | } 24 | -------------------------------------------------------------------------------- /django_comments_tree/tests/factories.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.geos import GEOSGeometry 2 | 3 | import factory 4 | import django_comments_tree.models as models 5 | import django_comments_tree.tests.models as tmodels 6 | from django.contrib.auth.models import User 7 | from django.utils.text import slugify 8 | 9 | 10 | class UserFactory(factory.DjangoModelFactory): 11 | class Meta: 12 | model = User 13 | 14 | username = factory.Sequence(lambda n: f"username{n}") 15 | first_name = factory.Faker('first_name') 16 | last_name = factory.Faker('first_name') 17 | email = factory.Faker('email') 18 | 19 | 20 | class ArticleFactory(factory.DjangoModelFactory): 21 | class Meta: 22 | model = tmodels.Article 23 | 24 | title = factory.Sequence(lambda n: f"Article Title {n}") 25 | slug = factory.LazyAttribute(lambda obj: slugify(obj.title)) 26 | body = factory.Faker('paragraphs', nb=3) 27 | 28 | 29 | class DiaryFactory(factory.DjangoModelFactory): 30 | class Meta: 31 | model = tmodels.Diary 32 | body = factory.Faker('paragraphs', nb=3) 33 | 34 | 35 | class CommentAssociationFactory(factory.DjangoModelFactory): 36 | class Meta: 37 | model = models.CommentAssociation 38 | 39 | 40 | class TreeCommentFactory(factory.DjangoModelFactory): 41 | class Meta: 42 | model = models.TreeComment 43 | 44 | 45 | class TreeCommentFlagFactory(factory.DjangoModelFactory): 46 | class Meta: 47 | model = models.TreeCommentFlag 48 | 49 | flag = "" 50 | -------------------------------------------------------------------------------- /django_comments_tree/tests/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.db import models 4 | from django.urls import reverse 5 | 6 | from django_comments_tree.moderation import moderator, TreeCommentModerator 7 | 8 | 9 | class PublicManager(models.Manager): 10 | """Returns published articles that are not in the future.""" 11 | 12 | def published(self): 13 | return self.get_query_set().filter(publish__lte=datetime.now()) 14 | 15 | 16 | class Article(models.Model): 17 | """Article, that accepts comments.""" 18 | 19 | title = models.CharField('title', max_length=200) 20 | slug = models.SlugField('slug', unique_for_date='publish') 21 | body = models.TextField('body') 22 | allow_comments = models.BooleanField('allow comments', default=True) 23 | publish = models.DateTimeField('publish', default=datetime.now) 24 | 25 | objects = PublicManager() 26 | 27 | class Meta: 28 | db_table = 'demo_articles' 29 | ordering = ('-publish',) 30 | 31 | def get_absolute_url(self): 32 | return reverse( 33 | 'article-detail', 34 | kwargs={'year': self.publish.year, 35 | 'month': int(self.publish.strftime('%m').lower()), 36 | 'day': self.publish.day, 37 | 'slug': self.slug}) 38 | 39 | 40 | class Diary(models.Model): 41 | """Diary, that accepts comments.""" 42 | body = models.TextField('body') 43 | allow_comments = models.BooleanField('allow comments', default=True) 44 | publish = models.DateTimeField('publish', default=datetime.now) 45 | 46 | objects = PublicManager() 47 | 48 | class Meta: 49 | db_table = 'demo_diary' 50 | ordering = ('-publish',) 51 | 52 | 53 | class DiaryCommentModerator(TreeCommentModerator): 54 | email_notification = True 55 | enable_field = 'allow_comments' 56 | auto_moderate_field = 'publish' 57 | moderate_after = 2 58 | removal_suggestion_notification = True 59 | 60 | 61 | moderator.register(Diary, DiaryCommentModerator) 62 | -------------------------------------------------------------------------------- /django_comments_tree/tests/settings_pg.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import imp 5 | import django 6 | import markdown 7 | from django_comments_tree.render import render_draftjs, render_plain 8 | 9 | from .settings import * 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.postgres', 14 | 'NAME': 'django_comments_tree', 15 | 'USER': 'django_user', 16 | 'PASSWORD': 'testing_password', 17 | 'HOST': 'localhost', 18 | 'PORT': '5440', 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /django_comments_tree/tests/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}django-comments-tree tutorial{% endblock %} 7 | 8 | 12 | 19 | 20 | 21 |
22 |
23 |
24 |
django-comments-tree tests
25 |
26 |
27 |
28 |
29 | 30 |
31 | {% block content %} 32 | {% endblock %} 33 |
34 | 35 |
36 |
37 |
38 |

django-comments-tree tests.

39 |
40 |
41 |
42 | 43 | {% block extra-js %} 44 | {% endblock %} 45 | 46 | 47 | -------------------------------------------------------------------------------- /django_comments_tree/tests/templates/registration/login.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/tests/templates/registration/login.html -------------------------------------------------------------------------------- /django_comments_tree/tests/test_api_serializers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.contrib.sites.models import Site 5 | from django.test import TestCase 6 | 7 | from django_comments_tree.api.serializers import (APICommentSerializer, 8 | WriteCommentSerializer) 9 | from django_comments_tree.models import (TreeComment) 10 | from django_comments_tree.tests.models import Article 11 | from django.contrib.auth.models import User 12 | from django.urls import reverse 13 | 14 | from rest_framework.test import APIRequestFactory, force_authenticate 15 | 16 | 17 | class TestSerializerBase(TestCase): 18 | 19 | def setUp(self): 20 | super().setUp() 21 | 22 | self.request_factory = APIRequestFactory() 23 | 24 | self.article = Article.objects.create( 25 | title="September", slug="september", body="During September...") 26 | self.article_ct = ContentType.objects.get(app_label="tests", 27 | model="article") 28 | 29 | self.site = Site.objects.get(pk=1) 30 | 31 | self.root = TreeComment.objects.get_or_create_root(self.article) 32 | 33 | self.comment = self.root.add_child(comment="just a testing comment") 34 | self.user = User.objects.create_user("bob", "", "pwd") 35 | 36 | def get_request(self, url, data): 37 | request = self.request_factory.post(url, data) 38 | 39 | if self.user: 40 | force_authenticate(request, user=self.user) 41 | return request 42 | 43 | 44 | class TestApiSerializer(TestSerializerBase): 45 | 46 | def test_serialize_data(self): 47 | """ Serialize some data, make sure it works """ 48 | 49 | serializer = APICommentSerializer(self.comment) 50 | self.assertTrue(serializer.is_valid) 51 | 52 | def test_serializer_render(self): 53 | 54 | serializer = APICommentSerializer(self.comment, 55 | context={'request': None}) 56 | rendered = json.dumps(serializer.data) 57 | self.assertIsNotNone(rendered) 58 | 59 | def test_serializer_save(self): 60 | url = reverse('comments-post-comment') 61 | request = self.get_request(url, {}) 62 | serializer = APICommentSerializer(self.comment, 63 | data={'comment': 'comment value', 64 | 'comment.rendered': '

value

', 65 | }, 66 | context={'request': request} 67 | ) 68 | self.assertTrue(serializer.is_valid()) 69 | serializer.save() 70 | self.comment.refresh_from_db() 71 | c = TreeComment.objects.get(pk=self.comment.pk) 72 | self.assertEqual(c.comment.raw, 'comment value') 73 | 74 | 75 | class TestWriteSerializer(TestSerializerBase): 76 | 77 | def test_write_serialize(self): 78 | """ Serialize some data, make sure it works """ 79 | 80 | url = reverse('comments-post-comment') 81 | request = self.get_request(url, {}) 82 | 83 | serializer = WriteCommentSerializer(self.comment, 84 | context={'request': request}) 85 | self.assertTrue(serializer.is_valid) 86 | 87 | def test_write_serializer_save(self): 88 | """ This is a big method to test """ 89 | 90 | -------------------------------------------------------------------------------- /django_comments_tree/tests/test_api_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | try: 4 | from unittest.mock import patch 5 | except ImportError: 6 | from mock import patch 7 | 8 | from django.contrib.auth.models import User 9 | from django.contrib.contenttypes.models import ContentType 10 | from django.test import TestCase 11 | from django.urls import reverse 12 | 13 | from rest_framework.test import APIRequestFactory, force_authenticate 14 | 15 | import django_comments_tree 16 | from django_comments_tree.api.views import CommentCreate 17 | from django_comments_tree.tests.models import Article, Diary 18 | 19 | 20 | request_factory = APIRequestFactory() 21 | 22 | 23 | def post_comment(data, auth_user=None): 24 | request = request_factory.post(reverse('comments-tree-api-create'), data) 25 | if auth_user: 26 | force_authenticate(request, user=auth_user) 27 | view = CommentCreate.as_view() 28 | return view(request) 29 | 30 | 31 | class CommentCreateTestCase(TestCase): 32 | def setUp(self): 33 | patcher = patch('django_comments_tree.views.comments.send_mail') 34 | self.mock_mailer = patcher.start() 35 | self.article = Article.objects.create( 36 | title="October", slug="october", body="What I did on October...") 37 | self.form = django_comments_tree.get_form()(self.article) 38 | 39 | def test_post_returns_2xx_response(self): 40 | data = {"name": "Bob", "email": "fulanito@detal.com", 41 | "followup": True, "reply_to": 0, "level": 1, "order": 1, 42 | "comment": "Es war einmal eine kleine...", 43 | "honeypot": ""} 44 | data.update(self.form.initial) 45 | response = post_comment(data) 46 | self.assertEqual(response.status_code, 204) 47 | self.assertEqual(self.mock_mailer.call_count, 1) 48 | 49 | def test_post_returns_4xx_response(self): 50 | # It uses an authenticated user, but the user has no mail address. 51 | self.user = User.objects.create_user("bob", "", "pwd") 52 | data = {"name": "", "email": "", 53 | "followup": True, "reply_to": 0, "level": 1, "order": 1, 54 | "comment": "Es war einmal eine kleine...", 55 | "honeypot": ""} 56 | data.update(self.form.initial) 57 | response = post_comment(data, auth_user=self.user) 58 | self.assertEqual(response.status_code, 400) 59 | self.assertTrue('name' in response.data) 60 | self.assertTrue('email' in response.data) 61 | self.assertEqual(self.mock_mailer.call_count, 0) 62 | -------------------------------------------------------------------------------- /django_comments_tree/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | import django_comments_tree 4 | from django_comments_tree.models import TmpTreeComment 5 | from django_comments_tree.forms import TreeCommentForm 6 | from django_comments_tree.tests.models import Article 7 | 8 | 9 | class GetFormTestCase(TestCase): 10 | 11 | def test_get_form(self): 12 | # check function django_comments_tree.get_form retrieves the due class 13 | self.assertTrue(django_comments_tree.get_form() == TreeCommentForm) 14 | 15 | 16 | class TreeCommentFormTestCase(TestCase): 17 | 18 | def setUp(self): 19 | self.article = Article.objects.create(title="September", 20 | slug="september", 21 | body="What I did on September...") 22 | self.form = django_comments_tree.get_form()(self.article) 23 | 24 | def test_get_comment_model(self): 25 | # check get_comment_model retrieves the due model class 26 | self.assertTrue(self.form.get_comment_model() == TmpTreeComment) 27 | 28 | def test_get_comment_create_data(self): 29 | # as it's used in django_comments.views.comments 30 | data = {"name": "Daniel", 31 | "email": "danirus@eml.cc", 32 | "followup": True, 33 | "reply_to": 0, "level": 1, "order": 1, 34 | "comment": "Es war einmal iene kleine..."} 35 | data.update(self.form.initial) 36 | form = django_comments_tree.get_form()(self.article, data) 37 | self.assertTrue(self.form.security_errors() == {}) 38 | self.assertTrue(self.form.errors == {}) 39 | comment = form.get_comment_object() 40 | 41 | # it does have the new field 'followup' 42 | self.assertTrue("followup" in comment) 43 | -------------------------------------------------------------------------------- /django_comments_tree/tests/test_model_associations.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from textwrap import dedent 3 | from os.path import join, dirname 4 | 5 | from django.db import connection, reset_queries 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.contrib.sites.models import Site 8 | from django.conf import settings 9 | from django.test import TestCase as DjangoTestCase, override_settings 10 | 11 | from django_comments_tree.models import (TreeComment, CommentAssociation, 12 | MaxThreadLevelExceededException) 13 | from django_comments_tree.tests.models import Article, Diary 14 | 15 | 16 | class ArticleBaseTestCase(DjangoTestCase): 17 | def setUp(self): 18 | self.article_1 = Article.objects.create( 19 | title="September", slug="september", body="During September...") 20 | self.article_2 = Article.objects.create( 21 | title="October", slug="october", body="What I did on October...") 22 | 23 | def add_comment(self, root, comment="Just a comment"): 24 | """ 25 | Add Two Comments for the article 26 | 27 | root - 28 | comment 1 29 | comment 2 30 | """ 31 | child = root.add_child(comment=comment, 32 | submit_date=datetime.now()) 33 | #root.refresh_from_db() 34 | return child 35 | 36 | def add_comments(self, root, level=2): 37 | current = root 38 | for lvl in range(level): 39 | current = self.add_comment(current) 40 | 41 | 42 | class CommentAssociationTestCase(ArticleBaseTestCase): 43 | def setUp(self): 44 | super().setUp() 45 | self.article_ct = ContentType.objects.get(app_label="tests", 46 | model="article") 47 | 48 | self.site = Site.objects.get(pk=1) 49 | self.root = TreeComment.objects.get_or_create_root(self.article_1) 50 | 51 | @override_settings(DEBUG=True) 52 | def test_association_is_added(self): 53 | root = self.root 54 | reset_queries() 55 | comment = self.add_comment(root) 56 | 57 | self.assertEqual(comment.assoc, root.commentassociation) 58 | 59 | # Only the insert of comment and update of root depth 60 | self.assertEqual(len(connection.queries), 2) 61 | 62 | @override_settings(DEBUG=True) 63 | def test_association_is_added_at_depth(self): 64 | root = self.root 65 | reset_queries() 66 | comment = self.add_comment(root) 67 | comment2 = self.add_comment(comment) 68 | comment3 = self.add_comment(comment2) 69 | 70 | self.assertEqual(comment2.assoc, root.commentassociation) 71 | self.assertEqual(comment3.assoc, root.commentassociation) 72 | 73 | # Only the insert of comment and update of root depth 74 | self.assertEqual(len(connection.queries), 6) 75 | 76 | def test_association_is_updated_on_change(self): 77 | root = self.root 78 | comment = self.add_comment(root) 79 | comment2 = self.add_comment(comment) 80 | comment3 = self.add_comment(comment2) 81 | 82 | self.assertEqual(comment2.assoc, root.commentassociation) 83 | self.assertEqual(comment3.assoc, root.commentassociation) 84 | 85 | 86 | -------------------------------------------------------------------------------- /django_comments_tree/tests/test_model_manager.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from textwrap import dedent 3 | from os.path import join, dirname 4 | 5 | from django.db import connection, reset_queries 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.contrib.sites.models import Site 8 | from django.conf import settings 9 | from django.test import TestCase as DjangoTestCase, override_settings 10 | 11 | from django_comments_tree.models import (TreeComment, CommentAssociation, 12 | MaxThreadLevelExceededException) 13 | from django_comments_tree.tests.models import Article, Diary 14 | 15 | from django_comments_tree.models import (LIKEDIT_FLAG, DISLIKEDIT_FLAG, 16 | TreeCommentFlag) 17 | from django_comments_tree.tests.factories import (ArticleFactory, 18 | UserFactory, 19 | TreeCommentFactory, 20 | TreeCommentFlagFactory) 21 | 22 | 23 | class ManagerTestBase(DjangoTestCase): 24 | 25 | @classmethod 26 | def setUpTestData(cls): 27 | cls.article_1 = ArticleFactory.create() 28 | cls.article_2 = ArticleFactory.create() 29 | cls.user1 = UserFactory.create() 30 | cls.user2 = UserFactory.create() 31 | cls.site = Site.objects.get(pk=1) 32 | cls.root_1 = TreeComment.objects.get_or_create_root(cls.article_1) 33 | cls.root_2 = TreeComment.objects.get_or_create_root(cls.article_2) 34 | 35 | cls.c1list = [] 36 | cls.c2list = [] 37 | for x in range(10): 38 | cls.c1list.append(cls.root_1.add_child(comment=f"Comment Root1 {x}")) 39 | cls.c2list.append(cls.root_2.add_child(comment=f"Comment Root2 {x}")) 40 | 41 | TreeCommentFlagFactory.create(user=cls.user1, 42 | comment=cls.c1list[0], 43 | flag=LIKEDIT_FLAG) 44 | TreeCommentFlagFactory.create(user=cls.user1, 45 | comment=cls.c1list[1], 46 | flag=DISLIKEDIT_FLAG) 47 | TreeCommentFlagFactory.create(user=cls.user1, 48 | comment=cls.c1list[2], 49 | flag=LIKEDIT_FLAG) 50 | TreeCommentFlagFactory.create(user=cls.user1, 51 | comment=cls.c1list[3], 52 | flag=DISLIKEDIT_FLAG) 53 | TreeCommentFlagFactory.create(user=cls.user1, 54 | comment=cls.c1list[7], 55 | flag=LIKEDIT_FLAG) 56 | TreeCommentFlagFactory.create(user=cls.user1, 57 | comment=cls.c1list[7], 58 | flag=TreeCommentFlag.SUGGEST_REMOVAL) 59 | 60 | 61 | 62 | class TestModelManager(ManagerTestBase): 63 | 64 | def test_user_likes(self): 65 | 66 | result = TreeComment.objects.user_flags_for_model(self.user1, 67 | self.article_1) 68 | 69 | self.assertIsNotNone(result) 70 | 71 | self.assertIn('user', result) 72 | likes = result['liked'] 73 | dislikes = result['disliked'] 74 | reported = result['reported'] 75 | self.assertEqual(len(likes), 3) 76 | self.assertEqual(len(dislikes), 2) 77 | self.assertEqual(len(reported), 1) 78 | self.assertEqual(likes, [self.c1list[0].id, self.c1list[2].id, self.c1list[7].id]) 79 | -------------------------------------------------------------------------------- /django_comments_tree/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib.auth import views as auth_views 3 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 4 | 5 | from django_comments_tree.tests import views 6 | 7 | urlpatterns = [ 8 | url(r'^accounts/login/$', auth_views.LoginView), 9 | url(r'^articles/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/' 10 | r'(?P[-\w]+)/$', 11 | views.dummy_view, 12 | name='article-detail'), 13 | url(r'^diary/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/', 14 | views.dummy_view, 15 | name='diary-detail'), 16 | url(r'^comments/', include('django_comments_tree.urls')), 17 | ] 18 | urlpatterns += staticfiles_urlpatterns() 19 | -------------------------------------------------------------------------------- /django_comments_tree/tests/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def dummy_view(request, *args, **kwargs): 5 | return HttpResponse("Got it") 6 | -------------------------------------------------------------------------------- /django_comments_tree/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib.contenttypes.views import shortcut 3 | from rest_framework.urlpatterns import format_suffix_patterns 4 | 5 | from django_comments_tree import api 6 | from .views.comments import (comment_done, confirm, dislike, dislike_done, 7 | FlagView, like, like_done, mute, post_comment, 8 | reply, sent) 9 | from .views.moderation import (approve, approve_done, delete, delete_done, 10 | flag, flag_done) 11 | 12 | urlpatterns = [ 13 | url(r'^sent/$', sent, name='comments-tree-sent'), 14 | url(r'^confirm/(?P[^/]+)/$', confirm, 15 | name='comments-tree-confirm'), 16 | url(r'^mute/(?P[^/]+)/$', mute, name='comments-tree-mute'), 17 | url(r'^reply/(?P[\d]+)/$', reply, name='comments-tree-reply'), 18 | 19 | # Remap comments-flag to check allow-flagging is enabled. 20 | url(r'^flag/(\d+)/$', FlagView.as_view(), name='comments-flag'), 21 | # New flags in addition to those provided by django-contrib-comments. 22 | url(r'^like/(\d+)/$', like, name='comments-tree-like'), 23 | url(r'^liked/$', like_done, name='comments-tree-like-done'), 24 | url(r'^dislike/(\d+)/$', dislike, name='comments-tree-dislike'), 25 | url(r'^disliked/$', dislike_done, name='comments-tree-dislike-done'), 26 | 27 | # API handlers. 28 | url(r'^api/comment/$', api.CommentCreate.as_view(), 29 | name='comments-tree-api-create'), 30 | url(r'^api/(?P\w+[-]{1}\w+)/(?P[-\w]+)/$', 31 | api.CommentList.as_view(), name='comments-tree-api-list'), 32 | url(r'^api/(?P\w+[-]{1}\w+)/(?P[-\w]+)/count/$', 33 | api.CommentCount.as_view(), name='comments-tree-api-count'), 34 | url(r'^api/feedback/$', api.ToggleFeedbackFlag.as_view(), 35 | name='comments-tree-api-feedback'), 36 | url(r'^api/flag/$', api.CreateReportFlag.as_view(), 37 | name='comments-tree-api-flag'), 38 | url(r'^api/flag/(?P\d+)/$', api.RemoveReportFlag.as_view(), 39 | name='comments-tree-api-remove-flag'), 40 | ] 41 | 42 | # Migrated from original django-contrib-comments 43 | urlpatterns += [ 44 | url(r'^post/$', post_comment, name='comments-post-comment'), 45 | url(r'^posted/$', comment_done, name='comments-comment-done'), 46 | url(r'^flag/(\d+)/$', flag, name='comments-flag'), 47 | url(r'^flagged/$', flag_done, name='comments-flag-done'), 48 | url(r'^delete/(\d+)/$', delete, name='comments-delete'), 49 | url(r'^deleted/$', delete_done, name='comments-delete-done'), 50 | url(r'^approve/(\d+)/$', approve, name='comments-approve'), 51 | url(r'^approved/$', approve_done, name='comments-approve-done'), 52 | 53 | url(r'^cr/(\d+)/(.+)/$', shortcut, name='comments-url-redirect'), 54 | ] 55 | 56 | urlpatterns = format_suffix_patterns(urlpatterns) 57 | -------------------------------------------------------------------------------- /django_comments_tree/utils.py: -------------------------------------------------------------------------------- 1 | # Idea borrowed from Selwin Ong post: 2 | # http://ui.co.id/blog/asynchronous-send_mail-in-django 3 | 4 | import queue as queue # python3 5 | 6 | import threading 7 | 8 | from django.core.mail import EmailMultiAlternatives 9 | 10 | from django_comments_tree.conf import settings 11 | 12 | 13 | mail_sent_queue = queue.Queue() 14 | 15 | 16 | class EmailThread(threading.Thread): 17 | def __init__(self, subject, body, from_email, recipient_list, 18 | fail_silently, html): 19 | self.subject = subject 20 | self.body = body 21 | self.recipient_list = recipient_list 22 | self.from_email = from_email 23 | self.fail_silently = fail_silently 24 | self.html = html 25 | threading.Thread.__init__(self) 26 | 27 | def run(self): 28 | _send_mail(self.subject, self.body, self.from_email, 29 | self.recipient_list, self.fail_silently, self.html) 30 | mail_sent_queue.put(True) 31 | 32 | 33 | def _send_mail(subject, body, from_email, recipient_list, 34 | fail_silently=False, html=None): 35 | msg = EmailMultiAlternatives(subject, body, from_email, recipient_list) 36 | if html: 37 | msg.attach_alternative(html, "text/html") 38 | msg.send(fail_silently) 39 | 40 | 41 | def send_mail(subject, body, from_email, recipient_list, 42 | fail_silently=False, html=None): 43 | if settings.COMMENTS_TREE_THREADED_EMAILS: 44 | EmailThread(subject, body, from_email, recipient_list, 45 | fail_silently, html).start() 46 | else: 47 | _send_mail(subject, body, from_email, recipient_list, 48 | fail_silently, html) 49 | 50 | 51 | def has_app_model_option(comment): 52 | _default = { 53 | 'allow_flagging': False, 54 | 'allow_feedback': False, 55 | 'show_feedback': False 56 | } 57 | # content_type = ContentType.objects.get_for_model(comment.content_object) 58 | content_type = comment.content_type 59 | key = "%s.%s" % (content_type.app_label, content_type.model) 60 | try: 61 | return settings.COMMENTS_TREE_APP_MODEL_OPTIONS[key] 62 | except KeyError: 63 | return settings.COMMENTS_TREE_APP_MODEL_OPTIONS.setdefault( 64 | 'default', _default) 65 | -------------------------------------------------------------------------------- /django_comments_tree/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /django_comments_tree/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/django_comments_tree/views/__init__.py -------------------------------------------------------------------------------- /django_comments_tree/views/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | A few bits of helper functions for comment views. 3 | """ 4 | 5 | import textwrap 6 | 7 | from urllib.parse import urlencode 8 | 9 | from django.http import HttpResponseRedirect 10 | from django.shortcuts import render, resolve_url 11 | from django.core.exceptions import ObjectDoesNotExist 12 | from django.utils.http import is_safe_url 13 | 14 | import django_comments_tree 15 | 16 | 17 | def next_redirect(request, fallback, **get_kwargs): 18 | """ 19 | Handle the "where should I go next?" part of comment views. 20 | 21 | The next value could be a 22 | ``?next=...`` GET arg or the URL of a given view (``fallback``). See 23 | the view modules for examples. 24 | 25 | Returns an ``HttpResponseRedirect``. 26 | """ 27 | next = request.POST.get('next') 28 | if not is_safe_url(url=next, allowed_hosts={request.get_host()}): 29 | next = resolve_url(fallback) 30 | 31 | if get_kwargs: 32 | if '#' in next: 33 | tmp = next.rsplit('#', 1) 34 | next = tmp[0] 35 | anchor = '#' + tmp[1] 36 | else: 37 | anchor = '' 38 | 39 | joiner = ('?' in next) and '&' or '?' 40 | next += joiner + urlencode(get_kwargs) + anchor 41 | return HttpResponseRedirect(next) 42 | 43 | 44 | def confirmation_view(template, doc="Display a confirmation view."): 45 | """ 46 | Confirmation view generator for the "comment was 47 | posted/flagged/deleted/approved" views. 48 | """ 49 | 50 | def confirmed(request): 51 | comment = None 52 | if 'c' in request.GET: 53 | try: 54 | comment = django_comments_tree.get_model().objects.get(pk=request.GET['c']) 55 | except (ObjectDoesNotExist, ValueError): 56 | pass 57 | return render(request, template, {'comment': comment}) 58 | 59 | confirmed.__doc__ = textwrap.dedent("""\ 60 | %s 61 | 62 | Templates: :template:`%s`` 63 | Context: 64 | comment 65 | The posted comment 66 | """ % (doc, template) 67 | ) 68 | return confirmed 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | volumes: 4 | postgres_data_dev: 5 | 6 | 7 | services: 8 | postgres: 9 | container_name: ${NAME:-django_comments_tree}-postgres 10 | build: ./compose/postgres 11 | volumes: 12 | # You can also modify this to point to your local Dropbox location where the shared 13 | # backups are stored. For me this is: 14 | - "${BACKUP_ROOT}:/backups" 15 | - "${LOCAL_BACKUPS}:/local_backups" 16 | environment: 17 | - POSTGRES_USER=django_user 18 | - POSTGRES_DB=comments_tree 19 | - POSTGRES_PASSWORD 20 | ports: 21 | - "${PG_PORT:-5448}:5432" 22 | 23 | -------------------------------------------------------------------------------- /docs/extensions.py: -------------------------------------------------------------------------------- 1 | def setup(app): 2 | app.add_crossref_type( 3 | directivename = "setting", 4 | rolename = "setting", 5 | indextemplate = "pair: %s; setting", 6 | ) 7 | app.add_crossref_type( 8 | directivename = "templatetag", 9 | rolename = "ttag", 10 | indextemplate = "pair: %s; template tag" 11 | ) 12 | app.add_crossref_type( 13 | directivename = "templatefilter", 14 | rolename = "tfilter", 15 | indextemplate = "pair: %s; template filter" 16 | ) 17 | app.add_crossref_type( 18 | directivename = "class", 19 | rolename = "pclass", 20 | indextemplate = "pair: %s; class" 21 | ) 22 | -------------------------------------------------------------------------------- /docs/images/comments-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/comments-enabled.png -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/extend-comments-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/extend-comments-app.png -------------------------------------------------------------------------------- /docs/images/feedback-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/feedback-buttons.png -------------------------------------------------------------------------------- /docs/images/feedback-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/feedback-users.png -------------------------------------------------------------------------------- /docs/images/flag-counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/flag-counter.png -------------------------------------------------------------------------------- /docs/images/markdown-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/markdown-comment.png -------------------------------------------------------------------------------- /docs/images/markdown-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/markdown-input.png -------------------------------------------------------------------------------- /docs/images/preview-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/preview-comment.png -------------------------------------------------------------------------------- /docs/images/reply-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/reply-link.png -------------------------------------------------------------------------------- /docs/images/update-comment-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/docs/images/update-comment-tree.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-comments-tree documentation master file, created by 2 | sphinx-quickstart on Mon Dec 19 19:20:12 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ===================== 7 | django-comments-tree 8 | ===================== 9 | 10 | .. module:: django_comments_tree 11 | :synopsis: django-comments-extended. 12 | 13 | .. highlightlang:: html+django 14 | 15 | A Django pluggable application that adds comments to your project. It extends the once official `Django Comments Framework `_ with the following features: 16 | 17 | .. index:: 18 | single: Features 19 | 20 | #. Comments model based on `django-treebeard `_ to provide a robust and fast threaded and nested comment structure. 21 | #. Efficiently associate a comment tree with any model through a link table which avoids populating each comment with extra link data. 22 | #. Customizable maximum thread level, either for all models or on a per app.model basis. 23 | #. Optional notifications on follow-up comments via email. 24 | #. Mute links to allow cancellation of follow-up notifications. 25 | #. Comment confirmation via email when users are not authenticated. 26 | #. Comments hit the database only after they have been confirmed. 27 | #. Registered users can like/dislike comments and can suggest comments removal. 28 | #. Template tags to list/render the last N comments posted to any given list of app.model pairs. 29 | #. Emails sent through threads (can be disable to allow other solutions, like a Celery app). 30 | #. Fully functional JavaScript plugin using ReactJS, jQuery, Bootstrap, Remarkable and MD5. 31 | 32 | .. image:: images/cover.png 33 | 34 | Contents 35 | ======== 36 | 37 | .. toctree:: 38 | :maxdepth: 1 39 | 40 | quickstart 41 | tutorial 42 | example 43 | logic 44 | webapi 45 | javascript 46 | templatetags 47 | migrating 48 | extending 49 | i18n 50 | settings 51 | templates 52 | -------------------------------------------------------------------------------- /docs/migrating.rst: -------------------------------------------------------------------------------- 1 | .. _ref-migrating: 2 | 3 | ================================ 4 | Migrating to django-comments-tree 5 | ================================ 6 | 7 | If your project uses django-contrib-comments you can easily plug django-comments-tree to add extra functionalities like comment confirmation by mail, comment threading and follow-up notifications. 8 | 9 | This section describes how to make django-comments-tree take over comments support in a project in which django-contrib-comments tables have received data already. 10 | 11 | 12 | Preparation 13 | =========== 14 | 15 | First of all, install django-comments-tree: 16 | 17 | .. code-block:: bash 18 | 19 | (venv)$ cd mysite 20 | (venv)$ pip install django-comments-tree 21 | 22 | Then edit the settings module and change your :setting:`INSTALLED_APPS` so that django_comments_tree and django_comments are listed in this order. Also change the :setting:`COMMENTS_APP` and add the ``EMAIL_*`` settings to be able to send mail messages: 23 | 24 | .. code-block:: python 25 | 26 | INSTALLED_APPS = [ 27 | ... 28 | 'django_comments_tree', 29 | 'django_comments', 30 | ... 31 | ] 32 | ... 33 | COMMENTS_APP = 'django_comments_tree' 34 | 35 | # Either enable sending mail messages to the console: 36 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 37 | 38 | # Or set up the EMAIL_* settings so that Django can send emails: 39 | EMAIL_HOST = "smtp.mail.com" 40 | EMAIL_PORT = "587" 41 | EMAIL_HOST_USER = "alias@mail.com" 42 | EMAIL_HOST_PASSWORD = "yourpassword" 43 | EMAIL_USE_TLS = True 44 | DEFAULT_FROM_EMAIL = "Helpdesk " 45 | 46 | 47 | Edit the urls module of the project and mount django_comments_tree's URLs in the path in which you had django_comments' URLs, django_comments_tree's URLs includes django_comments': 48 | 49 | .. code-block:: python 50 | 51 | from django.conf.urls import include, url 52 | 53 | urlpatterns = [ 54 | ... 55 | url(r'^comments/', include('django_comments_tree.urls')), 56 | ... 57 | ] 58 | 59 | 60 | Now create the tables for django-comments-tree: 61 | 62 | .. code-block:: bash 63 | 64 | (venv)$ python manage.py migrate 65 | 66 | 67 | Populate comment data 68 | ===================== 69 | 70 | The following step will populate **TreeComment**'s table with data from the **Comment** model. For that purpose you can use the ``populate_xtdcomments`` management command: 71 | 72 | .. code-block:: bash 73 | 74 | (venv)$ python manage.py populate_xtdcomments 75 | Added 3468 TreeComment object(s). 76 | 77 | You can pass as many DB connections as you have defined in :setting:`DATABASES` and the command will run in each of the databases, populating the **TreeComment**'s table with data from the comments table existing in each database. 78 | 79 | Now the project is ready to handle comments with django-comments-tree. 80 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Example Directory ## 2 | 3 | Contains three example projects: 4 | 5 | 1. Simple 6 | 2. Custom 7 | 3. Comp 8 | 9 | ### Simple ### 10 | 11 | The Simple demo site is a project with an 'articles' application and an 'Article' model whose instances accept comments. It features: 12 | 13 | * Comments have to be confirmed by mail before they hit the database, unless users are authenticated or `COMMENTS_TREE_CONFIRM_EMAIL` is set to False. 14 | * Commenters may request follow up notifications. 15 | * Mute links to allow cancellation of follow-up notifications. 16 | 17 | 18 | ### Custom ### 19 | 20 | The Custom demo exhibits how to extend django-comments-tree. It uses the same **articles** app present in the other demos, plus: 21 | 22 | * A new application, called `mycomments`, with a model `MyComment` that extends the `django_comments_tree.models.MyComment` model with a field `title`. 23 | * Checkout the [custom](https://github.com/sharpertool/django-comments-tree/example/custom/) demo directory and [Customizing django-comments-tree](http://django-comments-tree.readthedocs.io/en/latest/extending.html) in the documentation. 24 | 25 | 26 | ### Comp ### 27 | 28 | The Comp demo implements two apps, each of which contains a model whose instances can received comments: 29 | 30 | 1. App `articles` with the model `Article` 31 | 1. App `quotes` with the model `Quote` 32 | 33 | It features: 34 | 35 | 1. Comments can be nested, and the maximum thread level is established to 2. 36 | 1. Comment confirmation via mail when the users are not authenticated. 37 | 1. Comments hit the database only after they have been confirmed. 38 | 1. Follow up notifications via mail. 39 | 1. Mute links to allow cancellation of follow-up notifications. 40 | 1. Registered users can like/dislike comments and can suggest comments removal. 41 | 1. Registered users can see the list of users that liked/disliked comments. 42 | 1. The homepage presents the last 5 comments posted either to the `articles.Article` or the `quotes.Quote` model. 43 | -------------------------------------------------------------------------------- /example/comp/README.md: -------------------------------------------------------------------------------- 1 | ## Comp example project ## 2 | 3 | The Comp Demo implements two apps, each of which contains a model whose instances can received comments: 4 | 5 | 1. App `articles` with the model `Article` 6 | 1. App `quotes` with the model `Quote` contained inside the `extra` directory. 7 | ### Features 8 | 9 | 1. Comments can be nested, and the maximum thread level is established to 2. 10 | 1. Comment confirmation via mail when the users are not authenticated. 11 | 1. Comments hit the database only after they have been confirmed. 12 | 1. Follow up notifications via mail. 13 | 1. Mute links to allow cancellation of follow-up notifications. 14 | 1. Registered users can like/dislike comments and can suggest comments removal. 15 | 1. Registered users can see the list of users that liked/disliked comments. 16 | 1. The homepage presents the last 5 comments posted either to the `articles.Article` or the `quotes.Quote` model. 17 | 18 | #### Threaded comments 19 | 20 | The setting `COMMENTS_TREE_MAX_THREAD_LEVEL` is set to 2, meaning that comments may be threaded up to 2 levels below the the first level (internally known as level 0):: 21 | 22 | First comment (level 0) 23 | |-- Comment to "First comment" (level 1) 24 | |-- Comment to "Comment to First comment" (level 2) 25 | 26 | #### `render_treecomment_tree` 27 | 28 | By using the `render_treecomment_tree` templatetag, both, `article_detail.html` and `quote_detail.html`, show the tree of comments posted. `article_detail.html` makes use of the arguments `allow_feedback`, `show_feedback` and `allow_flagging`, while `quote_detail.html` only show the list of comments, with no extra arguments, so users can't flag comments for removal, and neither can submit like/dislike feedback. 29 | 30 | #### `render_last_treecomments` 31 | 32 | The **Last 5 Comments** shown in the block at the rigght uses the templatetag `render_last_treecomments` to show the last 5 comments posted to either `articles.Article` or `quotes.Quote` instances. The templatetag receives the list of pairs `app.model` from which we want to gather comments and shows the given N last instances posted. The templatetag renders the template `django_comments_tree/comment.html` for each comment retrieve. 33 | -------------------------------------------------------------------------------- /example/comp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/__init__.py -------------------------------------------------------------------------------- /example/comp/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/articles/__init__.py -------------------------------------------------------------------------------- /example/comp/articles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from comp.articles.models import Article 4 | 5 | class ArticleAdmin(admin.ModelAdmin): 6 | list_display = ('title', 'publish', 'allow_comments') 7 | list_filter = ('publish',) 8 | search_fields = ('title', 'body') 9 | prepopulated_fields = {'slug': ('title',)} 10 | fieldsets = ((None, 11 | {'fields': ('title', 'slug', 'body', 12 | 'allow_comments', 'publish',)}),) 13 | 14 | admin.site.register(Article, ArticleAdmin) 15 | -------------------------------------------------------------------------------- /example/comp/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-12 14:43 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200, verbose_name='title')), 22 | ('slug', models.SlugField(unique_for_date='publish', verbose_name='slug')), 23 | ('body', models.TextField(verbose_name='body')), 24 | ('allow_comments', models.BooleanField(default=True, verbose_name='allow comments')), 25 | ('publish', models.DateTimeField(default=datetime.datetime.now, verbose_name='publish')), 26 | ], 27 | options={ 28 | 'db_table': 'comp_articles', 29 | 'ordering': ('-publish',), 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /example/comp/articles/migrations/0002_auto_20170523_1614.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-05-23 14:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('articles', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='article', 18 | name='publish', 19 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /example/comp/articles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/articles/migrations/__init__.py -------------------------------------------------------------------------------- /example/comp/articles/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django 4 | from django.db import models 5 | from django.urls import reverse 6 | from django.utils import timezone 7 | 8 | 9 | class PublicManager(models.Manager): 10 | """Returns published articles that are not in the future.""" 11 | 12 | def published(self): 13 | return self.get_queryset().filter(publish__lte=timezone.now()) 14 | 15 | 16 | class Article(models.Model): 17 | """Article, that accepts comments.""" 18 | 19 | title = models.CharField('title', max_length=200) 20 | slug = models.SlugField('slug', unique_for_date='publish') 21 | body = models.TextField('body') 22 | allow_comments = models.BooleanField('allow comments', default=True) 23 | publish = models.DateTimeField('publish', default=timezone.now) 24 | 25 | objects = PublicManager() 26 | 27 | class Meta: 28 | db_table = 'comp_articles' 29 | ordering = ('-publish',) 30 | 31 | def __str__(self): 32 | return self.title 33 | 34 | def get_absolute_url(self): 35 | return reverse( 36 | 'articles-article-detail', 37 | kwargs={'year': self.publish.year, 38 | 'month': int(self.publish.strftime('%m').lower()), 39 | 'day': self.publish.day, 40 | 'slug': self.slug}) 41 | -------------------------------------------------------------------------------- /example/comp/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import ListView, DateDetailView 3 | 4 | from comp.articles.models import Article 5 | from comp.articles.views import ArticleDetailView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', 10 | ListView.as_view(queryset=Article.objects.published()), 11 | name='articles-index'), 12 | 13 | url((r'^(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/' 14 | r'(?P[-\w]+)/$'), 15 | ArticleDetailView.as_view(), 16 | name='articles-article-detail'), 17 | ] 18 | -------------------------------------------------------------------------------- /example/comp/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.views.generic import DateDetailView 3 | 4 | from comp.articles.models import Article 5 | 6 | 7 | class ArticleDetailView(DateDetailView): 8 | model = Article 9 | date_field = "publish" 10 | month_format = "%m" 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | context.update({'next': reverse('comments-tree-sent')}) 15 | return context 16 | -------------------------------------------------------------------------------- /example/comp/context_processors.py: -------------------------------------------------------------------------------- 1 | from django_comments_tree.conf import settings as _settings 2 | 3 | def settings(request): 4 | return {'settings': _settings} 5 | -------------------------------------------------------------------------------- /example/comp/extra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/extra/__init__.py -------------------------------------------------------------------------------- /example/comp/extra/quotes/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'comp.extra.quotes.apps.QuotesConfig' 2 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from comp.extra.quotes.models import Quote 4 | 5 | class QuoteAdmin(admin.ModelAdmin): 6 | list_display = ('title', 'author', 'publish', 'allow_comments') 7 | list_filter = ('publish',) 8 | search_fields = ('title', 'quote', 'author') 9 | prepopulated_fields = {'slug': ('title',)} 10 | fieldsets = ((None, 11 | {'fields': ('title', 'slug', 'quote', 'author', 'url_source', 12 | 'allow_comments', 'publish',)}),) 13 | 14 | admin.site.register(Quote, QuoteAdmin) 15 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class QuotesConfig(AppConfig): 5 | name = 'comp.extra.quotes' 6 | verbose_name = 'Quotes' 7 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-04 11:45 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Quote', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200, verbose_name='title')), 22 | ('slug', models.SlugField(unique_for_date='publish', verbose_name='slug')), 23 | ('quote', models.TextField(verbose_name='quote')), 24 | ('author', models.CharField(max_length=255, verbose_name='author')), 25 | ('url_source', models.URLField(blank=True, null=True, verbose_name='url source')), 26 | ('allow_comments', models.BooleanField(default=True, verbose_name='allow comments')), 27 | ('publish', models.DateTimeField(default=datetime.datetime.now, verbose_name='publish')), 28 | ], 29 | options={ 30 | 'db_table': 'comp_quotes', 31 | 'ordering': ('-publish',), 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/migrations/0002_auto_20170523_1614.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-05-23 14:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('quotes', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='quote', 18 | name='publish', 19 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/extra/quotes/migrations/__init__.py -------------------------------------------------------------------------------- /example/comp/extra/quotes/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django 4 | from django.db import models 5 | from django.urls import reverse 6 | from django.utils import timezone 7 | 8 | 9 | from django_comments_tree.moderation import moderator, SpamModerator 10 | 11 | 12 | class PublicManager(models.Manager): 13 | """Returns published quotes that are not in the future.""" 14 | 15 | def published(self): 16 | return self.get_queryset().filter(publish__lte=timezone.now()) 17 | 18 | 19 | class Quote(models.Model): 20 | """Quote, that accepts comments.""" 21 | 22 | title = models.CharField('title', max_length=200) 23 | slug = models.SlugField('slug', unique_for_date='publish') 24 | quote = models.TextField('quote') 25 | author = models.CharField('author', max_length=255) 26 | url_source = models.URLField('url source', blank=True, null=True) 27 | allow_comments = models.BooleanField('allow comments', default=True) 28 | publish = models.DateTimeField('publish', default=timezone.now) 29 | 30 | objects = PublicManager() 31 | 32 | class Meta: 33 | db_table = 'comp_quotes' 34 | ordering = ('-publish',) 35 | 36 | def __str__(self): 37 | return self.title 38 | 39 | def get_absolute_url(self): 40 | return reverse('quotes-quote-detail', kwargs={'slug': self.slug}) 41 | 42 | 43 | class QuoteCommentModerator(SpamModerator): 44 | email_notification = True 45 | auto_moderate_field = 'publish' 46 | moderate_after = 365 47 | 48 | 49 | moderator.register(Quote, QuoteCommentModerator) 50 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import ListView, DateDetailView 3 | 4 | from comp.extra.quotes.models import Quote 5 | from comp.extra.quotes.views import QuoteDetailView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', ListView.as_view(queryset=Quote.objects.published()), 10 | name='quotes-index'), 11 | 12 | url((r'^(?P[-\w]+)/$'), QuoteDetailView.as_view(), 13 | name='quotes-quote-detail'), 14 | ] 15 | -------------------------------------------------------------------------------- /example/comp/extra/quotes/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.views.generic import DetailView 3 | 4 | from comp.extra.quotes.models import Quote 5 | 6 | 7 | class QuoteDetailView(DetailView): 8 | model = Quote 9 | 10 | def get_context_data(self, **kwargs): 11 | context = super().get_context_data(**kwargs) 12 | context.update({'next': reverse('comments-tree-sent')}) 13 | return context 14 | -------------------------------------------------------------------------------- /example/comp/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd `dirname $0` > /dev/null 4 | PRJPATH=`pwd` 5 | popd > /dev/null 6 | FIXTURESPATH=${PRJPATH}/../fixtures 7 | 8 | check_ret() { 9 | [[ $? -ne 0 ]] && echo "Failed." && exit 1 10 | } 11 | 12 | cd ${PRJPATH} 13 | 14 | #------------------------------ 15 | echo "Checking Django version... " 16 | # Retrieve 1.7 as 1.07, so that they can be compared as decimal numbers. 17 | version=`python -c 'import django; print("%d.%02d" % django.VERSION[:2])'` 18 | if [[ ${version} < "1.07" ]]; then 19 | python manage.py syncdb --noinput || check_ret 20 | python manage.py migrate django_comments_tree || check_ret 21 | else 22 | python manage.py migrate --noinput || check_ret 23 | fi 24 | 25 | #------------------------------ 26 | echo "Loading fixture files... " 27 | fixtures=(auth sites articles quotes) 28 | for file in ${fixtures[@]}; do 29 | python manage.py loaddata ${FIXTURESPATH}/${file}.json || check_ret 30 | done 31 | -------------------------------------------------------------------------------- /example/comp/manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, '../..') # parent of django_comments_tree directory 5 | sys.path.insert(0, '..') # demos directory 6 | 7 | if __name__ == "__main__": 8 | from django.core.management import execute_from_command_line 9 | import imp 10 | try: 11 | imp.find_module('settings') # Assumed to be in the same directory. 12 | except ImportError: 13 | import sys 14 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 15 | sys.exit(1) 16 | 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "comp.settings") 18 | execute_from_command_line(sys.argv) 19 | -------------------------------------------------------------------------------- /example/comp/requirements.txt: -------------------------------------------------------------------------------- 1 | django-markdown2 2 | -e git+ssh://git@github.com/mbi/django-rosetta.git#egg=django-rosetta 3 | -------------------------------------------------------------------------------- /example/comp/templates/articles/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load comments_tree %} 5 | 6 | {% block title %} 7 | {{ block.super }} » {{ object.title}} 8 | {% endblock %} 9 | 10 | {% block menu-class-articles %}active{% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 |

{{ object.title }}

17 |
{{ object.publish }}
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ object.body|linebreaks }} 25 |
26 |
27 |
28 | 29 |
30 |
31 |

32 | Back to the articles list 33 |

34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | {% endblock content %} 44 | 45 | {% block extra_js %} 46 | 56 | 57 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /example/comp/templates/articles/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n comments %} 3 | 4 | {% block menu-class-articles %}active{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |

{% trans "List of articles" %}

11 |
12 |
13 |
14 |
15 |
16 | {% if object_list %} 17 |
    18 | {% for article in object_list %} 19 | {% get_comment_count for article as comment_count %} 20 |
  • 21 | {{ article.title }} 22 | {{ article.publish|date:"d-F-Y" }} 23 | {% if comment_count %} 24 | - {{ comment_count }} comment{{ comment_count|pluralize }} 25 | {% endif %} 26 |
  • 27 | {% endfor %} 28 |
29 | {% endif %} 30 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /example/comp/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load md2 %} 3 | {% load comments_tree %} 4 | 5 | {% block menu-class-homepage %}active{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

{{ readme_text|markdown }}

17 |
18 |
19 |
20 |
21 |
22 |
Last 5 comments
23 |
24 | {% get_treecomment_count as comment_count for articles.article quotes.quote %} 25 | {% if comment_count %} 26 |
27 | {% render_last_treecomments 5 for articles.article quotes.quote %} 28 |
29 |
30 | comment list 31 |
32 | {% else %} 33 |

No comments yet.

34 | {% endif %} 35 |
36 |
37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /example/comp/templates/includes/django_comments_xtd/comment_content.html: -------------------------------------------------------------------------------- 1 | {% load md2 %} 2 | {{ content|markdown:"safe, code-friendly, code-color" }} 3 | -------------------------------------------------------------------------------- /example/comp/templates/quotes/quote_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block menu-class-quotes %}active{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |

{{ object.title }}

12 |
{{ object.publish }}
13 |
14 |
15 |
16 | 17 |
18 |
19 |
{{ object.quote|linebreaks }}
20 |

21 | {% if object.url_source %}{{ object.author }}{% else %}{{ object.author }}{% endif %} 22 |

23 |
24 |
25 | 26 |
27 |
28 |
29 | {% get_comment_count for object as comment_count %} 30 | {% if comment_count %} 31 |
32 | {% blocktrans count comment_count=comment_count %} 33 | {{ comment_count }} comment. 34 | {% plural %} 35 | {{ comment_count }} comments. 36 | {% endblocktrans %} 37 |
38 | {% endif %} 39 | 40 | {% if object.allow_comments %} 41 |
42 |
43 |

Post your comment

44 | {% render_comment_form for object %} 45 |
46 |
47 | {% else %} 48 |
comments are disabled for this quote
49 | {% endif %} 50 | 51 | {% if comment_count %} 52 |
53 | {% render_treecomment_tree for object allow_feedback show_feedback allow_flagging %} 54 |
55 | {% endif %} 56 |
57 |
58 |
59 | {% endblock %} 60 | 61 | {% block extra_js %} 62 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /example/comp/templates/quotes/quote_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n comments %} 3 | 4 | {% block menu-class-quotes %}active{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |

{% trans "List of quotes" %}

11 |
12 |
13 |
14 |
15 |
16 | {% if object_list %} 17 |
    18 | {% for quote in object_list %} 19 | {% get_comment_count for quote as comment_count %} 20 |
  • 21 | {{ quote.title }} 22 | ⋅ 23 | by {{ quote.author }} 24 | {% if comment_count %} 25 | - {{ comment_count }} comment{{ comment_count|pluralize }} 26 | {% endif %} 27 |
  • 28 | {% endfor %} 29 |
30 | {% endif %} 31 |
32 |
33 |
34 | {% endblock %} 35 | 36 | -------------------------------------------------------------------------------- /example/comp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/comp/templatetags/__init__.py -------------------------------------------------------------------------------- /example/comp/templatetags/comp_filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.template import Library, TemplateSyntaxError 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from django.conf import settings 6 | 7 | 8 | register = Library() 9 | 10 | 11 | lang_names = { 12 | 'de': _('German'), 13 | 'en': _('English'), 14 | 'es': _('Spanish'), 15 | 'fi': _('Finnish'), 16 | 'fr': _('French'), 17 | 'hu': _('Hungarian'), 18 | 'it': _('Italian'), 19 | 'nl': _('Dutch'), 20 | 'pl': _('Polish'), 21 | 'pt': _('Portuguese'), 22 | 'ru': _('Russian'), 23 | 'tr': _('Turkish'), 24 | 'uk': _('Ukrainian'), 25 | 'zh': _('Chinese'), 26 | } 27 | 28 | 29 | lang_orig_names = { 30 | 'de': 'Deutsch', 31 | 'en': 'English', 32 | 'es': 'español', 33 | 'fi': 'suomi', 34 | 'fr': 'français', 35 | 'hu': 'magyar', 36 | 'it': 'italiano', 37 | 'nl': 'Nederlands', 38 | 'pl': 'polski', 39 | 'pt': 'português', 40 | 'ru': 'русский', 41 | 'tr': 'Türkçe', 42 | 'uk': 'українська', 43 | 'zh': '中文', 44 | } 45 | 46 | 47 | @register.filter 48 | def language_name(value): 49 | try: 50 | return lang_orig_names[value] 51 | except KeyError: 52 | return value 53 | language_name.is_safe = True 54 | 55 | 56 | @register.filter 57 | def language_tuple(value): 58 | try: 59 | return "%s, %s" % (lang_orig_names[value], lang_names[value]) 60 | except KeyError: 61 | return value 62 | language_tuple.is_safe = True 63 | -------------------------------------------------------------------------------- /example/comp/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | 6 | if django.VERSION[:2] < (2, 0): 7 | from django.conf.urls import include, url as re_path 8 | else: 9 | from django.urls import include, re_path 10 | 11 | from django.views.i18n import JavaScriptCatalog 12 | 13 | from django_comments_tree import LatestCommentFeed 14 | from django_comments_tree.views.comments import TreeCommentListView 15 | 16 | from comp import views 17 | 18 | 19 | admin.autodiscover() 20 | 21 | 22 | urlpatterns = [ 23 | re_path(r'^$', views.HomepageView.as_view(), name='homepage'), 24 | re_path(r'^i18n/', include('django.conf.urls.i18n')), 25 | re_path(r'^articles/', include('comp.articles.urls')), 26 | re_path(r'^quotes/', include('comp.extra.quotes.urls')), 27 | re_path(r'^comments/', include('django_comments_tree.urls')), 28 | re_path(r'^comments/$', 29 | TreeCommentListView.as_view(content_types=["articles.article", 30 | "quotes.quote"], 31 | paginate_by=10, page_range=5), 32 | name='comments-tree-list'), 33 | re_path(r'^feeds/comments/$', LatestCommentFeed(), name='comments-feed'), 34 | re_path(r'^api-auth/', include('rest_framework.urls', 35 | namespace='rest_framework')), 36 | re_path(r'^jsi18n/$', JavaScriptCatalog.as_view(), 37 | name='javascript-catalog'), 38 | re_path(r'admin/', admin.site.urls), 39 | ] 40 | 41 | 42 | if settings.DEBUG: 43 | urlpatterns += staticfiles_urlpatterns() 44 | 45 | 46 | if 'rosetta' in settings.INSTALLED_APPS: 47 | urlpatterns += [re_path(r'^rosetta/', include('rosetta.urls'))] 48 | -------------------------------------------------------------------------------- /example/comp/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class HomepageView(TemplateView): 5 | template_name = "homepage.html" 6 | 7 | def get(self, request, *args, **kwargs): 8 | context = self.get_context_data(**kwargs) 9 | text = open("README.md").read() 10 | context['readme_text'] = text.split("\n", 1)[1][1:] 11 | return self.render_to_response(context) 12 | -------------------------------------------------------------------------------- /example/comp/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "comp.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /example/custom/README.md: -------------------------------------------------------------------------------- 1 | ## Custom example project ## 2 | 3 | The Custom Demo exhibits how to extend django-comments-tree. This demo used the same **articles** app present in the other two demos, plus: 4 | 5 | * A new django application, called `mycomments`, with a model `MyComment` that extends the `django_comments_tree.models.MyComment` model with a field `title`. 6 | 7 | To extend django-comments-tree follow the next steps: 8 | 9 | 1. Set up `COMMENTS_APP` to `django_comments_tree` 10 | 1. Set up `COMMENTS_TREE_MODEL` to the new model class name, for this demo: `mycomments.models.MyComment` 11 | 1. Set up `COMMENTS_TREE_FORM_CLASS` to the new form class name, for this demo: `mycomments.forms.MyCommentForm` 12 | 1. Change the following templates: 13 | * `comments/form.html` to include new fields. 14 | * `comments/preview.html` to preview new fields. 15 | * `django_comments_tree/email_confirmation_request.{txt|html}` to add the new fields to the confirmation request, if it was necessary. This demo overrides them to include the `title` field in the mail. 16 | * `django_comments_tree/comments_tree.html` to show the new field when displaying the comments. If your project doesn't allow nested comments you can use either this template or `comments/list.html`. 17 | * `django_comments_tree/reply.html` to show the new field when displaying the comment the user is replying to. 18 | -------------------------------------------------------------------------------- /example/custom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/custom/__init__.py -------------------------------------------------------------------------------- /example/custom/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/custom/articles/__init__.py -------------------------------------------------------------------------------- /example/custom/articles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from custom.articles.models import Article 4 | 5 | class ArticleAdmin(admin.ModelAdmin): 6 | list_display = ('title', 'publish', 'allow_comments') 7 | list_filter = ('publish',) 8 | search_fields = ('title', 'body') 9 | prepopulated_fields = {'slug': ('title',)} 10 | fieldsets = ((None, 11 | {'fields': ('title', 'slug', 'body', 12 | 'allow_comments', 'publish',)}),) 13 | 14 | admin.site.register(Article, ArticleAdmin) 15 | -------------------------------------------------------------------------------- /example/custom/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-12 14:29 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200, verbose_name='title')), 22 | ('slug', models.SlugField(unique_for_date='publish', verbose_name='slug')), 23 | ('body', models.TextField(verbose_name='body')), 24 | ('allow_comments', models.BooleanField(default=True, verbose_name='allow comments')), 25 | ('publish', models.DateTimeField(default=datetime.datetime.now, verbose_name='publish')), 26 | ], 27 | options={ 28 | 'db_table': 'custom_articles', 29 | 'ordering': ('-publish',), 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /example/custom/articles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/custom/articles/migrations/__init__.py -------------------------------------------------------------------------------- /example/custom/articles/models.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from datetime import datetime 5 | 6 | import django 7 | from django.db import models 8 | from django.urls import reverse 9 | 10 | 11 | class PublicManager(models.Manager): 12 | """Returns published articles that are not in the future.""" 13 | 14 | if django.VERSION < (1, 6): 15 | get_queryset = models.Manager.get_query_set 16 | 17 | def published(self): 18 | return self.get_queryset().filter(publish__lte=datetime.now()) 19 | 20 | 21 | class Article(models.Model): 22 | """Article, that accepts comments.""" 23 | 24 | title = models.CharField('title', max_length=200) 25 | slug = models.SlugField('slug', unique_for_date='publish') 26 | body = models.TextField('body') 27 | allow_comments = models.BooleanField('allow comments', default=True) 28 | publish = models.DateTimeField('publish', default=datetime.now) 29 | 30 | objects = PublicManager() 31 | 32 | class Meta: 33 | db_table = 'custom_articles' 34 | ordering = ('-publish',) 35 | 36 | def __str__(self): 37 | return '%s' % self.title 38 | 39 | def get_absolute_url(self): 40 | return reverse( 41 | 'articles-article-detail', 42 | kwargs={'year': self.publish.year, 43 | 'month': int(self.publish.strftime('%m').lower()), 44 | 'day': self.publish.day, 45 | 'slug': self.slug}) 46 | -------------------------------------------------------------------------------- /example/custom/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import ListView, DateDetailView 3 | 4 | from custom.articles.models import Article 5 | from custom.articles.views import ArticleDetailView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', 10 | ListView.as_view(queryset=Article.objects.published()), 11 | name='articles-index'), 12 | 13 | url((r'^(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/' 14 | r'(?P[-\w]+)/$'), 15 | ArticleDetailView.as_view(), 16 | name='articles-article-detail'), 17 | ] 18 | -------------------------------------------------------------------------------- /example/custom/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.views.generic import DateDetailView 3 | 4 | from custom.articles.models import Article 5 | 6 | 7 | class ArticleDetailView(DateDetailView): 8 | model = Article 9 | date_field = "publish" 10 | month_format = "%m" 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | context.update({'next': reverse('comments-tree-sent')}) 15 | return context 16 | -------------------------------------------------------------------------------- /example/custom/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd `dirname $0` > /dev/null 4 | PRJPATH=`pwd` 5 | popd > /dev/null 6 | FIXTURESPATH=${PRJPATH}/../fixtures 7 | 8 | check_ret() { 9 | [[ $? -ne 0 ]] && echo "Failed." && exit 1 10 | } 11 | 12 | cd ${PRJPATH} 13 | 14 | #------------------------------ 15 | echo "Checking Django version... " 16 | # Retrieve 1.7 as 1.07, so that they can be compared as decimal numbers. 17 | version=`python -c 'import django; print("%d.%02d" % django.VERSION[:2])'` 18 | if [[ ${version} < "1.07" ]]; then 19 | python manage.py syncdb --noinput || check_ret 20 | python manage.py migrate django_comments_tree || check_ret 21 | else 22 | python manage.py migrate --noinput || check_ret 23 | fi 24 | 25 | #------------------------------ 26 | echo "Loading fixture files... " 27 | fixtures=(auth sites articles) 28 | for file in ${fixtures[@]}; do 29 | python manage.py loaddata ${FIXTURESPATH}/${file}.json || check_ret 30 | done 31 | -------------------------------------------------------------------------------- /example/custom/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.insert(0, '../..') # parent of django_comments_tree directory 6 | sys.path.insert(0, '..') # demos directory 7 | 8 | if __name__ == "__main__": 9 | from django.core.management import execute_from_command_line 10 | import imp 11 | try: 12 | imp.find_module('settings') # Assumed to be in the same directory. 13 | except ImportError: 14 | import sys 15 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 16 | sys.exit(1) 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "custom.settings") 19 | execute_from_command_line(sys.argv) 20 | -------------------------------------------------------------------------------- /example/custom/mycomments/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/custom/mycomments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from django_comments_tree.admin import TreeCommentsAdmin 5 | from custom.mycomments.models import MyComment 6 | 7 | 8 | class MyCommentAdmin(TreeCommentsAdmin): 9 | list_display = ('title', 'name', 10 | 'object_id', 'submit_date', 'followup', 'is_public', 11 | 'is_removed') 12 | list_display_links = ('cid', 'title') 13 | fieldsets = ( 14 | (_('Content'), {'fields': ('title', 'user', 'user_name', 'user_email', 15 | 'user_url', 'comment', 'followup')}), 16 | (_('Metadata'), {'fields': ('submit_date', 'ip_address', 17 | 'is_public', 'is_removed')}), 18 | ) 19 | 20 | admin.site.register(MyComment, MyCommentAdmin) 21 | 22 | -------------------------------------------------------------------------------- /example/custom/mycomments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyCommentsConfig(AppConfig): 5 | name = 'mycomments' 6 | 7 | -------------------------------------------------------------------------------- /example/custom/mycomments/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from django_comments_tree.forms import TreeCommentForm 5 | from django_comments_tree.models import TmpTreeComment 6 | 7 | 8 | class MyCommentForm(TreeCommentForm): 9 | title = forms.CharField(max_length=256, 10 | widget=forms.TextInput( 11 | attrs={'placeholder': _('title'), 12 | 'class': 'form-control'})) 13 | 14 | def get_comment_create_data(self, site_id=None): 15 | data = super().get_comment_create_data() 16 | data.update({'title': self.cleaned_data['title']}) 17 | return data 18 | -------------------------------------------------------------------------------- /example/custom/mycomments/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-16 20:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('django_comments_tree', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='MyComment', 20 | fields=[ 21 | ('xtdcomment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='django_comments_tree.TreeComment')), 22 | ('title', models.CharField(max_length=256)), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | bases=('django_comments_tree.treecomment',), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /example/custom/mycomments/migrations/0002_auto_20170523_1624.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-05-23 16:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('mycomments', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='mycomment', 17 | options={'ordering': ('submit_date',), 'permissions': [('can_moderate', 'Can moderate comments')], 'verbose_name': 'comment', 'verbose_name_plural': 'comments'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /example/custom/mycomments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/custom/mycomments/migrations/__init__.py -------------------------------------------------------------------------------- /example/custom/mycomments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_comments_tree.models import TreeComment 4 | 5 | 6 | class MyComment(TreeComment): 7 | title = models.CharField(max_length=256) 8 | -------------------------------------------------------------------------------- /example/custom/mycomments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/custom/mycomments/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /example/custom/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/custom/requirements.txt -------------------------------------------------------------------------------- /example/custom/templates/articles/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block header %}Article Detail{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{{ object.title }}

10 |
{{ object.publish }}
11 |
12 |
13 |
14 | {{ object.body|linebreaks }} 15 |
16 |

17 | back to the articles list 18 |

19 | 20 |
21 | {% get_comment_count for object as comment_count %} 22 | {% if comment_count %} 23 |
24 | {% blocktrans count comment_count=comment_count %} 25 | There is {{ comment_count }} comment below. 26 | {% plural %} 27 | There are {{ comment_count }} comments below. 28 | {% endblocktrans %} 29 |
30 |
31 | {% endif %} 32 | 33 | {% if object.allow_comments %} 34 |
35 |

Post your comment

36 |
37 | {% render_comment_form for object %} 38 |
39 |
40 | {% else %} 41 |
comments are disabled for this article
42 | {% endif %} 43 | 44 | {% if comment_count %} 45 |
46 |
    47 | {% render_treecomment_tree for object %} 48 |
49 | {% endif %} 50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /example/custom/templates/articles/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n comments %} 3 | 4 | {% block header %}{% trans "Article List" %}{% endblock %} 5 | 6 | {% block content %} 7 | {% if object_list %} 8 |
    9 | {% for article in object_list %} 10 | {% get_comment_count for article as comment_count %} 11 |
  • 12 | {{ article.title }} 13 | {{ article.publish|date:"d-F-Y" }} 14 | {% if comment_count %} 15 | - {{ comment_count }} comment{{ comment_count|pluralize }} 16 | {% endif %} 17 |
  • 18 | {% endfor %} 19 |
20 | {% endif %} 21 |

22 | {% trans "Home" %} 23 |

24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /example/custom/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}django-comments-tree - simple demo{% endblock %} 7 | 8 | 18 | 19 | 20 |
21 | 25 |
26 | 27 |
28 | {% block content %} 29 | {% endblock %} 30 |
31 | 32 |
33 |
34 |
35 |
36 |

django-comments-tree custom demo.

37 |
38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/custom/templates/comments/form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load comments_tree %} 3 | 4 |
5 | {% csrf_token %} 6 |
7 | 8 | 9 | 11 | 12 | {% for field in form %} 13 | {% if field.is_hidden %}
{{ field }}
{% endif %} 14 | {% endfor %} 15 | 16 |
{{ form.honeypot }}
17 | 18 |
19 | 22 |
23 | {{ form.title }} 24 |
25 |
26 | 27 |
28 |
29 | {{ form.comment }} 30 |
31 |
32 | 33 | {% if not request.user.is_authenticated %} 34 |
35 | 38 |
39 | {{ form.name }} 40 |
41 |
42 | 43 |
44 | 47 |
48 | {{ form.email }} 49 | {{ form.email.help_text }} 50 |
51 |
52 | 53 |
54 | 57 |
58 | {{ form.url }} 59 |
60 |
61 | {% endif %} 62 | 63 |
64 |
65 |
66 | 69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | -------------------------------------------------------------------------------- /example/custom/templates/comments/preview.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block content %} 6 |

{% trans "Preview your comment:" %}

7 |
8 |
9 |
10 | {% if not comment %} 11 | {% trans "Empty comment." %} 12 | {% else %} 13 | 18 |
19 |
20 | {% now "N j, Y, P" %} -  21 | {% if form.cleaned_data.url %} 22 | {% endif %} 23 | {{ form.cleaned_data.name }} 24 | {% if form.cleaned_data.url %}{% endif %} 25 |
26 | {{ form.cleaned_data.title }}
27 |
{{ comment }}
28 |
29 | {% endif %} 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {% include "comments/form.html" %} 38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /example/custom/templates/django_comments_xtd/email_confirmation_request.html: -------------------------------------------------------------------------------- 1 |

{{ comment.user_name }},

2 | 3 |

You or someone in behalf of you have requested to post a comment into this page:
4 | http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} 5 |

6 | 7 | Comment title: {{ comment.title }}
8 | 9 |

The comment:
10 | {{ comment.comment }} 11 |


12 | 13 |

If you do not wish to post the comment, please ignore this message or report an incident to {{ contact }}. Otherwise click on the link below to confirm the comment.

14 | 15 |

http://{{ site.domain }}{{ confirmation_url|slice:":40" }}...

16 | 17 |

If clicking does not work, you can also copy and paste the address into your browser's address window.

18 | 19 |

Thanks for your comment!
20 | --
21 | Kind regards,
22 | {{ site }}

23 | -------------------------------------------------------------------------------- /example/custom/templates/django_comments_xtd/email_confirmation_request.txt: -------------------------------------------------------------------------------- 1 | {{ comment.user_name }}, 2 | 3 | You or someone in behalf of you have requested to post a comment to the following URL. 4 | 5 | URL: http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} 6 | 7 | --- Comment title: --- 8 | {{ comment.title }} 9 | 10 | --- Comment: --- 11 | {{ comment.comment }} 12 | ---------------- 13 | 14 | If you do not wish to post the comment, please ignore this message or report an incident to {{ contact|safe }}. Otherwise click on the link below to confirm the comment. 15 | 16 | http://{{ site.domain }}{{ confirmation_url }} 17 | 18 | If clicking does not work, you can also copy and paste the address into your browser's address window. 19 | Thanks for your comment! 20 | 21 | -- 22 | Kind regards, 23 | {{ site }} 24 | -------------------------------------------------------------------------------- /example/custom/templates/django_comments_xtd/reply.html: -------------------------------------------------------------------------------- 1 | {% extends "django_comments_tree/base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block title %}{% trans "Comment reply" %}{% endblock %} 6 | 7 | {% block header %} 8 | {{ comment.content_object }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
{% trans "Reply to comment" %}
13 |
14 |
15 |
16 |
17 |
18 | {% if comment.user_url %} 19 | 20 | {{ comment.user_email|tree_comment_gravatar }} 21 | 22 | {% else %} 23 | {{ comment.user_email|tree_comment_gravatar }} 24 | {% endif %} 25 |
26 |
27 |
28 | {{ comment.submit_date|date:"N j, Y, P" }} -  29 | {% if comment.user_url %} 30 | {% endif %} 31 | {{ comment.user_name }}{% if comment.user_url %}{% endif %} 32 |
33 | {{ comment.title }}
34 |
{{ comment.comment }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% include "comments/form.html" %} 44 |
45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /example/custom/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load md2 %} 3 | 4 | {% block header %}Homepage{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

{{ readme_text|markdown }}

9 |
10 |
11 |

12 | Article list 13 |

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /example/custom/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.views.generic import TemplateView 6 | 7 | if django.VERSION[:2] < (2, 0): 8 | from django.conf.urls import include, url as re_path 9 | else: 10 | from django.urls import include, re_path 11 | 12 | from django_comments_tree import LatestCommentFeed 13 | 14 | from custom import views 15 | 16 | 17 | admin.autodiscover() 18 | 19 | 20 | urlpatterns = [ 21 | re_path(r'^$', views.HomepageView.as_view(), name='homepage'), 22 | re_path(r'^admin/', admin.site.urls), 23 | re_path(r'^articles/', include('custom.articles.urls')), 24 | re_path(r'^comments/', include('django_comments_tree.urls')), 25 | re_path(r'^feeds/comments/$', LatestCommentFeed(), name='comments-feed'), 26 | ] 27 | 28 | 29 | if settings.DEBUG: 30 | urlpatterns += staticfiles_urlpatterns() 31 | -------------------------------------------------------------------------------- /example/custom/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class HomepageView(TemplateView): 5 | template_name = "homepage.html" 6 | 7 | def get(self, request, *args, **kwargs): 8 | context = self.get_context_data(**kwargs) 9 | text = open("README.md").read() 10 | context['readme_text'] = text.split("\n", 1)[1][1:] 11 | return self.render_to_response(context) 12 | -------------------------------------------------------------------------------- /example/fixtures/articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "articles.article", 4 | "pk": 1, 5 | "fields": { 6 | "title": "Net Neutrality in Jeopardy", 7 | "slug": "net-neutrality-jeopardy", 8 | "body": "The battle over the future of the Internet is a power grab that pits well-heeled lobbyists, corrupt legislators, phony front groups and the world\u2019s most powerful telecommunications companies against the rest of us \u2014 the millions of Americans who use the Internet every day, in increasingly inventive ways.\r\n\r\nPolicymakers are public officials, and it\u2019s their job to serve the public interest. You can play an important role by helping spread the word about Net Neutrality, meeting face to face with elected officials and urging them to protect the open Internet.\r\n\r\nYou can start by urging the Senate to stand up for Net Neutrality. ", 9 | "allow_comments": true, 10 | "publish": "2012-01-10T14:53:23Z" 11 | } 12 | }, 13 | { 14 | "model": "articles.article", 15 | "pk": 2, 16 | "fields": { 17 | "title": "Colonies in space may be only hope, says Hawking", 18 | "slug": "colonies-space-only-hope", 19 | "body": "The human race is likely to be wiped out by a doomsday virus before the Millennium is out, unless we set up colonies in space, Prof Stephen Hawking warns today.\r\n\r\nIn an interview with The Telegraph, Prof Hawking, the world's best known cosmologist, says that biology, rather than physics, presents the biggest challenge to human survival.\r\n\r\n\"Although September 11 was horrible, it didn't threaten the survival of the human race, like nuclear weapons do,\" said the Cambridge University scientist.\r\n\r\n\"In the long term, I am more worried about biology. Nuclear weapons need large facilities, but genetic engineering can be done in a small lab. You can't regulate every lab in the world. The danger is that either by accident or design, we create a virus that destroys us.\r\n\r\n\"I don't think the human race will survive the next thousand years, unless we spread into space. There are too many accidents that can befall life on a single planet. But I'm an optimist. We will reach out to the stars.\"", 20 | "allow_comments": true, 21 | "publish": "2001-11-16T10:20:59Z" 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /example/fixtures/auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$10000$pm3XyzIh0qKs$RfHX2F66DJ3pIy8FY1CAmsrjQ3lHNbsHN8Z8DOxbdEw=", 7 | "last_login": "2012-10-02T13:16:12.362Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "Administrator", 11 | "last_name": "", 12 | "email": "admin@example.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2012-01-12T09:49:55Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | }, 20 | { 21 | "model": "auth.user", 22 | "pk": 2, 23 | "fields": { 24 | "password": "pbkdf2_sha256$36000$kvAXQRetU9Zd$oHCNN5rdpjhsJwsxWgXFwWRaPHf6LZieT4Zy6UuoVvQ=", 25 | "last_login": "2017-05-17T08:15:35.580Z", 26 | "is_superuser": false, 27 | "username": "fulanito", 28 | "first_name": "Fulanito", 29 | "last_name": "De Tal", 30 | "email": "fulanito@example.com", 31 | "is_staff": true, 32 | "is_active": true, 33 | "date_joined": "2017-04-10T11:28:30Z", 34 | "groups": [], 35 | "user_permissions": [] 36 | } 37 | }, 38 | { 39 | "model": "auth.user", 40 | "pk": 3, 41 | "fields": { 42 | "password": "pbkdf2_sha256$36000$nfGvzlMY1u4s$f2K6vorNYOOosJQ1ZaqHAEJUI8XaH9deGFq/DFYYLd0=", 43 | "last_login": "2017-05-05T09:41:46.859Z", 44 | "is_superuser": false, 45 | "username": "mengano", 46 | "first_name": "Mengano", 47 | "last_name": "G\u00f3mez", 48 | "email": "mbox@danir.us", 49 | "is_staff": true, 50 | "is_active": true, 51 | "date_joined": "2017-05-03T10:14:24Z", 52 | "groups": [], 53 | "user_permissions": [] 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /example/fixtures/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "quotes.quote", 4 | "pk": 1, 5 | "fields": { 6 | "title": "Unusual farewell", 7 | "slug": "unusual-farewell", 8 | "quote": "I don\u2019t know half of you half as well as I should like; and I like less than half of you half as well as you deserve.", 9 | "author": "Bilbo Baggins", 10 | "url_source": "", 11 | "allow_comments": true, 12 | "publish": "2017-04-04T12:59:59Z" 13 | } 14 | }, 15 | { 16 | "model": "quotes.quote", 17 | "pk": 2, 18 | "fields": { 19 | "title": "On education", 20 | "slug": "education", 21 | "quote": "The child is capable of developing and giving us tangible proof of the possibility of a better humanity. He has shown us the true process of construction of the human being. We have seen children totally change as they acquire a love for things and as their sense of order, discipline, and self-control develops within them.... The child is both a hope and a promise for mankind.", 22 | "author": "Maria Montessori", 23 | "url_source": "", 24 | "allow_comments": true, 25 | "publish": "2017-04-04T13:00:00Z" 26 | } 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /example/fixtures/sites.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "sites.site", 5 | "fields": { 6 | "domain": "localhost:8000", 7 | "name": "localhost:8000" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /example/simple/README.md: -------------------------------------------------------------------------------- 1 | ## Simple example project ## 2 | 3 | The Simple Demo features: 4 | 5 | 1. An Articles App, with a model `Article` whose instances accept comments. 6 | 1. Confirmation by mail is required before the comment hit the database, unless `COMMENTS_TREE_CONFIRM_EMAIL` is set to False. Authenticated users don't have to confirm comments. 7 | 1. Follow up notifications via mail. 8 | 1. Mute links to allow cancellation of follow-up notifications. 9 | 1. It uses the template tag `render_markup_comment` to render comment content. So you can use line breaks, Markdown or reStructuredText to format comments. To use special formatting, start the comment with the line `#!` being `` any of the following: 10 | * markdown 11 | * restructuredtext 12 | * linebreaks 13 | 1. No nested comments. 14 | -------------------------------------------------------------------------------- /example/simple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/simple/__init__.py -------------------------------------------------------------------------------- /example/simple/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/simple/articles/__init__.py -------------------------------------------------------------------------------- /example/simple/articles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from simple.articles.models import Article 4 | 5 | class ArticleAdmin(admin.ModelAdmin): 6 | list_display = ('title', 'publish', 'allow_comments') 7 | list_filter = ('publish',) 8 | search_fields = ('title', 'body') 9 | prepopulated_fields = {'slug': ('title',)} 10 | fieldsets = ((None, 11 | {'fields': ('title', 'slug', 'body', 12 | 'allow_comments', 'publish',)}),) 13 | 14 | admin.site.register(Article, ArticleAdmin) 15 | -------------------------------------------------------------------------------- /example/simple/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-12 14:29 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200, verbose_name='title')), 22 | ('slug', models.SlugField(unique_for_date='publish', verbose_name='slug')), 23 | ('body', models.TextField(verbose_name='body')), 24 | ('allow_comments', models.BooleanField(default=True, verbose_name='allow comments')), 25 | ('publish', models.DateTimeField(default=datetime.datetime.now, verbose_name='publish')), 26 | ], 27 | options={ 28 | 'db_table': 'simple_articles', 29 | 'ordering': ('-publish',), 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /example/simple/articles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/simple/articles/migrations/__init__.py -------------------------------------------------------------------------------- /example/simple/articles/models.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from datetime import datetime 5 | 6 | import django 7 | from django.db import models 8 | from django.urls import reverse 9 | 10 | 11 | class PublicManager(models.Manager): 12 | """Returns published articles that are not in the future.""" 13 | 14 | if django.VERSION < (1, 6): 15 | get_queryset = models.Manager.get_query_set 16 | 17 | def published(self): 18 | return self.get_queryset().filter(publish__lte=datetime.now()) 19 | 20 | 21 | class Article(models.Model): 22 | """Article, that accepts comments.""" 23 | 24 | title = models.CharField('title', max_length=200) 25 | slug = models.SlugField('slug', unique_for_date='publish') 26 | body = models.TextField('body') 27 | allow_comments = models.BooleanField('allow comments', default=True) 28 | publish = models.DateTimeField('publish', default=datetime.now) 29 | 30 | objects = PublicManager() 31 | 32 | class Meta: 33 | db_table = 'simple_articles' 34 | ordering = ('-publish',) 35 | 36 | def __str__(self): 37 | return '%s' % self.title 38 | 39 | def get_absolute_url(self): 40 | return reverse( 41 | 'articles-article-detail', 42 | kwargs={'year': self.publish.year, 43 | 'month': int(self.publish.strftime('%m').lower()), 44 | 'day': self.publish.day, 45 | 'slug': self.slug}) 46 | -------------------------------------------------------------------------------- /example/simple/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import ListView, DateDetailView 3 | 4 | from simple.articles.models import Article 5 | from simple.articles.views import ArticleDetailView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', 10 | ListView.as_view(queryset=Article.objects.published()), 11 | name='articles-index'), 12 | 13 | url((r'^(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/' 14 | r'(?P[-\w]+)/$'), 15 | ArticleDetailView.as_view(), 16 | name='articles-article-detail'), 17 | ] 18 | -------------------------------------------------------------------------------- /example/simple/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.views.generic import DateDetailView 3 | 4 | from simple.articles.models import Article 5 | 6 | 7 | class ArticleDetailView(DateDetailView): 8 | model = Article 9 | date_field = "publish" 10 | month_format = "%m" 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | context.update({'next': reverse('comments-tree-sent')}) 15 | return context 16 | -------------------------------------------------------------------------------- /example/simple/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd `dirname $0` > /dev/null 4 | PRJPATH=`pwd` 5 | popd > /dev/null 6 | FIXTURESPATH=${PRJPATH}/../fixtures 7 | 8 | check_ret() { 9 | [[ $? -ne 0 ]] && echo "Failed." && exit 1 10 | } 11 | 12 | cd ${PRJPATH} 13 | 14 | #------------------------------ 15 | echo "Checking Django version... " 16 | # Retrieve 1.7 as 1.07, so that they can be compared as decimal numbers. 17 | version=`python -c 'import django; print("%d.%02d" % django.VERSION[:2])'` 18 | if [[ ${version} < "1.07" ]]; then 19 | python manage.py syncdb --noinput || check_ret 20 | python manage.py migrate django_comments_tree || check_ret 21 | else 22 | python manage.py migrate --noinput || check_ret 23 | fi 24 | 25 | #------------------------------ 26 | echo "Loading fixture files... " 27 | fixtures=(auth sites articles) 28 | for file in ${fixtures[@]}; do 29 | python manage.py loaddata ${FIXTURESPATH}/${file}.json || check_ret 30 | done 31 | -------------------------------------------------------------------------------- /example/simple/manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, '../..') # parent of django_comments_tree directory 5 | sys.path.insert(0, '..') # demos directory 6 | 7 | if __name__ == "__main__": 8 | from django.core.management import execute_from_command_line 9 | import imp 10 | try: 11 | imp.find_module('settings') # Assumed to be in the same directory. 12 | except ImportError: 13 | import sys 14 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 15 | sys.exit(1) 16 | 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "simple.settings") 18 | execute_from_command_line(sys.argv) 19 | -------------------------------------------------------------------------------- /example/simple/requirements.txt: -------------------------------------------------------------------------------- 1 | django-markdown2 2 | -------------------------------------------------------------------------------- /example/simple/templates/articles/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load comments_tree %} 4 | 5 | {% block header %}Article Detail{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{{ object.title }}

10 |
{{ object.publish }}
11 |
12 |
13 |
14 | {{ object.body|linebreaks }} 15 |
16 |

17 | back to the articles list 18 |

19 | 20 |
21 | {% get_comment_count for object as comment_count %} 22 | {% if comment_count %} 23 |
24 | {% blocktrans count comment_count=comment_count %} 25 | There is {{ comment_count }} comment below. 26 | {% plural %} 27 | There are {{ comment_count }} comments below. 28 | {% endblocktrans %} 29 |
30 |
31 | {% endif %} 32 | 33 | {% if object.allow_comments %} 34 |
35 |

Post your comment

36 |
37 | {% render_comment_form for object %} 38 |
39 |
40 | {% else %} 41 |
comments are disabled for this article
42 | {% endif %} 43 | 44 | {% if comment_count %} 45 |
46 |
47 | {% render_comment_list for object %} 48 |
49 | {% endif %} 50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /example/simple/templates/articles/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n comments %} 3 | 4 | {% block header %}{% trans "Article List" %}{% endblock %} 5 | 6 | {% block content %} 7 | {% if object_list %} 8 |
    9 | {% for article in object_list %} 10 | {% get_comment_count for article as comment_count %} 11 |
  • 12 | {{ article.title }} 13 | {{ article.publish|date:"d-F-Y" }} 14 | {% if comment_count %} 15 | - {{ comment_count }} comment{{ comment_count|pluralize }} 16 | {% endif %} 17 |
  • 18 | {% endfor %} 19 |
20 | {% endif %} 21 | 22 |

23 | {% trans "Home" %} 24 |

25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /example/simple/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}django-comments-tree simple demo{% endblock %} 7 | 8 | 18 | 19 | 20 |
21 | 25 |
26 | 27 |
28 | {% block content %} 29 | {% endblock %} 30 |
31 | 32 |
33 |
34 |
35 |
36 |

django-comments-tree simple demo.

37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /example/simple/templates/django_comments_xtd/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /example/simple/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load md2 %} 3 | 4 | {% block header %}Homepage{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

{{ readme_text|markdown }}

9 |
10 |
11 |

12 | Article list 13 |

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /example/simple/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.views.generic import TemplateView 6 | 7 | if django.VERSION[:2] < (2, 0): 8 | from django.conf.urls import include, url as re_path 9 | else: 10 | from django.urls import include, re_path 11 | 12 | from django_comments_tree import LatestCommentFeed 13 | 14 | from simple import views 15 | 16 | 17 | admin.autodiscover() 18 | 19 | 20 | urlpatterns = [ 21 | re_path(r'^$', views.HomepageView.as_view(), name='homepage'), 22 | re_path(r'^admin/', admin.site.urls), 23 | re_path(r'^articles/', include('simple.articles.urls')), 24 | re_path(r'^comments/', include('django_comments_tree.urls')), 25 | re_path(r'^feeds/comments/$', LatestCommentFeed(), name='comments-feed'), 26 | ] 27 | 28 | 29 | if settings.DEBUG: 30 | urlpatterns += staticfiles_urlpatterns() 31 | -------------------------------------------------------------------------------- /example/simple/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class HomepageView(TemplateView): 5 | template_name = "homepage.html" 6 | 7 | def get(self, request, *args, **kwargs): 8 | context = self.get_context_data(**kwargs) 9 | text = open("README.md").read() 10 | context['readme_text'] = text.split("\n", 1)[1][1:] 11 | return self.render_to_response(context) 12 | -------------------------------------------------------------------------------- /example/tutorial.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpertool/django-comments-tree/2f86f694d127b1722baf7d025eb5fd22b184b88f/example/tutorial.tar.gz -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | flake8 django_comments_tree 4 | if [[ $? -ne 0 ]] 5 | then 6 | echo "Flake 8 failed" 7 | exit 1 8 | fi 9 | 10 | tox 11 | if [[ $? -ne 0 ]] 12 | then 13 | echo "Tox tests failed. Please review" 14 | exit 2 15 | fi 16 | 17 | ./make_no_test.sh 18 | -------------------------------------------------------------------------------- /make_no_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=$(sed 's/__version__ = "\(.*\)"/\1/' django_comments_tree/version.py) 4 | git tag --force $version && git push && git push --tags --force 5 | 6 | python setup.py sdist 7 | twine upload dist/* && rm dist/* 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-comments-tree-plugins", 3 | "version": "2.4.0", 4 | "description": "Provides django-comments-tree reactjs plugin", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "watch": "webpack --watch", 9 | "build": "webpack --mode production" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/core": "^7.1.2", 16 | "@babel/preset-env": "^7.1.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-cli": "^6.26.0", 19 | "babel-loader": "^8.0.4", 20 | "react": "^16.5.2", 21 | "react-dom": "^16.5.2", 22 | "webpack": "^4.21.0", 23 | "webpack-cli": "^3.1.2" 24 | }, 25 | "dependencies": { 26 | "bootstrap": "^4.1.3", 27 | "jquery": "^3.3.1", 28 | "md5": "^2.2.1", 29 | "remarkable": "^1.7.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-comments-tree" 3 | version = "0.1.0" 4 | description = "Django Comments Framework extension app with django-treebeard tree support, follow up notifications and email confirmations." 5 | authors = ["Ed Henderson "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.4.0" 9 | django-contrib-comments = "^1.8.0" 10 | djangorestframework = "^3.6.0" 11 | six = "^1.12.0" 12 | Django = "^2.0.0" 13 | docutils = "^0.14.0" 14 | django-markdown2 = "^0.3.1" 15 | 16 | [tool.poetry.dev-dependencies] 17 | coverage = "^4.5.0" 18 | mock = "^2.0.0" 19 | 20 | [build-system] 21 | requires = ["poetry>=0.12"] 22 | build-backend = "poetry.masonry.api" 23 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.settings 3 | django_find_project = false 4 | python_paths = django_comments_tree 5 | python_files = test_*.py 6 | addopts = --rootdir=./django_comments_tree -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | Django 2 | django-treebeard 3 | django-contrib-comments 4 | djangorestframework 5 | draftjs-exporter 6 | django-markupfield 7 | markdown 8 | Markdown 9 | django-markup 10 | django-markdown2 11 | docutils 12 | six 13 | psycopg2-binary==2.8.4 14 | -------------------------------------------------------------------------------- /requirements_tests.pip: -------------------------------------------------------------------------------- 1 | -r requirements.pip 2 | coverage 3 | mock 4 | tox 5 | pytest-django 6 | pytest-pythonpath 7 | factory_boy 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup, find_packages 4 | from setuptools.command.test import test 5 | 6 | version = {} 7 | with open("django_comments_tree/version.py") as fp: 8 | exec(fp.read(), version) 9 | 10 | 11 | def run_tests(*args): 12 | from django_comments_tree.tests import run_tests 13 | errors = run_tests() 14 | if errors: 15 | sys.exit(1) 16 | else: 17 | sys.exit(0) 18 | 19 | 20 | test.run_tests = run_tests 21 | 22 | 23 | setup( 24 | name="django-comments-tree", 25 | version=version['__version__'], 26 | packages=find_packages(exclude=['tests']), 27 | scripts=[], 28 | include_package_data=True, 29 | license="MIT", 30 | description=("Django Comments Framework extension app with django-treebeard " 31 | "support, follow up notifications and email " 32 | "confirmations, as well as real-time comments using Firebase " 33 | "for notifications."), 34 | long_description=("A reusable Django app that uses django-treebeard " 35 | "to create a threaded" 36 | "comments Framework, following up " 37 | "notifications and comments that only hits the " 38 | "database after users confirm them by email." 39 | "Real-time comment updates are also available using " 40 | "Django channels as a notification mechanism of comment updates. " 41 | "Clients can connect to channels for updates, and then query " 42 | "the backend for the actual changes, so that all data is " 43 | "located in the backend database." 44 | ), 45 | author="Ed Henderson", 46 | author_email="ed@sharpertool.com", 47 | maintainer="Ed Henderson", 48 | maintainer_email="ed@sharpertool.com", 49 | keywords="django comments treebeard threaded django-channels websockets", 50 | url="https://github.com/sharpertool/django-comments-tree", 51 | project_urls={ 52 | 'Documentation': 'https://django-comments-tree.readthedocs.io/en/latest/', 53 | 'Github': 'https://github.com/sharpertool/django-comments-tree', 54 | 'Original Package': 'https://github.com/danirus/django-comments-xtd', 55 | }, 56 | python_requires='>=3.7', 57 | install_requires=[ 58 | 'Django>=2.2', 59 | 'django-treebeard>=4.1.0', 60 | 'djangorestframework>=3.6', 61 | 'draftjs_exporter>=2.1.6', 62 | 'django-markupfield>=1.5.1', 63 | 'markdown>=3.1.1', 64 | 'docutils', 65 | 'six', 66 | ], 67 | extras_requires=[ 68 | ], 69 | classifiers=[ 70 | 'Development Status :: 3 - Alpha', 71 | 'Environment :: Web Environment', 72 | 'Intended Audience :: Developers', 73 | 'License :: OSI Approved :: MIT License', 74 | 'Operating System :: OS Independent', 75 | 'Programming Language :: Python :: 3', 76 | 'Programming Language :: Python :: 3.7', 77 | 'Programming Language :: Python :: 3.8', 78 | 'Framework :: Django', 79 | 'Framework :: Django :: 2.2', 80 | 'Framework :: Django :: 3.0', 81 | 'Natural Language :: English', 82 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', 83 | ], 84 | test_suite="dummy", 85 | zip_safe=True 86 | ) 87 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | flake8 django_comments_tree 4 | if [[ $? -ne 0 ]] 5 | then 6 | echo "Flake 8 failed" 7 | exit 1 8 | fi 9 | 10 | tox 11 | if [[ $? -ne 0 ]] 12 | then 13 | echo "tox failed" 14 | exit 2 15 | fi 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | [pytest] 6 | DJANGO_SETTINGS_MODULES=tests.settings 7 | django_find_project = false 8 | python_paths = django_comments_tree 9 | python_files = test_*.py 10 | 11 | [tox] 12 | skipsdist = True 13 | envlist = py{37,38}-django{220,300} 14 | 15 | [travis] 16 | python = 17 | 3.7: py37 18 | 3.8: py38 19 | 20 | [travis:env] 21 | DJANGO = 22 | 2.2: django220 23 | 3.0: django330 24 | 25 | [testenv] 26 | changedir = {toxinidir}/django_comments_tree 27 | commands = pytest {posargs} # -rw --cov-config .coveragerc --cov django_comments_tree 28 | deps = 29 | #-rrequirements.pip 30 | pip 31 | six 32 | docutils 33 | Markdown 34 | django-markup 35 | markdown 36 | django-markdown2 37 | draftjs-exporter 38 | pytest 39 | pytest-cov 40 | pytest-django 41 | selenium 42 | factory_boy 43 | django-treebeard 44 | djangorestframework 45 | django-markupfield 46 | py37-django220: django>=2.2,<2.3 47 | py37-django330: django>=3.0,<3.1 48 | py38-django220: django>=2.2,<2.3 49 | py38-django330: django>=3.0,<3.1 50 | setenv = 51 | PYTHONPATH = {toxinidir}:{toxinidir} 52 | DJANGO_SETTINGS_MODULE=django_comments_tree.tests.settings 53 | 54 | [flake8] 55 | ignore = D203,C901,W503 56 | exclude = .git,.venv3,__pycache__,docs/source/conf.py,old,build,dist,.tox,docs,django_comments_tree/tests,django_comments_tree/migrations 57 | max-complexity = 10 58 | max-line-length = 100 59 | 60 | [testenv:pep8] 61 | show-source = True 62 | commands = {envbindir}/flake8 --ignore C901 --max-line-length=100 --exclude=.tox,docs,django_comments_tree/tests,django_comments_tree/migrations django_comments_tree 63 | # Flake8 only needed when linting. 64 | # Do not care about other dependencies, it's just for linting. 65 | deps = flake8 66 | changedir = {toxinidir} 67 | 68 | [testenv:js] 69 | commands = 70 | npm install --prefix {toxinidir} 71 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const STATIC_DIR = path.resolve(__dirname, 5 | 'django_comments_tree', 'static', 6 | 'django_comments_tree', 'js'); 7 | const SOURCE_DIR = path.resolve(STATIC_DIR, 'src'); 8 | 9 | module.exports = { 10 | mode: "none", 11 | devtool: 'source-map', 12 | entry: { 13 | plugin: path.resolve(SOURCE_DIR, 'index.js') 14 | }, 15 | output: { 16 | filename: '[name]-1.4.0.js', 17 | path: STATIC_DIR 18 | }, 19 | optimization: { 20 | splitChunks: { 21 | cacheGroups: { 22 | default: false, 23 | vendors: false, 24 | 25 | vendor: { 26 | chunks: 'all', 27 | test: /node_modules/ 28 | } 29 | } 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.jsx?/, 36 | include: SOURCE_DIR, 37 | use: { 38 | loader: 'babel-loader' 39 | } 40 | } 41 | ] 42 | }, 43 | externals: { 44 | jquery: 'jQuery', 45 | bootstrap: 'bootstrap', 46 | django: 'django' 47 | } 48 | }; 49 | --------------------------------------------------------------------------------