~netlandish/django-wiki

0d6727427ad31745d8e885df0ea626614c250bce — Benjamin Bach 6 years ago 2701927 + 650ceee
Merge branch 'master' into diff_refractor
98 files changed, 233 insertions(+), 353 deletions(-)

M .gitignore
M src/wiki/__init__.py
M src/wiki/admin.py
M src/wiki/apps.py
M src/wiki/conf/__init__.py
M src/wiki/conf/settings.py
M src/wiki/core/markdown/__init__.py
M src/wiki/core/markdown/mdx/codehilite.py
M src/wiki/core/paginator.py
M src/wiki/core/permissions.py
M src/wiki/core/plugins/base.py
M src/wiki/core/plugins/loader.py
M src/wiki/core/plugins/registry.py
M src/wiki/core/utils.py
M src/wiki/decorators.py
M src/wiki/editors/markitup.py
M src/wiki/forms.py
M src/wiki/managers.py
M src/wiki/migrations/0001_initial.py
M src/wiki/migrations/0002_urlpath_moved_to.py
M src/wiki/models/__init__.py
M src/wiki/models/article.py
M src/wiki/models/pluginbase.py
M src/wiki/models/urlpath.py
M src/wiki/plugins/attachments/apps.py
M src/wiki/plugins/attachments/forms.py
M src/wiki/plugins/attachments/migrations/0001_initial.py
M src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py
M src/wiki/plugins/attachments/models.py
M src/wiki/plugins/attachments/views.py
M src/wiki/plugins/attachments/wiki_plugin.py
M src/wiki/plugins/globalhistory/apps.py
M src/wiki/plugins/globalhistory/models.py
M src/wiki/plugins/globalhistory/views.py
M src/wiki/plugins/haystack/__init__.py
M src/wiki/plugins/haystack/views.py
M src/wiki/plugins/help/apps.py
M src/wiki/plugins/help/wiki_plugin.py
M src/wiki/plugins/images/admin.py
M src/wiki/plugins/images/apps.py
M src/wiki/plugins/images/forms.py
M src/wiki/plugins/images/markdown_extensions.py
M src/wiki/plugins/images/migrations/0001_initial.py
M src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py
M src/wiki/plugins/images/models.py
M src/wiki/plugins/images/templatetags/wiki_images_tags.py
M src/wiki/plugins/images/views.py
M src/wiki/plugins/images/wiki_plugin.py
M src/wiki/plugins/links/apps.py
M src/wiki/plugins/links/mdx/urlize.py
M src/wiki/plugins/links/wiki_plugin.py
M src/wiki/plugins/macros/apps.py
M src/wiki/plugins/macros/mdx/macro.py
M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py
M src/wiki/plugins/macros/wiki_plugin.py
M src/wiki/plugins/notifications/apps.py
M src/wiki/plugins/notifications/forms.py
M src/wiki/plugins/notifications/migrations/0001_initial.py
M src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py
M src/wiki/plugins/notifications/models.py
M src/wiki/plugins/notifications/util.py
M src/wiki/plugins/notifications/views.py
M src/wiki/templatetags/wiki_tags.py
M src/wiki/urls.py
M src/wiki/views/accounts.py
M src/wiki/views/article.py
M src/wiki/views/deleted_list.py
M src/wiki/views/mixins.py
M testproject/manage.py
M testproject/testproject/settings/__init__.py
M testproject/testproject/settings/base.py
M testproject/testproject/settings/codehilite.py
M testproject/testproject/settings/customauthuser.py
M testproject/testproject/settings/haystack.py
M testproject/testproject/settings/sendfile.py
M testproject/testproject/urls.py
M testproject/testproject/wsgi.py
M tests/base.py
M tests/core/test_accounts.py
M tests/core/test_basic.py
M tests/core/test_commands.py
M tests/core/test_managers.py
M tests/core/test_markdown.py
M tests/core/test_models.py
M tests/core/test_template_filters.py
M tests/core/test_template_tags.py
M tests/core/test_urls.py
M tests/core/test_views.py
M tests/plugins/attachments/test_commands.py
M tests/plugins/attachments/test_views.py
M tests/plugins/globalhistory/__init__.py
M tests/plugins/globalhistory/test_globalhistory.py
M tests/plugins/images/__init__.py
M tests/plugins/images/test_views.py
M tests/plugins/links/test_links.py
M tests/plugins/macros/test_toc.py
M tests/testdata/models.py
M tests/testdata/urls.py
M .gitignore => .gitignore +1 -0
@@ 2,6 2,7 @@
build
dist
*.egg-info
.eggs
testproject/testproject/whoosh_index/
testproject/testproject/xapian_index/


M src/wiki/__init__.py => src/wiki/__init__.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
# This package and all its sub-packages are part of django-wiki,
# except where otherwise stated.
#

M src/wiki/admin.py => src/wiki/admin.py +3 -3
@@ 1,7 1,7 @@
from django import forms
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from mptt.admin import MPTTModelAdmin

from . import editors, models


@@ 21,7 21,7 @@ class ArticleRevisionForm(forms.ModelForm):
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(ArticleRevisionForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        # TODO: This pattern is too weird
        editor = editors.getEditor()
        self.fields['content'].widget = editor.get_admin_widget()


@@ 55,7 55,7 @@ class ArticleForm(forms.ModelForm):
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(ArticleForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        if self.instance.pk:
            revisions = models.ArticleRevision.objects.filter(
                article=self.instance)

M src/wiki/apps.py => src/wiki/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class WikiConfig(AppConfig):

M src/wiki/conf/__init__.py => src/wiki/conf/__init__.py +0 -1
@@ 1,1 0,0 @@
# -*- coding: utf-8 -*-

M src/wiki/conf/settings.py => src/wiki/conf/settings.py +1 -1
@@ 5,7 5,7 @@ from django.conf import settings as django_settings
from django.contrib.messages import constants as messages
from django.core.files.storage import default_storage
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _

#: Should urls be case sensitive?
URL_CASE_SENSITIVE = getattr(django_settings, 'WIKI_URL_CASE_SENSITIVE', False)

M src/wiki/core/markdown/__init__.py => src/wiki/core/markdown/__init__.py +1 -1
@@ 33,7 33,7 @@ class ArticleMarkdown(markdown.Markdown):
        return extensions

    def convert(self, text, *args, **kwargs):
        html = super(ArticleMarkdown, self).convert(text, *args, **kwargs)
        html = super().convert(text, *args, **kwargs)
        if settings.MARKDOWN_SANITIZE_HTML:
            tags = settings.MARKDOWN_HTML_WHITELIST + plugin_registry.get_html_whitelist()


M src/wiki/core/markdown/mdx/codehilite.py => src/wiki/core/markdown/mdx/codehilite.py +1 -1
@@ 42,7 42,7 @@ class WikiFencedBlockPreprocessor(Preprocessor):
    CODE_WRAP = '<pre>%s</pre>'

    def __init__(self, md):
        super(WikiFencedBlockPreprocessor, self).__init__(md)
        super().__init__(md)

        self.checked_for_codehilite = False
        self.codehilite_conf = {}

M src/wiki/core/paginator.py => src/wiki/core/paginator.py +2 -2
@@ 8,12 8,12 @@ class WikiPaginator(Paginator):
        :param side_pages: How many pages should be shown before and after the current page
        """
        self.side_pages = kwargs.pop('side_pages', 4)
        super(WikiPaginator, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def page(self, number):
        # Save last accessed page number for context-based lookup in page_range
        self.last_accessed_page_number = number
        return super(WikiPaginator, self).page(number)
        return super().page(number)

    @property
    def page_range(self):

M src/wiki/core/permissions.py => src/wiki/core/permissions.py +9 -9
@@ 23,11 23,11 @@ def can_read(article, user):
            return False

        # Check access for other users...
        if user.is_anonymous() and not settings.ANONYMOUS:
        if user.is_anonymous and not settings.ANONYMOUS:
            return False
        elif article.other_read:
            return True
        elif user.is_anonymous():
        elif user.is_anonymous:
            return False
        if user == article.owner:
            return True


@@ 44,11 44,11 @@ def can_write(article, user):
    if callable(settings.CAN_WRITE):
        return settings.CAN_WRITE(article, user)
    # Check access for other users...
    if user.is_anonymous() and not settings.ANONYMOUS_WRITE:
    if user.is_anonymous and not settings.ANONYMOUS_WRITE:
        return False
    elif article.other_write:
        return True
    elif user.is_anonymous():
    elif user.is_anonymous:
        return False
    if user == article.owner:
        return True


@@ 64,7 64,7 @@ def can_write(article, user):
def can_assign(article, user):
    if callable(settings.CAN_ASSIGN):
        return settings.CAN_ASSIGN(article, user)
    return not user.is_anonymous() and user.has_perm('wiki.assign')
    return not user.is_anonymous and user.has_perm('wiki.assign')


def can_assign_owner(article, user):


@@ 77,7 77,7 @@ def can_change_permissions(article, user):
    if callable(settings.CAN_CHANGE_PERMISSIONS):
        return settings.CAN_CHANGE_PERMISSIONS(article, user)
    return (
        not user.is_anonymous() and (
        not user.is_anonymous and (
            article.owner == user or
            user.has_perm('wiki.assign')
        )


@@ 87,16 87,16 @@ def can_change_permissions(article, user):
def can_delete(article, user):
    if callable(settings.CAN_DELETE):
        return settings.CAN_DELETE(article, user)
    return not user.is_anonymous() and article.can_write(user)
    return not user.is_anonymous and article.can_write(user)


def can_moderate(article, user):
    if callable(settings.CAN_MODERATE):
        return settings.CAN_MODERATE(article, user)
    return not user.is_anonymous() and user.has_perm('wiki.moderate')
    return not user.is_anonymous and user.has_perm('wiki.moderate')


def can_admin(article, user):
    if callable(settings.CAN_ADMIN):
        return settings.CAN_ADMIN(article, user)
    return not user.is_anonymous() and user.has_perm('wiki.admin')
    return not user.is_anonymous and user.has_perm('wiki.admin')

M src/wiki/core/plugins/base.py => src/wiki/core/plugins/base.py +1 -1
@@ 1,5 1,5 @@
from django import forms
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _


"""Base classes for different plugin objects.

M src/wiki/core/plugins/loader.py => src/wiki/core/plugins/loader.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.utils.module_loading import autodiscover_modules



M src/wiki/core/plugins/registry.py => src/wiki/core/plugins/registry.py +1 -4
@@ 1,8 1,5 @@
# -*- coding: utf-8 -*-
from importlib import import_module

from django.utils.six import string_types

_cache = {}
_settings_forms = []
_markdown_extensions = []


@@ 24,7 21,7 @@ def register(PluginClass):

    settings_form = getattr(PluginClass, 'settings_form', None)
    if settings_form:
        if isinstance(settings_form, string_types):
        if isinstance(settings_form, str):
            klassname = settings_form.split(".")[-1]
            modulename = ".".join(settings_form.split(".")[:-1])
            form_module = import_module(modulename)

M src/wiki/core/utils.py => src/wiki/core/utils.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from importlib import import_module

from django.http.response import JsonResponse

M src/wiki/decorators.py => src/wiki/decorators.py +2 -3
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from functools import wraps

from django.core.urlresolvers import reverse


@@ 12,7 11,7 @@ from wiki.core.exceptions import NoRootURL


def response_forbidden(request, article, urlpath):
    if request.user.is_anonymous():
    if request.user.is_anonymous:
        qs = request.META.get('QUERY_STRING', '')
        if qs:
            qs = urlquote('?' + qs)


@@ 143,7 142,7 @@ def get_article(func=None, can_read=True, can_write=False,  # noqa
            return response_forbidden(request, article, urlpath)

        if can_create and not (
                request.user.is_authenticated() or settings.ANONYMOUS_CREATE):
                request.user.is_authenticated or settings.ANONYMOUS_CREATE):
            return response_forbidden(request, article, urlpath)

        if can_delete and not article.can_delete(request.user):

M src/wiki/editors/markitup.py => src/wiki/editors/markitup.py +2 -3
@@ 1,4 1,3 @@
# -*- coding: utf-8
from django import forms
from django.forms.utils import flatatt
from django.utils.encoding import force_text


@@ 19,7 18,7 @@ class MarkItUpAdminWidget(BuildAttrsCompat, forms.Widget):
                         'rows': '10', 'cols': '40', }
        if attrs:
            default_attrs.update(attrs)
        super(MarkItUpAdminWidget, self).__init__(default_attrs)
        super().__init__(default_attrs)

    def render(self, name, value, attrs=None, renderer=None):
        if value is None:


@@ 40,7 39,7 @@ class MarkItUpWidget(BuildAttrsCompat, forms.Widget):
                         'rows': '10', 'cols': '40', }
        if attrs:
            default_attrs.update(attrs)
        super(MarkItUpWidget, self).__init__(default_attrs)
        super().__init__(default_attrs)

    def render(self, name, value, attrs=None, renderer=None):
        if value is None:

M src/wiki/forms.py => src/wiki/forms.py +34 -35
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import random
import string
from datetime import timedelta


@@ 19,8 18,8 @@ from django.utils.encoding import force_text
from django.utils.html import conditional_escape, escape
from django.utils.safestring import mark_safe
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _

from wiki import models
from wiki.conf import settings


@@ 59,10 58,10 @@ class WikiSlugField(forms.SlugField):
def _clean_slug(slug, urlpath):
    if slug.startswith("_"):
        raise forms.ValidationError(
            ugettext('A slug may not begin with an underscore.'))
            gettext('A slug may not begin with an underscore.'))
    if slug == 'admin':
        raise forms.ValidationError(
            ugettext("'admin' is not a permitted slug name."))
            gettext("'admin' is not a permitted slug name."))

    if settings.URL_CASE_SENSITIVE:
        already_existing_slug = models.URLPath.objects.filter(


@@ 77,11 76,11 @@ def _clean_slug(slug, urlpath):
        already_urlpath = already_existing_slug[0]
        if already_urlpath.article and already_urlpath.article.current_revision.deleted:
            raise forms.ValidationError(
                ugettext('A deleted article with slug "%s" already exists.') %
                gettext('A deleted article with slug "%s" already exists.') %
                already_urlpath.slug)
        else:
            raise forms.ValidationError(
                ugettext('A slug named "%s" already exists.') %
                gettext('A slug named "%s" already exists.') %
                already_urlpath.slug)

    if settings.CHECK_SLUG_URL_AVAILABLE:


@@ 90,7 89,7 @@ def _clean_slug(slug, urlpath):
            match = resolve(urlpath.path + '/' + slug + '/')
            if match.app_name != 'wiki':
                raise forms.ValidationError(
                    ugettext('This slug conflicts with an existing URL.'))
                    gettext('This slug conflicts with an existing URL.'))
        except Resolver404:
            pass



@@ 117,14 116,14 @@ class SpamProtectionMixin(object):
        request = self.request
        user = None
        ip_address = None
        if request.user.is_authenticated():
        if request.user.is_authenticated:
            user = request.user
        else:
            ip_address = request.META.get('REMOTE_ADDR', None)

        if not (user or ip_address):
            raise forms.ValidationError(
                ugettext(
                gettext(
                    'Spam protection failed to find both a logged in user and an IP address.'))

        def check_interval(from_time, max_count, interval_name):


@@ 140,7 139,7 @@ class SpamProtectionMixin(object):
            revisions = revisions.count()
            if revisions >= max_count:
                raise forms.ValidationError(
                    ugettext('Spam protection: You are only allowed to create or edit %(revisions)d article(s) per %(interval_name)s.') % {
                    gettext('Spam protection: You are only allowed to create or edit %(revisions)d article(s) per %(interval_name)s.') % {
                        'revisions': max_count,
                        'interval_name': interval_name,
                    })


@@ 151,7 150,7 @@ class SpamProtectionMixin(object):
            return

        from_time = timezone.now() - timedelta(minutes=settings.REVISIONS_MINUTES_LOOKBACK)
        if request.user.is_authenticated():
        if request.user.is_authenticated:
            per_minute = settings.REVISIONS_PER_MINUTES
        else:
            per_minute = settings.REVISIONS_PER_MINUTES_ANONYMOUS


@@ 164,7 163,7 @@ class SpamProtectionMixin(object):
        )

        from_time = timezone.now() - timedelta(minutes=60)
        if request.user.is_authenticated():
        if request.user.is_authenticated:
            per_hour = settings.REVISIONS_PER_MINUTES
        else:
            per_hour = settings.REVISIONS_PER_MINUTES_ANONYMOUS


@@ 193,7 192,7 @@ class MoveForm(forms.Form):
                                  required=False)

    def clean(self):
        cd = super(MoveForm, self).clean()
        cd = super().clean()
        if cd.get('slug'):
            dest_path = get_object_or_404(models.URLPath, pk=self.cleaned_data['destination'])
            cd['slug'] = _clean_slug(cd['slug'], dest_path)


@@ 261,13 260,13 @@ class EditForm(forms.Form, SpamProtectionMixin):

            kwargs['initial'] = initial

        super(EditForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def clean_title(self):
        title = self.cleaned_data.get('title', None)
        title = (title or "").strip()
        if not title:
            raise forms.ValidationError(ugettext('Article is missing title or has an invalid title'))
            raise forms.ValidationError(gettext('Article is missing title or has an invalid title'))
        return title

    def clean(self):


@@ 275,16 274,16 @@ class EditForm(forms.Form, SpamProtectionMixin):
        No new revisions have been created since user attempted to edit
        Revision title or content has changed
        """
        cd = super(EditForm, self).clean()
        cd = super().clean()
        if self.no_clean or self.preview:
            return cd
        if not str(self.initial_revision.id) == str(self.presumed_revision):
            raise forms.ValidationError(
                ugettext(
                gettext(
                    'While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below.'))
        if ('title' in cd) and cd['title'] == self.initial_revision.title and cd[
                'content'] == self.initial_revision.content:
            raise forms.ValidationError(ugettext('No changes made. Nothing to save.'))
            raise forms.ValidationError(gettext('No changes made. Nothing to save.'))
        self.check_spam()
        return cd



@@ 299,10 298,10 @@ class SelectWidgetBootstrap(BuildAttrsCompat, forms.Select):
        attrs['class'] = 'btn-group pull-left btn-group-form'
        self.disabled = disabled
        self.noscript_widget = forms.Select(attrs={}, choices=choices)
        super(SelectWidgetBootstrap, self).__init__(attrs, choices)
        super().__init__(attrs, choices)

    def __setattr__(self, k, value):
        super(SelectWidgetBootstrap, self).__setattr__(k, value)
        super().__setattr__(k, value)
        if k not in ('attrs', 'disabled'):
            self.noscript_widget.__setattr__(k, value)



@@ 361,10 360,10 @@ class TextInputPrepend(forms.TextInput):

    def __init__(self, *args, **kwargs):
        self.prepend = kwargs.pop('prepend', "")
        super(TextInputPrepend, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def render(self, *args, **kwargs):
        html = super(TextInputPrepend, self).render(*args, **kwargs)
        html = super().render(*args, **kwargs)
        return mark_safe(
            '<div class="input-group"><span class="input-group-addon">%s</span>%s</div>' %
            (self.prepend, html))


@@ 373,7 372,7 @@ class TextInputPrepend(forms.TextInput):
class CreateForm(forms.Form, SpamProtectionMixin):

    def __init__(self, request, urlpath_parent, *args, **kwargs):
        super(CreateForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        self.request = request
        self.urlpath_parent = urlpath_parent



@@ 397,7 396,7 @@ class CreateForm(forms.Form, SpamProtectionMixin):
        return _clean_slug(self.cleaned_data['slug'], self.urlpath_parent)

    def clean(self):
        super(CreateForm, self).clean()
        super().clean()
        self.check_spam()
        return self.cleaned_data



@@ 407,7 406,7 @@ class DeleteForm(forms.Form):
    def __init__(self, *args, **kwargs):
        self.article = kwargs.pop('article')
        self.has_children = kwargs.pop('has_children')
        super(DeleteForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    confirm = forms.BooleanField(required=False,
                                 label=_('Yes, I am sure'))


@@ 420,12 419,12 @@ class DeleteForm(forms.Form):
                                      widget=HiddenInput(), required=False)

    def clean(self):
        cd = super(DeleteForm, self).clean()
        cd = super().clean()
        if not cd['confirm']:
            raise forms.ValidationError(ugettext('You are not sure enough!'))
            raise forms.ValidationError(gettext('You are not sure enough!'))
        if cd['revision'] != self.article.current_revision:
            raise forms.ValidationError(
                ugettext(
                gettext(
                    'While you tried to delete this article, it was modified. TAKE CARE!'))
        return cd



@@ 481,7 480,7 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):
        kwargs['instance'] = article
        kwargs['initial'] = {'locked': article.current_revision.locked}

        super(PermissionsForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

        self.can_change_groups = False
        self.can_assign = False


@@ 532,7 531,7 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):
                    user = User.objects.get(**kwargs)
                except User.DoesNotExist:
                    raise forms.ValidationError(
                        ugettext('No user with that username'))
                        gettext('No user with that username'))
            else:
                user = None
        else:


@@ 540,7 539,7 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):
        return user

    def save(self, commit=True):
        article = super(PermissionsForm, self).save(commit=False)
        article = super().save(commit=False)

        # Alter the owner according to the form field owner_username
        # TODO: Why not rename this field to 'owner' so this happens


@@ 620,7 619,7 @@ class UserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True)

    def __init__(self, *args, **kwargs):
        super(UserCreationForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

        # Add honeypots
        self.honeypot_fieldnames = "address", "phone"


@@ 638,7 637,7 @@ class UserCreationForm(UserCreationForm):
            )

    def clean(self):
        cd = super(UserCreationForm, self).clean()
        cd = super().clean()
        for fieldname in self.honeypot_fieldnames:
            if cd[fieldname]:
                raise forms.ValidationError(


@@ 655,7 654,7 @@ class UserUpdateForm(forms.ModelForm):
    password2 = forms.CharField(label="Confirm password", widget=forms.PasswordInput(), required=False)

    def clean(self):
        cd = super(UserUpdateForm, self).clean()
        cd = super().clean()
        password1 = cd.get('password1')
        password2 = cd.get('password2')


M src/wiki/managers.py => src/wiki/managers.py +4 -4
@@ 11,7 11,7 @@ class ArticleQuerySet(QuerySet):
        are included"""
        if user.has_perm('wiki.moderator'):
            return self
        if user.is_anonymous():
        if user.is_anonymous:
            q = self.filter(other_read=True)
        else:
            q = self.filter(Q(other_read=True) |


@@ 25,7 25,7 @@ class ArticleQuerySet(QuerySet):
        are included"""
        if user.has_perm('wiki.moderator'):
            return self
        if user.is_anonymous():
        if user.is_anonymous:
            q = self.filter(other_write=True)
        else:
            q = self.filter(Q(other_write=True) |


@@ 57,7 57,7 @@ class ArticleFkQuerySetMixin(object):
        are included"""
        if user.has_perm('wiki.moderate'):
            return self
        if user.is_anonymous():
        if user.is_anonymous:
            q = self.filter(article__other_read=True)
        else:
            # https://github.com/django-wiki/django-wiki/issues/67


@@ 72,7 72,7 @@ class ArticleFkQuerySetMixin(object):
        are included"""
        if user.has_perm('wiki.moderate'):
            return self
        if user.is_anonymous():
        if user.is_anonymous:
            q = self.filter(article__other_write=True)
        else:
            # https://github.com/django-wiki/django-wiki/issues/67

M src/wiki/migrations/0001_initial.py => src/wiki/migrations/0001_initial.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import django.db.models.deletion
import mptt.fields
from django.conf import settings

M src/wiki/migrations/0002_urlpath_moved_to.py => src/wiki/migrations/0002_urlpath_moved_to.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-06-06 23:18
import django.db.models.deletion
import mptt.fields

M src/wiki/models/__init__.py => src/wiki/models/__init__.py +2 -4
@@ 1,8 1,6 @@
# -*- coding: utf-8 -*-
from django.apps import apps
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.six import string_types, text_type

# TODO: Don't use wildcards
from .article import *  # noqa


@@ 77,7 75,7 @@ def reverse(*args, **kwargs):
    return the result of calling reverse._transform_url(reversed_url)
    for every url in the wiki namespace.
    """
    if isinstance(args[0], string_types) and args[0].startswith('wiki:'):
    if isinstance(args[0], str) and args[0].startswith('wiki:'):
        url_kwargs = kwargs.get('kwargs', {})
        path = url_kwargs.get('path', False)
        # If a path is supplied then discard the article_id


@@ 96,7 94,7 @@ def reverse(*args, **kwargs):


# Now we redefine reverse method
reverse_lazy = lazy(reverse, text_type)
reverse_lazy = lazy(reverse, str)
urlresolvers.reverse = reverse
urlresolvers.reverse_lazy = reverse_lazy


M src/wiki/models/article.py => src/wiki/models/article.py +2 -7
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache


@@ 7,9 6,8 @@ from django.db import models
from django.db.models.fields import GenericIPAddressField as IPAddressField
from django.db.models.signals import post_save, pre_delete, pre_save
from django.utils import translation
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel
from wiki import managers
from wiki.conf import settings


@@ 18,7 16,6 @@ from wiki.core.markdown import article_markdown
from wiki.decorators import disable_signal_for_loaddata


@python_2_unicode_compatible
class Article(models.Model):

    objects = managers.ArticleManager()


@@ 248,7 245,6 @@ class Article(models.Model):
            return reverse('wiki:get', kwargs={'article_id': self.id})


@python_2_unicode_compatible
class ArticleForObject(models.Model):

    objects = managers.ArticleFkManager()


@@ 316,7 312,7 @@ class BaseRevisionMixin(models.Model):
    )

    def set_from_request(self, request):
        if request.user.is_authenticated():
        if request.user.is_authenticated:
            self.user = request.user
            if settings.LOG_IPS_USERS:
                self.ip_address = request.META.get('REMOTE_ADDR', None)


@@ 343,7 339,6 @@ class BaseRevisionMixin(models.Model):
        abstract = True


@python_2_unicode_compatible
class ArticleRevision(BaseRevisionMixin, models.Model):

    """This is where main revision data is stored. To make it easier to

M src/wiki/models/pluginbase.py => src/wiki/models/pluginbase.py +2 -3
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
"""
There are three kinds of plugin base models:



@@ 21,7 20,7 @@ There are three kinds of plugin base models:
"""
from django.db import models
from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from wiki.decorators import disable_signal_for_loaddata

from .article import ArticleRevision, BaseRevisionMixin


@@ 131,7 130,7 @@ class SimplePlugin(ArticlePlugin):

    def __init__(self, *args, **kwargs):
        article = kwargs.pop('article', None)
        super(SimplePlugin, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        if not self.pk and not article:
            raise SimplePluginCreateError(
                "Keyword argument 'article' expected.")

M src/wiki/models/urlpath.py => src/wiki/models/urlpath.py +6 -9
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import logging
import warnings



@@ 10,9 9,8 @@ from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.db.models.signals import post_save, pre_delete
# Django 1.6 transaction API, required for 1.8+
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
from wiki import managers


@@ 24,7 22,6 @@ from wiki.models.article import Article, ArticleForObject, ArticleRevision
log = logging.getLogger(__name__)


@python_2_unicode_compatible
class URLPath(MPTTModel):

    """


@@ 177,12 174,12 @@ class URLPath(MPTTModel):

    def __str__(self):
        path = self.path
        return path if path else ugettext("(root)")
        return path if path else gettext("(root)")

    def delete(self, *args, **kwargs):
        assert not (self.parent and self.get_children()
                    ), "You cannot delete a root article with children."
        super(URLPath, self).delete(*args, **kwargs)
        super().delete(*args, **kwargs)

    class Meta:
        verbose_name = _('URL path')


@@ 203,7 200,7 @@ class URLPath(MPTTModel):
                raise ValidationError(
                    _('There is already a root node on %s') %
                    self.site)
        super(URLPath, self).clean(*args, **kwargs)
        super().clean(*args, **kwargs)

    @classmethod
    def get_by_path(cls, path, select_related=False):


@@ 314,7 311,7 @@ class URLPath(MPTTModel):
        """
        user = None
        ip_address = None
        if not request.user.is_anonymous():
        if not request.user.is_anonymous:
            user = request.user
            if settings.LOG_IPS_USERS:
                ip_address = request.META.get('REMOTE_ADDR', None)

M src/wiki/plugins/attachments/apps.py => src/wiki/plugins/attachments/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class AttachmentsConfig(AppConfig):

M src/wiki/plugins/attachments/forms.py => src/wiki/plugins/attachments/forms.py +11 -12
@@ 1,11 1,10 @@
# -*- coding: utf-8 -*-
import tempfile
import zipfile

from django import forms
from django.core.files.uploadedfile import File
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from wiki.core.permissions import can_moderate
from wiki.plugins.attachments import models
from wiki.plugins.attachments.models import IllegalFileExtension


@@ 23,7 22,7 @@ class AttachmentForm(forms.ModelForm):
        self.article = kwargs.pop('article', None)
        self.request = kwargs.pop('request', None)
        self.attachment = kwargs.pop('attachment', None)
        super(AttachmentForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def clean_file(self):
        uploaded_file = self.cleaned_data.get('file', None)


@@ 36,7 35,7 @@ class AttachmentForm(forms.ModelForm):

    def save(self, *args, **kwargs):
        commit = kwargs.get('commit', True)
        attachment_revision = super(AttachmentForm, self).save(commit=False)
        attachment_revision = super().save(commit=False)

        # Added because of AttachmentArchiveForm removing file from fields
        # should be more elegant


@@ 85,7 84,7 @@ class AttachmentArchiveForm(AttachmentForm):
        required=False)

    def __init__(self, *args, **kwargs):
        super(AttachmentArchiveForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        ordered_fields = ['unzip_archive', 'file']
        self.fields.keyOrder = ordered_fields + [k
                                                 for k in self.fields.keys()


@@ 102,16 101,16 @@ class AttachmentArchiveForm(AttachmentForm):
                    except IllegalFileExtension as e:
                        raise forms.ValidationError(e)
            except zipfile.BadZipfile:
                raise forms.ValidationError(ugettext("Not a zip file"))
                raise forms.ValidationError(gettext("Not a zip file"))
        else:
            return super(AttachmentArchiveForm, self).clean_file()
            return super().clean_file()
        return uploaded_file

    def clean(self):
        super(AttachmentArchiveForm, self).clean()
        super().clean()
        if not can_moderate(self.article, self.request.user):
            raise forms.ValidationError(
                ugettext("User not allowed to moderate this article"))
                gettext("User not allowed to moderate this article"))
        return self.cleaned_data

    def save(self, *args, **kwargs):


@@ 148,7 147,7 @@ class AttachmentArchiveForm(AttachmentForm):
                raise
            return new_attachments
        else:
            return super(AttachmentArchiveForm, self).save(*args, **kwargs)
            return super().save(*args, **kwargs)

    class Meta(AttachmentForm.Meta):
        fields = ['description', ]


@@ 162,7 161,7 @@ class DeleteForm(forms.Form):

    def clean_confirm(self):
        if not self.cleaned_data['confirm']:
            raise forms.ValidationError(ugettext('You are not sure enough!'))
            raise forms.ValidationError(gettext('You are not sure enough!'))
        return True



M src/wiki/plugins/attachments/migrations/0001_initial.py => src/wiki/plugins/attachments/migrations/0001_initial.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

M src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py => src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models



M src/wiki/plugins/attachments/models.py => src/wiki/plugins/attachments/models.py +5 -9
@@ 1,12 1,10 @@
# -*- coding: utf-8 -*-
import os

from django.conf import settings as django_settings
from django.db import models
from django.db.models import signals
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from wiki import managers
from wiki.decorators import disable_signal_for_loaddata
from wiki.models.article import BaseRevisionMixin


@@ 21,7 19,6 @@ class IllegalFileExtension(Exception):
    pass


@python_2_unicode_compatible
class Attachment(ReusablePlugin):

    objects = managers.ArticleFkManager()


@@ 40,7 37,7 @@ class Attachment(ReusablePlugin):
        null=True)

    def can_write(self, user):
        if not settings.ANONYMOUS and (not user or user.is_anonymous()):
        if not settings.ANONYMOUS and (not user or user.is_anonymous):
            return False
        return ReusablePlugin.can_write(self, user)



@@ 68,12 65,12 @@ def extension_allowed(filename):
    except IndexError:
        # No extension
        raise IllegalFileExtension(
            ugettext("No file extension found in filename. That's not okay!"))
            gettext("No file extension found in filename. That's not okay!"))
    if not extension.lower() in map(
            lambda x: x.lower(),
            settings.FILE_EXTENSIONS):
        raise IllegalFileExtension(
            ugettext(
            gettext(
                "The following filename is illegal: {filename:s}. Extension "
                "has to be one of {extensions:s}"
            ).format(


@@ 115,7 112,6 @@ def upload_path(instance, filename):
    return os.path.join(upload_path, filename)


@python_2_unicode_compatible
class AttachmentRevision(BaseRevisionMixin, models.Model):

    attachment = models.ForeignKey('Attachment', on_delete=models.CASCADE)

M src/wiki/plugins/attachments/views.py => src/wiki/plugins/attachments/views.py +19 -20
@@ 1,11 1,10 @@
# -*- coding: utf-8 -*-
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View
from django.views.generic.edit import FormView
from django.views.generic.list import ListView


@@ 37,11 36,11 @@ class AttachmentView(ArticleMixin, FormView):

        # Fixing some weird transaction issue caused by adding commit_manually
        # to form_valid
        return super(AttachmentView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def form_valid(self, form):

        if (self.request.user.is_anonymous() and not settings.ANONYMOUS or
        if (self.request.user.is_anonymous and not settings.ANONYMOUS or
                not self.article.can_write(self.request.user) or
                self.article.current_revision.locked):
            return response_forbidden(self.request, self.article, self.urlpath)


@@ 65,7 64,7 @@ class AttachmentView(ArticleMixin, FormView):
            article_id=self.article.id)

    def get_form_kwargs(self):
        kwargs = super(AttachmentView, self).get_form_kwargs()
        kwargs = super().get_form_kwargs()
        kwargs['article'] = self.article
        kwargs['request'] = self.request
        return kwargs


@@ 81,8 80,8 @@ class AttachmentView(ArticleMixin, FormView):
            current_revision__deleted=True)
        kwargs['search_form'] = forms.SearchForm()
        kwargs['selected_tab'] = 'attachments'
        kwargs['anonymous_disallowed'] = self.request.user.is_anonymous() and not settings.ANONYMOUS
        return super(AttachmentView, self).get_context_data(**kwargs)
        kwargs['anonymous_disallowed'] = self.request.user.is_anonymous and not settings.ANONYMOUS
        return super().get_context_data(**kwargs)


class AttachmentHistoryView(ArticleMixin, TemplateView):


@@ 101,14 100,14 @@ class AttachmentHistoryView(ArticleMixin, TemplateView):
                models.Attachment.objects.active(),
                id=attachment_id,
                articles=article)
        return super(AttachmentHistoryView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['attachment'] = self.attachment
        kwargs['revisions'] = self.attachment.attachmentrevision_set.all().order_by(
            '-revision_number')
        kwargs['selected_tab'] = 'attachments'
        return super(AttachmentHistoryView, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class AttachmentReplaceView(ArticleMixin, FormView):


@@ 118,7 117,7 @@ class AttachmentReplaceView(ArticleMixin, FormView):

    @method_decorator(get_article(can_write=True, not_locked=True))
    def dispatch(self, request, article, attachment_id, *args, **kwargs):
        if request.user.is_anonymous() and not settings.ANONYMOUS:
        if request.user.is_anonymous and not settings.ANONYMOUS:
            return response_forbidden(request, article, kwargs.get('urlpath', None))
        if article.can_moderate(request.user):
            self.attachment = get_object_or_404(


@@ 132,7 131,7 @@ class AttachmentReplaceView(ArticleMixin, FormView):
                id=attachment_id,
                articles=article)
            self.can_moderate = False
        return super(AttachmentReplaceView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_form_class(self):
        if self.can_moderate:


@@ 182,13 181,13 @@ class AttachmentReplaceView(ArticleMixin, FormView):
            article_id=self.article.id)

    def get_form(self, form_class=None):
        form = super(AttachmentReplaceView, self).get_form(form_class=form_class)
        form = super().get_form(form_class=form_class)
        form.fields['file'].help_text = _(
            'Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed.')
        return form

    def get_form_kwargs(self):
        kwargs = super(AttachmentReplaceView, self).get_form_kwargs()
        kwargs = super().get_form_kwargs()
        kwargs['article'] = self.article
        kwargs['request'] = self.request
        kwargs['attachment'] = self.attachment


@@ 202,7 201,7 @@ class AttachmentReplaceView(ArticleMixin, FormView):
            kwargs['form'] = self.get_form()
        kwargs['attachment'] = self.attachment
        kwargs['selected_tab'] = 'attachments'
        return super(AttachmentReplaceView, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class AttachmentDownloadView(ArticleMixin, View):


@@ 227,7 226,7 @@ class AttachmentDownloadView(ArticleMixin, View):
                attachment__articles=article)
        else:
            self.revision = self.attachment.current_revision
        return super(AttachmentDownloadView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        if self.revision:


@@ 266,7 265,7 @@ class AttachmentChangeRevisionView(ArticleMixin, View):
            models.AttachmentRevision,
            id=revision_id,
            attachment__articles=article)
        return super(AttachmentChangeRevisionView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.attachment.current_revision = self.revision


@@ 297,7 296,7 @@ class AttachmentAddView(ArticleMixin, View):
            models.Attachment.objects.active().can_write(
                request.user),
            id=attachment_id)
        return super(AttachmentAddView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        if not self.attachment.articles.filter(id=self.article.id):


@@ 329,7 328,7 @@ class AttachmentDeleteView(ArticleMixin, FormView):
        self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
        if not self.attachment.can_delete(request.user):
            return response_forbidden(request, article, kwargs.get('urlpath', None))
        return super(AttachmentDeleteView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def form_valid(self, form):



@@ 359,7 358,7 @@ class AttachmentDeleteView(ArticleMixin, FormView):
        kwargs['selected_tab'] = 'attachments'
        if 'form' not in kwargs:
            kwargs['form'] = self.get_form()
        return super(AttachmentDeleteView, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class AttachmentSearchView(ArticleMixin, ListView):


@@ 372,7 371,7 @@ class AttachmentSearchView(ArticleMixin, ListView):

    @method_decorator(get_article(can_write=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(AttachmentSearchView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_queryset(self):
        self.query = self.request.GET.get('query', None)

M src/wiki/plugins/attachments/wiki_plugin.py => src/wiki/plugins/attachments/wiki_plugin.py +1 -2
@@ 1,6 1,5 @@
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from wiki.core.plugins import registry
from wiki.core.plugins.base import BasePlugin
from wiki.plugins.attachments import models, settings, views

M src/wiki/plugins/globalhistory/apps.py => src/wiki/plugins/globalhistory/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class GlobalHistoryConfig(AppConfig):

M src/wiki/plugins/globalhistory/models.py => src/wiki/plugins/globalhistory/models.py +0 -1
@@ 1,1 0,0 @@
# -*- coding: utf-8 -*-

M src/wiki/plugins/globalhistory/views.py => src/wiki/plugins/globalhistory/views.py +2 -2
@@ 17,7 17,7 @@ class GlobalHistory(ListView):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        self.only_last = kwargs.get('only_last', 0)
        return super(GlobalHistory, self).dispatch(
        return super().dispatch(
            request, *args, **kwargs)

    def get_queryset(self):


@@ 29,4 29,4 @@ class GlobalHistory(ListView):

    def get_context_data(self, **kwargs):
        kwargs['only_last'] = self.only_last
        return super(GlobalHistory, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)

M src/wiki/plugins/haystack/__init__.py => src/wiki/plugins/haystack/__init__.py +2 -2
@@ 21,7 21,7 @@ class SearchViewHaystack(haystack_views.SearchView):

    def dispatch(self, request, *args, **kwargs):
        # Do not allow anonymous users to search if they cannot read content
        if request.user.is_anonymous() and not settings.ANONYMOUS:
        if request.user.is_anonymous and not settings.ANONYMOUS:
            return redirect(settings.LOGIN_URL)
        return super().dispatch(request, *args, **kwargs)



@@ 30,7 30,7 @@ class SearchViewHaystack(haystack_views.SearchView):
         are included"""
        if user.has_perm('wiki.moderator'):
            return self.results
        if user.is_anonymous():
        if user.is_anonymous:
            q = self.results.filter(other_read='True')
            return q
        else:

M src/wiki/plugins/haystack/views.py => src/wiki/plugins/haystack/views.py +1 -1
@@ 12,7 12,7 @@ class HaystackSearchView(SearchView):

    def get_queryset(self):
        qs = SearchQuerySet().all()
        if self.request.user.is_authenticated():
        if self.request.user.is_authenticated:
            if not permissions.can_moderate(
                    models.URLPath.root().article,
                    self.request.user):

M src/wiki/plugins/help/apps.py => src/wiki/plugins/help/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class HelpConfig(AppConfig):

M src/wiki/plugins/help/wiki_plugin.py => src/wiki/plugins/help/wiki_plugin.py +1 -1
@@ 1,4 1,4 @@
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from wiki.core.plugins import registry
from wiki.core.plugins.base import BasePlugin


M src/wiki/plugins/images/admin.py => src/wiki/plugins/images/admin.py +1 -1
@@ 11,7 11,7 @@ class ImageForm(forms.ModelForm):
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(ImageForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        if self.instance.pk:
            revisions = models.ImageRevision.objects.filter(
                plugin=self.instance)

M src/wiki/plugins/images/apps.py => src/wiki/plugins/images/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class ImagesConfig(AppConfig):

M src/wiki/plugins/images/forms.py => src/wiki/plugins/images/forms.py +10 -10
@@ 1,6 1,6 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from wiki.core.plugins.base import PluginSidebarFormMixin
from wiki.plugins.images import models



@@ 10,11 10,11 @@ class SidebarForm(PluginSidebarFormMixin):
    def __init__(self, article, request, *args, **kwargs):
        self.article = article
        self.request = request
        super(SidebarForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        self.fields['image'].required = True

    def get_usermessage(self):
        return ugettext(
        return gettext(
            "New image %s was successfully uploaded. You can use it by selecting it from the list of available images.") % self.instance.get_filename()

    def save(self, *args, **kwargs):


@@ 22,11 22,11 @@ class SidebarForm(PluginSidebarFormMixin):
            image = models.Image()
            image.article = self.article
            kwargs['commit'] = False
            revision = super(SidebarForm, self).save(*args, **kwargs)
            revision = super().save(*args, **kwargs)
            revision.set_from_request(self.request)
            image.add_revision(self.instance, save=True)
            return revision
        return super(SidebarForm, self).save(*args, **kwargs)
        return super().save(*args, **kwargs)

    class Meta:
        model = models.ImageRevision


@@ 38,19 38,19 @@ class RevisionForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.image = kwargs.pop('image')
        self.request = kwargs.pop('request')
        super(RevisionForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        self.fields['image'].required = True

    def save(self, *args, **kwargs):
        if not self.instance.id:
            kwargs['commit'] = False
            revision = super(RevisionForm, self).save(*args, **kwargs)
            revision = super().save(*args, **kwargs)
            revision.inherit_predecessor(self.image, skip_image_file=True)
            revision.deleted = False  # Restore automatically if deleted
            revision.set_from_request(self.request)
            self.image.add_revision(self.instance, save=True)
            return revision
        return super(RevisionForm, self).save(*args, **kwargs)
        return super().save(*args, **kwargs)

    class Meta:
        model = models.ImageRevision


@@ 64,5 64,5 @@ class PurgeForm(forms.Form):
    def clean_confirm(self):
        confirm = self.cleaned_data['confirm']
        if not confirm:
            raise forms.ValidationError(ugettext('You are not sure enough!'))
            raise forms.ValidationError(gettext('You are not sure enough!'))
        return confirm

M src/wiki/plugins/images/markdown_extensions.py => src/wiki/plugins/images/markdown_extensions.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import re

import markdown

M src/wiki/plugins/images/migrations/0001_initial.py => src/wiki/plugins/images/migrations/0001_initial.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import wiki.plugins.images.models
from django.db import migrations, models


M src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py => src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models



M src/wiki/plugins/images/models.py => src/wiki/plugins/images/models.py +7 -10
@@ 5,9 5,8 @@ from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models import signals
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from wiki.models.pluginbase import RevisionPlugin, RevisionPluginRevision

from . import settings


@@ 29,14 28,13 @@ def upload_path(instance, filename):
    return os.path.join(upload_path, filename)


@python_2_unicode_compatible
class Image(RevisionPlugin):

    # The plugin system is so awesome that the inheritor doesn't need to do
    # anything! :D

    def can_write(self, user):
        if not settings.ANONYMOUS and (not user or user.is_anonymous()):
        if not settings.ANONYMOUS and (not user or user.is_anonymous):
            return False
        return RevisionPlugin.can_write(self, user)



@@ 50,12 48,11 @@ class Image(RevisionPlugin):

    def __str__(self):
        if self.current_revision:
            return ugettext('Image: %s') % self.current_revision.imagerevision.get_filename()
            return gettext('Image: %s') % self.current_revision.imagerevision.get_filename()
        else:
            return ugettext('Current revision not set!!')
            return gettext('Current revision not set!!')


@python_2_unicode_compatible
class ImageRevision(RevisionPluginRevision):

    image = models.ImageField(upload_to=upload_path,


@@ 91,7 88,7 @@ class ImageRevision(RevisionPluginRevision):
        be unset if it's the initial history entry.
        """
        predecessor = image.current_revision.imagerevision
        super(ImageRevision, self).inherit_predecessor(image)
        super().inherit_predecessor(image)
        self.plugin = predecessor.plugin
        self.deleted = predecessor.deleted
        self.locked = predecessor.locked


@@ 111,7 108,7 @@ class ImageRevision(RevisionPluginRevision):
        ordering = ('-created',)

    def __str__(self):
        return ugettext('Image Revision: %d') % self.revision_number
        return gettext('Image Revision: %d') % self.revision_number


def on_image_revision_delete(instance, *args, **kwargs):

M src/wiki/plugins/images/templatetags/wiki_images_tags.py => src/wiki/plugins/images/templatetags/wiki_images_tags.py +1 -1
@@ 13,6 13,6 @@ def images_for_article(article):

@register.filter
def images_can_add(article, user):
    if not settings.ANONYMOUS and (not user or user.is_anonymous()):
    if not settings.ANONYMOUS and (not user or user.is_anonymous):
        return False
    return article.can_write(user)

M src/wiki/plugins/images/views.py => src/wiki/plugins/images/views.py +5 -5
@@ 4,7 4,7 @@ from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.views.generic.base import RedirectView
from django.views.generic.edit import FormView
from django.views.generic.list import ListView


@@ 28,7 28,7 @@ class ImageView(ArticleMixin, ListView):

    @method_decorator(get_article(can_read=True, not_locked=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(ImageView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_queryset(self):
        if (self.article.can_moderate(self.request.user) or


@@ 93,7 93,7 @@ class PurgeView(ArticleMixin, FormView):
    def dispatch(self, request, article, *args, **kwargs):
        self.image = get_object_or_404(models.Image, article=article,
                                       id=kwargs.get('image_id', None))
        return super(PurgeView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def form_valid(self, form):



@@ 165,7 165,7 @@ class RevisionAddView(ArticleMixin, FormView):
        return ArticleMixin.dispatch(self, request, article, *args, **kwargs)

    def get_form_kwargs(self, **kwargs):
        kwargs = super(RevisionAddView, self).get_form_kwargs(**kwargs)
        kwargs = super().get_form_kwargs(**kwargs)
        kwargs['image'] = self.image
        kwargs['request'] = self.request
        return kwargs


@@ 175,7 175,7 @@ class RevisionAddView(ArticleMixin, FormView):
        # with the form instance
        if 'form' not in kwargs:
            kwargs['form'] = self.get_form()
        kwargs = super(RevisionAddView, self).get_context_data(**kwargs)
        kwargs = super().get_context_data(**kwargs)
        kwargs['image'] = self.image
        return kwargs


M src/wiki/plugins/images/wiki_plugin.py => src/wiki/plugins/images/wiki_plugin.py +1 -2
@@ 1,6 1,5 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from wiki.core.plugins import registry
from wiki.core.plugins.base import BasePlugin
from wiki.plugins.images import forms, models, settings, views

M src/wiki/plugins/links/apps.py => src/wiki/plugins/links/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class LinksConfig(AppConfig):

M src/wiki/plugins/links/mdx/urlize.py => src/wiki/plugins/links/mdx/urlize.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import re

import markdown

M src/wiki/plugins/links/wiki_plugin.py => src/wiki/plugins/links/wiki_plugin.py +1 -2
@@ 1,7 1,6 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from wiki.core.plugins import registry
from wiki.core.plugins.base import BasePlugin
from wiki.plugins.links import settings, views

M src/wiki/plugins/macros/apps.py => src/wiki/plugins/macros/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class MacrosConfig(AppConfig):

M src/wiki/plugins/macros/mdx/macro.py => src/wiki/plugins/macros/mdx/macro.py +2 -4
@@ 1,10 1,8 @@
# -*- coding: utf-8 -*-
import re

import markdown
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.utils.six import string_types
from django.utils.translation import gettext as _
from wiki.plugins.macros import settings

# See:


@@ 52,7 50,7 @@ class MacroPreprocessor(markdown.preprocessors.Preprocessor):
                            value = kwarg.group('value')
                            if value is None:
                                value = True
                            if isinstance(value, string_types):
                            if isinstance(value, str):
                                # If value is enclosed with ': Remove and
                                # remove escape sequences
                                if value.startswith("'") and len(value) > 2:

M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py => src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +1 -1
@@ 15,7 15,7 @@ def article_list(context, urlpath, depth):
    return context


@register.assignment_tag
@register.simple_tag
def allowed_macros():
    for method in settings.METHODS:
        try:

M src/wiki/plugins/macros/wiki_plugin.py => src/wiki/plugins/macros/wiki_plugin.py +1 -1
@@ 1,4 1,4 @@
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from wiki.core.plugins import registry
from wiki.core.plugins.base import BasePlugin
from wiki.plugins.macros import settings

M src/wiki/plugins/notifications/apps.py => src/wiki/plugins/notifications/apps.py +1 -1
@@ 1,5 1,5 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class NotificationsConfig(AppConfig):

M src/wiki/plugins/notifications/forms.py => src/wiki/plugins/notifications/forms.py +13 -13
@@ 2,8 2,8 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
from django.forms.models import BaseModelFormSet, modelformset_factory
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from django_nyt.models import NotificationType, Settings, Subscription
from wiki.core.plugins.base import PluginSettingsFormMixin
from wiki.plugins.notifications import models


@@ 13,7 13,7 @@ from wiki.plugins.notifications.settings import ARTICLE_EDIT
class SettingsModelChoiceField(forms.ModelChoiceField):

    def label_from_instance(self, obj):
        return ugettext(
        return gettext(
            "Receive notifications %(interval)s"
        ) % {
            'interval': obj.get_interval_display()


@@ 24,7 24,7 @@ class ArticleSubscriptionModelMultipleChoiceField(
        forms.ModelMultipleChoiceField):

    def label_from_instance(self, obj):
        return ugettext("%(title)s - %(url)s") % {
        return gettext("%(title)s - %(url)s") % {
            'title': obj.article.current_revision.title,
            'url': obj.article.get_absolute_url()
        }


@@ 33,7 33,7 @@ class ArticleSubscriptionModelMultipleChoiceField(
class SettingsModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(SettingsModelForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        instance = kwargs.get('instance', None)
        self.__editing_instance = False
        if instance:


@@ 41,17 41,17 @@ class SettingsModelForm(forms.ModelForm):
            self.fields['delete_subscriptions'] = ArticleSubscriptionModelMultipleChoiceField(
                models.ArticleSubscription.objects.filter(
                    subscription__settings=instance),
                label=ugettext("Remove subscriptions"),
                label=gettext("Remove subscriptions"),
                required=False,
                help_text=ugettext("Select article subscriptions to remove from notifications"),
                help_text=gettext("Select article subscriptions to remove from notifications"),
                initial=models.ArticleSubscription.objects.none(),
            )
            self.fields['email'] = forms.TypedChoiceField(
                label=_("Email digests"),
                choices=(
                    (0, ugettext('Unchanged (selected on each article)')),
                    (1, ugettext('No emails')),
                    (2, ugettext('Email on any change')),
                    (0, gettext('Unchanged (selected on each article)')),
                    (1, gettext('No emails')),
                    (2, gettext('Email on any change')),
                ),
                coerce=lambda x: int(x) if x is not None else None,
                widget=forms.RadioSelect(),


@@ 60,7 60,7 @@ class SettingsModelForm(forms.ModelForm):
            )

    def save(self, *args, **kwargs):
        instance = super(SettingsModelForm, self).save(*args, **kwargs)
        instance = super().save(*args, **kwargs)
        if self.__editing_instance:
            # Django < 1.5 returns list objects when ModelMultipleChoiceField
            # is empty.. so check before calling delete()


@@ 81,7 81,7 @@ class BaseSettingsFormSet(BaseModelFormSet):

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super(BaseSettingsFormSet, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def get_queryset(self):
        return Settings.objects.filter(


@@ 152,7 152,7 @@ class SubscriptionForm(PluginSettingsFormMixin, forms.Form):
                'settings': self.default_settings,
            }
        kwargs['initial'] = initial
        super(SubscriptionForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        self.fields['settings'].queryset = Settings.objects.filter(
            user=request.user,
        )

M src/wiki/plugins/notifications/migrations/0001_initial.py => src/wiki/plugins/notifications/migrations/0001_initial.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models



M src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py => src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models



M src/wiki/plugins/notifications/models.py => src/wiki/plugins/notifications/models.py +1 -4
@@ 1,9 1,7 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_nyt.models import Subscription
from django_nyt.utils import notify
from wiki import models as wiki_models


@@ 14,7 12,6 @@ from wiki.plugins.notifications import settings
from wiki.plugins.notifications.util import get_title


@python_2_unicode_compatible
class ArticleSubscription(ArticlePlugin):

    subscription = models.OneToOneField(Subscription, on_delete=models.CASCADE)

M src/wiki/plugins/notifications/util.py => src/wiki/plugins/notifications/util.py +1 -1
@@ 1,4 1,4 @@
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _


def get_title(article):

M src/wiki/plugins/notifications/views.py => src/wiki/plugins/notifications/views.py +2 -2
@@ 2,7 2,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.views.generic.edit import FormView

from . import forms, models


@@ 15,7 15,7 @@ class NotificationSettings(FormView):

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(NotificationSettings, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, formset):
        for form in formset:

M src/wiki/templatetags/wiki_tags.py => src/wiki/templatetags/wiki_tags.py +2 -3
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import re

from django.apps import apps


@@ 22,7 21,7 @@ register = template.Library()
_cache = {}


@register.assignment_tag(takes_context=True)
@register.simple_tag(takes_context=True)
def article_for_object(context, obj):
    if not isinstance(obj, Model):
        raise TypeError(


@@ 186,7 185,7 @@ def is_locked(model):
    return (model.current_revision and model.current_revision.locked)


@register.assignment_tag(takes_context=True)
@register.simple_tag(takes_context=True)
def login_url(context):
    request = context['request']
    qs = request.META.get('QUERY_STRING', '')

M src/wiki/urls.py => src/wiki/urls.py +0 -1
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from wiki.conf import settings
from wiki.core.plugins import registry

M src/wiki/views/accounts.py => src/wiki/views/accounts.py +12 -13
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
"""Here is a very basic handling of accounts.
If you have your own account handling, don't worry,
just switch off account handling in


@@ 17,7 16,7 @@ from django.contrib.auth import logout as auth_logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.views.generic.base import View
from django.views.generic.edit import CreateView, FormView, UpdateView
from wiki import forms


@@ 33,7 32,7 @@ class Signup(CreateView):

    def dispatch(self, request, *args, **kwargs):
        # Let logged in super users continue
        if not request.user.is_anonymous() and not request.user.is_superuser:
        if not request.user.is_anonymous and not request.user.is_superuser:
            return redirect('wiki:root')
        # If account handling is disabled, don't go here
        if not settings.ACCOUNT_HANDLING:


@@ 43,10 42,10 @@ class Signup(CreateView):
            c = {'error_msg': _('Account signup is only allowed for administrators.')}
            return render(request, "wiki/error.html", context=c)

        return super(Signup, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(Signup, self).get_context_data(**kwargs)
        context = super().get_context_data(**kwargs)
        context['honeypot_class'] = context['form'].honeypot_class
        context['honeypot_jsfunction'] = context['form'].honeypot_jsfunction
        return context


@@ 63,7 62,7 @@ class Logout(View):
    def dispatch(self, request, *args, **kwargs):
        if not settings.ACCOUNT_HANDLING:
            return redirect(settings.LOGOUT_URL)
        return super(Logout, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        auth_logout(request)


@@ 77,26 76,26 @@ class Login(FormView):
    template_name = "wiki/accounts/login.html"

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_anonymous():
        if not request.user.is_anonymous:
            return redirect('wiki:root')
        if not settings.ACCOUNT_HANDLING:
            return redirect(settings.LOGIN_URL)
        return super(Login, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        self.request.session.set_test_cookie()
        kwargs = super(Login, self).get_form_kwargs()
        kwargs = super().get_form_kwargs()
        kwargs['request'] = self.request
        return kwargs

    def post(self, request, *args, **kwargs):
        self.referer = request.session.get('login_referer', '')
        return super(Login, self).post(request, *args, **kwargs)
        return super().post(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        self.referer = request.META.get('HTTP_REFERER', '')
        request.session['login_referer'] = self.referer
        return super(Login, self).get(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

    def form_valid(self, form, *args, **kwargs):
        auth_login(self.request, form.get_user())


@@ 125,11 124,11 @@ class Update(UpdateView):
        """
        self.referer = request.META.get('HTTP_REFERER', '')
        request.session['login_referer'] = self.referer
        return super(Update, self).get(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.referer = request.session.get('login_referer', '')
        return super(Update, self).post(request, *args, **kwargs)
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        pw = form.cleaned_data["password1"]

M src/wiki/views/article.py => src/wiki/views/article.py +31 -33
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
import difflib
import logging



@@ 10,8 9,8 @@ from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django.views.generic import DetailView
from django.views.generic.base import RedirectView, TemplateView, View
from django.views.generic.edit import FormView


@@ 37,7 36,7 @@ class ArticleView(ArticleMixin, TemplateView):

    @method_decorator(get_article(can_read=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(ArticleView, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['selected_tab'] = 'view'


@@ 51,8 50,7 @@ class Create(FormView, ArticleMixin):

    @method_decorator(get_article(can_write=True, can_create=True))
    def dispatch(self, request, article, *args, **kwargs):

        return super(Create, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_form(self, form_class=None):
        """


@@ 159,13 157,13 @@ class Delete(FormView, ArticleMixin):
                else:
                    self.cannot_delete_root = True

        return super(Delete, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_initial(self):
        return {'revision': self.article.current_revision}

    def get_form(self, form_class=None):
        form = super(Delete, self).get_form(form_class=form_class)
        form = super().get_form(form_class=form_class)
        if self.article.can_moderate(self.request.user):
            form.fields['purge'].widget = forms.forms.CheckboxInput()
        return form


@@ 231,7 229,7 @@ class Delete(FormView, ArticleMixin):
        kwargs['delete_children'] = self.children_slice[:20]
        kwargs['delete_children_more'] = len(self.children_slice) > 20
        kwargs['cannot_delete_children'] = cannot_delete_children
        return super(Delete, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class Edit(ArticleMixin, FormView):


@@ 245,7 243,7 @@ class Edit(ArticleMixin, FormView):
    def dispatch(self, request, article, *args, **kwargs):
        self.sidebar_plugins = plugin_registry.get_sidebar()
        self.sidebar = []
        return super(Edit, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_initial(self):
        initial = FormView.get_initial(self)


@@ 300,7 298,7 @@ class Edit(ArticleMixin, FormView):
            else:
                form = None
            self.sidebar.append((plugin, form))
        return super(Edit, self).get(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # Generate sidebar forms


@@ 351,7 349,7 @@ class Edit(ArticleMixin, FormView):
            else:
                form = None
            self.sidebar.append((plugin, form))
        return super(Edit, self).post(request, *args, **kwargs)
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        """Create a new article revision when the edit form is valid


@@ 384,7 382,7 @@ class Edit(ArticleMixin, FormView):
        kwargs['editor'] = editors.getEditor()
        kwargs['selected_tab'] = 'edit'
        kwargs['sidebar'] = self.sidebar
        return super(Edit, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class Move(ArticleMixin, FormView):


@@ 395,7 393,7 @@ class Move(ArticleMixin, FormView):
    @method_decorator(login_required)
    @method_decorator(get_article(can_write=True, not_locked=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(Move, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_form(self, form_class=None):
        if form_class is None:


@@ 408,7 406,7 @@ class Move(ArticleMixin, FormView):
            kwargs['form'] = self.get_form()
        kwargs['root_path'] = models.URLPath.root()

        return super(Move, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)

    @transaction.atomic
    def form_valid(self, form):


@@ 491,7 489,7 @@ class Move(ArticleMixin, FormView):

            messages.success(
                self.request,
                ungettext(
                ngettext(
                    "Article successfully moved! Created {n} redirect.",
                    "Article successfully moved! Created {n} redirects.",
                    len(descendants)


@@ 554,7 552,7 @@ class Deleted(Delete):
                else:
                    return redirect('wiki:get', article_id=article.id)

        return super(Deleted, self).dispatch1(request, article, *args, **kwargs)
        return super().dispatch1(request, article, *args, **kwargs)

    def get_initial(self):
        return {'revision': self.article.current_revision,


@@ 575,7 573,7 @@ class Source(ArticleMixin, TemplateView):

    @method_decorator(get_article(can_read=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(Source, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['selected_tab'] = 'source'


@@ 605,7 603,7 @@ class History(ListView, ArticleMixin):

    @method_decorator(get_article(can_read=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(History, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)


class Dir(ListView, ArticleMixin):


@@ 624,7 622,7 @@ class Dir(ListView, ArticleMixin):
            self.query = self.filter_form.cleaned_data['query']
        else:
            self.query = None
        return super(Dir, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_queryset(self):
        children = self.urlpath.get_children().can_read(self.request.user)


@@ 666,14 664,14 @@ class SearchView(ListView):
    def dispatch(self, request, *args, **kwargs):
        self.urlpath = None
        # Do not allow anonymous users to search if they cannot read content
        if request.user.is_anonymous() and not settings.ANONYMOUS:
        if request.user.is_anonymous and not settings.ANONYMOUS:
            return redirect(settings.LOGIN_URL)
        self.search_form = forms.SearchForm(request.GET)
        if self.search_form.is_valid():
            self.query = self.search_form.cleaned_data['q']
        else:
            self.query = None
        return super(SearchView, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_queryset(self):
        if not self.query:


@@ 698,7 696,7 @@ class SearchView(ListView):
        return articles.order_by('-current_revision__created')

    def get_context_data(self, **kwargs):
        kwargs = super(SearchView, self).get_context_data(**kwargs)
        kwargs = super().get_context_data(**kwargs)
        kwargs['search_form'] = self.search_form
        kwargs['search_query'] = self.query
        kwargs['urlpath'] = self.urlpath


@@ 723,7 721,7 @@ class Settings(ArticleMixin, TemplateView):
    @method_decorator(login_required)
    @method_decorator(get_article(can_read=True))
    def dispatch(self, request, article, *args, **kwargs):
        return super(Settings, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def get_form_classes(self,):
        """


@@ 763,7 761,7 @@ class Settings(ArticleMixin, TemplateView):
            else:
                form = Form(self.article, self.request)
            self.forms.append(form)
        return super(Settings, self).get(*args, **kwargs)
        return super().get(*args, **kwargs)

    def get(self, *args, **kwargs):
        self.forms = []


@@ 775,7 773,7 @@ class Settings(ArticleMixin, TemplateView):
        for Form in self.get_form_classes():
            self.forms.append(Form(new_article, self.request))

        return super(Settings, self).get(*args, **kwargs)
        return super().get(*args, **kwargs)

    def get_success_url(self):
        if self.urlpath:


@@ 785,7 783,7 @@ class Settings(ArticleMixin, TemplateView):
    def get_context_data(self, **kwargs):
        kwargs['selected_tab'] = 'settings'
        kwargs['forms'] = self.forms
        return super(Settings, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)


class ChangeRevisionView(RedirectView):


@@ 798,7 796,7 @@ class ChangeRevisionView(RedirectView):
        self.urlpath = kwargs.pop('kwargs', False)
        self.change_revision()

        return super(ChangeRevisionView, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_redirect_url(self, **kwargs):
        if self.urlpath:


@@ 845,7 843,7 @@ class Preview(ArticleMixin, TemplateView):
            )
        else:
            self.revision = None
        return super(Preview, self).dispatch(request, article, *args, **kwargs)
        return super().dispatch(request, article, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        edit_form = forms.EditForm(request, self.article.current_revision, request.POST, preview=True)


@@ 853,14 851,14 @@ class Preview(ArticleMixin, TemplateView):
            self.title = edit_form.cleaned_data['title']
            self.content = edit_form.cleaned_data['content']
            self.preview = True
        return super(Preview, self).get(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        if self.revision and not self.title:
            self.title = self.revision.title
        if self.revision and not self.content:
            self.content = self.revision.content
        return super(Preview, self).get(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['title'] = self.title


@@ 987,7 985,7 @@ class CreateRootView(FormView):
            # then it might cascade to delete a lot of things on an existing
            # installation.... / benjaoming
            root.delete()
        return super(CreateRootView, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        models.URLPath.create_root(


@@ 998,7 996,7 @@ class CreateRootView(FormView):
        return redirect("wiki:root")

    def get_context_data(self, **kwargs):
        kwargs = super(CreateRootView, self).get_context_data(**kwargs)
        kwargs = super().get_context_data(**kwargs)
        kwargs['editor'] = editors.getEditor()
        # Needed since Django 1.9 because get_context_data is no longer called
        # with the form instance

M src/wiki/views/deleted_list.py => src/wiki/views/deleted_list.py +2 -2
@@ 12,7 12,7 @@ class DeletedListView(TemplateView):
        if not request.user.is_superuser:
            return redirect('wiki:root')

        return super(DeletedListView, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        article_list = models.Article.objects.all()


@@ 21,4 21,4 @@ class DeletedListView(TemplateView):
            if(article.current_revision.deleted):
                deleted_articles.append(article)
        kwargs['deleted_articles'] = deleted_articles
        return super(DeletedListView, self).get_context_data(**kwargs)
        return super().get_context_data(**kwargs)

M src/wiki/views/mixins.py => src/wiki/views/mixins.py +1 -1
@@ 30,7 30,7 @@ class ArticleMixin(TemplateResponseMixin):
                    "Attribute error most likely caused by wrong MPTT version. Use 0.5.3+.\n\n" +
                    str(e))
                raise
        return super(ArticleMixin, self).dispatch(request, *args, **kwargs)
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['urlpath'] = self.urlpath

M testproject/manage.py => testproject/manage.py +0 -2
@@ 1,6 1,4 @@
#!/usr/bin/env python
from __future__ import unicode_literals

import os
import sys


M testproject/testproject/settings/__init__.py => testproject/testproject/settings/__init__.py +0 -3
@@ 1,6 1,3 @@
from __future__ import unicode_literals, absolute_import


try:
    from .local import *
except ImportError:

M testproject/testproject/settings/base.py => testproject/testproject/settings/base.py +0 -3
@@ 1,4 1,3 @@
# -*- coding: utf-8 -*-
"""
Generated by 'django-admin startproject' using Django 1.9.5.



@@ 9,8 8,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""

from __future__ import unicode_literals

import os

from django.core.urlresolvers import reverse_lazy

M testproject/testproject/settings/codehilite.py => testproject/testproject/settings/codehilite.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from testproject.settings import *
from testproject.settings.local import *


M testproject/testproject/settings/customauthuser.py => testproject/testproject/settings/customauthuser.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

import os  # noqa @UnusedImport

from .base import *  # noqa @UnusedWildImport

M testproject/testproject/settings/haystack.py => testproject/testproject/settings/haystack.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from .base import *  # noqa @UnusedWildImport



M testproject/testproject/settings/sendfile.py => testproject/testproject/settings/sendfile.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from .base import *  # noqa @UnusedWildImport



M testproject/testproject/urls.py => testproject/testproject/urls.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin

M testproject/testproject/wsgi.py => testproject/testproject/wsgi.py +0 -2
@@ 13,8 13,6 @@ middleware here, or combine a Django application with an application of another
framework.

"""
from __future__ import unicode_literals

import os
import sys


M tests/base.py => tests/base.py +4 -6
@@ 1,5 1,3 @@
from __future__ import unicode_literals

import os
import unittest



@@ 18,7 16,7 @@ SUPERUSER1_PASSWORD = 'secret'
class RequireSuperuserMixin(object):

    def setUp(self):
        super(RequireSuperuserMixin, self).setUp()
        super().setUp()

        from django.contrib.auth import get_user_model
        User = get_user_model()


@@ 44,7 42,7 @@ class TestBase(RequireBasicData, TestCase):
class RequireRootArticleMixin(object):

    def setUp(self):
        super(RequireRootArticleMixin, self).setUp()
        super().setUp()
        self.root = URLPath.create_root()
        self.root_article = URLPath.root().article
        rev = self.root_article.current_revision


@@ 62,7 60,7 @@ class ArticleTestBase(RequireRootArticleMixin, TestBase):

class DjangoClientTestBase(TestBase):
    def setUp(self):
        super(DjangoClientTestBase, self).setUp()
        super().setUp()

        self.client.login(username=SUPERUSER1_USERNAME, password=SUPERUSER1_PASSWORD)



@@ 72,7 70,7 @@ class WebTestCommonMixin(RequireBasicData, django_functest.ShortcutLoginMixin):
    Common setup required for WebTest and Selenium tests
    """
    def setUp(self):
        super(WebTestCommonMixin, self).setUp()
        super().setUp()

        self.shortcut_login(username=SUPERUSER1_USERNAME,
                            password=SUPERUSER1_PASSWORD)

M tests/core/test_accounts.py => tests/core/test_accounts.py +0 -3
@@ 1,6 1,3 @@
from __future__ import print_function, unicode_literals


from django.conf import settings as django_settings
from django.contrib.auth import authenticate
from django.shortcuts import resolve_url

M tests/core/test_basic.py => tests/core/test_basic.py +0 -2
@@ 1,5 1,3 @@
from __future__ import print_function, unicode_literals

from django.test import TestCase

from wiki.conf import settings as wiki_settings

M tests/core/test_commands.py => tests/core/test_commands.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

import os
import sys
import tempfile

M tests/core/test_managers.py => tests/core/test_managers.py +0 -2
@@ 4,8 4,6 @@ because the pattern of building them is different from Django
1.5 to 1.6 to 1.7 so there will be 3 patterns in play at the
same time.
"""
from __future__ import print_function, unicode_literals

from wiki.models import Article, URLPath
from wiki.plugins.attachments.models import Attachment


M tests/core/test_markdown.py => tests/core/test_markdown.py +1 -4
@@ 1,6 1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import markdown
from django.test import TestCase
from mock import patch


@@ 43,7 40,7 @@ class ArticleMarkdownTests(ArticleTestBase):
class ResponsiveTableExtensionTests(TestCase):

    def setUp(self):
        super(ResponsiveTableExtensionTests, self).setUp()
        super().setUp()
        self.md = markdown.Markdown(extensions=[
            'extra',
            ResponsiveTableExtension()

M tests/core/test_models.py => tests/core/test_models.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from django.apps import apps
from django.conf.urls import url
from django.contrib.auth import get_user_model

M tests/core/test_template_filters.py => tests/core/test_template_filters.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from django.contrib.auth import get_user_model
from wiki.models import Article, ArticleRevision
from wiki.templatetags.wiki_tags import (can_delete, can_moderate, can_read,

M tests/core/test_template_tags.py => tests/core/test_template_tags.py +5 -9
@@ 1,13 1,9 @@
"""
Almost all test cases covers both tag calling and template using.
"""

from __future__ import print_function, unicode_literals

from django.conf import settings as django_settings
from django.contrib.contenttypes.models import ContentType
from django.http import HttpRequest
from django.utils.six import assertCountEqual

from wiki.conf import settings
from wiki.forms import CreateRootForm


@@ 30,7 26,7 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase):
    """

    def setUp(self):
        super(ArticleForObjectTemplatetagTest, self).setUp()
        super().setUp()
        from wiki.templatetags import wiki_tags
        wiki_tags._cache = {}



@@ 151,7 147,7 @@ class WikiRenderTest(TemplateTestCase):
    def tearDown(self):
        from wiki.core.plugins import registry
        registry._cache = {}
        super(WikiRenderTest, self).tearDown()
        super().tearDown()

    keys = ['article',
            'content',


@@ 171,7 167,7 @@ class WikiRenderTest(TemplateTestCase):

        output = wiki_render({}, article)

        assertCountEqual(self, self.keys, output)
        self.assertCountEqual(self.keys, output)

        self.assertEqual(output['article'], article)
        self.assertIsNone(output['content'])


@@ 210,7 206,7 @@ class WikiRenderTest(TemplateTestCase):
        registry._cache = {'spam': 'eggs'}

        output = wiki_render({}, article, preview_content=content)
        assertCountEqual(self, self.keys, output)
        self.assertCountEqual(self.keys, output)
        self.assertEqual(output['article'], article)
        self.assertMultiLineEqual(output['content'], expected_markdown)
        self.assertIs(output['preview'], True)


@@ 239,7 235,7 @@ class WikiRenderTest(TemplateTestCase):

        output = wiki_render({}, article, preview_content=content)

        assertCountEqual(self, self.keys, output)
        self.assertCountEqual(self.keys, output)

        self.assertEqual(output['article'], article)


M tests/core/test_urls.py => tests/core/test_urls.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site

M tests/core/test_views.py => tests/core/test_views.py +0 -2
@@ 1,5 1,3 @@
from __future__ import print_function, unicode_literals

import pprint

from django.contrib.auth import authenticate

M tests/plugins/attachments/test_commands.py => tests/plugins/attachments/test_commands.py +2 -4
@@ 1,5 1,3 @@
from __future__ import unicode_literals

import os
import tempfile



@@ 14,7 12,7 @@ class TestAttachmentManagementCommands(TestManagementCommands):
    """

    def setUp(self):
        super(TestAttachmentManagementCommands, self).setUp()
        super().setUp()

        self.test_file = tempfile.NamedTemporaryFile('w', delete=False, suffix=".txt")
        self.test_file.write("test")


@@ 32,4 30,4 @@ class TestAttachmentManagementCommands(TestManagementCommands):

    def tearDown(self):
        os.unlink(self.test_file.name)
        super(TestAttachmentManagementCommands, self).tearDown()
        super().tearDown()

M tests/plugins/attachments/test_views.py => tests/plugins/attachments/test_views.py +1 -3
@@ 1,5 1,3 @@
from __future__ import print_function, unicode_literals

from io import BytesIO

from django.core.files.uploadedfile import InMemoryUploadedFile


@@ 12,7 10,7 @@ from ...base import RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTe
class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):

    def setUp(self):
        super(AttachmentTests, self).setUp()
        super().setUp()
        self.article = self.root_article
        self.test_data = "This is a plain text file"
        self.test_description = 'My file'

M tests/plugins/globalhistory/__init__.py => tests/plugins/globalhistory/__init__.py +0 -1
@@ 1,1 0,0 @@
from __future__ import absolute_import

M tests/plugins/globalhistory/test_globalhistory.py => tests/plugins/globalhistory/test_globalhistory.py +0 -2
@@ 1,5 1,3 @@
from __future__ import print_function, unicode_literals

from django.core.urlresolvers import reverse
from wiki.models import URLPath


M tests/plugins/images/__init__.py => tests/plugins/images/__init__.py +0 -1
@@ 1,1 0,0 @@
from __future__ import absolute_import

M tests/plugins/images/test_views.py => tests/plugins/images/test_views.py +1 -3
@@ 1,5 1,3 @@
from __future__ import print_function, unicode_literals

import base64
from io import BytesIO



@@ 17,7 15,7 @@ from ...base import (ArticleWebTestUtils, DjangoClientTestBase,
class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):

    def setUp(self):
        super(ImageTests, self).setUp()
        super().setUp()
        self.article = self.root_article
        # A black 1x1 gif
        self.test_data = "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="

M tests/plugins/links/test_links.py => tests/plugins/links/test_links.py +0 -3
@@ 1,6 1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import markdown
from django.core.urlresolvers import reverse_lazy
from django.test import TestCase

M tests/plugins/macros/test_toc.py => tests/plugins/macros/test_toc.py +0 -4
@@ 1,7 1,3 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

import markdown
from django.test import TestCase
from wiki.plugins.macros.mdx.toc import WikiTocExtension

M tests/testdata/models.py => tests/testdata/models.py +0 -2
@@ 1,5 1,3 @@
from __future__ import absolute_import

from django.contrib.auth.models import AbstractUser
from django.db import models


M tests/testdata/urls.py => tests/testdata/urls.py +0 -2
@@ 1,5 1,3 @@
from __future__ import unicode_literals

from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin