Repository: openedx/django-wiki (language: python)

Issue:
Upgrade markdown version and fix tests
This PR is being made in order to fix the errors coming up in https://github.com/edx/edx-platform/pull/26072. 
Removed markdown constraint to update its version to the latest and fixed the failing tests.
This PR needs the base PR https://github.com/edx/django-wiki/pull/65 so that the latest version of markdown is fetched which we need in edx-platform.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..5b486340f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: Python CI
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches:
+    - '**'
+
+jobs:
+  run_tests:
+    name: Tests
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-20.04]
+        python-version: ['3.8']
+        toxenv: [django22, django30, django31]
+    steps:
+    - uses: actions/checkout@v1
+    - name: setup python
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install pip
+      run: pip install -r requirements/pip.txt
+
+    - name: Install Dependencies
+      run: pip install -r requirements/ci.txt
+
+    - name: Run Tests
+      env:
+        TOXENV: ${{ matrix.toxenv }}
+      run: tox
diff --git a/.travis.yml b/.travis.yml
index b8baa020e..c04bbe4bf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,11 @@
 language: python
 python:
-  - 3.5
   - 3.8
 install:
-  - pip install -r requirements/travis.txt
-matrix:
-  - python: 3.5
-    env: TOXENV=py35-django22
-  - python: 3.8
-    env: TOXENV=py38-django22
+  - pip install -r requirements/ci.txt
+env:
+  - TOXENV=py38-django22
+  - TOXENV=py38-django30
+  - TOXENV=py38-django31
 script:
   - tox
diff --git a/Makefile b/Makefile
index 73686e09c..aac2eaff4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,13 @@
 upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
 upgrade:
 	pip install -q -r requirements/pip_tools.txt
+	pip-compile --upgrade --allow-unsafe --rebuild -o requirements/pip.txt requirements/pip.in
 	pip-compile --upgrade -o requirements/pip_tools.txt requirements/pip_tools.in
 	pip-compile --upgrade -o requirements/base.txt requirements/base.in
 	pip-compile --upgrade -o requirements/docs.txt requirements/docs.in
 	pip-compile --upgrade -o requirements/test.txt requirements/test.in
 	pip-compile --upgrade -o requirements/tox.txt requirements/tox.in
-	pip-compile --upgrade -o requirements/travis.txt requirements/travis.in
+	pip-compile --upgrade -o requirements/ci.txt requirements/ci.in
 	# Let tox control the Django version for tests
 	grep -e "^django==" requirements/base.txt > requirements/django.txt
 	sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp
diff --git a/django_notify/admin.py b/django_notify/admin.py
index 19eaee01e..1abc3a9d3 100644
--- a/django_notify/admin.py
+++ b/django_notify/admin.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.contrib import admin
 
 from django_notify import models
diff --git a/django_notify/decorators.py b/django_notify/decorators.py
index 6d87ccca9..518881ace 100644
--- a/django_notify/decorators.py
+++ b/django_notify/decorators.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 import json
 
 from django.contrib.auth.decorators import login_required
diff --git a/django_notify/migrations/0001_initial.py b/django_notify/migrations/0001_initial.py
index 4f9972b60..4a77c0d96 100644
--- a/django_notify/migrations/0001_initial.py
+++ b/django_notify/migrations/0001_initial.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 import django.db.models.deletion
 from django.conf import settings
 from django.db import migrations, models
diff --git a/django_notify/models.py b/django_notify/models.py
index 89ffd1f57..0687bb3c1 100644
--- a/django_notify/models.py
+++ b/django_notify/models.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
@@ -15,9 +12,9 @@ class NotificationType(models.Model):
     """
     Notification types are added on-the-fly by the
     applications adding new notifications"""
-    key = models.CharField(max_length=128, primary_key=True, verbose_name=_(u'unique key'),
+    key = models.CharField(max_length=128, primary_key=True, verbose_name=_('unique key'),
                            unique=True)
-    label = models.CharField(max_length=128, verbose_name=_(u'verbose name'),
+    label = models.CharField(max_length=128, verbose_name=_('verbose name'),
                              blank=True, null=True)
     content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
     
@@ -27,23 +24,23 @@ def __unicode__(self):
     class Meta:
         app_label = 'django_notify'
         db_table = settings.DB_TABLE_PREFIX + '_notificationtype'
-        verbose_name = _(u'type')
-        verbose_name_plural = _(u'types')
+        verbose_name = _('type')
+        verbose_name_plural = _('types')
     
 class Settings(models.Model):
     
     user = models.ForeignKey(User, on_delete=models.CASCADE)
-    interval = models.SmallIntegerField(choices=settings.INTERVALS, verbose_name=_(u'interval'),
+    interval = models.SmallIntegerField(choices=settings.INTERVALS, verbose_name=_('interval'),
                                         default=settings.INTERVALS_DEFAULT)
     
     def __unicode__(self):
-        return _(u"Settings for %s") % self.user.username
+        return _("Settings for %s") % self.user.username
     
     class Meta:
         app_label = 'django_notify'
         db_table = settings.DB_TABLE_PREFIX + '_settings'
-        verbose_name = _(u'settings')
-        verbose_name_plural = _(u'settings')
+        verbose_name = _('settings')
+        verbose_name_plural = _('settings')
 
 class Subscription(models.Model):
     
@@ -51,7 +48,7 @@ class Subscription(models.Model):
     settings = models.ForeignKey(Settings, on_delete=models.CASCADE)
     notification_type = models.ForeignKey(NotificationType, on_delete=models.CASCADE)
     object_id = models.CharField(max_length=64, null=True, blank=True, 
-                                 help_text=_(u'Leave this blank to subscribe to any kind of object'))
+                                 help_text=_('Leave this blank to subscribe to any kind of object'))
     send_emails = models.BooleanField(default=True)
 
     def __unicode__(self):
@@ -60,14 +57,14 @@ def __unicode__(self):
     class Meta:
         app_label = 'django_notify'
         db_table = settings.DB_TABLE_PREFIX + '_subscription'
-        verbose_name = _(u'subscription')
-        verbose_name_plural = _(u'subscriptions')
+        verbose_name = _('subscription')
+        verbose_name_plural = _('subscriptions')
 
 class Notification(models.Model):
     
     subscription = models.ForeignKey(Subscription, null=True, blank=True, on_delete=models.SET_NULL)
     message = models.TextField()
-    url = models.URLField(blank=True, null=True, verbose_name=_(u'link for notification'))
+    url = models.URLField(blank=True, null=True, verbose_name=_('link for notification'))
     is_viewed = models.BooleanField(default=False)
     is_emailed = models.BooleanField(default=False)
     created = models.DateTimeField(auto_now_add=True)
@@ -106,8 +103,8 @@ def __unicode__(self):
     class Meta:
         app_label = 'django_notify'
         db_table = settings.DB_TABLE_PREFIX + '_notification'
-        verbose_name = _(u'notification')
-        verbose_name_plural = _(u'notifications')
+        verbose_name = _('notification')
+        verbose_name_plural = _('notifications')
 
 
 def notify(message, key, target_object=None, url=None):
@@ -133,7 +130,7 @@ def notify(message, key, target_object=None, url=None):
 
     if target_object:
         if not isinstance(target_object, Model):
-            raise TypeError(_(u"You supplied a target_object that's not an instance of a django Model."))
+            raise TypeError(_("You supplied a target_object that's not an instance of a django Model."))
         object_id = target_object.id
     else:
         object_id = None
diff --git a/django_notify/settings.py b/django_notify/settings.py
index 70dcc232b..334d42341 100644
--- a/django_notify/settings.py
+++ b/django_notify/settings.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.conf import settings as django_settings
 
 _ = lambda x: x
@@ -22,9 +20,9 @@
 WEEKLY = 7*24-1
 
 INTERVALS = getattr(django_settings, "NOTIFY_INTERVALS",
-                    [(INSTANTLY, _(u'instantly')),
-                     (DAILY, _(u'daily')),
-                     (WEEKLY, _(u'weekly'))])
+                    [(INSTANTLY, _('instantly')),
+                     (DAILY, _('daily')),
+                     (WEEKLY, _('weekly'))])
 
 INTERVALS_DEFAULT = INSTANTLY
 
diff --git a/django_notify/urls.py b/django_notify/urls.py
index 03516ddb3..90c318303 100644
--- a/django_notify/urls.py
+++ b/django_notify/urls.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.conf.urls import url
 
 from django_notify import views
@@ -8,8 +5,8 @@
 urlpatterns = [
     url('^json/get/$', views.get_notifications, name='json_get', kwargs={}),
     url('^json/mark-read/$', views.mark_read, name='json_mark_read_base', kwargs={}),
-    url('^json/mark-read/(\d+)/$', views.mark_read, name='json_mark_read', kwargs={}),
-    url('^goto/(?P<notification_id>\d+)/$', views.goto, name='goto', kwargs={}),
+    url(r'^json/mark-read/(\d+)/$', views.mark_read, name='json_mark_read', kwargs={}),
+    url(r'^goto/(?P<notification_id>\d+)/$', views.goto, name='goto', kwargs={}),
     url('^goto/$', views.goto, name='goto_base', kwargs={}),
 ]
 
diff --git a/django_notify/views.py b/django_notify/views.py
index e4866dcf5..99fda5d8a 100644
--- a/django_notify/views.py
+++ b/django_notify/views.py
@@ -1,7 +1,3 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
 from django.contrib.auth.decorators import login_required
 from django.shortcuts import get_object_or_404, redirect
 
diff --git a/docs/conf.py b/docs/conf.py
index 9456640de..4e5504f41 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # django-wiki documentation build configuration file, created by
 # sphinx-quickstart on Mon Jul 23 16:13:51 2012.
@@ -11,7 +10,6 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-from __future__ import absolute_import
 
 import os
 import sys
@@ -43,8 +41,8 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'django-wiki'
-copyright = u'2012, Benjamin Bach'
+project = 'django-wiki'
+copyright = '2012, Benjamin Bach'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -186,8 +184,8 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'django-wiki.tex', u'django-wiki Documentation',
-   u'Benjamin Bach', 'manual'),
+  ('index', 'django-wiki.tex', 'django-wiki Documentation',
+   'Benjamin Bach', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -216,8 +214,8 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'django-wiki', u'django-wiki Documentation',
-     [u'Benjamin Bach'], 1)
+    ('index', 'django-wiki', 'django-wiki Documentation',
+     ['Benjamin Bach'], 1)
 ]
 
 # If true, show URL addresses after external links.
@@ -230,8 +228,8 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'django-wiki', u'django-wiki Documentation',
-   u'Benjamin Bach', 'django-wiki', 'One line description of project.',
+  ('index', 'django-wiki', 'django-wiki Documentation',
+   'Benjamin Bach', 'django-wiki', 'One line description of project.',
    'Miscellaneous'),
 ]
 
diff --git a/requirements/base.in b/requirements/base.in
index bb1d13c64..8feb9227e 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -6,5 +6,4 @@ django
 django-mptt
 django-sekizai
 Markdown
-six					# Use for strings and unicode compatibility
 sorl-thumbnail     	# Required by wiki.plugins.images
diff --git a/requirements/base.txt b/requirements/base.txt
index 1380df0b9..71432101a 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,17 +4,36 @@
 #
 #    make upgrade
 #
-bleach==3.1.5             # via -r requirements/base.in
-django-classy-tags==1.0.0  # via django-sekizai
-django-js-asset==1.2.2    # via django-mptt
-django-mptt==0.11.0       # via -r requirements/base.in
-django-sekizai==1.1.0     # via -r requirements/base.in
-django==2.2.14            # via -c requirements/constraints.txt, -r requirements/base.in, django-classy-tags, django-mptt, django-sekizai
-markdown==2.6.11          # via -c requirements/constraints.txt, -r requirements/base.in
-packaging==20.4           # via bleach
-pyparsing==2.4.7          # via packaging
-pytz==2020.1              # via django
-six==1.15.0               # via -r requirements/base.in, bleach, django-classy-tags, django-sekizai, packaging
-sorl-thumbnail==12.6.3    # via -r requirements/base.in
-sqlparse==0.3.1           # via django
-webencodings==0.5.1       # via bleach
+bleach==3.2.1
+    # via -r requirements/base.in
+django-classy-tags==2.0.0
+    # via django-sekizai
+django-js-asset==1.2.2
+    # via django-mptt
+django-mptt==0.11.0
+    # via -r requirements/base.in
+django-sekizai==2.0.0
+    # via -r requirements/base.in
+django==2.2.17
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/base.in
+    #   django-classy-tags
+    #   django-mptt
+    #   django-sekizai
+markdown==3.3.3
+    # via -r requirements/base.in
+packaging==20.8
+    # via bleach
+pyparsing==2.4.7
+    # via packaging
+pytz==2020.5
+    # via django
+six==1.15.0
+    # via bleach
+sorl-thumbnail==12.7.0
+    # via -r requirements/base.in
+sqlparse==0.4.1
+    # via django
+webencodings==0.5.1
+    # via bleach
diff --git a/requirements/travis.in b/requirements/ci.in
similarity index 100%
rename from requirements/travis.in
rename to requirements/ci.in
diff --git a/requirements/ci.txt b/requirements/ci.txt
new file mode 100644
index 000000000..72b43e872
--- /dev/null
+++ b/requirements/ci.txt
@@ -0,0 +1,50 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+appdirs==1.4.4
+    # via
+    #   -r requirements/tox.txt
+    #   virtualenv
+distlib==0.3.1
+    # via
+    #   -r requirements/tox.txt
+    #   virtualenv
+filelock==3.0.12
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+    #   virtualenv
+packaging==20.8
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+pluggy==0.13.1
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+py==1.10.0
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+pyparsing==2.4.7
+    # via
+    #   -r requirements/tox.txt
+    #   packaging
+six==1.15.0
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+    #   virtualenv
+toml==0.10.2
+    # via
+    #   -r requirements/tox.txt
+    #   tox
+tox==3.21.1
+    # via -r requirements/tox.txt
+virtualenv==20.3.1
+    # via
+    #   -r requirements/tox.txt
+    #   tox
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index e5853f8af..8a2cc2b0a 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -12,6 +12,3 @@
 
 # Use latest Django LTS version
 Django<2.3.0
-
-# Use version 2.6 to avoid markdown errors
-Markdown<2.7
diff --git a/requirements/django.txt b/requirements/django.txt
index 3cdbc8d88..34249f7c4 100644
--- a/requirements/django.txt
+++ b/requirements/django.txt
@@ -1,1 +1,1 @@
-django==2.2.14            # via -c requirements/constraints.txt, -r requirements/base.in, django-classy-tags, django-mptt, django-sekizai
+django==2.2.17
diff --git a/requirements/docs.txt b/requirements/docs.txt
index 34bc60738..cf7b7199c 100644
--- a/requirements/docs.txt
+++ b/requirements/docs.txt
@@ -4,31 +4,58 @@
 #
 #    make upgrade
 #
-alabaster==0.7.12         # via sphinx
-babel==2.8.0              # via sphinx
-certifi==2020.6.20        # via requests
-chardet==3.0.4            # via requests
-docutils==0.16            # via sphinx
-edx-sphinx-theme==1.5.0   # via -r requirements/docs.in
-idna==2.10                # via requests
-imagesize==1.2.0          # via sphinx
-jinja2==2.11.2            # via sphinx
-markupsafe==1.1.1         # via jinja2
-packaging==20.4           # via sphinx
-pygments==2.6.1           # via sphinx
-pyparsing==2.4.7          # via packaging
-pytz==2020.1              # via babel
-requests==2.24.0          # via sphinx
-six==1.15.0               # via edx-sphinx-theme, packaging
-snowballstemmer==2.0.0    # via sphinx
-sphinx==3.1.1             # via -r requirements/docs.in, edx-sphinx-theme
-sphinxcontrib-applehelp==1.0.2  # via sphinx
-sphinxcontrib-devhelp==1.0.2  # via sphinx
-sphinxcontrib-htmlhelp==1.0.3  # via sphinx
-sphinxcontrib-jsmath==1.0.1  # via sphinx
-sphinxcontrib-qthelp==1.0.3  # via sphinx
-sphinxcontrib-serializinghtml==1.1.4  # via sphinx
-urllib3==1.25.9           # via requests
+alabaster==0.7.12
+    # via sphinx
+babel==2.9.0
+    # via sphinx
+certifi==2020.12.5
+    # via requests
+chardet==4.0.0
+    # via requests
+docutils==0.16
+    # via sphinx
+edx-sphinx-theme==1.6.0
+    # via -r requirements/docs.in
+idna==2.10
+    # via requests
+imagesize==1.2.0
+    # via sphinx
+jinja2==2.11.2
+    # via sphinx
+markupsafe==1.1.1
+    # via jinja2
+packaging==20.8
+    # via sphinx
+pygments==2.7.4
+    # via sphinx
+pyparsing==2.4.7
+    # via packaging
+pytz==2020.5
+    # via babel
+requests==2.25.1
+    # via sphinx
+six==1.15.0
+    # via edx-sphinx-theme
+snowballstemmer==2.0.0
+    # via sphinx
+sphinx==3.4.3
+    # via
+    #   -r requirements/docs.in
+    #   edx-sphinx-theme
+sphinxcontrib-applehelp==1.0.2
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2
+    # via sphinx
+sphinxcontrib-htmlhelp==1.0.3
+    # via sphinx
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.4
+    # via sphinx
+urllib3==1.26.2
+    # via requests
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools
diff --git a/requirements/pip.in b/requirements/pip.in
new file mode 100644
index 000000000..7015e2e2f
--- /dev/null
+++ b/requirements/pip.in
@@ -0,0 +1,3 @@
+pip
+setuptools
+wheel
diff --git a/requirements/pip.txt b/requirements/pip.txt
new file mode 100644
index 000000000..9acedf7fe
--- /dev/null
+++ b/requirements/pip.txt
@@ -0,0 +1,14 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+wheel==0.36.2
+    # via -r requirements/pip.in
+
+# The following packages are considered to be unsafe in a requirements file:
+pip==20.3.3
+    # via -r requirements/pip.in
+setuptools==51.1.2
+    # via -r requirements/pip.in
diff --git a/requirements/pip_tools.txt b/requirements/pip_tools.txt
index 279019f74..53310ee5c 100644
--- a/requirements/pip_tools.txt
+++ b/requirements/pip_tools.txt
@@ -4,9 +4,10 @@
 #
 #    make upgrade
 #
-click==7.1.2              # via pip-tools
-pip-tools==5.2.1          # via -r requirements/pip_tools.in
-six==1.15.0               # via pip-tools
+click==7.1.2
+    # via pip-tools
+pip-tools==5.5.0
+    # via -r requirements/pip_tools.in
 
 # The following packages are considered to be unsafe in a requirements file:
 # pip
diff --git a/requirements/tox.txt b/requirements/tox.txt
index 53bf2cd40..4214a5e52 100644
--- a/requirements/tox.txt
+++ b/requirements/tox.txt
@@ -4,17 +4,29 @@
 #
 #    make upgrade
 #
-appdirs==1.4.4            # via virtualenv
-distlib==0.3.1            # via virtualenv
-filelock==3.0.12          # via tox, virtualenv
-importlib-metadata==1.7.0  # via pluggy, tox, virtualenv
-importlib-resources==3.0.0  # via virtualenv
-packaging==20.4           # via tox
-pluggy==0.13.1            # via tox
-py==1.9.0                 # via tox
-pyparsing==2.4.7          # via packaging
-six==1.15.0               # via packaging, tox, virtualenv
-toml==0.10.1              # via tox
-tox==3.16.1               # via -r requirements/tox.in
-virtualenv==20.0.25       # via tox
-zipp==1.2.0               # via importlib-metadata, importlib-resources
+appdirs==1.4.4
+    # via virtualenv
+distlib==0.3.1
+    # via virtualenv
+filelock==3.0.12
+    # via
+    #   tox
+    #   virtualenv
+packaging==20.8
+    # via tox
+pluggy==0.13.1
+    # via tox
+py==1.10.0
+    # via tox
+pyparsing==2.4.7
+    # via packaging
+six==1.15.0
+    # via
+    #   tox
+    #   virtualenv
+toml==0.10.2
+    # via tox
+tox==3.21.1
+    # via -r requirements/tox.in
+virtualenv==20.3.1
+    # via tox
diff --git a/requirements/travis.txt b/requirements/travis.txt
deleted file mode 100644
index 498f5d84d..000000000
--- a/requirements/travis.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    make upgrade
-#
-appdirs==1.4.4            # via -r requirements/tox.txt, virtualenv
-distlib==0.3.1            # via -r requirements/tox.txt, virtualenv
-filelock==3.0.12          # via -r requirements/tox.txt, tox, virtualenv
-importlib-metadata==1.7.0  # via -r requirements/tox.txt, pluggy, tox, virtualenv
-importlib-resources==3.0.0  # via -r requirements/tox.txt, virtualenv
-packaging==20.4           # via -r requirements/tox.txt, tox
-pluggy==0.13.1            # via -r requirements/tox.txt, tox
-py==1.9.0                 # via -r requirements/tox.txt, tox
-pyparsing==2.4.7          # via -r requirements/tox.txt, packaging
-six==1.15.0               # via -r requirements/tox.txt, packaging, tox, virtualenv
-toml==0.10.1              # via -r requirements/tox.txt, tox
-tox==3.16.1               # via -r requirements/tox.txt
-virtualenv==20.0.25       # via -r requirements/tox.txt, tox
-zipp==1.2.0               # via -r requirements/tox.txt, importlib-metadata, importlib-resources
diff --git a/setup.py b/setup.py
index b550659ca..dcba30e68 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 import os
 
 from setuptools import find_packages, setup
@@ -55,17 +52,17 @@ def is_requirement(line):
 
 packages = find_packages()
 
-package_data = dict(
-    (package_name, template_patterns)
+package_data = {
+    package_name: template_patterns
     for package_name in packages
-)
+}
 
 setup(
     name="django-wiki",
-    version="0.1.1",
+    version="1.0.0",
     author="Benjamin Bach",
     author_email="benjamin@overtag.dk",
-    description=("A wiki system written for the Django framework."),
+    description="A wiki system written for the Django framework.",
     license="GPLv3",
     keywords="django wiki markdown",
     packages=find_packages(exclude=["testproject", "testproject.*"]),
@@ -80,10 +77,11 @@ def is_requirement(line):
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.8',
         'Framework :: Django',
         'Framework :: Django :: 2.2',
+        'Framework :: Django :: 3.0',
+        'Framework :: Django :: 3.1',
         'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
         'Topic :: Software Development',
         'Topic :: Software Development :: Libraries :: Application Frameworks',
diff --git a/tox.ini b/tox.ini
index 8533b7248..f2edeb42a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,12 @@
 [tox]
-envlist = {py35,py38}-django{22}
+envlist = py38-django{22,30,31}
 
 [testenv]
 deps =
     django22: -r requirements/django.txt
-    -r{toxinidir}/requirements/test.txt
+	django30: Django>=3.0,<3.1
+	django31: Django>=3.1,<3.2
+	-r{toxinidir}/requirements/test.txt
 changedir={toxinidir}/testproject/
 commands =
     pytest --cov wiki --cov django_notify
diff --git a/wiki/admin.py b/wiki/admin.py
index 6dcbc2d53..c8789338c 100644
--- a/wiki/admin.py
+++ b/wiki/admin.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django import forms
 from django.contrib import admin
 from django.contrib.contenttypes.admin import GenericTabularInline
@@ -22,7 +20,7 @@ class Meta:
         fields = '__all__'
         
     def __init__(self, *args, **kwargs):
-        super(ArticleRevisionForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         EditorClass = editors.getEditorClass()
         editor = editors.getEditor()
         self.fields['content'].widget = editor.get_admin_widget()
@@ -54,7 +52,7 @@ class Meta:
         fields = '__all__'
 
     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)
             self.fields['current_revision'].queryset = revisions
@@ -77,7 +75,7 @@ class URLPathAdmin(MPTTModelAdmin):
     
     def get_created(self, instance):
         return instance.article.created
-    get_created.short_description = _(u'created')
+    get_created.short_description = _('created')
 
 
 admin.site.register(models.URLPath, URLPathAdmin)
diff --git a/wiki/apps.py b/wiki/apps.py
index 51417b761..42fb43f76 100644
--- a/wiki/apps.py
+++ b/wiki/apps.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
 from django.apps import AppConfig
 from django.utils.translation import ugettext_lazy as _
 
diff --git a/wiki/conf/__init__.py b/wiki/conf/__init__.py
index 40a96afc6..e69de29bb 100644
--- a/wiki/conf/__init__.py
+++ b/wiki/conf/__init__.py
@@ -1,1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/wiki/conf/settings.py b/wiki/conf/settings.py
index 178727b1f..e4d8c8bfe 100644
--- a/wiki/conf/settings.py
+++ b/wiki/conf/settings.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.conf import settings as django_settings
 from django.urls import reverse_lazy
 
diff --git a/wiki/core/__init__.py b/wiki/core/__init__.py
index de5adcdf3..4a73ce8ac 100644
--- a/wiki/core/__init__.py
+++ b/wiki/core/__init__.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import bleach
 import markdown
 
diff --git a/wiki/core/compat.py b/wiki/core/compat.py
index 8beeb5f79..ad73030e4 100644
--- a/wiki/core/compat.py
+++ b/wiki/core/compat.py
@@ -3,7 +3,7 @@
 # so we restore that version.
 # When support for Django < 1.11 is dropped, we should look at using the
 # new template based rendering, at which point this probably won't be needed at all.
-class BuildAttrsCompat(object):
+class BuildAttrsCompat:
     def build_attrs_compat(self, extra_attrs=None, **kwargs):
         "Helper function for building an attribute dictionary."
         attrs = self.attrs.copy()
diff --git a/wiki/core/diff.py b/wiki/core/diff.py
index e19bdac18..4c5fd6f18 100644
--- a/wiki/core/diff.py
+++ b/wiki/core/diff.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import difflib
 
 
diff --git a/wiki/core/exceptions.py b/wiki/core/exceptions.py
index a95264532..442b85c9d 100644
--- a/wiki/core/exceptions.py
+++ b/wiki/core/exceptions.py
@@ -1,4 +1,3 @@
-
 # If no root URL is found, we raise this...
 class NoRootURL(Exception):
     pass
diff --git a/wiki/core/extensions.py b/wiki/core/extensions.py
index 9fa6a9385..5d19e893c 100644
--- a/wiki/core/extensions.py
+++ b/wiki/core/extensions.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from markdown.extensions import Extension
 
 from wiki.core.processors import AnchorTagProcessor
diff --git a/wiki/core/http.py b/wiki/core/http.py
index 3ad4160c3..c93833046 100644
--- a/wiki/core/http.py
+++ b/wiki/core/http.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import mimetypes
 import os
 from datetime import datetime
diff --git a/wiki/core/permissions.py b/wiki/core/permissions.py
index a953194db..307869c5c 100644
--- a/wiki/core/permissions.py
+++ b/wiki/core/permissions.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from wiki.conf import settings
 
 
diff --git a/wiki/core/plugins/base.py b/wiki/core/plugins/base.py
index eca464da6..1c4e2cbbe 100644
--- a/wiki/core/plugins/base.py
+++ b/wiki/core/plugins/base.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.utils.translation import ugettext_lazy as _
 
 
@@ -13,7 +11,7 @@
 plugin's models.
 """
 
-class BasePlugin(object):
+class BasePlugin:
     """Plugins should inherit from this"""
     # Must fill in!
     slug = None
@@ -38,13 +36,13 @@ class RenderMedia:
         js = []
         css = {}
 
-class PluginSidebarFormMixin(object):
+class PluginSidebarFormMixin:
 
     def get_usermessage(self):
         pass
 
-class PluginSettingsFormMixin(object):    
-    settings_form_headline = _(u'Settings for plugin')
+class PluginSettingsFormMixin:    
+    settings_form_headline = _('Settings for plugin')
     settings_order = 1
     settings_write_access = False
     
diff --git a/wiki/core/plugins/loader.py b/wiki/core/plugins/loader.py
index 6e61aa2a4..122acfc80 100644
--- a/wiki/core/plugins/loader.py
+++ b/wiki/core/plugins/loader.py
@@ -1,11 +1,9 @@
-# -*- coding: utf-8 -*-
 """
 Credits to ojii, functions get_module and load are from:
 https://github.com/ojii/django-load.
 
 Thanks for the technique!
 """
-from __future__ import absolute_import, print_function
 
 from importlib import import_module
 
diff --git a/wiki/core/plugins/registry.py b/wiki/core/plugins/registry.py
index 6b3afb817..ecbb598f0 100644
--- a/wiki/core/plugins/registry.py
+++ b/wiki/core/plugins/registry.py
@@ -1,10 +1,5 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from importlib import import_module
 
-import six
-
 _cache = {}
 _settings_forms = []
 _markdown_extensions = []
@@ -20,25 +15,25 @@ def register(PluginClass):
         raise Exception("Plugin class already registered")
     plugin = PluginClass()
     _cache[PluginClass] = plugin
-    
+
     settings_form = getattr(PluginClass, 'settings_form', None)
     if settings_form:
-        if isinstance(settings_form, six.string_types):
+        if isinstance(settings_form, str):
             klassname = settings_form.split(".")[-1]
             modulename = ".".join(settings_form.split(".")[:-1])
             form_module = import_module(modulename)
             settings_form = getattr(form_module, klassname)
         _settings_forms.append(settings_form)
-    
-    
+
+
     if getattr(PluginClass, 'article_tab', None):
         _article_tabs.append(plugin)
-    
+
     if getattr(PluginClass, 'sidebar', None):
         _sidebar.append(plugin)
 
-    _markdown_extensions.extend(getattr(PluginClass, 'markdown_extensions', []))        
-    
+    _markdown_extensions.extend(getattr(PluginClass, 'markdown_extensions', []))
+
 def get_plugins():
     """Get loaded plugins - do not call before all plugins are loaded."""
     return _cache
diff --git a/wiki/core/processors.py b/wiki/core/processors.py
index 216c01f15..d1b00a8e6 100644
--- a/wiki/core/processors.py
+++ b/wiki/core/processors.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from markdown.treeprocessors import Treeprocessor
 
 
diff --git a/wiki/decorators.py b/wiki/decorators.py
index e90c4634d..810f0fe1c 100644
--- a/wiki/decorators.py
+++ b/wiki/decorators.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 import json
 
 from django.conf import settings as django_settings
@@ -10,7 +7,6 @@
 from django.template.context import RequestContext
 from django.template.loader import render_to_string
 from django.urls import reverse
-from six.moves import filter
 
 from wiki.core.exceptions import NoRootURL
 
diff --git a/wiki/editors/__init__.py b/wiki/editors/__init__.py
index d1a3d88eb..a03d34e48 100644
--- a/wiki/editors/__init__.py
+++ b/wiki/editors/__init__.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.urls import get_callable
 
 from wiki.conf import settings
diff --git a/wiki/editors/base.py b/wiki/editors/base.py
index 94687e39f..c7e589dc8 100644
--- a/wiki/editors/base.py
+++ b/wiki/editors/base.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django import forms
 
 
diff --git a/wiki/editors/markitup.py b/wiki/editors/markitup.py
index 67c7c51ba..399ab8379 100644
--- a/wiki/editors/markitup.py
+++ b/wiki/editors/markitup.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django import forms
 from django.forms.utils import flatatt
 from django.utils.encoding import force_text
@@ -18,12 +16,12 @@ def __init__(self, attrs=None):
                          '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):
         if value is None: value = ''
         final_attrs = self.build_attrs_compat(attrs, name=name)
-        return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
+        return mark_safe('<textarea%s>%s</textarea>' % (flatatt(final_attrs),
                 conditional_escape(force_text(value))))
 
 
@@ -34,12 +32,12 @@ def __init__(self, attrs=None):
                          '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: value = ''
         final_attrs = self.build_attrs_compat(attrs, name=name)
-        return mark_safe(u'<div><textarea%s>%s</textarea></div>' % (flatatt(final_attrs),
+        return mark_safe('<div><textarea%s>%s</textarea></div>' % (flatatt(final_attrs),
                 conditional_escape(force_text(value))))
 
 class MarkItUp(BaseEditor):
diff --git a/wiki/forms.py b/wiki/forms.py
index ffd11e296..c6a6e382f 100644
--- a/wiki/forms.py
+++ b/wiki/forms.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, print_function
-
 from itertools import chain
 
 from django import forms
@@ -37,19 +34,19 @@ def check_spam(self, current_revision, request):
 
 class CreateRootForm(forms.Form):
     
-    title = forms.CharField(label=_(u'Title'), help_text=_(u'Initial title of the article. May be overridden with revision titles.'))
-    content = forms.CharField(label=_(u'Type in some contents'),
-                              help_text=_(u'This is just the initial contents of your article. After creating it, you can use more complex features like adding plugins, meta data, related articles etc...'),
+    title = forms.CharField(label=_('Title'), help_text=_('Initial title of the article. May be overridden with revision titles.'))
+    content = forms.CharField(label=_('Type in some contents'),
+                              help_text=_('This is just the initial contents of your article. After creating it, you can use more complex features like adding plugins, meta data, related articles etc...'),
                               required=False, widget=getEditor().get_widget()) #@UndefinedVariable
     
 
 class EditForm(forms.Form):
     
-    title = forms.CharField(label=_(u'Title'),)
-    content = forms.CharField(label=_(u'Contents'),
+    title = forms.CharField(label=_('Title'),)
+    content = forms.CharField(label=_('Contents'),
                               required=False, widget=getEditor().get_widget()) #@UndefinedVariable
     
-    summary = forms.CharField(label=_(u'Summary'), help_text=_(u'Give a short reason for your edit, which will be stated in the revision log.'),
+    summary = forms.CharField(label=_('Summary'), help_text=_('Give a short reason for your edit, which will be stated in the revision log.'),
                               required=False)
     
     current_revision = forms.IntegerField(required=False, widget=forms.HiddenInput())
@@ -87,7 +84,7 @@ def __init__(self, current_revision, *args, **kwargs):
                 
             kwargs['initial'] = initial
         
-        super(EditForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
     
     def clean_title(self):
         title = strip_tags(self.cleaned_data['title'])
@@ -98,9 +95,9 @@ def clean(self):
         if self.no_clean or self.preview:
             return cd
         if not str(self.initial_revision.id) == str(self.presumed_revision):
-            raise forms.ValidationError(_(u'While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below.'))
+            raise forms.ValidationError(_('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 cd['title'] == self.initial_revision.title and cd['content'] == self.initial_revision.content:
-            raise forms.ValidationError(_(u'No changes made. Nothing to save.'))
+            raise forms.ValidationError(_('No changes made. Nothing to save.'))
         return cd
 
 
@@ -138,10 +135,10 @@ class SelectWidgetBootstrap(BuildAttrsCompat, forms.Select):
     """)
     def __init__(self, attrs={'class': 'btn-group pull-left btn-group-form'}, choices=()):
         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 != 'attrs':
             self.noscript_widget.__setattr__(k, value)
     
@@ -162,57 +159,57 @@ def render(self, name, value, attrs=None, choices=()):
                   """<noscript>%(noscript)s</noscript>"""
                    % {'attrs': flatatt(final_attrs),
                       'options':self.render_options(choices, [value]),
-                      'label': _(u'Select an option'),
+                      'label': _('Select an option'),
                       'name': name,
                       'js': SelectWidgetBootstrap.js,
                       'noscript': self.noscript_widget.render(name, value, {}, choices)} ]
-        return mark_safe(u'\n'.join(output))
+        return mark_safe('\n'.join(output))
 
     def render_option(self, selected_choices, option_value, option_label):
         option_value = force_text(option_value)
-        selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
-        return u'<li><a href="javascript:void(0)" data-value="%s"%s>%s</a></li>' % (
+        selected_html = (option_value in selected_choices) and ' selected="selected"' or ''
+        return '<li><a href="javascript:void(0)" data-value="%s"%s>%s</a></li>' % (
             escape(option_value), selected_html,
             conditional_escape(force_text(option_label)))
 
     def render_options(self, choices, selected_choices):
         # Normalize to strings.
-        selected_choices = set([force_text(v) for v in selected_choices])
+        selected_choices = {force_text(v) for v in selected_choices}
         output = []
         for option_value, option_label in chain(self.choices, choices):
             if isinstance(option_label, (list, tuple)):
-                output.append(u'<li class="divider" label="%s"></li>' % escape(force_text(option_value)))
+                output.append('<li class="divider" label="%s"></li>' % escape(force_text(option_value)))
                 for option in option_label:
                     output.append(self.render_option(selected_choices, *option))
             else:
                 output.append(self.render_option(selected_choices, option_value, option_label))
-        return u'\n'.join(output)
+        return '\n'.join(output)
     
 
 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-prepend"><span class="add-on">%s</span>%s</div>' % (self.prepend, html))
     
 
 class CreateForm(forms.Form):
     
     def __init__(self, urlpath_parent, *args, **kwargs):
-        super(CreateForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.urlpath_parent = urlpath_parent
     
-    title = forms.CharField(label=_(u'Title'),)
-    slug = forms.SlugField(label=_(u'Slug'), help_text=_(u"This will be the address where your article can be found. Use only alphanumeric characters and - or _. Note that you cannot change the slug after creating the article."),
+    title = forms.CharField(label=_('Title'),)
+    slug = forms.SlugField(label=_('Slug'), help_text=_("This will be the address where your article can be found. Use only alphanumeric characters and - or _. Note that you cannot change the slug after creating the article."),
                            max_length=models.URLPath.SLUG_MAX_LENGTH)
-    content = forms.CharField(label=_(u'Contents'),
+    content = forms.CharField(label=_('Contents'),
                               required=False, widget=getEditor().get_widget()) #@UndefinedVariable
     
-    summary = forms.CharField(label=_(u'Summary'), help_text=_(u"Write a brief message for the article's history log."),
+    summary = forms.CharField(label=_('Summary'), help_text=_("Write a brief message for the article's history log."),
                               required=False)
     
     def clean_title(self):
@@ -222,7 +219,7 @@ def clean_title(self):
     def clean_slug(self):
         slug = self.cleaned_data['slug']
         if slug.startswith("_"):
-            raise forms.ValidationError(_(u'A slug may not begin with an underscore.'))
+            raise forms.ValidationError(_('A slug may not begin with an underscore.'))
         
         if settings.URL_CASE_SENSITIVE:
             already_existing_slug = models.URLPath.objects.filter(slug=slug, parent=self.urlpath_parent)
@@ -231,9 +228,9 @@ def clean_slug(self):
         if already_existing_slug:
             already_urlpath = already_existing_slug[0]
             if already_urlpath.article and already_urlpath.article.current_revision.deleted:
-                raise forms.ValidationError(_(u'A deleted article with slug "%s" already exists.') % already_urlpath.slug)
+                raise forms.ValidationError(_('A deleted article with slug "%s" already exists.') % already_urlpath.slug)
             else:
-                raise forms.ValidationError(_(u'A slug named "%s" already exists.') % already_urlpath.slug)
+                raise forms.ValidationError(_('A slug named "%s" already exists.') % already_urlpath.slug)
         
         return slug
 
@@ -243,42 +240,42 @@ 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=_(u'Yes, I am sure'))
+                                 label=_('Yes, I am sure'))
     purge = forms.BooleanField(widget=HiddenInput(), required=False,
-                               label=_(u'Purge'),
-                               help_text=_(u'Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place.'))
+                               label=_('Purge'),
+                               help_text=_('Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place.'))
     revision = forms.ModelChoiceField(models.ArticleRevision.objects.all(),
                                       widget=HiddenInput(), required=False)
     
     def clean(self):
         cd = self.cleaned_data
         if not cd['confirm']:
-            raise forms.ValidationError(_(u'You are not sure enough!'))
+            raise forms.ValidationError(_('You are not sure enough!'))
         if cd['revision'] != self.article.current_revision:
-            raise forms.ValidationError(_(u'While you tried to delete this article, it was modified. TAKE CARE!'))
+            raise forms.ValidationError(_('While you tried to delete this article, it was modified. TAKE CARE!'))
         return cd
 
 
 class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):
     
-    locked = forms.BooleanField(label=_(u'Lock article'), help_text=_(u'Deny all users access to edit this article.'),
+    locked = forms.BooleanField(label=_('Lock article'), help_text=_('Deny all users access to edit this article.'),
                                 required=False)
 
-    settings_form_headline = _(u'Permissions')
+    settings_form_headline = _('Permissions')
     settings_order = 5
     settings_write_access = False
 
-    owner_username = forms.CharField(required=False, label=_(u'Owner'),
-                                     help_text=_(u'Enter the username of the owner.'))
-    group = forms.ModelChoiceField(models.Group.objects.all(), empty_label=_(u'(none)'),
+    owner_username = forms.CharField(required=False, label=_('Owner'),
+                                     help_text=_('Enter the username of the owner.'))
+    group = forms.ModelChoiceField(models.Group.objects.all(), empty_label=_('(none)'),
                                      required=False)
     if settings.USE_BOOTSTRAP_SELECT_WIDGET:
         group.widget= SelectWidgetBootstrap()
     
-    recursive = forms.BooleanField(label=_(u'Inherit permissions'), help_text=_(u'Check here to apply the above permissions recursively to articles under this one.'),
+    recursive = forms.BooleanField(label=_('Inherit permissions'), help_text=_('Check here to apply the above permissions recursively to articles under this one.'),
                                    required=False)
     
     def get_usermessage(self):
@@ -293,7 +290,7 @@ def __init__(self, article, request, *args, **kwargs):
         self.request = request
         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
@@ -324,7 +321,7 @@ def clean_owner_username(self):
                 try:
                     user = User.objects.get(username=username)
                 except models.User.DoesNotExist:
-                    raise forms.ValidationError(_(u'No user with that username'))
+                    raise forms.ValidationError(_('No user with that username'))
             else:
                 user = None
         else:
@@ -332,7 +329,7 @@ def clean_owner_username(self):
         return user
     
     def save(self, commit=True):
-        article = super(PermissionsForm, self).save(commit=False)
+        article = super().save(commit=False)
         article.owner = self.cleaned_data['owner_username']
         if not self.can_change_groups:
             article.group = self.article.group
@@ -346,14 +343,14 @@ def save(self, commit=True):
                 revision = models.ArticleRevision()
                 revision.inherit_predecessor(self.article)
                 revision.set_from_request(self.request)
-                revision.automatic_log = _(u'Article locked for editing')
+                revision.automatic_log = _('Article locked for editing')
                 revision.locked = True
                 self.article.add_revision(revision)
             elif not self.cleaned_data['locked'] and article.current_revision.locked:
                 revision = models.ArticleRevision()
                 revision.inherit_predecessor(self.article)
                 revision.set_from_request(self.request)
-                revision.automatic_log = _(u'Article unlocked for editing')
+                revision.automatic_log = _('Article unlocked for editing')
                 revision.locked = False
                 self.article.add_revision(revision)                
         
@@ -366,4 +363,4 @@ class Meta:
 
 class DirFilterForm(forms.Form):
     
-    query = forms.CharField(label=_(u'Filter'), widget=forms.TextInput(attrs={'class': 'search-query'}))
+    query = forms.CharField(label=_('Filter'), widget=forms.TextInput(attrs={'class': 'search-query'}))
diff --git a/wiki/management/commands/wikiviz.py b/wiki/management/commands/wikiviz.py
index 9e6c53563..7062d5b9e 100644
--- a/wiki/management/commands/wikiviz.py
+++ b/wiki/management/commands/wikiviz.py
@@ -43,7 +43,6 @@
     -e, --inheritance
     show inheritance arrows.
 """
-from __future__ import absolute_import, print_function
 
 import os
 import sys
@@ -205,7 +204,7 @@ def add_attributes(field):
 
                 t = type(field).__name__
                 if isinstance(field, (OneToOneField, ForeignKey)):
-                    t += " ({0})".format(field.rel.field_name)
+                    t += f" ({field.rel.field_name})"
                 # TODO: ManyToManyField, GenericRelation
 
                 model['fields'].append({
diff --git a/wiki/managers.py b/wiki/managers.py
index 1c6c0f5e2..3876c15ee 100644
--- a/wiki/managers.py
+++ b/wiki/managers.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.db import models
 from django.db.models import Q
 from django.db.models.query import EmptyQuerySet, QuerySet
diff --git a/wiki/middleware.py b/wiki/middleware.py
index 803eb8a7c..29185f4b4 100644
--- a/wiki/middleware.py
+++ b/wiki/middleware.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import importlib
 import threading
 
@@ -23,7 +21,7 @@ class _RequestCache(threading.local):
         A thread-local for storing the per-request cache.
         """
         def __init__(self):
-            super(_RequestCache, self).__init__()
+            super().__init__()
             self.data = {}
             self.request = None
 
diff --git a/wiki/migrations/0001_initial.py b/wiki/migrations/0001_initial.py
index 00915fcfc..8b52ac707 100644
--- a/wiki/migrations/0001_initial.py
+++ b/wiki/migrations/0001_initial.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 import mptt.fields
 from django.conf import settings
 from django.db import migrations, models
@@ -222,7 +219,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AlterUniqueTogether(
             name='urlpath',
-            unique_together=set([('site', 'parent', 'slug')]),
+            unique_together={('site', 'parent', 'slug')},
         ),
         migrations.AddField(
             model_name='revisionpluginrevision',
@@ -280,7 +277,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AlterUniqueTogether(
             name='articlerevision',
-            unique_together=set([('article', 'revision_number')]),
+            unique_together={('article', 'revision_number')},
         ),
         migrations.AddField(
             model_name='articleplugin',
@@ -290,7 +287,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AlterUniqueTogether(
             name='articleforobject',
-            unique_together=set([('content_type', 'object_id')]),
+            unique_together={('content_type', 'object_id')},
         ),
         migrations.AddField(
             model_name='article',
diff --git a/wiki/migrations/0002_remove_article_subscription.py b/wiki/migrations/0002_remove_article_subscription.py
index be93ad704..55d1295c2 100644
--- a/wiki/migrations/0002_remove_article_subscription.py
+++ b/wiki/migrations/0002_remove_article_subscription.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 from django.db import migrations, models
 
 
diff --git a/wiki/migrations/0003_ip_address_conv.py b/wiki/migrations/0003_ip_address_conv.py
index 22612fb9c..5d6e6cba1 100644
--- a/wiki/migrations/0003_ip_address_conv.py
+++ b/wiki/migrations/0003_ip_address_conv.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 from django.db import migrations, models
 
 
diff --git a/wiki/migrations/0004_increase_slug_size.py b/wiki/migrations/0004_increase_slug_size.py
index ef5038f0d..01e491dc8 100644
--- a/wiki/migrations/0004_increase_slug_size.py
+++ b/wiki/migrations/0004_increase_slug_size.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 from django.db import migrations, models
 
 
diff --git a/wiki/migrations/0005_remove_attachments_and_images.py b/wiki/migrations/0005_remove_attachments_and_images.py
index c44b9105d..308664e38 100644
--- a/wiki/migrations/0005_remove_attachments_and_images.py
+++ b/wiki/migrations/0005_remove_attachments_and_images.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.11.5 on 2017-09-07 19:03
-from __future__ import absolute_import, unicode_literals
 
 from django.db import migrations
 
diff --git a/wiki/migrations/0006_auto_20200110_1003.py b/wiki/migrations/0006_auto_20200110_1003.py
index fd82ade41..4fb7a1662 100644
--- a/wiki/migrations/0006_auto_20200110_1003.py
+++ b/wiki/migrations/0006_auto_20200110_1003.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.11.27 on 2020-01-10 10:03
-from __future__ import unicode_literals
 
 from django.db import migrations, models
 
diff --git a/wiki/models/__init__.py b/wiki/models/__init__.py
index a3e127ac3..e95e840fa 100644
--- a/wiki/models/__init__.py
+++ b/wiki/models/__init__.py
@@ -1,9 +1,5 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.conf import settings as django_settings
 from django.core.exceptions import ImproperlyConfigured
-from six import string_types, text_type
 
 # TODO: Don't use wildcards
 from .article import *
@@ -75,7 +71,7 @@ def reverse(*args, **kwargs):
     return the result of calling reverse._transform_url(reversed_url)
     for every url in the wiki namespace.
     """
-    if args and isinstance(args[0], string_types) and args[0].startswith('wiki:'):
+    if args and 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
@@ -94,7 +90,7 @@ def reverse(*args, **kwargs):
 
 
 # Now we redefine reverse method
-reverse_lazy = lazy(reverse, text_type)
+reverse_lazy = lazy(reverse, str)
 urls.reverse = reverse
 urls.reverse_lazy = reverse_lazy
 
diff --git a/wiki/models/article.py b/wiki/models/article.py
index 9991341a1..901e86275 100644
--- a/wiki/models/article.py
+++ b/wiki/models/article.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 import bleach
 from django.contrib.auth.models import Group, User
 from django.contrib.contenttypes import fields
@@ -23,27 +20,27 @@ class Article(models.Model):
     objects = managers.ArticleManager()
 
     current_revision = models.OneToOneField('ArticleRevision',
-                                            verbose_name=_(u'current revision'),
+                                            verbose_name=_('current revision'),
                                             blank=True, null=True, related_name='current_set',
-                                            help_text=_(u'The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field.'), on_delete=models.CASCADE
+                                            help_text=_('The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field.'), on_delete=models.CASCADE
                                             )
 
-    created = models.DateTimeField(auto_now_add=True, verbose_name=_(u'created'))
-    modified = models.DateTimeField(auto_now=True, verbose_name=_(u'modified'),
-                                    help_text=_(u'Article properties last modified'))
+    created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
+    modified = models.DateTimeField(auto_now=True, verbose_name=_('modified'),
+                                    help_text=_('Article properties last modified'))
 
     owner = models.ForeignKey(User, verbose_name=_('owner'),
                               blank=True, null=True, related_name='owned_articles',
-                              help_text=_(u'The owner of the article, usually the creator. The owner always has both read and write access.'), on_delete=models.CASCADE)
+                              help_text=_('The owner of the article, usually the creator. The owner always has both read and write access.'), on_delete=models.CASCADE)
 
     group = models.ForeignKey(Group, verbose_name=_('group'),
                               blank=True, null=True,
-                              help_text=_(u'Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system.'), on_delete=models.CASCADE)
+                              help_text=_('Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system.'), on_delete=models.CASCADE)
 
-    group_read = models.BooleanField(default=True, verbose_name=_(u'group read access'))
-    group_write = models.BooleanField(default=True, verbose_name=_(u'group write access'))
-    other_read = models.BooleanField(default=True, verbose_name=_(u'others read access'))
-    other_write = models.BooleanField(default=True, verbose_name=_(u'others write access'))
+    group_read = models.BooleanField(default=True, verbose_name=_('group read access'))
+    group_write = models.BooleanField(default=True, verbose_name=_('group write access'))
+    other_read = models.BooleanField(default=True, verbose_name=_('others read access'))
+    other_write = models.BooleanField(default=True, verbose_name=_('others write access'))
 
     # TODO: Do not use kwargs, it can lead to dangerous situations with bad
     # permission checking patterns. Also, since there are no other keywords,
@@ -96,8 +93,7 @@ def can_assign(self, user):
     def descendant_objects(self):
         """NB! This generator is expensive, so use it with care!!"""
         for obj in self.articleforobject_set.filter(is_mptt=True):
-            for descendant in obj.content_object.get_descendants():
-                yield descendant
+            yield from obj.content_object.get_descendants()
 
     def get_children(self, max_num=None, user_can_read=None, **kwargs):
         """NB! This generator is expensive, so use it with care!!"""
@@ -174,7 +170,7 @@ def get_for_object(cls, obj):
     def __unicode__(self):
         if self.current_revision:
             return self.current_revision.title
-        return _(u'Article without content (%(id)d)') % {'id': self.id}
+        return _('Article without content (%(id)d)') % {'id': self.id}
 
     class Meta:
         permissions = (
@@ -210,8 +206,8 @@ class ArticleForObject(models.Model):
     is_mptt = models.BooleanField(default=False, editable=False)
 
     class Meta:
-        verbose_name = _(u'Article for object')
-        verbose_name_plural = _(u'Articles for object')
+        verbose_name = _('Article for object')
+        verbose_name_plural = _('Articles for object')
         # Do not allow several objects
         unique_together = ('content_type', 'object_id')
 
@@ -220,7 +216,7 @@ class BaseRevisionMixin(models.Model):
     """This is an abstract model used as a mixin: Do not override any of the
     core model methods but respect the inheritor's freedom to do so itself."""
 
-    revision_number = models.IntegerField(editable=False, verbose_name=_(u'revision number'))
+    revision_number = models.IntegerField(editable=False, verbose_name=_('revision number'))
 
     user_message = models.TextField(blank=True)
     automatic_log = models.TextField(blank=True, editable=False)
@@ -246,8 +242,8 @@ class BaseRevisionMixin(models.Model):
     # NOTE! The semantics of these fields are not related to the revision itself
     # but the actual related object. If the latest revision says "deleted=True" then
     # the related object should be regarded as deleted.
-    deleted = models.BooleanField(verbose_name=_(u'deleted'), default=False)
-    locked = models.BooleanField(verbose_name=_(u'locked'), default=False)
+    deleted = models.BooleanField(verbose_name=_('deleted'), default=False)
+    locked = models.BooleanField(verbose_name=_('locked'), default=False)
 
     def set_from_request(self, request):
         if request.user.is_authenticated:
@@ -266,15 +262,15 @@ class ArticleRevision(BaseRevisionMixin, models.Model):
     copy, do NEVER create m2m relationships."""
 
     article = models.ForeignKey('Article', on_delete=models.CASCADE,
-                                verbose_name=_(u'article'))
+                                verbose_name=_('article'))
 
     # This is where the content goes, with whatever markup language is used
-    content = models.TextField(blank=True, verbose_name=_(u'article contents'))
+    content = models.TextField(blank=True, verbose_name=_('article contents'))
 
     # This title is automatically set from either the article's title or
     # the last used revision...
-    title = models.CharField(max_length=512, verbose_name=_(u'article title'),
-                             null=False, blank=False, help_text=_(u'Each revision contains a title field that must be filled out, even if the title has not changed'))
+    title = models.CharField(max_length=512, verbose_name=_('article title'),
+                             null=False, blank=False, help_text=_('Each revision contains a title field that must be filled out, even if the title has not changed'))
 
     # TODO:
     # Allow a revision to redirect to another *article*. This
@@ -316,7 +312,7 @@ def save(self, *args, **kwargs):
                 self.revision_number = 1
 
         self.clean_data()
-        super(ArticleRevision, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
 
         if not self.article.current_revision:
             # If I'm saved from Django admin, then article.current_revision is me!
diff --git a/wiki/models/pluginbase.py b/wiki/models/pluginbase.py
index f78519998..d97d8ed70 100644
--- a/wiki/models/pluginbase.py
+++ b/wiki/models/pluginbase.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.db import models
 from django.db.models import signals
 from django.utils.translation import ugettext_lazy as _
@@ -39,7 +36,7 @@ class ArticlePlugin(models.Model):
     properties in the future..."""    
     
     article = models.ForeignKey(Article, on_delete=models.CASCADE, 
-                                verbose_name=_(u"article"))
+                                verbose_name=_("article"))
     
     deleted = models.BooleanField(default=False)
     
@@ -75,8 +72,8 @@ class ReusablePlugin(ArticlePlugin):
     # The article on which the plugin was originally created.
     # Used to apply permissions.
     ArticlePlugin.article.on_delete=models.SET_NULL
-    ArticlePlugin.article.verbose_name=_(u'original article')
-    ArticlePlugin.article.help_text=_(u'Permissions are inherited from this article')
+    ArticlePlugin.article.verbose_name=_('original article')
+    ArticlePlugin.article.help_text=_('Permissions are inherited from this article')
     ArticlePlugin.article.null = True
     ArticlePlugin.article.blank = True
     
@@ -101,7 +98,7 @@ def save(self, *args, **kwargs):
             if articles.count() == 0:
                 self.article = articles[0]
             
-        super(ReusablePlugin, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
 
 
 class SimplePluginCreateError(Exception): pass
@@ -128,13 +125,13 @@ class YourPlugin(SimplePlugin):
     article_revision = models.ForeignKey(ArticleRevision, on_delete=models.CASCADE)
     
     def __init__(self, *args, **kwargs):
-        super(SimplePlugin, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         if not self.id and not 'article' in kwargs:
             raise SimplePluginCreateError("Keyword argument 'article' expected.")
             self.article = kwargs['article']
         
     def get_logmessage(self):
-        return _(u"A plugin was changed")
+        return _("A plugin was changed")
     
     def save(self, *args, **kwargs):
         if not self.id:
@@ -146,7 +143,7 @@ def save(self, *args, **kwargs):
             new_revision.save()
             
             self.article_revision = new_revision
-        super(SimplePlugin, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
 
 
 class RevisionPlugin(ArticlePlugin):
@@ -159,9 +156,9 @@ class RevisionPlugin(ArticlePlugin):
     """
     # The current revision of this plugin, if any!
     current_revision = models.OneToOneField('RevisionPluginRevision', 
-                                            verbose_name=_(u'current revision'),
+                                            verbose_name=_('current revision'),
                                             blank=True, null=True, related_name='plugin_set',
-                                            help_text=_(u'The revision being displayed for this plugin.'
+                                            help_text=_('The revision being displayed for this plugin.'
                                                          'If you need to do a roll-back, simply change the value of this field.'),
                                             on_delete=models.CASCADE
                                             )
@@ -213,7 +210,7 @@ def save(self, *args, **kwargs):
             except RevisionPluginRevision.DoesNotExist:
                 self.revision_number = 1
 
-        super(RevisionPluginRevision, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
         
         if not self.plugin.current_revision:
             # If I'm saved from Django admin, then plugin.current_revision is me!
diff --git a/wiki/models/urlpath.py b/wiki/models/urlpath.py
index 5f717b73f..88562425b 100644
--- a/wiki/models/urlpath.py
+++ b/wiki/models/urlpath.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, print_function
-
 import logging
 
 from django.contrib.contenttypes import fields
@@ -40,13 +37,13 @@ class URLPath(MPTTModel):
     
     # Do NOT modify this field - it is updated with signals whenever ArticleForObject is changed.
     article = models.ForeignKey(Article, on_delete=models.CASCADE, editable=False,
-                                verbose_name=_(u'Cache lookup value for articles'))
+                                verbose_name=_('Cache lookup value for articles'))
     
     # The slug is constructed from course key and will in practice be much shorter then 255 characters
     # since course keys are capped at 65 characters in the Studio (https://openedx.atlassian.net/browse/TNL-889).
     SLUG_MAX_LENGTH = 255
     
-    slug = models.SlugField(verbose_name=_(u'slug'), null=True, blank=True,
+    slug = models.SlugField(verbose_name=_('slug'), null=True, blank=True,
                             max_length=SLUG_MAX_LENGTH)
     site = models.ForeignKey(Site, on_delete=models.CASCADE)
     parent = TreeForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE)
@@ -55,7 +52,7 @@ def __init__(self, *args, **kwargs):
         pass
         # Fixed in django-mptt 0.5.3
         #self._tree_manager = URLPath.objects
-        return super(URLPath, self).__init__(*args, **kwargs)
+        return super().__init__(*args, **kwargs)
     
     @property
     def cached_ancestors(self):
@@ -136,29 +133,29 @@ class MPTTMeta:
     
     def __unicode__(self):
         path = self.path
-        return path if path else ugettext(u"(root)")
+        return path if path else ugettext("(root)")
     
     def save(self, *args, **kwargs):
-        super(URLPath, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
     
     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 = _(u'URL path')
-        verbose_name_plural = _(u'URL paths')
+        verbose_name = _('URL path')
+        verbose_name_plural = _('URL paths')
         unique_together = ('site', 'parent', 'slug')
     
     def clean(self, *args, **kwargs):
         if self.slug and not self.parent:
-            raise ValidationError(_(u'Sorry but you cannot have a root article with a slug.'))
+            raise ValidationError(_('Sorry but you cannot have a root article with a slug.'))
         if not self.slug and self.parent:
-            raise ValidationError(_(u'A non-root note must always have a slug.'))
+            raise ValidationError(_('A non-root note must always have a slug.'))
         if not self.parent:
             if URLPath.objects.root_nodes().filter(site=self.site).exclude(id=self.id):
-                raise ValidationError(_(u'There is already a root node on %s') % self.site)
-        super(URLPath, self).clean(*args, **kwargs)
+                raise ValidationError(_('There is already a root node on %s') % self.site)
+        super().clean(*args, **kwargs)
     
     @classmethod
     def get_by_path(cls, path, select_related=False):
@@ -262,10 +259,10 @@ def on_article_delete(instance, *args, **kwargs):
                           other_read = False,
                           other_write = False)
         article.add_revision(ArticleRevision(
-                 content=_(u'Articles who lost their parents\n'
+                 content=_('Articles who lost their parents\n'
                             '===============================\n\n'
                             'The children of this article have had their parents deleted. You should probably find a new home for them.'),
-                 title=_(u"Lost and found")))
+                 title=_("Lost and found")))
         lost_and_found = URLPath.objects.create(slug=settings.LOST_AND_FOUND_SLUG,
                                                 parent=URLPath.root(),
                                                 site=site,
diff --git a/wiki/plugins/attachments/__init__.py b/wiki/plugins/attachments/__init__.py
index 44433dce7..fe5b55e46 100644
--- a/wiki/plugins/attachments/__init__.py
+++ b/wiki/plugins/attachments/__init__.py
@@ -1,3 +1,1 @@
-from __future__ import unicode_literals
-
 default_app_config = 'wiki.apps.AttachmentsConfig'
diff --git a/wiki/plugins/attachments/admin.py b/wiki/plugins/attachments/admin.py
index 0f6ccdf50..c02aae1aa 100644
--- a/wiki/plugins/attachments/admin.py
+++ b/wiki/plugins/attachments/admin.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.contrib import admin
 
 from wiki.plugins.attachments import models
diff --git a/wiki/plugins/attachments/forms.py b/wiki/plugins/attachments/forms.py
index 8163afe67..2b5db386f 100644
--- a/wiki/plugins/attachments/forms.py
+++ b/wiki/plugins/attachments/forms.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
@@ -9,8 +6,8 @@
 
 class AttachmentForm(forms.ModelForm):
     
-    description = forms.CharField(label=_(u'Description'),
-                                  help_text=_(u'A short summary of what the file contains'),
+    description = forms.CharField(label=_('Description'),
+                                  help_text=_('A short summary of what the file contains'),
                                   required=False)
     
     class Meta:
@@ -19,12 +16,12 @@ class Meta:
 
 class DeleteForm(forms.Form):
     """This form is both used for dereferencing and deleting attachments"""
-    confirm = forms.BooleanField(label=_(u'Yes I am sure...'),
+    confirm = forms.BooleanField(label=_('Yes I am sure...'),
                                  required=False)
     
     def clean_confirm(self):
         if not self.cleaned_data['confirm']:
-            raise forms.ValidationError(_(u'You are not sure enough!'))
+            raise forms.ValidationError(_('You are not sure enough!'))
         return True
 
 class SearchForm(forms.Form):
diff --git a/wiki/plugins/attachments/markdown_extensions.py b/wiki/plugins/attachments/markdown_extensions.py
index 3ccabeaf3..3767da5d8 100644
--- a/wiki/plugins/attachments/markdown_extensions.py
+++ b/wiki/plugins/attachments/markdown_extensions.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import re
 
 import markdown
@@ -32,9 +30,9 @@ def run(self, lines):
                                                                id=attachment_id, current_revision__deleted=False)
                     url = reverse('wiki:attachments_download', kwargs={'article_id': self.markdown.article.id,
                                                                        'attachment_id':attachment.id,})
-                    line = line.replace(m.group(1), u"""<span class="attachment"><a href="%s" title="%s">%s</a>""" % 
-                                        (url, _(u"Click to download file"), attachment.original_filename))
+                    line = line.replace(m.group(1), """<span class="attachment"><a href="%s" title="%s">%s</a>""" % 
+                                        (url, _("Click to download file"), attachment.original_filename))
                 except models.Attachment.DoesNotExist:
-                    line = line.replace(m.group(1), u"""<span class="attachment attachment-deleted">Attachment with ID #%s is deleted.</span>""" % attachment_id)                    
+                    line = line.replace(m.group(1), """<span class="attachment attachment-deleted">Attachment with ID #%s is deleted.</span>""" % attachment_id)                    
             new_text.append(line)
         return new_text
diff --git a/wiki/plugins/attachments/migrations/0001_initial.py b/wiki/plugins/attachments/migrations/0001_initial.py
index a3d777159..aebbc234b 100644
--- a/wiki/plugins/attachments/migrations/0001_initial.py
+++ b/wiki/plugins/attachments/migrations/0001_initial.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 from django.conf import settings
 from django.db import migrations, models
 
diff --git a/wiki/plugins/attachments/models.py b/wiki/plugins/attachments/models.py
index 832cc7868..74dc5862c 100644
--- a/wiki/plugins/attachments/models.py
+++ b/wiki/plugins/attachments/models.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
@@ -19,13 +17,13 @@ class Attachment(ReusablePlugin):
     objects = managers.ArticleFkManager()
 
     current_revision = models.OneToOneField('AttachmentRevision', 
-                                            verbose_name=_(u'current revision'),
+                                            verbose_name=_('current revision'),
                                             blank=True, null=True, related_name='current_set',
-                                            help_text=_(u'The revision of this attachment currently in use (on all articles using the attachment)'),
+                                            help_text=_('The revision of this attachment currently in use (on all articles using the attachment)'),
                                             on_delete=models.CASCADE
                                             )
     
-    original_filename = models.CharField(max_length=256, verbose_name=_(u'original filename'), blank=True, null=True)
+    original_filename = models.CharField(max_length=256, verbose_name=_('original filename'), blank=True, null=True)
 
     def can_write(self, **kwargs):
         user = kwargs.get('user', None)
@@ -37,8 +35,8 @@ def can_delete(self, user):
         return self.can_write(user=user)
     
     class Meta:
-        verbose_name = _(u'attachment')
-        verbose_name_plural = _(u'attachments')
+        verbose_name = _('attachment')
+        verbose_name_plural = _('attachments')
         db_table = 'wiki_attachments_attachment'
     
     def __unicode__(self):
@@ -80,14 +78,14 @@ class AttachmentRevision(BaseRevisionMixin, models.Model):
     attachment = models.ForeignKey('Attachment', on_delete=models.CASCADE)
 
     file = models.FileField(upload_to=upload_path, #@ReservedAssignment
-                            verbose_name=_(u'file'),
+                            verbose_name=_('file'),
                             storage=settings.STORAGE_BACKEND)
         
     description = models.TextField(blank=True)
     
     class Meta:
-        verbose_name = _(u'attachment revision')
-        verbose_name_plural = _(u'attachment revisions')
+        verbose_name = _('attachment revision')
+        verbose_name_plural = _('attachment revisions')
         ordering = ('created',)
         get_latest_by = 'revision_number'
         db_table = 'wiki_attachments_attachmentrevision'
@@ -128,7 +126,7 @@ def save(self, *args, **kwargs):
             except (AttachmentRevision.DoesNotExist, Attachment.DoesNotExist):
                 self.revision_number = 1
         
-        super(AttachmentRevision, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
         
         if not self.attachment.current_revision:
             # If I'm saved from Django admin, then article.current_revision is me!
diff --git a/wiki/plugins/attachments/settings.py b/wiki/plugins/attachments/settings.py
index e0fa297ab..d72149386 100644
--- a/wiki/plugins/attachments/settings.py
+++ b/wiki/plugins/attachments/settings.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.conf import settings as django_settings
 from django.core.files.storage import default_storage
 
diff --git a/wiki/plugins/attachments/views.py b/wiki/plugins/attachments/views.py
index cc7896d0a..da151101e 100644
--- a/wiki/plugins/attachments/views.py
+++ b/wiki/plugins/attachments/views.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.contrib import messages
 from django.db import transaction
 from django.db.models import Q
@@ -31,7 +28,7 @@ def dispatch(self, request, article, *args, **kwargs):
             self.attachments = models.Attachment.objects.active().filter(articles=article)
         
         # 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 
@@ -48,11 +45,11 @@ def form_valid(self, form):
                 attachment_revision.attachment = attachment
                 attachment_revision.set_from_request(self.request)
                 attachment_revision.save()
-                messages.success(self.request, _(u'%s was successfully added.') % attachment_revision.get_filename())
+                messages.success(self.request, _('%s was successfully added.') % attachment_revision.get_filename())
         except models.IllegalFileExtension as e:
-            messages.error(self.request, _(u'Your file could not be saved: %s') % e)
+            messages.error(self.request, _('Your file could not be saved: %s') % e)
         except Exception:
-            messages.error(self.request, _(u'Your file could not be saved, probably because of a permission error on the web server.'))
+            messages.error(self.request, _('Your file could not be saved, probably because of a permission error on the web server.'))
         
         return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id)
     
@@ -66,7 +63,7 @@ def get_context_data(self, **kwargs):
         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)
+        return super().get_context_data(**kwargs)
 
 
 class AttachmentHistoryView(ArticleMixin, TemplateView):
@@ -79,13 +76,13 @@ def dispatch(self, request, article, attachment_id, *args, **kwargs):
             self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
         else:
             self.attachment = get_object_or_404(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):
@@ -101,7 +98,7 @@ def dispatch(self, request, article, attachment_id, *args, **kwargs):
             self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
         else:
             self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
-        return super(AttachmentReplaceView, self).dispatch(request, article, *args, **kwargs)
+        return super().dispatch(request, article, *args, **kwargs)
     
     def form_valid(self, form):
         
@@ -113,13 +110,13 @@ def form_valid(self, form):
             attachment_revision.save()
             self.attachment.current_revision = attachment_revision
             self.attachment.save()
-            messages.success(self.request, _(u'%s uploaded and replaces old attachment.') % attachment_revision.get_filename())
+            messages.success(self.request, _('%s uploaded and replaces old attachment.') % attachment_revision.get_filename())
         except models.IllegalFileExtension as e:
-            messages.error(self.request, _(u'Your file could not be saved: %s') % e)
+            messages.error(self.request, _('Your file could not be saved: %s') % e)
             return redirect("wiki:attachments_replace", attachment_id=self.attachment.id,
                             path=self.urlpath.path, article_id=self.article.id)
         except Exception:
-            messages.error(self.request, _(u'Your file could not be saved, probably because of a permission error on the web server.'))
+            messages.error(self.request, _('Your file could not be saved, probably because of a permission error on the web server.'))
             return redirect("wiki:attachments_replace", attachment_id=self.attachment.id,
                             path=self.urlpath.path, article_id=self.article.id)
         
@@ -127,7 +124,7 @@ def form_valid(self, form):
     
     def get_form(self, form_class):
         form = FormView.get_form(self, form_class)
-        form.fields['file'].help_text = _(u'Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed.')
+        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_initial(self, **kwargs):
@@ -138,7 +135,7 @@ def get_context_data(self, **kwargs):
             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):
     
@@ -153,7 +150,7 @@ def dispatch(self, request, article, attachment_id, *args, **kwargs):
             self.revision = get_object_or_404(models.AttachmentRevision, id=revision_id, 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:
@@ -176,12 +173,12 @@ def dispatch(self, request, article, attachment_id, revision_id, *args, **kwargs
         else:
             self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
         self.revision = get_object_or_404(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
         self.attachment.save()
-        messages.success(self.request, _(u'Current revision changed for %s.') % self.attachment.original_filename)
+        messages.success(self.request, _('Current revision changed for %s.') % self.attachment.original_filename)
         
         return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id)
     
@@ -196,13 +193,13 @@ class AttachmentAddView(ArticleMixin, View):
     @method_decorator(get_article(can_write=True))
     def dispatch(self, request, article, attachment_id, *args, **kwargs):
         self.attachment = get_object_or_404(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 self.attachment.articles.filter(id=self.article.id):
             self.attachment.articles.add(self.article)
             self.attachment.save()
-        messages.success(self.request, _(u'Added a reference to "%(att)s" from "%(art)s".') % 
+        messages.success(self.request, _('Added a reference to "%(att)s" from "%(art)s".') % 
                          {'att': self.attachment.original_filename,
                           'art': self.article.current_revision.title})        
         return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id)
@@ -218,7 +215,7 @@ def dispatch(self, request, article, attachment_id, *args, **kwargs):
         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):
         
@@ -232,10 +229,10 @@ def form_valid(self, form):
             revision.save()
             self.attachment.current_revision = revision
             self.attachment.save()
-            messages.info(self.request, _(u'The file %s was deleted.') % self.attachment.original_filename)
+            messages.info(self.request, _('The file %s was deleted.') % self.attachment.original_filename)
         else:
             self.attachment.articles.remove(self.article)
-            messages.info(self.request, _(u'This article is no longer related to the file %s.') % self.attachment.original_filename)
+            messages.info(self.request, _('This article is no longer related to the file %s.') % self.attachment.original_filename)
         
         return redirect("wiki:get", path=self.urlpath.path, article_id=self.article.id)
 
@@ -244,7 +241,7 @@ def get_context_data(self, **kwargs):
         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):
@@ -256,7 +253,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)
diff --git a/wiki/plugins/attachments/wiki_plugin.py b/wiki/plugins/attachments/wiki_plugin.py
index 4a3900590..06a4187f9 100644
--- a/wiki/plugins/attachments/wiki_plugin.py
+++ b/wiki/plugins/attachments/wiki_plugin.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.conf.urls import url
 from django.utils.translation import ugettext_lazy as _
 
@@ -28,13 +25,13 @@ class AttachmentPlugin(BasePlugin):
         url(r'^change/(?P<attachment_id>\d+)/revision/(?P<revision_id>\d+)/$', views.AttachmentChangeRevisionView.as_view(), name='attachments_revision_change'),
     ]
     
-    article_tab = (_(u'Attachments'), "icon-file")
+    article_tab = (_('Attachments'), "icon-file")
     article_view = views.AttachmentView().dispatch
     
     # List of notifications to construct signal handlers for. This
     # is handled inside the notifications plugin.
     notifications = [{'model': models.AttachmentRevision,
-                      'message': lambda obj: (_(u"A file was changed: %s") if not obj.deleted else _(u"A file was deleted: %s")) % obj.get_filename(),
+                      'message': lambda obj: (_("A file was changed: %s") if not obj.deleted else _("A file was deleted: %s")) % obj.get_filename(),
                       'key': ARTICLE_EDIT,
                       'created': True,
                       'get_article': lambda obj: obj.attachment.article}
diff --git a/wiki/plugins/help/models.py b/wiki/plugins/help/models.py
index dbe627f0e..71a836239 100644
--- a/wiki/plugins/help/models.py
+++ b/wiki/plugins/help/models.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.db import models
 
 # Create your models here.
diff --git a/wiki/plugins/help/wiki_plugin.py b/wiki/plugins/help/wiki_plugin.py
index b56c79950..5ac4edb5e 100644
--- a/wiki/plugins/help/wiki_plugin.py
+++ b/wiki/plugins/help/wiki_plugin.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.utils.translation import ugettext_lazy as _
 
 from wiki.core.plugins import registry
diff --git a/wiki/plugins/images/__init__.py b/wiki/plugins/images/__init__.py
index 5486d78b8..f2ecac4de 100644
--- a/wiki/plugins/images/__init__.py
+++ b/wiki/plugins/images/__init__.py
@@ -1,2 +1,1 @@
-from __future__ import unicode_literals
 default_app_config = 'wiki.apps.ImagesConfig'
diff --git a/wiki/plugins/images/admin.py b/wiki/plugins/images/admin.py
index ed6d5a176..1456e4112 100644
--- a/wiki/plugins/images/admin.py
+++ b/wiki/plugins/images/admin.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django.contrib import admin
 from django import forms
 from wiki.plugins.images import models
@@ -11,7 +10,7 @@ class Meta:
         fields = '__all__'
 
     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)
             self.fields['current_revision'].queryset = revisions
diff --git a/wiki/plugins/images/forms.py b/wiki/plugins/images/forms.py
index d10b9c363..b85edbb8c 100644
--- a/wiki/plugins/images/forms.py
+++ b/wiki/plugins/images/forms.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
@@ -11,21 +10,21 @@ class SidebarForm(forms.ModelForm, PluginSidebarFormMixin):
     def __init__(self, article, request, *args, **kwargs):
         self.article = article
         self.request = request
-        super(SidebarForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
     
     def get_usermessage(self):
-        return _(u"New image %s was successfully uploaded. You can use it by selecting it from the list of available images.") % self.instance.get_filename()
+        return _("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):
         if not self.instance.id:
             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
@@ -37,12 +36,12 @@ 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)
     
     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.set_from_request(self.request)
             #revision.save()
@@ -57,10 +56,10 @@ class Meta:
 
 class PurgeForm(forms.Form):
     
-    confirm = forms.BooleanField(label=_(u'Are you sure?'), required=False)
+    confirm = forms.BooleanField(label=_('Are you sure?'), required=False)
     
     def clean_confirm(self):
         confirm = self.cleaned_data['confirm']
         if not confirm:
-            raise forms.ValidationError(_(u'You are not sure enough!'))
+            raise forms.ValidationError(_('You are not sure enough!'))
         return confirm
diff --git a/wiki/plugins/images/markdown_extensions.py b/wiki/plugins/images/markdown_extensions.py
index 49d310909..8cca18c18 100644
--- a/wiki/plugins/images/markdown_extensions.py
+++ b/wiki/plugins/images/markdown_extensions.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import markdown
 import re
 
diff --git a/wiki/plugins/images/migrations/0001_initial.py b/wiki/plugins/images/migrations/0001_initial.py
index 3d948851b..dd2e2ddeb 100644
--- a/wiki/plugins/images/migrations/0001_initial.py
+++ b/wiki/plugins/images/migrations/0001_initial.py
@@ -1,7 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from __future__ import absolute_import
 from django.db import models, migrations
 import wiki.plugins.images.models
 
diff --git a/wiki/plugins/images/models.py b/wiki/plugins/images/models.py
index 3eefbab45..cbd1df356 100644
--- a/wiki/plugins/images/models.py
+++ b/wiki/plugins/images/models.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django.conf import settings as django_settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db import models
@@ -41,12 +40,12 @@ def can_delete(self, user):
         return self.can_write(user=user)
 
     class Meta:
-        verbose_name = _(u'image')
-        verbose_name_plural = _(u'images')
+        verbose_name = _('image')
+        verbose_name_plural = _('images')
         db_table = 'wiki_images_image'
     
     def __unicode__(self):
-        title = (_(u'Image: %s') % self.current_revision.imagerevision.get_filename()) if self.current_revision else _(u'Current revision not set!!')
+        title = (_('Image: %s') % self.current_revision.imagerevision.get_filename()) if self.current_revision else _('Current revision not set!!')
         return title
 
 
@@ -90,14 +89,14 @@ def inherit_predecessor(self, image, skip_image_file=False):
                 self.image = predecessor.image
                 self.width = predecessor.width
                 self.height = predecessor.height
-            except IOError:
+            except OSError:
                 self.image = None
 
     class Meta:
-        verbose_name = _(u'image revision')
-        verbose_name_plural = _(u'image revisions')
+        verbose_name = _('image revision')
+        verbose_name_plural = _('image revisions')
         db_table = 'wiki_images_imagerevision'
         ordering = ('-created',)
 
     def __unicode__(self):
-        return _(u'Image Revsion: %d') % self.revision_number
+        return _('Image Revsion: %d') % self.revision_number
diff --git a/wiki/plugins/images/settings.py b/wiki/plugins/images/settings.py
index 9dac168ee..7bcc27e7b 100644
--- a/wiki/plugins/images/settings.py
+++ b/wiki/plugins/images/settings.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django.conf import settings as django_settings
 
 # Where to store images
diff --git a/wiki/plugins/images/templatetags/wiki_images_tags.py b/wiki/plugins/images/templatetags/wiki_images_tags.py
index 74bbbe1e7..dcbfec8cf 100644
--- a/wiki/plugins/images/templatetags/wiki_images_tags.py
+++ b/wiki/plugins/images/templatetags/wiki_images_tags.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django import template
 
 from wiki.plugins.images import models
diff --git a/wiki/plugins/images/views.py b/wiki/plugins/images/views.py
index 784994a3b..dd302369e 100644
--- a/wiki/plugins/images/views.py
+++ b/wiki/plugins/images/views.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django.contrib import messages
 from django.urls import reverse
 from django.shortcuts import get_object_or_404, redirect
@@ -24,7 +23,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
@@ -140,7 +139,7 @@ def dispatch(self, request, article, *args, **kwargs):
         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
@@ -150,7 +149,7 @@ def get_context_data(self, **kwargs):
         # 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
     
diff --git a/wiki/plugins/images/wiki_plugin.py b/wiki/plugins/images/wiki_plugin.py
index 4eeaa0613..3873c364a 100644
--- a/wiki/plugins/images/wiki_plugin.py
+++ b/wiki/plugins/images/wiki_plugin.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
 from django.conf.urls import url
 from django.utils.translation import ugettext_lazy as _
 
@@ -24,7 +22,7 @@ class ImagePlugin(BasePlugin):
     # is handled inside the notifications plugin.
     notifications = [
         {'model': models.ImageRevision,
-         'message': lambda obj: _(u"An image was added: %s") % obj.get_filename(),
+         'message': lambda obj: _("An image was added: %s") % obj.get_filename(),
          'key': ARTICLE_EDIT,
          'created': False,
          'ignore': lambda revision: bool(revision.previous_revision), # Ignore if there is a previous revision... the image isn't new
@@ -43,11 +41,11 @@ class RenderMedia:
 
     urlpatterns = [
         url('^$', views.ImageView.as_view(), name='images_index'),
-        url('^delete/(?P<image_id>\d+)/$', views.DeleteView.as_view(), name='images_delete'),
-        url('^restore/(?P<image_id>\d+)/$', views.DeleteView.as_view(), name='images_restore', kwargs={'restore': True}),
-        url('^purge/(?P<image_id>\d+)/$', views.PurgeView.as_view(), name='images_purge'),
-        url('^(?P<image_id>\d+)/revision/change/(?P<rev_id>\d+)/$', views.RevisionChangeView.as_view(), name='images_restore'),
-        url('^(?P<image_id>\d+)/revision/add/$', views.RevisionAddView.as_view(), name='images_add_revision'),
+        url(r'^delete/(?P<image_id>\d+)/$', views.DeleteView.as_view(), name='images_delete'),
+        url(r'^restore/(?P<image_id>\d+)/$', views.DeleteView.as_view(), name='images_restore', kwargs={'restore': True}),
+        url(r'^purge/(?P<image_id>\d+)/$', views.PurgeView.as_view(), name='images_purge'),
+        url(r'^(?P<image_id>\d+)/revision/change/(?P<rev_id>\d+)/$', views.RevisionChangeView.as_view(), name='images_restore'),
+        url(r'^(?P<image_id>\d+)/revision/add/$', views.RevisionAddView.as_view(), name='images_add_revision'),
     ]
 
     markdown_extensions = [ImageExtension()]
diff --git a/wiki/plugins/links/mdx/djangowikilinks.py b/wiki/plugins/links/mdx/djangowikilinks.py
index 9316d8e4f..77e8a9d9a 100755
--- a/wiki/plugins/links/mdx/djangowikilinks.py
+++ b/wiki/plugins/links/mdx/djangowikilinks.py
@@ -20,7 +20,6 @@
 '''
 
 
-from __future__ import absolute_import
 import markdown
 from os import path as os_path
 
@@ -41,16 +40,16 @@ def __init__(self, **kwargs):
             'live_lookups': [True, 'If the plugin should try and match links to real articles'],
             'default_level': [2, 'The level that most articles are created at. Relative links will tend to start at that level.']
         }
-        
+
         # Override defaults with user settings
-        super(WikiPathExtension, self).__init__(**kwargs)
-        
+        super().__init__(**kwargs)
+
     def extendMarkdown(self, md, md_globals):
         self.md = md
-        
+
         # append to end of inline patterns
         WIKI_RE =  r'\[(?P<linkTitle>[^\]]+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d\./_-]*)\)'
-        wikiPathPattern = WikiPath(WIKI_RE, self.config, markdown_instance=md)
+        wikiPathPattern = WikiPath(WIKI_RE, self.config, md=md)
         wikiPathPattern.md = md
         md.inlinePatterns.add('djangowikipath', wikiPathPattern, "<reference")
 
@@ -59,7 +58,7 @@ class WikiPath(markdown.inlinepatterns.Pattern):
     def __init__(self, pattern, config, **kwargs):
         markdown.inlinepatterns.Pattern.__init__(self, pattern, **kwargs)
         self.config = config
-    
+
     def handleMatch(self, m) :
         from wiki import models
         article_title = m.group('wikiTitle')
@@ -67,7 +66,7 @@ def handleMatch(self, m) :
         if article_title.startswith("/"):
             absolute = True
         article_title = article_title.strip("/")
-        
+
         # Use this to calculate some kind of meaningful path
         # from the link, regardless of whether or not something can be
         # looked up
@@ -76,7 +75,7 @@ def handleMatch(self, m) :
         if absolute:
             base_path = self.config['base_url'][0]
             path_from_link = os_path.join(base_path, article_title)
-            
+
             urlpath = None
             path = path_from_link
             if self.config['live_lookups'][0]:
@@ -92,23 +91,23 @@ def handleMatch(self, m) :
             # one more component would make a path of length self.config['default_level']
             starting_level = max(0, self.config['default_level'][0] - 1 )
             starting_path = "/".join(source_components[ : starting_level ])
-            
+
             path_from_link = os_path.join(starting_path, article_title)
-            
+
             lookup = models.URLPath.objects.none()
             if self.config['live_lookups'][0]:
                 if urlpath.parent:
                     lookup = urlpath.parent.get_descendants().filter(slug=article_title)
                 else:
                     lookup = urlpath.get_descendants().filter(slug=article_title)
-            
+
             if lookup.count() > 0:
                 urlpath = lookup[0]
                 path = urlpath.get_absolute_url()
             else:
                 urlpath = None
                 path = self.config['base_url'][0] + path_from_link
-            
+
         label = m.group('linkTitle')
         a = etree.Element('a')
         a.set('href', path)
@@ -117,9 +116,9 @@ def handleMatch(self, m) :
         else:
             a.set('class', self.config['html_class'][0])
         a.text = label
-            
+
         return a
-        
+
     def _getMeta(self):
         """ Return meta data or config data. """
         base_url = self.config['base_url'][0]
diff --git a/wiki/plugins/links/mdx/urlize.py b/wiki/plugins/links/mdx/urlize.py
index a800f283b..68e2b591d 100644
--- a/wiki/plugins/links/mdx/urlize.py
+++ b/wiki/plugins/links/mdx/urlize.py
@@ -39,7 +39,6 @@
 
 """
 
-from __future__ import absolute_import
 import markdown
 import re
 
@@ -59,8 +58,8 @@
 class UrlizePattern(markdown.inlinepatterns.Pattern):
 
     def __init__(self, pattern, markdown_instance=None):
-        markdown.inlinepatterns.Pattern.__init__(self, pattern, markdown_instance=markdown_instance)
-        self.compiled_re = re.compile("^(.*?)%s(.*?)$" % pattern, 
+        markdown.inlinepatterns.Pattern.__init__(self, pattern, md=markdown_instance)
+        self.compiled_re = re.compile("^(.*?)%s(.*?)$" % pattern,
                                       re.DOTALL | re.UNICODE | re.IGNORECASE)
 
     """ Return a link Element given an autolink (`http://example/com`). """
@@ -69,18 +68,18 @@ def handleMatch(self, m):
 
         if url.startswith('<'):
             url = url[1:-1]
-            
+
         text = url
-        
+
         if not url.split('://')[0] in ('http','https','ftp'):
             if '@' in url and not '/' in url:
                 url = 'mailto:' + url
             else:
                 url = 'http://' + url
-        
+
         icon = markdown.util.etree.Element("span")
         icon.set('class', 'icon-globe')
-        
+
         span_text = markdown.util.etree.Element("span")
         span_text.text = markdown.util.AtomicString(" " + text)
         el = markdown.util.etree.Element("a")
diff --git a/wiki/plugins/links/models.py b/wiki/plugins/links/models.py
index 00539216d..71a836239 100644
--- a/wiki/plugins/links/models.py
+++ b/wiki/plugins/links/models.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django.db import models
 
 # Create your models here.
diff --git a/wiki/plugins/links/views.py b/wiki/plugins/links/views.py
index c955db7d4..f17578d84 100644
--- a/wiki/plugins/links/views.py
+++ b/wiki/plugins/links/views.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from wiki.decorators import json_view, get_article
 from django.views.generic.base import View
 from django.utils.decorators import method_decorator
diff --git a/wiki/plugins/links/wiki_plugin.py b/wiki/plugins/links/wiki_plugin.py
index 3b72db0d2..a3fe719d7 100644
--- a/wiki/plugins/links/wiki_plugin.py
+++ b/wiki/plugins/links/wiki_plugin.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
 from django.conf.urls import url
 from django.utils.translation import ugettext_lazy as _
 
diff --git a/wiki/plugins/notifications/__init__.py b/wiki/plugins/notifications/__init__.py
index 17d97fad5..94436a565 100644
--- a/wiki/plugins/notifications/__init__.py
+++ b/wiki/plugins/notifications/__init__.py
@@ -1,4 +1,3 @@
-from __future__ import unicode_literals
 default_app_config = 'wiki.apps.NotifcationsConfig'
 
 # Key for django_notify
diff --git a/wiki/plugins/notifications/forms.py b/wiki/plugins/notifications/forms.py
index b4121185f..d457aa2b7 100644
--- a/wiki/plugins/notifications/forms.py
+++ b/wiki/plugins/notifications/forms.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
@@ -12,12 +11,12 @@
 
 class SubscriptionForm(PluginSettingsFormMixin, forms.Form):
     
-    settings_form_headline = _(u'Notifications')
+    settings_form_headline = _('Notifications')
     settings_order = 1
     settings_write_access = False
     
-    edit = forms.BooleanField(required=False, label=_(u'When this article is edited'))
-    edit_email = forms.BooleanField(required=False, label=_(u'Also receive emails about article edits'),
+    edit = forms.BooleanField(required=False, label=_('When this article is edited'))
+    edit_email = forms.BooleanField(required=False, label=_('Also receive emails about article edits'),
                                     widget=forms.CheckboxInput(attrs={'onclick': mark_safe("$('#id_edit').attr('checked', $(this).is(':checked'));")}))
     
     def __init__(self, article, request, *args, **kwargs):
@@ -36,7 +35,7 @@ def __init__(self, article, request, *args, **kwargs):
             initial = {'edit': bool(self.edit_notifications),
                        'edit_email': bool(self.edit_notifications.filter(send_emails=True))}
         kwargs['initial'] = initial
-        super(SubscriptionForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
     
     def get_usermessage(self):
         if self.changed_data:
diff --git a/wiki/plugins/notifications/migrations/0001_initial.py b/wiki/plugins/notifications/migrations/0001_initial.py
index 9f541c809..70b364467 100644
--- a/wiki/plugins/notifications/migrations/0001_initial.py
+++ b/wiki/plugins/notifications/migrations/0001_initial.py
@@ -1,7 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from __future__ import absolute_import
 from django.db import models, migrations
 
 
diff --git a/wiki/plugins/notifications/models.py b/wiki/plugins/notifications/models.py
index a9bdca2af..9988f3523 100644
--- a/wiki/plugins/notifications/models.py
+++ b/wiki/plugins/notifications/models.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 from django.db.models import signals
@@ -16,7 +14,7 @@
 class ArticleSubscription(ArticlePlugin, Subscription):
     
     def __unicode__(self):
-        return (_(u"%(user)s subscribing to %(article)s (%(type)s)") % 
+        return (_("%(user)s subscribing to %(article)s (%(type)s)") % 
                 {'user': self.settings.user.username,
                  'article': self.article.current_revision.title,
                  'type': self.notification_type.label})
@@ -39,13 +37,13 @@ def post_article_revision_save(instance, **kwargs):
     if kwargs.get('created', False):
         url = default_url(instance.article)
         if instance.deleted:
-            notify(_(u'Article deleted: %s') % instance.title, ARTICLE_EDIT,
+            notify(_('Article deleted: %s') % instance.title, ARTICLE_EDIT,
                    target_object=instance.article, url=url)
         elif instance.previous_revision:
-            notify(_(u'Article modified: %s') % instance.title, ARTICLE_EDIT,
+            notify(_('Article modified: %s') % instance.title, ARTICLE_EDIT,
                    target_object=instance.article, url=url)
         else:
-            notify(_(u'New article created: %s') % instance.title, ARTICLE_EDIT,
+            notify(_('New article created: %s') % instance.title, ARTICLE_EDIT,
                    target_object=instance, url=url)
 
 
diff --git a/wiki/plugins/notifications/wiki_plugin.py b/wiki/plugins/notifications/wiki_plugin.py
index 5f284059f..e2cf3be5d 100644
--- a/wiki/plugins/notifications/wiki_plugin.py
+++ b/wiki/plugins/notifications/wiki_plugin.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from wiki.core.plugins import registry
 from wiki.core.plugins.base import BasePlugin
 
diff --git a/wiki/templatetags/wiki_tags.py b/wiki/templatetags/wiki_tags.py
index 91f1cdf61..54c3a256f 100644
--- a/wiki/templatetags/wiki_tags.py
+++ b/wiki/templatetags/wiki_tags.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django import template
 from django.conf import settings as django_settings
 from django.contrib.contenttypes.models import ContentType
diff --git a/wiki/urls.py b/wiki/urls.py
index f0c4183b6..f89331299 100644
--- a/wiki/urls.py
+++ b/wiki/urls.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from django.conf.urls import include, url
 
 from wiki.conf import settings
diff --git a/wiki/views/accounts.py b/wiki/views/accounts.py
index 126e04114..850ccc092 100644
--- a/wiki/views/accounts.py
+++ b/wiki/views/accounts.py
@@ -1,8 +1,6 @@
-# -*- coding: utf-8 -*-
 """This is nothing but the usual handling of django user accounts, so
 go ahead and replace it or disable it!"""
 
-from __future__ import absolute_import
 
 from django.conf import settings as django_settings
 from django.contrib import messages
@@ -25,14 +23,14 @@ class Signup(CreateView):
     template_name = "wiki/accounts/signup.html"
 
     def get_success_url(self, *args):
-        messages.success(self.request, _(u'You are now sign up... and now you can sign in!'))
+        messages.success(self.request, _('You are now sign up... and now you can sign in!'))
         return reverse("wiki:login")
 
 class Logout(View):
     
     def get(self, request, *args, **kwargs):
         auth_logout(request)
-        messages.info(request, _(u"You are no longer logged in. Bye bye!"))
+        messages.info(request, _("You are no longer logged in. Bye bye!"))
         return redirect("wiki:get", URLPath.root().path)
 
 class Login(FormView):
@@ -42,7 +40,7 @@ class Login(FormView):
     
     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
     
@@ -57,7 +55,7 @@ def get(self, request, *args, **kwargs):
     
     def form_valid(self, form, *args, **kwargs):
         auth_login(self.request, form.get_user())
-        messages.info(self.request, _(u"You are now logged in! Have fun!"))
+        messages.info(self.request, _("You are now logged in! Have fun!"))
         if self.request.GET.get("next", None):
             return redirect(self.request.GET['next'])
         if django_settings.LOGIN_REDIRECT_URL:
diff --git a/wiki/views/article.py b/wiki/views/article.py
index f9b3ba929..711fd4cef 100644
--- a/wiki/views/article.py
+++ b/wiki/views/article.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 import difflib
 
 from django.contrib import messages
@@ -14,7 +11,6 @@
 from django.views.generic.base import TemplateView, View
 from django.views.generic.edit import FormView
 from django.views.generic.list import ListView
-from six.moves import range
 
 from wiki import editors, forms, models
 from wiki.conf import settings
@@ -32,7 +28,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'
@@ -46,7 +42,7 @@ class Create(FormView, ArticleMixin):
     
     @method_decorator(get_article(can_write=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):
         """
@@ -88,14 +84,14 @@ def form_valid(self, form):
                                     'other_read': self.article.other_read,
                                     'other_write': self.article.other_write,
                                     })
-                messages.success(self.request, _(u"New article '%s' created.") % self.newpath.article.current_revision.title)
+                messages.success(self.request, _("New article '%s' created.") % self.newpath.article.current_revision.title)
 
         # TODO: Handle individual exceptions better and give good feedback.
         except Exception as e:
             if self.request.user.is_superuser:
-                messages.error(self.request, _(u"There was an error creating this article: %s") % str(e))
+                messages.error(self.request, _("There was an error creating this article: %s") % str(e))
             else:
-                messages.error(self.request, _(u"There was an error creating this article."))
+                messages.error(self.request, _("There was an error creating this article."))
             return redirect('wiki:get', '')
 
         url = self.get_success_url()
@@ -147,13 +143,13 @@ def dispatch1(self, request, article, *args, **kwargs):
                     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
@@ -176,7 +172,7 @@ def form_valid(self, form):
             cannot_delete_children = True
 
         if self.cannot_delete_root or cannot_delete_children:
-            messages.error(self.request, _(u'This article cannot be deleted because it has children or is a root article.'))
+            messages.error(self.request, _('This article cannot be deleted because it has children or is a root article.'))
             return redirect('wiki:get', article_id=self.article.id)
 
 
@@ -187,14 +183,14 @@ def form_valid(self, form):
             else:
                 self.article.delete()
             
-            messages.success(self.request, _(u'This article together with all its contents are now completely gone! Thanks!'))
+            messages.success(self.request, _('This article together with all its contents are now completely gone! Thanks!'))
         else:
             revision = models.ArticleRevision()
             revision.inherit_predecessor(self.article)
             revision.set_from_request(self.request)
             revision.deleted = True
             self.article.add_revision(revision)
-            messages.success(self.request, _(u'The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!') % revision.title)
+            messages.success(self.request, _('The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!') % revision.title)
         return self.get_success_url()
 
     def get_success_url(self):
@@ -214,7 +210,7 @@ def get_context_data(self, **kwargs):
         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):
@@ -227,7 +223,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_form(self, form_class=None):
         """
@@ -262,7 +258,7 @@ def get(self, request, *args, **kwargs):
             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
@@ -277,7 +273,7 @@ def post(self, request, *args, **kwargs):
                         if usermessage:
                             messages.success(self.request, usermessage)
                         else:
-                            messages.success(self.request, _(u'Your changes were saved.'))
+                            messages.success(self.request, _('Your changes were saved.'))
                         if self.urlpath:
                             return redirect('wiki:edit', path=self.urlpath.path)
                         return redirect('wiki:edit', article_id=self.article.id)
@@ -287,7 +283,7 @@ def post(self, request, *args, **kwargs):
             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
@@ -300,7 +296,7 @@ def form_valid(self, form):
         revision.deleted = False
         revision.set_from_request(self.request)
         self.article.add_revision(revision)
-        messages.success(self.request, _(u'A new revision of the article was successfully added.'))
+        messages.success(self.request, _('A new revision of the article was successfully added.'))
         return self.get_success_url()
 
     def get_success_url(self):
@@ -318,7 +314,7 @@ def get_context_data(self, **kwargs):
         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 Deleted(Delete):
@@ -359,13 +355,13 @@ def dispatch(self, request, article, *args, **kwargs):
                 revision.deleted = False
                 revision.automatic_log = _('Restoring article')
                 self.article.add_revision(revision)
-                messages.success(request, _(u'The article "%s" and its children are now restored.') % revision.title)
+                messages.success(request, _('The article "%s" and its children are now restored.') % revision.title)
                 if self.urlpath:
                     return redirect('wiki:get', path=self.urlpath.path)
                 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,
@@ -385,7 +381,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'
@@ -413,7 +409,7 @@ def get_context_data(self, **kwargs):
 
     @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):
@@ -431,7 +427,7 @@ def dispatch(self, request, article, *args, **kwargs):
             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)
@@ -478,7 +474,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,):
         """
@@ -512,7 +508,7 @@ def post(self, *args, **kwargs):
             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 = []
@@ -524,7 +520,7 @@ def get(self, *args, **kwargs):
         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:
@@ -534,7 +530,7 @@ def get_success_url(self):
     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)
 
 
 # TODO: Throw in a class-based view
@@ -543,7 +539,7 @@ def change_revision(request, article, revision_id=None, urlpath=None):
     revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id)
     article.current_revision = revision
     article.save()
-    messages.success(request, _(u'The article %(title)s is now set to display revision #%(revision_number)d') % {
+    messages.success(request, _('The article %(title)s is now set to display revision #%(revision_number)d') % {
         'title': revision.title,
         'revision_number': revision.revision_number,
     })
@@ -566,7 +562,7 @@ def dispatch(self, request, article, *args, **kwargs):
             self.revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id)
         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(self.article.current_revision, request.POST, preview=True)
@@ -574,14 +570,14 @@ def post(self, request, *args, **kwargs):
             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
@@ -608,7 +604,7 @@ def diff(request, revision_id, other_revision_id=None):
     other_changes = []
 
     if not other_revision or other_revision.title != revision.title:
-        other_changes.append((_(u'New title'), revision.title))
+        other_changes.append((_('New title'), revision.title))
 
     return dict(diff=list(diff), other_changes=other_changes)
 
@@ -632,11 +628,11 @@ def merge(request, article, revision_id, urlpath=None, template_file="wiki/previ
         new_revision.locked = False
         new_revision.title=article.current_revision.title
         new_revision.content=content
-        new_revision.automatic_log = (_(u'Merge between Revision #%(r1)d and Revision #%(r2)d') % 
+        new_revision.automatic_log = (_('Merge between Revision #%(r1)d and Revision #%(r2)d') % 
                                       {'r1': revision.revision_number, 
                                        'r2': old_revision.revision_number})
         article.add_revision(new_revision, save=True)
-        messages.success(request, _(u'A new revision was created: Merge between Revision #%(r1)d and Revision #%(r2)d') % 
+        messages.success(request, _('A new revision was created: Merge between Revision #%(r1)d and Revision #%(r2)d') % 
                          {'r1': revision.revision_number,
                           'r2': old_revision.revision_number})
         if urlpath:
@@ -674,7 +670,7 @@ def dispatch(self, request, *args, **kwargs):
             # TODO: This is too dangerous... let's say there is no root.article and we end up here,
             # 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(
@@ -685,6 +681,6 @@ def form_valid(self, form):
         return redirect("wiki:root")
 
     def get_context_data(self, **kwargs):
-        data = super(CreateRootView, self).get_context_data(**kwargs)
+        data = super().get_context_data(**kwargs)
         data['editor'] = editors.getEditor()
         return data
diff --git a/wiki/views/mixins.py b/wiki/views/mixins.py
index 45cd0cfbc..90423a9b9 100644
--- a/wiki/views/mixins.py
+++ b/wiki/views/mixins.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.views.generic.base import TemplateResponseMixin
 
 from wiki.conf import settings
@@ -21,7 +19,7 @@ def dispatch(self, request, article, *args, **kwargs):
                                                    articles__article__current_revision__deleted=False,
                                                    user_can_read=request.user):
                 self.children_slice.append(child)
-        return super(ArticleMixin, self).dispatch(request, *args, **kwargs)
+        return super().dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
 


Test patch:
diff --git a/django_notify/tests.py b/django_notify/tests.py
index 1ddecbc38..06eeec547 100644
--- a/django_notify/tests.py
+++ b/django_notify/tests.py
@@ -5,7 +5,6 @@
 Replace this with more appropriate tests for your application.
 """
 
-from __future__ import absolute_import
 
 from django.test import TestCase
 
diff --git a/requirements/test.txt b/requirements/test.txt
index eb6cdf418..1b2ca1cfa 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -4,28 +4,73 @@
 #
 #    make upgrade
 #
-attrs==19.3.0             # via pytest
-bleach==3.1.5             # via -r requirements/base.txt
-coverage==5.1             # via pytest-cov
-django-classy-tags==1.0.0  # via -r requirements/base.txt, django-sekizai
-django-js-asset==1.2.2    # via -r requirements/base.txt, django-mptt
-django-mptt==0.11.0       # via -r requirements/base.txt
-django-sekizai==1.1.0     # via -r requirements/base.txt
-importlib-metadata==1.7.0  # via pluggy, pytest
-markdown==2.6.11          # via -c requirements/constraints.txt, -r requirements/base.txt
-more-itertools==8.4.0     # via pytest
-packaging==20.4           # via -r requirements/base.txt, bleach, pytest
-pathlib2==2.3.5           # via pytest
-pluggy==0.13.1            # via pytest
-py==1.9.0                 # via pytest
-pyparsing==2.4.7          # via -r requirements/base.txt, packaging
-pytest-cov==2.10.0        # via -r requirements/test.in
-pytest-django==3.9.0      # via -r requirements/test.in
-pytest==5.4.3             # via -r requirements/test.in, pytest-cov, pytest-django
-pytz==2020.1              # via -r requirements/base.txt, django
-six==1.15.0               # via -r requirements/base.txt, bleach, django-classy-tags, django-sekizai, packaging, pathlib2
-sorl-thumbnail==12.6.3    # via -r requirements/base.txt
-sqlparse==0.3.1           # via -r requirements/base.txt, django
-wcwidth==0.2.5            # via pytest
-webencodings==0.5.1       # via -r requirements/base.txt, bleach
-zipp==1.2.0               # via importlib-metadata
+attrs==20.3.0
+    # via pytest
+bleach==3.2.1
+    # via -r requirements/base.txt
+coverage==5.3.1
+    # via pytest-cov
+django-classy-tags==2.0.0
+    # via
+    #   -r requirements/base.txt
+    #   django-sekizai
+django-js-asset==1.2.2
+    # via
+    #   -r requirements/base.txt
+    #   django-mptt
+django-mptt==0.11.0
+    # via -r requirements/base.txt
+django-sekizai==2.0.0
+    # via -r requirements/base.txt
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/base.txt
+    #   django-classy-tags
+    #   django-mptt
+    #   django-sekizai
+iniconfig==1.1.1
+    # via pytest
+markdown==3.3.3
+    # via -r requirements/base.txt
+packaging==20.8
+    # via
+    #   -r requirements/base.txt
+    #   bleach
+    #   pytest
+pluggy==0.13.1
+    # via pytest
+py==1.10.0
+    # via pytest
+pyparsing==2.4.7
+    # via
+    #   -r requirements/base.txt
+    #   packaging
+pytest-cov==2.10.1
+    # via -r requirements/test.in
+pytest-django==4.1.0
+    # via -r requirements/test.in
+pytest==6.2.1
+    # via
+    #   -r requirements/test.in
+    #   pytest-cov
+    #   pytest-django
+pytz==2020.5
+    # via
+    #   -r requirements/base.txt
+    #   django
+six==1.15.0
+    # via
+    #   -r requirements/base.txt
+    #   bleach
+sorl-thumbnail==12.7.0
+    # via -r requirements/base.txt
+sqlparse==0.4.1
+    # via
+    #   -r requirements/base.txt
+    #   django
+toml==0.10.2
+    # via pytest
+webencodings==0.5.1
+    # via
+    #   -r requirements/base.txt
+    #   bleach
diff --git a/testproject/manage.py b/testproject/manage.py
index 8c9f710b6..06c89afba 100755
--- a/testproject/manage.py
+++ b/testproject/manage.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-from __future__ import absolute_import
 
 import os
 import sys
diff --git a/testproject/testproject/settings.py b/testproject/testproject/settings.py
index 7a3ec92e5..d70c498d4 100644
--- a/testproject/testproject/settings.py
+++ b/testproject/testproject/settings.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
 from os import path as os_path
 
 PROJECT_PATH = os_path.abspath(os_path.split(__file__)[0])
diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py
index 2e1cc9485..eaa47c094 100644
--- a/testproject/testproject/urls.py
+++ b/testproject/testproject/urls.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from django.conf.urls import include, url
 from django.conf import settings
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
diff --git a/testproject/testproject/wsgi.py b/testproject/testproject/wsgi.py
index cba768928..fc4f30d7d 100644
--- a/testproject/testproject/wsgi.py
+++ b/testproject/testproject/wsgi.py
@@ -13,7 +13,6 @@
 framework.
 
 """
-from __future__ import absolute_import
 import os
 import sys
 
diff --git a/testproject/vmanage.py b/testproject/vmanage.py
index 2a831bb22..68fe4826a 100755
--- a/testproject/vmanage.py
+++ b/testproject/vmanage.py
@@ -1,5 +1,4 @@
 #!virtualenv/bin/python
-from __future__ import absolute_import
 
 import os
 import sys
diff --git a/wiki/plugins/help/tests.py b/wiki/plugins/help/tests.py
index 1ddecbc38..06eeec547 100644
--- a/wiki/plugins/help/tests.py
+++ b/wiki/plugins/help/tests.py
@@ -5,7 +5,6 @@
 Replace this with more appropriate tests for your application.
 """
 
-from __future__ import absolute_import
 
 from django.test import TestCase
 
diff --git a/wiki/plugins/links/tests.py b/wiki/plugins/links/tests.py
index e585808ad..501deb776 100644
--- a/wiki/plugins/links/tests.py
+++ b/wiki/plugins/links/tests.py
@@ -5,7 +5,6 @@
 Replace this with more appropriate tests for your application.
 """
 
-from __future__ import absolute_import
 from django.test import TestCase
 
 
diff --git a/wiki/plugins/notifications/tests.py b/wiki/plugins/notifications/tests.py
index e585808ad..501deb776 100644
--- a/wiki/plugins/notifications/tests.py
+++ b/wiki/plugins/notifications/tests.py
@@ -5,7 +5,6 @@
 Replace this with more appropriate tests for your application.
 """
 
-from __future__ import absolute_import
 from django.test import TestCase