model relationship design for Blog app in Wagtail site - wagtail

Below models.py to build a blog in wagtail site is from this post.
class BlogPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
class PostPageBlogCategory(models.Model):
page = ParentalKey(
"blog.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blog_category = models.ForeignKey(
"blog.BlogCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blog_category"),
]
class Meta:
unique_together = ("page", "blog_category")
#register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=80)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
def __str__(self):
return self.name
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
#register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
I am wondering, what are the major reasons to introduce extra Intermediary model (class PostPageBlogCategory(models.Model): & class PostPageTag(TaggedItemBase):) to link PostPage to Category & Tag?
Why not just simply use ParentalForeignkey or ParentalManyToManyKey ?

The short answer is that Django's ManyToManyField has some limitations in how the relationship needs to be built when models on either end to not yet exist, adding an 'intermediary model' helps to work around this.
Longer Answer
As with anything in software, there are multiple ways to do something and each has its own pros & cons. Django's built in field is simple and lets you do lots of powerful things to represent relational data but that simplicity comes with some reduced flexibility in how these relationships are managed.
One of the main goals of the Wagtail CMS Admin interface is the ability to work as though the data has been created (including relationships) before actually clicking 'save'. While this may seem simple at first glance, getting to that point requires a bit of nuance under the hood once you start to consider relational data.
Wagtail comes built in with a very powerful library called django-modelcluster which has been purpose built for many of the cases where you want to work with relational data without having all the bits in the DB first.
Each Wagtail Page actually inherits the modelcluster.models.ClusterableModel, which is why some of the features in the blog post seem to work in the editor, even when the DB entries have not yet been saved.
On the blog post you linked, there is a section towards the end with the heading 'ParentalKey' that further explains this nuance and how just using Django's basic approach has some draw backs.
On the Django docs for many-to-many relationships, have a read through and note that each individual model instance must be in the database first and only then can you 'link' the two instances with a second update on each.

Related

How to create a tags field for multiple Page classes in Wagtail?

I want to add a field for keywords that almost every model in my site will have. It would be ideal if I didn't have to define a "TaggedPage" class for every Page model. So I created a BasePage abstract model but ParentalKey doesn't appear to work with an abstract model. How can I solve this?
I get this error:
home.TaggedPage.content_object: (fields.E300) Field defines a relation with model 'home.BasePage', which is either not installed, or is abstract.
home.TaggedPage.content_object: (fields.E307) The field home.TaggedPage.content_object was declared with a lazy reference to 'home.basepage', but app 'home' doesn't provide model 'basepage'.
home/models.py contains these models:
class PageTag(TagBase):
""" Tag used for the keywords meta html tag"""
class Meta:
verbose_name = "search/meta keyword"
verbose_name_plural = "search/meta keywords"
class TaggedPage(ItemBase):
tag = models.ForeignKey(
PageTag,
related_name="tagged_pages",
on_delete=models.CASCADE,
)
content_object = ParentalKey(
to='home.BasePage',
on_delete=models.CASCADE,
related_name='tagged_items'
)
class BasePage(MetadataPageMixin, Page):
tags = ClusterTaggableManager(
through='home.TaggedPage',
blank=True,
help_text="Used for the keywords meta html tag",
)
promote_panels = Page.promote_panels + [
FieldPanel('tags'),
]
class Meta:
abstract = True
class HomePage(BasePage):
parent_page_types = []
Point the ParentalKey at the Page model instead:
content_object = ParentalKey(
to='wagtailcore.Page',
on_delete=models.CASCADE,
related_name='tagged_items'
)
At the database level this will have the behaviour you're looking for: a foreign key field referencing the ID of the page object. The one minor side effect is that the tagged_items relation will be defined on all Page objects, not just ones inheriting from BasePage, but that shouldn't cause any problems.

How to reference a Autofield from one model to another

I need some input about referring an Autofield into another model.
I tried making a foreign key, but I am not sure if the syntax is right
I have a main model class with id as Autofield
class Metadataform(models.Model):
id = models.AutoField(primary_key=True)
Authors_Name = models.CharField(max_length=500, blank=True, null=True)
Then I have a upload class
(I basically want to fetch the same id from the main model into the Meta_id in the uploadmeta model)
class uploadmeta(models.Model):
Meta_id = models.ForeignKey('Metadataform',on_delete=models.CASCADE, null=True)
Authors_Name = models.CharField(max_length=500, blank=True, null=True)
tar_gif = models.FileField(upload_to='mnt/storage/DB-Data/')
def __str__(self):
return self.Meta_id
Your approach is indeed correct, although you do not need to add a primary key yourself. Django does that. Furthermore your code does not really follows the naming conventions.
You can implement this as:
class MetadataForm(models.Model):
author_name = models.CharField(max_length=500, blank=True, null=True)
class UploadMeta(models.Model):
metadata_form = models.ForeignKey('MetadataForm', on_delete=models.CASCADE, null=True)
tar_gif = models.FileField(upload_to='mnt/storage/DB-Data/')
Furthermore it might not make much sense to set blank=True. This means that by default that field will not show up in forms. null=True not be necessary as well, since that means you can add None (in the database NULL) to that field.
The __str__ of a model typically includes some field value(s) of that model. Since by only using values from a ForeignKey relation, it means two or more UploadMetas that have different values, have the same textual representation if these refer to the same MetaDataForm?
It is not very clear to me eather why you defined a field named Authors_Name in the UploadMeta as well. If that is the same as the metadata_form's author_name, then you better do not include an extra one since that creates data duplication.
Perhaps it is better to make an Author model as well, and let MetadataForm refer through a ForeignKey to the author of that MetadataForm model.

Inline creation of snippet in a streamfield block (Wagtail 2.3+)

so lets say I have the following models set up for Wagtail:
#register_snippet
class MySnippet(models.Model):
name = models.CharField(max_length=200, null=True)
panels = [FieldPanel('name'),]
def __str__(self):
return self.name
class Meta:
ordering = ['name',]
class MyPage(Page):
body = StreamField([
('mysnippet', SnippetChooserBlock(required=False, label='MySnippet', target_model='MySnippet')),
], blank=True, help_text='')
content_panels = Page.content_panels + [
StreamFieldPanel('body', heading='Stuff to add'),
]
My client will be creating a lot of MySnippet items as they go. It's going to be super awkward for them to move to another view in their CMS, create a MySnippet, then come back to their main MyPage editor to choose it.
Q1 Is there a simple way to add a SnippetChooseOrInlineCreate() block so clients can add new MySnippets as they create MyPages?
Q2 If there's no existing simple way, how would you recommend approaching this?

How to give user option to select a wagtail collection of images in page?

I am looking for a way to show a list of wagtail collection as a field in a page (just like it showing when you upload an image). A user can select a collection and I can programmatically filter the images to the selected collection. I am still new to wagtail and I am not sure how should I implement this in code.
Thank you in advance for your help.
So there's a couple ways you can do this. The first, and probably the least-ideal way is to register Collection as a snippet and use a SnippetChooserPanel.
"""Register Collection snippet."""
from wagtail.snippets.models import register_snippet
from wagtail.core.models import Collection
# Register Collections as Snippets so we can use the SnippetChooserPanel to select a collection
register_snippet(Collection)
And then in your model you can use a SnippetChooserPanel, like so (note, this is all untested code)
from django.db import models
from wagtail.core.models import Page
class CustomPage(Page):
# ...
collection = models.ForeignKey(
'wagtailcore.Collection',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
)
content_panels = Page.content_panels + [
# ...
SnippetChooserPanel('collection'),
]
#gasman's comment on the answer has a link to another solution that's much more elegant than mine.
I've managed to do this using wagtail-generic-chooser just following the instructions on the README.md, and using wagtail core Collection model instead of People.
Aug 2022 - Wagtail 2.15.5 - display Wagtail hierarchical collection
from wagtail.admin.templatetags.wagtailadmin_tags import format_collection
class Meeting(models.Model):
COLLECTION_CHOICES = []
for c in Collection.objects.all():
COLLECTION_CHOICES.append((c.id, format_collection(c)))
title = models.CharField(max_length=100)
collection = models.ForeignKey(Collection, on_delete=models.PROTECT, help_text="Choose the 'Collection' folder for the meeting's related documents", choices=COLLECTION_CHOICES)
Edit: If you add a new collection to collections and go back the this Meeting model the new collection will not be in the list. As the COLLECTION_CHOICES is only created once for optimization. If you want a dynamic collection choice you need to make a custom form on top of your model e.g.
from wagtail.admin.forms import WagtailAdminModelForm
class MeetingAdminForm(WagtailAdminModelForm):
# This below field will be automatically added to the Meeting panel fields
meeting_collection = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(MeetingAdminForm, self).__init__(*args, **kwargs)
self.fields['meeting_collection'] = forms.ChoiceField(
initial=self.instance.collection_id,
choices=[(c.id, format_collection(c)) for c in Collection.objects.all()]
)
def save(self, commit=True):
instance = super().save(commit=False)
instance.collection_id = self.cleaned_data['meeting_collection']
if commit:
instance.save()
return instance
class Meeting(models.Model):
base_form_class = MeetingAdminForm
class Meta:
""" Meta options """
ordering = ['title']
title = models.CharField(max_length=100)
meeting_datetime = models.DateTimeField()
location = models.TextField(null=True)
collection = models.ForeignKey(Collection, on_delete=models.PROTECT, help_text="Choose the 'Collection' folder for the meeting's agenda, minutes and related documents")
committee = models.ForeignKey(Committee, on_delete=models.CASCADE)
panels = [
FieldPanel('title'),
FieldPanel('meeting_datetime'),
FieldPanel('location'),
FieldPanel('meeting_collection'),
FieldPanel('committee'),
]

Wagtail Inlinepanel demo erorr

I am relatively new to Django Wagtail and I was following the demo from the docs.wagtail.io website which can be found here on how to add a list of Links to a Page using an InlinePanel with Related links
I seem to have reached an error that I donot fully understand it's meaning.
The error says
AttributeError: type object 'BookPageRelatedLinks' has no attribute 'rel'
The code for the demo is as follows
from wagtail.wagtailcore.models import Orderable, Page
from modelcluster.fields import ParentalKey
from wagtail.wagtailadmin.edit_handlers import FieldPanel,InlinePanel
from django.db import models
class BookPage(Page):
# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
title = models.CharField(max_length=255)
link_external = models.URLField("External link", blank=True)
panels = [
FieldPanel('title'),
FieldPanel('link_external'),
]
class Meta:
abstract = True
# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
page = ParentalKey('demo.BookPage', related_name='related_links')
content_panels = Page.content_panels + [
InlinePanel('BookPageRelatedLinks', label="Related Links"),
]
My primary objective was to learn this so I can add image links to a sidebar on a BlogPage app I am developing.
Your InlinePanel declaration isn't quite correct - it needs to be:
InlinePanel('related_links', label="Related Links")
Here's what's going on:
By defining a ParentalKey with related_name='related_links', you set up a one-to-many relation called related_links on BookPage. This allows you to retrieve all of the BookPageRelatedLinks objects associated with a given BookPage instance (for example, if your BookPage instance was called page, you could write page.related_links.all()).
The InlinePanel declaration then tells Wagtail to make the related_links property editable within the admin.
The reason you're getting a misleading error message is that you've defined the RelatedLink and BookPageRelatedLinks classes inside the BookPage - which is a little bit unusual, but still valid. This results in BookPageRelatedLinks being defined as a property of BookPage (i.e. BookPage.BookPageRelatedLinks). Then, when Wagtail tries to set up the InlinePanel, it retrieves that property and fails because it's not the expected type of object (it's a class definition, not a relation).
If you write your models file in the more conventional way, with the related models defined below (or above) BookPage:
class BookPage(Page):
content_panels = Page.content_panels + [
InlinePanel('BookPageRelatedLinks', label="Related Links"),
]
# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
title = models.CharField(max_length=255)
link_external = models.URLField("External link", blank=True)
panels = [
FieldPanel('title'),
FieldPanel('link_external'),
]
class Meta:
abstract = True
# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
page = ParentalKey('home.BookPage', related_name='related_links')
...then you would get a more informative error: AttributeError: type object 'BookPage' has no attribute 'BookPageRelatedLinks'.

Resources