Skip to content

Commit

Permalink
feat(#153): Add support for rendering content in markdown format
Browse files Browse the repository at this point in the history
  • Loading branch information
abhiabhi94 committed Aug 18, 2021
1 parent 3b54a8a commit e1ab5ab
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -9,7 +9,7 @@ python:
- 3.9
env:
- DJANGO=2.1
- DJANGO=3.1
- DJANGO=3.2
- DJANGO=main

install: pip install tox-travis coveralls
Expand Down
4 changes: 4 additions & 0 deletions comment/conf/defaults.py
Expand Up @@ -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 = {}
3 changes: 2 additions & 1 deletion comment/context.py
Expand Up @@ -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,
}
6 changes: 5 additions & 1 deletion comment/templates/comment/comments/comment_content.html
Expand Up @@ -3,7 +3,11 @@

<div id="{{ comment.urlhash }}" class="js-updated-comment {% if comment.has_flagged_state %}flagged-comment {% endif %}{% block content_wrapper_cls %}{% if has_valid_profile %}col-9 col-md-10{% else %}co-11 mx-3{% endif %}{% endblock content_wrapper_cls %}" >
{% block comment_content %}
{% render_content comment %}
{% if render_markdown %}
{% render_content comment markdown=True %}
{% else %}
{% render_content comment markdown=False %}
{% endif %}
{% endblock comment_content %}
{% get_username_for_comment comment as username %}
<div class="{% block footer_wrapper_cls %}mt-2 text-muted{% endblock footer_wrapper_cls %}">
Expand Down
37 changes: 36 additions & 1 deletion comment/templatetags/comment_tags.py
Expand Up @@ -4,6 +4,7 @@
from django import template
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from django.core.exceptions import ImproperlyConfigured

from comment.models import ReactionInstance, FlagInstance, Follower, BlockedUser
from comment.forms import CommentForm
Expand All @@ -14,6 +15,8 @@
from comment.managers import FlagInstanceManager
from comment.messages import ReactionError
from comment.context import DABContext
from comment.conf import settings


MULTIPLE_NEW_LINE_RE = re.compile(r'(.*)(\n){2,}(.*)')
SINGLE_NEW_LINE_RE = re.compile(r'(.*)(\n)(.*)')
Expand Down Expand Up @@ -97,7 +100,39 @@ def _restrict_line_breaks(content):
return SINGLE_NEW_LINE_RE.sub(r'\1<br>\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, *, 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):
Expand Down
47 changes: 47 additions & 0 deletions comment/tests/test_template_tags.py
@@ -1,3 +1,4 @@
import sys
from unittest.mock import patch

from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -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)
Expand All @@ -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"
Expand All @@ -184,6 +187,7 @@ def test_single_line_breaks(self):
self.assertIn('<br>', result['text_1'])
self.assertNotIn('<br><br>', 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"
Expand All @@ -196,6 +200,7 @@ def test_multiple_line_breaks(self):
self.assertNotIn('<br><br><br>', 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()
Expand All @@ -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 <p> tag to cater for connditional escaping which prevents from XSS attacks.
self.assertEqual(result['text_1'], f'<p>{self.comment.content}</p>')
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'], '<h3>Hi</h3>\n<p><em>italic</em></p>')
self.assertEqual(result['text_2'], '')
self.assertEqual(result['urlhash'], self.comment.urlhash)


class GetUsernameForCommentTest(BaseTemplateTagsTest):
@classmethod
Expand Down
30 changes: 30 additions & 0 deletions docs/source/settings.rst
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions docs/source/usage.rst
Expand Up @@ -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
3 changes: 3 additions & 0 deletions setup.cfg
Expand Up @@ -39,6 +39,9 @@ include_package_data = True
install_requires = django
zip_safe = False

[options.extras_require]
markdown = markdown

[options.packages.find]
exclude =
docs
Expand Down
2 changes: 2 additions & 0 deletions test/settings/base.py
Expand Up @@ -119,3 +119,5 @@

COMMENT_ALLOW_BLOCKING_USERS = True
COMMENT_ALLOW_MODERATOR_TO_BLOCK = True

COMMENT_ALLOW_MARKDOWN = True
3 changes: 3 additions & 0 deletions tox.ini
Expand Up @@ -32,6 +32,9 @@ deps =
django32: Django>=3.2,<4.0
djangomain: https://github.com/django/django/archive/main.tar.gz

extras =
markdown

usedevelop = True
ignore_outcome =
djangomain: True
Expand Down

0 comments on commit e1ab5ab

Please sign in to comment.