├── .coveragerc
├── .coveralls.yml
├── .gitignore
├── .travis.yml
├── AUTHORS
├── CHANGES.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs
├── Makefile
├── conf.py
├── index.rst
├── make.bat
├── quickstart.rst
├── reference
│ └── settings.rst
├── releases.rst
└── topics
│ ├── akismet.rst
│ ├── captcha.rst
│ ├── email.rst
│ ├── form_layout.rst
│ ├── gdpr.rst
│ ├── ipaddress.rst
│ ├── moderate.rst
│ ├── templates.rst
│ └── threaded_comments.rst
├── example
├── __init__.py
├── article
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── factories.py
│ │ ├── test_admin.py
│ │ ├── test_forms.py
│ │ ├── test_moderation.py
│ │ └── test_views.py
│ ├── urls.py
│ └── views.py
├── frontend
│ ├── __init__.py
│ ├── static
│ │ └── vendor
│ │ │ ├── bootstrap.css
│ │ │ ├── bootstrap.scss
│ │ │ └── jquery-2.2.0.min.js
│ └── templates
│ │ ├── article
│ │ ├── details.html
│ │ ├── list.html
│ │ └── list_full.html
│ │ ├── base.html
│ │ └── comments
│ │ └── base.html
├── manage.py
├── requirements.txt
├── settings.py
└── urls.py
├── fluent_comments
├── __init__.py
├── admin.py
├── akismet.py
├── apps.py
├── appsettings.py
├── email.py
├── forms
│ ├── __init__.py
│ ├── _captcha.py
│ ├── base.py
│ ├── captcha.py
│ ├── compact.py
│ ├── default.py
│ ├── helper.py
│ └── recaptcha.py
├── locale
│ ├── cs
│ │ └── LC_MESSAGES
│ │ │ └── django.po
│ ├── nl
│ │ └── LC_MESSAGES
│ │ │ └── django.po
│ ├── sk
│ │ └── LC_MESSAGES
│ │ │ └── django.po
│ └── zh
│ │ └── LC_MESSAGES
│ │ └── django.po
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── moderation.py
├── receivers.py
├── static
│ └── fluent_comments
│ │ ├── css
│ │ └── ajaxcomments.css
│ │ ├── img
│ │ └── ajax-wait.gif
│ │ └── js
│ │ └── ajaxcomments.js
├── templates
│ ├── comments
│ │ ├── comment.html
│ │ ├── comment_notification_email.html
│ │ ├── comment_notification_email.txt
│ │ ├── deleted.html
│ │ ├── flagged.html
│ │ ├── form.html
│ │ ├── list.html
│ │ ├── posted.html
│ │ └── preview.html
│ └── fluent_comments
│ │ └── templatetags
│ │ ├── ajax_comment_tags.html
│ │ ├── flat_list.html
│ │ └── threaded_list.html
├── templatetags
│ ├── __init__.py
│ └── fluent_comments_tags.py
├── tests
│ ├── __init__.py
│ ├── test_utils.py
│ └── utils.py
├── urls.py
├── utils.py
└── views.py
├── pyproject.toml
├── runtests.sh
├── setup.cfg
├── setup.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | include = fluent_comments*
3 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-pro
2 | repo_token: NYefOmqPlep2cS55RbgiFrLekn2y6JYpW
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pyo
3 | *.mo
4 | *.db
5 | *.egg-info/
6 | .project
7 | .idea/
8 | .pydevproject
9 | .idea/workspace.xml
10 | .DS_Store
11 | build/
12 | dist/
13 | docs/_build/
14 | .tox/
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | cache: pip
4 | python:
5 | - '3.8'
6 | env:
7 | - PACKAGES="Django~=2.2"
8 | - PACKAGES="Django~=2.2 django-threadedcomments>=1.2"
9 | - PACKAGES="Django~=3.0"
10 | - PACKAGES="Django~=3.0 django-threadedcomments>=1.2"
11 | - PACKAGES="Django~=3.1"
12 | - PACKAGES="Django~=3.1 django-threadedcomments>=1.2"
13 | - PACKAGES="Django~=3.2"
14 | - PACKAGES="Django~=3.2 django-threadedcomments>=1.2"
15 | - PACKAGES='https://github.com/django/django/archive/master.tar.gz'
16 | matrix:
17 | allow_failures:
18 | - env: PACKAGES='https://github.com/django/django/archive/master.tar.gz'
19 | before_install:
20 | - pip install codecov
21 | install:
22 | - pip install -U pip wheel
23 | - pip install $PACKAGES -e .
24 | script:
25 | - coverage run example/manage.py test
26 | after_success:
27 | - codecov
28 | notifications:
29 | irc:
30 | channels:
31 | - irc.freenode.org#django-fluent
32 | template:
33 | - '%{repository}#%{build_number} (%{commit}) %{message} -- %{build_url}'
34 | skip_join: true
35 | email:
36 | recipients:
37 | - travis@edoburu.nl
38 | on_success: never
39 | on_failure: always
40 | slack:
41 | secure: UQYHU07XN2kSAea1wFMMm9OsDJdavodhQsE8oT3SjxdXCbfBe/M1HXE8lbY2sZetWNiyQrZfodICDxhquQsNnUQ9IwMiSkWyofdT7rFA/2txPtf8bCnVMb0j0qJL+eKhFLBdtAEhi3PmuD2DqxcQxyz81enwtCk3vq8hzBEZu4I=
42 | on_success: never
43 | on_failure: always
44 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Original authors:
2 |
3 | - Diederik van der Boor (@vdboor)
4 |
5 | Contributions by:
6 |
7 | - Addy Yeow (@ayeowch)
8 | - Andy Kish (@Kobold)
9 | - Dmitry Kirillov (@kirill-the-terrible)
10 | - James Pic (@jpic)
11 | - Joshua Jonah (@joshuajonah)
12 | - Mickaël (@mickael9)
13 | - Petr Dlouhý (@PetrDlouhy)
14 | - Philippe Luickx (@philippeluickx)
15 | - Matej Badin (@matejbadin)
16 | - Roberto Cáceres (@rcaceres)
17 | - Ross Crawford-d'Heureuse (@rosscdh)
18 | - Tomas Babej (@tbabej)
19 | - @baffolobill
20 | - @bee-keeper
21 | - @rchrd2
22 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Version 3.0 (2021-05-11)
2 | ------------------------
3 |
4 | * Added Django 3 compatibility.
5 | * Added HTML email support (``FLUENT_COMMENTS_MULTIPART_EMAILS = True`` setting)
6 | * Fix duplicated comment forms in threaded response.
7 | * Drop Django 1.8, 1.9 and 1.10 compatibility.
8 | * Drop Python 2 support.
9 |
10 |
11 | Version 2.1 (2018-08-27)
12 | ------------------------
13 |
14 | * Make sure comment moderation is always active.
15 |
16 | * Added a "default moderator" for the models that are not registered via ``moderate_model()``.
17 | * The default moderator is configurable via ``FLUENT_COMMENTS_DEFAULT_MODERATOR``.
18 | * Spam filtering works, but "auto close/moderate after" support needs a registration via ``moderate_model()``.
19 |
20 | * Added simple captcha support.
21 | * Added "no captcha" reCAPTCHA2 support.
22 | * Add new default ``FLUENT_COMMENTS_AKISMET_ACTION=auto`` option that
23 | completely discards comments when Akismet classifies as definitive spam.
24 | * Fixed using ``force_text()`` to generate the content object title for email.
25 | * Fixed showing HTML in the comments admin.
26 | * Fixed showing the preview multiple times for threaded comments.
27 | * Included ``form.is_preview`` flag.
28 |
29 |
30 | Version 2.0.2 (2018-05-08)
31 | --------------------------
32 |
33 | * Fixed comment moderation when django-threadedcomments_ was used.
34 |
35 |
36 | Version 2.0.1 (2018-05-04)
37 | --------------------------
38 |
39 | * Fixed migration file.
40 | * Fixed missing Dutch translations.
41 | * Improved default form button labels.
42 |
43 |
44 | Version 2.0 (2018-01-22)
45 | ------------------------
46 |
47 | * Added Django 2.0 support.
48 | * Dropped Django 1.5 / 1.6 / 1.7 support.
49 | * Dropped Python 2.6 support.
50 | * Dropped django.contrib.comments_ support.
51 |
52 |
53 | Version 1.4.3 (2017-08-16)
54 | --------------------------
55 |
56 | * Fixed the IP-address reported in the email notification,
57 | the database records stored the actual correct value.
58 | * Fixed missing ``request`` variable in templates.
59 | * Fixed wrapping of the ``ThreadedComment`` model by the ``FluentComment`` proxy model too.
60 |
61 |
62 | Version 1.4.2 (2017-07-08)
63 | --------------------------
64 |
65 | * Fixed Django 1.11 appearance of compact labels; e-mail and URL field didn't receive a placeholder anymore.
66 | * Fixed HTML position of the hidden ``parent`` field.
67 | * Enforce python-akismet_ >= 0.3 for Python 3 compatibility.
68 |
69 |
70 | Version 1.4.1 (2017-02-06)
71 | --------------------------
72 |
73 | * Fixed compatibility with django_comments_ 1.8.
74 |
75 |
76 | Version 1.4 (2017-02-03)
77 | ------------------------
78 |
79 | * Added ``fluent_comments.forms.CompactLabelsCommentForm`` style for ``FLUENT_COMMENTS_FORM_CLASS``.
80 | * Added ``FLUENT_COMMENTS_MODERATE_BAD_WORDS`` setting, to auto moderate on profanity or spammy words.
81 | * Added ``FLUENT_COMMENTS_AKISMET_ACTION = "soft_delete"`` to auto-remove spammy comments. This is now the new default too.
82 | * Exposed all form styles through ``fluent_comments.forms`` now.
83 | * Fixed ``is_superuser`` check in moderation.
84 | * Fixed ``blog_language`` parameter for Akismet.
85 |
86 |
87 | Version 1.3 (2017-01-02)
88 | ------------------------
89 |
90 | * Added Akismet support for Python 3, via python-akismet_.
91 | * Added field reordering support, via the ``FLUENT_COMMENTS_FIELD_ORDER`` setting.
92 | * Added form class swapping, through the ``FLUENT_COMMENTS_FORM_CLASS`` setting.
93 | * Added new compact-form style, enable using::
94 |
95 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.CompactCommentForm'
96 | FLUENT_COMMENTS_COMPACT_FIELDS = ('name', 'email', 'url')
97 |
98 | * Added template blocks to override ``comments/form.html`` via ``comments/app_name/app_label/form.html``.
99 | * Added support for ``app_name/app_label`` template overrides to our ``comments/comment.html`` template.
100 |
101 |
102 | Version 1.2.2 (2016-08-29)
103 | --------------------------
104 |
105 | * Allow non-integer primary key
106 | * Added Slovak translation
107 |
108 |
109 | Version 1.2.1 (2016-05-23)
110 | --------------------------
111 |
112 | * Fixed error handling in JavaScript when server reports an error.
113 |
114 |
115 | Version 1.2 (2015-02-03)
116 | ------------------------
117 |
118 | * Fixed Django 1.9 support.
119 |
120 |
121 | Version 1.1 (2015-12-28)
122 | ------------------------
123 |
124 | * Fix Django 1.9 issue with imports.
125 | * Fix error in the admin for non-existing objects.
126 | * Fix Python 3 installation error (dropped Akismet_ requirement).
127 | * Drop Django 1.4 compatibility (in the templates).
128 |
129 |
130 | Version 1.0.5 (2015-10-17)
131 | --------------------------
132 |
133 | * Fix Django 1.9 issue with importing models in ``__init__.py``.
134 | * Fix django-threadedcomments_ 1.0.1 support
135 |
136 |
137 | Version 1.0.4 (2015-10-01)
138 | --------------------------
139 |
140 | * Fixed ``get_comments_model()`` import.
141 |
142 |
143 | Version 1.0.3 (2015-09-01)
144 | --------------------------
145 |
146 | * Fix support for ``TEMPLATE_STRING_IF_INVALID``, avoid parsing the "for" argument in ``{% ajax_comment_tags for object %}``.
147 | * Look for the correct ``#id_parent`` node (in case there are multiple)
148 | * Improve Bootstrap 3 appearance (template can be overwritten).
149 |
150 | Version 1.0.2
151 | -------------
152 |
153 | * Fixed packaging bug
154 |
155 | Version 1.0.1
156 | -------------
157 |
158 | * Fix app registry errors in Django 1.7
159 | * Fix security hash formatting errors on bad requests.
160 |
161 | Version 1.0.0
162 | -------------
163 |
164 | * Added Django 1.8 support, can use either the django_comments_ or the django.contrib.comments_ package now.
165 | * Fixed Python 3 issue in the admin
166 | * Fixed unicode support in for subject of notification email
167 |
168 | Released as 1.0b1
169 | ~~~~~~~~~~~~~~~~~
170 |
171 | * Fixed ajax-comment-busy check
172 | * Fixed clearing the whole container on adding comment
173 |
174 | Released as 1.0a2
175 | ~~~~~~~~~~~~~~~~~
176 |
177 | * Fix installation at Python 2.6
178 |
179 | Released as 1.0a1
180 | ~~~~~~~~~~~~~~~~~
181 |
182 | * Added support for Python 3 (with the exception of Akismet_ support).
183 | * Added support for multiple comment area's in the same page.
184 |
185 | **NOTE:** any custom templates need to be updated, to
186 | use the new ``id``, ``class`` and ``data-object-id`` attributes.
187 |
188 |
189 | Version 0.9.2
190 | -------------
191 |
192 | * Fix errors in Ajax view, due to a ``json`` variable name conflict
193 | * Fix support for old jQuery and new jQuery (.on vs .live)
194 | * Fix running the example project with Django 1.5
195 | * Fix error messages in ``post_comment_ajax`` view.
196 | * Fix empty user name column in the admin list.
197 | * Fix undesired "reply" link in the preview while using django-threadedcomments_.
198 | * Fix HTML layout of newly added threaded comments.
199 | * Fix Python 3 support
200 |
201 |
202 | Version 0.9.1
203 | -------------
204 |
205 | * Fix running at Django 1.6 alpha 1
206 |
207 |
208 | Version 0.9
209 | -----------
210 |
211 | * Full support for django-threadedcomments_ out of the box.
212 | * Fix CSS class for primary submit button, is now ``.btn-primary``.
213 |
214 |
215 | Version 0.8.0
216 | -------------
217 |
218 | First public release
219 |
220 | * Ajax-based preview and posting of comments
221 | * Configurable form layouts using django-crispy-forms_ and settings to exclude fields.
222 | * Comment moderation, using Akismet_ integration and auto-closing after N days.
223 | * E-mail notification to the site managers of new comments.
224 | * Rudimentary support for django-threadedcomments_
225 |
226 | .. _Akismet: https://pypi.python.org/pypi/akismet
227 | .. _python-akismet: https://pypi.python.org/pypi/python-akismet
228 | .. _django_comments: https://github.com/django/django-contrib-comments
229 | .. _django.contrib.comments: https://docs.djangoproject.com/en/1.7/ref/contrib/comments/
230 | .. _django-crispy-forms: http://django-crispy-forms.readthedocs.org
231 | .. _django-threadedcomments: https://github.com/HonzaKral/django-threadedcomments.git
232 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS
2 | include README.rst
3 | include LICENSE
4 | recursive-include fluent_comments/locale *.po *.mo
5 | recursive-include fluent_comments/templates *.html *.txt
6 | recursive-include fluent_comments/static *.css *.gif *.js
7 | global-exclude .DS_Store
8 | global-exclude Thumbs.db
9 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | django-fluent-comments
2 | ======================
3 |
4 | .. image:: https://travis-ci.org/django-fluent/django-fluent-comments.svg?branch=master
5 | :target: http://travis-ci.org/django-fluent/django-fluent-comments
6 | .. image:: https://img.shields.io/pypi/v/django-fluent-comments.svg
7 | :target: https://pypi.python.org/pypi/django-fluent-comments/
8 | .. image:: https://img.shields.io/pypi/l/django-fluent-comments.svg
9 | :target: https://pypi.python.org/pypi/django-fluent-comments/
10 | .. image:: https://img.shields.io/codecov/c/github/django-fluent/django-fluent-comments/master.svg
11 | :target: https://codecov.io/github/django-fluent/django-fluent-comments?branch=master
12 |
13 | The *django-fluent-comments* module enhances the default appearance of
14 | the django_comments_ application to be directly usable in web sites.
15 |
16 | Features:
17 |
18 | * Ajax-based preview and posting of comments
19 | * Configurable and flexible form layouts.
20 | * Comment moderation, with auto-closing / auto-moderation after N days.
21 | * E-mail notification to the site managers of new comments.
22 | * Optional threaded comments support via django-threadedcomments_.
23 | * Optional Akismet_ integration for spam detection.
24 | * Optional reCAPTCHA2 support via django-recaptcha_ or django-nocaptcha-recaptcha_.
25 | * Optional simple captcha support via django-simple-captcha_.
26 |
27 | The application is designed to be plug&play;
28 | installing it should already give a better comment layout.
29 |
30 | Installation
31 | ============
32 |
33 | First install the module and django_comments, preferably in a virtual environment::
34 |
35 | pip install django-fluent-comments
36 |
37 | Configuration
38 | -------------
39 |
40 | Please follow the documentation at https://django-fluent-comments.readthedocs.io/
41 |
42 |
43 | Contributing
44 | ============
45 |
46 | This module is designed to be generic, and easy to plug into your site.
47 | In case there is anything you didn't like about it, or think it's not
48 | flexible enough, please let us know. We'd love to improve it!
49 |
50 | If you have any other valuable contribution, suggestion or idea,
51 | please let us know as well because we will look into it.
52 | Pull requests are welcome too. :-)
53 |
54 |
55 | .. _django_comments: https://github.com/django/django-contrib-comments
56 | .. _django-crispy-forms: http://django-crispy-forms.readthedocs.org/
57 | .. _django-nocaptcha-recaptcha: https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha
58 | .. _django-recaptcha: https://github.com/praekelt/django-recaptcha
59 | .. _django-simple-captcha: https://github.com/mbi/django-simple-captcha
60 | .. _django-threadedcomments: https://github.com/HonzaKral/django-threadedcomments.git
61 | .. _Akismet: http://akismet.com
62 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = django-fluent-comments
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | # import os
16 | # import sys
17 | # sys.path.insert(0, os.path.abspath('.'))
18 |
19 |
20 | # -- Project information -----------------------------------------------------
21 |
22 | project = "django-fluent-comments"
23 | copyright = "2021, Diederik van der Boor"
24 | author = "Diederik van der Boor"
25 |
26 | # The short X.Y version
27 | version = "3.0"
28 | # The full version, including alpha/beta/rc tags
29 | release = "3.0"
30 |
31 |
32 | # -- General configuration ---------------------------------------------------
33 |
34 | # If your documentation needs a minimal Sphinx version, state it here.
35 | #
36 | # needs_sphinx = '1.0'
37 |
38 | # Add any Sphinx extension module names here, as strings. They can be
39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
40 | # ones.
41 | extensions = [
42 | "sphinx.ext.autodoc",
43 | "sphinx.ext.intersphinx",
44 | ]
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ["_templates"]
48 |
49 | # The suffix(es) of source filenames.
50 | # You can specify multiple suffix as a list of string:
51 | #
52 | # source_suffix = ['.rst', '.md']
53 | source_suffix = ".rst"
54 |
55 | # The master toctree document.
56 | master_doc = "index"
57 |
58 | # The language for content autogenerated by Sphinx. Refer to documentation
59 | # for a list of supported languages.
60 | #
61 | # This is also used if you do content translation via gettext catalogs.
62 | # Usually you set "language" from the command line for these cases.
63 | language = None
64 |
65 | # List of patterns, relative to source directory, that match files and
66 | # directories to ignore when looking for source files.
67 | # This pattern also affects html_static_path and html_extra_path .
68 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
69 |
70 | # The name of the Pygments (syntax highlighting) style to use.
71 | pygments_style = "sphinx"
72 |
73 |
74 | # -- Options for HTML output -------------------------------------------------
75 |
76 | # The theme to use for HTML and HTML Help pages. See the documentation for
77 | # a list of builtin themes.
78 | #
79 | # html_theme = 'alabaster'
80 |
81 | # Theme options are theme-specific and customize the look and feel of a theme
82 | # further. For a list of options available for each theme, see the
83 | # documentation.
84 | #
85 | # html_theme_options = {}
86 |
87 | # Add any paths that contain custom static files (such as style sheets) here,
88 | # relative to this directory. They are copied after the builtin static files,
89 | # so a file named "default.css" will overwrite the builtin "default.css".
90 | html_static_path = ["_static"]
91 |
92 | # Custom sidebar templates, must be a dictionary that maps document names
93 | # to template names.
94 | #
95 | # The default sidebars (for documents that don't match any pattern) are
96 | # defined by theme itself. Builtin themes are using these templates by
97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
98 | # 'searchbox.html']``.
99 | #
100 | # html_sidebars = {}
101 |
102 |
103 | # -- Options for HTMLHelp output ---------------------------------------------
104 |
105 | # Output file base name for HTML help builder.
106 | htmlhelp_basename = "django-fluent-commentsdoc"
107 |
108 |
109 | # -- Options for LaTeX output ------------------------------------------------
110 |
111 | latex_elements = {
112 | # The paper size ('letterpaper' or 'a4paper').
113 | #
114 | # 'papersize': 'letterpaper',
115 | # The font size ('10pt', '11pt' or '12pt').
116 | #
117 | # 'pointsize': '10pt',
118 | # Additional stuff for the LaTeX preamble.
119 | #
120 | # 'preamble': '',
121 | # Latex figure (float) alignment
122 | #
123 | # 'figure_align': 'htbp',
124 | }
125 |
126 | # Grouping the document tree into LaTeX files. List of tuples
127 | # (source start file, target name, title,
128 | # author, documentclass [howto, manual, or own class]).
129 | latex_documents = [
130 | (
131 | master_doc,
132 | "django-fluent-comments.tex",
133 | "django-fluent-comments Documentation",
134 | "Diederik van der Boor",
135 | "manual",
136 | ),
137 | ]
138 |
139 |
140 | # -- Options for manual page output ------------------------------------------
141 |
142 | # One entry per manual page. List of tuples
143 | # (source start file, name, description, authors, manual section).
144 | man_pages = [
145 | (master_doc, "django-fluent-comments", "django-fluent-comments Documentation", [author], 1)
146 | ]
147 |
148 |
149 | # -- Options for Texinfo output ----------------------------------------------
150 |
151 | # Grouping the document tree into Texinfo files. List of tuples
152 | # (source start file, target name, title, author,
153 | # dir menu entry, description, category)
154 | texinfo_documents = [
155 | (
156 | master_doc,
157 | "django-fluent-comments",
158 | "django-fluent-comments Documentation",
159 | author,
160 | "django-fluent-comments",
161 | "One line description of project.",
162 | "Miscellaneous",
163 | ),
164 | ]
165 |
166 |
167 | # -- Extension configuration -------------------------------------------------
168 |
169 | # -- Options for intersphinx extension ---------------------------------------
170 |
171 | # Example configuration for intersphinx: refer to the Python standard library.
172 | intersphinx_mapping = {"https://docs.python.org/": None}
173 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. django-fluent-comments documentation master file, created by
2 | sphinx-quickstart on Mon Jun 4 23:21:38 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to django-fluent-comments's documentation!
7 | ==================================================
8 |
9 | The *django-fluent-comments* module enhances the default appearance of
10 | the django_comments_ application to be directly usable in web sites.
11 |
12 | Features:
13 |
14 | * Ajax-based preview and posting of comments
15 | * Configurable and flexible form layouts.
16 | * Comment moderation, with auto-closing / auto-moderation after N days.
17 | * E-mail notification to the site managers of new comments.
18 | * Optional threaded comments support via django-threadedcomments_.
19 | * Optional Akismet_ integration for spam detection.
20 | * Optional reCAPTCHA2 support via django-recaptcha_ or django-nocaptcha-recaptcha_.
21 | * Optional simple captcha support via django-simple-captcha_.
22 |
23 | The application is designed to be plug&play;
24 | installing it should already give a better comment layout.
25 |
26 | .. toctree::
27 | :maxdepth: 1
28 | :caption: Contents:
29 |
30 | quickstart
31 | topics/form_layout
32 | topics/templates
33 | topics/captcha
34 | topics/akismet
35 | topics/email
36 | topics/moderate
37 | topics/threaded_comments
38 | topics/ipaddress
39 | topics/gdpr
40 | reference/settings
41 | releases
42 |
43 |
44 | Indices and tables
45 | ==================
46 |
47 | * :ref:`genindex`
48 | * :ref:`modindex`
49 | * :ref:`search`
50 |
51 | .. _django_comments: https://github.com/django/django-contrib-comments
52 | .. _django-crispy-forms: http://django-crispy-forms.readthedocs.org/
53 | .. _django-nocaptcha-recaptcha: https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha
54 | .. _django-recaptcha: https://github.com/praekelt/django-recaptcha
55 | .. _django-simple-captcha: https://github.com/mbi/django-simple-captcha
56 | .. _django-threadedcomments: https://github.com/HonzaKral/django-threadedcomments.git
57 | .. _Akismet: http://akismet.com
58 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=django-fluent-comments
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/quickstart.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | First install the module and django_comments, preferably in a virtual environment::
5 |
6 | pip install django-fluent-comments
7 |
8 | Configuration
9 | -------------
10 |
11 | To use comments, the following settings are required:
12 |
13 | .. code-block:: python
14 |
15 | INSTALLED_APPS += (
16 | 'fluent_comments', # must be before django_comments
17 | 'crispy_forms',
18 | 'django_comments',
19 | 'django.contrib.sites',
20 | )
21 |
22 | CRISPY_TEMPLATE_PACK = 'bootstrap3'
23 |
24 | COMMENTS_APP = 'fluent_comments'
25 |
26 | Add the following in ``urls.py``:
27 |
28 | .. code-block:: python
29 |
30 | urlpatterns += patterns('',
31 | url(r'^blog/comments/', include('fluent_comments.urls')),
32 | )
33 |
34 | The database can be created afterwards:
35 |
36 | .. code-block:: bash
37 |
38 | ./manage.py migrate
39 |
40 | Usage in the page
41 | -----------------
42 |
43 | Provide a template that displays the comments for the ``object`` and includes the required static files:
44 |
45 | .. code-block:: html+django
46 |
47 | {% load comments static %}
48 |
49 |
50 |
51 |
52 | {% render_comment_list for object %}
53 | {% render_comment_form for object %}
54 |
55 | .. note::
56 |
57 | When using the comment module via django-fluent-contents_ or django-fluent-blogs_,
58 | this step can be omitted.
59 |
60 | Template for non-ajax pages
61 | ---------------------------
62 |
63 | The templates which django_comments_ renders use a single base template for all layouts.
64 | This template is empty by default since it's only serves as a placeholder.
65 | To complete the configuration of the comments module, create a ``comments/base.html`` file
66 | that maps the template blocks onto your website base template. For example:
67 |
68 | .. code-block:: html+django
69 |
70 | {% extends "mysite/base.html" %}{% load i18n %}
71 |
72 | {% block meta-title %}{% block title %}{% trans "Responses for page" %}{% endblock %}{% endblock %}
73 |
74 | {% block main %}
75 |
76 | {% block content %}{% endblock %}
77 |
78 | {% endblock %}
79 |
80 | In this example, the base template has a ``meta-title`` and ``main`` block,
81 | which contain the ``content`` and ``title`` blocks that django_comments_ needs to see.
82 | This application also outputs an ``extrahead`` block for a meta-refresh tag.
83 | The ``extrahead`` block can be included in the site base template directly,
84 | so it doesn't have to be included in the ``comments/base.html`` file.
85 |
86 | .. _django_comments: https://github.com/django/django-contrib-comments
87 | .. _django-fluent-blogs: https://github.com/django-fluent/django-fluent-blogs
88 | .. _django-fluent-contents: https://github.com/django-fluent/django-fluent-contents
89 |
--------------------------------------------------------------------------------
/docs/reference/settings.rst:
--------------------------------------------------------------------------------
1 | Configuration reference
2 | =======================
3 |
4 | The default settings are:
5 |
6 | .. code-block:: python
7 |
8 | AKISMET_API_KEY = None
9 | AKISMET_BLOG_URL = None
10 | AKISMET_IS_TEST = False
11 |
12 | CRISPY_TEMPLATE_PACK = 'bootstrap'
13 |
14 | FLUENT_COMMENTS_REPLACE_ADMIN = True
15 |
16 | # Akismet spam fighting
17 | FLUENT_CONTENTS_USE_AKISMET = bool(AKISMET_API_KEY)
18 | FLUENT_COMMENTS_AKISMET_ACTION = 'soft_delete'
19 |
20 | # Moderation
21 | FLUENT_COMMENTS_DEFAULT_MODERATOR = 'default'
22 | FLUENT_COMMENTS_CLOSE_AFTER_DAYS = None
23 | FLUENT_COMMENTS_MODERATE_BAD_WORDS = ()
24 | FLUENT_COMMENTS_MODERATE_AFTER_DAYS = None
25 | FLUENT_COMMENTS_USE_EMAIL_NOTIFICATION = True
26 | FLUENT_COMMENTS_MULTIPART_EMAILS = False
27 |
28 | # Form layouts
29 | FLUENT_COMMENTS_FIELD_ORDER = ()
30 | FLUENT_COMMENTS_EXCLUDE_FIELDS = ()
31 | FLUENT_COMMENTS_FORM_CLASS = None
32 | FLUENT_COMMENTS_FORM_CSS_CLASS = 'comments-form form-horizontal'
33 | FLUENT_COMMENTS_LABEL_CSS_CLASS = 'col-sm-2'
34 | FLUENT_COMMENTS_FIELD_CSS_CLASS = 'col-sm-10'
35 |
36 | # Compact style settings
37 | FLUENT_COMMENTS_COMPACT_FIELDS = ('name', 'email', 'url')
38 | FLUENT_COMMENTS_COMPACT_GRID_SIZE = 12
39 | FLUENT_COMMENTS_COMPACT_COLUMN_CSS_CLASS = "col-sm-{size}"
40 |
41 |
42 | .. _FLUENT_COMMENTS_FORM_CLASS:
43 |
44 | FLUENT_COMMENTS_FORM_CLASS
45 | --------------------------
46 |
47 | Defines a dotted Python path to the form class to use.
48 | The built-in options include:
49 |
50 | Standard forms:
51 |
52 | * ``fluent_comments.forms.DefaultCommentForm`` The standard form.
53 | * ``fluent_comments.forms.CompactLabelsCommentForm`` A form where labels are hidden.
54 | * ``fluent_comments.forms.CompactCommentForm`` A compact row
55 |
56 | Variations with reCAPTCHA v2:
57 |
58 | * ``fluent_comments.forms.recaptcha.DefaultCommentForm``
59 | * ``fluent_comments.forms.recaptcha.CompactLabelsCommentForm``
60 | * ``fluent_comments.forms.recaptcha.CompactCommentForm``
61 |
62 | Variations wiwth a simple self-hosted captcha:
63 |
64 | * ``fluent_comments.forms.captcha.DefaultCommentForm``
65 | * ``fluent_comments.forms.captcha.CompactLabelsCommentForm``
66 | * ``fluent_comments.forms.captcha.CompactCommentForm``
67 |
68 |
69 | .. _FLUENT_COMMENTS_AKISMET_ACTION:
70 |
71 | FLUENT_COMMENTS_AKISMET_ACTION
72 | ------------------------------
73 |
74 | What to do when spam is detected, see :ref:`akismet_usage`.
75 |
76 |
77 | .. _FLUENT_COMMENTS_FIELD_ORDER:
78 |
79 | FLUENT_COMMENTS_FIELD_ORDER
80 | ---------------------------
81 |
82 | Defines the field ordering, see :ref:`field-order`.
83 |
--------------------------------------------------------------------------------
/docs/releases.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | .. include:: ../CHANGES.rst
5 |
--------------------------------------------------------------------------------
/docs/topics/akismet.rst:
--------------------------------------------------------------------------------
1 | .. _akismet_usage:
2 |
3 | Akismet spam detection
4 | ======================
5 |
6 | Akismet_ is used out of the box when the ``AKISMET_API_KEY`` setting is defined:
7 |
8 | .. code-block:: python
9 |
10 | AKISMET_API_KEY = "your-api-key"
11 |
12 | This can also be enabled explicitly:
13 |
14 | .. code-block:: python
15 |
16 | FLUENT_CONTENTS_USE_AKISMET = True # Enabled by default when AKISMET_API_KEY is set.
17 |
18 | The following settings are optional:
19 |
20 | .. code-block:: python
21 |
22 | AKISMET_BLOG_URL = "http://example.com" # Optional, to override auto detection
23 | AKISMET_IS_TEST = False # Enable to make test runs
24 |
25 | When spam is detected, the default behavior depends on the spam score.
26 | Obvious spam is discarded with an HTTP 400 response, while possible spam is marked for moderation.
27 |
28 | The :ref:`FLUENT_COMMENTS_AKISMET_ACTION` setting can be one of these values:
29 |
30 | * ``auto`` chooses between ``moderate``, ``soft_delete`` and ``delete`` based on the spam score.
31 | * ``moderate`` will always mark the comment for moderation.
32 | * ``soft_delete`` will mark the comment as moderated + removed, but it can still be seen in the admin.
33 | * ``delete`` will outright reject posting the comment and respond with a HTTP 400 Bad Request.
34 |
35 | .. tip::
36 |
37 | By default, Akismet will not report any post from the Django superuser as spam.
38 | Comments with the name "viagra-test-123" will always be flagged as spam.
39 |
40 | .. warning::
41 |
42 | Akismet is a third party service by Automattic.
43 | Note that :doc:`GDPR Compliance ` is next-to-impossible with this service.
44 |
45 | .. _Akismet: http://akismet.com
46 |
--------------------------------------------------------------------------------
/docs/topics/captcha.rst:
--------------------------------------------------------------------------------
1 | Captcha support
2 | ===============
3 |
4 | Users can be required to enter a captcha.
5 |
6 | This is done by changing the :ref:`FLUENT_COMMENTS_FORM_CLASS` setting.
7 |
8 | .. note::
9 |
10 | When :ref:`FLUENT_COMMENTS_FIELD_ORDER` is configured, also include the ``"captcha"`` field!
11 |
12 | Using django-recaptcha
13 | ----------------------
14 |
15 | django-recaptcha_ provides "no captcha" reCAPTCHA v2 support.
16 | Choose one of the form layout classes:
17 |
18 | .. code-block:: python
19 |
20 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.DefaultCommentForm' # default
21 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.CompactLabelsCommentForm' # no labels
22 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.CompactCommentForm' # compact row
23 |
24 | And configure it's settings:
25 |
26 | .. code-block:: python
27 |
28 | RECAPTCHA_PUBLIC_KEY = "the Google provided site_key"
29 | RECAPTCHA_PRIVATE_KEY = "the Google provided secret_key"
30 |
31 | NOCAPTCHA = True # Important! Required to get "no captcha" reCAPTCHA v2
32 |
33 | INSTALLED_APPS += (
34 | 'captcha',
35 | )
36 |
37 | Using django-nocaptcha-recaptcha
38 | ---------------------------------
39 |
40 | django-nocaptcha-recaptcha_ also provides "no captcha" reCAPTCHA v2 support.
41 | The same form classes are used, as the correct imports are detected at startup:
42 |
43 | .. code-block:: python
44 |
45 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.DefaultCommentForm' # default
46 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.CompactLabelsCommentForm' # no labels
47 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.recaptcha.CompactCommentForm' # compact row
48 |
49 | It's settings differ slightly:
50 |
51 | .. code-block:: python
52 |
53 | NORECAPTCHA_SITE_KEY = "the Google provided site_key"
54 | NORECAPTCHA_SECRET_KEY = "the Google provided secret_key"
55 |
56 | INSTALLED_APPS += (
57 | 'nocaptcha_recaptcha',
58 | )
59 |
60 | Using django-simple-captcha
61 | ---------------------------
62 |
63 | django-simple-captcha_ provides a simple local captcha test.
64 | It does not require external services, but it can be easier to break.
65 |
66 | .. code-block:: python
67 |
68 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.captcha.DefaultCommentForm' # default
69 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.captcha.CompactLabelsCommentForm' # no labels
70 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.captcha.CompactCommentForm' # compact row
71 |
72 | And configure the app:
73 |
74 | .. code-block:: python
75 |
76 | CAPTCHA_NOISE_FUNCTIONS = ()
77 | CAPTCHA_FONT_SIZE = 30
78 | CAPTCHA_LETTER_ROTATION = (-10,10)
79 |
80 | INSTALLED_APPS += (
81 | 'captcha',
82 | )
83 |
84 | .. warning::
85 |
86 | Note that both django-simple-captcha_ and django-recaptcha_ use the same "captcha" module name.
87 | These packages can't be installed together.
88 |
89 |
90 | .. _django-nocaptcha-recaptcha: https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha
91 | .. _django-recaptcha: https://github.com/praekelt/django-recaptcha
92 | .. _django-simple-captcha: https://github.com/mbi/django-simple-captcha
93 |
--------------------------------------------------------------------------------
/docs/topics/email.rst:
--------------------------------------------------------------------------------
1 | E-mail notification
2 | ===================
3 |
4 | By default, the ``MANAGERS`` of a Django site will receive an e-mail notification of new comments.
5 | This feature can be enabled or disabled using:
6 |
7 | .. code-block:: python
8 |
9 | FLUENT_COMMENTS_USE_EMAIL_NOTIFICATION = False
10 |
11 | By default, plain-text e-mail messages are generated using the template ``comments/comment_notification_email.txt``.
12 |
13 | Multi-part (HTML) e-mails are supported using the template ``comments/comment_notification_email.html``. To enabled
14 | multi-part e-mails, set:
15 |
16 | .. code-block:: python
17 |
18 | FLUENT_COMMENTS_MULTIPART_EMAILS = True
19 |
20 | In addition to the standard django-comments_ package, the ``request`` and ``site`` fields
21 | are available in the template context data. This allows generating absolute URLs to the site.
22 |
23 | .. _django-comments: https://github.com/django/django-contrib-comments
24 |
--------------------------------------------------------------------------------
/docs/topics/form_layout.rst:
--------------------------------------------------------------------------------
1 | Changing the form layout
2 | ========================
3 |
4 | Form layouts generally differ across web sites, hence this application doesn't dictate a specific form layout.
5 | Instead, this application uses django-crispy-forms_ which allows configuration of the form appearance.
6 |
7 | The defaults are set to Bootstrap 3 layouts, but can be changed.
8 |
9 | For example, use:
10 |
11 | .. code-block:: python
12 |
13 | CRISPY_TEMPLATE_PACK = 'bootstrap4'
14 |
15 |
16 | Using a different form class
17 | ----------------------------
18 |
19 | By choosing a different form class, the form layout can be redefined at once:
20 |
21 | The default is:
22 |
23 | .. code-block:: python
24 |
25 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.DefaultCommentForm'
26 |
27 | FLUENT_COMMENTS_FORM_CSS_CLASS = 'comments-form form-horizontal'
28 | FLUENT_COMMENTS_LABEL_CSS_CLASS = 'col-sm-2'
29 | FLUENT_COMMENTS_FIELD_CSS_CLASS = 'col-sm-10'
30 |
31 | You can replace the labels with placeholders using:
32 |
33 | .. code-block:: python
34 |
35 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.CompactLabelsCommentForm'
36 |
37 | Or place some fields at a single row:
38 |
39 | .. code-block:: python
40 |
41 | FLUENT_COMMENTS_FORM_CLASS = 'fluent_comments.forms.CompactCommentForm'
42 |
43 | # Optional settings for the compact style:
44 | FLUENT_COMMENTS_COMPACT_FIELDS = ('name', 'email', 'url')
45 | FLUENT_COMMENTS_COMPACT_GRID_SIZE = 12
46 | FLUENT_COMMENTS_COMPACT_COLUMN_CSS_CLASS = "col-sm-{size}"
47 |
48 | .. _field-order:
49 |
50 | Changing the field order
51 | ------------------------
52 |
53 | The default is:
54 |
55 | .. code-block:: python
56 |
57 | FLUENT_COMMENTS_FIELD_ORDER = ('name', 'email', 'url', 'comment')
58 |
59 | For a more modern look, consider placing the comment first:
60 |
61 | .. code-block:: python
62 |
63 | FLUENT_COMMENTS_FIELD_ORDER = ('comment', 'name', 'email', 'url')
64 |
65 |
66 | Hiding form fields
67 | ------------------
68 |
69 | Form fields can be hidden using the following settings:
70 |
71 | .. code-block:: python
72 |
73 | FLUENT_COMMENTS_EXCLUDE_FIELDS = ('name', 'email', 'url')
74 |
75 | When :doc:`django-threadedcomments ` are used, the ``title`` field can also be removed.
76 |
77 | .. note::
78 |
79 | Omitting fields from ``FLUENT_COMMENTS_FIELD_ORDER`` has the same effect.
80 |
81 |
82 | Using a custom form class
83 | -------------------------
84 |
85 | When the settings above don't provide the layout you need,
86 | you can define a custom form class entirely:
87 |
88 | .. code-block:: python
89 |
90 | from fluent_comments.forms import CompactLabelsCommentForm
91 |
92 | # Or for recaptcha as base, import:
93 | from fluent_comments.forms.recaptcha import CompactCommentForm
94 |
95 |
96 | class CommentForm(CompactLabelsCommentForm):
97 | """
98 | The comment form to use
99 | """
100 |
101 | def __init__(self, *args, **kwargs):
102 | super().__init__(*args, **kwargs)
103 | self.fields['url'].label = _("Website") # Changed the label
104 | self.fields['email'].label = _("Email address (will not be published)")
105 |
106 | And use that class in the ``FLUENT_COMMENTS_FORM_CLASS`` setting.
107 | The ``helper`` attribute defines how the layout is constructed by django-crispy-forms_,
108 | and should be redefined the change the field ordering or appearance.
109 |
110 |
111 | Switching form templates
112 | ------------------------
113 |
114 | By default, the forms can be rendered with 2 well known CSS frameworks:
115 |
116 | * `Bootstrap`_ The default template pack. The popular simple and flexible HTML, CSS, and Javascript for user interfaces from Twitter.
117 | * `Uni-form`_ Nice looking, well structured, highly customizable, accessible and usable forms.
118 |
119 | The ``CRISPY_TEMPLATE_PACK`` setting can be used to switch between both layouts.
120 | For more information, see the django-crispy-forms_ documentation.
121 |
122 | Both CSS frameworks have a wide range of themes available, which should give a good head-start to have a good form layout.
123 | In fact, we would encourage to adopt django-crispy-forms_ for all your applications to have a consistent layout across all your Django forms.
124 |
125 | If your form CSS framework is not supported, you can create a template pack
126 | for it and submit a pull request to the django-crispy-forms_ authors for inclusion.
127 |
128 |
129 |
130 | .. _`Bootstrap`: http://twitter.github.com/bootstrap/index.html
131 | .. _`Uni-form`: http://sprawsm.com/uni-form
132 |
133 |
134 | .. _django-crispy-forms: http://django-crispy-forms.readthedocs.org/
135 |
--------------------------------------------------------------------------------
/docs/topics/gdpr.rst:
--------------------------------------------------------------------------------
1 | Privacy concerns (GDPR)
2 | =======================
3 |
4 | Comment support needs to consider the General Data Protection Regulation (GDPR)
5 | when when you serve European customers. Any personal data (email address, IP-address)
6 | should only be stored as long as this is truely needed, and it must be clear whom it's shared with.
7 |
8 | .. tip::
9 |
10 | For a simple introduction, see https://premium.wpmudev.org/blog/gdpr-compliance/
11 |
12 | The Django comments model also stores the email address and IP-address of the commenter,
13 | which counts as personal information a user should give consent for. Consider running
14 | a background task that removes the IP-address or email address after a certain period.
15 |
16 | Concerns for third-party services
17 | ---------------------------------
18 |
19 | When using :doc:`Akismet `, the comment data and IP-address is passed to the servers of Akismet_.
20 |
21 | In case you update templates to display user avatars using Gravatar_, this this also
22 | provides privacy-sensitive information to a third party. Gravatar acts like a tracking-pixel,
23 | noticing every place you visit. It also makes your user's email address public.
24 | While the URL field is encoded as MD5, Gravatar doesn't use salted hashes so the
25 | data can be easily reverse engineered back to real user accounts.
26 |
27 | .. seealso::
28 |
29 | For more information, read:
30 |
31 | * https://meta.stackexchange.com/questions/21117/is-using-gravatar-a-security-risk
32 | * https://webapps.stackexchange.com/questions/9973/is-it-safe-to-use-gravatar/30605#30605
33 | * http://onemansblog.com/2007/02/02/protect-your-privacy-delete-internet-usage-tracks/comment-page-1/#comment-46204
34 | * https://www.wordfence.com/blog/2016/12/gravatar-advisory-protect-email-address-identity/
35 |
36 | .. _Akismet: https://akismet.com
37 | .. _Gravatar: https://gravatar.com
38 |
--------------------------------------------------------------------------------
/docs/topics/ipaddress.rst:
--------------------------------------------------------------------------------
1 | IP-Address detection
2 | ====================
3 |
4 | This package stores the remote IP of the visitor in the model,
5 | and passes it to :doc:`Akismet ` for spam detection.
6 | The IP Address is read from the ``REMOTE_ADDR`` meta field.
7 | In case your site is behind a HTTP proxy (e.g. using Gunicorn or a load balancer),
8 | this would make all comments appear to be posted from the load balancer IP.
9 |
10 | The best and most secure way to fix this, is using WsgiUnproxy_ middleware in your ``wsgi.py``:
11 |
12 | .. code-block:: python
13 |
14 | from django.core.wsgi import get_wsgi_application
15 | from django.conf import settings
16 | from wsgiunproxy import unproxy
17 |
18 | application = get_wsgi_application()
19 | application = unproxy(trusted_proxies=settings.TRUSTED_X_FORWARDED_FOR_IPS)(application)
20 |
21 | In your ``settings.py``, you can define which hosts may pass the ``X-Forwarded-For``
22 | header in the HTTP request. For example:
23 |
24 | .. code-block:: python
25 |
26 | TRUSTED_X_FORWARDED_FOR_IPS = (
27 | '11.22.33.44',
28 | '192.168.0.1',
29 | )
30 |
31 | .. warning::
32 |
33 | Please don't try to read ``HTTP_X_FORWARDED_FOR`` blindly with a fallback to ``HTTP_REMOTE_ADDR``.
34 | These headers could be provided by hackers, effectively circumventing your IP-address checks.
35 | Use WsgiUnproxy_ instead, which protects against maliciously injected headers.
36 |
37 | Amazon Web Services Support
38 | ---------------------------
39 |
40 | For AWS hosting, there is also `wsgi-aws-unproxy`_
41 | which does the same for all CloudFront IP addresses.
42 |
43 | IP-Subnet filtering
44 | -------------------
45 |
46 | Use the ``netaddr`` package to trust a full IP-block, e.g. for Kubernetes Ingress:
47 |
48 | .. code-block:: python
49 |
50 | from django.core.wsgi import get_wsgi_application
51 | from netaddr import IPNetwork
52 | from wsgiunproxy import unproxy
53 |
54 | application = get_wsgi_application()
55 | application = unproxy(trusted_proxies=IPNetwork('10.0.0.0/8'))(application)
56 |
57 | .. _Akismet: http://akismet.com
58 | .. _WsgiUnproxy: https://pypi.python.org/pypi/WsgiUnproxy
59 | .. _wsgi-aws-unproxy: https://github.com/LabD/wsgi-aws-unproxy
60 |
--------------------------------------------------------------------------------
/docs/topics/moderate.rst:
--------------------------------------------------------------------------------
1 | Auto comment moderation
2 | =======================
3 |
4 | By default, any comment receives moderation from the "default moderator"
5 | This ensures random comments also receive :doc:`akismet ` checks,
6 | bad word filtering and send :doc:`email notifications `.
7 |
8 | Some moderation features require more knowledge of the model.
9 | This includes:
10 |
11 | * Toggling an "enable comments" checkbox on the model.
12 | * Auto-closing comments after X days since the publication of the article.
13 | * Auto-moderating comments after X days since the publication of the article.
14 |
15 | Comment moderation can be enabled for the specific models using:
16 |
17 | .. code-block:: python
18 |
19 | from fluent_comments.moderation import moderate_model
20 | from myblog.models import BlogPost
21 |
22 | moderate_model(BlogPost,
23 | publication_date_field='publication_date',
24 | enable_comments_field='enable_comments',
25 | )
26 |
27 | This code can be placed in a ``models.py`` file.
28 | The provided field names are optional. By providing the field names,
29 | the comments can be auto-moderated or auto-closed after a number of days since the publication date.
30 |
31 | The following settings are available for comment moderation:
32 |
33 | .. code-block:: python
34 |
35 | FLUENT_COMMENTS_CLOSE_AFTER_DAYS = None # Auto-close comments after N days
36 | FLUENT_COMMENTS_MODERATE_AFTER_DAYS = None # Auto-moderate comments after N days.
37 |
38 | The default moderator
39 | ---------------------
40 |
41 | The default moderator is configurable using:
42 |
43 | .. code-block:: python
44 |
45 | FLUENT_COMMENTS_DEFAULT_MODERATOR = 'default'
46 |
47 | Possible values are:
48 |
49 | * ``default`` installs the standard moderator that all packages use.
50 | * ``deny`` will reject all comments placed on models which don't have an explicit moderator registered with ``moderate_model()``.
51 | * ``None`` will accept all comments, as if there is no default moderator installed.
52 | * A dotted Python path will import this class.
53 |
54 | When using a custom moderator class, consider inheriting
55 | ``fluent_comments.moderation.FluentCommentsModerator``
56 | to preserve the email notification feature.
57 |
--------------------------------------------------------------------------------
/docs/topics/templates.rst:
--------------------------------------------------------------------------------
1 | Using custom comment templates
2 | ==============================
3 |
4 | Besides the standard templates of django-comments_, this package provides
5 | a ``comments/comment.html`` template to render a single comment.
6 |
7 | It's default looks like:
8 |
9 | .. code-block:: html+django
10 |
11 | {% load i18n %}
12 |
29 |
30 | .. note::
31 |
32 | The ``id="comment-preview"``, ``data-comment-id`` fields are required for proper JavaScript actions.
33 | The div id should be ``id="c{{ comment.id }}"``, because ``Comment.get_absolute_url()`` points to it.
34 |
35 | Adding a Bootstrap 4 layout, including Gravatar_ would look like:
36 |
37 | .. code-block:: html+django
38 |
39 | {% load i18n gravatar %}
40 |
41 |
23 | {% render_comment_list for article %}
24 | {% render_comment_form for article %}
25 |
26 |
27 | However, to have a nice invitation to post comments,
28 | you can use the following example below.
29 |
30 | {% endcomment %}
31 |
32 | {% get_comment_count for article as comments_count %}
33 |
34 |
35 | {% if comments_count %}
36 |
{% blocktrans with entry_title=article.title count comments_count=comments_count %}{{ comments_count }} comment to {{ entry_title }}{% plural %}{{ comments_count }} comments to {{ entry_title }}{% endblocktrans %}
37 | {% render_comment_list for object %}
38 |
39 | {% if not article|comments_are_open %}
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/example/frontend/templates/comments/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}{% load i18n %}
2 | {% comment %}
3 |
4 | This template only exists to support the non-Ajax pages. Those are:
5 | - links to the moderation options in the email.
6 | - Pages for visitors with JavaScript disabled.
7 |
8 | Those pages are rendered with "comments/base.html" as base template.
9 |
10 | This file simply maps those blocks (headtitle, extrahead and content)
11 | to the blocks that are found in the site theme base template.
12 | See for example:
13 |
14 | http://127.0.0.1:8000/comments/posted/
15 | http://127.0.0.1:8000/comments/flag/1/
16 | http://127.0.0.1:8000/comments/flagged/
17 | http://127.0.0.1:8000/comments/delete/1/
18 | http://127.0.0.1:8000/comments/deleted/
19 | http://127.0.0.1:8000/comments/approve/1/
20 | http://127.0.0.1:8000/comments/approved/
21 |
22 | {% endcomment %}
23 |
24 | {% block headtitle %}{% block title %}{% trans "Responses for page" %}{% endblock %} - Example CMS{% endblock %}
25 |
26 | {% block main %}
27 |
Comments
28 |
29 |
30 | {% block content %}{% endblock %}
31 |
32 | {% endblock %}
33 |
34 | {# NOTE that the base template also needs to have an 'extrahead' block, or map it here like 'main' and 'headtitle' are mapped. #}
35 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if len(sys.argv) > 1 and sys.argv[1] == "test":
6 | # Same effect as python -Wd when run via coverage:
7 | import warnings
8 |
9 | warnings.simplefilter("always", DeprecationWarning)
10 |
11 | if __name__ == "__main__":
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
13 |
14 | from django.core.management import execute_from_command_line
15 |
16 | # Allow starting the app without installing the module.
17 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
18 |
19 | execute_from_command_line(sys.argv)
20 |
--------------------------------------------------------------------------------
/example/requirements.txt:
--------------------------------------------------------------------------------
1 | Django>=3.2.2
2 |
3 | # Base setup requirements
4 | django-crispy-forms>=1.11.2
5 | django-tag-parser>=3.2
6 | django-contrib-comments>=2.1.0
7 |
8 | akismet>=1.1
9 |
--------------------------------------------------------------------------------
/example/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for example project.
2 | from os.path import join, dirname, realpath
3 | import django
4 |
5 | # Add parent path,
6 | # Allow starting the app without installing the module.
7 | import sys
8 |
9 | sys.path.insert(0, dirname(dirname(realpath(__file__))))
10 |
11 | DEBUG = True
12 |
13 | ADMINS = (
14 | # ('Your Name', 'your_email@example.com'),
15 | )
16 |
17 | MANAGERS = ADMINS
18 | # database engine
19 | DATABASES = {
20 | "default": {
21 | "ENGINE": "django.db.backends.sqlite3",
22 | "NAME": dirname(__file__) + "/demo.db",
23 | }
24 | }
25 |
26 | TIME_ZONE = "Europe/Amsterdam"
27 | LANGUAGE_CODE = "en-us"
28 | SITE_ID = 1
29 |
30 | USE_I18N = True
31 | USE_L10N = True
32 |
33 | MEDIA_ROOT = join(dirname(__file__), "media")
34 | MEDIA_URL = "/media/"
35 | STATIC_ROOT = join(dirname(__file__), "static")
36 | STATIC_URL = "/static/"
37 |
38 | STATICFILES_DIRS = ()
39 | STATICFILES_FINDERS = (
40 | "django.contrib.staticfiles.finders.FileSystemFinder",
41 | "django.contrib.staticfiles.finders.AppDirectoriesFinder",
42 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
43 | )
44 |
45 | # Make this unique, and don't share it with anybody.
46 | SECRET_KEY = "-#@bi6bue%#1j)6+4bi0g-*xro@%f@_#zwv=2-g_@n3n_kj5"
47 |
48 | TEMPLATES = [
49 | {
50 | "BACKEND": "django.template.backends.django.DjangoTemplates",
51 | "APP_DIRS": True,
52 | "OPTIONS": {
53 | "debug": DEBUG,
54 | "context_processors": [
55 | "django.contrib.auth.context_processors.auth",
56 | "django.template.context_processors.request",
57 | "django.template.context_processors.static",
58 | "django.contrib.messages.context_processors.messages",
59 | ],
60 | },
61 | },
62 | ]
63 |
64 |
65 | MIDDLEWARE = (
66 | "django.middleware.common.CommonMiddleware",
67 | "django.contrib.sessions.middleware.SessionMiddleware",
68 | "django.middleware.csrf.CsrfViewMiddleware",
69 | "django.contrib.auth.middleware.AuthenticationMiddleware",
70 | "django.contrib.messages.middleware.MessageMiddleware",
71 | )
72 |
73 | if django.VERSION < (1, 10):
74 | MIDDLEWARE_CLASSES = MIDDLEWARE
75 |
76 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
77 |
78 | ROOT_URLCONF = "urls"
79 |
80 | INSTALLED_APPS = (
81 | "django.contrib.auth",
82 | "django.contrib.contenttypes",
83 | "django.contrib.sessions",
84 | "django.contrib.sites",
85 | "django.contrib.messages",
86 | "django.contrib.staticfiles",
87 | "django.contrib.admin",
88 | # Site theme
89 | "frontend",
90 | # Example app
91 | "article",
92 | # Required modules
93 | "crispy_forms",
94 | "fluent_comments",
95 | "django_comments",
96 | )
97 |
98 | try:
99 | import threadedcomments
100 | except ImportError:
101 | pass
102 | else:
103 | print("Using django-threadedcomments, added to INSTALLED_APPS")
104 | INSTALLED_APPS += ("threadedcomments",)
105 |
106 | LOGGING = {
107 | "version": 1,
108 | "disable_existing_loggers": False,
109 | "handlers": {"mail_admins": {"level": "ERROR", "class": "django.utils.log.AdminEmailHandler"}},
110 | "loggers": {
111 | "django.request": {
112 | "handlers": ["mail_admins"],
113 | "level": "ERROR",
114 | "propagate": True,
115 | },
116 | },
117 | }
118 |
119 | CRISPY_TEMPLATE_PACK = "bootstrap3"
120 |
121 | # fluent-comments settings:
122 | COMMENTS_APP = "fluent_comments"
123 |
124 | FLUENT_COMMENTS_USE_EMAIL_MODERATION = True
125 | FLUENT_COMMENTS_MODERATE_AFTER_DAYS = 14
126 | FLUENT_COMMENTS_CLOSE_AFTER_DAYS = 60
127 | FLUENT_COMMENTS_AKISMET_ACTION = "moderate"
128 |
129 | AKISMET_API_KEY = None # Add your Akismet key here to enable Akismet support
130 | AKISMET_IS_TEST = True # for development/example apps.
131 |
--------------------------------------------------------------------------------
/example/urls.py:
--------------------------------------------------------------------------------
1 | import article.urls
2 | import fluent_comments.urls
3 |
4 | from django.contrib import admin
5 | from django.views.generic import RedirectView
6 | from django.urls import include, path
7 |
8 | admin.autodiscover()
9 |
10 | urlpatterns = [
11 | path("admin/", admin.site.urls),
12 | path("comments/", include(fluent_comments.urls)),
13 | path("articles/", include(article.urls)),
14 | path("", RedirectView.as_view(url="articles/", permanent=False)),
15 | ]
16 |
--------------------------------------------------------------------------------
/fluent_comments/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | API for :ref:`custom-comment-app-api`
3 | """
4 | default_app_config = "fluent_comments.apps.FluentCommentsApp"
5 |
6 | form_class = None
7 | model_class = None
8 |
9 | # following PEP 440
10 | __version__ = "3.0"
11 |
12 |
13 | def get_model():
14 | """
15 | Return the model to use for commenting.
16 | """
17 | global model_class
18 | if model_class is None:
19 | from fluent_comments.models import FluentComment
20 |
21 | # Our proxy model that performs select_related('user') for the comments
22 | model_class = FluentComment
23 |
24 | return model_class
25 |
26 |
27 | def get_form():
28 | """
29 | Return the form to use for commenting.
30 | """
31 | global form_class
32 | from fluent_comments import appsettings
33 |
34 | if form_class is None:
35 | if appsettings.FLUENT_COMMENTS_FORM_CLASS:
36 | from django.utils.module_loading import import_string
37 |
38 | form_class = import_string(appsettings.FLUENT_COMMENTS_FORM_CLASS)
39 | else:
40 | from fluent_comments.forms import FluentCommentForm
41 |
42 | form_class = FluentCommentForm
43 |
44 | return form_class
45 |
--------------------------------------------------------------------------------
/fluent_comments/admin.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib import admin
3 | from django.contrib.admin.widgets import AdminTextInputWidget
4 | from django.core.exceptions import ImproperlyConfigured
5 | from django.utils.encoding import force_str
6 | from django.utils.html import escape, format_html
7 | from django.utils.translation import gettext_lazy as _
8 | from fluent_comments import appsettings
9 | from django_comments import get_model as get_comments_model
10 |
11 |
12 | # Ensure the admin app is loaded,
13 | # so the model is unregistered here, and not loaded twice.
14 | if appsettings.USE_THREADEDCOMMENTS:
15 | # Avoid getting weird situations where both comment apps are loaded in the admin.
16 | if not hasattr(settings, "COMMENTS_APP") or settings.COMMENTS_APP == "comments":
17 | raise ImproperlyConfigured("To use 'threadedcomments', specify the COMMENTS_APP as well")
18 |
19 | from threadedcomments.admin import ThreadedCommentsAdmin as CommentsAdminBase
20 | else:
21 | from django_comments.admin import CommentsAdmin as CommentsAdminBase
22 |
23 |
24 | class FluentCommentsAdmin(CommentsAdminBase):
25 | """
26 | Updated admin screen for the comments model.
27 |
28 | The ability to add a comment is removed here, the admin screen can only be used for managing comments.
29 | Adding comments can happen at the frontend instead.
30 |
31 | The fieldsets are more logically organized, and the generic relation is a readonly field instead of a massive pulldown + textarea.
32 | The class supports both the standard ``django_comments`` and the ``threadedcomments`` applications.
33 | """
34 |
35 | fieldsets = [
36 | (
37 | _("Content"),
38 | {
39 | "fields": (
40 | "object_link",
41 | "user_name",
42 | "user_email",
43 | "user_url",
44 | "comment",
45 | "submit_date",
46 | )
47 | },
48 | ),
49 | (_("Account information"), {"fields": ("user", "ip_address")}),
50 | (_("Moderation"), {"fields": ("is_public", "is_removed")}),
51 | ]
52 |
53 | list_display = (
54 | "user_name_col",
55 | "object_link",
56 | "ip_address",
57 | "submit_date",
58 | "is_public",
59 | "is_removed",
60 | )
61 | readonly_fields = (
62 | "object_link",
63 | "user",
64 | "ip_address",
65 | "submit_date",
66 | )
67 |
68 | # Adjust the fieldsets for threaded comments
69 | if appsettings.USE_THREADEDCOMMENTS:
70 | fieldsets[0][1]["fields"] = (
71 | "object_link",
72 | "user_name",
73 | "user_email",
74 | "user_url",
75 | "title",
76 | "comment",
77 | "submit_date",
78 | ) # add title field.
79 | fieldsets.insert(2, (_("Hierarchy"), {"fields": ("parent",)}))
80 | raw_id_fields = ("parent",)
81 |
82 | def get_queryset(self, request):
83 | return super().get_queryset(request).select_related("user")
84 |
85 | def object_link(self, comment):
86 | try:
87 | object = comment.content_object
88 | except AttributeError:
89 | return ""
90 |
91 | if not object:
92 | return ""
93 |
94 | title = force_str(object)
95 | if hasattr(object, "get_absolute_url"):
96 | return format_html(u'{1}', object.get_absolute_url(), title)
97 | else:
98 | return title
99 |
100 | object_link.short_description = _("Page")
101 | object_link.allow_tags = True
102 |
103 | def user_name_col(self, comment):
104 | if comment.user_name:
105 | return comment.user_name
106 | elif comment.user_id:
107 | return force_str(comment.user)
108 | else:
109 | return None
110 |
111 | user_name_col.short_description = _("user's name")
112 |
113 | def has_add_permission(self, request):
114 | return False
115 |
116 | def formfield_for_dbfield(self, db_field, **kwargs):
117 | if db_field.name == "title":
118 | kwargs["widget"] = AdminTextInputWidget
119 | return super().formfield_for_dbfield(db_field, **kwargs)
120 |
121 |
122 | # Replace the old admin screen.
123 | if appsettings.FLUENT_COMMENTS_REPLACE_ADMIN:
124 | CommentModel = get_comments_model()
125 | try:
126 | admin.site.unregister(CommentModel)
127 | except admin.sites.NotRegistered as e:
128 | pass
129 |
130 | admin.site.register(CommentModel, FluentCommentsAdmin)
131 |
--------------------------------------------------------------------------------
/fluent_comments/akismet.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from akismet import Akismet, SpamStatus
4 | from django.contrib.sites.shortcuts import get_current_site
5 | from django.core.exceptions import ImproperlyConfigured
6 | from django.utils.encoding import smart_str
7 |
8 | import fluent_comments
9 | from fluent_comments import appsettings
10 |
11 |
12 | def akismet_check(comment, content_object, request):
13 | """
14 | Connects to Akismet and evaluates to True if Akismet marks this comment as spam.
15 |
16 | :rtype: akismet.SpamStatus
17 | """
18 | # Return previously cached response
19 | akismet_result = getattr(comment, "_akismet_result_", None)
20 | if akismet_result is not None:
21 | return akismet_result
22 |
23 | # Get Akismet data
24 | AKISMET_API_KEY = appsettings.AKISMET_API_KEY
25 | if not AKISMET_API_KEY:
26 | raise ImproperlyConfigured(
27 | "You must set AKISMET_API_KEY to use comment moderation with Akismet."
28 | )
29 |
30 | current_domain = get_current_site(request).domain
31 | auto_blog_url = "{0}://{1}/".format(request.is_secure() and "https" or "http", current_domain)
32 | blog_url = appsettings.AKISMET_BLOG_URL or auto_blog_url
33 |
34 | akismet = Akismet(
35 | AKISMET_API_KEY,
36 | blog=blog_url,
37 | is_test=int(bool(appsettings.AKISMET_IS_TEST)),
38 | application_user_agent="django-fluent-comments/{0}".format(fluent_comments.__version__),
39 | )
40 |
41 | akismet_data = _get_akismet_data(blog_url, comment, content_object, request)
42 | akismet_result = akismet.check(**akismet_data) # raises AkismetServerError when key is invalid
43 | setattr(comment, "_akismet_result_", akismet_result)
44 | return akismet_result
45 |
46 |
47 | def _get_akismet_data(blog_url, comment, content_object, request):
48 | # Field documentation:
49 | # http://akismet.com/development/api/#comment-check
50 | data = {
51 | # Comment info
52 | "permalink": urljoin(blog_url, content_object.get_absolute_url()),
53 | # see http://blog.akismet.com/2012/06/19/pro-tip-tell-us-your-comment_type/
54 | "comment_type": "comment", # comment, trackback, pingback
55 | "comment_author": getattr(comment, "name", ""),
56 | "comment_author_email": getattr(comment, "email", ""),
57 | "comment_author_url": getattr(comment, "url", ""),
58 | "comment_content": smart_str(comment.comment),
59 | "comment_date": comment.submit_date,
60 | # Request info
61 | "referrer": request.META.get("HTTP_REFERER", ""),
62 | "user_agent": request.META.get("HTTP_USER_AGENT", ""),
63 | "user_ip": comment.ip_address,
64 | }
65 |
66 | if comment.user_id and comment.user.is_superuser:
67 | data["user_role"] = "administrator" # always passes test
68 |
69 | # If the language is known, provide it.
70 | language = _get_article_language(content_object)
71 | if language:
72 | data["blog_lang"] = language
73 |
74 | return data
75 |
76 |
77 | def _get_article_language(article):
78 | try:
79 | # django-parler uses this attribute
80 | return article.get_current_language()
81 | except AttributeError:
82 | pass
83 |
84 | try:
85 | return article.language_code
86 | except AttributeError:
87 | pass
88 |
89 | return None
90 |
--------------------------------------------------------------------------------
/fluent_comments/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class FluentCommentsApp(AppConfig):
6 | name = "fluent_comments"
7 | verbose_name = _("Comments")
8 |
9 | def ready(self):
10 | # This installs the comment_will_be_posted signal
11 | import fluent_comments.receivers # noqa
12 |
--------------------------------------------------------------------------------
/fluent_comments/appsettings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.exceptions import ImproperlyConfigured
3 |
4 | AKISMET_API_KEY = getattr(settings, "AKISMET_API_KEY", None)
5 | AKISMET_BLOG_URL = getattr(
6 | settings, "AKISMET_BLOG_URL", None
7 | ) # Optional, to override auto detection
8 | AKISMET_IS_TEST = getattr(settings, "AKISMET_IS_TEST", False) # Enable in case of testing
9 |
10 | CRISPY_TEMPLATE_PACK = getattr(settings, "CRISPY_TEMPLATE_PACK", "bootstrap")
11 |
12 | USE_THREADEDCOMMENTS = "threadedcomments" in settings.INSTALLED_APPS
13 |
14 | FLUENT_COMMENTS_REPLACE_ADMIN = getattr(settings, "FLUENT_COMMENTS_REPLACE_ADMIN", True)
15 |
16 | FLUENT_CONTENTS_USE_AKISMET = getattr(
17 | settings, "FLUENT_CONTENTS_USE_AKISMET", bool(AKISMET_API_KEY)
18 | ) # enable when an API key is set.
19 | FLUENT_COMMENTS_DEFAULT_MODERATOR = getattr(
20 | settings, "FLUENT_COMMENTS_DEFAULT_MODERATOR", "default"
21 | )
22 | FLUENT_COMMENTS_USE_EMAIL_NOTIFICATION = getattr(
23 | settings, "FLUENT_COMMENTS_USE_EMAIL_NOTIFICATION", True
24 | ) # enable by default
25 | FLUENT_COMMENTS_MULTIPART_EMAILS = getattr(
26 | settings, "FLUENT_COMMENTS_MULTIPART_EMAILS", False
27 | ) # disable by default
28 | FLUENT_COMMENTS_CLOSE_AFTER_DAYS = getattr(settings, "FLUENT_COMMENTS_CLOSE_AFTER_DAYS", None)
29 | FLUENT_COMMENTS_MODERATE_BAD_WORDS = getattr(settings, "FLUENT_COMMENTS_MODERATE_BAD_WORDS", ())
30 | FLUENT_COMMENTS_MODERATE_AFTER_DAYS = getattr(
31 | settings, "FLUENT_COMMENTS_MODERATE_AFTER_DAYS", None
32 | )
33 | FLUENT_COMMENTS_AKISMET_ACTION = getattr(
34 | settings, "FLUENT_COMMENTS_AKISMET_ACTION", "soft_delete"
35 | ) # or 'moderate', 'delete, 'soft_delete'
36 |
37 | FLUENT_COMMENTS_FIELD_ORDER = tuple(getattr(settings, "FLUENT_COMMENTS_FIELD_ORDER", ()) or ())
38 | FLUENT_COMMENTS_EXCLUDE_FIELDS = tuple(
39 | getattr(settings, "FLUENT_COMMENTS_EXCLUDE_FIELDS", ()) or ()
40 | )
41 | FLUENT_COMMENTS_FORM_CLASS = getattr(settings, "FLUENT_COMMENTS_FORM_CLASS", None)
42 | FLUENT_COMMENTS_FORM_CSS_CLASS = getattr(
43 | settings, "FLUENT_COMMENTS_FORM_CSS_CLASS", "comments-form form-horizontal"
44 | )
45 | FLUENT_COMMENTS_LABEL_CSS_CLASS = getattr(settings, "FLUENT_COMMENTS_LABEL_CSS_CLASS", "col-sm-2")
46 | FLUENT_COMMENTS_FIELD_CSS_CLASS = getattr(settings, "FLUENT_COMMENTS_FIELD_CSS_CLASS", "col-sm-10")
47 |
48 | # Compact style settings
49 | FLUENT_COMMENTS_COMPACT_FIELDS = getattr(
50 | settings, "FLUENT_COMMENTS_COMPACT_FIELDS", ("name", "email", "url")
51 | )
52 | FLUENT_COMMENTS_COMPACT_GRID_SIZE = getattr(settings, "FLUENT_COMMENTS_COMPACT_GRID_SIZE", 12)
53 | FLUENT_COMMENTS_COMPACT_COLUMN_CSS_CLASS = getattr(
54 | settings, "FLUENT_COMMENTS_COMPACT_COLUMN_CSS_CLASS", "col-sm-{size}"
55 | )
56 |
57 |
58 | if FLUENT_COMMENTS_AKISMET_ACTION not in ("auto", "moderate", "soft_delete", "delete"):
59 | raise ImproperlyConfigured(
60 | "FLUENT_COMMENTS_AKISMET_ACTION can be 'auto', 'moderate', 'soft_delete' or 'delete'"
61 | )
62 |
63 | if FLUENT_COMMENTS_EXCLUDE_FIELDS or FLUENT_COMMENTS_FORM_CLASS or FLUENT_COMMENTS_FIELD_ORDER:
64 | # The exclude option only works when our form is used.
65 | # Allow derived packages to inherit our form class too.
66 | if not hasattr(settings, "COMMENTS_APP") or settings.COMMENTS_APP == "comments":
67 | raise ImproperlyConfigured(
68 | "To use django-fluent-comments, also specify: COMMENTS_APP = 'fluent_comments'"
69 | )
70 |
--------------------------------------------------------------------------------
/fluent_comments/email.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.sites.shortcuts import get_current_site
3 | from django.core.mail import send_mail
4 | from django.template.loader import render_to_string
5 | from django.utils.encoding import force_str
6 | from fluent_comments import appsettings
7 |
8 |
9 | def send_comment_posted(comment, request):
10 | """
11 | Send the email to staff that an comment was posted.
12 |
13 | While the django_comments module has email support,
14 | it doesn't pass the 'request' to the context.
15 | This also changes the subject to show the page title.
16 | """
17 | recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
18 | site = get_current_site(request)
19 | content_object = comment.content_object
20 | content_title = force_str(content_object)
21 |
22 | if comment.is_removed:
23 | subject = u'[{0}] Spam comment on "{1}"'.format(site.name, content_title)
24 | elif not comment.is_public:
25 | subject = u'[{0}] Moderated comment on "{1}"'.format(site.name, content_title)
26 | else:
27 | subject = u'[{0}] New comment posted on "{1}"'.format(site.name, content_title)
28 |
29 | context = {"site": site, "comment": comment, "content_object": content_object}
30 |
31 | message = render_to_string("comments/comment_notification_email.txt", context, request=request)
32 | if appsettings.FLUENT_COMMENTS_MULTIPART_EMAILS:
33 | html_message = render_to_string(
34 | "comments/comment_notification_email.html", context, request=request
35 | )
36 | else:
37 | html_message = None
38 |
39 | send_mail(
40 | subject,
41 | message,
42 | settings.DEFAULT_FROM_EMAIL,
43 | recipient_list,
44 | fail_silently=True,
45 | html_message=html_message,
46 | )
47 |
--------------------------------------------------------------------------------
/fluent_comments/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import AbstractCommentForm, CommentFormHelper
2 | from .default import DefaultCommentForm
3 | from .compact import CompactLabelsCommentForm, CompactCommentForm
4 | from .helper import CommentFormHelper, SubmitButton, PreviewButton, CompactLabelsCommentFormHelper
5 |
6 | FluentCommentForm = DefaultCommentForm # noqa, for backwards compatibility
7 |
8 | __all__ = (
9 | "AbstractCommentForm",
10 | "CommentFormHelper",
11 | "DefaultCommentForm",
12 | "CompactLabelsCommentFormHelper",
13 | "CompactLabelsCommentForm",
14 | "CompactCommentForm",
15 | "SubmitButton",
16 | "PreviewButton",
17 | )
18 |
--------------------------------------------------------------------------------
/fluent_comments/forms/_captcha.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 |
3 |
4 | class CaptchaFormMixin(object):
5 | def _reorder_fields(self, ordering):
6 | """
7 | Test that the 'captcha' field is really present.
8 | This could be broken by a bad FLUENT_COMMENTS_FIELD_ORDER configuration.
9 | """
10 | if "captcha" not in ordering:
11 | raise ImproperlyConfigured(
12 | "When using 'FLUENT_COMMENTS_FIELD_ORDER', "
13 | "make sure the 'captcha' field included too to use '{}' form. ".format(
14 | self.__class__.__name__
15 | )
16 | )
17 | super()._reorder_fields(ordering)
18 |
19 | # Avoid making captcha required for previews.
20 | if self.is_preview:
21 | self.fields.pop("captcha")
22 |
--------------------------------------------------------------------------------
/fluent_comments/forms/base.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 |
3 | import django_comments
4 | from django.core.exceptions import ImproperlyConfigured
5 | from fluent_comments import appsettings
6 | from fluent_comments.forms.helper import CommentFormHelper
7 | from fluent_comments.forms.helper import (
8 | SubmitButton,
9 | PreviewButton,
10 | ) # noqa, import at old class location too
11 |
12 | if appsettings.USE_THREADEDCOMMENTS:
13 | from threadedcomments.forms import ThreadedCommentForm as base_class
14 | else:
15 | from django_comments.forms import CommentForm as base_class
16 |
17 |
18 | class AbstractCommentForm(base_class):
19 | """
20 | The comment form, applies various settings.
21 | """
22 |
23 | #: Helper for {% crispy %} template tag
24 | helper = CommentFormHelper()
25 |
26 | def __init__(self, *args, **kwargs):
27 | self.is_preview = kwargs.pop("is_preview", False)
28 | super().__init__(*args, **kwargs)
29 |
30 | # Remove fields from the form.
31 | # This has to be done in the constructor, because the ThreadedCommentForm
32 | # inserts the title field in the __init__, instead of the static form definition.
33 | for name in appsettings.FLUENT_COMMENTS_EXCLUDE_FIELDS:
34 | try:
35 | self.fields.pop(name)
36 | except KeyError:
37 | raise ImproperlyConfigured(
38 | "Field name '{0}' in FLUENT_COMMENTS_EXCLUDE_FIELDS is invalid, "
39 | "it does not exist in the '{1}' class.".format(name, self.__class__.__name__)
40 | )
41 |
42 | if appsettings.FLUENT_COMMENTS_FIELD_ORDER:
43 | ordering = (
44 | CommentFormHelper.BASE_FIELDS_TOP
45 | + appsettings.FLUENT_COMMENTS_FIELD_ORDER
46 | + CommentFormHelper.BASE_FIELDS_END
47 | )
48 | self._reorder_fields(ordering)
49 |
50 | def _reorder_fields(self, ordering):
51 | new_fields = OrderedDict()
52 | for name in ordering:
53 | new_fields[name] = self.fields[name]
54 | self.fields = new_fields
55 |
56 | def get_comment_model(self):
57 | # Provide the model used for comments. When this doesn't match
58 | # the sender used by django_comments.moderation.Moderator used in
59 | # `comment_will_be_posted.connect(..., sender=...)`, it will break moderation.
60 | #
61 | # Since ThreadedCommentForm overrides this method, it breaks moderation
62 | # with COMMENTS_APP="fluent_comments". Hence, by default let this match
63 | # the the model the app is configured with.
64 | return django_comments.get_model()
65 |
66 | def get_comment_create_data(self, *args, **kwargs):
67 | # Fake form data for excluded fields, so there are no KeyError exceptions
68 | for name in appsettings.FLUENT_COMMENTS_EXCLUDE_FIELDS:
69 | self.cleaned_data[name] = ""
70 |
71 | # Pass args, kwargs for django-contrib-comments 1.8, which accepts a ``site_id`` argument.
72 | return super().get_comment_create_data(*args, **kwargs)
73 |
--------------------------------------------------------------------------------
/fluent_comments/forms/captcha.py:
--------------------------------------------------------------------------------
1 | """
2 | Variations of the form that use
3 | """
4 | from django.utils.translation import pgettext_lazy
5 |
6 | from fluent_comments.forms._captcha import CaptchaFormMixin
7 | from .compact import CompactCommentForm, CompactLabelsCommentForm
8 | from .default import DefaultCommentForm
9 |
10 | try:
11 | from captcha.fields import CaptchaField
12 | except ImportError:
13 | raise ImportError(
14 | "To use '{}', you need to have django-simple-captcha installed.".format(__name__)
15 | )
16 |
17 | captcha_field = CaptchaField(help_text=pgettext_lazy("captcha-help-text", u"Type the text."))
18 |
19 |
20 | class DefaultCommentForm(CaptchaFormMixin, DefaultCommentForm):
21 | """
22 | Comment form with reCAPTCHA field.
23 | """
24 |
25 | captcha = captcha_field
26 |
27 |
28 | class CompactCommentForm(CaptchaFormMixin, CompactCommentForm):
29 | """
30 | Compact variation 1.
31 | """
32 |
33 | captcha = captcha_field
34 |
35 |
36 | class CompactLabelsCommentForm(CaptchaFormMixin, CompactLabelsCommentForm):
37 | """
38 | Compact variation 2.
39 | """
40 |
41 | captcha = captcha_field
42 |
--------------------------------------------------------------------------------
/fluent_comments/forms/compact.py:
--------------------------------------------------------------------------------
1 | """
2 | Different form layout, very compact
3 | """
4 | from crispy_forms.layout import Column, Layout, Row
5 | from django.utils.functional import cached_property
6 |
7 | from fluent_comments import appsettings
8 | from fluent_comments.forms.base import AbstractCommentForm, PreviewButton, SubmitButton
9 | from fluent_comments.forms.helper import CompactLabelsCommentFormHelper
10 |
11 |
12 | class CompactLabelsCommentForm(AbstractCommentForm):
13 | """
14 | A form layout where the labels are replaced with ``placeholder`` attributes.
15 | """
16 |
17 | @cached_property
18 | def helper(self):
19 | # Initialize on demand
20 | helper = CompactLabelsCommentFormHelper()
21 | helper.layout = Layout(*self.fields.keys())
22 | helper.add_input(SubmitButton())
23 | helper.add_input(PreviewButton())
24 | return helper
25 |
26 |
27 | class CompactCommentForm(AbstractCommentForm):
28 | """
29 | A form with a very compact layout;
30 | all the name/email/phone fields are displayed in a single top row.
31 | It uses Bootstrap 3 layout by default to generate the columns.
32 | """
33 |
34 | top_row_fields = appsettings.FLUENT_COMMENTS_COMPACT_FIELDS
35 | top_row_columns = appsettings.FLUENT_COMMENTS_COMPACT_GRID_SIZE
36 | top_column_class = appsettings.FLUENT_COMMENTS_COMPACT_COLUMN_CSS_CLASS
37 |
38 | @cached_property
39 | def helper(self):
40 | # As extra service, auto-adjust the layout based on the project settings.
41 | # This allows defining the top-row, and still get either 2 or 3 columns
42 | compact_fields = [name for name in self.fields.keys() if name in self.top_row_fields]
43 | other_fields = [name for name in self.fields.keys() if name not in self.top_row_fields]
44 | col_size = int(self.top_row_columns / len(compact_fields))
45 | col_class = self.top_column_class.format(size=col_size)
46 |
47 | compact_row = Row(*[Column(name, css_class=col_class) for name in compact_fields])
48 |
49 | # The fields are already ordered by the AbstractCommentForm.__init__ method.
50 | # See where the compact row should be.
51 | pos = list(self.fields.keys()).index(compact_fields[0])
52 | new_fields = other_fields
53 | new_fields.insert(pos, compact_row)
54 |
55 | helper = CompactLabelsCommentFormHelper()
56 | helper.layout = Layout(*new_fields)
57 | helper.add_input(SubmitButton())
58 | helper.add_input(PreviewButton())
59 | return helper
60 |
--------------------------------------------------------------------------------
/fluent_comments/forms/default.py:
--------------------------------------------------------------------------------
1 | from fluent_comments.forms.base import AbstractCommentForm, CommentFormHelper
2 |
3 |
4 | class DefaultCommentForm(AbstractCommentForm):
5 | """
6 | A simple comment form, backed by a model to save all data (in case email fails).
7 | """
8 |
9 | helper = CommentFormHelper()
10 |
--------------------------------------------------------------------------------
/fluent_comments/forms/helper.py:
--------------------------------------------------------------------------------
1 | from crispy_forms.helper import FormHelper
2 | from crispy_forms.layout import Button, Submit
3 | from crispy_forms.utils import TEMPLATE_PACK
4 | from django import forms
5 | from django.forms.widgets import Input
6 | from django.utils.translation import gettext_lazy as _
7 | from django_comments import get_form_target
8 |
9 | from fluent_comments import appsettings
10 |
11 |
12 | class CommentFormHelper(FormHelper):
13 | """
14 | The django-crispy-forms configuration that handles form appearance.
15 | The default is configured to show bootstrap forms nicely.
16 | """
17 |
18 | form_tag = False # we need to define the form_tag
19 | form_id = "comment-form-ID"
20 | form_class = "js-comments-form {0}".format(appsettings.FLUENT_COMMENTS_FORM_CSS_CLASS)
21 | label_class = appsettings.FLUENT_COMMENTS_LABEL_CSS_CLASS
22 | field_class = appsettings.FLUENT_COMMENTS_FIELD_CSS_CLASS
23 | render_unmentioned_fields = True # like honeypot and security_hash
24 |
25 | BASE_FIELDS_TOP = ("content_type", "object_pk", "timestamp", "security_hash")
26 | BASE_FIELDS_END = ("honeypot",)
27 |
28 | if appsettings.USE_THREADEDCOMMENTS:
29 | BASE_FIELDS_TOP += ("parent",)
30 |
31 | @property
32 | def form_action(self):
33 | return get_form_target() # reads get_form_target from COMMENTS_APP
34 |
35 | def __init__(self, form=None):
36 | super().__init__(form=form)
37 | if form is not None:
38 | # When using the helper like this, it could generate all fields.
39 | self.form_id = "comment-form-{0}".format(form.target_object.pk)
40 | self.attrs = {
41 | "data-object-id": form.target_object.pk,
42 | }
43 |
44 |
45 | class CompactLabelsCommentFormHelper(CommentFormHelper):
46 | """
47 | Compact labels in the form, show them as placeholder text instead.
48 |
49 | .. note::
50 | Make sure that the :attr:`layout` attribute is defined and
51 | it has fields added to it, otherwise the placeholders don't appear.
52 |
53 | The text input can easily be resized using CSS like:
54 |
55 | .. code-block: css
56 |
57 | @media only screen and (min-width: 768px) {
58 | form.comments-form input.form-control {
59 | width: 50%;
60 | }
61 | }
62 |
63 | """
64 |
65 | form_class = (
66 | CommentFormHelper.form_class.replace("form-horizontal", "form-vertical")
67 | + " comments-form-compact"
68 | )
69 | label_class = "sr-only"
70 | field_class = ""
71 |
72 | def render_layout(self, form, context, template_pack=TEMPLATE_PACK):
73 | """
74 | Copy any field label to the ``placeholder`` attribute.
75 | Note, this method is called when :attr:`layout` is defined.
76 | """
77 | # Writing the label values into the field placeholders.
78 | # This is done at rendering time, so the Form.__init__() could update any labels before.
79 | # Django 1.11 no longer lets EmailInput or URLInput inherit from TextInput,
80 | # so checking for `Input` instead while excluding `HiddenInput`.
81 | for field in form.fields.values():
82 | if (
83 | field.label
84 | and isinstance(field.widget, (Input, forms.Textarea))
85 | and not isinstance(field.widget, forms.HiddenInput)
86 | ):
87 | field.widget.attrs["placeholder"] = u"{0}:".format(field.label)
88 |
89 | return super().render_layout(form, context, template_pack=template_pack)
90 |
91 |
92 | class SubmitButton(Submit):
93 | """
94 | The submit button to add to the layout.
95 |
96 | Note: the ``name=post`` is mandatory, it helps the
97 | """
98 |
99 | def __init__(self, text=_("Post Comment"), **kwargs):
100 | super().__init__(name="post", value=text, **kwargs)
101 |
102 |
103 | class PreviewButton(Button):
104 | """
105 | The preview button to add to the layout.
106 |
107 | Note: the ``name=post`` is mandatory, it helps the
108 | """
109 |
110 | input_type = "submit"
111 |
112 | def __init__(self, text=_("Preview"), **kwargs):
113 | kwargs.setdefault("css_class", "btn-default")
114 | super().__init__(name="preview", value=text, **kwargs)
115 |
--------------------------------------------------------------------------------
/fluent_comments/forms/recaptcha.py:
--------------------------------------------------------------------------------
1 | """
2 | Variations of the form that use
3 | """
4 | from django.conf import settings
5 | from django.core.exceptions import ImproperlyConfigured
6 |
7 | from fluent_comments.forms._captcha import CaptchaFormMixin
8 | from .compact import CompactCommentForm, CompactLabelsCommentForm
9 | from .default import DefaultCommentForm
10 |
11 | try:
12 | from nocaptcha_recaptcha.fields import NoReCaptchaField
13 |
14 | captcha_field = NoReCaptchaField()
15 | except ImportError:
16 | try:
17 | from captcha.fields import ReCaptchaField
18 |
19 | captcha_field = ReCaptchaField()
20 |
21 | if not getattr(settings, "NOCAPTCHA", False):
22 | raise ImproperlyConfigured(
23 | "reCAPTCHA v1 is phased out. Add `NOCAPTCHA = True` to your settings "
24 | 'to use the modern "no captcha" reCAPTCHA v2.'
25 | )
26 | except ImportError:
27 | raise ImportError(
28 | "To use '{}', you need to have django-nocaptcha-recaptcha"
29 | " or django-recaptcha2 installed.".format(__name__)
30 | )
31 |
32 |
33 | class DefaultCommentForm(CaptchaFormMixin, DefaultCommentForm):
34 | """
35 | Contact form with reCAPTCHA field.
36 | """
37 |
38 | captcha = captcha_field
39 |
40 | class Media:
41 | js = ("https://www.google.com/recaptcha/api.js",)
42 |
43 |
44 | class CompactCommentForm(CaptchaFormMixin, CompactCommentForm):
45 | """
46 | Compact variation 1.
47 | """
48 |
49 | captcha = captcha_field
50 |
51 | class Media:
52 | js = ("https://www.google.com/recaptcha/api.js",)
53 |
54 |
55 | class CompactLabelsCommentForm(CaptchaFormMixin, CompactLabelsCommentForm):
56 | """
57 | Compact variation 2.
58 | """
59 |
60 | captcha = captcha_field
61 |
62 | class Media:
63 | js = ("https://www.google.com/recaptcha/api.js",)
64 |
--------------------------------------------------------------------------------
/fluent_comments/locale/cs/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2018-05-04 14:18+0200\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
20 |
21 | #: admin.py:36
22 | msgid "Content"
23 | msgstr "Obsah"
24 |
25 | #: admin.py:39
26 | msgid "Account information"
27 | msgstr "Informace o účtu"
28 |
29 | #: admin.py:42
30 | msgid "Moderation"
31 | msgstr "Moderování"
32 |
33 | #: admin.py:53
34 | msgid "Hierarchy"
35 | msgstr "Hierarchie"
36 |
37 | #: admin.py:74
38 | msgid "Page"
39 | msgstr "Strana"
40 |
41 | #: admin.py:85
42 | msgid "user's name"
43 | msgstr "jméno uživatele"
44 |
45 | #: forms/helper.py:93 templates/comments/form.html:21
46 | msgid "Post Comment"
47 | msgstr "Odeslat váš komentář"
48 |
49 | #: forms/helper.py:105 templates/comments/form.html:22
50 | #: templates/comments/preview.html:27
51 | msgid "Preview"
52 | msgstr "Náhled"
53 |
54 | #: models.py:44
55 | msgid "Comment"
56 | msgstr ""
57 |
58 | #: models.py:45
59 | msgid "Comments"
60 | msgstr ""
61 |
62 | #: templates/comments/comment.html:22 templates/comments/preview.html:12
63 | msgid "Preview of your comment"
64 | msgstr "Náhled komentáře"
65 |
66 | #: templates/comments/comment.html:26
67 | msgid "Anonymous"
68 | msgstr "Anonymní"
69 |
70 | #: templates/comments/comment.html:29
71 | #, python-format
72 | msgid "on %(submit_date)s"
73 | msgstr "poslal %(submit_date)s"
74 |
75 | #: templates/comments/comment.html:30
76 | msgid "moderated"
77 | msgstr "moderován"
78 |
79 | #: templates/comments/comment.html:31
80 | msgid "reply"
81 | msgstr "odpovědět"
82 |
83 | #: templates/comments/deleted.html:4
84 | msgid "Thanks for removing"
85 | msgstr "Děkujeme za odstranění"
86 |
87 | #: templates/comments/deleted.html:12
88 | msgid "Thanks for removing the comment"
89 | msgstr "Děkujeme za odstranění komentáře"
90 |
91 | #: templates/comments/deleted.html:14 templates/comments/flagged.html:14
92 | msgid ""
93 | "\n"
94 | " Thanks for taking the time to improve the quality of discussion on our "
95 | "site. \n"
96 | " You will be sent back to the article...\n"
97 | " "
98 | msgstr ""
99 | "\n"
100 | " Děkuji za váš čas věnovaný na zlepšení kvality diskuze na naší stránce."
101 | " \n"
102 | " Budete přesměrováni zpět na článek...\n"
103 | " "
104 |
105 | #: templates/comments/deleted.html:20 templates/comments/flagged.html:20
106 | #: templates/comments/posted.html:26
107 | msgid "Back to the article"
108 | msgstr "Zpět na článek"
109 |
110 | #: templates/comments/flagged.html:4
111 | msgid "Thanks for flagging"
112 | msgstr "Děkujeme za označkování"
113 |
114 | #: templates/comments/flagged.html:12
115 | msgid "Thanks for flagging the comment"
116 | msgstr "Děkujeme za označkování komentáře"
117 |
118 | #: templates/comments/form.html:4
119 | msgid "Comments are closed."
120 | msgstr "Komentáře jsou uzavřené"
121 |
122 | #: templates/comments/posted.html:3
123 | msgid "Thanks for commenting"
124 | msgstr "Děkujeme za komentář"
125 |
126 | #: templates/comments/posted.html:11
127 | msgid "Thanks for posting your comment"
128 | msgstr "Děkujeme za odeslání komentáře"
129 |
130 | #: templates/comments/posted.html:13
131 | msgid ""
132 | "\n"
133 | " We have received your comment, and posted it on the web site. \n"
134 | " You will be sent back to the article...\n"
135 | " "
136 | msgstr ""
137 | "\n"
138 | " Váš komentář byl přijat a publikován na naší stránce \n"
139 | " Budete přesměrován zpět na článek...\n"
140 | " "
141 |
142 | #: templates/comments/preview.html:3
143 | msgid "Preview your comment"
144 | msgstr "Náhled vašeho komentáře"
145 |
146 | #: templates/comments/preview.html:10
147 | msgid "Please correct the error below"
148 | msgid_plural "Please correct the errors below"
149 | msgstr[0] "Prosím opravte chybu níže"
150 | msgstr[1] "Prosím opravte chyby níže"
151 |
152 | #: templates/comments/preview.html:17
153 | msgid "Post your comment"
154 | msgstr "Odeslat váš komentář"
155 |
156 | #: templates/comments/preview.html:20
157 | msgid "Or make changes"
158 | msgstr "Nebo udělat změny"
159 |
160 | #: templates/comments/preview.html:26
161 | msgid "Post"
162 | msgstr "Odeslat"
163 |
164 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:2
165 | msgid "cancel reply"
166 | msgstr "zrušit odpověď"
167 |
168 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:4
169 | msgid "Please wait . . ."
170 | msgstr "Prosím čekejte . . ."
171 |
172 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:6
173 | msgid "Your comment has been posted!"
174 | msgstr "Váš komentář byl publikován!"
175 |
176 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:7
177 | msgid ""
178 | "Your comment has been posted, it will be visible for other users after "
179 | "approval."
180 | msgstr ""
181 | "Váš komentář byl odeslán, bude zobrazen dalším uživatelům jakmile bude "
182 | "schválen."
183 |
--------------------------------------------------------------------------------
/fluent_comments/locale/nl/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2012
2 | # This file is distributed under the same license as the PACKAGE package.
3 | #
4 | # Diederik van der Boor , 2010-2012
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: \n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2018-05-04 14:18+0200\n"
10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 | "Last-Translator: Diederik van der Boor \n"
12 | "Language-Team: Dutch \n"
13 | "Language: nl\n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n"
18 |
19 | #: admin.py:36
20 | msgid "Content"
21 | msgstr "Inhoud"
22 |
23 | #: admin.py:39
24 | msgid "Account information"
25 | msgstr "Account informatie"
26 |
27 | #: admin.py:42
28 | msgid "Moderation"
29 | msgstr "Moderatie"
30 |
31 | #: admin.py:53
32 | msgid "Hierarchy"
33 | msgstr "Hiërarchie"
34 |
35 | #: admin.py:74
36 | msgid "Page"
37 | msgstr "Pagina"
38 |
39 | #: admin.py:85
40 | msgid "user's name"
41 | msgstr ""
42 |
43 | #: forms/helper.py:93 templates/comments/form.html:21
44 | msgid "Post Comment"
45 | msgstr "Plaats reactie"
46 |
47 | #: forms/helper.py:105 templates/comments/form.html:22
48 | #: templates/comments/preview.html:27
49 | msgid "Preview"
50 | msgstr "Voorbeeld"
51 |
52 | #: models.py:44
53 | msgid "Comment"
54 | msgstr ""
55 |
56 | #: models.py:45
57 | msgid "Comments"
58 | msgstr ""
59 |
60 | #: templates/comments/comment.html:22 templates/comments/preview.html:12
61 | msgid "Preview of your comment"
62 | msgstr "Voorbeeld van je reactie"
63 |
64 | #: templates/comments/comment.html:26
65 | msgid "Anonymous"
66 | msgstr "Anoniem"
67 |
68 | #: templates/comments/comment.html:29
69 | #, python-format
70 | msgid "on %(submit_date)s"
71 | msgstr "op %(submit_date)s"
72 |
73 | #: templates/comments/comment.html:30
74 | msgid "moderated"
75 | msgstr "gemodereerd"
76 |
77 | #: templates/comments/comment.html:31
78 | msgid "reply"
79 | msgstr "reageren"
80 |
81 | #: templates/comments/deleted.html:4
82 | msgid "Thanks for removing"
83 | msgstr "Bedankt voor het verwijderen"
84 |
85 | #: templates/comments/deleted.html:12
86 | msgid "Thanks for removing the comment"
87 | msgstr "Bedankt voor het verwijderen van de reactie"
88 |
89 | #: templates/comments/deleted.html:14 templates/comments/flagged.html:14
90 | msgid ""
91 | "\n"
92 | " Thanks for taking the time to improve the quality of discussion on our "
93 | "site. \n"
94 | " You will be sent back to the article...\n"
95 | " "
96 | msgstr ""
97 | "\n"
98 | " Bedankt voor je tijd om de kwaliteit van discussie op onze site te "
99 | "verbeteren. \n"
100 | " Je wordt nu teruggestuurd naar de vorige pagina...\n"
101 | " "
102 |
103 | #: templates/comments/deleted.html:20 templates/comments/flagged.html:20
104 | #: templates/comments/posted.html:26
105 | msgid "Back to the article"
106 | msgstr "Terug naar de pagina"
107 |
108 | #: templates/comments/flagged.html:4
109 | msgid "Thanks for flagging"
110 | msgstr "Bedankt voor je melding"
111 |
112 | #: templates/comments/flagged.html:12
113 | msgid "Thanks for flagging the comment"
114 | msgstr "Bedankt voor het rapporteren van de reactie"
115 |
116 | #: templates/comments/form.html:4
117 | msgid "Comments are closed."
118 | msgstr "Reacties zijn gesloten."
119 |
120 | #: templates/comments/posted.html:3
121 | msgid "Thanks for commenting"
122 | msgstr "Bedankt voor je reactie"
123 |
124 | #: templates/comments/posted.html:11
125 | msgid "Thanks for posting your comment"
126 | msgstr "Bedankt voor het insturen van je reactie"
127 |
128 | #: templates/comments/posted.html:13
129 | msgid ""
130 | "\n"
131 | " We have received your comment, and posted it on the web site. \n"
132 | " You will be sent back to the article...\n"
133 | " "
134 | msgstr ""
135 | "\n"
136 | " We hebben je reactie ontvangen, en geplaatst op de website. \n"
137 | " Je wordt nu teruggestuurd naar de vorige pagina...\n"
138 | " "
139 |
140 | #: templates/comments/preview.html:3
141 | msgid "Preview your comment"
142 | msgstr "Voorbeeld van je reactie"
143 |
144 | #: templates/comments/preview.html:10
145 | msgid "Please correct the error below"
146 | msgid_plural "Please correct the errors below"
147 | msgstr[0] "Corrigeer a.u.b. de fout hieronder"
148 | msgstr[1] "Corrigeer a.u.b. de fouten hieronder"
149 |
150 | #: templates/comments/preview.html:17
151 | msgid "Post your comment"
152 | msgstr "Reactie plaatsen"
153 |
154 | #: templates/comments/preview.html:20
155 | msgid "Or make changes"
156 | msgstr "Of pas het bericht aan"
157 |
158 | #: templates/comments/preview.html:26
159 | msgid "Post"
160 | msgstr "Plaatsen"
161 |
162 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:2
163 | msgid "cancel reply"
164 | msgstr "annuleer reactie"
165 |
166 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:4
167 | msgid "Please wait . . ."
168 | msgstr "Even geduld . . ."
169 |
170 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:6
171 | msgid "Your comment has been posted!"
172 | msgstr "Je reactie is geplaatst!"
173 |
174 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:7
175 | msgid ""
176 | "Your comment has been posted, it will be visible for other users after "
177 | "approval."
178 | msgstr ""
179 | "Je reactie is geplaatst en wordt pas na goedkeuring zichtbaar voor andere "
180 | "gebruikers."
181 |
--------------------------------------------------------------------------------
/fluent_comments/locale/sk/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2018-05-04 14:18+0200\n"
12 | "PO-Revision-Date: 2016-05-09 22:00+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
20 |
21 | #: admin.py:36
22 | msgid "Content"
23 | msgstr "Obsah"
24 |
25 | #: admin.py:39
26 | msgid "Account information"
27 | msgstr "Informácie o účte"
28 |
29 | #: admin.py:42
30 | msgid "Moderation"
31 | msgstr "Moderácia"
32 |
33 | #: admin.py:53
34 | msgid "Hierarchy"
35 | msgstr "Hierarchia"
36 |
37 | #: admin.py:74
38 | msgid "Page"
39 | msgstr "Stránka"
40 |
41 | #: admin.py:85
42 | msgid "user's name"
43 | msgstr "používateľské meno"
44 |
45 | #: forms/helper.py:93 templates/comments/form.html:21
46 | msgid "Post Comment"
47 | msgstr "Odoslať komentár"
48 |
49 | #: forms/helper.py:105 templates/comments/form.html:22
50 | #: templates/comments/preview.html:27
51 | msgid "Preview"
52 | msgstr "Náhľad"
53 |
54 | #: models.py:44
55 | msgid "Comment"
56 | msgstr ""
57 |
58 | #: models.py:45
59 | msgid "Comments"
60 | msgstr ""
61 |
62 | #: templates/comments/comment.html:22 templates/comments/preview.html:12
63 | msgid "Preview of your comment"
64 | msgstr "Náhľad komentára"
65 |
66 | #: templates/comments/comment.html:26
67 | msgid "Anonymous"
68 | msgstr "Anonym"
69 |
70 | #: templates/comments/comment.html:29
71 | #, python-format
72 | msgid "on %(submit_date)s"
73 | msgstr "odoslané %(submit_date)s"
74 |
75 | #: templates/comments/comment.html:30
76 | msgid "moderated"
77 | msgstr "moderované"
78 |
79 | #: templates/comments/comment.html:31
80 | msgid "reply"
81 | msgstr "odpovedať"
82 |
83 | #: templates/comments/deleted.html:4
84 | msgid "Thanks for removing"
85 | msgstr "Ďakujeme za odstránenie"
86 |
87 | #: templates/comments/deleted.html:12
88 | msgid "Thanks for removing the comment"
89 | msgstr "Ďakujeme za odstránenie komentára"
90 |
91 | #: templates/comments/deleted.html:14 templates/comments/flagged.html:14
92 | msgid ""
93 | "\n"
94 | " Thanks for taking the time to improve the quality of discussion on our "
95 | "site. \n"
96 | " You will be sent back to the article...\n"
97 | " "
98 | msgstr ""
99 | "\n"
100 | " Vďaka, že ste si našli čas na zlepšenie kvality diskusie na našom webe."
101 | " \n"
102 | " Presmerujeme vás späť na diskusiu...\n"
103 | " "
104 |
105 | #: templates/comments/deleted.html:20 templates/comments/flagged.html:20
106 | #: templates/comments/posted.html:26
107 | msgid "Back to the article"
108 | msgstr "Naspäť k diskusii"
109 |
110 | #: templates/comments/flagged.html:4
111 | msgid "Thanks for flagging"
112 | msgstr "Ďakujeme za označenie"
113 |
114 | #: templates/comments/flagged.html:12
115 | msgid "Thanks for flagging the comment"
116 | msgstr "Vďaka za označenie komentára"
117 |
118 | #: templates/comments/form.html:4
119 | msgid "Comments are closed."
120 | msgstr "Diskusia je uzavretá."
121 |
122 | #: templates/comments/posted.html:3
123 | msgid "Thanks for commenting"
124 | msgstr "Vďaka za komentár"
125 |
126 | #: templates/comments/posted.html:11
127 | msgid "Thanks for posting your comment"
128 | msgstr "Vďaka za komentár"
129 |
130 | #: templates/comments/posted.html:13
131 | msgid ""
132 | "\n"
133 | " We have received your comment, and posted it on the web site. \n"
134 | " You will be sent back to the article...\n"
135 | " "
136 | msgstr ""
137 | "\n"
138 | " Dostali sme váš komentár a zobrazili sme ho na stránke. \n"
139 | " Presmerujeme vás späť na diskusiu...\n"
140 | " "
141 |
142 | #: templates/comments/preview.html:3
143 | msgid "Preview your comment"
144 | msgstr "Náhľad komentára"
145 |
146 | #: templates/comments/preview.html:10
147 | msgid "Please correct the error below"
148 | msgid_plural "Please correct the errors below"
149 | msgstr[0] "Prosím opravte nasledujúcu chybu"
150 | msgstr[1] "Prosím opravte nasledujúce chyby"
151 |
152 | #: templates/comments/preview.html:17
153 | msgid "Post your comment"
154 | msgstr "Odoslať komentár"
155 |
156 | #: templates/comments/preview.html:20
157 | msgid "Or make changes"
158 | msgstr "Alebo spravte zmeny"
159 |
160 | #: templates/comments/preview.html:26
161 | msgid "Post"
162 | msgstr "Odoslať"
163 |
164 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:2
165 | msgid "cancel reply"
166 | msgstr "zrušiť odpoveď"
167 |
168 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:4
169 | msgid "Please wait . . ."
170 | msgstr "Počkajte, prosím . . ."
171 |
172 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:6
173 | msgid "Your comment has been posted!"
174 | msgstr "Váš komentár bol publikovaný!"
175 |
176 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:7
177 | msgid ""
178 | "Your comment has been posted, it will be visible for other users after "
179 | "approval."
180 | msgstr ""
181 | "Komentár bol odoslaný, pre ostatných užívateľov bude viditeľný po súhlase "
182 | "moderátora."
183 |
--------------------------------------------------------------------------------
/fluent_comments/locale/zh/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: 0.1\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2018-05-04 14:18+0200\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: Zhiwei Wang \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: Chinese\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n"
20 |
21 | #: admin.py:36
22 | msgid "Content"
23 | msgstr "内容"
24 |
25 | #: admin.py:39
26 | msgid "Account information"
27 | msgstr "账户信息"
28 |
29 | #: admin.py:42
30 | msgid "Moderation"
31 | msgstr "审核"
32 |
33 | #: admin.py:53
34 | msgid "Hierarchy"
35 | msgstr "层级"
36 |
37 | #: admin.py:74
38 | msgid "Page"
39 | msgstr "页面"
40 |
41 | #: admin.py:85
42 | msgid "user's name"
43 | msgstr "用户名称"
44 |
45 | #: forms/helper.py:93 templates/comments/form.html:21
46 | msgid "Post Comment"
47 | msgstr "发布评论"
48 |
49 | #: forms/helper.py:105 templates/comments/form.html:22
50 | #: templates/comments/preview.html:27
51 | msgid "Preview"
52 | msgstr "预览"
53 |
54 | #: models.py:44
55 | msgid "Comment"
56 | msgstr ""
57 |
58 | #: models.py:45
59 | msgid "Comments"
60 | msgstr ""
61 |
62 | #: templates/comments/comment.html:22 templates/comments/preview.html:12
63 | msgid "Preview of your comment"
64 | msgstr "预览评论"
65 |
66 | #: templates/comments/comment.html:26
67 | msgid "Anonymous"
68 | msgstr "匿名"
69 |
70 | #: templates/comments/comment.html:29
71 | #, python-format
72 | msgid "on %(submit_date)s"
73 | msgstr "于%(submit_date)s"
74 |
75 | #: templates/comments/comment.html:30
76 | msgid "moderated"
77 | msgstr "正在审核"
78 |
79 | #: templates/comments/comment.html:31
80 | msgid "reply"
81 | msgstr "回复"
82 |
83 | #: templates/comments/deleted.html:4
84 | msgid "Thanks for removing"
85 | msgstr "感谢移除"
86 |
87 | #: templates/comments/deleted.html:12
88 | msgid "Thanks for removing the comment"
89 | msgstr "感谢移除该评论"
90 |
91 | #: templates/comments/deleted.html:14 templates/comments/flagged.html:14
92 | msgid ""
93 | "\n"
94 | " Thanks for taking the time to improve the quality of discussion on our "
95 | "site. \n"
96 | " You will be sent back to the article...\n"
97 | " "
98 | msgstr ""
99 | "\n"
100 | " 感谢您花时间提升我们网站的评论质量。 \n"
101 | " 您将回到文章页面。。。\n"
102 | " "
103 |
104 | #: templates/comments/deleted.html:20 templates/comments/flagged.html:20
105 | #: templates/comments/posted.html:26
106 | msgid "Back to the article"
107 | msgstr "返回文章"
108 |
109 | #: templates/comments/flagged.html:4
110 | msgid "Thanks for flagging"
111 | msgstr "感谢标记"
112 |
113 | #: templates/comments/flagged.html:12
114 | msgid "Thanks for flagging the comment"
115 | msgstr "感谢标记该评论"
116 |
117 | #: templates/comments/form.html:4
118 | msgid "Comments are closed."
119 | msgstr "评论已关闭"
120 |
121 | #: templates/comments/posted.html:3
122 | msgid "Thanks for commenting"
123 | msgstr "感谢评论"
124 |
125 | #: templates/comments/posted.html:11
126 | msgid "Thanks for posting your comment"
127 | msgstr "感谢发布您的评论"
128 |
129 | #: templates/comments/posted.html:13
130 | msgid ""
131 | "\n"
132 | " We have received your comment, and posted it on the web site. \n"
133 | " You will be sent back to the article...\n"
134 | " "
135 | msgstr ""
136 | "\n"
137 | " 我们已经收到您的评论并发布在了网站上。 \n"
138 | " 您将回到文章页面。。。\n"
139 | " "
140 |
141 | #: templates/comments/preview.html:3
142 | msgid "Preview your comment"
143 | msgstr "预览评论"
144 |
145 | #: templates/comments/preview.html:10
146 | msgid "Please correct the error below"
147 | msgid_plural "Please correct the errors below"
148 | msgstr[0] "请修正下面的错误"
149 | msgstr[1] "请修正以下这些错误"
150 |
151 | #: templates/comments/preview.html:17
152 | msgid "Post your comment"
153 | msgstr "发布评论"
154 |
155 | #: templates/comments/preview.html:20
156 | msgid "Or make changes"
157 | msgstr "修改"
158 |
159 | #: templates/comments/preview.html:26
160 | msgid "Post"
161 | msgstr "发布"
162 |
163 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:2
164 | msgid "cancel reply"
165 | msgstr "取消回复"
166 |
167 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:4
168 | msgid "Please wait . . ."
169 | msgstr "请稍候。。。"
170 |
171 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:6
172 | msgid "Your comment has been posted!"
173 | msgstr "您的评论已发布!"
174 |
175 | #: templates/fluent_comments/templatetags/ajax_comment_tags.html:7
176 | msgid ""
177 | "Your comment has been posted, it will be visible for other users after "
178 | "approval."
179 | msgstr "您的评论已发布,待审核后可见。"
180 |
--------------------------------------------------------------------------------
/fluent_comments/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.db import models, migrations
3 |
4 |
5 | class Migration(migrations.Migration):
6 |
7 | dependencies = [
8 | ("django_comments", "__first__"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="FluentComment",
14 | fields=[],
15 | options={
16 | "managed": False,
17 | "proxy": True,
18 | "managed": False,
19 | "verbose_name": "Comment",
20 | "verbose_name_plural": "Comments",
21 | },
22 | bases=("django_comments.comment",),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/fluent_comments/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-fluent/django-fluent-comments/5d53578544b5b5ecd8c34719672fd43668fb2dd4/fluent_comments/migrations/__init__.py
--------------------------------------------------------------------------------
/fluent_comments/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.contenttypes.fields import GenericRelation
2 | from django.utils.translation import gettext_lazy as _
3 | from django_comments import get_model as get_comments_model
4 | from django_comments.managers import CommentManager
5 |
6 | from fluent_comments import appsettings
7 |
8 | if appsettings.USE_THREADEDCOMMENTS:
9 | from threadedcomments.models import ThreadedComment as BaseModel
10 | else:
11 | from django_comments.models import Comment as BaseModel
12 |
13 |
14 | class FluentCommentManager(CommentManager):
15 | """
16 | Manager to optimize SQL queries for comments.
17 | """
18 |
19 | def get_queryset(self):
20 | return super().get_queryset().select_related("user")
21 |
22 |
23 | class FluentComment(BaseModel):
24 | """
25 | Proxy model to make sure that a ``select_related()`` is performed on the ``user`` field.
26 | """
27 |
28 | objects = FluentCommentManager()
29 |
30 | class Meta:
31 | verbose_name = _("Comment")
32 | verbose_name_plural = _("Comments")
33 | proxy = True
34 | managed = False
35 |
36 |
37 | def get_comments_for_model(content_object, include_moderated=False):
38 | """
39 | Return the QuerySet with all comments for a given model.
40 | """
41 | qs = get_comments_model().objects.for_model(content_object)
42 |
43 | if not include_moderated:
44 | qs = qs.filter(is_public=True, is_removed=False)
45 |
46 | return qs
47 |
48 |
49 | class CommentsRelation(GenericRelation):
50 | """
51 | A :class:`~django.contrib.contenttypes.generic.GenericRelation` which can be applied to a parent model that
52 | is expected to have comments. For example:
53 |
54 | .. code-block:: python
55 |
56 | class Article(models.Model):
57 | comments_set = CommentsRelation()
58 | """
59 |
60 | def __init__(self, *args, **kwargs):
61 | super().__init__(
62 | to=get_comments_model(),
63 | content_type_field="content_type",
64 | object_id_field="object_pk",
65 | **kwargs
66 | )
67 |
--------------------------------------------------------------------------------
/fluent_comments/moderation.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from urllib.parse import urljoin
3 |
4 | from akismet import SpamStatus
5 | from django_comments.moderation import moderator, CommentModerator
6 |
7 | from fluent_comments import appsettings
8 | from fluent_comments.akismet import akismet_check
9 | from fluent_comments.email import send_comment_posted
10 | from fluent_comments.utils import split_words
11 |
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | # Akismet code originally based on django-comments-spamfighter.
16 |
17 | __all__ = (
18 | "FluentCommentsModerator",
19 | "moderate_model",
20 | "get_model_moderator",
21 | "comments_are_open",
22 | "comments_are_moderated",
23 | )
24 |
25 |
26 | class FluentCommentsModerator(CommentModerator):
27 | """
28 | Moderation policy for fluent-comments.
29 | """
30 |
31 | auto_close_field = None
32 | auto_moderate_field = None
33 | enable_field = None
34 |
35 | close_after = appsettings.FLUENT_COMMENTS_CLOSE_AFTER_DAYS
36 | moderate_after = appsettings.FLUENT_COMMENTS_MODERATE_AFTER_DAYS
37 | email_notification = appsettings.FLUENT_COMMENTS_USE_EMAIL_NOTIFICATION
38 | akismet_check = appsettings.FLUENT_CONTENTS_USE_AKISMET
39 | akismet_check_action = appsettings.FLUENT_COMMENTS_AKISMET_ACTION
40 | moderate_bad_words = set(appsettings.FLUENT_COMMENTS_MODERATE_BAD_WORDS)
41 |
42 | def allow(self, comment, content_object, request):
43 | """
44 | Determine whether a given comment is allowed to be posted on a given object.
45 |
46 | Returns ``True`` if the comment should be allowed, ``False`` otherwise.
47 | """
48 | # Parent class check
49 | if not super().allow(comment, content_object, request):
50 | return False
51 |
52 | # Akismet check
53 | if self.akismet_check:
54 | akismet_result = akismet_check(comment, content_object, request)
55 | if self.akismet_check_action == "delete" and akismet_result in (
56 | SpamStatus.ProbableSpam,
57 | SpamStatus.DefiniteSpam,
58 | ):
59 | return False # Akismet marked the comment as spam.
60 | elif self.akismet_check_action == "auto" and akismet_result == SpamStatus.DefiniteSpam:
61 | return False # Clearly spam
62 |
63 | return True
64 |
65 | def moderate(self, comment, content_object, request):
66 | """
67 | Determine whether a given comment on a given object should be allowed to show up immediately,
68 | or should be marked non-public and await approval.
69 |
70 | Returns ``True`` if the comment should be moderated (marked non-public), ``False`` otherwise.
71 | """
72 |
73 | # Soft delete checks are done first, so these comments are not mistakenly "just moderated"
74 | # for expiring the `close_after` date, but correctly get marked as spam instead.
75 | # This helps staff to quickly see which comments need real moderation.
76 | if self.akismet_check:
77 | akismet_result = akismet_check(comment, content_object, request)
78 | if akismet_result:
79 | # Typically action=delete never gets here, unless the service was having problems.
80 | if (
81 | akismet_result
82 | in (
83 | SpamStatus.ProbableSpam,
84 | SpamStatus.DefiniteSpam,
85 | )
86 | and self.akismet_check_action in ("auto", "soft_delete", "delete")
87 | ):
88 | comment.is_removed = True # Set extra marker
89 |
90 | # SpamStatus.Unknown or action=moderate will end up in the moderation queue
91 | return True
92 |
93 | # Parent class check
94 | if super().moderate(comment, content_object, request):
95 | return True
96 |
97 | # Bad words check
98 | if self.moderate_bad_words:
99 | input_words = split_words(comment.comment)
100 | if self.moderate_bad_words.intersection(input_words):
101 | return True
102 |
103 | # Akismet check
104 | if self.akismet_check and self.akismet_check_action not in ("soft_delete", "delete"):
105 | # Return True if akismet marks this comment as spam and we want to moderate it.
106 | if akismet_check(comment, content_object, request):
107 | return True
108 |
109 | return False
110 |
111 | def email(self, comment, content_object, request):
112 | """
113 | Overwritten for a better email notification.
114 | """
115 | if not self.email_notification:
116 | return
117 |
118 | send_comment_posted(comment, request)
119 |
120 |
121 | class NullModerator(FluentCommentsModerator):
122 | """
123 | A moderator class that has the same effect as not being here.
124 | It allows all comments, disabling all moderation.
125 | This can be used in ``FLUENT_COMMENTS_DEFAULT_MODERATOR``.
126 | """
127 |
128 | def allow(self, comment, content_object, request):
129 | return True
130 |
131 | def moderate(self, comment, content_object, request):
132 | logger.info("Unconditionally allow comment, no default moderation set.")
133 | return False
134 |
135 |
136 | class AlwaysModerate(FluentCommentsModerator):
137 | """
138 | A moderator class that will always mark the comment as moderated.
139 | This can be used in ``FLUENT_COMMENTS_DEFAULT_MODERATOR``.
140 | """
141 |
142 | def moderate(self, comment, content_object, request):
143 | # Still calling super in case Akismet marks the comment as spam.
144 | return super().moderate(comment, content_object, request) or True
145 |
146 |
147 | class AlwaysDeny(FluentCommentsModerator):
148 | """
149 | A moderator that will deny any comments to be posted.
150 | This can be used in ``FLUENT_COMMENTS_DEFAULT_MODERATOR``.
151 | """
152 |
153 | def allow(self, comment, content_object, request):
154 | logger.warning(
155 | "Discarded comment on unregistered model '%s'", content_object.__class__.__name__
156 | )
157 | return False
158 |
159 |
160 | def moderate_model(ParentModel, publication_date_field=None, enable_comments_field=None):
161 | """
162 | Register a parent model (e.g. ``Blog`` or ``Article``) that should receive comment moderation.
163 |
164 | :param ParentModel: The parent model, e.g. a ``Blog`` or ``Article`` model.
165 | :param publication_date_field: The field name of a :class:`~django.db.models.DateTimeField` in the parent model which stores the publication date.
166 | :type publication_date_field: str
167 | :param enable_comments_field: The field name of a :class:`~django.db.models.BooleanField` in the parent model which stores the whether comments are enabled.
168 | :type enable_comments_field: str
169 | """
170 | attrs = {
171 | "auto_close_field": publication_date_field,
172 | "auto_moderate_field": publication_date_field,
173 | "enable_field": enable_comments_field,
174 | }
175 | ModerationClass = type(ParentModel.__name__ + "Moderator", (FluentCommentsModerator,), attrs)
176 | moderator.register(ParentModel, ModerationClass)
177 |
178 |
179 | def get_model_moderator(model):
180 | """
181 | Return the moderator class that is registered with a content object.
182 | If there is no associated moderator with a class, None is returned.
183 |
184 | :param model: The Django model registered with :func:`moderate_model`
185 | :type model: :class:`~django.db.models.Model`
186 | :return: The moderator class which holds the moderation policies.
187 | :rtype: :class:`~django_comments.moderation.CommentModerator`
188 | """
189 | try:
190 | return moderator._registry[model]
191 | except KeyError:
192 | return None
193 |
194 |
195 | def comments_are_open(content_object):
196 | """
197 | Return whether comments are still open for a given target object.
198 | """
199 | moderator = get_model_moderator(content_object.__class__)
200 | if moderator is None:
201 | return True
202 |
203 | # Check the 'enable_field', 'auto_close_field' and 'close_after',
204 | # by reusing the basic Django policies.
205 | return CommentModerator.allow(moderator, None, content_object, None)
206 |
207 |
208 | def comments_are_moderated(content_object):
209 | """
210 | Return whether comments are moderated for a given target object.
211 | """
212 | moderator = get_model_moderator(content_object.__class__)
213 | if moderator is None:
214 | return False
215 |
216 | # Check the 'auto_moderate_field', 'moderate_after',
217 | # by reusing the basic Django policies.
218 | return CommentModerator.moderate(moderator, None, content_object, None)
219 |
--------------------------------------------------------------------------------
/fluent_comments/receivers.py:
--------------------------------------------------------------------------------
1 | """
2 | The comment signals are handled to fallback to a default moderator.
3 |
4 | This avoids not checking for spam or sending email notifications
5 | for comments that bypassed the moderator registration
6 | (e.g. posting a comment on a different page).
7 |
8 | This is especially useful when a django-fluent-contents "CommentsAreaItem"
9 | element is added to a random page subclass (which is likely not registered).
10 | """
11 | import logging
12 |
13 | import django_comments
14 | from django.core.exceptions import ImproperlyConfigured
15 | from django.dispatch import receiver
16 | from django.utils.module_loading import import_string
17 | from django_comments import signals
18 |
19 | from fluent_comments import appsettings, moderation
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 |
24 | def load_default_moderator():
25 | """
26 | Find a moderator object
27 | """
28 | if appsettings.FLUENT_COMMENTS_DEFAULT_MODERATOR == "default":
29 | # Perform spam checks
30 | return moderation.FluentCommentsModerator(None)
31 | elif appsettings.FLUENT_COMMENTS_DEFAULT_MODERATOR == "deny":
32 | # Deny all comments not from known registered models.
33 | return moderation.AlwaysDeny(None)
34 | elif str(appsettings.FLUENT_COMMENTS_DEFAULT_MODERATOR).lower() == "none":
35 | # Disables default moderator
36 | return moderation.NullModerator(None)
37 | elif "." in appsettings.FLUENT_COMMENTS_DEFAULT_MODERATOR:
38 | return import_string(appsettings.FLUENT_COMMENTS_DEFAULT_MODERATOR)(None)
39 | else:
40 | raise ImproperlyConfigured(
41 | "Bad FLUENT_COMMENTS_DEFAULT_MODERATOR value. Provide default/deny/none or a dotted path"
42 | )
43 |
44 |
45 | default_moderator = load_default_moderator()
46 | CommentModel = django_comments.get_model()
47 |
48 |
49 | @receiver(signals.comment_will_be_posted)
50 | def on_comment_will_be_posted(sender, comment, request, **kwargs):
51 | """
52 | Make sure both the Ajax and regular comments are checked for moderation.
53 | This signal is also used to link moderators to the comment posting.
54 | """
55 | content_object = comment.content_object
56 | moderator = moderation.get_model_moderator(content_object.__class__)
57 | if moderator and comment.__class__ is not CommentModel:
58 | # Help with some hard to diagnose problems. The default Django moderator connects
59 | # to the configured comment model. When this model differs from the signal sender,
60 | # the the form stores a different model then COMMENTS_APP provides.
61 | moderator = None
62 | logger.warning(
63 | "Comment of type '%s' was not moderated by '%s', "
64 | "because the parent '%s' has a moderator installed for '%s' instead",
65 | comment.__class__.__name__,
66 | moderator.__class__.__name__,
67 | content_object.__class__.__name__,
68 | CommentModel.__name__,
69 | )
70 |
71 | if moderator is None:
72 | logger.info(
73 | "Using default moderator for comment '%s' on parent '%s'",
74 | comment.__class__.__name__,
75 | content_object.__class__.__name__,
76 | )
77 | _run_default_moderator(comment, content_object, request)
78 |
79 |
80 | def _run_default_moderator(comment, content_object, request):
81 | """
82 | Run the default moderator
83 | """
84 | # The default moderator will likely not check things like "auto close".
85 | # It can still provide akismet and bad word checking.
86 | if not default_moderator.allow(comment, content_object, request):
87 | # Comment will be disallowed outright (HTTP 403 response)
88 | return False
89 |
90 | if default_moderator.moderate(comment, content_object, request):
91 | comment.is_public = False
92 |
93 |
94 | @receiver(signals.comment_was_posted)
95 | def on_comment_posted(sender, comment, request, **kwargs):
96 | """
97 | Send email notification of a new comment to site staff when email notifications have been requested.
98 | """
99 | content_object = comment.content_object
100 |
101 | moderator = moderation.get_model_moderator(content_object.__class__)
102 | if moderator is None or comment.__class__ is not CommentModel:
103 | # No custom moderator means no email would be sent.
104 | # This still pass the comment to the default moderator.
105 | default_moderator.email(comment, content_object, request)
106 |
--------------------------------------------------------------------------------
/fluent_comments/static/fluent_comments/css/ajaxcomments.css:
--------------------------------------------------------------------------------
1 | #comment-waiting {
2 | line-height: 16px;
3 | }
4 |
5 | #comment-waiting img {
6 | vertical-align: middle;
7 | padding: 0 4px 0 10px;
8 | }
9 |
10 | #comment-added-message,
11 | #comment-thanks {
12 | padding-left: 10px;
13 | }
14 |
15 | .comment-moderated-flag {
16 | font-variant: small-caps;
17 | margin-left: 5px;
18 | }
19 |
20 | #div_id_honeypot {
21 | /* Hide the honeypot from django_comments by default */
22 | display: none;
23 | }
24 |
25 |
26 | /* ---- threaded comments ---- */
27 |
28 | ul.comment-list-wrapper {
29 | /* to avoid touching our own "ul" tags, our tags are explicitly decorated with a class selector */
30 | margin-left: 0;
31 | padding-left: 0;
32 | }
33 |
34 | ul.comment-list-wrapper ul.comment-list-wrapper {
35 | margin-left: 1em;
36 | padding-left: 0;
37 | }
38 |
39 | li.comment-wrapper {
40 | list-style: none;
41 | margin-left: 0;
42 | padding-left: 0;
43 | }
44 |
45 | .js-comments-form-orig-position .comment-cancel-reply-link {
46 | display: none;
47 | }
48 |
--------------------------------------------------------------------------------
/fluent_comments/static/fluent_comments/img/ajax-wait.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-fluent/django-fluent-comments/5d53578544b5b5ecd8c34719672fd43668fb2dd4/fluent_comments/static/fluent_comments/img/ajax-wait.gif
--------------------------------------------------------------------------------
/fluent_comments/templates/comments/comment.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Something that django_comments does not provide:
3 | An individual template for a single comment, to easily be reused.
4 |
5 | This include is also used by the Ajax comments view.
6 | The div id should be "c{id}", because the comment.get_absolute_url() points to it.
7 |
8 | NOTE: to override the displayed date format, don't replace this template.
9 | Instead, define DATETIME_FORMAT in a locale file. Requires setting:
10 |
11 | FORMAT_MODULE_PATH = 'settings.locale'
12 |
13 | Then create 'settings/locale/XY/formats.py' with:
14 |
15 | DATETIME_FORMAT = '...'
16 |
17 | This should give you consistent dates across all views.
18 | {% endcomment %}
19 | {% load i18n %}
20 |
14 | {% blocktrans %}
15 | Thanks for taking the time to improve the quality of discussion on our site.
16 | You will be sent back to the article...
17 | {% endblocktrans %}
18 |
14 | {% blocktrans %}
15 | Thanks for taking the time to improve the quality of discussion on our site.
16 | You will be sent back to the article...
17 | {% endblocktrans %}
18 |
5 | {% else %}
6 | {% if not form.helper or not form.helper.form_tag %}
7 |
34 | {% endif %}
35 | {% endif %}
36 |
--------------------------------------------------------------------------------
/fluent_comments/templates/comments/list.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 |
3 | Since we support both flat comments, and threadedcomments,
4 | the 'fluent_comments_list' templatetag loads the proper template.
5 |
6 | It either loads:
7 | - fluent_comments/templatetags/flat_html.html
8 | - fluent_comments/templatetags/threaded_list.html
9 |
10 | Both reuse comments/comment.html eventually.
11 | To style comments, consider overwriting the comment.html template.
12 |
13 | {% endcomment %}
14 | {% load fluent_comments_tags %}{% fluent_comments_list %}
15 |
--------------------------------------------------------------------------------
/fluent_comments/templates/comments/posted.html:
--------------------------------------------------------------------------------
1 | {% extends "comments/base.html" %}{% load i18n fluent_comments_tags %}
2 |
3 | {% block title %}{% trans "Thanks for commenting" %}{% endblock %}
4 |
5 | {% block extrahead %}
6 | {{ block.super }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
{% trans "Thanks for posting your comment" %}
12 |
13 | {% blocktrans %}
14 | We have received your comment, and posted it on the web site.
15 | You will be sent back to the article...
16 | {% endblocktrans %}
17 |
18 |
19 | {% if comment %}{# django_comments accepts GET without data #}
20 | {# Use identical formatting to normal comment list #}
21 |
{% trans "Your comment has been posted, it will be visible for other users after approval." %}
8 |
--------------------------------------------------------------------------------
/fluent_comments/templates/fluent_comments/templatetags/flat_list.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 |
3 | The normal list, spiced up.
4 | This is included via comments/list.html
5 |
6 | The id="comments-..." is required for JavaScript,
7 | the 'comments/comment.html template is also used by the Ajax view.
8 |
9 | {% endcomment %}
10 | {% load fluent_comments_tags %}
11 |
12 | {% for comment in comment_list %}{% render_comment comment %}{% endfor %}
13 |
14 |
--------------------------------------------------------------------------------
/fluent_comments/templates/fluent_comments/templatetags/threaded_list.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 |
3 | The template for threadedcomments, spiced up.
4 | This is included via comments/list.html
5 |
6 | The id="comments-..." is required for JavaScript,
7 | the 'comments/comment.html template is also used by the Ajax view.
8 |
9 | {% endcomment %}
10 | {% load fluent_comments_tags threadedcomments_tags %}
11 |
12 | {% for comment in comment_list|fill_tree|annotate_tree %}
13 | {% ifchanged comment.parent_id %}{% else %}{% endifchanged %}
14 | {% if not comment.open and not comment.close %}{% endif %}
15 | {% if comment.open %}
{% endif %}
16 |
17 | {% render_comment comment %}
18 | {% for close in comment.close %}
{% endfor %}
19 | {% endfor %}
20 |
21 |
--------------------------------------------------------------------------------
/fluent_comments/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-fluent/django-fluent-comments/5d53578544b5b5ecd8c34719672fd43668fb2dd4/fluent_comments/templatetags/__init__.py
--------------------------------------------------------------------------------
/fluent_comments/templatetags/fluent_comments_tags.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.template import Library, Node, context_processors
3 | from django.template.loader import get_template
4 | from fluent_comments.utils import get_comment_template_name, get_comment_context_data
5 | from tag_parser import parse_token_kwargs
6 | from tag_parser.basetags import BaseInclusionNode
7 | from fluent_comments import appsettings
8 | from fluent_comments.models import get_comments_for_model
9 | from fluent_comments.moderation import comments_are_open, comments_are_moderated
10 |
11 |
12 | register = Library()
13 |
14 |
15 | class AjaxCommentTags(BaseInclusionNode):
16 | """
17 | Custom inclusion node with some special parsing features.
18 | Using the ``@register.inclusion_tag`` is not sufficient,
19 | because some keywords require custom parsing.
20 | """
21 |
22 | template_name = "fluent_comments/templatetags/ajax_comment_tags.html"
23 | min_args = 1
24 | max_args = 1
25 |
26 | @classmethod
27 | def parse(cls, parser, token):
28 | """
29 | Custom parsing for the ``{% ajax_comment_tags for ... %}`` tag.
30 | """
31 | # Process the template line.
32 | tag_name, args, kwargs = parse_token_kwargs(
33 | parser,
34 | token,
35 | allowed_kwargs=cls.allowed_kwargs,
36 | compile_args=False, # Only overrule here, keep at render() phase.
37 | compile_kwargs=cls.compile_kwargs,
38 | )
39 |
40 | # remove "for" keyword, so all other args can be resolved in render().
41 | if args[0] == "for":
42 | args.pop(0)
43 |
44 | # And apply the compilation afterwards
45 | for i in range(len(args)):
46 | args[i] = parser.compile_filter(args[i])
47 |
48 | cls.validate_args(tag_name, *args, **kwargs)
49 | return cls(tag_name, *args, **kwargs)
50 |
51 | def get_context_data(self, parent_context, *tag_args, **tag_kwargs):
52 | """
53 | The main logic for the inclusion node, analogous to ``@register.inclusion_node``.
54 | """
55 | target_object = tag_args[0] # moved one spot due to .pop(0)
56 | new_context = {
57 | "STATIC_URL": parent_context.get("STATIC_URL", None),
58 | "USE_THREADEDCOMMENTS": appsettings.USE_THREADEDCOMMENTS,
59 | "target_object": target_object,
60 | }
61 |
62 | # Be configuration independent:
63 | if new_context["STATIC_URL"] is None:
64 | try:
65 | request = parent_context["request"]
66 | except KeyError:
67 | new_context.update({"STATIC_URL": settings.STATIC_URL})
68 | else:
69 | new_context.update(context_processors.static(request))
70 |
71 | return new_context
72 |
73 |
74 | @register.tag
75 | def ajax_comment_tags(parser, token):
76 | """
77 | Display the required ``
`` elements to let the Ajax comment functionality work with your form.
78 | """
79 | return AjaxCommentTags.parse(parser, token)
80 |
81 |
82 | register.filter("comments_are_open", comments_are_open)
83 | register.filter("comments_are_moderated", comments_are_moderated)
84 |
85 |
86 | @register.filter
87 | def comments_count(content_object):
88 | """
89 | Return the number of comments posted at a target object.
90 |
91 | You can use this instead of the ``{% get_comment_count for [object] as [varname] %}`` tag.
92 | """
93 | return get_comments_for_model(content_object).count()
94 |
95 |
96 | class FluentCommentsList(Node):
97 | def render(self, context):
98 | # Include proper template, avoid parsing it twice by operating like @register.inclusion_tag()
99 | if not getattr(self, "nodelist", None):
100 | if appsettings.USE_THREADEDCOMMENTS:
101 | template = get_template("fluent_comments/templatetags/threaded_list.html")
102 | else:
103 | template = get_template("fluent_comments/templatetags/flat_list.html")
104 | self.nodelist = template
105 |
106 | # NOTE NOTE NOTE
107 | # HACK: Determine the parent object based on the comment list queryset.
108 | # the {% render_comment_list for article %} tag does not pass the object in a general form to the template.
109 | # Not assuming that 'object.pk' holds the correct value.
110 | #
111 | # This obviously doesn't work when the list is empty.
112 | # To address that, the client-side code also fixes that, by looking for the object ID in the nearby form.
113 | target_object_id = context.get("target_object_id", None)
114 | if not target_object_id:
115 | comment_list = context["comment_list"]
116 | if isinstance(comment_list, list) and comment_list:
117 | target_object_id = comment_list[0].object_pk
118 |
119 | # Render the node
120 | context["USE_THREADEDCOMMENTS"] = appsettings.USE_THREADEDCOMMENTS
121 | context["target_object_id"] = target_object_id
122 |
123 | context = context.flatten()
124 | return self.nodelist.render(context)
125 |
126 |
127 | @register.tag
128 | def fluent_comments_list(parser, token):
129 | """
130 | A tag to select the proper template for the current comments app.
131 | """
132 | return FluentCommentsList()
133 |
134 |
135 | class RenderCommentNode(BaseInclusionNode):
136 | min_args = 1
137 | max_args = 1
138 |
139 | def get_template_name(self, *tag_args, **tag_kwargs):
140 | return get_comment_template_name(comment=tag_args[0])
141 |
142 | def get_context_data(self, parent_context, *tag_args, **tag_kwargs):
143 | context = get_comment_context_data(comment=tag_args[0])
144 | context["request"] = parent_context.get("request")
145 | return context
146 |
147 |
148 | @register.tag
149 | def render_comment(parser, token):
150 | """
151 | Render a single comment.
152 | This tag does not exist in the standard django_comments,
153 | because it only renders a complete list.
154 | """
155 | return RenderCommentNode.parse(parser, token)
156 |
--------------------------------------------------------------------------------
/fluent_comments/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-fluent/django-fluent-comments/5d53578544b5b5ecd8c34719672fd43668fb2dd4/fluent_comments/tests/__init__.py
--------------------------------------------------------------------------------
/fluent_comments/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from django.test import SimpleTestCase
2 |
3 | from fluent_comments.utils import split_words
4 |
5 |
6 | class TestUtils(SimpleTestCase):
7 | def test_split_words(self):
8 | text = """college scholarship essays - how to write a good introduction for a college essay
9 | boston university college essay how to write an essay for college
10 | https://collegeessays.us/
11 | http://www.monkeyface.com/__media__/js/netsoltrademark.php?d=collegeessays.us"""
12 | self.assertEqual(
13 | split_words(text),
14 | {
15 | "__media__",
16 | "a",
17 | "an",
18 | "boston",
19 | "college",
20 | "collegeessays",
21 | "com",
22 | "d",
23 | "essay",
24 | "essays",
25 | "for",
26 | "good",
27 | "how",
28 | "href",
29 | "http",
30 | "https",
31 | "introduction",
32 | "js",
33 | "monkeyface",
34 | "netsoltrademark",
35 | "php",
36 | "scholarship",
37 | "to",
38 | "university",
39 | "us",
40 | "write",
41 | "www",
42 | },
43 | )
44 |
--------------------------------------------------------------------------------
/fluent_comments/tests/utils.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 |
3 | import fluent_comments
4 | from fluent_comments import appsettings
5 | from fluent_comments.moderation import FluentCommentsModerator
6 |
7 |
8 | def override_appsettings(**settings):
9 | """
10 | Temporary override the appsettings.
11 | """
12 |
13 | def _dec(func):
14 | @wraps(func)
15 | def _inner(*args, **kwargs):
16 | # Apply new settings, backup old, clear caches
17 | old_values = {}
18 | for key, new_value in settings.items():
19 | old_values[key] = getattr(appsettings, key)
20 | setattr(appsettings, key, new_value)
21 | _reset_setting_caches()
22 |
23 | func(*args, **kwargs)
24 | for key, old_value in old_values.items():
25 | setattr(appsettings, key, old_value)
26 |
27 | # reset caches
28 | _reset_setting_caches()
29 |
30 | return _inner
31 |
32 | return _dec
33 |
34 |
35 | def _reset_setting_caches():
36 | fluent_comments.form_class = None
37 | fluent_comments.model_class = None
38 | FluentCommentsModerator.close_after = appsettings.FLUENT_COMMENTS_CLOSE_AFTER_DAYS
39 | FluentCommentsModerator.moderate_after = appsettings.FLUENT_COMMENTS_MODERATE_AFTER_DAYS
40 | FluentCommentsModerator.akismet_check = appsettings.FLUENT_CONTENTS_USE_AKISMET
41 | FluentCommentsModerator.akismet_check_action = appsettings.FLUENT_COMMENTS_AKISMET_ACTION
42 | FluentCommentsModerator.moderate_bad_words = set(
43 | appsettings.FLUENT_COMMENTS_MODERATE_BAD_WORDS
44 | )
45 |
46 |
47 | class MockedResponse(object):
48 | def __init__(self, result, definitive=False):
49 | self.result = result
50 | self.headers = {}
51 | if definitive:
52 | self.headers["X-Akismet-Pro-Tip"] = "discard"
53 |
54 | def json(self):
55 | return self.result
56 |
--------------------------------------------------------------------------------
/fluent_comments/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | import django_comments.urls
3 |
4 | from . import views
5 |
6 | urlpatterns = [
7 | path("post/ajax/", views.post_comment_ajax, name="comments-post-comment-ajax"),
8 | path("", include(django_comments.urls)),
9 | ]
10 |
--------------------------------------------------------------------------------
/fluent_comments/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Internal utils
3 | """
4 | import re
5 |
6 | from django.contrib.contenttypes.models import ContentType
7 |
8 | from fluent_comments import appsettings
9 |
10 |
11 | RE_INTERPUNCTION = re.compile(r"\W+")
12 |
13 |
14 | def get_comment_template_name(comment):
15 | """
16 | Internal function for the rendering of comments.
17 | """
18 | ctype = ContentType.objects.get_for_id(comment.content_type_id)
19 | return [
20 | "comments/%s/%s/comment.html" % (ctype.app_label, ctype.model),
21 | "comments/%s/comment.html" % ctype.app_label,
22 | "comments/comment.html",
23 | ]
24 |
25 |
26 | def get_comment_context_data(comment, action=None):
27 | """
28 | Internal function for the rendering of comments.
29 | """
30 | return {
31 | "comment": comment,
32 | "action": action,
33 | "preview": (action == "preview"),
34 | "USE_THREADEDCOMMENTS": appsettings.USE_THREADEDCOMMENTS,
35 | }
36 |
37 |
38 | def split_words(comment):
39 | """
40 | Internal function to split words
41 | """
42 | return set(RE_INTERPUNCTION.sub(" ", comment).split())
43 |
--------------------------------------------------------------------------------
/fluent_comments/views.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | import django_comments
5 | from django.apps import apps
6 | from django.core.exceptions import ObjectDoesNotExist, ValidationError
7 | from django.http import HttpResponse, HttpResponseBadRequest
8 | from django.template.loader import render_to_string
9 | from django.utils.html import escape
10 | from django.views.decorators.csrf import csrf_protect
11 | from django.views.decorators.http import require_POST
12 | from django_comments import signals
13 | from django_comments.views.comments import CommentPostBadRequest
14 |
15 | from fluent_comments.utils import get_comment_template_name, get_comment_context_data
16 | from fluent_comments import appsettings
17 |
18 | if sys.version_info[0] >= 3:
19 | long = int
20 |
21 |
22 | @csrf_protect
23 | @require_POST
24 | def post_comment_ajax(request, using=None):
25 | """
26 | Post a comment, via an Ajax call.
27 | """
28 | is_ajax = request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
29 | if not is_ajax:
30 | return HttpResponseBadRequest("Expecting Ajax call")
31 |
32 | # This is copied from django_comments.
33 | # Basically that view does too much, and doesn't offer a hook to change the rendering.
34 | # The request object is not passed to next_redirect for example.
35 | #
36 | # This is a separate view to integrate both features. Previously this used django-ajaxcomments
37 | # which is unfortunately not thread-safe (it it changes the comment view per request).
38 |
39 | # Fill out some initial data fields from an authenticated user, if present
40 | data = request.POST.copy()
41 | if request.user.is_authenticated:
42 | if not data.get("name", ""):
43 | data["name"] = request.user.get_full_name() or request.user.username
44 | if not data.get("email", ""):
45 | data["email"] = request.user.email
46 |
47 | # Look up the object we're trying to comment about
48 | ctype = data.get("content_type")
49 | object_pk = data.get("object_pk")
50 | if ctype is None or object_pk is None:
51 | return CommentPostBadRequest("Missing content_type or object_pk field.")
52 | try:
53 | model = apps.get_model(*ctype.split(".", 1))
54 | target = model._default_manager.using(using).get(pk=object_pk)
55 | except ValueError:
56 | return CommentPostBadRequest("Invalid object_pk value: {0}".format(escape(object_pk)))
57 | except (TypeError, LookupError):
58 | return CommentPostBadRequest("Invalid content_type value: {0}".format(escape(ctype)))
59 | except AttributeError:
60 | return CommentPostBadRequest(
61 | "The given content-type {0} does not resolve to a valid model.".format(escape(ctype))
62 | )
63 | except ObjectDoesNotExist:
64 | return CommentPostBadRequest(
65 | "No object matching content-type {0} and object PK {1} exists.".format(
66 | escape(ctype), escape(object_pk)
67 | )
68 | )
69 | except (ValueError, ValidationError) as e:
70 | return CommentPostBadRequest(
71 | "Attempting go get content-type {0!r} and object PK {1!r} exists raised {2}".format(
72 | escape(ctype), escape(object_pk), e.__class__.__name__
73 | )
74 | )
75 |
76 | # Do we want to preview the comment?
77 | is_preview = "preview" in data
78 |
79 | # Construct the comment form
80 | form = django_comments.get_form()(target, data=data, is_preview=is_preview)
81 |
82 | # Check security information
83 | if form.security_errors():
84 | return CommentPostBadRequest(
85 | "The comment form failed security verification: {0}".format(form.security_errors())
86 | )
87 |
88 | # If there are errors or if we requested a preview show the comment
89 | if is_preview:
90 | comment = form.get_comment_object() if not form.errors else None
91 | return _ajax_result(request, form, "preview", comment, object_id=object_pk)
92 | if form.errors:
93 | return _ajax_result(request, form, "post", object_id=object_pk)
94 |
95 | # Otherwise create the comment
96 | comment = form.get_comment_object()
97 | comment.ip_address = request.META.get("REMOTE_ADDR", None)
98 | if request.user.is_authenticated:
99 | comment.user = request.user
100 |
101 | # Signal that the comment is about to be saved
102 | responses = signals.comment_will_be_posted.send(
103 | sender=comment.__class__, comment=comment, request=request
104 | )
105 |
106 | for (receiver, response) in responses:
107 | if response is False:
108 | return CommentPostBadRequest(
109 | "comment_will_be_posted receiver {0} killed the comment".format(receiver.__name__)
110 | )
111 |
112 | # Save the comment and signal that it was saved
113 | comment.save()
114 | signals.comment_was_posted.send(sender=comment.__class__, comment=comment, request=request)
115 |
116 | return _ajax_result(request, form, "post", comment, object_id=object_pk)
117 |
118 |
119 | def _ajax_result(request, form, action, comment=None, object_id=None):
120 | # Based on django-ajaxcomments, BSD licensed.
121 | # Copyright (c) 2009 Brandon Konkle and individual contributors.
122 | #
123 | # This code was extracted out of django-ajaxcomments because
124 | # django-ajaxcomments is not threadsafe, and it was refactored afterwards.
125 |
126 | success = True
127 | json_errors = {}
128 |
129 | if form.errors:
130 | for field_name in form.errors:
131 | field = form[field_name]
132 | json_errors[field_name] = _render_errors(field)
133 | success = False
134 |
135 | json_return = {
136 | "success": success,
137 | "action": action,
138 | "errors": json_errors,
139 | "object_id": object_id,
140 | "use_threadedcomments": bool(appsettings.USE_THREADEDCOMMENTS),
141 | }
142 |
143 | if comment is not None:
144 | # Render the comment, like {% render_comment comment %} does
145 | context = get_comment_context_data(comment, action)
146 | context["request"] = request
147 | template_name = get_comment_template_name(comment)
148 |
149 | comment_html = render_to_string(template_name, context, request=request)
150 | json_return.update(
151 | {
152 | "html": comment_html,
153 | "comment_id": comment.id,
154 | "parent_id": None,
155 | "is_moderated": not comment.is_public, # is_public flags changes in comment_will_be_posted
156 | }
157 | )
158 | if appsettings.USE_THREADEDCOMMENTS:
159 | json_return["parent_id"] = comment.parent_id
160 |
161 | json_response = json.dumps(json_return)
162 | return HttpResponse(json_response, content_type="application/json")
163 |
164 |
165 | def _render_errors(field):
166 | """
167 | Render form errors in crispy-forms style.
168 | """
169 | template = "{0}/layout/field_errors.html".format(appsettings.CRISPY_TEMPLATE_PACK)
170 | return render_to_string(
171 | template,
172 | {
173 | "field": field,
174 | "form_show_errors": True,
175 | },
176 | )
177 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.isort]
2 | profile = "black"
3 | line_length = 99
4 |
5 | [tool.black]
6 | line-length = 99
7 | exclude = '''
8 | /(
9 | \.git
10 | | \.tox
11 | | \.venv
12 | | dist
13 | )/
14 | '''
15 |
--------------------------------------------------------------------------------
/runtests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | python -c "
4 | import sys, django, os
5 | sys.stderr.write('Using Python version {0} from {1}\n'.format(sys.version[:5], sys.executable))
6 | sys.stderr.write('Using Django version {0} from {1}\n'.format(
7 | django.get_version(), os.path.dirname(os.path.abspath(django.__file__))))
8 | "
9 |
10 | exec python -Wd example/manage.py test "$@"
11 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | # create "py2.py3-none-any.whl" package
3 | universal = 1
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from setuptools import setup, find_packages
3 | from os import path
4 | import codecs
5 | import os
6 | import re
7 | import sys
8 |
9 |
10 | # When creating the sdist, make sure the django.mo file also exists:
11 | if "sdist" in sys.argv or "develop" in sys.argv:
12 | os.chdir("fluent_comments")
13 | try:
14 | from django.core import management
15 |
16 | management.call_command("compilemessages", stdout=sys.stderr, verbosity=1)
17 | except ImportError:
18 | if "sdist" in sys.argv:
19 | raise
20 | finally:
21 | os.chdir("..")
22 |
23 |
24 | def read(*parts):
25 | file_path = path.join(path.dirname(__file__), *parts)
26 | return codecs.open(file_path, encoding="utf-8").read()
27 |
28 |
29 | def find_version(*parts):
30 | version_file = read(*parts)
31 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
32 | if version_match:
33 | return str(version_match.group(1))
34 | raise RuntimeError("Unable to find version string.")
35 |
36 |
37 | setup(
38 | name="django-fluent-comments",
39 | version=find_version("fluent_comments", "__init__.py"),
40 | license="Apache 2.0",
41 | install_requires=[
42 | "django-crispy-forms>=1.9.2",
43 | "django-tag-parser>=3.2",
44 | "django-contrib-comments>=2.0.0",
45 | "python-akismet>=0.4.1", # Python 3 port, replaces Python 2-only "akismet" library.
46 | ],
47 | requires=[
48 | "Django (>=2.2)",
49 | ],
50 | extras_require={
51 | "threadedcomments": ["django-threadedcomments>=1.2"],
52 | },
53 | description="A modern, ajax-based appearance for django_comments",
54 | long_description=read("README.rst"),
55 | author="Diederik van der Boor",
56 | author_email="opensource@edoburu.nl",
57 | url="https://github.com/edoburu/django-fluent-comments",
58 | download_url="https://github.com/edoburu/django-fluent-comments/zipball/master",
59 | packages=find_packages(exclude=("example*",)),
60 | include_package_data=True,
61 | zip_safe=False,
62 | classifiers=[
63 | "Development Status :: 5 - Production/Stable",
64 | "Environment :: Web Environment",
65 | "Framework :: Django",
66 | "Intended Audience :: Developers",
67 | "License :: OSI Approved :: Apache Software License",
68 | "Operating System :: OS Independent",
69 | "Programming Language :: Python",
70 | "Programming Language :: Python :: 3.7",
71 | "Programming Language :: Python :: 3.8",
72 | "Programming Language :: Python :: 3.9",
73 | "Framework :: Django",
74 | "Framework :: Django :: 2.2",
75 | "Framework :: Django :: 3.0",
76 | "Framework :: Django :: 3.1",
77 | "Framework :: Django :: 3.2",
78 | "Topic :: Internet :: WWW/HTTP",
79 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
80 | "Topic :: Software Development :: Libraries :: Application Frameworks",
81 | "Topic :: Software Development :: Libraries :: Python Modules",
82 | ],
83 | )
84 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=
3 | py38-django{22,31,32},
4 | py38-django{22,30,31,32}-tc
5 |
6 | [testenv]
7 | deps =
8 | django22: Django~=2.2
9 | django30: Django~=3.0
10 | django31: Django~=3.1
11 | django32: Django~=3.2
12 | tc: django-threadedcomments >= 1.2
13 | django-dev: https://github.com/django/django/tarball/master
14 | commands=
15 | ./runtests.sh
16 |
--------------------------------------------------------------------------------
{% trans "Preview of your comment" %}
{% endif %} 15 |16 | {% block comment_title %} 17 | {% if comment.url %}{% endif %} 18 | {% if comment.name %}{{ comment.name }}{% else %}{% trans "Anonymous" %}{% endif %}{% comment %} 19 | {% endcomment %}{% if comment.url %}{% endif %} 20 | {% blocktrans with submit_date=comment.submit_date %}on {{ submit_date }}{% endblocktrans %} 21 | {% if not comment.is_public %}({% trans "moderated" %}){% endif %} 22 | {% if USE_THREADEDCOMMENTS and not preview %} {% endif %} 23 | {% endblock %} 24 |
25 | 26 |