Column clean_name does not exist (Wagtail 2.10) - wagtail

I just upgraded from Wagtail 2.9.3 to 2.10 and discovered that my FormPages aren't working anymore due to a missing clean_name field. I read the documentation about this new change but I am at a complete loss as to how to fix it.
After upgrading, I tried running my local server but was told I needed to run migrations which I did. How can I add the clean_name column to my field? Do I just add the column to my FormField class below, like I normally would?
Below is the stack trace and relevant models.
ProgrammingError at /support-us/volunteer/
column core_formfield.clean_name does not exist
LINE 1: ...e_formfield"."id", "core_formfield"."sort_order", "core_form...
class FormField(AbstractFormField):
page = ParentalKey(
'FormPage',
on_delete=models.CASCADE,
related_name='form_fields',
)
class FormPage(MetadataPageMixin, AbstractEmailForm):
body = StreamField(BaseStreamBlock())
confirmation_text = RichTextField(blank=True)
content_panels = AbstractEmailForm.content_panels + [
StreamFieldPanel('body'),
InlinePanel('form_fields', label='Form Fields'),
FieldPanel('confirmation_text'),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('from_address', classname="col6"),
FieldPanel('to_address', classname="col6"),
]),
FieldPanel("subject"),
], heading="Email Settings"),
]
class Meta:
verbose_name = 'Form Page'
verbose_name_plural = 'Form Pages'

Running ./manage.py makemigrations and ./manage.py migrate after upgrading should be sufficient. No changes to your code are required - the new clean_name field is defined as part of AbstractFormField, so it doesn't need to be added to your own Formfield definition.

Related

How do I access a model in wagtail which is linked by a central model as a many-to-many relationship?

I'm working with wagtail and implemented a many-to-many relationship between page models by using a central model which handles this relationship. I did the same, as what is written in this issue: Wagtail many-to-many links between different Page models and it works so far in the wagtail admin.
Now I struggle with accessing the information of the related page model and displaying it on a template. So let's assume, that I use the code from the mentioned issue:
Example Code
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel
class PersonPageSitePageRelation(models.Model):
person = ParentalKey('app.PersonPage', on_delete=models.CASCADE, related_name='sites')
site = ParentalKey('app.SitePage', on_delete=models.CASCADE, related_name='people')
# Optional: some additional fields (e.g. 'note') for this relation
# Important: NOT setting any `panels` here, will be set individually for each 'direction'
class Meta:
unique_together = ('person', 'site')
class PersonPage(Page):
# ... fields (note: `sites` does NOT need to be declared as a field)
# Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `sites`, but the panels available will be the PersonPageSitePageRelation's field `site`
content_panels = Page.content_panels + [
# ... other FieldPanel etc
InlinePanel('sites', label='Related Sites', [PageChooserPanel('site')]),
]
class SitePage(Page):
# ... fields (note: `people` does NOT need to be declared as a field)
# Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `people`, but the panels available will be the PersonPageSitePageRelation's field `person`
content_panels = Page.content_panels + [
# ... other FieldPanel etc
InlinePanel('people', label='Related People', panels=[PageChooserPanel('person')]),
]
I'm able to select related person-pages on a site page by a page-chooser-panel. How can I now display all persons of one site on a site_page.html template?
If I'm accessing one specific SidePage via site_page.html I tried the tag {{page.people}} but I just see PersonPageSitePageRelation.None. I'm so confused by the use of the central PersonPageSitePageRelation model and I don't find any similar case in other issues.
Thank you a lot in advance!

Snippet title problem after upgrade from wagtail 1.13 to 2.0

I am working on wagtail upgrade on a site from version 1.13.4 . Was mostly following this post https://wagtail.org/blog/upgrading-wagtail/ . However, since the first update to wagtail 2.0 snippet titles are wrong. I could sort it out on the templated front end pages, but they still appear wrong on the admin panel.
How titles are displayed on the admin panel
And this same issue is with fields that link to those snippets
I got as far as updating wagtail to 2.4, since the blog post said that it should support python 3.7, but issue still persists. As far as I can tell, there are no error messages on the terminal output, migrations run without any issues. Any clue where I should start looking?
This should be the relevant artist snippet declaration from the project
#register_snippet
class Artist(index.Indexed, models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
return unicode(self.name)
objects = ArtistQuerySet.as_manager()
name = models.CharField(max_length=512)
name_encoded = models.CharField(max_length=512)
image_url = models.URLField(blank=True, max_length=512)
image = models.ForeignKey(
'wagtailimages.Image',
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='+',
)
#property
def status(self):
# If there are no events currently attached to this artist
if not self.artistatevent_set.all():
return NO_CURRENT_DATES
# This artist does have some events, next see if there's any tickets
if self.total_ticket_count is not 0:
# Are they all the same type?
if self.available_ticket_count == self.total_ticket_count:
return AVAILABLE
if self.sold_out_ticket_count == self.total_ticket_count:
return SOLD_OUT
if self.cancelled_ticket_count == self.total_ticket_count:
return CANCELLED
if self.unavailable_ticket_count == self.total_ticket_count:
return UNAVAILABLE
# See if we have ANY of the following
if self.available_ticket_count:
return AVAILABLE
if self.coming_soon_ticket_count:
return COMING_SOON
if self.sold_out_ticket_count:
return SOLD_OUT
# Nothing matched, so default
return UNAVAILABLE
panels = [
FieldPanel('name'),
ImageChooserPanel('image'),
]
search_fields = [
index.SearchField('name', partial_match=True),
]
EDIT:
This is a snippet from admin.py declaration
...
class NameIDAdmin(admin.ModelAdmin):
list_display = ('name', 'id')
...
admin.site.register(Artist, NameIDAdmin)
When upgrading from Python 2 to 3, you should replace the __unicode__ method with __str__: https://docs.djangoproject.com/en/1.10/topics/python3/#str-and-unicode-methods

Wagtail many-to-many links between different Page models

Does anyone have or know of a recipe (sample code and/or instructions) on setting up many-to-many relationships between different Page models? If I have PersonPage and SitePage models, how do I connect the pages (a person can work at multiple sites and a site can have multiple people working there)?
Here's what I've found related to, but not directly on, this topic—
Wagtail docs: from a search for "many-to-many" the only hit is in the section on the taggit module (Recipes page).
Wagtail docs: the only reference to the ParentalManyToManyField is a demo of how it can be used to create M2Ms between pages and categories (Tutorial)
This 2015 post on M2M relationships in Wagtail (it's referenced in an SO 'answer' to basically the same question I'm asking here). Although it doesn't discuss page-page relationships the approach presented might be adapted to work. My modified imitation failed with various errors depending on how I tried to set up the InlinePanel call — but the sample code from the post fails in just the same ways, so either it wasn't tested or it's been made obsolete in 2.x.
class PersonPage(Page):
pass
PersonPage.content_panels = [
InlinePanel('ps_links', label='PS Links'),
]
class PersonSitePageLink():
spage = models.ForeignKey('SitePage', on_delete=models.SET_NULL, related_name='sites')
ppage = ParentalKey('PersonPage', related_name='ps_links', on_delete=models.SET_NULL,)
panels = [
FieldPanel('spage')
]
class SitePage(Page):
pass
This technique works fine for relating a Page model to itself, but expanding it to encompass two distinct models creates two parallel but unconnected sets of relationships (you can pick arbitrary Bug pages to link to any Plant page, or vice versa, but the Plants you picked don't show when you edit Bugs). I see why in the code, I think, but I don't see how to make a single M2M connection between the two pages.
class PlantPage(Page):
related_bugs = ParentalManyToManyField('BugPage', blank=True)
content_panels = Page.content_panels + [
FieldPanel('related_bugs'),
]
class BugPage(Page):
related_plants = ParentalManyToManyField('PlantPage', blank=True)
content_panels = Page.content_panels + [
FieldPanel('related_plants'),
]
This one also only talks about intra-page model (rather than inter-page model) M2Ms. (It is pre-ParentalManyToManyField and in fact only available from the Wayback Machine.)
I hope this helps, I took inspiration from this article about moving from ParentalManyToManyField to a central model that 'links' each page from this AccordBox article.
It turns out that InlinePanel does not fully support ParentalManyToManyField, hence the issues you were running into.
I was able to implement a refined approach to your option one above and it should solve your problem.
A reminder that all Page models already extend ClusterableModel so there is no need to add that to any models you create.
Overview
Create a new 'relation' that extends models.Model which will be the relation between these two page models.
Each field within this new model will be the two page types via the model-cluster ParentalKey each with a logical related_name set that is the OTHER side of the relationship.
No need to set panels on this model as we will declare the panels individually via the panels kwarg to InlinePanel - see the InlinePanel docs.
Finally, each individual Page's content_panels has an InlinePanel added that refers to the central relation model indirectly via that model's related_name, adding the other side reference to PageChooserPanel.
Example Code
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel
class PersonPageSitePageRelation(models.Model):
person = ParentalKey('app.PersonPage', on_delete=models.CASCADE, related_name='sites')
site = ParentalKey('app.SitePage', on_delete=models.CASCADE, related_name='people')
# Optional: some additional fields (e.g. 'note') for this relation
# Important: NOT setting any `panels` here, will be set individually for each 'direction'
class Meta:
unique_together = ('person', 'site')
class PersonPage(Page):
# ... fields (note: `sites` does NOT need to be declared as a field)
# Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `sites`, but the panels available will be the PersonPageSitePageRelation's field `site`
content_panels = Page.content_panels + [
# ... other FieldPanel etc
InlinePanel('sites', label='Related Sites', [PageChooserPanel('site')]),
]
class SitePage(Page):
# ... fields (note: `people` does NOT need to be declared as a field)
# Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `people`, but the panels available will be the PersonPageSitePageRelation's field `person`
content_panels = Page.content_panels + [
# ... other FieldPanel etc
InlinePanel('people', label='Related People', panels=[PageChooserPanel('person')]),
]
Further Reading
Read about Django Modelcluster - which is the library that ParentalKey comes from.

How to manage third party app migrations with custom WAGTAILIMAGES_IMAGE_MODEL

Situation
I have a custom image and rendition model, and have followed the wagtail v2.4 guide to implement them:
class AccreditedImage(AbstractImage):
"""
AccreditedImage - Customised image model with optional caption and accreditation
"""
caption = models.CharField(max_length=255, blank=True)
accreditation = models.CharField(max_length=255, blank=True, null=True)
admin_form_fields = Image.admin_form_fields + (
'caption',
'accreditation',
)
class Meta:
verbose_name = 'Accredited image'
verbose_name_plural = 'Accredited images'
def __str__(self):
credit = ' ({})'.format(self.accreditation) if (self.accreditation is not None) and (len(self.accreditation) > 0) else ''
return '{}{}'.format(self.title, credit)
class AccreditedRendition(AbstractRendition):
"""
AccreditedRendition - stores renditions for the AccreditedImage model
"""
image = models.ForeignKey(AccreditedImage, on_delete=models.CASCADE, related_name='renditions')
class Meta:
unique_together = (('image', 'filter_spec', 'focal_point_key'),)
verbose_name = 'Accredited Image Rendition'
verbose_name_plural = 'Accredited Image Renditions'
In settings I have:
WAGTAILIMAGES_IMAGE_MODEL = 'cms.AccreditedImage'
But, I have two third party plugins installed: puput and wagtail_events, each of which use a foreign key to wagtail images.
When I run `manage.py makemigrations, additional migrations are created in the puput and wagtail_events site_packages folders to handle the change in FK. The migrations look like this:
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('puput', '0005_blogpage_main_color'),
]
operations = [
migrations.AlterField(
model_name='blogpage',
name='header_image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='cms.AccreditedImage', verbose_name='Header image'),
),
migrations.AlterField(
model_name='entrypage',
name='header_image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='cms.AccreditedImage', verbose_name='Header image'),
),
]
The Problem
If I actually apply these migrations, then puput or wagtail_events releases a new version, then the migration history becomes corrupted - e.g. my autogenerated 0006* migration on puput and their new 0006* migration fork the history
The Question
Is there a way of overcoming this? Or a recommended practice for what to do?
At this point I'm in very early beta, so I could dump the entire DB and start again if the recommended strategy is to set this up from the outset to avoid the issue.
Thanks for any help, folks!
Answer 1 - if you have control over your third party libraries
The initial migration in the third party library should define a swappable dependency, for example:
from wagtail.images import get_image_model_string
dependencies = [
migrations.swappable_dependency(get_image_model_string()),
]
operations = [
migrations.CreateModel(
name='ThirdPartyModel',
fields=[
...
('image', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=get_image_model_string())),
],
...
),
This is not automatically created by makemigrations. get_image_model_string needs to be used like this in every migration affecting that FK, made through the entire migration history of the library.
If you change the setting at some point in the project, you will still need to do a data migration ('Migrate an existing swappable dependency' might help), but this solves the forking problem described above if starting clean.
It has the drawback of requiring control over the third party library. I'm not holding my breath for a project like puput to go back and alter their early migration history to allow for a swappable image model (puput's initial migration hard-codes wagtailimages.Image). But I've implemented this for wagtail_events (my own project) to save other people this hassle.
Answer 2 - if you don't have control
Ugh. I've been working on this a while and all candidate solutions are all pretty horrible. I considered getting my custom image class to impersonate wagtail.images.model.Image via the db_table meta attributes, and even by creating another app which essentially duplicates wagtail images. It's all either a lot of work or super hacky.
I've chosen to take over migrations manually using the MIGRATION_MODULES setting.
For my scenario, I've taken the entire migration history of puput and copied all the files into a separate folder, root/custom_puput_migrations/. I set
MIGRATION_MODULES = {
'puput': 'custom_puput_migrations'
}
WAGTAILIMAGES_IMAGE_MODEL = 'otherapp.AccreditedImage'
Then I pull the ol' switcharoo by editing 0001_initial.py in that folder to refer to the model via the setting, rather than by hard coding:
...
from wagtail.images import get_image_model_string
class Migration(migrations.Migration):
dependencies = [
...
migrations.swappable_dependency(get_image_model_string())
]
operations = [
migrations.CreateModel(
name='BlogPage',
fields=[
...
('header_image', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='Header image', blank=True, to=get_image_model_string(), null=True)),
],
...
Drawbacks
1) The actual table relation created is not strictly determined by the migration file, but by the setting, which could change independently. If you prefer to avoid this, you could instead simply hard-code your referred model in the custom migration.
2) This approach leaves you pretty vulnerable to developers upgrading the library version requirement without realising that they also have to manually copy across the migrations files. I suggest a check (f/ex ensuring that the number of files in the default migrations folder is the same as the number of files in the custom one) before allowing the app to boot, to ensure your development and production databases all run on the same set of migrations.

Why is the SnippetChooserPanel not opening in Wagtail?

Some time ago I stopped using #register_snippet to decorate Snippets. This takes the Snippet out of the snippets section of admin.
Instead I used the wagtail_hooks.py to show the snippet directly in the left admin panel for user convenience. See below. This works nicely as the user can go directly to the snippet and you can also alter the displayed fields and ordering of fields - nice.
So in the below example I removed the line that says #register_snippet. What's the catch? The SnippetChooserPanel does not work! Later I was building a complex model and the SnippetChooserPanel did not work. I wasted quite a bit of time thinking the problem was in the complexity of my model. I want to save others' time!
wagtail_hooks.py:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.wagtailsnippets.models import register_snippet
from demo.models import Advert
class AdvertAdmin(ModelAdmin):
model = Advert
modeladmin_register(AdvertAdmin)
Here is the snippet example from Wagtail: snippets
#register_snippet #<------- Source of issue (I removed this line!)
#python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Python 2
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __str__(self):
return self.text
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
content_panels = Page.content_panels + [
SnippetChooserPanel('advert'),
# ...
]
If you make your Snippets editable via modelAdmin you still need to apply the decorator #register_snippet. Otherwise the chooser panel route/view won't be available. This view is requested by the ajax request fired on SnippetChooser modal open. Missing #register snippet will trow a 404.
You can register menu items via construct_main_menu hook. You can use the same hook to remove exiting menu-items. If you do not want the 'Snippets' menu item remove it. In wagtail_hooks.py:
#hooks.register('construct_main_menu')
def hide_snippet(request, menu_items):
menu_items[:] = [item for item in menu_items if item.name != 'snippets']
The solution is always use #register_snippet decorator otherwise the SnippetChooserPanel doesn't work!
#register_snippet
#python_2_unicode_compatible
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __str__(self):
return self.text

Resources