Repository: torchbox/wagtail-markdown (language: python)

Issue:
Use wagtail_hooks mechanism to inject editor JS/CSS
Markdown editor doesn't load at least on Wagtail 1.1. Apparently JS and CSS are injected now through a hooks mechanism.

This PR implements the `wagtail_hooks` mechanism.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 23e49c3..d86a6c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ env:
   PIP_DISABLE_PIP_VERSION_CHECK: "1"
   PIP_NO_PYTHON_VERSION_WARNING: "1"
   # Keep in sync with .pre-commit-config.yaml/default_language_version/python.
-  PYTHON_LATEST: "3.11"
+  PYTHON_LATEST: "3.12"
 
 jobs:
   tests:
@@ -28,7 +28,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - name: Harden Runner
@@ -41,9 +41,9 @@ jobs:
             github.com:443
             pypi.org:443
             api.github.com:443
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
@@ -60,9 +60,9 @@ jobs:
         run: tox --installpkg ./dist/*.whl
 
       - name: ⬆️ Upload coverage data
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: coverage-data
+          name: coverage-data-${{ matrix.python-version }}
           path: .coverage.*
           if-no-files-found: ignore
           retention-days: 1
@@ -82,8 +82,8 @@ jobs:
             github.com:443
             pypi.org:443
             api.github.com:443
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
           # Use latest Python, so it understands all syntax.
           python-version: ${{env.PYTHON_LATEST}}
@@ -91,9 +91,10 @@ jobs:
       - run: python -Im pip install --upgrade coverage[toml]
 
       - name: Download coverage data
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
-          name: coverage-data
+          pattern: coverage-data-*
+          merge-multiple: true
 
       - name: ＋  Combine coverage
         run: |
@@ -103,7 +104,7 @@ jobs:
           echo "## Coverage summary" >> $GITHUB_STEP_SUMMARY
           python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
       - name: 📈 Upload HTML report if check failed.
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: html-report
           path: htmlcov
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 036b06c..4251783 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,13 +22,13 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 75967e5..fb44e4c 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -11,13 +11,13 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: '3.11'
+          python-version: '3.12'
           cache: "pip"
           cache-dependency-path: "**/pyproject.toml"
 
@@ -29,7 +29,7 @@ jobs:
       - name: 🏗️ Build
         run: python -Im flit build
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           path: ./dist
 
@@ -44,7 +44,7 @@ jobs:
       # Mandatory for trusted publishing
       id-token: write
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
 
       - name: 🚀 Publish package distributions to PyPI
         uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8be14e2..599ac6c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ ci:
   autofix_prs: false
 
 default_language_version:
-  python: python3.11
+  python: python3.12
 
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 310d8e3..aa013f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Unreleased
 
+- Support for the Django 5.0
+- Support for the Wagtail 6.0
+- Dropped support for Wagtail < 5.2
+
 ## [0.11.1] - 2023-08-02
 
 ### Changed
diff --git a/README.md b/README.md
index b44ad4c..6a84196 100644
--- a/README.md
+++ b/README.md
@@ -348,11 +348,11 @@ tox -p
 To run tests for a specific environment:
 
 ```shell
-tox -e py311-django3.2-wagtail4.1
+tox -e py312-django5.0-wagtail6.0
 ```
 
 or, a specific test
 
 ```shell
-tox -e py311-django3.2-wagtail4.1 -- tests.testapp.tests.test_admin.TestFieldsAdmin
+tox -e py312-django5.0-wagtail6.0 -- tests.testapp.tests.test_admin.TestFieldsAdmin
 ```
diff --git a/pyproject.toml b/pyproject.toml
index cdd8967..d3c0e0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,15 +18,20 @@ classifiers = [
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Framework :: Django",
+    "Framework :: Django :: 3.2",
+    "Framework :: Django :: 4.2",
+    "Framework :: Django :: 5.0",
     "Framework :: Wagtail",
-    "Framework :: Wagtail :: 4",
     "Framework :: Wagtail :: 5",
+    "Framework :: Wagtail :: 6",
 ]
 
 dynamic = ["version"]
 requires-python = ">=3.8"
 dependencies = [
-    "Wagtail>=4.1",
+    "Wagtail>=5.2",
     "Markdown>=3.3,<4",
     # note: bleach5 requires bleach[css]. Will make one of the next versions require >= 5
     "bleach>=3.3,<5",
diff --git a/src/wagtailmarkdown/static/wagtailmarkdown/js/easymde-controller.js b/src/wagtailmarkdown/static/wagtailmarkdown/js/easymde-controller.js
new file mode 100644
index 0000000..fd42fd7
--- /dev/null
+++ b/src/wagtailmarkdown/static/wagtailmarkdown/js/easymde-controller.js
@@ -0,0 +1,17 @@
+// wagtailmarkdown/static/wagtailmarkdown/js/easymde-controller.js
+
+class EasyMDEContainer extends window.StimulusModule.Controller {
+    static values = { autodownload: String };
+
+    connect() {
+        let autodownload = this.autodownloadValue || null;
+
+        if (autodownload) {
+            easymdeAttach(this.element.id, autodownload);
+        } else {
+            easymdeAttach(this.element.id);
+        }
+    }
+}
+
+window.wagtail.app.register('easymde', EasyMDEContainer);
diff --git a/src/wagtailmarkdown/widgets.py b/src/wagtailmarkdown/widgets.py
index 91b766b..f6ac79b 100644
--- a/src/wagtailmarkdown/widgets.py
+++ b/src/wagtailmarkdown/widgets.py
@@ -2,41 +2,79 @@
 from django.conf import settings
 from wagtail import VERSION as WAGTAIL_VERSION
 from wagtail.telepath import register
-from wagtail.utils.widgets import WidgetWithScript
 from wagtail.widget_adapters import WidgetAdapter
 
 
-class MarkdownTextarea(WidgetWithScript, forms.widgets.Textarea):
-    def render_js_init(self, id_, name, value):
-        if (
-            hasattr(settings, "WAGTAILMARKDOWN")
-            and "autodownload_fontawesome" in settings.WAGTAILMARKDOWN
-        ):
-            autodownload = (
-                "true"
-                if settings.WAGTAILMARKDOWN["autodownload_fontawesome"]
-                else "false"
+if WAGTAIL_VERSION >= (6, 0):
+
+    class MarkdownTextarea(forms.widgets.Textarea):  # type: ignore PGH003
+        def build_attrs(self, *args, **kwargs):
+            attrs = super().build_attrs(*args, **kwargs)
+            attrs["data-controller"] = "easymde"
+
+            if (
+                hasattr(settings, "WAGTAILMARKDOWN")
+                and "autodownload_fontawesome" in settings.WAGTAILMARKDOWN
+            ):
+                autodownload = (
+                    "true"
+                    if settings.WAGTAILMARKDOWN["autodownload_fontawesome"]
+                    else "false"
+                )
+                attrs["data-easymde-autodownload-value"] = autodownload
+
+            return attrs
+
+        @property
+        def media(self):
+            css = (
+                "wagtailmarkdown/css/easymde.min.css",
+                "wagtailmarkdown/css/easymde.tweaks.css",
+                "wagtailmarkdown/css/easymde.darkmode.css",
+            )
+
+            return forms.Media(
+                css={"all": css},
+                js=(
+                    "wagtailmarkdown/js/easymde.min.js",
+                    "wagtailmarkdown/js/easymde.attach.js",
+                    "wagtailmarkdown/js/easymde-controller.js",
+                ),
+            )
+
+else:
+    from wagtail.utils.widgets import WidgetWithScript
+
+    class MarkdownTextarea(WidgetWithScript, forms.widgets.Textarea):
+        def render_js_init(self, id_, name, value):
+            if (
+                hasattr(settings, "WAGTAILMARKDOWN")
+                and "autodownload_fontawesome" in settings.WAGTAILMARKDOWN
+            ):
+                autodownload = (
+                    "true"
+                    if settings.WAGTAILMARKDOWN["autodownload_fontawesome"]
+                    else "false"
+                )
+                return f'easymdeAttach("{id_}", {autodownload});'
+
+            return f'easymdeAttach("{id_}");'
+
+        @property
+        def media(self):
+            css = (
+                "wagtailmarkdown/css/easymde.min.css",
+                "wagtailmarkdown/css/easymde.tweaks.css",
+                "wagtailmarkdown/css/easymde.darkmode.css",
+            )
+
+            return forms.Media(
+                css={"all": css},
+                js=(
+                    "wagtailmarkdown/js/easymde.min.js",
+                    "wagtailmarkdown/js/easymde.attach.js",
+                ),
             )
-            return f'easymdeAttach("{id_}", {autodownload});'
-
-        return f'easymdeAttach("{id_}");'
-
-    @property
-    def media(self):
-        css = (
-            "wagtailmarkdown/css/easymde.min.css",
-            "wagtailmarkdown/css/easymde.tweaks.css",
-        )
-        if WAGTAIL_VERSION >= (5, 0):
-            css += ("wagtailmarkdown/css/easymde.darkmode.css",)
-
-        return forms.Media(
-            css={"all": css},
-            js=(
-                "wagtailmarkdown/js/easymde.min.js",
-                "wagtailmarkdown/js/easymde.attach.js",
-            ),
-        )
 
 
 class MarkdownTextareaAdapter(WidgetAdapter):
diff --git a/tox.ini b/tox.ini
index d5feba2..b1ce03a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,8 +2,9 @@
 min_version = 4.0
 
 envlist =
-    py{38,39,310,311}-django{3.2,4.1}-wagtail{4.1,5.0,5.1,main}
-    py{39,310,311}-django{4.2}-wagtail{5.0,5.1,main}
+    py{38,39,310}-django3.2-wagtail5.2
+    py{38,39,310,311}-django4.2-wagtail{5.2,6.0}
+    py{311,312}-django5.0-wagtail{5.2,6.0,main}
 
 [gh-actions]
 python =
@@ -11,6 +12,7 @@ python =
     3.9: py39
     3.10: py310
     3.11: py311
+    3.12: py312
 
 [testenv]
 package = wheel
@@ -29,11 +31,11 @@ deps =
     flit>=3.8
     coverage[toml]>=7.0,<8.0
     django3.2: Django~=3.2
-    django4.1: Django~=4.1
     django4.2: Django~=4.2
+    django5.0: Django~=5.0
     wagtail4.1: wagtail>=4.1,<4.2
-    wagtail5.0: wagtail>=5.0,<5.1
-    wagtail5.1: wagtail>=5.1,<5.2
+    wagtail5.2: wagtail>=5.2,<5.3
+    wagtail6.0: wagtail>=6.0,<6.1
 
 install_command = python -m pip install -U {opts} {packages}
 
@@ -47,7 +49,7 @@ commands =
 
 [testenv:interactive]
 description = An interactive environment for local testing purposes
-base_python = python3.11
+base_python = python3.12
 
 ; Note: the following are commented out for development convenience,
 ;       so as to test the interactive mode with a different Wagtail version


Test patch:
diff --git a/.github/workflows/nightly-tests.yaml b/.github/workflows/nightly-tests.yaml
index 1873e33..e899233 100644
--- a/.github/workflows/nightly-tests.yaml
+++ b/.github/workflows/nightly-tests.yaml
@@ -15,11 +15,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v3
-      - name: Set up Python 3.11
-        uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5
         with:
-          python-version: "3.11"
+          python-version: "3.12"
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
diff --git a/tests/manage.py b/tests/manage.py
index 3950e25..838b40a 100644
--- a/tests/manage.py
+++ b/tests/manage.py
@@ -4,7 +4,7 @@
 
 
 if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings")
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings")
 
     from django.core.management import execute_from_command_line
 
diff --git a/tests/testapp/templates/testapp/test_with_stream_field_page.html b/tests/testapp/templates/testapp/test_with_stream_field_page.html
new file mode 100644
index 0000000..0121d60
--- /dev/null
+++ b/tests/testapp/templates/testapp/test_with_stream_field_page.html
@@ -0,0 +1,4 @@
+{% load wagtailmarkdown %}
+<article>
+{{ self.body|markdown }}
+</article>
diff --git a/tests/testapp/tests/test_admin.py b/tests/testapp/tests/test_admin.py
index 636efb8..c7067e9 100644
--- a/tests/testapp/tests/test_admin.py
+++ b/tests/testapp/tests/test_admin.py
@@ -1,5 +1,6 @@
 from django.test import TestCase, override_settings
 from django.urls import reverse
+from wagtail import VERSION as WAGTAIL_VERSION
 from wagtail.models import Page
 from wagtail.test.utils import WagtailTestUtils
 
@@ -20,10 +21,16 @@ def test_markdown_field_in_admin(self):
                 args=("testapp", "testpage", self.root_page.id),
             )
         )
-
-        self.assertContains(response, '<script>easymdeAttach("id_body");</script>')
         self.assertContains(response, "easymde.attach.js")
 
+        if WAGTAIL_VERSION >= (6, 0):
+            self.assertContains(
+                response,
+                '<textarea name="body" cols="40" rows="10" id="id_body" data-controller="easymde">',
+            )
+        else:
+            self.assertContains(response, '<script>easymdeAttach("id_body");</script>')
+
     def test_markdown_block(self):
         block = MarkdownBlock()
         self.assertEqual(block.render_basic("# hello"), "<h1>hello</h1>")
@@ -36,16 +43,28 @@ def test_markdown_block_in_admin(self):
             )
         )
         self.assertContains(response, "easymde.attach.js")
-        self.assertContains(
-            response,
-            "[&quot;markdown&quot;, {&quot;_type&quot;: "
-            "&quot;wagtailmarkdown.widgets.MarkdownTextarea&quot;, "
-            "&quot;_args&quot;: [&quot;&lt;textarea name=\\&quot;__NAME__\\"
-            "&quot; cols=\\&quot;40\\&quot; "
-            "rows=\\&quot;1\\&quot; id=\\&quot;__ID__\\&quot;&gt;\\n&lt;/textarea&gt;"
-            "&lt;script&gt;easymdeAttach(\\&quot;__ID__\\&quot;);"
-            "&lt;/script&gt;&quot;, &quot;__ID__&quot;]}",
-        )
+        if WAGTAIL_VERSION >= (6, 0):
+            self.assertContains(
+                response,
+                "[&quot;markdown&quot;, {&quot;_type&quot;: "
+                "&quot;wagtailmarkdown.widgets.MarkdownTextarea&quot;, "
+                "&quot;_args&quot;: [&quot;&lt;textarea name=\\&quot;__NAME__\\"
+                "&quot; cols=\\&quot;40\\&quot; "
+                "rows=\\&quot;1\\&quot; id=\\&quot;__ID__\\&quot; "
+                "data-controller=\\&quot;easymde\\&quot;&gt;\\n&lt;/textarea&gt;&quot;, "
+                "&quot;__ID__&quot;]}",
+            )
+        else:
+            self.assertContains(
+                response,
+                "[&quot;markdown&quot;, {&quot;_type&quot;: "
+                "&quot;wagtailmarkdown.widgets.MarkdownTextarea&quot;, "
+                "&quot;_args&quot;: [&quot;&lt;textarea name=\\&quot;__NAME__\\"
+                "&quot; cols=\\&quot;40\\&quot; "
+                "rows=\\&quot;1\\&quot; id=\\&quot;__ID__\\&quot;&gt;\\n&lt;/textarea&gt;"
+                "&lt;script&gt;easymdeAttach(\\&quot;__ID__\\&quot;);"
+                "&lt;/script&gt;&quot;, &quot;__ID__&quot;]}",
+            )
         self.assertContains(response, "markdown-textarea-adapter.js")
 
     def test_markdown_field(self):
@@ -54,10 +73,26 @@ def test_markdown_field(self):
 
     def test_widget(self):
         widget = MarkdownTextarea()
-        init = widget.render_js_init("the_id", "name", "value")
 
-        self.assertEqual(init, 'easymdeAttach("the_id");')
+        if WAGTAIL_VERSION >= (6, 0):
+            init = widget.render("name", "value", attrs={"id": "the_id"})
+
+            self.assertEqual(
+                init,
+                '<textarea name="name" cols="40" rows="10" id="the_id" data-controller="easymde">\nvalue</textarea>',
+            )
 
-        with override_settings(WAGTAILMARKDOWN={"autodownload_fontawesome": False}):
+            with override_settings(WAGTAILMARKDOWN={"autodownload_fontawesome": False}):
+                init = widget.render("name", "value", attrs={"id": "the_id"})
+                self.assertEqual(
+                    init,
+                    '<textarea name="name" cols="40" rows="10" id="the_id" data-controller="easymde" data-easymde-autodownload-value="false">\nvalue</textarea>',
+                )
+        else:
             init = widget.render_js_init("the_id", "name", "value")
-            self.assertEqual(init, 'easymdeAttach("the_id", false);')
+
+            self.assertEqual(init, 'easymdeAttach("the_id");')
+
+            with override_settings(WAGTAILMARKDOWN={"autodownload_fontawesome": False}):
+                init = widget.render_js_init("the_id", "name", "value")
+                self.assertEqual(init, 'easymdeAttach("the_id", false);')