~netlandish/django-wiki

05123abfa3a4a7fd796cef443a7026123d5f36df — Mathias Rav 6 years ago b970bed + 122d38b
Merge remote-tracking branch 'origin/master' into txbug
42 files changed, 368 insertions(+), 309 deletions(-)

M .travis.yml
M Makefile
M README.rst
M docs/development/testing.rst
M docs/development/testproject.rst
M docs/index.rst
M docs/settings.rst
M setup.py
M src/wiki/__init__.py
M src/wiki/conf/settings.py
M src/wiki/locale/da/LC_MESSAGES/django.mo
M src/wiki/locale/en/LC_MESSAGES/django.po
M src/wiki/locale/ja/LC_MESSAGES/django.mo
M src/wiki/locale/zh_CN/LC_MESSAGES/django.mo
M src/wiki/plugins/images/apps.py
M src/wiki/plugins/images/markdown_extensions.py
M src/wiki/plugins/images/models.py
M src/wiki/plugins/macros/mdx/macro.py
M src/wiki/plugins/macros/mdx/toc.py
M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py
M src/wiki/plugins/redlinks/mdx/redlinks.py
M src/wiki/sites.py
M src/wiki/static/wiki/bootstrap/LICENSE
M src/wiki/static/wiki/bootstrap/fonts/glyphicons-halflings-regular.svg
M src/wiki/static/wiki/font-awesome/font/fontawesome-webfont.svg
M src/wiki/templatetags/wiki_tags.py
M src/wiki/urls.py
M src/wiki/views/article.py
M testproject/testproject/db/prepopulated.db
M testproject/testproject/settings/base.py
M tests/core/test_checks.py
M tests/core/test_markdown.py
M tests/core/test_template_filters.py
M tests/core/test_views.py
M tests/plugins/attachments/test_models.py
M tests/plugins/images/test_forms.py
A tests/plugins/images/test_markdown.py
M tests/plugins/images/test_views.py
M tests/plugins/links/test_urlize.py
A tests/plugins/macros/test_macro.py
M tests/plugins/redlinks/test_redlinks.py
M tests/testdata/urls.py
M .travis.yml => .travis.yml +0 -2
@@ 16,8 16,6 @@ env:

matrix:
  exclude:
  - python: "3.4"
    env: LINT="yes"
  - python: "3.5"
    env: LINT="yes"
  - python: "3.6"

M Makefile => Makefile +3 -1
@@ 60,7 60,9 @@ coverage:  ## Generate test coverage report
	coverage run --source wiki setup.py test
	coverage report -m

translation-push:  ## Pushes translation sources
translation-push:  ## Updates and pushes
	cd src/wiki && django-admin makemessages -l en
	cd ..
	tx push -s

translation-pull:  ## Pulls translation languages

M README.rst => README.rst +1 -1
@@ 26,7 26,7 @@ The below table explains which Django versions are supported.
+------------------+----------------+--------------+
| Release          | Django         | Upgrade from |
+==================+================+==============+
| 0.4a5            | 1.11, 2.0      | 0.3          |
| 0.4b1            | 1.11, 2.0      | 0.3          |
+------------------+----------------+--------------+
| 0.3.x            | 1.8, 1.9,      | 0.2          |
|                  | 1.10, 1.11     |              |

M docs/development/testing.rst => docs/development/testing.rst +0 -1
@@ 62,4 62,3 @@ fixtures for tests e.g. a root article.
  Javascript, and can be tested using the fast WebTest method, rather than
  relying on the slow and fragile Selenium method. Selenium tests are not run by
  default.


M docs/development/testproject.rst => docs/development/testproject.rst +0 -1
@@ 8,4 8,3 @@ an sqlite database. Login for django admin is ``admin:admin``. This
project should always be maintained, but please do not commit changes to
the SQLite database as we only care about its contents in case data
models are changed.


M docs/index.rst => docs/index.rst +0 -1
@@ 26,4 26,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`


M docs/settings.rst => docs/settings.rst +0 -1
@@ 33,4 33,3 @@ Plugin macros

.. automodule:: wiki.plugins.macros.settings
   :members:


M setup.py => setup.py +1 -0
@@ 44,6 44,7 @@ test_requirements = [

test_lint_requirements = [
    'flake8>=3.5,<3.6',
    'flake8-isort',
]

setup_requirements = [

M src/wiki/__init__.py => src/wiki/__init__.py +1 -2
@@ 17,8 17,7 @@

from wiki.core.version import get_version


default_app_config = 'wiki.apps.WikiConfig'

VERSION = (0, 4, 0, 'alpha', 5)
VERSION = (0, 4, 0, 'beta', 1)
__version__ = get_version(VERSION)

M src/wiki/conf/settings.py => src/wiki/conf/settings.py +0 -1
@@ 1,5 1,4 @@
import bleach
from django.apps import apps
from django.conf import settings as django_settings
from django.contrib.messages import constants as messages
from django.core.files.storage import default_storage

M src/wiki/locale/da/LC_MESSAGES/django.mo => src/wiki/locale/da/LC_MESSAGES/django.mo +0 -0
M src/wiki/locale/en/LC_MESSAGES/django.po => src/wiki/locale/en/LC_MESSAGES/django.po +76 -104
@@ 8,7 8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-26 16:02+0200\n"
"POT-Creation-Date: 2018-08-05 14:47+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"


@@ 25,8 25,8 @@ msgstr ""
msgid "Wiki"
msgstr ""

#: conf/settings.py:50
msgid "Table of Contents"
#: conf/settings.py:63 forms.py:198 forms.py:342
msgid "Contents"
msgstr ""

#: core/plugins/base.py:63


@@ 115,10 115,6 @@ msgstr ""
msgid "Create a redirect page for every moved article?"
msgstr ""

#: forms.py:198 forms.py:342
msgid "Contents"
msgstr ""

#: forms.py:203 forms.py:347
msgctxt "Revision comment"
msgid "Summary"


@@ 330,52 326,52 @@ msgstr ""
msgid "others write access"
msgstr ""

#: models/article.py:178
#: models/article.py:179
#, python-format
msgid "Article without content (%(id)d)"
msgstr ""

#: models/article.py:183
#: models/article.py:184
msgid "Can edit all articles and lock/unlock/restore"
msgstr ""

#: models/article.py:184
#: models/article.py:185
msgid "Can change ownership of any article"
msgstr ""

#: models/article.py:185
#: models/article.py:186
msgid "Can assign permissions to other users"
msgstr ""

#: models/article.py:260
#: models/article.py:261
msgid "content type"
msgstr ""

#: models/article.py:262
#: models/article.py:263
msgid "object ID"
msgstr ""

#: models/article.py:271
#: models/article.py:272
msgid "Article for object"
msgstr ""

#: models/article.py:272
#: models/article.py:273
msgid "Articles for object"
msgstr ""

#: models/article.py:284
#: models/article.py:285
msgid "revision number"
msgstr ""

#: models/article.py:290
#: models/article.py:291
msgid "IP address"
msgstr ""

#: models/article.py:294
#: models/article.py:295
msgid "user"
msgstr ""

#: models/article.py:309
#: models/article.py:310
#: plugins/attachments/templates/wiki/plugins/attachments/history.html:23
#: plugins/attachments/templates/wiki/plugins/attachments/index.html:25
#: plugins/attachments/templates/wiki/plugins/attachments/search.html:44


@@ 384,25 380,25 @@ msgstr ""
msgid "deleted"
msgstr ""

#: models/article.py:313
#: models/article.py:314
#: plugins/globalhistory/templates/wiki/plugins/globalhistory/globalhistory.html:74
#: templates/wiki/article.html:23 templates/wiki/includes/revision_info.html:21
msgid "locked"
msgstr ""

#: models/article.py:351 models/pluginbase.py:44 models/urlpath.py:56
#: models/article.py:352 models/pluginbase.py:44 models/urlpath.py:56
msgid "article"
msgstr ""

#: models/article.py:354
#: models/article.py:355
msgid "article contents"
msgstr ""

#: models/article.py:359
#: models/article.py:360
msgid "article title"
msgstr ""

#: models/article.py:362
#: models/article.py:363
msgid ""
"Each revision contains a title field that must be filled out, even if the "
"title has not changed"


@@ 609,10 605,8 @@ msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/delete.html:40
msgid ""
"\n"
"    You can remove a reference to a file, but it will retain its references "
"on other articles.\n"
"    "
"You can remove a reference to a file, but it will retain its references on "
"other articles."
msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/delete.html:55


@@ 680,12 674,9 @@ msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/index.html:11
msgid ""
"\n"
"      Complete markdown code syntax: <code>[attachment:id title:\"text\" "
"size]</code><br>\n"
"      &nbsp;&nbsp;title: Link text replacement for the file name.\n"
"      size: Show file size after the title.\n"
"    "
"Complete markdown code syntax: <code>[attachment:id title:\"text\" size]</"
"code><br> &nbsp;&nbsp;title: Link text replacement for the file name. size: "
"Show file size after the title."
msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/index.html:32


@@ 771,33 762,26 @@ msgstr ""
#: plugins/attachments/templates/wiki/plugins/attachments/replace.html:12
#, python-format
msgid ""
"\n"
"  Replacing an attachment means adding a new file that will be used in its "
"Replacing an attachment means adding a new file that will be used in its "
"place. All references to the file will be replaced by the one you upload and "
"the file will be downloaded as <strong>%(filename)s</strong>. Please note "
"that this attachment is in use on other articles, you may distort contents. "
"However, do not hestitate to take advantage of this and make replacements "
"for the listed articles where necessary. This way of working is more "
"efficient....\n"
"  "
"efficient...."
msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/replace.html:17
#, python-format
msgid ""
"\n"
"  Articles using %(filename)s\n"
"  "
msgid "Articles using %(filename)s"
msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/replace.html:26
#, python-format
msgid ""
"\n"
"  Replacing an attachment means adding a new file that will be used in its "
"Replacing an attachment means adding a new file that will be used in its "
"place. All references to the file will be replaced by the one you upload and "
"the file will be downloaded as <strong>%(filename)s</strong>.\n"
"  "
"the file will be downloaded as <strong>%(filename)s</strong>."
msgstr ""

#: plugins/attachments/templates/wiki/plugins/attachments/replace.html:43


@@ 903,14 887,8 @@ msgstr ""

#: plugins/globalhistory/templates/wiki/plugins/globalhistory/globalhistory.html:14
#, python-format
msgid ""
"\n"
"        List of <strong>%(cnt)s change</strong> in the wiki.\n"
"        "
msgid_plural ""
"\n"
"        List of <strong>%(cnt)s changes</strong> in the wiki.\n"
"      "
msgid "List of <strong>%(cnt)s change</strong> in the wiki."
msgid_plural "List of <strong>%(cnt)s changes</strong> in the wiki."
msgstr[0] ""
msgstr[1] ""



@@ 1263,31 1241,31 @@ msgstr ""
msgid "Wiki macros"
msgstr ""

#: plugins/macros/mdx/macro.py:79
#: plugins/macros/mdx/macro.py:67
msgid "Article list"
msgstr ""

#: plugins/macros/mdx/macro.py:80
#: plugins/macros/mdx/macro.py:68
msgid "Insert a list of articles in this level."
msgstr ""

#: plugins/macros/mdx/macro.py:82
#: plugins/macros/mdx/macro.py:70
msgid "Maximum depth to show levels for."
msgstr ""

#: plugins/macros/mdx/macro.py:88
#: plugins/macros/mdx/macro.py:76
msgid "Table of contents"
msgstr ""

#: plugins/macros/mdx/macro.py:89
#: plugins/macros/mdx/macro.py:77
msgid "Insert a table of contents matching the headings."
msgstr ""

#: plugins/macros/mdx/macro.py:97
#: plugins/macros/mdx/macro.py:85
msgid "WikiLinks"
msgstr ""

#: plugins/macros/mdx/macro.py:99
#: plugins/macros/mdx/macro.py:87
msgid "Insert a link to another wiki page with a short notation."
msgstr ""



@@ 1299,7 1277,7 @@ msgstr ""
msgid "Nothing below this level"
msgstr ""

#: plugins/macros/wiki_plugin.py:14
#: plugins/macros/wiki_plugin.py:11
msgid "Macros"
msgstr ""



@@ 1430,6 1408,10 @@ msgstr ""
msgid "You will receive notifications %(interval)s for %(articles)d articles"
msgstr ""

#: plugins/redlinks/apps.py:7
msgid "Wiki red links"
msgstr ""

#: templates/wiki/accounts/account_settings.html:4
#: templates/wiki/accounts/account_settings.html:7
#: templates/wiki/base_site.html:79


@@ 1638,11 1620,9 @@ msgstr ""
#: templates/wiki/dir.html:40
#, python-format
msgid ""
"\n"
"      Browsing <strong><a href=\"%(self_url)s\">/%(path)s</a></strong>. "
"There %(articles_plur_verb)s <strong>%(cnt)s %(articles_plur)s</strong> in "
"this level.\n"
"    "
"Browsing <strong><a href=\"%(self_url)s\">/%(path)s</a></strong>. There "
"%(articles_plur_verb)s <strong>%(cnt)s %(articles_plur)s</strong> in this "
"level."
msgstr ""

#: templates/wiki/dir.html:50 templates/wiki/search.html:36


@@ 1760,10 1740,8 @@ msgstr ""
#: templates/wiki/includes/anonymous_blocked.html:7
#, python-format
msgid ""
"\n"
"  You need to <a href=\"%(login_url)s\">log in</a> or <a href="
"\"%(signup_url)s\">sign up</a> to use this function.\n"
"  "
"You need to <a href=\"%(login_url)s\">log in</a> or <a href=\"%(signup_url)s"
"\">sign up</a> to use this function."
msgstr ""

#: templates/wiki/includes/anonymous_blocked.html:11


@@ 1820,17 1798,11 @@ msgstr ""
#: templates/wiki/move.html:23
#, python-format
msgid ""
"\n"
"          %(cnt)s nested article will also be moved.<br>\n"
"          Be careful, links to this article and %(cnt)s article nested\n"
"          in this hierarchy will not be updated.\n"
"          "
"%(cnt)s nested article will also be moved.<br> Be careful, links to this "
"article and %(cnt)s article nested in this hierarchy will not be updated."
msgid_plural ""
"\n"
"          %(cnt)s nested articles will also be moved.<br>\n"
"          Be careful, links to this article and %(cnt)s articles nested\n"
"          in this hierarchy will not be updated.\n"
"        "
"%(cnt)s nested articles will also be moved.<br> Be careful, links to this "
"article and %(cnt)s articles nested in this hierarchy will not be updated."
msgstr[0] ""
msgstr[1] ""



@@ 1862,7 1834,7 @@ msgstr ""
msgid "and"
msgstr ""

#: templates/wiki/preview_inline.html:22 views/article.py:922
#: templates/wiki/preview_inline.html:22 views/article.py:924
msgid "You cannot merge with a deleted revision"
msgstr ""



@@ 1948,107 1920,107 @@ msgstr ""
msgid "Account info saved!"
msgstr ""

#: views/article.py:89
#: views/article.py:90
#, python-format
msgid "New article '%s' created."
msgstr ""

#: views/article.py:97
#: views/article.py:98
#, python-format
msgid "There was an error creating this article: %s"
msgstr ""

#: views/article.py:100
#: views/article.py:101
msgid "There was an error creating this article."
msgstr ""

#: views/article.py:190
#: views/article.py:191
msgid ""
"This article cannot be deleted because it has children or is a root article."
msgstr ""

#: views/article.py:200
#: views/article.py:201
msgid ""
"This article together with all its contents are now completely gone! Thanks!"
msgstr ""

#: views/article.py:209
#: views/article.py:210
#, python-format
msgid ""
"The article \"%s\" is now marked as deleted! Thanks for keeping the site "
"free from unwanted material!"
msgstr ""

#: views/article.py:321
#: views/article.py:322
msgid "Your changes were saved."
msgstr ""

#: views/article.py:335
#: views/article.py:336
msgid "Please note that your article text has not yet been saved!"
msgstr ""

#: views/article.py:366
#: views/article.py:367
msgid "A new revision of the article was successfully added."
msgstr ""

#: views/article.py:415
#: views/article.py:416
msgid "This article cannot be moved because it is a root article."
msgstr ""

#: views/article.py:429
#: views/article.py:430
msgid "This article cannot be moved to a child of itself."
msgstr ""

#: views/article.py:482
#: views/article.py:483
#, python-brace-format
msgid "Moved: {title}"
msgstr ""

#: views/article.py:483
#: views/article.py:484
#, python-brace-format
msgid "Article moved to {link}"
msgstr ""

#: views/article.py:484
#: views/article.py:485
msgid "Created redirect (auto)"
msgstr ""

#: views/article.py:492
#: views/article.py:493
#, python-brace-format
msgid "Article successfully moved! Created {n} redirect."
msgid_plural "Article successfully moved! Created {n} redirects."
msgstr[0] ""
msgstr[1] ""

#: views/article.py:501
#: views/article.py:502
msgid "Article successfully moved!"
msgstr ""

#: views/article.py:543
#: views/article.py:544
msgid "Restoring article"
msgstr ""

#: views/article.py:547
#: views/article.py:548
#, python-format
msgid "The article \"%s\" and its children are now restored."
msgstr ""

#: views/article.py:815
#: views/article.py:816
#, python-format
msgid ""
"The article %(title)s is now set to display revision #%(revision_number)d"
msgstr ""

#: views/article.py:888
#: views/article.py:890
msgid "New title"
msgstr ""

#: views/article.py:935
#: views/article.py:937
#, python-format
msgid "Merge between revision #%(r1)d and revision #%(r2)d"
msgstr ""

#: views/article.py:946
#: views/article.py:948
#, python-format
msgid ""
"A new revision was created: Merge between revision #%(r1)d and revision #"

M src/wiki/locale/ja/LC_MESSAGES/django.mo => src/wiki/locale/ja/LC_MESSAGES/django.mo +0 -0
M src/wiki/locale/zh_CN/LC_MESSAGES/django.mo => src/wiki/locale/zh_CN/LC_MESSAGES/django.mo +0 -0
M src/wiki/plugins/images/apps.py => src/wiki/plugins/images/apps.py +1 -1
@@ 1,6 1,6 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from django.core.checks import register
from django.utils.translation import gettext_lazy as _

from . import checks


M src/wiki/plugins/images/markdown_extensions.py => src/wiki/plugins/images/markdown_extensions.py +54 -76
@@ 1,12 1,22 @@
import re

import markdown
from django.template.loader import render_to_string
from wiki.plugins.images import models, settings

IMAGE_RE = re.compile(
    r'.*(\[image\:(?P<id>[0-9]+)(\s+align\:(?P<align>right|left))?(\s+size\:(?P<size>default|small|medium|large|orig))?\s*\]).*',
    re.IGNORECASE)
IMAGE_RE = (
    r"(?:(?im)" +
    # Match '[image:N'
    r"\[image\:(?P<id>[0-9]+)" +
    # Match optional 'align'
    r"(?:\s+align\:(?P<align>right|left))?" +
    # Match optional 'size'
    r"(?:\s+size\:(?P<size>default|small|medium|large|orig))?" +
    # Match ']' and rest of line.
    # Normally [^\n] could be replaced with a dot '.', since '.'
    # does not match newlines, but inline processors run with re.DOTALL.
    r"\s*\](?P<trailer>[^\n]*)$" +
    # Match zero or more caption lines, each indented by four spaces.
    r"(?P<caption>(?:\n    [^\n]*)*))"
)


class ImageExtension(markdown.Extension):


@@ 14,93 24,61 @@ class ImageExtension(markdown.Extension):
    """ Images plugin markdown extension for django-wiki. """

    def extendMarkdown(self, md, md_globals):
        """ Insert ImagePreprocessor before ReferencePreprocessor. """
        md.preprocessors.add('dw-images', ImagePreprocessor(md), '>html_block')
        md.inlinePatterns.add('dw-images', ImagePattern(IMAGE_RE, md), '>link')
        md.postprocessors.add('dw-images-cleanup', ImagePostprocessor(md), '>raw_html')


class ImagePreprocessor(markdown.preprocessors.Preprocessor):
class ImagePattern(markdown.inlinepatterns.Pattern):
    """
    django-wiki image preprocessor
    Parse text for [image:id align:left|right|center] references.
    Parse text for [image:N align:ALIGN size:SIZE] references.

    For instance:

    [image:id align:left|right|center]
    [image:id align:left|right]
        This is the caption text maybe with [a link](...)

    So: Remember that the caption text is fully valid markdown!
    """

    def run(self, lines):  # NOQA
        new_text = []
        previous_line = ""
        line_index = None
        previous_line_was_image = False
    def handleMatch(self, m):
        image = None
        image_id = None
        alignment = None
        size = settings.THUMBNAIL_SIZES['default']
        caption_lines = []
        for line in lines:
            m = IMAGE_RE.match(line)
            if m:
                previous_line_was_image = True
                image_id = m.group('id').strip()
                alignment = m.group('align')
                if m.group('size'):
                    size = settings.THUMBNAIL_SIZES[m.group('size')]
                try:
                    image = models.Image.objects.get(
                        article=self.markdown.article,
                        id=image_id,
                        current_revision__deleted=False)
                except models.Image.DoesNotExist:
                    pass
                line_index = line.find(m.group(1))
                line = line.replace(m.group(1), "")
                previous_line = line
                caption_lines = []
            elif previous_line_was_image:
                if line.startswith("    "):
                    caption_lines.append(line[4:])
                    line = None
                else:
                    caption_placeholder = "{{{IMAGECAPTION}}}"
                    width = size.split("x")[0] if size else None
                    html = render_to_string(
                        "wiki/plugins/images/render.html",
                        context={
                            'image': image,
                            'caption': caption_placeholder,
                            'align': alignment,
                            'size': size,
                            'width': width
                        })
                    html_before, html_after = html.split(caption_placeholder)
                    placeholder_before = self.markdown.htmlStash.store(
                        html_before,
                        safe=True)
                    placeholder_after = self.markdown.htmlStash.store(
                        html_after,
                        safe=True)
                    new_line = placeholder_before + "\n".join(
                        caption_lines) + placeholder_after + "\n"
                    previous_line_was_image = False
                    if previous_line is not "":
                        if previous_line[line_index:] is not "":
                            new_line = new_line[0:-1]
                        new_text[-1] = (previous_line[0:line_index] +
                                        new_line +
                                        previous_line[line_index:] +
                                        "\n" +
                                        line)
                        line = None
                    else:
                        line = new_line + line
            if line is not None:
                new_text.append(line)
        return new_text
        size = settings.THUMBNAIL_SIZES["default"]

        image_id = m.group("id").strip()
        alignment = m.group("align")
        if m.group("size"):
            size = settings.THUMBNAIL_SIZES[m.group("size")]
        try:
            image = models.Image.objects.get(
                article=self.markdown.article,
                id=image_id,
                current_revision__deleted=False,
            )
        except models.Image.DoesNotExist:
            pass

        caption = m.group("caption")
        trailer = m.group('trailer')

        caption_placeholder = "{{{IMAGECAPTION}}}"
        width = size.split("x")[0] if size else None
        html = render_to_string(
            "wiki/plugins/images/render.html",
            context={
                "image": image,
                "caption": caption_placeholder,
                "align": alignment,
                "size": size,
                "width": width,
            },
        )
        html_before, html_after = html.split(caption_placeholder)
        placeholder_before = self.markdown.htmlStash.store(html_before, safe=True)
        placeholder_after = self.markdown.htmlStash.store(html_after, safe=True)
        return placeholder_before + caption + placeholder_after + trailer


class ImagePostprocessor(markdown.postprocessors.Postprocessor):

M src/wiki/plugins/images/models.py => src/wiki/plugins/images/models.py +2 -2
@@ 107,9 107,9 @@ class ImageRevision(RevisionPluginRevision):
def on_image_revision_delete(instance, *args, **kwargs):
    if not instance.image:
        return
    # Remove image file    
    # Remove image file
    instance.image.delete(save=False)
    

    try:
        path = instance.image.path.split("/")[:-1]
    except NotImplemented:

M src/wiki/plugins/macros/mdx/macro.py => src/wiki/plugins/macros/mdx/macro.py +27 -39
@@ 9,9 9,7 @@ from wiki.plugins.macros import settings
# http://stackoverflow.com/questions/430759/regex-for-managing-escaped-characters-for-items-like-string-literals
re_sq_short = r"'([^'\\]*(?:\\.[^'\\]*)*)'"

MACRO_RE = re.compile(
    r'.*(\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\]).*',
    re.IGNORECASE)
MACRO_RE = r"((?i)\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\])"
KWARG_RE = re.compile(
    r'\s*(?P<arg>\w+)(:(?P<value>([^\']+|%s)))?' %
    re_sq_short,


@@ 23,48 21,38 @@ class MacroExtension(markdown.Extension):
    """ Macro plugin markdown extension for django-wiki. """

    def extendMarkdown(self, md, md_globals):
        """ Insert MacroPreprocessor before ReferencePreprocessor. """
        md.preprocessors.add('dw-macros', MacroPreprocessor(md), '>html_block')
        md.inlinePatterns.add('dw-macros', MacroPattern(MACRO_RE, md), '>link')


class MacroPreprocessor(markdown.preprocessors.Preprocessor):
class MacroPattern(markdown.inlinepatterns.Pattern):

    """django-wiki macro preprocessor - parse text for various [some_macro] and
    [some_macro (kw:arg)*] references. """

    def run(self, lines):
        # Look at all those indentations.
        # That's insane, let's get a helper library
        # Please note that this pattern is also in plugins.images
        new_text = []
        for line in lines:
            m = MACRO_RE.match(line)
            if m:
                macro = m.group('macro').strip()
                if macro in settings.METHODS and hasattr(self, macro):
                    kwargs = m.group('kwargs')
                    if kwargs:
                        kwargs_dict = {}
                        for kwarg in KWARG_RE.finditer(kwargs):
                            arg = kwarg.group('arg')
                            value = kwarg.group('value')
                            if value is None:
                                value = True
                            if isinstance(value, str):
                                # If value is enclosed with ': Remove and
                                # remove escape sequences
                                if value.startswith("'") and len(value) > 2:
                                    value = value[1:-1]
                                    value = value.replace("\\\\", "¤KEEPME¤")
                                    value = value.replace("\\", "")
                                    value = value.replace("¤KEEPME¤", "\\")
                            kwargs_dict[str(arg)] = value
                        line = getattr(self, macro)(**kwargs_dict)
                    else:
                        line = getattr(self, macro)()
            if line is not None:
                new_text.append(line)
        return new_text
    def handleMatch(self, m):
        macro = m.group('macro').strip()
        if macro not in settings.METHODS or not hasattr(self, macro):
            return m.group(2)

        kwargs = m.group('kwargs')
        if not kwargs:
            return getattr(self, macro)()
        kwargs_dict = {}
        for kwarg in KWARG_RE.finditer(kwargs):
            arg = kwarg.group('arg')
            value = kwarg.group('value')
            if value is None:
                value = True
            if isinstance(value, str):
                # If value is enclosed with ': Remove and
                # remove escape sequences
                if value.startswith("'") and len(value) > 2:
                    value = value[1:-1]
                    value = value.replace("\\\\", "¤KEEPME¤")
                    value = value.replace("\\", "")
                    value = value.replace("¤KEEPME¤", "\\")
            kwargs_dict[str(arg)] = value
        return getattr(self, macro)(**kwargs_dict)

    def article_list(self, depth="2"):
        html = render_to_string(

M src/wiki/plugins/macros/mdx/toc.py => src/wiki/plugins/macros/mdx/toc.py +1 -2
@@ 1,7 1,6 @@
import re

from markdown.extensions.toc import TocTreeprocessor, TocExtension, slugify
from markdown.util import etree
from markdown.extensions.toc import TocExtension, TocTreeprocessor, slugify
from wiki.plugins.macros import settings

HEADER_ID_PREFIX = "wiki-toc-"

M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py => src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +2 -2
@@ 1,6 1,6 @@
from django import template
from wiki.plugins.macros import settings
from wiki.plugins.macros.mdx.macro import MacroPreprocessor
from wiki.plugins.macros.mdx.macro import MacroPattern

register = template.Library()



@@ 19,6 19,6 @@ def article_list(context, urlpath, depth):
def allowed_macros():
    for method in settings.METHODS:
        try:
            yield getattr(MacroPreprocessor, method).meta
            yield getattr(MacroPattern, method).meta
        except AttributeError:
            continue

M src/wiki/plugins/redlinks/mdx/redlinks.py => src/wiki/plugins/redlinks/mdx/redlinks.py +18 -5
@@ 1,6 1,9 @@
from urllib.parse import urlparse, urljoin
from markdown.treeprocessors import Treeprocessor
import html
from urllib.parse import urljoin, urlparse

from markdown.extensions import Extension
from markdown.postprocessors import AndSubstitutePostprocessor
from markdown.treeprocessors import Treeprocessor
from wiki.models import URLPath




@@ 22,7 25,7 @@ def urljoin_internal(base, url):
    res1 = urljoin(canary1 + base, url)
    res2 = urljoin(canary2 + base, url)
    if res1.startswith(canary1) and res2.startswith(canary2):
        return res1[len(canary1) :]
        return res1[len(canary1):]


class LinkTreeprocessor(Treeprocessor):


@@ 47,11 50,21 @@ class LinkTreeprocessor(Treeprocessor):
        return self._my_urlpath

    def get_class(self, el):
        href = el.get("href")
        if not href:
            return
        # The autolinker turns email links into links with many HTML entities.
        # These entities are further escaped using markdown-specific codes.
        # First unescape the markdown-specific, then use html.unescape.
        href = AndSubstitutePostprocessor().run(href)
        href = html.unescape(href)
        try:
            url = urlparse(el.get("href"))
            url = urlparse(href)
        except ValueError:
            return
        if url.netloc or url.path.startswith("/"):
        if url.scheme == "mailto":
            return
        if url.scheme or url.netloc or url.path.startswith("/"):
            # Contains a hostname or is an absolute link => external
            return self.external_class
        # Ensure that path ends with a slash

M src/wiki/sites.py => src/wiki/sites.py +1 -1
@@ 1,8 1,8 @@
from django.apps import apps
from django.utils.functional import LazyObject
from django.utils.module_loading import import_string
from wiki.conf import settings
from wiki.compat import include, url
from wiki.conf import settings
from wiki.core.plugins import registry



M src/wiki/static/wiki/bootstrap/LICENSE => src/wiki/static/wiki/bootstrap/LICENSE +1 -1
@@ 173,4 173,4 @@
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS
\ No newline at end of file
   END OF TERMS AND CONDITIONS

M src/wiki/static/wiki/bootstrap/fonts/glyphicons-halflings-regular.svg => src/wiki/static/wiki/bootstrap/fonts/glyphicons-halflings-regular.svg +1 -1
@@ 285,4 285,4 @@
<glyph unicode="&#x1f511;" d="M250 1200h600q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-500l-255 -178q-19 -9 -32 -1t-13 29v650h-150q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM400 1100v-100h300v100h-300z" />
<glyph unicode="&#x1f6aa;" d="M250 1200h750q39 0 69.5 -40.5t30.5 -84.5v-933l-700 -117v950l600 125h-700v-1000h-100v1025q0 23 15.5 49t34.5 26zM500 525v-100l100 20v100z" />
</font>
</defs></svg> 
\ No newline at end of file
</defs></svg>

M src/wiki/static/wiki/font-awesome/font/fontawesome-webfont.svg => src/wiki/static/wiki/font-awesome/font/fontawesome-webfont.svg +1 -1
@@ 517,4 517,4 @@
<glyph unicode="&#xf20e;" horiz-adv-x="1792" />
<glyph unicode="&#xf500;" horiz-adv-x="1792" />
</font>
</defs></svg> 
\ No newline at end of file
</defs></svg>

M src/wiki/templatetags/wiki_tags.py => src/wiki/templatetags/wiki_tags.py +21 -19
@@ 109,31 109,33 @@ def get_content_snippet(content, keyword, max_words=30):

        # remove html tags
        content = striptags(content)
        # remove newlines
        content = content.replace("\n", " ").split(" ")
        # remove whitespace
        words = content.split()

        return list(filter(lambda x: x != "", content))
        return words

    max_words = int(max_words)

    pattern = re.compile(
        r'(?P<before>.*)%s(?P<after>.*)' % re.escape(keyword),
        re.MULTILINE | re.IGNORECASE | re.DOTALL
    )
    match_position = content.lower().find(keyword.lower())

    match = pattern.search(content)

    if match:
        words = clean_text(match.group("before"))
        before_words = words[-max_words // 2:]
        words = clean_text(match.group("after"))

        after = " ".join(words[:max_words - len(before_words)])
    if match_position != -1:
        try:
            match_start = content.rindex(' ', 0, match_position) + 1
        except ValueError:
            match_start = 0
        try:
            match_end = content.index(' ', match_position + len(keyword))
        except ValueError:
            match_end = len(content)
        all_before = clean_text(content[:match_start])
        match = content[match_start:match_end]
        all_after = clean_text(content[match_end:])
        before_words = all_before[-max_words // 2:]
        after_words = all_after[:max_words - len(before_words)]
        before = " ".join(before_words)

        html = "%s %s %s" % (before, striptags(keyword), after)

        kw_p = re.compile(r'(%s)' % keyword, re.IGNORECASE)
        after = " ".join(after_words)
        html = ("%s %s %s" % (before, striptags(match), after)).strip()
        kw_p = re.compile(r'(\S*%s\S*)' % keyword, re.IGNORECASE)
        html = kw_p.sub(r"<strong>\1</strong>", html)

        return mark_safe(html)

M src/wiki/urls.py => src/wiki/urls.py +1 -2
@@ 1,11 1,10 @@
from django.utils.module_loading import import_string
from wiki import sites
from wiki.compat import include, url
from wiki.conf import settings
from wiki.core.plugins import registry
from wiki import sites
from wiki.views import accounts, article, deleted_list


urlpatterns = [
    url(r'^', sites.site.urls),
]

M src/wiki/views/article.py => src/wiki/views/article.py +2 -0
@@ 15,6 15,7 @@ from django.views.generic import DetailView
from django.views.generic.base import RedirectView, TemplateView, View
from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from django.views.decorators.clickjacking import xframe_options_sameorigin
from wiki import editors, forms, models
from wiki.conf import settings
from wiki.core import permissions


@@ 822,6 823,7 @@ class Preview(ArticleMixin, TemplateView):

    template_name = "wiki/preview_inline.html"

    @method_decorator(xframe_options_sameorigin)
    @method_decorator(get_article(can_read=True, deleted_contents=True))
    def dispatch(self, request, article, *args, **kwargs):
        revision_id = request.GET.get('r', None)

M testproject/testproject/db/prepopulated.db => testproject/testproject/db/prepopulated.db +0 -0
M testproject/testproject/settings/base.py => testproject/testproject/settings/base.py +0 -1
@@ 59,7 59,6 @@ MIDDLEWARE = [
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',

M tests/core/test_checks.py => tests/core/test_checks.py +1 -1
@@ 3,7 3,7 @@ import copy
from django.conf import settings
from django.core.checks import Error, registry
from django.test import TestCase
from wiki.checks import OBSOLETE_INSTALLED_APPS, REQUIRED_CONTEXT_PROCESSORS, REQUIRED_INSTALLED_APPS, Tags
from wiki.checks import REQUIRED_CONTEXT_PROCESSORS, REQUIRED_INSTALLED_APPS, Tags


def _remove(settings, arg):

M tests/core/test_markdown.py => tests/core/test_markdown.py +2 -1
@@ 1,6 1,7 @@
from unittest.mock import patch

import markdown
from django.test import TestCase
from unittest.mock import patch
from wiki.core.markdown import ArticleMarkdown
from wiki.core.markdown.mdx.codehilite import WikiCodeHiliteExtension
from wiki.core.markdown.mdx.responsivetable import ResponsiveTableExtension

M tests/core/test_template_filters.py => tests/core/test_template_filters.py +20 -26
@@ 19,7 19,7 @@ class GetContentSnippet(TemplateTestCase):
        content = text + ' list'
        expected = (
            'lorem lorem lorem lorem lorem lorem lorem lorem lorem '
            'lorem lorem lorem lorem lorem lorem <strong>list</strong> '
            'lorem lorem lorem lorem lorem lorem <strong>list</strong>'
        )

        output = get_content_snippet(content, 'list')


@@ 30,7 30,7 @@ class GetContentSnippet(TemplateTestCase):
        text = 'lorem ' * 80
        content = 'list ' + text
        expected = (
            ' <strong>list</strong> lorem lorem lorem lorem lorem '
            '<strong>list</strong> lorem lorem lorem lorem lorem '
            'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem '
            'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem '
            'lorem lorem lorem'


@@ 40,18 40,9 @@ class GetContentSnippet(TemplateTestCase):

        self.assertEqual(output, expected)

    def test_whole_content_is_consist_from_keywords(self):
    def test_whole_content_consists_of_keywords(self):
        content = 'lorem ' * 80
        expected = (
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
            '<strong>lorem</strong> <strong>lorem</strong> '
        )
        expected = '<strong>lorem</strong>' + 30 * ' <strong>lorem</strong>'

        output = get_content_snippet(content, 'lorem')



@@ 79,11 70,7 @@ class GetContentSnippet(TemplateTestCase):
        text = 'dolorum ' * 80
        content += text + ' list'

        expected = (
            'dolorum dolorum dolorum dolorum dolorum dolorum dolorum '
            'dolorum dolorum dolorum dolorum dolorum dolorum dolorum dolorum '
            '<strong>list</strong> '
        )
        expected = '<strong>list</strong>' + 30 * ' lorem'

        output = get_content_snippet(content, 'list')



@@ 95,7 82,7 @@ class GetContentSnippet(TemplateTestCase):
        content = text + ' list'

        output = get_content_snippet(content, 'list', 0)
        expected = 'spam ' * 800 + '<strong>list</strong> '
        expected = 'spam ' * 800 + '<strong>list</strong>'

        self.assertEqual(output, expected)



@@ 105,7 92,7 @@ class GetContentSnippet(TemplateTestCase):
        content = text + ' list'

        output = get_content_snippet(content, 'list', -10)
        expected = 'spam ' * 75 + '<strong>list</strong> '
        expected = 'spam ' * 75 + '<strong>list</strong>'

        self.assertEqual(output, expected)



@@ 145,8 132,7 @@ class GetContentSnippet(TemplateTestCase):
        keyword = 'maybe'

        content = """
        <h1>Some dummy</h1> text. <div>Actually</div> I don't what to write,
        heh. Don't now, <b>maybe</b> I should citate Shakespeare or Byron.
        I should citate Shakespeare or Byron.
        Or <a>maybe</a> copy paste from <a href="http://python.org">python</a>
        or django documentation. Maybe.
        """


@@ 154,7 140,7 @@ class GetContentSnippet(TemplateTestCase):
        expected = (
            'I should citate Shakespeare or Byron. '
            'Or <strong>maybe</strong> copy paste from python '
            'or django documentation. <strong>maybe</strong> .'
            'or django documentation. <strong>Maybe.</strong>'
        )

        output = get_content_snippet(content, keyword, 30)


@@ 168,8 154,8 @@ class GetContentSnippet(TemplateTestCase):
        content = """
        knight eggs spam ham eggs guido python eggs circus
        """
        expected = ('<strong>eggs</strong> guido python '
                    '<strong>eggs</strong> circus')
        expected = ('knight <strong>eggs</strong> spam ham '
                    '<strong>eggs</strong> guido')

        output = get_content_snippet(content, keyword, 5)



@@ 179,10 165,18 @@ class GetContentSnippet(TemplateTestCase):

        expected = (
            'knight <strong>eggs</strong> spam ham '
            '<strong>eggs</strong> guido python <strong>eggs</strong> '
            '<strong>eggs</strong> guido python <strong>eggs</strong>'
        )
        self.assertEqual(output, expected)

    def test_content_case_preserved(self):
        keyword = 'DOlOr'
        match = 'DoLoR'
        content = 'lorem ipsum %s sit amet' % match
        output = get_content_snippet(content, keyword)
        self.assertIn(match, output)
        self.assertNotIn(keyword, output)


class CanRead(TemplateTestCase):


M tests/core/test_views.py => tests/core/test_views.py +20 -2
@@ 270,11 270,11 @@ class MoveViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTes

        response = self.get_by_path('test0/test2/')
        self.assertContains(response, 'Moved: Test1')
        self.assertRegex(response.content, br'moved to <a[^>]*>wiki:/test1new/')
        self.assertRegex(response.rendered_content, r'moved to <a[^>]*>wiki:/test1new/')

        response = self.get_by_path('test0/test2/test020/')
        self.assertContains(response, 'Moved: Test020')
        self.assertRegex(response.content, br'moved to <a[^>]*>wiki:/test1new/test020')
        self.assertRegex(response.rendered_content, r'moved to <a[^>]*>wiki:/test1new/test020')

        # Check that moved_to was correctly set
        urlsrc = URLPath.get_by_path('/test0/test2/')


@@ 368,6 368,24 @@ class EditViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTes

        self.assertContains(response, 'The modified text')

    def test_preview_xframe_options_sameorigin(self):
        """Ensure that preview response has X-Frame-Options: SAMEORIGIN"""

        example_data = {
            'content': 'The modified text',
            'current_revision': str(URLPath.root().article.current_revision.id),
            'preview': '1',
            'summary': 'why edited',
            'title': 'wiki test'
        }

        response = self.client.post(
            resolve_url('wiki:preview', path=''),
            example_data
        )

        self.assertEquals(response.get('X-Frame-Options'), 'SAMEORIGIN')

    def test_revision_conflict(self):
        """
        Test the warning if the same article is being edited concurrently.

M tests/plugins/attachments/test_models.py => tests/plugins/attachments/test_models.py +1 -1
@@ 1,5 1,5 @@
from tests.base import RequireRootArticleMixin, TestBase
from wiki.plugins.attachments.models import AttachmentRevision, Attachment
from wiki.plugins.attachments.models import Attachment, AttachmentRevision


class AttachmentRevisionTests(RequireRootArticleMixin, TestBase):

M tests/plugins/images/test_forms.py => tests/plugins/images/test_forms.py +0 -1
@@ 1,6 1,5 @@
from django.test import TestCase
from django.utils.translation import gettext

from wiki.plugins.images.forms import PurgeForm



A tests/plugins/images/test_markdown.py => tests/plugins/images/test_markdown.py +81 -0
@@ 0,0 1,81 @@
import base64
from io import BytesIO

from django.core.files.uploadedfile import InMemoryUploadedFile
from tests.base import RequireRootArticleMixin, TestBase
from wiki.core import markdown
from wiki.plugins.images import models


class ImageMarkdownTests(RequireRootArticleMixin, TestBase):
    def setUp(self):
        super().setUp()

        self.image_revision = models.ImageRevision(
            image=self._create_test_gif_file(), width=1, height=1
        )
        self.image = models.Image(article=self.root_article)
        self.image.add_revision(self.image_revision)
        self.assertEqual(1, self.image.id)

    def _create_test_gif_file(self):
        # A black 1x1 gif
        str_base64 = "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="
        filename = "test.gif"
        data = base64.b64decode(str_base64)
        filedata = BytesIO(data)
        return InMemoryUploadedFile(filedata, None, filename, "image", len(data), None)

    def test_before_and_after(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("before [image:%s align:left] after" % self.image.id)
        before_pos = md_text.index("before")
        figure_pos = md_text.index("<figure")
        after_pos = md_text.index("after")
        self.assertTrue(before_pos < figure_pos < after_pos)

    def test_markdown(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("[image:%s align:left]" % self.image.id)
        self.assertIn("<figure", md_text)
        self.assertNotIn("[image:%s align:left]" % self.image.id, md_text)
        md_text = md.convert("image: [image:%s align:left]\nadasd" % self.image.id)
        self.assertIn("<figure", md_text)
        self.assertIn("<figcaption", md_text)
        md_text = md.convert(
            "image: [image:%s align:right size:medium]\nadasd" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertIn("<figcaption", md_text)
        md_text = md.convert("image: [image:123 align:left size:medium]\nadasd")
        self.assertIn("Image not found", md_text)
        self.assertIn("<figcaption", md_text)

    def test_caption(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert(
            "[image:%s align:left]\n    this is visual" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertRegex(
            md_text, r'<figcaption class="caption">\s*this is visual\s*</figcaption>'
        )
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert(
            "[image:%s align:left]\n    this is visual\n    second line" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertRegex(
            md_text,
            r'<figcaption class="caption">\s*this is visual\s*second line\s*</figcaption>',
        )

    def check_escape(self, text_to_escape):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("`%s`" % text_to_escape)
        self.assertNotIn("<figure", md_text)
        self.assertIn(text_to_escape, md_text)

    def test_escape(self):
        self.check_escape("[image:%s align:left]" % self.image.id)
        self.check_escape("image tag: [image:%s]" % self.image.id)

M tests/plugins/images/test_views.py => tests/plugins/images/test_views.py +1 -4
@@ 9,10 9,7 @@ from wiki.models import URLPath
from wiki.plugins.images import models
from wiki.plugins.images.wiki_plugin import ImagePlugin

from ...base import (
    ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin,
    wiki_override_settings,
)
from ...base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, wiki_override_settings


class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):

M tests/plugins/links/test_urlize.py => tests/plugins/links/test_urlize.py +2 -4
@@ 1,11 1,9 @@
import html
import markdown
from unittest import mock

import markdown
import pytest

from wiki.plugins.links.mdx.urlize import makeExtension, UrlizeExtension

from wiki.plugins.links.mdx.urlize import UrlizeExtension, makeExtension

# Template accepts two strings - href value and link text value.
EXPECTED_LINK_TEMPLATE = (

A tests/plugins/macros/test_macro.py => tests/plugins/macros/test_macro.py +16 -0
@@ 0,0 1,16 @@
from tests.base import RequireRootArticleMixin, TestBase
from wiki.core import markdown


class MacroTests(RequireRootArticleMixin, TestBase):
    def test_article_list(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("[article_list depth:2]")
        self.assertIn("Nothing below this level", md_text)
        self.assertNotIn("[article_list depth:2]", md_text)

    def test_escape(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("`[article_list depth:2]`")
        self.assertNotIn("Nothing below this level", md_text)
        self.assertIn("[article_list depth:2]", md_text)

M tests/plugins/redlinks/test_redlinks.py => tests/plugins/redlinks/test_redlinks.py +9 -0
@@ 52,3 52,12 @@ class RedlinksTests(RequireRootArticleMixin, TestBase):
        self.assertNotIn("wiki-internal", html)
        self.assertNotIn("wiki-external", html)
        self.assertIn("wiki-broken", html)

    def test_mailto(self):
        md = markdown.ArticleMarkdown(article=self.root.article)
        md_text = "<foo@example.com>"
        html = md.convert(md_text)
        self.assertNotIn("wiki-internal", html)
        self.assertNotIn("wiki-external", html)
        self.assertNotIn("wiki-broken", html)
        self.assertIn("<a ", html)

M tests/testdata/urls.py => tests/testdata/urls.py +0 -1
@@ 3,7 3,6 @@ from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from wiki.compat import include, url


urlpatterns = [
    url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
    url(r'^admin/', admin.site.urls),