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"
-" 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> 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="🔑" 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="🚪" 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="" horiz-adv-x="1792" />
<glyph unicode="" 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),