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

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?

Related

model relationship design for Blog app in Wagtail site

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.

how to update more than one database table within one modeladmin form in wagtail?

In my question, there are three related models:
class DicSoftware(index.Indexed, ClusterableModel):
name = models.CharField("软件名称", max_length=255, blank=True)
version = models.CharField("版本号", max_length=255, blank=True)
panels = [MultiFieldPanel([
FieldPanel('name', classname="col10"),
FieldPanel('version', classname="col10"),
], "模拟软件")]
class Simulation(index.Indexed, ClusterableModel):
name = models.CharField("算例名称", max_length=255, blank=True)
software = ParentalManyToManyField('DicSoftware', related_name='模拟软件')
panels = [ MultiFieldPanel([
FieldPanel('name', classname="col10"),
FieldPanel('software', classname="col10"),
], "算例")]
With these two models above, wagtail automatically generate table simulation_software with three fields, id, simulation_id, dicsoftware_id. However, I want to add other two fields in table simulation_software, inputFile and inputFilePath. The model of the final table simulation_software should be:
class SimulationSoftware(index.Indexed, ClusterableModel):
simulation = models.ForeignKey(Simulation, verbose_name="算例", help_text="/admin/home/simulation/", on_delete=models.CASCADE, blank=True, null=True, related_name='+')
software = models.ForeignKey(DicSoftware, verbose_name="模拟软件", help_text="/admin/home/dicsoftware/", on_delete=models.CASCADE, blank=True, null=True, related_name='+')
#把读入的文件内容存入inputJSON
inputFile = models.FileField("初始化参数文件", upload_to="files", default="")
outputFilePath = models.CharField("结果文件存储位置", max_length=255, blank=True)
panels = [MultiFieldPanel([
FieldPanel('simulation', classname="col10"),
FieldPanel('software', classname="col10"),
FieldPanel('inputFile', classname="col10"),
FieldPanel('outputFilePath', classname="col10"),
], "算例输入")]
When users add one simulation, they have to give the information as follows:
simulation name.
specify the software(one or more software) used in this simulation.
In one simulation, there can be one or more sofware. If two software are used in this simulation, users should give these information at the same time:
the first sofware's inputFile and outputFilePath.
the second sofware's inputFile and outputFilePath.
How to manage the models' structure and the input panels for inputFiles and outputFilePaths of the software(one or more software).
Anyone can give me some sugguestions? Wish for your help. Thank you very much!
you can relate the sub-models by adding attribute (page) like this:
page = ParentalKey('SimulationSoftware', related_name='<sub-class>_model', on_delete=models.CASCADE)
in each of subclasses (DicSoftware and Simulation)
and then in the content_panels of main clustarable model, use the related_name attributes of each sub-class: FieldPanel("<sub-class>_model", classname="col10"),

Access other object fields in StreamField / RichText field/block in wagtail

I would like to do the following:
Create some django model (or wagtail page) that contains data.
Create another Page type that includes a StreamField or RichtextField.
3.
When the author or editor enters text in this field, he/she can have the information of this appear somewhere in the rendered output of the text. Preferably using common template techniques.
So, lets say, we have a model like this:
class Person(models.Model):
name = models.CharField(max_length=30)
Then I have a normal page that uses a StreamField:
class NormalPage(Page):
body = StreamField(block_types=[
('paragraph', blocks.RichTextBlock()),
])
content_panels = Page.content_panels + [
StreamFieldPanel('body')
]
I would like for the editor to choose which Person he/she would like from the database and the be able to do something like this in the RichTextBlock:
{{ person.name }}
Is this possible?
In order to be able to choose a plain Django model, register it as a snippet as shown here, and then use SnippetChooserPanel to choose the person:
from wagtail.snippets.models import register_snippet
#register_snippet
class Person(models.Model):
name = models.CharField(max_length=30)
And then if you want to associate a particular person with a particular page:
from wagtail.snippets.edit_handlers import SnippetChooserPanel
class NormalPage(Page):
body = StreamField(block_types=[
('paragraph', blocks.RichTextBlock()),
])
person = models.ForeignKeyField(Person)
content_panels = Page.content_panels + [
StreamFieldPanel('body'),
SnippetChooserPanel('person'),
]

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'),
]

Relationships in Django Admin

I get really confused with many-to-many database relationships, so can some one please clarify how I would achieve this?
I need a table of "Tags" (as in tag words) and a table for "Entries", such at many "Entries" could correspond to many Tag words.
Right now I have my models like this:
# models.py
class Tags(models.Model):
tag = models.CharField(max_length=255)
entry = models.ManyToManyField(Entry)
class Entry(models.Model):
entry = models.CharField(max_length=255)
description = models.TextField()
Now I'm confused, how would I setup my admin.py so I could then add tags when I create a new entry?
What you need is using the through feature of models:
class Tag(models.Model):
tag = models.CharField(max_length=255)
entry = models.ManyToManyField(Entry, through='TaggedEntries')
class Entry(models.Model):
entry = models.CharField(max_length=255)
description = models.TextField()
class TaggedEntries(models.Model):
entry = models.ForeignKey(Entry)
tag = models.ForeignKey(Tag)
and now use that model in your admin:
class TagsInline(admin.TabularInline):
model = TaggedEntries
extra = 1
class EntryAdmin(admin.ModelAdmin):
inlines = (TagsInline, )
admin.site.register(Entry, EntryAdmin)
admin.site.register(Tag)
You will need something along the lines of:
# admin.py
from django.contrib import admin
from models import *
class TagsInline(admin.TabularInline):
model = Tag
extra = 1
class EntryAdmin(admin.ModelAdmin):
inlines = (TagsInline, )
admin.site.register(Entry, EntryAdmin)
admin.site.register(Tag)
(Note, this code was written in a browser!)

Resources