├── tests
├── __init__.py
├── testextends
│ ├── __init__.py
│ └── templates
│ │ └── wagtailmedia
│ │ └── media
│ │ ├── index.html
│ │ ├── legacy
│ │ └── index.html
│ │ └── edit.html
├── testapp
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── __init__.py
│ ├── apps.py
│ ├── forms.py
│ ├── templates
│ │ └── wagtailmedia_tests
│ │ │ ├── blog_stream_page.html
│ │ │ └── event_page.html
│ ├── urls.py
│ ├── fixtures
│ │ └── test.json
│ ├── settings.py
│ └── models.py
├── testextends_legacy
│ ├── __init__.py
│ └── templates
│ │ └── wagtailmedia
│ │ └── media
│ │ ├── legacy
│ │ └── index.html
│ │ └── edit.html
├── .gitignore
├── templates
│ └── wagtailmedia
│ │ └── media
│ │ ├── add.html
│ │ └── edit.html
├── test_tags.py
├── test_utils.py
├── test_settings.py
├── utils.py
├── manage.py
├── test_form_override.py
├── test_admin.py
├── test_widgets.py
├── test_legacy_template_usage.py
├── test_blocks.py
├── test_compare.py
├── test_permissions.py
└── test_edit_handlers.py
├── src
└── wagtailmedia
│ ├── locale
│ ├── .gitkeep
│ ├── de
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ ├── ro
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ ├── ru
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ ├── uk
│ │ └── LC_MESSAGES
│ │ │ └── django.mo
│ └── zh_Hans
│ │ └── LC_MESSAGES
│ │ └── django.mo
│ ├── views
│ ├── __init__.py
│ ├── chooser.py
│ └── media.py
│ ├── migrations
│ ├── __init__.py
│ ├── 0005_alter_media_options.py
│ ├── 0004_duration_optional_floatfield.py
│ ├── 0003_copy_media_permissions_to_collections.py
│ ├── 0002_initial_data.py
│ └── 0001_initial.py
│ ├── templatetags
│ ├── __init__.py
│ └── media_tags.py
│ ├── __init__.py
│ ├── static
│ └── wagtailmedia
│ │ ├── css
│ │ ├── wagtailmedia.css
│ │ └── wagtailmedia-comparison.css
│ │ └── js
│ │ ├── media-chooser-telepath.js
│ │ ├── media-chooser.js
│ │ └── media-chooser-modal.js
│ ├── deprecation.py
│ ├── templates
│ └── wagtailmedia
│ │ ├── media
│ │ ├── _file_field_legacy.html
│ │ ├── _file_field.html
│ │ ├── _thumbnail_field_legacy.html
│ │ ├── _thumbnail_field.html
│ │ ├── _file_field_as_li.html
│ │ ├── _thumbnail_field_as_li.html
│ │ ├── confirm_delete.html
│ │ ├── results.html
│ │ ├── usage.html
│ │ ├── index.html
│ │ ├── add.html
│ │ ├── edit.html
│ │ └── list.html
│ │ ├── permissions
│ │ └── includes
│ │ │ └── media_permissions_formset.html
│ │ ├── widgets
│ │ ├── media_chooser.html
│ │ └── compare.html
│ │ ├── icons
│ │ ├── wagtailmedia-audio.svg
│ │ └── wagtailmedia-video.svg
│ │ ├── homepage
│ │ └── site_summary_media.html
│ │ └── chooser
│ │ ├── results.html
│ │ └── chooser.html
│ ├── permissions.py
│ ├── admin.py
│ ├── signal_handlers.py
│ ├── api
│ ├── serializers.py
│ └── views.py
│ ├── admin_urls.py
│ ├── apps.py
│ ├── utils.py
│ ├── edit_handlers.py
│ ├── widgets.py
│ ├── forms.py
│ ├── blocks.py
│ ├── settings.py
│ ├── wagtail_hooks.py
│ └── models.py
├── .github
├── workflows
│ ├── ruff.yml
│ ├── publish.yml
│ ├── nightly-tests.yml
│ └── test.yml
├── ISSUE_TEMPLATE.md
└── report_nightly_build_failure.py
├── ruff.toml
├── .editorconfig
├── .pre-commit-config.yaml
├── .coveragerc
├── Makefile
├── .gitignore
├── LICENSE
├── SPECIFICATION.md
├── pyproject.toml
└── tox.ini
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/testextends/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/wagtailmedia/views/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/testapp/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/testextends_legacy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/wagtailmedia/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | test-media
2 | test-static
3 |
--------------------------------------------------------------------------------
/src/wagtailmedia/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.17.2"
2 |
--------------------------------------------------------------------------------
/src/wagtailmedia/static/wagtailmedia/css/wagtailmedia.css:
--------------------------------------------------------------------------------
1 | .wagtailmedia-tabs .messages {
2 | margin-bottom: 1em;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/templates/wagtailmedia/media/add.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailmedia/media/add.html" %}
2 |
3 | {% block action %}/somewhere/else{% endblock %}
4 |
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/en/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/en/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/ro/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/ro/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/ru/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/ru/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/uk/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/uk/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tests/templates/wagtailmedia/media/edit.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailmedia/media/edit.html" %}
2 |
3 | {% block action %}/somewhere/else/edit{% endblock %}
4 |
--------------------------------------------------------------------------------
/src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torchbox/wagtailmedia/HEAD/src/wagtailmedia/locale/zh_Hans/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tests/testextends/templates/wagtailmedia/media/index.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailmedia/media/index.html" %}
2 |
3 | {% block add_actions %}You shan't act{% endblock %}
4 |
--------------------------------------------------------------------------------
/tests/testextends/templates/wagtailmedia/media/legacy/index.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailmedia/media/legacy/index.html" %}
2 |
3 | {% block add_actions %}You shan't act{% endblock %}
4 |
--------------------------------------------------------------------------------
/tests/testextends_legacy/templates/wagtailmedia/media/legacy/index.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailmedia/media/legacy/index.html" %}
2 |
3 | {% block add_actions %}You shan't act{% endblock %}
4 |
--------------------------------------------------------------------------------
/src/wagtailmedia/deprecation.py:
--------------------------------------------------------------------------------
1 | class RemovedInWagtailMedia015Warning(PendingDeprecationWarning):
2 | pass
3 |
4 |
5 | class RemovedInWagtailMedia016Warning(DeprecationWarning):
6 | pass
7 |
--------------------------------------------------------------------------------
/tests/testapp/__init__.py:
--------------------------------------------------------------------------------
1 | from django import VERSION as DJANGO_VERSION
2 |
3 |
4 | if DJANGO_VERSION >= (3, 2):
5 | # The declaration is only needed for older Django versions
6 | pass
7 | else:
8 | default_app_config = "testapp.apps.WagtailmediaTestsAppConfig"
9 |
--------------------------------------------------------------------------------
/tests/testapp/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class WagtailmediaTestsAppConfig(AppConfig):
5 | name = "testapp"
6 | label = "wagtailmedia_tests"
7 | verbose_name = "Wagtail media tests"
8 | default_auto_field = "django.db.models.AutoField"
9 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templatetags/media_tags.py:
--------------------------------------------------------------------------------
1 | from django.template import Library
2 | from wagtail.utils.version import get_main_version
3 |
4 |
5 | register = Library()
6 |
7 |
8 | @register.simple_tag
9 | def wagtail_version_gte(version: str) -> bool:
10 | return get_main_version() >= version
11 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templates/wagtailmedia/media/_file_field_legacy.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailadmin/shared/field.html" %}
2 | {% load i18n %}
3 | {% block form_field %}
4 | {{ media.filename }}
5 | {% trans "Change media file:" %}
6 | {{ field }}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/src/wagtailmedia/permissions.py:
--------------------------------------------------------------------------------
1 | from wagtail.permission_policies.collections import CollectionOwnershipPermissionPolicy
2 |
3 | from wagtailmedia.models import Media, get_media_model
4 |
5 |
6 | permission_policy = CollectionOwnershipPermissionPolicy(
7 | get_media_model(), auth_model=Media, owner_field_name="uploaded_by_user"
8 | )
9 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templates/wagtailmedia/media/_file_field.html:
--------------------------------------------------------------------------------
1 | {% load i18n wagtailadmin_tags %}
2 | {% rawformattedfield field=field %}
3 | {% icon name="media" classname="middle" %} {{ media.filename }}
4 | {% trans "Change media file:" %}
5 | {{ field }}
6 | {% endrawformattedfield %}
7 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templates/wagtailmedia/permissions/includes/media_permissions_formset.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailadmin/permissions/includes/collection_member_permissions_formset.html" %}
2 | {% load i18n %}
3 |
4 | {% block icon %}media{% endblock %}
5 | {% block title %}{% trans "Media permissions" %}{% endblock %}
6 | {% block add_button_label %}{% trans "Add a media permission" %}{% endblock %}
7 |
--------------------------------------------------------------------------------
/tests/testapp/forms.py:
--------------------------------------------------------------------------------
1 | from django.forms.widgets import Widget
2 |
3 | from wagtailmedia.forms import BaseMediaForm
4 |
5 |
6 | class OverridenWidget(Widget):
7 | pass
8 |
9 |
10 | class AlternateMediaForm(BaseMediaForm):
11 | class Meta:
12 | widgets = {
13 | "tags": OverridenWidget,
14 | "file": OverridenWidget,
15 | "thumbnail": OverridenWidget,
16 | }
17 |
--------------------------------------------------------------------------------
/src/wagtailmedia/templates/wagtailmedia/media/_thumbnail_field_legacy.html:
--------------------------------------------------------------------------------
1 | {% extends "wagtailadmin/shared/field.html" %}
2 | {% load i18n %}
3 | {% block form_field %}
4 |
From to , in {{ page.location }}.
15 | {{ page.body|richtext }} 16 |{% trans "Are you sure you want to delete this media file?" %}
14 | 20 |{% blocktrans %}Sorry, no media files match "{{ query_string }}"{% endblocktrans %}
21 | {% else %} 22 | {% if media_type %} 23 | {% url 'wagtailmedia:add' media_type as wagtailmedia_add_url %} 24 | {% else %} 25 | {% url 'wagtailmedia:add' 'media' as wagtailmedia_add_url %} 26 | {% endif %} 27 | {% if current_collection %} 28 |{% blocktrans %}You haven't uploaded any media in this collection. Why not upload one now?{% endblocktrans %}
29 | {% else %} 30 |{% blocktrans %}You haven't uploaded any media. Why not upload one now?{% endblocktrans %}
31 | {% endif %} 32 | {% endif %} 33 | {% endif %} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | .vscode 27 | .DS_Store 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | coverage_html_report 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask instance folder 56 | instance/ 57 | 58 | # Scrapy stuff: 59 | .scrapy 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # IPython Notebook 68 | .ipynb_checkpoints 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # celery beat schedule file 74 | celerybeat-schedule 75 | 76 | # dotenv 77 | .env 78 | 79 | # virtualenv 80 | venv/ 81 | ENV/ 82 | .venv 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # SQLite database 91 | *.sqlite3 92 | .ruff_cache 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Torchbox Ltd and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Torchbox nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /SPECIFICATION.md: -------------------------------------------------------------------------------- 1 | # Wagtail audio / video module 2 | 3 | 4 | A module for Wagtail that provides functionality similar to `wagtail.wagtaildocs` module, 5 | but for audio and video files. 6 | 7 | 8 | ## Models 9 | 10 | ### Essential 11 | 12 | * Extend the existing `Document` model or introduce new model for audio/video files (like `Image`). 13 | * The model should contain at least: duration (for audio and video), dimensions (for video), thumbnail (for video). 14 | 15 | ### Optional 16 | 17 | * Get duration of audio files automatically. 18 | * Get duration and dimensions of video files automatically. 19 | * Generate thumbnail automatically. 20 | 21 | ### Out of scope 22 | 23 | * Uploaded videos will not be converted / compressed / resized 24 | 25 | ## Admin 26 | 27 | ### Essential 28 | 29 | * Allow users to manage audio / video. 30 | * Allow users to upload custom thumbnail to videos. 31 | * Provide custom `StreamField` blocks for media items. 32 | 33 | ### Optional 34 | 35 | * Preview video / audio in an embedded player. 36 | * Allow users to insert audio / video files within the rich text editor. 37 | * Support oEmbed. 38 | * Note this removes the previous requirement, since oEmbed is already supported by the rich text editor. 39 | * Requires us to provide audio and video players within the app (because we need to generate the HTML code that initializes the player). 40 | * Template tags, providing shortcuts for template designers. This feature also requires a player. 41 | 42 | ## Tests 43 | 44 | Comprehensive unit test coverage. 45 | 46 | ## Documentation 47 | 48 | A detailed README in the Github project, for site implementers and module developers. 49 | -------------------------------------------------------------------------------- /src/wagtailmedia/templates/wagtailmedia/media/results.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailadmin_tags %} 2 | {% if media_files %} 3 | {% if is_searching %} 4 |{% blocktrans %}You haven't uploaded any media files in this collection. You can upload audio or video files.{% endblocktrans %}
28 | {% else %} 29 |{% blocktrans %}You haven't uploaded any media files. You can upload audio or video files.{% endblocktrans %}
30 | {% endif %} 31 | {% endif %} 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /src/wagtailmedia/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from django.forms.utils import flatatt 6 | from django.utils.html import format_html, format_html_join 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | 10 | try: 11 | from wagtail.admin.paginator import WagtailPaginator as Paginator 12 | except ImportError: 13 | from django.core.paginator import Paginator 14 | 15 | 16 | if TYPE_CHECKING: 17 | from django.core.paginator import Page as PaginatorPage 18 | from django.http import HttpRequest 19 | 20 | from .models import AbstractMedia 21 | 22 | DEFAULT_PAGE_KEY: str = "p" 23 | 24 | 25 | def paginate( 26 | request: HttpRequest, items, page_key: str = DEFAULT_PAGE_KEY, per_page: int = 20 27 | ) -> tuple[Paginator, PaginatorPage]: 28 | paginator = Paginator(items, per_page) 29 | page = paginator.get_page(request.GET.get(page_key)) 30 | return paginator, page 31 | 32 | 33 | def format_audio_html(item: AbstractMedia) -> str: 34 | return format_html( 35 | "", 36 | sources=format_html_join( 37 | "\n", "Chestnuts roasting on an open fire
", 58 | "cost": "Free" 59 | } 60 | }, 61 | { 62 | "pk": 1, 63 | "model": "wagtailmedia.Media", 64 | "fields": { 65 | "title": "test media", 66 | "created_at": "2014-01-01T12:00:00.000Z", 67 | "type": "video", 68 | "file": "media/music.mp3", 69 | "duration": "100" 70 | } 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /.github/workflows/nightly-tests.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Wagtail test 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1" 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | nightly-test: 14 | # Cannot check the existence of secrets, so limiting to repository name to prevent all forks to run nightly. 15 | # See: https://github.com/actions/runner/issues/520 16 | if: ${{ github.repository == 'torchbox/wagtailmedia' }} 17 | runs-on: ubuntu-latest 18 | 19 | services: 20 | postgres: 21 | image: postgres:15 22 | env: 23 | POSTGRES_PASSWORD: postgres 24 | ports: 25 | - 5432:5432 26 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | persist-credentials: false 32 | - name: Set up Python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: "3.13" 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install "psycopg2>=2.9" 40 | pip install "git+https://github.com/wagtail/wagtail.git@main#egg=wagtail" 41 | pip install -e .[testing] 42 | - name: Test 43 | id: test 44 | continue-on-error: true 45 | run: | 46 | cd tests 47 | python manage.py test 48 | env: 49 | DATABASE_ENGINE: django.db.backends.postgresql 50 | DATABASE_HOST: localhost 51 | DATABASE_USER: postgres 52 | DATABASE_PASS: postgres 53 | 54 | - name: Send Slack notification on failure 55 | if: steps.test.outcome == 'failure' 56 | run: | 57 | python .github/report_nightly_build_failure.py 58 | env: 59 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "wagtailmedia" 3 | description = "A Wagtail module for audio and video files." 4 | authors = [{name = "Mikalai Radchuk ", email = "hello@torchbox.com"}] 5 | maintainers = [{name = "Dan Braghis", email="dan.braghis@torchbox.com"}] 6 | readme = "README.md" 7 | license = "BSD-3-Clause" 8 | license-files = [ "LICENSE" ] 9 | keywords = ["Wagtail", "Django", "media"] 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Environment :: Web Environment", 13 | "Intended Audience :: Developers", 14 | "Operating System :: OS Independent", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Programming Language :: Python :: 3.13", 22 | "Framework :: Wagtail", 23 | "Framework :: Wagtail :: 6", 24 | "Framework :: Wagtail :: 7", 25 | ] 26 | 27 | dynamic = ["version"] 28 | requires-python = ">=3.9" 29 | dependencies = [ 30 | "Wagtail>=6.3", 31 | "Django>=4.2", 32 | ] 33 | 34 | [project.optional-dependencies] 35 | testing = [ 36 | "coverage>=7.10.0", 37 | "tox>=4.28.4", 38 | ] 39 | linting = [ 40 | "pre-commit>=4.3.0", 41 | ] 42 | 43 | [project.urls] 44 | Repository = "https://github.com/torchbox/wagtailmedia" 45 | Changelog = "https://github.com/torchbox/wagtailmedia/blob/main/CHANGELOG.md" 46 | Issues = "https://github.com/torchbox/wagtailmedia/issues" 47 | 48 | [build-system] 49 | requires = ["flit_core >=3.11,<4"] 50 | build-backend = "flit_core.buildapi" 51 | 52 | [tool.flit.module] 53 | name = "wagtailmedia" 54 | 55 | [tool.flit.sdist] 56 | exclude = [ 57 | "tests", 58 | "Makefile", 59 | "docs", 60 | ".*", 61 | "*.json", 62 | "*.ini", 63 | "*.yml", 64 | "CHANGELOG.md", 65 | "SPECIFICATION.md", 66 | "ruff.toml", 67 | ] 68 | -------------------------------------------------------------------------------- /src/wagtailmedia/migrations/0003_copy_media_permissions_to_collections.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def get_media_permissions(apps): 5 | # return a queryset of the 'add_media' and 'change_media' permissions 6 | Permission = apps.get_model("auth.Permission") 7 | ContentType = apps.get_model("contenttypes.ContentType") 8 | 9 | media_content_type, _created = ContentType.objects.get_or_create( 10 | model="media", 11 | app_label="wagtailmedia", 12 | ) 13 | return Permission.objects.filter( 14 | content_type=media_content_type, codename__in=["add_media", "change_media"] 15 | ) 16 | 17 | 18 | def copy_media_permissions_to_collections(apps, schema_editor): 19 | Collection = apps.get_model("wagtailcore.Collection") 20 | Group = apps.get_model("auth.Group") 21 | GroupCollectionPermission = apps.get_model("wagtailcore.GroupCollectionPermission") 22 | 23 | root_collection = Collection.objects.get(depth=1) 24 | 25 | for permission in get_media_permissions(apps): 26 | for group in Group.objects.filter(permissions=permission): 27 | GroupCollectionPermission.objects.create( 28 | group=group, collection=root_collection, permission=permission 29 | ) 30 | 31 | 32 | def remove_media_permissions_from_collections(apps, schema_editor): 33 | GroupCollectionPermission = apps.get_model("wagtailcore.GroupCollectionPermission") 34 | media_permissions = get_media_permissions(apps) 35 | 36 | GroupCollectionPermission.objects.filter(permission__in=media_permissions).delete() 37 | 38 | 39 | class Migration(migrations.Migration): 40 | dependencies = [ 41 | ("wagtailmedia", "0002_initial_data"), 42 | ("wagtailcore", "0026_group_collection_permission"), 43 | ] 44 | 45 | operations = [ 46 | migrations.RunPython( 47 | copy_media_permissions_to_collections, 48 | remove_media_permissions_from_collections, 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import shutil 6 | import sys 7 | import warnings 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | 12 | os.environ["DJANGO_SETTINGS_MODULE"] = "testapp.settings" 13 | sys.path.append("tests") 14 | 15 | 16 | def make_parser(): 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument( 19 | "--deprecation", 20 | choices=["all", "pending", "imminent", "none"], 21 | default="imminent", 22 | ) 23 | return parser 24 | 25 | 26 | def parse_args(args=None): 27 | return make_parser().parse_known_args(args) 28 | 29 | 30 | def runtests(): 31 | args, rest = parse_args() 32 | 33 | only_wagtail = r"^wagtail(\.|$)" 34 | if args.deprecation == "all": 35 | # Show all deprecation warnings from all packages 36 | warnings.simplefilter("default", DeprecationWarning) 37 | warnings.simplefilter("default", PendingDeprecationWarning) 38 | elif args.deprecation == "pending": 39 | # Show all deprecation warnings from wagtail 40 | warnings.filterwarnings( 41 | "default", category=DeprecationWarning, module=only_wagtail 42 | ) 43 | warnings.filterwarnings( 44 | "default", category=PendingDeprecationWarning, module=only_wagtail 45 | ) 46 | elif args.deprecation == "imminent": 47 | # Show only imminent deprecation warnings from wagtail 48 | warnings.filterwarnings( 49 | "default", category=DeprecationWarning, module=only_wagtail 50 | ) 51 | elif args.deprecation == "none": 52 | # Deprecation warnings are ignored by default 53 | pass 54 | 55 | argv = [sys.argv[0]] + rest 56 | 57 | try: 58 | execute_from_command_line(argv) 59 | finally: 60 | from wagtail.test.settings import MEDIA_ROOT, STATIC_ROOT 61 | 62 | shutil.rmtree(STATIC_ROOT, ignore_errors=True) 63 | shutil.rmtree(MEDIA_ROOT, ignore_errors=True) 64 | 65 | 66 | if __name__ == "__main__": 67 | runtests() 68 | -------------------------------------------------------------------------------- /tests/test_form_override.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.test import TestCase, override_settings 3 | from testapp.forms import AlternateMediaForm, OverridenWidget 4 | from wagtail.admin import widgets 5 | 6 | from wagtailmedia import models 7 | from wagtailmedia.forms import BaseMediaForm, get_media_base_form, get_media_form 8 | 9 | 10 | class TestFormOverride(TestCase): 11 | def test_get_media_base_form(self): 12 | self.assertIs(get_media_base_form(), BaseMediaForm) 13 | 14 | def test_get_media_form(self): 15 | bases = get_media_form(models.Media).__bases__ 16 | self.assertIn(BaseMediaForm, bases) 17 | self.assertNotIn(AlternateMediaForm, bases) 18 | 19 | def test_get_media_form_widgets(self): 20 | Form = get_media_form(models.Media) 21 | form = Form() 22 | self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget) 23 | self.assertIsInstance(form.fields["file"].widget, forms.FileInput) 24 | self.assertIsInstance(form.fields["thumbnail"].widget, forms.ClearableFileInput) 25 | 26 | @override_settings( 27 | WAGTAILMEDIA={"MEDIA_FORM_BASE": "testapp.forms.AlternateMediaForm"} 28 | ) 29 | def test_overridden_base_form(self): 30 | self.assertIs(get_media_base_form(), AlternateMediaForm) 31 | 32 | @override_settings( 33 | WAGTAILMEDIA={"MEDIA_FORM_BASE": "testapp.forms.AlternateMediaForm"} 34 | ) 35 | def test_get_overridden_media_form(self): 36 | bases = get_media_form(models.Media).__bases__ 37 | self.assertNotIn(BaseMediaForm, bases) 38 | self.assertIn(AlternateMediaForm, bases) 39 | 40 | @override_settings( 41 | WAGTAILMEDIA={"MEDIA_FORM_BASE": "testapp.forms.AlternateMediaForm"} 42 | ) 43 | def test_get_overridden_media_form_widgets(self): 44 | Form = get_media_form(models.Media) 45 | form = Form() 46 | self.assertIsInstance(form.fields["tags"].widget, OverridenWidget) 47 | self.assertIsInstance(form.fields["file"].widget, OverridenWidget) 48 | self.assertIsInstance(form.fields["thumbnail"].widget, OverridenWidget) 49 | -------------------------------------------------------------------------------- /src/wagtailmedia/templates/wagtailmedia/media/usage.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% blocktrans trimmed with title=media_item.title %}Usage of {{ title }}{% endblocktrans %}{% endblock %} 4 | {% block content %} 5 | {% trans "Usage of" as usage_str %} 6 | {% include "wagtailadmin/shared/header.html" with title=usage_str subtitle=media_item.title %} 7 | 8 || {% trans "Title" %} | 13 |{% trans "Type" %} | 14 |{% trans "Field" %} | 15 |
|---|---|---|
|
21 |
22 | {% if edit_url %}{% endif %}
23 | {{ label }}
24 | {% if edit_url %}{% endif %}
25 |
26 | |
27 | {{ verbose_name }} | 28 |
29 |
|
39 |
| 12 | {% if not is_searching %} 13 | {% if ordering == "-title" %} 14 | 15 | {% elif ordering == "title" %} 16 | 17 | {% else %}{# ordering by created at, so default to title, asc #} 18 | 19 | {% endif %} 20 | {% trans "Title" %} 21 | 22 | {% else %} 23 | {% trans "Title" %} 24 | {% endif %} 25 | | 26 |{% trans "File" %} | 27 |{% trans "Type" %} | 28 | {% if collections %} 29 |{% trans "Collection" %} | 30 | {% endif %} 31 |32 | {% if not is_searching %} 33 | {% if ordering == "-created_at" %} 34 | 35 | {% else %} 36 | 37 | {% endif %} 38 | {% trans "Uploaded" %} 39 | 40 | {% else %} 41 | {% trans "Uploaded" %} 42 | {% endif %} 43 | | 44 |
|---|---|---|---|---|
| 50 | {% if choosing %} 51 | 52 | {% else %} 53 | 54 | {% endif %} 55 | | 56 |57 | {% if choosing %} 58 | {{ media_file.filename }} 59 | {% else %} 60 | {{ media_file.filename }} 61 | {% endif %} 62 | | 63 |{{ media_file.get_type_display }} | 64 | {% if collections %} 65 |{{ media_file.collection.name }} | 66 | {% endif %} 67 |{% human_readable_date media_file.created_at %} | 68 |