diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 659a27a1c3..6bb1a28872 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -20,6 +20,7 @@ Changelog * Added instructions on how to generate urls for `ModelAdmin` to documentation (LB (Ben Johnston), Andy Babic) * Added option to specify a fallback URL on `{% pageurl %}` (Arthur Holzner) * Add support for more rich text formats, disabled by default: `blockquote`, `superscript`, `subscript`, `strikethrough`, `code` (Md Arifin Ibne Matin) + * Added `max_count_per_parent` option on page models to limit the number of pages of a given type that can be created under one parent page (Wesley van Lee) * Fix: Set `SERVER_PORT` to 443 in `Page.dummy_request()` for HTTPS sites (Sergey Fedoseev) * Fix: Include port number in `Host` header of `Page.dummy_request()` (Sergey Fedoseev) * Fix: Validation error messages in `InlinePanel` no longer count towards `max_num` when disabling the 'add' button (Todd Dembrey, Thibaud Colas) diff --git a/docs/reference/pages/model_reference.rst b/docs/reference/pages/model_reference.rst index 29d4906f3c..babab45f50 100644 --- a/docs/reference/pages/model_reference.rst +++ b/docs/reference/pages/model_reference.rst @@ -203,6 +203,10 @@ In addition to the model fields provided, ``Page`` has many properties and metho Controls the maximum number of pages of this type that can be created through the Wagtail administration interface. This is useful when needing "allow at most 3 of these pages to exist", or for singleton pages. + .. attribute:: max_count_per_parent + + Controls the maximum number of pages of this type that can be created under any one parent page. + .. attribute:: exclude_fields_in_copy An array of field names that will not be included when a Page is copied. diff --git a/docs/releases/2.5.rst b/docs/releases/2.5.rst index 6ccb064126..638b601855 100644 --- a/docs/releases/2.5.rst +++ b/docs/releases/2.5.rst @@ -30,6 +30,7 @@ Other features * Added instructions on how to generate urls for ``ModelAdmin`` to documentation (LB (Ben Johnston), Andy Babic) * Added option to specify a fallback URL on ``{% pageurl %}`` (Arthur Holzner) * Add support for more rich text formats, disabled by default: ``blockquote``, ``superscript``, ``subscript``, ``strikethrough``, ``code`` (Md Arifin Ibne Matin) + * Added ``max_count_per_parent`` option on page models to limit the number of pages of a given type that can be created under one parent page (Wesley van Lee) Bug fixes diff --git a/wagtail/core/models.py b/wagtail/core/models.py index d15fba45ef..f090fb6b03 100644 --- a/wagtail/core/models.py +++ b/wagtail/core/models.py @@ -343,6 +343,9 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): # Define the maximum number of instances this page type can have. Default to unlimited. max_count = None + # Define the maximum number of instances this page can have under a specific parent. Default to unlimited. + max_count_per_parent = None + # An array of additional field names that will not be included when a Page is copied. exclude_fields_in_copy = [] @@ -980,6 +983,9 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): if cls.max_count is not None: can_create = can_create and cls.objects.count() < cls.max_count + if cls.max_count_per_parent is not None: + can_create = can_create and parent.get_children().type(cls).count() < cls.max_count_per_parent + return can_create def can_move_to(self, parent): diff --git a/wagtail/core/tests/test_page_model.py b/wagtail/core/tests/test_page_model.py index 5833d396f8..502e0640b6 100644 --- a/wagtail/core/tests/test_page_model.py +++ b/wagtail/core/tests/test_page_model.py @@ -17,8 +17,8 @@ from wagtail.tests.testapp.models import ( AbstractPage, Advert, AlwaysShowInMenusPage, BlogCategory, BlogCategoryBlogPage, BusinessChild, BusinessIndex, BusinessNowherePage, BusinessSubIndex, CustomManager, CustomManagerPage, CustomPageQuerySet, EventCategory, EventIndex, EventPage, GenericSnippetPage, ManyToManyBlogPage, - MTIBasePage, MTIChildPage, MyCustomPage, OneToOnePage, PageWithExcludedCopyField, SimplePage, - SingleEventPage, SingletonPage, StandardIndex, TaggedPage) + MTIBasePage, MTIChildPage, MyCustomPage, OneToOnePage, PageWithExcludedCopyField, SimpleChildPage, + SimplePage, SimpleParentPage, SingleEventPage, SingletonPage, StandardIndex, TaggedPage) from wagtail.tests.utils import WagtailTestUtils @@ -1167,6 +1167,23 @@ class TestSubpageTypeBusinessRules(TestCase, WagtailTestUtils): self.assertFalse(BusinessChild.can_create_at(SimplePage())) self.assertFalse(BusinessSubIndex.can_create_at(SimplePage())) + def test_can_create_at_with_max_count_per_parent_limited_to_one(self): + root_page = Page.objects.get(url_path='/home/') + + # Create 2 parent pages for our limited page model + parent1 = root_page.add_child(instance=SimpleParentPage(title='simple parent', slug='simple-parent')) + parent2 = root_page.add_child(instance=SimpleParentPage(title='simple parent', slug='simple-parent-2')) + + # Add a child page to one of the pages (assert just to be sure) + self.assertTrue(SimpleChildPage.can_create_at(parent1)) + parent1.add_child(instance=SimpleChildPage(title='simple child', slug='simple-child')) + + # We already have a `SimpleChildPage` as a child of `parent1`, and since it is limited + # to have only 1 child page, we cannot create anoter one. However, we should still be able + # to create an instance for this page at a different location (as child of `parent2`) + self.assertFalse(SimpleChildPage.can_create_at(parent1)) + self.assertTrue(SimpleChildPage.can_create_at(parent2)) + def test_can_move_to(self): self.assertTrue(SimplePage().can_move_to(SimplePage())) diff --git a/wagtail/tests/testapp/migrations/0042_simplechildpage_simpleparentpage.py b/wagtail/tests/testapp/migrations/0042_simplechildpage_simpleparentpage.py new file mode 100644 index 0000000000..dd520872e9 --- /dev/null +++ b/wagtail/tests/testapp/migrations/0042_simplechildpage_simpleparentpage.py @@ -0,0 +1,35 @@ +# Generated by Django 2.1.7 on 2019-03-15 10:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), + ('tests', '0041_secretpage'), + ] + + operations = [ + migrations.CreateModel( + name='SimpleChildPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='SimpleParentPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py index 0709fac044..a659113fa9 100644 --- a/wagtail/tests/testapp/models.py +++ b/wagtail/tests/testapp/models.py @@ -1327,3 +1327,15 @@ class SecretPage(PerUserPageMixin, Page): superuser_content_panels = basic_content_panels + [ FieldPanel('secret_data'), ] + + +class SimpleParentPage(Page): + # `BusinessIndex` has been added to bring it in line with other tests + subpage_types = ['tests.SimpleChildPage', BusinessIndex] + + +class SimpleChildPage(Page): + # `Page` has been added to bring it in line with other tests + parent_page_types = ['tests.SimpleParentPage', Page] + + max_count_per_parent = 1