Get ancestor with flag set - wagtail

Trying to add context menu of nested pages. To do so, added a boolean to the page model called "root_page".
How can I find the ancestor of the current page with this set to True? The calling page may be 1 to 3 levels down from the "root page".
Tried the following but it gives 'NoneType' object has no attribute 'get_ancestors'
menu_tags.py:
def get_page_root(context, calling_page=None):
...
root = calling_page.get_ancestors().live().filter(longscrollpage__root_page=True).first()
...
model.py:
class LongScrollPage(Page):
...
include_left_menu = models.BooleanField(default=False)
root_page = models.BooleanField(default=False)
...
template.html:
...
{% if page.include_left_menu == True %}
{% get_page_root calling_page=self %}
...

Related

Get Image and intro text from wagtail page chooser

I am incorporating a page chooser in wagtail admin, however what I want to get is the image for each item in the streamblock for the page chooser and was wanting to know if there is a way to do this. So lets say if I have a category index page I would have the following code:
{% if page.case_study %}
{% image page.case_study.image fill-50x50-c100 class="" %}
{{ page.case_study }}
{% endif %}
All I seem to get are the links which is fine, but I need the image from that case study page. My model is as follows:
class CategoryPage(Page):
"""
Detail view for a specific category
"""
introduction = RichTextField(
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
)
case_study = StreamField([
('Cases', blocks.PageChooserBlock()),
], blank=True,
null=True,
verbose_name='Case Studies',
)
origin = models.ForeignKey(
Country,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
category_type = models.ForeignKey(
'categories.CategoryType',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
categories = ParentalManyToManyField('Category', blank=True)
content_panels = Page.content_panels + [
FieldPanel('introduction', classname="full"),
ImageChooserPanel('image'),
StreamFieldPanel('body'),
StreamFieldPanel('case_study'),
FieldPanel('origin'),
FieldPanel('category_type'),
]
search_fields = Page.search_fields + [
index.SearchField('body'),
]
parent_page_types = ['CategoriesIndexPage']
Any help would be greatly appreciated
A StreamField is a list of blocks, but when you write page.case_study.image you're trying to access it as if it's only a single item. I'd suggest you start by updating your field / block names to make this distinction clearer - case_studies is the list, and each block in it is a single case (note that conventionally block names are lower-case):
case_studies = StreamField(
[
('case', blocks.PageChooserBlock()),
],
blank=True,
null=True,
verbose_name='Case Studies',
)
In your template, you will now loop over page.case_studies. However, this will not give you the case study page objects directly - each item in the list is a 'block' object, with block_type and value properties. This is because a StreamField usually involves mixing multiple block types (rather than just the one type you've defined here) and in that situation, you need some way of finding out what block type you're working with on each iteration of the loop.
{% if page.case_studies %}
{% for block in page.case_studies %}
{# block.value now refers to the page object #}
{% endfor %}
{% endif %}
The next issue is that your PageChooserBlock currently lets you select any page type. This means that when your template is rendered, it has no way to know up-front which page type to retrieve, so (in order to avoid unnecessary database lookups) it returns it as a basic Page instance which only contains the core fields that are common to all pages, such as title and slug - consequently, your image field will not be available through block.value.image. You can get around this by using block.value.specific.image which performs the extra database lookup to retrieve the complete page data - however, a more efficient approach is to specify the page type on the PageChooserBlock (assuming you've set up a dedicated page type for your case studies):
('case', blocks.PageChooserBlock(page_type=CaseStudyPage)),
Your final template code then becomes:
{% if page.case_studies %}
{% for block in page.case_studies %}
{% image block.value.image fill-50x50-c100 class="" %}
{% endfor %}
{% endif %}

How to prevent duplicates when using ModelChoiceFilter in Django Filter and Wagtail

I am trying to use filters with a Wagtail Page model and a Orderable model. But I get duplicates in my filter now. How can I solve something like this?
My code:
class FieldPosition(Orderable):
page = ParentalKey('PlayerDetailPage', on_delete=models.CASCADE, related_name='field_position_relationship')
field_position = models.CharField(max_length=3, choices=FIELD_POSITION_CHOICES, null=True)
panels = [
FieldPanel('field_position')
]
def __str__(self):
return self.get_field_position_display()
class PlayerDetailPage(Page):
content_panels = Page.content_panels + [
InlinePanel('field_position_relationship', label="Field position", max_num=3),
]
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.all())
class Meta:
model = PlayerDetailPage
fields = []
So what I am trying to do is create a filter which uses the entries from FIELD_POSITION_CHOICES to filter out any page that has this position declared in the inline panel in Wagtail.
As you can see in the picture down below, the filters are coming through and the page is being rendered. (These are 2 pages with a list of 3 field positions).
So Page 1 and Page 2 both have a "Left Winger" entry, so this is double in the dropdown. The filtering works perfectly fine.
What can I do to prevent this?
The solution should be something like this (Credits to Harris for this):
I basically have one FieldPosition object per page-field position, so it's listing all of the objects correctly. I suspect I should not use the model chooser there, but a list of the hard coded values in FIELD_POSITION_CHOICES and then a filter to execute a query that looks something like PlayerDetailPage.objects.filter(field_position_relationship__field_position=str_field_position_choice). But what is the Django Filter way of doing this?
Raf
In my limited simplistic view it looks like
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.all())
is going to return all the objects from FieldPosition and if you have 2 entries for 'left wing' in here (one for page 1 and one for page 2) then it would make sense that this is duplicating in your list. So have you tried to filter this list queryset with a .distinct? Perhaps something like
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.values('field_position').distinct())
I found the solution after some trial and error:
The filter:
class PlayerDetailPageFilter(FilterSet):
field_position_relationship__field_position = filters.ChoiceFilter(choices=FIELD_POSITION_CHOICES)
class Meta:
model = PlayerDetailPage
fields = []
And then the view just like this:
context['filter_page'] = PlayerDetailPageFilter(request.GET, queryset=PlayerDetailPage.objects.all()
By accessing the field_position through the related name of the ParentalKey field_position_relationship with __.
Then using the Django Filter ChoiceFilter I get all the hard-coded entries now from the choice list and compare them against the entries inside the PlayerDetailPage query set.
In the template I can get the list using the Django Filter method and then just looping through the query set:
<form action="" method="get">
{{ filter_page.form.as_p }}
<input type="submit" />
</form>
{% for obj in filter_page.qs %}
{{ obj }} >
{% endfor %}

Select base template per site

I am building a setup that will contain a main site and a number of microsites. Each microsite is going to have a separate branding but use the same page types.
Given Wagtail already has a Site object which links to an appropriate Page tree, is there also built in functionality to configure the template loaders to choose an appropriate base.html or will I have to write a custom template loader?
Wagtail doesn't have any built-in functionality for this, as it doesn't make any assumptions about how your templates are put together. However, you could probably implement this yourself fairly easily using the wagtail.contrib.settings module, which provides the ability to attach custom properties to individual sites. For example, you could define a TemplateSettings model with a base_template field - your templates can then check this setting and dynamically extend the appropriate template, using something like:
{% load wagtailsettings_tags %}
{% get_settings %}
{% extends settings.my_app.TemplateSettings.base_template %}
I extended the above answer to provide an override of the {% extends ... %} tag to use a template_dir parameter.
myapp.models:
from wagtail.contrib.settings.models import BaseSetting, register_setting
#register_setting
class SiteSettings(BaseSetting):
"""Site settings for each microsite."""
# Database fields
template_dir = models.CharField(max_length=255,
help_text="Directory for base template.")
# Configuration
panels = ()
myapp.templatetags.local:
from django import template
from django.core.exceptions import ImproperlyConfigured
from django.template.loader_tags import ExtendsNode
from django.template.exceptions import TemplateSyntaxError
register = template.Library()
class SiteExtendsNode(ExtendsNode):
"""
An extends node that takes a site.
"""
def find_template(self, template_name, context):
try:
template_dir = \
context['settings']['cms']['SiteSettings'].template_dir
except KeyError:
raise ImproperlyConfigured(
"'settings' not in template context. "
"Did you forget the context_processor?"
)
return super().find_template('%s/%s' % (template_dir, template_name),
context)
#register.tag
def siteextends(parser, token):
"""
Inherit a parent template using the appropriate site.
"""
bits = token.split_contents()
if len(bits) != 2:
raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
parent_name = parser.compile_filter(bits[1])
nodelist = parser.parse()
if nodelist.get_nodes_by_type(ExtendsNode):
raise TemplateSyntaxError(
"'%s' cannot appear more than once in the same template" % bits[0])
return SiteExtendsNode(nodelist, parent_name)
myproject.settings:
TEMPLATES = [
{
...
'OPTIONS': {
...
'builtins': ['myapp.templatetags.local'],
},
},
]

Django Custom field's attributes making database queries

I am facing a very weird problem in one of my django projects. In my project I have a custom field class that handles foreign keys, one to one and many 2 many model fields. The class is some thing like the following.
from django import forms
class CustomRelatedField(forms.Field):
def __init__(self, model, limit=None, multiple=False, create_objects=True, *args, *kwargs):
self.model = model
self.limit = limit
self.multiple = multiple
self.create_objects = create_objects
super(CustomRelatedField, self).__init__(*args, **kwargs)
def clean(self, value):
""" Calls self.get_objects to get the actual model object instance(s)
from the given unicode value.
"""
# Do some value processing here
return self.get_objects(value)
def get_objects(self, values):
""" Returns the model object instances for the given unicode values.
"""
results = []
for value in values:
try:
obj = self.model.object.get_or_create(name=value)[0]
results.append(obj)
except Exception, err:
# Log the error here.
return results
def prepare_value(self, value):
""" Returns the value to be sent to the UI. The value
passed to this method is generally the object id or
a list of object id's (in case it is a many to many object).
So we need to make a database query to get the object and
then return the name attribute.
"""
if self.multiple:
result = [obj.name for obj in self.model.filter(pk__in=value)]
else:
result = self.model.object.get(pk=value)
return result
Recently while I was playing with the django-toolbar, I found out one of the pages that has a form with the above mentioned fields was ridiculously making multiple queries for the same objects again and again.
While debugging, I found out the prepare_value method was being called again and again. After some more debugging, I realized the culprit was the template. I have a generic template that I use for forms, It looks something like the following:
{% for field in form %}
{% if field.is_hidden %}
<!-- Do something here -->
{% endif %}
{% if field.field.required %}
<!-- Do something here -->
{% endif %}
<label>{{ field.label }}</label>
<div class="form-field">{{ field }}</div>
{% if field.field.widget.attrs.help_text %}
<!-- Do something here -->
{% elif field.errors %}
<!-- Do something here -->
{% endif %}
{% endfor %}
In the above code, each if statement calls the field class which calls the prepare_value method which then makes the database queries. Each of the following listed is making a database query, I am totally lost to why this is happening and have no clue about any solutions. Any help, suggestions would be really appreciated. Thanks.
field.is_hidden
field.field.required
field.label
field.label_tag
field
field.field.widget.attrs.help_text
field.errors
Also, why does this happen with my custom field class only, other fields (FKs, O2Os, M2M's) in the application and the application admin, just make one query, even though they are using a similar template.
Problem is with your prepare_value() method which does explicit queries. .get() does not get cached and always hits the db while iterating on .filter() queryset will evaluate that.
This might be causing you multiple queries.
This is not seen in default fields because they do not do any queries in prepare_value().
To resolve this, you can try to cache the value and result. If value hasn't changed, return cached result. Something like:
class CustomRelatedField(forms.Field):
def __init__(self, model, limit=None, multiple=False, create_objects=True, *args, *kwargs):
self.cached_result = None
self.cached_value = None
...
def prepare_value(self, value):
#check we have cached result
if value == self.cached_value:
return self.cached_result
if self.multiple:
result = [obj.name for obj in self.model.filter(pk__in=value)]
else:
result = self.model.object.get(pk=value)
#cache the result and value
self.cached_result = result
self.cached_value = value
return result
Not sure how good/bad this work around though!

Testing for presence of form instance in Jinja2

I'm using WTforms with Jinja2 and want to change my templates page title depending on whether I am creating a new instance of editing an existing form object.
This is what I wrote in the template:
{% block title %}{% if form.obj %}Edit{% else %}New{% endif %} Post{% endblock %}
What I expect to see:
if the form is filled out I expect to see "Edit Post" in the page title.
if the form is empty I expect to see "New Post" in the page title.
What I get: "New Post" in both instances.
Here is my PostHandler that is passing the form values.
def with_post(fun):
def decorate(self, post_id=None):
post = None
if post_id:
post = models.BlogPost.get_by_id(int(post_id))
if not post:
self.error(404)
return
fun(self, post)
return decorate
class PostHandler(BaseHandler):
def render_form(self, form):
self.render_to_response("edit.html", {'form': form})
#with_post
def get(self, post):
self.render_form(MyForm(obj=post))
#with_post
def post(self, post):
form = MyForm(formdata=self.request.POST, obj=post)
if post and form.validate():
form.populate_obj(post)
post.put()
post.publish()
self.render_to_response("published.html", {'post': post})
elif self.request.POST and form.validate():
post = models.BlogPost()
post.title = form.title.data
post.body = form.body.data
post.tags = form.tags.data
post.publish()
self.render_to_response("published.html", {'post': post})
else:
self.render_to_response('edit.html', {'form':form})
In short, all I'm trying to do is test whether the form is filled, and change my page title "New Post" or "Edit Post" accordingly.
While Form accepts a obj argument, it doesn't actually store it, it just uses that obj to fill in any blanks that formdata didn't provide. So when you ask Jinja2 {% if form.obj %} it's always going to be False, because there is never a obj property (unless you have a field that happens to be called obj of course).
If you're editing a post, you'll have an id to work with so you know which post to update in the database, so where are you currently storing that? Assuming you store it as a hidden field, you could just do:
{% if form.id.data == None %}Must be a New form {% endif %}
If you wanted to check if the entire form was empty, you could access the form.data dictionary, and make sure all the entries are None, although you need to be careful, because I know that FileField returns a u'None' instead of a real None, so you'd have to double check what Fields you care about.

Resources