diff --git a/docs/reference/pages/queryset_reference.rst b/docs/reference/pages/queryset_reference.rst index 8fba3738f7..5ec52acd6a 100644 --- a/docs/reference/pages/queryset_reference.rst +++ b/docs/reference/pages/queryset_reference.rst @@ -202,6 +202,24 @@ Reference .. automethod:: not_type + .. automethod:: exact_type + + Example: + + .. code-block:: python + + # Find all pages that are of the exact type EventPage + event_pages = Page.objects.exact_type(EventPage) + + .. automethod:: not_exact_type + + Example: + + .. code-block:: python + + # Find all pages that are not of the exact type EventPage (but may be a subclass) + non_event_pages = Page.objects.not_exact_type(EventPage) + .. automethod:: unpublish Example: diff --git a/wagtail/wagtailcore/management/commands/replace_text.py b/wagtail/wagtailcore/management/commands/replace_text.py index 8893f1aa1c..95837de649 100644 --- a/wagtail/wagtailcore/management/commands/replace_text.py +++ b/wagtail/wagtailcore/management/commands/replace_text.py @@ -32,7 +32,10 @@ class Command(BaseCommand): child_relation_names = [rel.get_accessor_name() for rel in get_all_child_relations(page_class)] - for page in page_class.objects.all(): + # Find all pages of this exact type; exclude subclasses, as they will + # appear in the get_page_models() list in their own right, and this + # ensures that replacement happens only once + for page in page_class.objects.exact_type(page_class): replace_in_model(page, from_text, to_text) for child_rel in child_relation_names: for child in getattr(page, child_rel).all(): diff --git a/wagtail/wagtailcore/query.py b/wagtail/wagtailcore/query.py index c3de7aa506..209ef8e3d3 100644 --- a/wagtail/wagtailcore/query.py +++ b/wagtail/wagtailcore/query.py @@ -191,6 +191,23 @@ class PageQuerySet(SearchableQuerySetMixin, TreeQuerySet): """ return self.exclude(self.type_q(model)) + def exact_type_q(self, klass): + return Q(content_type=ContentType.objects.get_for_model(klass)) + + def exact_type(self, model): + """ + This filters the QuerySet to only contain pages that are an instance of the specified model + (matching the model exactly, not subclasses). + """ + return self.filter(self.exact_type_q(model)) + + def not_exact_type(self, model): + """ + This filters the QuerySet to not contain any pages which are an instance of the specified model + (matching the model exactly, not subclasses). + """ + return self.exclude(self.exact_type_q(model)) + def public_q(self): from wagtail.wagtailcore.models import PageViewRestriction diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py index 473f284dd2..91901e9f76 100644 --- a/wagtail/wagtailcore/tests/test_page_queryset.py +++ b/wagtail/wagtailcore/tests/test_page_queryset.py @@ -294,6 +294,11 @@ class TestPageQuerySet(TestCase): event = Page.objects.get(url_path='/home/events/someone-elses-event/') self.assertTrue(pages.filter(id=event.id).exists()) + # Check that "Saint Patrick" (an instance of SingleEventPage, a subclass of EventPage) + # is in the results + event = Page.objects.get(url_path='/home/events/saint-patrick/') + self.assertTrue(pages.filter(id=event.id).exists()) + def test_type_includes_subclasses(self): from wagtail.wagtailforms.models import AbstractEmailForm pages = Page.objects.type(AbstractEmailForm) @@ -317,6 +322,38 @@ class TestPageQuerySet(TestCase): homepage = Page.objects.get(url_path='/home/') self.assertTrue(pages.filter(id=homepage.id).exists()) + def test_exact_type(self): + pages = Page.objects.exact_type(EventPage) + + # Check that all objects are EventPages (and not a subclass) + for page in pages: + self.assertEqual(type(page.specific), EventPage) + + # Check that "someone elses event" is in the results + event = Page.objects.get(url_path='/home/events/someone-elses-event/') + self.assertTrue(pages.filter(id=event.id).exists()) + + # Check that "Saint Patrick" (an instance of SingleEventPage, a subclass of EventPage) + # is NOT in the results + event = Page.objects.get(url_path='/home/events/saint-patrick/') + self.assertFalse(pages.filter(id=event.id).exists()) + + def test_not_exact_type(self): + pages = Page.objects.not_exact_type(EventPage) + + # Check that no objects are EventPages + for page in pages: + self.assertNotEqual(type(page.specific), EventPage) + + # Check that the homepage is in the results + homepage = Page.objects.get(url_path='/home/') + self.assertTrue(pages.filter(id=homepage.id).exists()) + + # Check that "Saint Patrick" (an instance of SingleEventPage, a subclass of EventPage) + # is in the results + event = Page.objects.get(url_path='/home/events/saint-patrick/') + self.assertTrue(pages.filter(id=event.id).exists()) + def test_public(self): events_index = Page.objects.get(url_path='/home/events/') event = Page.objects.get(url_path='/home/events/christmas/')