From d880a3aecac4bbd3909f1067de2e8aca811dd7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=82=85=E7=9D=BF=E6=9B=BC?= Date: Thu, 15 Aug 2024 16:14:05 +0800 Subject: [PATCH] fix prefetch error when current model has a m2m relation to grouper field --- .../monkeypatch/extensions.py | 2 +- djangocms_versioning/plugin_rendering.py | 32 +++++++-- .../test_utils/polls/cms_plugins.py | 10 ++- .../test_utils/polls/models.py | 7 ++ .../polls/templates/polls/polls.html | 0 setup.py | 12 ++-- tests/requirements.txt | 3 +- tests/test_monkeypatch.py | 2 +- tests/test_plugin_rendering.py | 70 +++++++++++++++++++ tox.ini | 2 +- 10 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 djangocms_versioning/test_utils/polls/templates/polls/polls.html create mode 100644 tests/test_plugin_rendering.py diff --git a/djangocms_versioning/monkeypatch/extensions.py b/djangocms_versioning/monkeypatch/extensions.py index 9e5dd8c2..703cdc82 100644 --- a/djangocms_versioning/monkeypatch/extensions.py +++ b/djangocms_versioning/monkeypatch/extensions.py @@ -4,7 +4,7 @@ def _copy_title_extensions(self, source_page, target_page, language, clone=False): """ - djangocms-cms/extensions/admin.py, last changed in: divio/django-cms@2894ae8 + djangocms-cms/extensions/admin.py, last changed in: django-cms/django-cms@2894ae8 The existing method ExtensionPool._copy_title_extensions will only ever get published versions, we change the queries to get the latest draft version diff --git a/djangocms_versioning/plugin_rendering.py b/djangocms_versioning/plugin_rendering.py index db38ad42..0ca7c8f4 100644 --- a/djangocms_versioning/plugin_rendering.py +++ b/djangocms_versioning/plugin_rendering.py @@ -1,3 +1,5 @@ +from django.db import models + from cms.plugin_rendering import ContentRenderer, StructureRenderer from cms.utils.placeholder import rescan_placeholders_for_obj @@ -11,6 +13,7 @@ def prefetch_versioned_related_objects(instance, toolbar): candidate_fields = [ f for f in instance._meta.get_fields() if f.is_relation and not f.auto_created ] + for field in candidate_fields: try: versionable = versionables.for_grouper(field.remote_field.model) @@ -24,14 +27,35 @@ def prefetch_versioned_related_objects(instance, toolbar): qs = versionable.content_model.objects.all() related_field = getattr(instance, field.name) if related_field: - filters = {versionable.grouper_field_name: related_field} + is_related_manager = isinstance(related_field, models.Manager) + + if is_related_manager: + filter_key = '{}__in'.format(versionable.grouper_field_name) + filter_values = related_field.get_queryset() + filters = {filter_key: filter_values} + else: + filters = {versionable.grouper_field_name: related_field} # TODO Figure out grouping values-awareness # for extra fields other than hardcoded 'language' if "language" in versionable.extra_grouping_fields: filters["language"] = toolbar.request_language - qs = qs.filter(**filters) - prefetch_cache = {versionable.grouper_field.remote_field.name: qs} - related_field._prefetched_objects_cache = prefetch_cache + try: + qs = qs.filter(**filters).order_by(versionable.grouper_field_name).distinct(versionable.grouper_field_name) + except: + # FIXME: there will be error caused by djangocms-internalsearch + # Cannot use QuerySet for "content model": Use a QuerySet for "grouper model" + # will catch the error here to avoid page corruption. + pass + + # TODO refine it after understand prefetch in many2many field. + # because if `related_field` is ManyRelatedManager, it is temporary, + # we can't store `_prefetched_objectes_cache` to it, so I decide to store the + # prefetched value to model instance. + if is_related_manager: + instance._prefetched_objects_cache = getattr(instance, '_prefetched_objects_cache', {}) + instance._prefetched_objects_cache[field.name] = qs + else: + related_field._prefetched_objects_cache = {versionable.grouper_field.remote_field.name: qs} class VersionContentRenderer(ContentRenderer): diff --git a/djangocms_versioning/test_utils/polls/cms_plugins.py b/djangocms_versioning/test_utils/polls/cms_plugins.py index 4603205e..f2cf088a 100644 --- a/djangocms_versioning/test_utils/polls/cms_plugins.py +++ b/djangocms_versioning/test_utils/polls/cms_plugins.py @@ -1,7 +1,7 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool -from .models import PollPlugin as Poll +from .models import PollManyPlugin as PollMany, PollPlugin as Poll @plugin_pool.register_plugin @@ -10,3 +10,11 @@ class PollPlugin(CMSPluginBase): name = "Poll" allow_children = True render_template = "polls/poll.html" + + +@plugin_pool.register_plugin +class PollManyPlugin(CMSPluginBase): + model = PollMany + name = "PollMany" + allow_children = True + render_template = "polls/polls.html" diff --git a/djangocms_versioning/test_utils/polls/models.py b/djangocms_versioning/test_utils/polls/models.py index 0fbee970..892cf4c3 100644 --- a/djangocms_versioning/test_utils/polls/models.py +++ b/djangocms_versioning/test_utils/polls/models.py @@ -39,3 +39,10 @@ class PollPlugin(CMSPlugin): def __str__(self): return str(self.poll) + + +class PollManyPlugin(CMSPlugin): + polls = models.ManyToManyField(Poll, blank=True) + + def __str__(self): + return '' diff --git a/djangocms_versioning/test_utils/polls/templates/polls/polls.html b/djangocms_versioning/test_utils/polls/templates/polls/polls.html new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index 95f44b68..0fe75ad0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ INSTALL_REQUIREMENTS = [ "Django>=1.11,<3.0", - "django-cms", + "django-cms<4.1.0", "django-fsm>=2.6,<2.7" ] @@ -38,12 +38,12 @@ author="Divio AG", test_suite="test_settings.run", author_email="info@divio.ch", - url="http://github.com/divio/djangocms-versioning", + url="http://github.com/django-cms/djangocms-versioning", license="BSD", zip_safe=False, tests_require=TEST_REQUIREMENTS, - dependency_links=[ - "http://github.com/divio/django-cms/tarball/release/4.0.x#egg=django-cms-4.0.0", - "https://github.com/divio/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor-4.0.x", - ] + # dependency_links=[ + # "https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms", + # "https://github.com/django-cms/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor", + # ] ) diff --git a/tests/requirements.txt b/tests/requirements.txt index 447e8e4c..e7aa37fc 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -9,5 +9,6 @@ beautifulsoup4 django-classy-tags<2.0.0 django-sekizai<2.0.0 +https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms # Get the lastest cms v4 compatible djangocms-text-ckeditor -https://github.com/divio/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor +https://github.com/django-cms/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor diff --git a/tests/test_monkeypatch.py b/tests/test_monkeypatch.py index bee42c22..a9dd6381 100644 --- a/tests/test_monkeypatch.py +++ b/tests/test_monkeypatch.py @@ -52,7 +52,7 @@ def test_copy_extensions(self): ) # No asserts, this test originally failed because the versioned manager was called # in copy_extensions, now we call the original manager instead - # https://github.com/divio/djangocms-versioning/pull/201/files#diff-fc33dd7b5aa9b1645545cf48dfc9b4ecR19 + # https://github.com/django-cms/djangocms-versioning/pull/201/files#diff-fc33dd7b5aa9b1645545cf48dfc9b4ecR19 class MonkeypatchTestCase(CMSTestCase): diff --git a/tests/test_plugin_rendering.py b/tests/test_plugin_rendering.py new file mode 100644 index 00000000..35c24de1 --- /dev/null +++ b/tests/test_plugin_rendering.py @@ -0,0 +1,70 @@ +from cms.api import add_plugin +from cms.test_utils.testcases import CMSTestCase +from cms.toolbar.toolbar import CMSToolbar + +from djangocms_versioning.plugin_rendering import VersionContentRenderer +from djangocms_versioning.test_utils.factories import ( + PageVersionFactory, + PlaceholderFactory, + PollVersionFactory, +) + + +class MonkeypatchTestCase(CMSTestCase): + def test_prefetch_versioned_m2m_objects(self): + ''' + test model with manytomany field to grouper model + ''' + request = self.get_request('/') + toolbar = CMSToolbar(request) + toolbar.edit_mode_active = True + request.toolbar = toolbar + context = {"request": request} + contentRenderer = VersionContentRenderer(request) + + version = PageVersionFactory() + placeholder = PlaceholderFactory(source=version.content) + poll_version = PollVersionFactory(content__language='en') + poll = poll_version.content.poll + + plugin = add_plugin( + placeholder, "PollManyPlugin", version.content.language + ) + + plugin.polls.add(poll) + plugin._prefetched_objects_cache = {} + + with self.assertNumQueries(1): + contentRenderer.render_plugin(plugin, context) + for i in range(1000): + self.assertEqual(1, len(plugin.polls.all())) + + with self.assertNumQueries(0): + self.assertEqual(1, len(plugin.polls.all())) + + self.assertIsNotNone(plugin._prefetched_objects_cache) + self.assertIsNotNone(plugin._prefetched_objects_cache['polls']) + self.assertEqual(len(plugin._prefetched_objects_cache['polls']), 1) + + def test_prefetch_versioned_m2o_objects(self): + ''' + #test model with one foreign key to grouper model + ''' + request = self.get_request('/') + toolbar = CMSToolbar(request) + toolbar.edit_mode_active = True + request.toolbar = toolbar + context = {"request": request} + contentRenderer = VersionContentRenderer(request) + + version = PageVersionFactory() + placeholder = PlaceholderFactory(source=version.content) + poll_version = PollVersionFactory(content__language='en') + poll = poll_version.content.poll + + plugin = add_plugin( + placeholder, "PollPlugin", version.content.language, poll=poll + ) + + contentRenderer.render_plugin(plugin, context) + self.assertIsNotNone(plugin.poll._prefetched_objects_cache) \ No newline at end of file diff --git a/tox.ini b/tox.ini index fcb494a3..49ef3353 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = dj21: Django>=2.1,<2.2 dj22: Django>=2.2,<3.0 - cms40: https://github.com/divio/django-cms/archive/release/4.0.x.zip + cms40: https://github.com/django-cms/django-cms/archive/release/4.0.x.zip basepython = py35: python3.5