How can I add Wagtail reverse field panel? - wagtail

I have two related models Tiers and Vouchers:
Class Tier:
vouchers = models.ManyToManyField(
to="Voucher",
related_name="tiers",
blank=True,
)
class Voucher:
......
How can i add tiers (reverse field) when creating a Voucher from wagtail panel ?
I have tried this way:
Class Voucher:
panels = [
MultiFieldPanel(
[
FieldPanel("tiers", classname="col12"),
],
heading="Main information",
classname="collapsible",
),
]
Got error Unknown field(s) (vouchers.tiers) specified for Voucher.
How can I add tiers fields in wagtail form when creating vouchers ?

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!

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.

Wagtail 2.11 translatable field with wagtai-localize

Is it possible to translate a non snipppet/page model in Wagtail >= 2.11 and wagtail-localize >= 0.9.3 ?
I've set up my model as follows:
class TrainingPlace(TranslatableMixin, models.Model):
name = models.CharField()
description = models.TextField()
class Meta(TranslatableMixin.Meta):
verbose_name = "Training Places"
translatable_fields = [
TranslatableField("description")
]
I have a "Training" submenu in my wagtail admin page, with Trainings, Training Photos, Trainers, Training Places, where I can add Trainers, new Trainings etc.
If I register a TraningPlace with a #register_snipppet I can translate it, but at the moment there are some problems:
I see many Trainings (menu) -> TrainingPlace recoreds in admin listing with no "Translate" option
If I go to Snippets (menu) => Training Place snippets I can see snipppets and have option to translate/create/sync translations, but when I click on change language button in admin I am getting a blank page (url points to a snippet with different ID) which may be a bug.
Can non snippet/page models be translated in admin when using TranslatableMixin on model ?

How can I set the number of initial empty (orderable) items in an InlinePanel?

I have an Orderable model called SetListItem with a ParentalKey on a ClusterableModel called FloorWithSets. The parent FloorWithSets model defines using an InlinePanel to control adding/ordering/removing of the SetListItems. The issue I have is that the admin form automatically renders three empty SetListItems for each FloorWithSets, and I cannot find any way to control this setting.
The InlinePanel class takes parameters to, e.g. set the minimum and maximum number of items, but nothing to set the number of initial empty items rendered.
I cannot find any information about this in the Wagtail docs. I've also dug into the source for InlinePanel and EditHandler but cannot find anything I could override.
I do see from the InlinePanel template file that there is a hidden input with id ending -INITIAL_FORMS which is being rendered via self.formset.management_form. The value of this field is consistently lower than a neighbouring hidden input with id ending -TOTAL_FORMS, which makes sense. I just don't understand where the value is coming from or how to control it.
The only information I can find about this INITIAL_FORMS all seems to relate to testing, (e.g. this documentation) and I cannot see how to relate what that says to what I need.
class FloorWithSets(ClusterableModel):
page = ParentalKey(EventPage, on_delete=models.CASCADE, related_name='floor_with_sets')
FLOOR_CHOICES = [
('1', 'X'),
('2', 'Y'),
('3', 'Z'),
]
floor = models.CharField(
max_length=1,
choices=FLOOR_CHOICES,
default='1',
)
panels = [
FieldPanel('floor'),
InlinePanel('set_list', label=_("set")),
]
class SetListItem(Orderable):
floor = ParentalKey(FloorWithSets, on_delete=models.CASCADE, related_name='set_list')
artist = models.CharField(max_length=255, blank=True, verbose_name=_('artist'))
label = models.CharField(max_length=255, blank=True, verbose_name=_('label'))
start_time = models.TimeField(blank=True, null=True, verbose_name=_('start time'))
end_time = models.TimeField(blank=True, null=True, verbose_name=_('end time'))
set_list_item = FieldRowPanel([
FieldPanel('artist', classname="col6"),
FieldPanel('label', classname="col6")
])
set_list_item_details = FieldRowPanel([
FieldPanel('start_time', classname="col6"),
FieldPanel('end_time', classname="col6")
])
panels = [set_list_item, set_list_item_details]
I think I've found a solution. Try creating a custom Form class for your EventPage model with a custom metaclass like so:
class EventPageFormMetaclass(WagtailAdminModelFormMetaclass):
#classmethod
def child_form(cls):
return EventPageForm
class EventPageForm(WagtailAdminPageForm, metaclass=EventPageFormMetaclass):
pass
class EventPage(Page):
# Whatever you have in your model
base_form_class = EventPageForm
I believe the problem stems from the fact that the ClusterFormMetaclass is hard-coded to create instances of ClusterForm for child models. So your EventPage gets a WagtailAdminPageForm, but the FloorWithSets models gets a ClusterForm. If you stop there, it's fine, but when FloorWithSets generates it's inline panels, it does so as a ClusterForm, whose metaclass has extra_form_count set to 3, as opposed to the WagtailAdminPageForm whose metaclass has it set to 0.
So the solution above creates a new Form class, whose metaclass overrides the child_form class method to return a Form class with extra_form_count set to 0.
Whew.

Wagtail: foreign key to streamfield value

I would like to allow a dropdown within wagtail admin to select from values (blocks) contained within another model's streamfield, is that possible? I imagined something like:
Feedback(page):
paper = models.ForeignKey('PaperPage', on_delete=models.CASCADE, null=True, blank=False, help_text="The paper associated with this feedback. Auto assigned.")
content_panels = [
InlinePanel('paper__drafts_id', label='Draft') <--- this doesn't work
]
where
Paper(page):
drafts = StreamField(
[
('draft_block', blocks.ListBlock(blocks.StructBlock([
('date', blocks.DateTimeBlock(required=True, label='Date draft uploaded')),
('uploaded_by', MemberBlock(required=False, label='Uploaded by',
help_text="Who is uploading this draft.")),
('draft_file', DocumentChooserBlock(required=False, label='Upload file')),
]), template='papers/blocks/drafts.html')),
],
blank=True
)
but I'm not sure if this is even possible? Any suggestions would be most appreciated. Thanks!
This is one downside of StreamField - the data is not stored as "true" database objects, but only as JSON text stored against the page, so there's no way to define relations such as ForeignKeys pointing to individual items in that data.
If there's only one block type in the stream, as in your example code, then it would be a better fit to define 'draft' as a child object (with an InlinePanel) on the Paper model instead; it will then exist as a true database model.

Resources