diff --git a/comment/conf/defaults.py b/comment/conf/defaults.py index 1482d66..49ce476 100644 --- a/comment/conf/defaults.py +++ b/comment/conf/defaults.py @@ -39,3 +39,7 @@ COMMENT_ALLOW_BLOCKING_USERS = False COMMENT_ALLOW_MODERATOR_TO_BLOCK = False COMMENT_RESPONSE_FOR_BLOCKED_USER = 'You cannot perform this action at the moment! Contact the admin for more details' + +COMMENT_ALLOW_MARKDOWN = False +COMMENT_MARKDOWN_EXTENSIONS = ['markdown.extensions.fenced_code'] +COMMENT_MARKDOWN_EXTENSION_CONFIG = {} diff --git a/comment/context.py b/comment/context.py index 7bd04dc..f231522 100644 --- a/comment/context.py +++ b/comment/context.py @@ -56,5 +56,6 @@ def __call__(self): 'is_translation_allowed': settings.COMMENT_ALLOW_TRANSLATION, 'is_subscription_allowed': settings.COMMENT_ALLOW_SUBSCRIPTION, 'is_blocking_allowed': settings.COMMENT_ALLOW_BLOCKING_USERS, - 'oauth': self.is_oauth() + 'oauth': self.is_oauth(), + 'render_markdown': settings.COMMENT_ALLOW_MARKDOWN, } diff --git a/comment/templates/comment/comments/comment_content.html b/comment/templates/comment/comments/comment_content.html index e2287cf..66b4cda 100644 --- a/comment/templates/comment/comments/comment_content.html +++ b/comment/templates/comment/comments/comment_content.html @@ -3,7 +3,11 @@
\3', content) -def render_content(comment, number=None): +def _render_markdown(content): + try: + import markdown as md + except ModuleNotFoundError: + raise ImproperlyConfigured( + 'Comment App: Cannot render content in markdown format because markdown extension is not available.' + 'You can install it by visting https://pypi.org/p/markdown or by using the command ' + '"python -m pip install django-comments-dab[markdown]".' + ) + else: + return md.markdown( + conditional_escape(content), + extensions=settings.COMMENT_MARKDOWN_EXTENSIONS, + extension_config=settings.COMMENT_MARKDOWN_EXTENSION_CONFIG + ) + + +def render_content(comment, number=None, **kwargs): + markdown = kwargs.get('markdown', False) + if markdown: + if number: + warnings.warn( + ( + 'The argument number is ignored when markdown is set to "True".' + 'No wrapping will take place for markdown formatted content.' + ), + RuntimeWarning, + ) + return { + 'text_1': mark_safe(_render_markdown(comment.content)), + 'text_2': '', + 'urlhash': comment.urlhash, + } + try: number = int(number) except (ValueError, TypeError): diff --git a/comment/tests/test_template_tags.py b/comment/tests/test_template_tags.py index 2a05883..4bc7668 100644 --- a/comment/tests/test_template_tags.py +++ b/comment/tests/test_template_tags.py @@ -1,3 +1,4 @@ +import sys from unittest.mock import patch from django.core.exceptions import ImproperlyConfigured @@ -164,6 +165,7 @@ def test_urlhash(self): self.assertEqual(result['urlhash'], self.comment.urlhash) @patch.object(settings, 'COMMENT_WRAP_CONTENT_WORDS', 20) + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', False) def test_content_wrapping_with_large_truncate_number(self): content_words = self.comment.content.split() self.assertIs(len(content_words) < 20, True) @@ -173,6 +175,7 @@ def test_content_wrapping_with_large_truncate_number(self): self.assertEqual(result['text_1'], self.comment.content) self.assertIsNone(result['text_2']) + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', False) def test_single_line_breaks(self): comment = self.parent_comment_1 comment.content = "Any long text\njust for testing render\ncontent function" @@ -184,6 +187,7 @@ def test_single_line_breaks(self): self.assertIn('
', result['text_1']) self.assertNotIn('
', result['text_1']) + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', False) def test_multiple_line_breaks(self): comment = self.parent_comment_1 comment.content = "Any long text\n\njust for testing render\n\n\ncontent function" @@ -196,6 +200,7 @@ def test_multiple_line_breaks(self): self.assertNotIn('
', result['text_1']) @patch.object(settings, 'COMMENT_WRAP_CONTENT_WORDS', 5) + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', False) def test_content_wrapping_with_small_truncate_number(self): self.comment.refresh_from_db() content_words = self.comment.content.split() @@ -207,6 +212,48 @@ def test_content_wrapping_with_small_truncate_number(self): self.assertEqual(result['text_1'], ' '.join(content_words[:5])) self.assertEqual(result['text_2'], ' '.join(content_words[5:])) + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', True) + def test_raises_runtime_warning_passing_number_with_markdown_set_to_true(self): + msg = ( + 'The argument number is ignored when markdown is set to "True".' + 'No wrapping will take place for markdown formatted content.' + ) + + with self.assertWarnsMessage(RuntimeWarning, msg): + result = render_content(self.comment, number=2, markdown=True) + + # The content is surrounded by
tag to cater for connditional escaping which prevents from XSS attacks. + self.assertEqual(result['text_1'], f'
{self.comment.content}
') + self.assertEqual(result['text_2'], '') + + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', True) + def test_raises_improperly_configured_error_with_markdown_not_installed_and_markdown_set_to_true(self): + with patch.dict(sys.modules, {'markdown': None}): + from importlib import reload + reload(sys.modules['comment.templatetags.comment_tags']) + from comment.templatetags.comment_tags import render_content + + msg = ( + 'Comment App: Cannot render content in markdown format because markdown extension is not available.' + 'You can install it by visting https://pypi.org/p/markdown or by using the command ' + '"python -m pip install django-comments-dab[markdown]".' + ) + + with self.assertRaisesMessage(ImproperlyConfigured, msg): + render_content(self.comment, markdown=True) + + @patch.object(settings, 'COMMENT_ALLOW_MARKDOWN', True) + def test_rendering_markdown_content(self): + self.comment.content = '### Hi\n_italic_' + self.comment.save() + self.comment.refresh_from_db() + + result = render_content(self.comment, markdown=True) + + self.assertEqual(result['text_1'], 'Hi
\nitalic
') + self.assertEqual(result['text_2'], '') + self.assertEqual(result['urlhash'], self.comment.urlhash) + class GetUsernameForCommentTest(BaseTemplateTagsTest): @classmethod diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 225e1f9..03e4a9c 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -184,3 +184,33 @@ COMMENT_RESPONSE_FOR_BLOCKED_USER ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The response message for blocking reason. Default to ``You cannot perform this action at the moment! Contact the admin for more details`` + +COMMENT_ALLOW_MARKDOWN +^^^^^^^^^^^^^^^^^^^^^^ + +Enable rendering comment content in markdown format. Defaults to ``False``. + +.. note:: + + When ``markdown`` format is being used to render content, no content wrapping is done. Passing a value for wrapping to the ``render_content`` template tag in such situations will raise a ``RuntimeWarning``. + + +.. _settings.comment_markdown_extensions: + +COMMENT_MARKDOWN_EXTENSIONS +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The list of extensions to be used for the rendering the ``markdown``. Defaults to ``['markdown.extensions.fenced_code']``. See `python markdown's documentation`_ for more information on this. + +.. note:: + + Both ``COMMENT_MARKDOWN_EXTENSIONS`` and ``COMMENT_MARKDOWN_EXTENSION_CONFIG`` will only be used when ``COMMENT_ALLOW_MARKDOWN`` is set to ``True``. + +.. _python markdown's documentation: https://python-markdown.github.io/extensions/extra/ + +.. _settings.comment_markdown_extension_config: + +COMMENT_MARKDOWN_EXTENSION_CONFIGS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration used for markdown-extensions. Defaults to ``{}``. See `python markdown's documentation`_ for more information. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 9c6e001..e4f6230 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -217,3 +217,16 @@ Blocking functionality is added in version 2.7.0. It allows moderators to block To enable blocking system set ``COMMENT_ALLOW_BLOCKING_USERS`` in ``settings`` to ``True``. This will grant access for the **admins** only to block users. However, in order to give the **moderators** this right, you need to add ``COMMENT_ALLOW_MODERATOR_TO_BLOCK = True`` to `settings` + + +8. Enable Markdown format +^^^^^^^^^^^^^^^^^^^^^^^^^ + +This functionality was added in version ``2.8.0``. It allows comment content to be rendered using the power of ``markdown`` format. + +To use this: + - Install additional dependency `python-markdown`_ may be installed using ``python -m pip install django-comments-dab[markdown]``. + - To enable set ``COMMENT_ALLOW_MARKDOWN`` to ``True`` in your ``settings`` file. + - For advanced configuration, you may use :ref:`settings.comment_markdown_extensions` and :ref:`settings.comment_markdown_extension_config`. + +.. _python-markdown: https://pypi.org/p/markdown diff --git a/setup.cfg b/setup.cfg index f5bee04..ef1b85a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,9 @@ include_package_data = True install_requires = django zip_safe = False +[options.extras_require] +markdown = markdown + [options.packages.find] exclude = docs diff --git a/test/settings/base.py b/test/settings/base.py index ac0505c..3c76585 100644 --- a/test/settings/base.py +++ b/test/settings/base.py @@ -119,3 +119,5 @@ COMMENT_ALLOW_BLOCKING_USERS = True COMMENT_ALLOW_MODERATOR_TO_BLOCK = True + +COMMENT_ALLOW_MARKDOWN = True diff --git a/tox.ini b/tox.ini index 44bcccc..c9053b8 100644 --- a/tox.ini +++ b/tox.ini @@ -43,6 +43,9 @@ deps = django32: Django>=3.2,<4.0 djangomain: https://github.com/django/django/archive/main.tar.gz +extras = + markdown + usedevelop = True commands =