wagtail
wagtail copied to clipboard
Slug auto generation does not work when slug field is removed from edit form
Issue Summary
Creating two sibling child pages with the same title results in a slug collision as the promote panel will auto generate a slug based on title, expected.
Having an empty promote panel should force a slug to be generated in the full_clean()
method of the Page
class. The full_clean()
method should then generate a base_slug
and call the _get_autogenated_slug()
method.
This doesn't seem to happen and a ValidationError
exception is raised instead of a new slug with an auto generated suffix being returned.
Steps to Reproduce
- Start a new project with
wagtail start myproject
- Create a page class that inherits from
wagtailcore.models.Page
- set
promote_panels = []
- Attempt to create two separate instances of the page with the same title
Technical details
- Python version: 3.6.1
- Django version: 1.11.3
- Wagtail version: 1.11.1
- Browser version: Chrome 59
Fixing this would also fix some of the issues encountered when wanting to customise the way Page.title is presented/used.
#161
It is auto-generated if the slug is empty. But on the model level the field is not marked optional (missing blank=True
) .
For now I patched the model form:
from wagtail.admin.forms.models import WagtailAdminModelForm
def clean(self):
cleaned_data = super(WagtailAdminModelForm, self).clean()
if isinstance(self.instance, Page):
if 'slug' in self.errors and not self.data['slug']:
cleaned_data['slug'] = ''
del self.errors['slug']
return cleaned_data
WagtailAdminModelForm.clean = clean
@joonis This worked for me in order to generate a custom unique slug if it is the same as another slug in the DB. Took me all day to figure out how to use the custom function with Wagtail (and I never had this issue on using Django normally in past), and I humbly thank you 🙇♂️!!!
I have the same issue. Apparently, the subpage being created has no parent_page set (I would expect it to have one, is it an init problem ?), which doesn't generate any new slug.
I stumbled upon the same problem.
A little background: I'm creating a website that hosts events. The problem was, that there are a lot reoccurring events with the same title. So I wanted to create custom slugs for the events that look like TITLE_YYYY-MM-DD. I also don't want the editors to have to think about slugs or being able to mess with them.
As there's no rule that two events on the same day must not have the same title I investigated how wagtail handles such cases and decided to override the Page's full_clean()
method, which gave me the ValidationError
error.
After digging into the code I think I found the root cause. As @Francois133 mentioned get_parent()
returns None
which in turn leads to _slug_is_available()
returning True
and no new slug is autogenerated. The cause that get_parent()
returns None
is, that there's no path stored in self.path
.
From here on I'm just guessing because that's way over my pay grade, but I assume, that it makes sense, that there's no path as long as the page is not saved into the DB. But I noticed, that the page preview also made get_parent()
to be called and it seemed to work.
Long story short: There's code in admin.views.pages.preview.PreviewOnCreate that handles the yet to be known path.
My solution based on @joonis solution (which btw didn't work for me):
from wagtail.admin.views.pages.create import CreateView
def post(self, request):
# TODO: remove this hack when it's no longer needed
# this code was copied from wagtail.admin.views.pages.preview.PreviewOnCreate
# to fight bug https://github.com/wagtail/wagtail/issues/3749
#
# We need to populate treebeard's path / depth fields in order to
# pass validation. We can't make these 100% consistent with the rest
# of the tree without making actual database changes (such as
# incrementing the parent's numchild field), but by calling treebeard's
# internal _get_path method, we can set a 'realistic' value that will
# hopefully enable tree traversal operations
# to at least partially work.
self.page.depth = self.parent_page.depth + 1
# Puts the page at the next available path
# for a child of `parent_page`.
if self.parent_page.is_leaf():
# set the path as the first child of parent_page
self.page.path = self.page._get_path(self.parent_page.path, self.page.depth, 1)
else:
# add the new page after the last child of parent_page
self.page.path = self.parent_page.get_last_child()._inc_path()
self.form = self.form_class(
self.request.POST,
self.request.FILES,
instance=self.page,
subscription=self.subscription,
parent_page=self.parent_page,
for_user=self.request.user,
)
if self.form.is_valid():
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
CreateView.post = post
I don't know if there are any undesired side effects (I've not seen any yet) nor if that's the correct way to fix this issue. But for now it works