I am trying to use django autocomplete light to set up Autocompletion for GenericForeignKey
However I am getting this error when I try to view the form:
I have a simple a setup. But because the urls are in an app's namespace, it can't find them.
models.py
class Prereq(IsAPrereqMixin, models.Model):
name = models.CharField(max_length=256, null=True, blank=True)
prereq_content_type = models.ForeignKey(ContentType, related_name='prereq_item',
verbose_name="Type of Prerequisite",
on_delete=models.CASCADE)
prereq_object_id = models.PositiveIntegerField(verbose_name="Prerequisite")
prereq_object = GenericForeignKey("prereq_content_type", "prereq_object_id")
# lots more fields ignored for now
forms.py
from dal import autocomplete
from prerequisites.models import Prereq
class PrereqForm(autocomplete.FutureModelForm):
prereq_object = autocomplete.Select2GenericForeignKeyModelField(
model_choice=[(Prereq, "Prereq"), ]
)
class Meta:
model = Prereq
fields = ['name']
urls.py
from django.urls import path
from prerequisites import views
from prerequisites.forms import PrereqForm
app_name = 'prerequisites'
urlpatterns = [
path('edit/<int:pk>/', views.PrereqUpdateView.as_view(), name='prereq_edit'),
]
# https://django-autocomplete-light.readthedocs.io/en/master/gfk.html#register-the-view-for-the-form
urlpatterns.extend(PrereqForm.as_urls())
I am busy making something like a knowledge graph in wagtail.
CurriculumContentItem is a node on that graph. It has a many-to-many relationship with itself, and the through model has important fields.
I'm struggling to get this to be usable in the admin page. Please see the inline comments:
class ContentItemOrder(models.Model):
post = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
)
pre = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
prerequisites = models.ManyToManyField(
"CurriculumContentItem",
related_name="unlocks",
through="ContentItemOrder",
symmetrical=False,
)
content_panels = Page.content_panels + [
# FieldPanel("prerequisites")
# FieldPanel just lets me select CurriculumContentItems, but I need to access fields in the through model
# InlinePanel("prerequisites"),
# This causes a recursion error
FieldPanel('body', classname="full collapsible"),
]
If I wanted to do this in the normal Django admin I would make use of an inlines to specify prerequisites. Something like:
class ContentItemOrderPostAdmin(admin.TabularInline):
model = models.ContentItem.prerequisites.through
fk_name = "post"
class ContentItemOrderPreAdmin(admin.TabularInline):
model = models.ContentItem.unlocks.through
fk_name = "pre"
Is there a similar mechanism in Wagtail?
It looks like I need to create a custom Panel for this.
I'd suggest constructing an InlinePanel pointing to your 'through' model, which means that you're working with a one-to-many relation rather than many-to-many:
class ContentItemOrder(models.Model):
post = ParentalKey(
"CurriculumContentItem", related_name="pre_ordered_content"
)
pre = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
panels = [
PageChooserPanel('pre'),
FieldPanel('hard_requirement')
]
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
InlinePanel("pre_ordered_content"),
FieldPanel('body', classname="full collapsible"),
]
This works:
Make the through model inherit from Orderable
Make use of ParentalKey instead of ForeignKey
Use InlinePanel referring to the related names of the fields in the through models
from modelcluster.fields import ParentalKey
from wagtail.core.models import Page, Orderable
class ContentItemOrder(Orderable): ### 1
post = ParentalKey( ### 2
"CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
)
pre = ParentalKey( ### 2
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
panels = [
PageChooserPanel('pre'),
PageChooserPanel('post'),
FieldPanel('hard_requirement'),
]
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
prerequisites = models.ManyToManyField(
"CurriculumContentItem",
related_name="unlocks",
through="ContentItemOrder",
symmetrical=False,
)
content_panels = Page.content_panels + [
InlinePanel('pre_ordered_content', label="prerequisites"), ### 3
InlinePanel('post_ordered_content', label="unlocks"), ### 3
FieldPanel('body', classname="full collapsible"),
]
I was worried that there would be 2 PageChooser fields available per inline but wagtail is clever (and magical) enough that it just draws the one we need
How to make Tags and Categories Separate... I have a tag for SCRC and a Tag for Libraries, but How can i make it so when I create a blog, and choose each different tag, the posts show up?
here is my models.py file. thank you for any suggestions.
This is my first django wagtail app and i am trying to create a test blog.
from django.db import models
from django.utils.translation import deactivate_all
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag as TaggitTag
from taggit.models import TaggedItemBase
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
PageChooserPanel,
StreamFieldPanel,
)
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet
class BlogsPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
#register_snippet
class BlogsCategory(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"
#How am i going to filter this categories ()
#register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
class PostPageBlogsCategory(models.Model):
page = ParentalKey(
"blogs.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blogs_category = models.ForeignKey(
"blogs.BlogsCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blogs_category"),
]
class Meta:
unique_together = ("page", "blogs_category")
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blogs.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
Snippets are just like any model, except it gets put into a different area of the Wagtail CMS.
Here is a basic example of a snippet in code
#register_snippet
class Category(index.Indexed, models.Model):
name = models.CharField(
max_length=100,
null=True,
)
panels = [
FieldPanel('name'),
]
search_fields = [
index.SearchField('name'),
]
def __str__(self):
return self.name
class Meta:
ordering = ['name']
Here is how you would reference a snippet on a Wagtail Page Model
class BlogPost(Page):
category = ParentalManyToManyField(
'Category',
blank=True,
)
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel(
'category',
widget=forms.CheckboxSelectMultiple,
)
],
heading='Category'
),
]
You can also remove the widget=forms.CheckboxSelectMultiple if that isn't something you want this to do.
By creating a snippet, you now have the option to create "Categories" and then have the ability to select them on the Blog Post.
Once you do that, all you have to do is query the category in the BlogPage model on the HTML template and display it or group it however you want to.
I use snippets when I want more flexibility that tags cannot offer.
EDIT
As I was looking through the Wagtail docs I found something that does exactly what I mentioned
I can't make file upload form field work. My references are:
https://github.com/lb-/bakerydemo/blob/stack-overflow/61289214-wagtail-form-file-upload/bakerydemo/base/models.py
https://dev.to/lb/image-uploads-in-wagtail-forms-39pl
The field was added to admin and appear correctly on my site, but when I try to send the form:
if the field is required, the form is reloaded without any error
if the field is not required, the form is sent but the file is not uploaded
What am I doing wrong? I've been working on this for 3 days and can't find any error message pointing what's wrong. When I get some error message it's always too generic.
Please help! :)
My models.py inside bakerydemo/bakerydemo/base
from __future__ import unicode_literals
import json
from os.path import splitext
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.html import format_html
from django.urls import reverse
from django import forms
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
PageChooserPanel,
StreamFieldPanel,
)
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Collection, Page
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormSubmission, AbstractFormField, FORM_FIELD_CHOICES
from wagtail.contrib.forms.views import SubmissionsListView
from wagtail.images import get_image_model
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.images.fields import WagtailImageField
from wagtail.search import index
from wagtail.snippets.models import register_snippet
from .blocks import BaseStreamBlock
from django.core.validators import ValidationError
import logging
#register_snippet
class People(index.Indexed, ClusterableModel):
"""
A Django model to store People objects.
It uses the `#register_snippet` decorator to allow it to be accessible
via the Snippets UI (e.g. /admin/snippets/base/people/)
`People` uses the `ClusterableModel`, which allows the relationship with
another model to be stored locally to the 'parent' model (e.g. a PageModel)
until the parent is explicitly saved. This allows the editor to use the
'Preview' button, to preview the content, without saving the relationships
to the database.
https://github.com/wagtail/django-modelcluster
"""
first_name = models.CharField("First name", max_length=254)
last_name = models.CharField("Last name", max_length=254)
job_title = models.CharField("Job title", max_length=254)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
panels = [
MultiFieldPanel([
FieldRowPanel([
FieldPanel('first_name', classname="col6"),
FieldPanel('last_name', classname="col6"),
])
], "Name"),
FieldPanel('job_title'),
ImageChooserPanel('image')
]
search_fields = [
index.SearchField('first_name'),
index.SearchField('last_name'),
]
#property
def thumb_image(self):
# Returns an empty string if there is no profile pic or the rendition
# file can't be found.
try:
return self.image.get_rendition('fill-50x50').img_tag()
except: # noqa: E722 FIXME: remove bare 'except:'
return ''
def __str__(self):
return '{} {}'.format(self.first_name, self.last_name)
class Meta:
verbose_name = 'Person'
verbose_name_plural = 'People'
#register_snippet
class FooterText(models.Model):
"""
This provides editable text for the site footer. Again it uses the decorator
`register_snippet` to allow it to be accessible via the admin. It is made
accessible on the template via a template tag defined in base/templatetags/
navigation_tags.py
"""
body = RichTextField()
panels = [
FieldPanel('body'),
]
def __str__(self):
return "Footer text"
class Meta:
verbose_name_plural = 'Footer Text'
class StandardPage(Page):
"""
A generic content page. On this demo site we use it for an about page but
it could be used for any type of page content that only needs a title,
image, introduction and body field
"""
introduction = models.TextField(
help_text='Text to describe the page',
blank=True)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Landscape mode only; horizontal width between 1000px and 3000px.'
)
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
content_panels = Page.content_panels + [
FieldPanel('introduction', classname="full"),
StreamFieldPanel('body'),
ImageChooserPanel('image'),
]
class GalleryPage(Page):
"""
This is a page to list locations from the selected Collection. We use a Q
object to list any Collection created (/admin/collections/) even if they
contain no items. In this demo we use it for a GalleryPage,
and is intended to show the extensibility of this aspect of Wagtail
"""
introduction = models.TextField(
help_text='Text to describe the page',
blank=True)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Landscape mode only; horizontal width between 1000px and '
'3000px.'
)
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
collection = models.ForeignKey(
Collection,
limit_choices_to=~models.Q(name__in=['Root']),
null=True,
blank=True,
on_delete=models.SET_NULL,
help_text='Select the image collection for this gallery.'
)
content_panels = Page.content_panels + [
FieldPanel('introduction', classname="full"),
StreamFieldPanel('body'),
ImageChooserPanel('image'),
FieldPanel('collection'),
]
# Defining what content type can sit under the parent. Since it's a blank
# array no subpage can be added
subpage_types = []
class HomePage(Page):
"""
The Home Page. This looks slightly more complicated than it is. You can
see if you visit your site and edit the homepage that it is split between
a:
- Hero area
- Body area
- A promotional area
- Moveable featured site sections
"""
# Hero section of HomePage
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Homepage image'
)
hero_text = models.CharField(
max_length=255,
help_text='Write an introduction for the bakery'
)
hero_cta = models.CharField(
verbose_name='Hero CTA',
max_length=255,
help_text='Text to display on Call to Action'
)
hero_cta_link = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
verbose_name='Hero CTA link',
help_text='Choose a page to link to for the Call to Action'
)
# Body section of the HomePage
body = StreamField(
BaseStreamBlock(), verbose_name="Home content block", blank=True
)
# Promo section of the HomePage
promo_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Promo image'
)
promo_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
promo_text = RichTextField(
null=True,
blank=True,
help_text='Write some promotional copy'
)
# Featured sections on the HomePage
# You will see on templates/base/home_page.html that these are treated
# in different ways, and displayed in different areas of the page.
# Each list their children items that we access via the children function
# that we define on the individual Page models e.g. BlogIndexPage
featured_section_1_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_1 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='First featured section for the homepage. Will display up to '
'three child items.',
verbose_name='Featured section 1'
)
featured_section_2_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_2 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Second featured section for the homepage. Will display up to '
'three child items.',
verbose_name='Featured section 2'
)
featured_section_3_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_3 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Third featured section for the homepage. Will display up to '
'six child items.',
verbose_name='Featured section 3'
)
content_panels = Page.content_panels + [
MultiFieldPanel([
ImageChooserPanel('image'),
FieldPanel('hero_text', classname="full"),
MultiFieldPanel([
FieldPanel('hero_cta'),
PageChooserPanel('hero_cta_link'),
]),
], heading="Hero section"),
MultiFieldPanel([
ImageChooserPanel('promo_image'),
FieldPanel('promo_title'),
FieldPanel('promo_text'),
], heading="Promo section"),
StreamFieldPanel('body'),
MultiFieldPanel([
MultiFieldPanel([
FieldPanel('featured_section_1_title'),
PageChooserPanel('featured_section_1'),
]),
MultiFieldPanel([
FieldPanel('featured_section_2_title'),
PageChooserPanel('featured_section_2'),
]),
MultiFieldPanel([
FieldPanel('featured_section_3_title'),
PageChooserPanel('featured_section_3'),
]),
], heading="Featured homepage sections", classname="collapsible")
]
def __str__(self):
return self.title
class FormField(AbstractFormField):
logging.warning('FormField')
page = ParentalKey('FormPage', related_name='form_fields', on_delete=models.CASCADE)
field_type = models.CharField(
verbose_name='field type',
max_length=16,
choices=FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
)
class CustomFormBuilder(FormBuilder):
logging.warning('CustomFormBuilder')
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class CustomSubmissionsListView(SubmissionsListView):
"""
further customisation of submission list can be done here
"""
logging.warning('CustomSubmissionsListView')
pass
class CustomFormSubmission(AbstractFormSubmission):
# important - adding this custom model will make existing submissions unavailable
# can be resolved with a custom migration
def get_data(self):
"""
Here we hook in to the data representation that the form submission returns
Note: there is another way to do this with a custom SubmissionsListView
However, this gives a bit more granular control
"""
logging.warning('CustomFormSubmission')
file_form_fields = [
field.clean_name for field in self.page.specific.get_form_fields()
if field.field_type == 'fileupload'
]
data = super().get_data()
for field_name, field_vale in data.items():
if field_name in file_form_fields:
# now we can update the 'representation' of this value
# we could query the FormUploadedFile based on field_vale (pk)
# then return the filename etc.
pass
return data
def upload_validator(value):
raise ValidationError(value)
class FormUploadedFile(models.Model):
logging.warning('FormUploadedFile')
file = models.FileField(upload_to="files/%Y/%m/%d", validators=[upload_validator])
# file = models.FileField(upload_to="files/%Y/%m/%d")
field_name = models.CharField(blank=True, max_length=254)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
submissions_list_view_class = CustomSubmissionsListView
logging.warning('FormPage')
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
# body = RichTextField(default="")
# body = StreamField(BaseStreamBlock())
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
thank_you_text = RichTextField(blank=True)
# Note how we include the FormField object via an InlinePanel using the
# related_name value
content_panels = AbstractEmailForm.content_panels + [
ImageChooserPanel('image'),
StreamFieldPanel('body'),
InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('from_address', classname="col6"),
FieldPanel('to_address', classname="col6"),
]),
FieldPanel('subject'),
], "Email"),
]
def get_submission_class(self):
"""
Returns submission class.
Important: will make your existing data no longer visible, only needed if you want to customise
the get_data call on the form submission class, but might come in handy if you do it early
You can override this method to provide custom submission class.
Your class must be inherited from AbstractFormSubmission.
"""
# print('get_submission_class')
return CustomFormSubmission
def process_form_submission(self, form):
"""
Accepts form instance with submitted data, user and page.
Creates submission instance.
You can override this method if you want to have custom creation logic.
For example, if you want to save reference to a user.
"""
# print('process_form_submission')
file_form_fields = [field.clean_name for field in self.get_form_fields() if field.field_type == 'fileupload']
for (field_name, field_value) in form.cleaned_data.items():
if field_name in file_form_fields:
uploaded_file = FormUploadedFile.objects.create(
file=field_value,
field_name=field_name
)
# store a reference to the pk (as this can be converted to JSON)
form.cleaned_data[field_name] = uploaded_file.pk
return self.get_submission_class().objects.create(
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
page=self,
)
I want to add a default value and have it populate the field in a Wagtail template based on a Django model. I know I am returning the value because if I populate 'help-text' attribute with this value, it works but I cannot get it populate the field with the default attribute. I am using a field panel for the content panel. This Class is very long so I did not post the whole thing.
def live_video_url():
return constants.streaming_info['live-video-captions']
class MeetingPage(Page):
live_video_url = models.URLField(
default=live_video_url,
blank=False,
help_text=live_video_url,
null=True
)
content_panels = [
FieldPanel('live_video_url'),
]
I am getting this in the actual field in the Wagtail editor, but the correct url string in help-text:
<django.db.models.query_utils.DeferredAttribute object at 0x104547470>
Example of adding a default value in a field in the wagtail admin
# models.py
from django.db import models
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.admin.forms import WagtailAdminPageForm
class MyItemForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
description = self.fields["description"]
description.widget.attrs["value"] = "My new initial data"
class MyItem(Page):
description = models.CharField(max_length=255, default='', blank=True)
content_panels = Page.content_panels + [
FieldPanel("description"),
]
base_form_class = MyItemForm