Repository: torchbox/wagtailmedia (language: python)

Issue:
Exclude tests folder from wheels distribution
### Issue Summary

#107 excludes the tests folder from our source distributions, but not from wheels, which are the most commonly used format. We need to find a way to exclude `tests` from both distributions. `packages=find_packages(exclude=["tests"])` doesn’t seem to work.


### Steps to Reproduce

```sh
python setup.py sdist bdist_wheel
```


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 2840467e..dd2221f2 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -6,10 +6,16 @@ source = wagtailmedia
 
 omit =
     */migrations/*
-    */tests/*
+    tests/*
 
+[paths]
+# the following paths should be considered equivalent
+source =
+    src
+    .tox/*/site-packages
 
 [report]
+show_missing = true
 # Regexes for lines to exclude from consideration
 exclude_lines =
     # Have to re-enable the standard pragma
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3df59a0c..e18af828 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,26 +1,39 @@
+default_language_version:
+  python: python3.8
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v3.3.0
+    rev: v4.0.1
     hooks:
-      - id: trailing-whitespace
-      - id: end-of-file-fixer
-      - id: check-yaml
-      - id: check-added-large-files
+    - id: check-added-large-files
+    - id: check-case-conflict
+    - id: check-json
+    - id: check-merge-conflict
+    - id: check-symlinks
+    - id: check-toml
+    - id: check-yaml
+    - id: end-of-file-fixer
+    - id: trailing-whitespace
   - repo: https://github.com/psf/black
-    rev: 20.8b1
+    rev: 21.7b0
     hooks:
       - id: black
         language_version: python3.8
-
-  - repo: https://github.com/timothycrosley/isort
+  - repo: https://github.com/pycqa/isort
     # isort config is in setup.cfg
-    rev: 5.6.4
+    rev: 5.9.3
     hooks:
       - id: isort
         language_version: python3
   - repo: https://gitlab.com/pycqa/flake8
     # flake8 config is in setup.cfg
-    rev: 3.8.4
+    rev: 3.9.2
     hooks:
-      - id: flake8
-        language_version: python3
+    - id: flake8
+      additional_dependencies:
+      - flake8-bugbear
+      - flake8-comprehensions
+  - repo: https://github.com/mgedmin/check-manifest
+    rev: "0.46"
+    hooks:
+    - id: check-manifest
+      args: [--no-build-isolation]
diff --git a/MANIFEST.in b/MANIFEST.in
index f3e7ee16..ad49b234 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,10 @@
-include LICENSE *.rst *.txt
-graft wagtailmedia
-prune wagtailmedia/tests
-global-exclude __pycache__
-global-exclude *.py[co]
+global-exclude *.py[cod] __pycache__
+prune __pycache__
+prune tests
+prune venv .venv
+prune .tox
+graft src/wagtailmedia
+include LICENSE *.md
+include pyproject.toml .editorconfig
+exclude .pre-commit-config.yaml .coveragerc codecov.yml tox.ini
+exclude manage.py runtests.py db.sqlite3 Makefile
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..0059f299
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+# ----------------------------------------------------------------------------
+# Self-Documented Makefile
+# ref: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
+# ----------------------------------------------------------------------------
+.PHONY: help
+.DEFAULT_GOAL := help
+
+help:											## ⁉️  - Display help comments for each make command
+	@grep -E '^[0-9a-zA-Z_-]+:.*? .*$$'  \
+		$(MAKEFILE_LIST)  \
+		| awk 'BEGIN { FS=":.*?## " }; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'  \
+		| sort
+
+clean:	## 🗑️  - Removes pycache and test media
+	@echo "🗑️ - Removing __pycache__ and test artifacts"
+	rm -rf test-media test-static
+	find . -name ".tox" -prune -o -type d -name  "__pycache__" -exec rm -r {} +
+
+package-setup:
+	@echo "📦 - Packaging for PyPI"
+	python3 setup.py clean --all sdist bdist_wheel
+
+package: clean package-setup  ## 📦 - Package for PyPI
+
+test:  ## 🧪 - Run test suite
+	@echo "🧪 - Running test suite"
+	tox
diff --git a/README.md b/README.md
index 1ccadde7..d3bf4af2 100644
--- a/README.md
+++ b/README.md
@@ -194,6 +194,8 @@ wagtailmedia has translations in French and Chinese. More translations welcome!
 
 ## Contributing
 
+All contributions are welcome!
+
 ### Install
 
 To make changes to this project, first clone this repository:
@@ -209,13 +211,31 @@ With your preferred virtualenv activated, install testing dependencies:
 pip install -e .[testing] -U
 ```
 
+### pre-commit
+
+Note that this project uses [pre-commit](https://github.com/pre-commit/pre-commit). To set up locally:
+
+```shell
+# if you don't have it yet, globally
+$ pip install pre-commit
+# go to the project directory
+$ cd wagtailmedia
+# initialize pre-commit
+$ pre-commit install
+
+# Optional, run all checks once for this, then the checks will run only on the changed files
+$ pre-commit run --all-files
+```
+
 ### How to run tests
 
 Now you can run tests as shown below:
 
 ```sh
-python runtests.py
+tox
 ```
 
-Alternately, you can skip local configuration and use the provided vagrant environment for testing.
-[Reference its README for further details.](vagrant/README)
+or, you can run them for a specific environment `tox -e py38-dj32-wagtail213` or specific test
+`tox -e py38-wagtail213 wagtailmedia.tests.test_views.TestMediaChooserUploadView`
+
+To run the test app interactively, use `tox -e interactive`, visit `http://127.0.0.1:8020/admin/` and log in with `admin`/`changeme`.
diff --git a/manage.py b/manage.py
index 0c430429..5db55472 100755
--- a/manage.py
+++ b/manage.py
@@ -1,13 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
 import os
 import sys
 
 
 if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wagtailmedia.tests.settings")
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
     from django.core.management import execute_from_command_line
 
     execute_from_command_line(sys.argv)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..fd4ee885
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,24 @@
+[tool.black]
+line_length = 88
+target-version = ['py38']
+exclude = '''
+(
+  /(
+      \.eggs         # exclude a few common directories in the
+    | \.git          # root of the project
+    | \.mypy_cache
+    | \.tox
+    | \.venv
+    | __pycache__
+    | _build
+    | build
+    | dist
+    | docs
+    | venv
+    | node_modules
+  )/
+)
+'''
+
+[tool.check-manifest]
+ignore-bad-ideas = ["*.mo"]
diff --git a/setup.cfg b/setup.cfg
index 45564084..be00deb3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,7 +13,7 @@ extend-ignore = E203
 known_first_party=wagtailmedia
 known_django=django
 known_wagtail=wagtail,modelcluster
-skip=migrations,.git,__pycache__,LC_MESSAGES,locale,build,dist,.github
+skip=migrations,.git,__pycache__,LC_MESSAGES,locale,build,dist,.github,.tox,venv
 blocked_extensions=rst,html,js,svg,txt,css,scss,png,snap,tsx,sh
 sections=FUTURE,STDLIB,DJANGO,WAGTAIL,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
 default_section=THIRDPARTY
diff --git a/setup.py b/setup.py
index 653d8e2c..1fb0755f 100644
--- a/setup.py
+++ b/setup.py
@@ -2,13 +2,9 @@
 
 import io
 
-from wagtailmedia import __version__
+from setuptools import find_packages, setup
 
-
-try:
-    from setuptools import find_packages, setup
-except ImportError:
-    from distutils.core import setup
+from src.wagtailmedia import __version__
 
 
 # Testing dependencies
@@ -17,13 +13,7 @@
     "mock>=1.0.0",
     # For coverage and PEP8 linting
     "coverage>=3.7.0",
-    "flake8>=3.6.0",
-    "isort==5.6.4",  # leave this pinned - it tends to change rules between patch releases
-    # Required for running tests in Vagrant
-    "tox==3.9.0",
-    # Required for interactive testing via tox
-    "psycopg2-binary<2.9",
-    "django-redis-cache",
+    "tox==3.23.1",
 ]
 
 with io.open("README.md", encoding="utf-8") as readme_file:
@@ -35,13 +25,20 @@
     description="A module for Wagtail that provides functionality "
     "similar to wagtail.documents module, but for audio and video files.",
     author="Mikalai Radchuk",
-    author_email="mikalai.radchuk@torchbox.com",
+    author_email="hello@torchbox.com",
+    maintainer="Dan Braghis",
+    maintainer_email="dan.braghis@torchbox.com",
+    project_urls={
+        "Changelog": "https://github.com/torchbox/wagtailmedia/blob/main/CHANGELOG.md",
+    },
     url="https://github.com/torchbox/wagtailmedia",
-    packages=find_packages(),
+    packages=find_packages(where="src"),
+    package_dir={"": "src"},
     include_package_data=True,
     license="BSD",
     long_description=long_description,
     long_description_content_type="text/markdown",
+    keywords=["wagtail", "django", "media"],
     classifiers=[
         "Development Status :: 4 - Beta",
         "Environment :: Web Environment",
@@ -55,16 +52,15 @@
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Framework :: Django",
-        "Framework :: Django :: 2.0",
-        "Framework :: Django :: 2.1",
         "Framework :: Django :: 2.2",
-        "Framework :: Django :: 3.0",
         "Framework :: Django :: 3.1",
+        "Framework :: Django :: 3.2",
+        "Framework :: Wagtail",
         "Framework :: Wagtail :: 2",
         "Topic :: Internet :: WWW/HTTP :: Site Management",
     ],
     install_requires=[
-        "wagtail>=2.7",
+        "wagtail>=2.11",
     ],
     extras_require={
         "testing": testing_extras,
diff --git a/wagtailmedia/__init__.py b/src/wagtailmedia/__init__.py
similarity index 100%
rename from wagtailmedia/__init__.py
rename to src/wagtailmedia/__init__.py
diff --git a/wagtailmedia/admin.py b/src/wagtailmedia/admin.py
similarity index 100%
rename from wagtailmedia/admin.py
rename to src/wagtailmedia/admin.py
diff --git a/wagtailmedia/admin_urls.py b/src/wagtailmedia/admin_urls.py
similarity index 100%
rename from wagtailmedia/admin_urls.py
rename to src/wagtailmedia/admin_urls.py
diff --git a/wagtailmedia/apps.py b/src/wagtailmedia/apps.py
similarity index 100%
rename from wagtailmedia/apps.py
rename to src/wagtailmedia/apps.py
diff --git a/wagtailmedia/blocks.py b/src/wagtailmedia/blocks.py
similarity index 100%
rename from wagtailmedia/blocks.py
rename to src/wagtailmedia/blocks.py
diff --git a/wagtailmedia/edit_handlers.py b/src/wagtailmedia/edit_handlers.py
similarity index 100%
rename from wagtailmedia/edit_handlers.py
rename to src/wagtailmedia/edit_handlers.py
diff --git a/wagtailmedia/forms.py b/src/wagtailmedia/forms.py
similarity index 85%
rename from wagtailmedia/forms.py
rename to src/wagtailmedia/forms.py
index 8a74321b..22351134 100644
--- a/wagtailmedia/forms.py
+++ b/src/wagtailmedia/forms.py
@@ -6,25 +6,16 @@
 from django.utils.module_loading import import_string
 from django.utils.translation import gettext_lazy as _
 
-from wagtail import VERSION as WAGTAIL_VERSION
 from wagtail.admin import widgets
+from wagtail.admin.forms.collections import (
+    BaseCollectionMemberForm,
+    collection_member_permission_formset_factory,
+)
 
 from wagtailmedia.models import Media
 from wagtailmedia.permissions import permission_policy as media_permission_policy
 
 
-if WAGTAIL_VERSION < (2, 5):
-    from wagtail.admin.forms import (
-        BaseCollectionMemberForm,
-        collection_member_permission_formset_factory,
-    )
-else:
-    from wagtail.admin.forms.collections import (
-        BaseCollectionMemberForm,
-        collection_member_permission_formset_factory,
-    )
-
-
 class BaseMediaForm(BaseCollectionMemberForm):
     class Meta:
         widgets = {
diff --git a/wagtailmedia/locale/.gitkeep b/src/wagtailmedia/locale/.gitkeep
similarity index 100%
rename from wagtailmedia/locale/.gitkeep
rename to src/wagtailmedia/locale/.gitkeep
diff --git a/wagtailmedia/locale/de/LC_MESSAGES/django.mo b/src/wagtailmedia/locale/de/LC_MESSAGES/django.mo
similarity index 100%
rename from wagtailmedia/locale/de/LC_MESSAGES/django.mo
rename to src/wagtailmedia/locale/de/LC_MESSAGES/django.mo
diff --git a/wagtailmedia/locale/de/LC_MESSAGES/django.po b/src/wagtailmedia/locale/de/LC_MESSAGES/django.po
similarity index 100%
rename from wagtailmedia/locale/de/LC_MESSAGES/django.po
rename to src/wagtailmedia/locale/de/LC_MESSAGES/django.po
diff --git a/wagtailmedia/locale/fr/LC_MESSAGES/django.mo b/src/wagtailmedia/locale/fr/LC_MESSAGES/django.mo
similarity index 100%
rename from wagtailmedia/locale/fr/LC_MESSAGES/django.mo
rename to src/wagtailmedia/locale/fr/LC_MESSAGES/django.mo
diff --git a/wagtailmedia/locale/fr/LC_MESSAGES/django.po b/src/wagtailmedia/locale/fr/LC_MESSAGES/django.po
similarity index 100%
rename from wagtailmedia/locale/fr/LC_MESSAGES/django.po
rename to src/wagtailmedia/locale/fr/LC_MESSAGES/django.po
diff --git a/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo b/src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo
similarity index 100%
rename from wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo
rename to src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo
diff --git a/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.po b/src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.po
similarity index 100%
rename from wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.po
rename to src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.po
diff --git a/wagtailmedia/migrations/0001_initial.py b/src/wagtailmedia/migrations/0001_initial.py
similarity index 100%
rename from wagtailmedia/migrations/0001_initial.py
rename to src/wagtailmedia/migrations/0001_initial.py
diff --git a/wagtailmedia/migrations/0002_initial_data.py b/src/wagtailmedia/migrations/0002_initial_data.py
similarity index 100%
rename from wagtailmedia/migrations/0002_initial_data.py
rename to src/wagtailmedia/migrations/0002_initial_data.py
diff --git a/wagtailmedia/migrations/0003_copy_media_permissions_to_collections.py b/src/wagtailmedia/migrations/0003_copy_media_permissions_to_collections.py
similarity index 100%
rename from wagtailmedia/migrations/0003_copy_media_permissions_to_collections.py
rename to src/wagtailmedia/migrations/0003_copy_media_permissions_to_collections.py
diff --git a/wagtailmedia/migrations/0004_duration_optional_floatfield.py b/src/wagtailmedia/migrations/0004_duration_optional_floatfield.py
similarity index 100%
rename from wagtailmedia/migrations/0004_duration_optional_floatfield.py
rename to src/wagtailmedia/migrations/0004_duration_optional_floatfield.py
diff --git a/wagtailmedia/migrations/__init__.py b/src/wagtailmedia/migrations/__init__.py
similarity index 100%
rename from wagtailmedia/migrations/__init__.py
rename to src/wagtailmedia/migrations/__init__.py
diff --git a/wagtailmedia/models.py b/src/wagtailmedia/models.py
similarity index 95%
rename from wagtailmedia/models.py
rename to src/wagtailmedia/models.py
index 62090c28..96ef7702 100644
--- a/wagtailmedia/models.py
+++ b/src/wagtailmedia/models.py
@@ -11,7 +11,7 @@
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
-from wagtail import VERSION as WAGTAIL_VERSION
+from wagtail.admin.models import get_object_usage
 from wagtail.core.models import CollectionMember
 from wagtail.search import index
 from wagtail.search.queryset import SearchableQuerySetMixin
@@ -19,12 +19,6 @@
 from taggit.managers import TaggableManager
 
 
-if WAGTAIL_VERSION < (2, 9):
-    from wagtail.admin.utils import get_object_usage
-else:
-    from wagtail.admin.models import get_object_usage
-
-
 class MediaQuerySet(SearchableQuerySetMixin, models.QuerySet):
     pass
 
diff --git a/wagtailmedia/permissions.py b/src/wagtailmedia/permissions.py
similarity index 100%
rename from wagtailmedia/permissions.py
rename to src/wagtailmedia/permissions.py
diff --git a/wagtailmedia/signal_handlers.py b/src/wagtailmedia/signal_handlers.py
similarity index 100%
rename from wagtailmedia/signal_handlers.py
rename to src/wagtailmedia/signal_handlers.py
diff --git a/wagtailmedia/static/wagtailmedia/js/media-chooser-modal.js b/src/wagtailmedia/static/wagtailmedia/js/media-chooser-modal.js
similarity index 100%
rename from wagtailmedia/static/wagtailmedia/js/media-chooser-modal.js
rename to src/wagtailmedia/static/wagtailmedia/js/media-chooser-modal.js
diff --git a/wagtailmedia/static/wagtailmedia/js/media-chooser-telepath.js b/src/wagtailmedia/static/wagtailmedia/js/media-chooser-telepath.js
similarity index 100%
rename from wagtailmedia/static/wagtailmedia/js/media-chooser-telepath.js
rename to src/wagtailmedia/static/wagtailmedia/js/media-chooser-telepath.js
diff --git a/wagtailmedia/static/wagtailmedia/js/media-chooser.js b/src/wagtailmedia/static/wagtailmedia/js/media-chooser.js
similarity index 100%
rename from wagtailmedia/static/wagtailmedia/js/media-chooser.js
rename to src/wagtailmedia/static/wagtailmedia/js/media-chooser.js
diff --git a/wagtailmedia/templates/wagtailmedia/chooser/chooser.html b/src/wagtailmedia/templates/wagtailmedia/chooser/chooser.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/chooser/chooser.html
rename to src/wagtailmedia/templates/wagtailmedia/chooser/chooser.html
diff --git a/wagtailmedia/templates/wagtailmedia/chooser/results.html b/src/wagtailmedia/templates/wagtailmedia/chooser/results.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/chooser/results.html
rename to src/wagtailmedia/templates/wagtailmedia/chooser/results.html
diff --git a/wagtailmedia/templates/wagtailmedia/homepage/site_summary_media.html b/src/wagtailmedia/templates/wagtailmedia/homepage/site_summary_media.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/homepage/site_summary_media.html
rename to src/wagtailmedia/templates/wagtailmedia/homepage/site_summary_media.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/_file_field.html b/src/wagtailmedia/templates/wagtailmedia/media/_file_field.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/_file_field.html
rename to src/wagtailmedia/templates/wagtailmedia/media/_file_field.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/_file_field_as_li.html b/src/wagtailmedia/templates/wagtailmedia/media/_file_field_as_li.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/_file_field_as_li.html
rename to src/wagtailmedia/templates/wagtailmedia/media/_file_field_as_li.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field.html b/src/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/_thumbnail_field.html
rename to src/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field_as_li.html b/src/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field_as_li.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/_thumbnail_field_as_li.html
rename to src/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field_as_li.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/add.html b/src/wagtailmedia/templates/wagtailmedia/media/add.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/add.html
rename to src/wagtailmedia/templates/wagtailmedia/media/add.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/confirm_delete.html b/src/wagtailmedia/templates/wagtailmedia/media/confirm_delete.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/confirm_delete.html
rename to src/wagtailmedia/templates/wagtailmedia/media/confirm_delete.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/edit.html b/src/wagtailmedia/templates/wagtailmedia/media/edit.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/edit.html
rename to src/wagtailmedia/templates/wagtailmedia/media/edit.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/index.html b/src/wagtailmedia/templates/wagtailmedia/media/index.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/index.html
rename to src/wagtailmedia/templates/wagtailmedia/media/index.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/list.html b/src/wagtailmedia/templates/wagtailmedia/media/list.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/list.html
rename to src/wagtailmedia/templates/wagtailmedia/media/list.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/results.html b/src/wagtailmedia/templates/wagtailmedia/media/results.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/results.html
rename to src/wagtailmedia/templates/wagtailmedia/media/results.html
diff --git a/wagtailmedia/templates/wagtailmedia/media/usage.html b/src/wagtailmedia/templates/wagtailmedia/media/usage.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/media/usage.html
rename to src/wagtailmedia/templates/wagtailmedia/media/usage.html
diff --git a/wagtailmedia/templates/wagtailmedia/permissions/includes/media_permissions_formset.html b/src/wagtailmedia/templates/wagtailmedia/permissions/includes/media_permissions_formset.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/permissions/includes/media_permissions_formset.html
rename to src/wagtailmedia/templates/wagtailmedia/permissions/includes/media_permissions_formset.html
diff --git a/wagtailmedia/templates/wagtailmedia/widgets/media_chooser.html b/src/wagtailmedia/templates/wagtailmedia/widgets/media_chooser.html
similarity index 100%
rename from wagtailmedia/templates/wagtailmedia/widgets/media_chooser.html
rename to src/wagtailmedia/templates/wagtailmedia/widgets/media_chooser.html
diff --git a/src/wagtailmedia/utils.py b/src/wagtailmedia/utils.py
new file mode 100644
index 00000000..c467d8b5
--- /dev/null
+++ b/src/wagtailmedia/utils.py
@@ -0,0 +1,10 @@
+from django.core.paginator import Paginator
+
+
+DEFAULT_PAGE_KEY = "p"
+
+
+def paginate(request, items, page_key=DEFAULT_PAGE_KEY, per_page=20):
+    paginator = Paginator(items, per_page)
+    page = paginator.get_page(request.GET.get(page_key))
+    return paginator, page
diff --git a/wagtailmedia/tests/__init__.py b/src/wagtailmedia/views/__init__.py
similarity index 100%
rename from wagtailmedia/tests/__init__.py
rename to src/wagtailmedia/views/__init__.py
diff --git a/wagtailmedia/views/chooser.py b/src/wagtailmedia/views/chooser.py
similarity index 93%
rename from wagtailmedia/views/chooser.py
rename to src/wagtailmedia/views/chooser.py
index 8f2891bb..37298533 100644
--- a/wagtailmedia/views/chooser.py
+++ b/src/wagtailmedia/views/chooser.py
@@ -1,7 +1,8 @@
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 
-from wagtail import VERSION as WAGTAIL_VERSION
+from wagtail.admin.auth import PermissionPolicyChecker
+from wagtail.admin.forms.search import SearchForm
 from wagtail.admin.modal_workflow import render_modal_workflow
 from wagtail.admin.models import popular_tags_for_model
 from wagtail.core import hooks
@@ -14,20 +15,7 @@
 from wagtailmedia.utils import paginate
 
 
-if WAGTAIL_VERSION < (2, 5):
-    from wagtail.admin.forms import SearchForm
-
-    pagination_template = "wagtailadmin/shared/pagination_nav.html"
-else:
-    from wagtail.admin.forms.search import SearchForm
-
-    pagination_template = "wagtailadmin/shared/ajax_pagination_nav.html"
-
-if WAGTAIL_VERSION < (2, 9):
-    from wagtail.admin.utils import PermissionPolicyChecker
-else:
-    from wagtail.admin.auth import PermissionPolicyChecker
-
+pagination_template = "wagtailadmin/shared/ajax_pagination_nav.html"
 permission_checker = PermissionPolicyChecker(permission_policy)
 
 
diff --git a/wagtailmedia/views/media.py b/src/wagtailmedia/views/media.py
similarity index 94%
rename from wagtailmedia/views/media.py
rename to src/wagtailmedia/views/media.py
index eeb640cc..7ed631f6 100644
--- a/wagtailmedia/views/media.py
+++ b/src/wagtailmedia/views/media.py
@@ -5,8 +5,10 @@
 from django.utils.translation import gettext as _
 from django.views.decorators.vary import vary_on_headers
 
-from wagtail import VERSION as WAGTAIL_VERSION
 from wagtail.admin import messages
+from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
+from wagtail.admin.forms.search import SearchForm
+from wagtail.admin.models import popular_tags_for_model
 from wagtail.core.models import Collection
 from wagtail.search.backends import get_search_backends
 
@@ -16,22 +18,6 @@
 from wagtailmedia.utils import paginate
 
 
-if WAGTAIL_VERSION < (2, 5):
-    from wagtail.admin.forms import SearchForm
-else:
-    from wagtail.admin.forms.search import SearchForm
-
-if WAGTAIL_VERSION < (2, 9):
-    from wagtail.admin.utils import (
-        PermissionPolicyChecker,
-        permission_denied,
-        popular_tags_for_model,
-    )
-else:
-    from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
-    from wagtail.admin.models import popular_tags_for_model
-
-
 permission_checker = PermissionPolicyChecker(permission_policy)
 
 
diff --git a/wagtailmedia/wagtail_hooks.py b/src/wagtailmedia/wagtail_hooks.py
similarity index 100%
rename from wagtailmedia/wagtail_hooks.py
rename to src/wagtailmedia/wagtail_hooks.py
diff --git a/wagtailmedia/widgets.py b/src/wagtailmedia/widgets.py
similarity index 100%
rename from wagtailmedia/widgets.py
rename to src/wagtailmedia/widgets.py
diff --git a/tox.ini b/tox.ini
index d3607554..974708af 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,81 +1,56 @@
 [tox]
-requires = tox-docker
-skipsdist = True
-toxworkdir = /home/vagrant/.tox
 usedevelop = True
 
 envlist =
-    py{36,37,38,39}-dj{22,31}-wag{211,212,213}
-    py{38,39}-dj{32}-wag{213}
+    py{36,37,38,39}-dj{22,31}-wagtail{211,212,213}
+    py{36,37,38,39}-dj31-wagtail214
+    py{38,39}-dj{32}-wagtail{213,214}
     interactive
-    flake
-    isort
-
 
 [testenv]
-install_command = pip install -e ".[testing]" -U {opts} {packages}
-
-commands =
-    {posargs:coverage run runtests.py}
+setenv =
+    PYTHONPATH = {toxinidir}
+    DJANGO_SETTINGS_MODULE = tests.settings
 
-basepython =
-    py36: python3.6
-    py37: python3.7
-    py38: python3.8
-    py39: python3.9
-    {interactive,flake,isort}: python3.9
+extras = testing
 
 deps =
+    coverage
     dj22: Django>=2.2,<2.3
     dj31: Django>=3.1,<3.2
-    dj32: Django>=3.2,<3.3
-    wag211: wagtail>=2.11,<2.12
-    wag212: wagtail>=2.12,<2.13
-    wag213: wagtail>=2.13,<2.14
-    {interactive,flake,isort}: wagtail
+    dj32: Django>=3.1,<3.2
+    wagtail211: wagtail>=2.11,<2.12
+    wagtail212: wagtail>=2.12,<2.13
+    wagtail213: wagtail>=2.13,<2.14
+    wagtail214: wagtail>=2.14,<2.15
+    interactive: wagtail>=2.14,<2.15
 
+install_command = pip install -U {opts} {packages}
 
-[testenv:flake]
-commands = {posargs:flake8 wagtailmedia}
-
-
-[testenv:isort]
-commands = {posargs:isort --check-only --diff wagtailmedia}
+commands_pre =
+    python {toxinidir}/manage.py migrate
 
+commands =
+    coverage run runtests.py {posargs: -v 2}
+    coverage report -m
 
 [testenv:interactive]
+basepython = python3.9
+
 commands_pre =
     python {toxinidir}/manage.py makemigrations
     python {toxinidir}/manage.py migrate
-    python {toxinidir}/manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('super', 'super@example.com', 'super')"
+    python {toxinidir}/manage.py shell -c "from django.contrib.auth.models import User;(not User.objects.filter(username='admin').exists()) and User.objects.create_superuser('admin', 'super@example.com', 'changeme')"
+    python {toxinidir}/manage.py createcachetable
 
 commands =
     {posargs:python manage.py runserver 0.0.0.0:8020}
 
-docker =
-    postgres:12.0
-    redis:5.0
-
-dockerenv =
-    POSTGRES_PASSWORD=pgpass
-    POSTGRES_USER=pguser
-    POSTGRES_DB=pgdb
-
 setenv =
     INTERACTIVE = 1
 
-
-[docker:redis:5.0]
-healthcheck_cmd = redis-cli ping | grep -q PONG
-healthcheck_interval = 3
-healthcheck_timeout = 3
-healthcheck_retries = 30
-healthcheck_start_period = 5
-
-
-[docker:postgres:12.0]
-healthcheck_cmd = pg_isready
-healthcheck_interval = 3
-healthcheck_timeout = 3
-healthcheck_retries = 30
-healthcheck_start_period = 5
+[testenv:wagtailmain]
+usedevelop = true
+deps =
+    coverage
+    wagtailmain: git+https://github.com/wagtail/wagtail.git@main#egg=Wagtail
diff --git a/vagrant/.gitignore b/vagrant/.gitignore
deleted file mode 100644
index a977916f..00000000
--- a/vagrant/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-.vagrant/
diff --git a/vagrant/README b/vagrant/README
deleted file mode 100644
index b9b62687..00000000
--- a/vagrant/README
+++ /dev/null
@@ -1,35 +0,0 @@
-CREATES A VAGRANT ENVIRONMENT TO FACILITATE LOCAL TESTING
-
-** VAGRANT MUST BE INSTALLED FOR THIS TO WORK! **
-IF YOU ARE NOT FAMILIAR WITH VAGRANT, [FOLLOW THE INSTALLATION INSTRUCTIONS BEFORE CONTINUING](https://www.vagrantup.com/docs/installation/).
-
-========
-USAGE:
-========
-To run tests:
-    cd to this directory on the host machine and issue the following commands:
-        vagrant up
-        vagrant ssh
-
-    then:
-        bash /vagrant/vagrant/runtox.sh -e py39-dj31-wag211
-
-To run interactive demo inside the vagrant guest:
-    ** note: you might need to manually pull the docker images in tox.ini manually on the first run! it hangs for me occasionally **
-    docker pull redis:5.0
-    docker pull postgres:12.0
-
-    bash /vagrant/vagrant/runtox.sh -e interactive
-
-    then:
-        open your browser on the host and navigate to http://localhost:8020/admin/
-
-    log in credentials:
-        username: super
-        password: super
-
-To run flake8 inside the vagrant guest:
-    bash /vagrant/vagrant/runtox.sh -e flake
-
-To run isort inside the vagrant guest:
-    bash /vagrant/vagrant/runtox.sh -e isort
diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile
deleted file mode 100644
index a6e2b91a..00000000
--- a/vagrant/Vagrantfile
+++ /dev/null
@@ -1,12 +0,0 @@
-Vagrant.configure("2") do |config|
-    config.vm.box = "hashicorp/bionic64"
-
-    config.vm.provider "virtualbox" do |v|
-      v.name = "wagtailmedia"
-    end
-
-    config.vm.network "forwarded_port", guest: 8020, host: 8020
-    config.vm.provision :shell, path: "provision.sh"
-    config.vm.synced_folder ".", "/vagrant", disabled: true
-    config.vm.synced_folder "../", "/vagrant"
-end
diff --git a/vagrant/provision.sh b/vagrant/provision.sh
deleted file mode 100644
index 089cea13..00000000
--- a/vagrant/provision.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-set -o errexit
-set -o pipefail
-set -o nounset
-shopt -s failglob
-set -o xtrace
-
-export DEBIAN_FRONTEND=noninteractive
-
-add-apt-repository -y ppa:deadsnakes/ppa
-apt-get update
-apt-get install -y python3-distutils build-essential libssl-dev python-dev python3.6 python3.6-dev python3.7 python3.7-dev python3.8 python3.8-dev python3.9 python3.9-dev
-
-curl -sSL https://get.docker.com/ | sh
-adduser vagrant docker
-
-curl https://bootstrap.pypa.io/get-pip.py | python3
-pip3 install tox tox-docker
diff --git a/vagrant/runtox.sh b/vagrant/runtox.sh
deleted file mode 100644
index a1680a32..00000000
--- a/vagrant/runtox.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-tox -c /vagrant/tox.ini -vv "$@"
-
-docker stop $(docker ps -q)
-docker system prune --force --volumes
diff --git a/wagtailmedia/utils.py b/wagtailmedia/utils.py
deleted file mode 100644
index ad846228..00000000
--- a/wagtailmedia/utils.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from wagtail import VERSION as WAGTAIL_VERSION
-
-
-if WAGTAIL_VERSION < (2, 5):
-    from wagtail.utils.pagination import paginate
-else:
-    from django.core.paginator import Paginator
-
-    DEFAULT_PAGE_KEY = "p"
-
-    def paginate(request, items, page_key=DEFAULT_PAGE_KEY, per_page=20):
-        paginator = Paginator(items, per_page)
-        page = paginator.get_page(request.GET.get(page_key))
-        return paginator, page


Test patch:
diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml
index efdf4d3a..8d2f438c 100644
--- a/.github/workflows/nightly-tests.yml
+++ b/.github/workflows/nightly-tests.yml
@@ -15,7 +15,7 @@ jobs:
 
     services:
       postgres:
-        image: postgres:10.8
+        image: postgres:12.7
         ports:
           - 5432:5432
         options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ad601ac1..71e05a9b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,67 +15,64 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
       - name: Set up Python 3.8
         uses: actions/setup-python@v2
         with:
-          python-version: 3.8
-      - uses: pre-commit/action@v2.0.0
+          python-version: "3.8"
+      - uses: pre-commit/action@v2.0.2
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
   test:
+    name: Python ${{ matrix.python-version }}
     runs-on: ubuntu-latest
     needs: lint
-    continue-on-error: ${{ matrix.experimental }}
+
     strategy:
       matrix:
+        python-version: ["3.6", "3.7", "3.8", "3.9"]
+        experimental: [false]
         include:
-          - python: 3.6
-            django: "Django>=2.2,<2.3"
-            wagtail: "wagtail>=2.11,<2.12"
-            experimental: false
-          - python: 3.7
-            django: "Django>=3.1,<3.2"
-            wagtail: "wagtail>=2.12,<2.13"
-            experimental: false
-          - python: 3.8
-            django: "Django>=3.2,<3.3"
-            wagtail: "wagtail>=2.13,<2.14"
-            experimental: false
-          - python: 3.9
-            django: "Django>=3.2,<3.3"
-            wagtail: "wagtail>=2.13,<2.14"
-            experimental: false
-          - python: 3.9
-            django: "Django>=3.2,<3.3"
-            wagtail: "git+https://github.com/wagtail/wagtail.git@main#egg=wagtail"
+          - python-version: "3.x"
+            toxenv: "wagtailmain"
             experimental: true
 
     services:
       postgres:
-        image: postgres:10.8
+        image: postgres:12.7
+        env:
+          POSTGRES_PASSWORD: postgres
         ports:
           - 5432:5432
         options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
 
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v2
+
+      - uses: actions/setup-python@v2
         with:
-          python-version: ${{ matrix.python }}
+          python-version: ${{ matrix.python-version }}
+
       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip
-          pip install "psycopg2>=2.6"
-          pip install "${{ matrix.django }}"
-          pip install "${{ matrix.wagtail }}"
-          pip install -e .[testing]
-      - name: Test
-        run: |
-          coverage run runtests.py
+          python -m pip install --upgrade pip setuptools wheel
+          python -m pip install --upgrade tox tox-py tox-venv
+      - name: Run tox targets for Python ${{ matrix.python-version }}
+        if: ${{ matrix.toxenv != 'wagtailmain' }}
+        continue-on-error: ${{ matrix.experimental }}
+        run: tox --py current
         env:
           DATABASE_ENGINE: django.db.backends.postgresql
           DATABASE_HOST: localhost
           DATABASE_USER: postgres
           DATABASE_PASS: postgres
+
+      - name: Run wagtailmain tox targets
+        if: ${{ matrix.toxenv == 'wagtailmain' }}
+        continue-on-error: ${{ matrix.experimental }}
+        run: tox -e ${{ matrix.toxenv }}
+
       - name: Upload coverage reports to Codecov
         run: |
           pip install codecov
diff --git a/runtests.py b/runtests.py
index 46ed935f..ac0d8d88 100755
--- a/runtests.py
+++ b/runtests.py
@@ -7,16 +7,10 @@
 
 from django.core.management import execute_from_command_line
 
+from tests.settings import MEDIA_ROOT, STATIC_ROOT
 
-try:
-    from wagtail.utils.deprecation import RemovedInWagtail27Warning
-except ImportError:
 
-    class RemovedInWagtail27Warning(Warning):
-        pass
-
-
-os.environ["DJANGO_SETTINGS_MODULE"] = "wagtailmedia.tests.settings"
+os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
 
 
 def runtests():
@@ -31,13 +25,13 @@ def runtests():
 
     args = sys.argv[1:]
     argv = sys.argv[:1] + ["test"] + args
+
+    shutil.rmtree(STATIC_ROOT, ignore_errors=True)
+    shutil.rmtree(MEDIA_ROOT, ignore_errors=True)
+
     try:
-        # adding an assert to catch RemovedInWagtail27Warning warnings
-        warnings.filterwarnings("error", category=RemovedInWagtail27Warning)
         execute_from_command_line(argv)
     finally:
-        from wagtailmedia.tests.settings import MEDIA_ROOT, STATIC_ROOT
-
         shutil.rmtree(STATIC_ROOT, ignore_errors=True)
         shutil.rmtree(MEDIA_ROOT, ignore_errors=True)
 
diff --git a/wagtailmedia/tests/.gitignore b/tests/.gitignore
similarity index 100%
rename from wagtailmedia/tests/.gitignore
rename to tests/.gitignore
diff --git a/wagtailmedia/tests/testapp/migrations/__init__.py b/tests/__init__.py
similarity index 100%
rename from wagtailmedia/tests/testapp/migrations/__init__.py
rename to tests/__init__.py
diff --git a/wagtailmedia/tests/settings.py b/tests/settings.py
similarity index 73%
rename from wagtailmedia/tests/settings.py
rename to tests/settings.py
index cc8c2041..57948a61 100644
--- a/wagtailmedia/tests/settings.py
+++ b/tests/settings.py
@@ -1,7 +1,5 @@
 import os
 
-from wagtail import VERSION as WAGTAIL_VERSION
-
 
 DEBUG = "INTERACTIVE" in os.environ
 
@@ -10,36 +8,23 @@
 MEDIA_ROOT = os.path.join(WAGTAILMEDIA_ROOT, "test-media")
 MEDIA_URL = "/media/"
 
-POSTGRES_PORT = os.getenv("POSTGRES_5432_TCP_PORT", "")
-if POSTGRES_PORT:
-    DATABASES = {
-        "default": {
-            "ENGINE": "django.db.backends.postgresql",
-            "NAME": "pgdb",
-            "USER": "pguser",
-            "PASSWORD": "pgpass",
-            "HOST": "localhost",
-            "PORT": POSTGRES_PORT,
-        }
-    }
-else:
-    DATABASES = {
-        "default": {
-            "ENGINE": os.environ.get("DATABASE_ENGINE", "django.db.backends.sqlite3"),
-            "NAME": os.environ.get("DATABASE_NAME", "db.sqlite3"),
-            "USER": os.environ.get("DATABASE_USER", None),
-            "PASSWORD": os.environ.get("DATABASE_PASS", None),
-            "HOST": os.environ.get("DATABASE_HOST", None),
-            "TEST": {
-                "NAME": os.environ.get("DATABASE_NAME", None),
-            },
-        }
+DATABASES = {
+    "default": {
+        "ENGINE": os.environ.get("DATABASE_ENGINE", "django.db.backends.sqlite3"),
+        "NAME": os.environ.get("DATABASE_NAME", "db.sqlite3"),
+        "USER": os.environ.get("DATABASE_USER", None),
+        "PASSWORD": os.environ.get("DATABASE_PASS", None),
+        "HOST": os.environ.get("DATABASE_HOST", None),
+        "TEST": {
+            "NAME": os.environ.get("DATABASE_NAME", None),
+        },
     }
+}
 
 
 SECRET_KEY = "not needed"
 
-ROOT_URLCONF = "wagtailmedia.tests.urls"
+ROOT_URLCONF = "tests.urls"
 
 STATIC_URL = "/static/"
 STATIC_ROOT = STATIC_ROOT
@@ -76,15 +61,12 @@
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
 ]
 
-if WAGTAIL_VERSION < (2, 9):
-    MIDDLEWARE += ["wagtail.core.middleware.SiteMiddleware"]
-
 MIDDLEWARE += [
     "wagtail.contrib.redirects.middleware.RedirectMiddleware",
 ]
 
 INSTALLED_APPS = [
-    "wagtailmedia.tests.testapp",
+    "tests.testapp",
     "wagtailmedia",
     "taggit",
     "django.contrib.auth",
diff --git a/wagtailmedia/tests/templates/wagtailmedia/media/add.html b/tests/templates/wagtailmedia/media/add.html
similarity index 96%
rename from wagtailmedia/tests/templates/wagtailmedia/media/add.html
rename to tests/templates/wagtailmedia/media/add.html
index 0c606ddc..bc05fc15 100644
--- a/wagtailmedia/tests/templates/wagtailmedia/media/add.html
+++ b/tests/templates/wagtailmedia/media/add.html
@@ -1,3 +1,3 @@
-{% extends "wagtailmedia/media/add.html" %}
-
-{% block action %}/somewhere/else{% endblock %}
+{% extends "wagtailmedia/media/add.html" %}
+
+{% block action %}/somewhere/else{% endblock %}
diff --git a/wagtailmedia/tests/test_admin.py b/tests/test_admin.py
similarity index 100%
rename from wagtailmedia/tests/test_admin.py
rename to tests/test_admin.py
diff --git a/wagtailmedia/tests/test_blocks.py b/tests/test_blocks.py
similarity index 100%
rename from wagtailmedia/tests/test_blocks.py
rename to tests/test_blocks.py
diff --git a/wagtailmedia/tests/test_form_override.py b/tests/test_form_override.py
similarity index 89%
rename from wagtailmedia/tests/test_form_override.py
rename to tests/test_form_override.py
index 95c63767..94818061 100644
--- a/wagtailmedia/tests/test_form_override.py
+++ b/tests/test_form_override.py
@@ -1,55 +1,55 @@
-from unittest.mock import patch
-
-from django import forms
-from django.test import TestCase, override_settings
-
-from wagtail.admin import widgets
-
-from wagtailmedia import models
-from wagtailmedia.forms import (
-    BaseMediaForm,
-    get_media_base_form,
-    get_media_form,
-    media_base_form,
-)
-from wagtailmedia.tests.testapp.forms import AlternateMediaForm, OverridenWidget
-
-
-class TestFormOverride(TestCase):
-    def test_media_base_form(self):
-        self.assertIs(media_base_form, BaseMediaForm)
-
-    def test_get_media_base_form(self):
-        self.assertIs(get_media_base_form(), BaseMediaForm)
-
-    def test_get_media_form(self):
-        bases = get_media_form(models.Media).__bases__
-        self.assertIn(BaseMediaForm, bases)
-        self.assertNotIn(AlternateMediaForm, bases)
-
-    def test_get_media_form_widgets(self):
-        Form = get_media_form(models.Media)
-        form = Form()
-        self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
-        self.assertIsInstance(form.fields["file"].widget, forms.FileInput)
-        self.assertIsInstance(form.fields["thumbnail"].widget, forms.ClearableFileInput)
-
-    @override_settings(
-        WAGTAILMEDIA_MEDIA_FORM_BASE="wagtailmedia.tests.testapp.forms.AlternateMediaForm"
-    )
-    def test_overridden_base_form(self):
-        self.assertIs(get_media_base_form(), AlternateMediaForm)
-
-    @patch("wagtailmedia.forms.media_base_form", AlternateMediaForm)
-    def test_get_overridden_media_form(self):
-        bases = get_media_form(models.Media).__bases__
-        self.assertNotIn(BaseMediaForm, bases)
-        self.assertIn(AlternateMediaForm, bases)
-
-    @patch("wagtailmedia.forms.media_base_form", AlternateMediaForm)
-    def test_get_overridden_media_form_widgets(self):
-        Form = get_media_form(models.Media)
-        form = Form()
-        self.assertIsInstance(form.fields["tags"].widget, OverridenWidget)
-        self.assertIsInstance(form.fields["file"].widget, OverridenWidget)
-        self.assertIsInstance(form.fields["thumbnail"].widget, OverridenWidget)
+from unittest.mock import patch
+
+from django import forms
+from django.test import TestCase, override_settings
+
+from wagtail.admin import widgets
+
+from tests.testapp.forms import AlternateMediaForm, OverridenWidget
+from wagtailmedia import models
+from wagtailmedia.forms import (
+    BaseMediaForm,
+    get_media_base_form,
+    get_media_form,
+    media_base_form,
+)
+
+
+class TestFormOverride(TestCase):
+    def test_media_base_form(self):
+        self.assertIs(media_base_form, BaseMediaForm)
+
+    def test_get_media_base_form(self):
+        self.assertIs(get_media_base_form(), BaseMediaForm)
+
+    def test_get_media_form(self):
+        bases = get_media_form(models.Media).__bases__
+        self.assertIn(BaseMediaForm, bases)
+        self.assertNotIn(AlternateMediaForm, bases)
+
+    def test_get_media_form_widgets(self):
+        Form = get_media_form(models.Media)
+        form = Form()
+        self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
+        self.assertIsInstance(form.fields["file"].widget, forms.FileInput)
+        self.assertIsInstance(form.fields["thumbnail"].widget, forms.ClearableFileInput)
+
+    @override_settings(
+        WAGTAILMEDIA_MEDIA_FORM_BASE="tests.testapp.forms.AlternateMediaForm"
+    )
+    def test_overridden_base_form(self):
+        self.assertIs(get_media_base_form(), AlternateMediaForm)
+
+    @patch("wagtailmedia.forms.media_base_form", AlternateMediaForm)
+    def test_get_overridden_media_form(self):
+        bases = get_media_form(models.Media).__bases__
+        self.assertNotIn(BaseMediaForm, bases)
+        self.assertIn(AlternateMediaForm, bases)
+
+    @patch("wagtailmedia.forms.media_base_form", AlternateMediaForm)
+    def test_get_overridden_media_form_widgets(self):
+        Form = get_media_form(models.Media)
+        form = Form()
+        self.assertIsInstance(form.fields["tags"].widget, OverridenWidget)
+        self.assertIsInstance(form.fields["file"].widget, OverridenWidget)
+        self.assertIsInstance(form.fields["thumbnail"].widget, OverridenWidget)
diff --git a/wagtailmedia/tests/test_models.py b/tests/test_models.py
similarity index 100%
rename from wagtailmedia/tests/test_models.py
rename to tests/test_models.py
diff --git a/wagtailmedia/tests/test_permissions.py b/tests/test_permissions.py
similarity index 100%
rename from wagtailmedia/tests/test_permissions.py
rename to tests/test_permissions.py
diff --git a/wagtailmedia/tests/test_views.py b/tests/test_views.py
similarity index 99%
rename from wagtailmedia/tests/test_views.py
rename to tests/test_views.py
index 097061f5..21056775 100644
--- a/wagtailmedia/tests/test_views.py
+++ b/tests/test_views.py
@@ -16,8 +16,8 @@
 
 from six import b
 
+from tests.testapp.models import EventPage, EventPageRelatedMedia
 from wagtailmedia import models
-from wagtailmedia.tests.testapp.models import EventPage, EventPageRelatedMedia
 
 
 class TestMediaIndexView(TestCase, WagtailTestUtils):
@@ -31,7 +31,7 @@ def test_simple(self):
         self.assertContains(response, "Add audio")
         self.assertContains(response, "Add video")
 
-    @modify_settings(INSTALLED_APPS={"prepend": "wagtailmedia.tests.testextends"})
+    @modify_settings(INSTALLED_APPS={"prepend": "tests.testextends"})
     def test_extends(self):
         response = self.client.get(reverse("wagtailmedia:index"))
         self.assertEqual(response.status_code, 200)
@@ -469,7 +469,7 @@ def test_simple(self):
         self.assertContains(response, "Filesize")
         self.assertNotContains(response, "wagtailadmin/js/draftail.js")
 
-    @modify_settings(INSTALLED_APPS={"prepend": "wagtailmedia.tests.testextends"})
+    @modify_settings(INSTALLED_APPS={"prepend": "tests.testextends"})
     def test_extends(self):
         response = self.client.get(reverse("wagtailmedia:edit", args=(self.media.id,)))
         self.assertEqual(response.status_code, 200)
diff --git a/wagtailmedia/tests/test_widgets.py b/tests/test_widgets.py
similarity index 100%
rename from wagtailmedia/tests/test_widgets.py
rename to tests/test_widgets.py
diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py
new file mode 100644
index 00000000..fcb2d213
--- /dev/null
+++ b/tests/testapp/__init__.py
@@ -0,0 +1,1 @@
+default_app_config = "tests.testapp.apps.WagtailmediaTestsAppConfig"
diff --git a/wagtailmedia/tests/testapp/apps.py b/tests/testapp/apps.py
similarity index 83%
rename from wagtailmedia/tests/testapp/apps.py
rename to tests/testapp/apps.py
index 769db91b..d1959bde 100644
--- a/wagtailmedia/tests/testapp/apps.py
+++ b/tests/testapp/apps.py
@@ -2,7 +2,7 @@
 
 
 class WagtailmediaTestsAppConfig(AppConfig):
-    name = "wagtailmedia.tests.testapp"
+    name = "tests.testapp"
     label = "wagtailmedia_tests"
     verbose_name = "Wagtail media tests"
     default_auto_field = "django.db.models.AutoField"
diff --git a/wagtailmedia/tests/testapp/fixtures/test.json b/tests/testapp/fixtures/test.json
similarity index 100%
rename from wagtailmedia/tests/testapp/fixtures/test.json
rename to tests/testapp/fixtures/test.json
diff --git a/wagtailmedia/tests/testapp/forms.py b/tests/testapp/forms.py
similarity index 95%
rename from wagtailmedia/tests/testapp/forms.py
rename to tests/testapp/forms.py
index ab613d44..ffd79ce1 100644
--- a/wagtailmedia/tests/testapp/forms.py
+++ b/tests/testapp/forms.py
@@ -1,15 +1,15 @@
-from django.forms import ModelForm
-from django.forms.widgets import Widget
-
-
-class OverridenWidget(Widget):
-    pass
-
-
-class AlternateMediaForm(ModelForm):
-    class Meta:
-        widgets = {
-            "tags": OverridenWidget,
-            "file": OverridenWidget,
-            "thumbnail": OverridenWidget,
-        }
+from django.forms import ModelForm
+from django.forms.widgets import Widget
+
+
+class OverridenWidget(Widget):
+    pass
+
+
+class AlternateMediaForm(ModelForm):
+    class Meta:
+        widgets = {
+            "tags": OverridenWidget,
+            "file": OverridenWidget,
+            "thumbnail": OverridenWidget,
+        }
diff --git a/wagtailmedia/tests/testapp/migrations/0001_initial.py b/tests/testapp/migrations/0001_initial.py
similarity index 95%
rename from wagtailmedia/tests/testapp/migrations/0001_initial.py
rename to tests/testapp/migrations/0001_initial.py
index d5fd6e89..2786e2d8 100644
--- a/wagtailmedia/tests/testapp/migrations/0001_initial.py
+++ b/tests/testapp/migrations/0001_initial.py
@@ -2,6 +2,8 @@
 # Generated by Django 1.9.5 on 2016-04-22 14:21
 from __future__ import unicode_literals
 
+import django.core.validators
+
 from django.conf import settings
 from django.db import migrations, models
 import django.db.models.deletion
@@ -121,8 +123,12 @@ class Migration(migrations.Migration):
                 ),
                 (
                     "duration",
-                    models.PositiveIntegerField(
-                        help_text="Duration in seconds", verbose_name="duration"
+                    models.FloatField(
+                        blank=True,
+                        default=0,
+                        help_text="Duration in seconds",
+                        validators=[django.core.validators.MinValueValidator(0)],
+                        verbose_name="duration",
                     ),
                 ),
                 (
diff --git a/wagtailmedia/tests/testapp/migrations/0002_blogstreampage.py b/tests/testapp/migrations/0002_blogstreampage.py
similarity index 75%
rename from wagtailmedia/tests/testapp/migrations/0002_blogstreampage.py
rename to tests/testapp/migrations/0002_blogstreampage.py
index 9cbbfd84..b278ca94 100644
--- a/wagtailmedia/tests/testapp/migrations/0002_blogstreampage.py
+++ b/tests/testapp/migrations/0002_blogstreampage.py
@@ -1,11 +1,10 @@
 # Generated by Django 3.2.5 on 2021-07-31 11:17
 
-import django.core.validators
 from django.db import migrations, models
 import django.db.models.deletion
 import wagtail.core.blocks
 import wagtail.core.fields
-import wagtailmedia.tests.testapp.models
+import tests.testapp.models
 
 
 class Migration(migrations.Migration):
@@ -47,9 +46,7 @@ class Migration(migrations.Migration):
                             ),
                             (
                                 "media",
-                                wagtailmedia.tests.testapp.models.TestMediaBlock(
-                                    icon="media"
-                                ),
+                                tests.testapp.models.TestMediaBlock(icon="media"),
                             ),
                         ]
                     ),
@@ -60,15 +57,4 @@ class Migration(migrations.Migration):
             },
             bases=("wagtailcore.page",),
         ),
-        migrations.AlterField(
-            model_name="custommedia",
-            name="duration",
-            field=models.FloatField(
-                blank=True,
-                default=0,
-                help_text="Duration in seconds",
-                validators=[django.core.validators.MinValueValidator(0)],
-                verbose_name="duration",
-            ),
-        ),
     ]
diff --git a/wagtailmedia/tests/testextends/__init__.py b/tests/testapp/migrations/__init__.py
similarity index 100%
rename from wagtailmedia/tests/testextends/__init__.py
rename to tests/testapp/migrations/__init__.py
diff --git a/wagtailmedia/tests/testapp/models.py b/tests/testapp/models.py
similarity index 100%
rename from wagtailmedia/tests/testapp/models.py
rename to tests/testapp/models.py
diff --git a/wagtailmedia/tests/testapp/templates/wagtailmedia_tests/event_page.html b/tests/testapp/templates/wagtailmedia_tests/event_page.html
similarity index 100%
rename from wagtailmedia/tests/testapp/templates/wagtailmedia_tests/event_page.html
rename to tests/testapp/templates/wagtailmedia_tests/event_page.html
diff --git a/wagtailmedia/views/__init__.py b/tests/testextends/__init__.py
similarity index 100%
rename from wagtailmedia/views/__init__.py
rename to tests/testextends/__init__.py
diff --git a/wagtailmedia/tests/testextends/templates/wagtailmedia/media/edit.html b/tests/testextends/templates/wagtailmedia/media/edit.html
similarity index 100%
rename from wagtailmedia/tests/testextends/templates/wagtailmedia/media/edit.html
rename to tests/testextends/templates/wagtailmedia/media/edit.html
diff --git a/wagtailmedia/tests/testextends/templates/wagtailmedia/media/index.html b/tests/testextends/templates/wagtailmedia/media/index.html
similarity index 97%
rename from wagtailmedia/tests/testextends/templates/wagtailmedia/media/index.html
rename to tests/testextends/templates/wagtailmedia/media/index.html
index 8b703c6c..104411f5 100644
--- a/wagtailmedia/tests/testextends/templates/wagtailmedia/media/index.html
+++ b/tests/testextends/templates/wagtailmedia/media/index.html
@@ -1,3 +1,3 @@
-{% extends "wagtailmedia/media/index.html" %}
-
-{% block add_actions %}You shan't act{% endblock %}
+{% extends "wagtailmedia/media/index.html" %}
+
+{% block add_actions %}You shan't act{% endblock %}
diff --git a/wagtailmedia/tests/urls.py b/tests/urls.py
similarity index 100%
rename from wagtailmedia/tests/urls.py
rename to tests/urls.py
diff --git a/wagtailmedia/tests/testapp/__init__.py b/wagtailmedia/tests/testapp/__init__.py
deleted file mode 100644
index fc196b69..00000000
--- a/wagtailmedia/tests/testapp/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-default_app_config = "wagtailmedia.tests.testapp.apps.WagtailmediaTestsAppConfig"