diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fdf7bb..961d47b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,6 +80,26 @@ jobs: name: flake8 command: venv/bin/tox -e flake8 + test_py312: + working_directory: ~/wagtail-autocomplete-repo + docker: + - image: cimg/python:3.12 + steps: + - checkout + - run: + name: install dependencies + command: | + python -m venv venv + . venv/bin/activate + pip install -U pip + pip install ".[test]" + - run: + name: tests + command: venv/bin/tox -f py312 + - run: + name: flake8 + command: venv/bin/tox -e flake8 + workflows: version: 2 build: @@ -88,3 +108,4 @@ workflows: - test_py39 - test_py310 - test_py311 + - test_py312 diff --git a/docs/changelog.rst b/docs/changelog.rst index 4ca7db1..a4eb937 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,10 +5,9 @@ Changelog Unreleased ---------- -* Remove tests for Wagtail 4.2 and 5.0 as they have reached their EOL -* Added Wagtail 5.x compatibility -* Added tests for Python 3.10 and 3.11 -* Remove support for versions of Wagtail < 4.1 (Wagtail 4.1 or later now required) +* Added Wagtail 6.0 compatibility +* Added tests for Django 5.0 +* Added tests for Python 3.12 0.11 Release ------------ @@ -16,6 +15,10 @@ Unreleased * Add handling of validation errors during creation of objects. * Fix bug where searches failed if Django's CSRF cookie was configured with ``CSRF_COOKIE_HTTPONLY`` set to ``True`` * Update Javascript dependencies to remove security vulnerabilities. +* Remove tests for Wagtail 4.2 and 5.0 as they have reached their EOL +* Added Wagtail 5.x compatibility +* Added tests for Python 3.10 and 3.11 +* Remove support for versions of Wagtail < 4.1 (Wagtail 4.1 or later now required) 0.10 Release ------------ diff --git a/setup.py b/setup.py index d195766..a48287d 100644 --- a/setup.py +++ b/setup.py @@ -54,15 +54,18 @@ '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', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4', - 'Framework :: Django :: 4.1', 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5', + 'Framework :: Django :: 5.0', 'Framework :: Wagtail', 'Framework :: Wagtail :: 4', 'Framework :: Wagtail :: 5', + 'Framework :: Wagtail :: 6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], diff --git a/tox.ini b/tox.ini index 46af05b..3a21c46 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,10 @@ skipsdist = True usedevelop = True envlist = - py{38,39,310}-dj{32,41}-wt{41,51,52} - py311-dj41-wt{41,51,52} - py311-dj42-wt{51,52} + py{38,39,310,311}-dj32-wt{41,52} + py312-dj32-wt52 + py{38,39,310,311,312}-dj42-wt{52,60} + py{310,311,312}-dj50-wt{52,60} [testenv] install_command = pip install -e ".[test]" -U {opts} {packages} @@ -14,15 +15,14 @@ basepython = py39: python3.9 py310: python3.10 py311: python3.11 + py312: python3.12 deps = dj32: django>=3.2,<4.0 - dj41: django>=4.1,<4.2 dj42: django>=4.2,<4.3 + dj50: django>=5.0,<5.1 wt41: wagtail>=4.1,<4.2 - wt42: wagtail>=4.2,<5.0 - wt50: wagtail>=5.0,<5.1 - wt51: wagtail>=5.1,<5.2 wt52: wagtail>=5.2,<5.3 + wt60: wagtail>=6.0,<6.1 [testenv:flake8] basepython = @@ -30,5 +30,6 @@ basepython = py39: python3.9 py310: python3.10 py311: python3.11 + py312: python3.12 deps = flake8>3.7 commands = flake8 wagtailautocomplete diff --git a/wagtailautocomplete/static/wagtailautocomplete/autocomplete-widget-controller.js b/wagtailautocomplete/static/wagtailautocomplete/autocomplete-widget-controller.js new file mode 100644 index 0000000..105c70e --- /dev/null +++ b/wagtailautocomplete/static/wagtailautocomplete/autocomplete-widget-controller.js @@ -0,0 +1,7 @@ +class AutoCompleteWidgetController extends window.StimulusModule.Controller { + connect() { + initAutoCompleteWidget(this.element.id); + } +} + +window.wagtail.app.register('autocomplete-widget', AutoCompleteWidgetController); diff --git a/wagtailautocomplete/tests/test_edit_handlers.py b/wagtailautocomplete/tests/test_edit_handlers.py index 4b156cd..f5498df 100644 --- a/wagtailautocomplete/tests/test_edit_handlers.py +++ b/wagtailautocomplete/tests/test_edit_handlers.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.admin.panels import ObjectList from wagtailautocomplete.edit_handlers import AutocompletePanel @@ -50,7 +51,11 @@ def test_form_field_media(self): def test_render_js_init(self): result = self.autocomplete_panel.render_html() - self.assertIn('initAutoCompleteWidget("id_owner");', result) + + if WAGTAIL_VERSION >= (6, 0): # type: ignore + self.assertIn('data-autocomplete-input-id="id_owner"', result) + else: + self.assertIn('initAutoCompleteWidget("id_owner");', result) def test_render_as_field(self): result = self.autocomplete_panel.render_html() diff --git a/wagtailautocomplete/widgets.py b/wagtailautocomplete/widgets.py index a050884..ae9c383 100644 --- a/wagtailautocomplete/widgets.py +++ b/wagtailautocomplete/widgets.py @@ -1,64 +1,127 @@ import json from django import forms +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.admin.staticfiles import versioned_static -from wagtail.utils.widgets import WidgetWithScript from .views import render_page +if WAGTAIL_VERSION >= (6, 0): # type: ignore -class Autocomplete(WidgetWithScript): - template_name = 'wagtailautocomplete/autocomplete.html' - - def __init__(self, target_model, can_create=False, is_single=True, attrs=None): - super().__init__(attrs) - - self.target_model = target_model - self.can_create = can_create - self.is_single = is_single - - def get_context(self, name, value, attrs): - context = super().get_context(name, value, attrs) - context['widget']['target_model'] = self.target_model._meta.label - context['widget']['can_create'] = self.can_create - context['widget']['is_single'] = self.is_single - return context - - def format_value(self, value): - if not value: - return 'null' - if isinstance(value, list): - return json.dumps([ - render_page(page) - for page in self.target_model.objects.filter(pk__in=value) - ]) - else: - return json.dumps(render_page(self.target_model.objects.get(pk=value))) - - def value_from_datadict(self, data, files, name): - # treat empty value as None to prevent deserialization error - original_value = super().value_from_datadict(data, files, name) - if not original_value: + from django.forms.widgets import Widget + + + class Autocomplete(Widget): # type: ignore + template_name = 'wagtailautocomplete/autocomplete.html' + + def __init__(self, target_model, can_create=False, is_single=True, attrs=None): + super().__init__(attrs) + + self.target_model = target_model + self.can_create = can_create + self.is_single = is_single + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context['widget']['target_model'] = self.target_model._meta.label + context['widget']['can_create'] = self.can_create + context['widget']['is_single'] = self.is_single + return context + + def format_value(self, value): + if not value: + return 'null' + if isinstance(value, list): + return json.dumps([ + render_page(page) + for page in self.target_model.objects.filter(pk__in=value) + ]) + else: + return json.dumps(render_page(self.target_model.objects.get(pk=value))) + + def value_from_datadict(self, data, files, name): + # treat empty value as None to prevent deserialization error + original_value = super().value_from_datadict(data, files, name) + if not original_value: + return None + + value = json.loads(original_value) + + if isinstance(value, list): + return [obj['pk'] for obj in value if 'pk' in obj] + if isinstance(value, dict): + return value.get('pk', None) + return None + + def build_attrs(self, *args, **kwargs): + attrs = super().build_attrs(*args, **kwargs) + attrs["data-controller"] = "autocomplete-widget" + return attrs + + @property + def media(self): + return forms.Media( + css={ + 'all': [versioned_static('wagtailautocomplete/dist.css')], + }, + js=[versioned_static('wagtailautocomplete/dist.js'), versioned_static('wagtailautocomplete/autocomplete-widget-controller.js')], + ) +else: + from wagtail.utils.widgets import WidgetWithScript + + + class Autocomplete(WidgetWithScript): + template_name = 'wagtailautocomplete/autocomplete.html' + + def __init__(self, target_model, can_create=False, is_single=True, attrs=None): + super().__init__(attrs) + + self.target_model = target_model + self.can_create = can_create + self.is_single = is_single + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context['widget']['target_model'] = self.target_model._meta.label + context['widget']['can_create'] = self.can_create + context['widget']['is_single'] = self.is_single + return context + + def format_value(self, value): + if not value: + return 'null' + if isinstance(value, list): + return json.dumps([ + render_page(page) + for page in self.target_model.objects.filter(pk__in=value) + ]) + else: + return json.dumps(render_page(self.target_model.objects.get(pk=value))) + + def value_from_datadict(self, data, files, name): + # treat empty value as None to prevent deserialization error + original_value = super().value_from_datadict(data, files, name) + if not original_value: + return None + + value = json.loads(original_value) + + if isinstance(value, list): + return [obj['pk'] for obj in value if 'pk' in obj] + if isinstance(value, dict): + return value.get('pk', None) return None - value = json.loads(original_value) - - if isinstance(value, list): - return [obj['pk'] for obj in value if 'pk' in obj] - if isinstance(value, dict): - return value.get('pk', None) - return None - - def render_js_init(self, id_, name, value): - return "initAutoCompleteWidget({id});".format( - id=json.dumps(id_), - ) - - @property - def media(self): - return forms.Media( - css={ - 'all': [versioned_static('wagtailautocomplete/dist.css')], - }, - js=[versioned_static('wagtailautocomplete/dist.js')], - ) + def render_js_init(self, id_, name, value): + return "initAutoCompleteWidget({id});".format( + id=json.dumps(id_), + ) + + @property + def media(self): + return forms.Media( + css={ + 'all': [versioned_static('wagtailautocomplete/dist.css')], + }, + js=[versioned_static('wagtailautocomplete/dist.js')], + )