Wagtail - how to (correctly) get other pages/models - python

Using Wagtail which is the more appropriate way of getting the records of other pages/models.
# models.py
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
context['children'] = ArticleDetailPage.objects.live().public()[:3]
return context
# template.html
{% for item in children %}
<li>{{ item }}</li>
{% endfor %}
Or,
# models.py
def articles(self):
articles = ArticleDetailPage.objects.live().public()[:3]
return articles
# template.html
{% for item in page.articles %}
<li>{{ item }}</li>
{% endfor %}
Both of these methods work and I see the former often used in the docs and either one used elsewhere. Is there a difference at all to which one is used in terms of performance or "correctness" or is it simply personal preference?

AFAIW, it's just personal preference. The first one is more explicit, the second one keeps your context more clean. The only difference I can think of is that in the second option ArticleDetailPage.objects.live().public()[:3] is only called when you actually include {{page.articles}} in your template, which could lead to a tiny boost in performance if the query is quite complex.
In general I tend to wrap these kind of variables in a property when either:
I know there is an explicit relationship there. So in this case, when they're subpages of the page rendering the template. A variable like shopping_cart or breadcrumb would be less likely to be coupled to the page itself.
or
The variable is used in other parts of my Django/Wagtail code apart from the template. It's very much possible that I want to access the articles of a page from somewhere else.
Otherwise, I just pass it directly in the context.

Related

Break long {% with %} assignment into multiple lines in a Django template [duplicate]

I am creating a custom django template tag by using such a code :
#register.simple_tag(takes_context=True)
def render_listing(context, *args, **kwargs):
... my code ...
This works well, but in my template, it seems that all parameters must be on a single line, for example :
this works:
{% render_listing param1=val1 param2=val2 ... paramN=valN %}
but on multiple lines, it does not work :
{% render_listing param1=val1
param2=val2
...
paramN=valN %}
I tried multiple escape sequences but I did not succeeded,
Is there a way to specify a template tag on multiple lines ?
No, the Django template language does not support multiple line tags. See ticket 8652, which was closed as WONTFIX, or this thread from the django-developers mailing list.
Sometimes, if there is a repeated prefix, you can make it more readable by using the with tag. For example if you have,
{% render_listing param1=long.common.prefix.val1 param2=long.common.prefix.val2 param2=long.common.prefix.val3 %}
you could rewrite as
{% with prefix=long.common.prefix %}
{% render_listing param1=prefix.val1 param2=prefix.val2 param2=prefix.val3 %}
{% endwith %}
Often (but not always), a really long tag is an indication that you're putting too much logic in the template. See if you can move some of it into the view, model method, template tag or template filter.
It's pretty straightforward to enable, though hackish:
import re
from django.template import base
base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL)
"Using" it is simple; one place I find it especially useful is {% include %} tags:
{% include 'my/sweet/modal-template.template' with
message="Hey, do you really want to frob the widget?"
yes="Heck yes I do!"
no="No frickin' way!"
icon="error"
%}
I haven't tested this in more recent versions of Django but I imagine it could be adapted; that worked at least back around 1.8. I should point out that in theory some tags that do custom parsing of their arguments could break; in practice I haven't had any trouble in the last ~10 years of Django programming.

Variable context between two blocks in Django templates?

I have two blocks that call the same method with same variables. I want to call the method only once, but the result is then outsite the scope of the block tags. I've tried calling this method in the parent template header.html and with a with tag, but nothing seems to work.
This is the layout:
{% extends "header.html" %}
{% load navigation_tags %}
{% block header %}
{% get_section site=site as section %}
{% include "foobar.html" with section=section %}
{% endblock header %}
{% block navigation %}
<nav>
<div class="container">
{% get_section site=site as section %}
{% navigation section.slug %}
</div>
</nav>
{% endblock navigation %}
navigation_tags.py
#register.assignment_tag
def get_parent_section(site):
if site.id == settings.FOOBAR_SITE_ID:
section = Section.objects.get(id=settings.FOOBAR_SECTION_ID)
else:
# This is also a section instance.
return site.default_section
As mentioned by 2pacho in another answer and Fernando Cezar in a comment, the easiest way to share values between different sections is to set it in the template context. If you are using the render shortcut function, you can pass a dict as the context parameter to add a value to the rendering context of the template. That would be a good place to add it and this would be the easiest place to put it.
return render(request, 'template.html', {'section': get_parent_section(site)})
However, if for some reason, you can't include it in the context, you can use a decorator to add memoization to your function, so that it will cache the computation results and return it immediately when called with the same parameters. You can use functools.lru_cache to do so, or it's Django backport at django.utils.lru_cache.lru_cache if you are using Python 2.x.
#register.assignment_tag
#functools.lru_cache()
def get_parent_section(site):
if site.id == settings.FOOBAR_SITE_ID:
section = Section.objects.get(id=settings.FOOBAR_SECTION_ID)
else:
# This is also a section instance.
return site.default_section
I wouldn't call a method outside .py . Think that this is using Jinja2 templates,
it's powerful but not in the way that the backend can be.
What I recommend you doing in this case is to generate a context for the template and use this variables there.
Would be as simple as adding it to your context where it's being generated.
context['site_parent'] = get_parent_section(site)
Think that Jinja2 (html) has to be as simple as possible and that can help you with basic coding and time saving (like loops to print the exact same information or show and hide code depending on the context) but I would keep it as simple you can when rendering.
If you would like you can read official django website about templates https://docs.djangoproject.com/en/2.0/topics/templates/
But from my expirience I would keep the method calls in the views.py

Django template: how to randomize order when populating page with objects?

I have a Survey and a Choice model, and each survey has a number of choices associated with it. When I'm rendering the actual HTML survey page with all the choices, I use the following Django templating code:
{% for choice in survey.choice_set.all %}
<li class="ui-state-default" choice_id={{ choice.id }}>{{ choice.choice_text }}</li>
{% endfor %}
However, instead of the choices appearing in the same order every time, I'd like them to populate in a random order to reduce any potential bias effect (e.g. someone may be more likely to vote for options that appear first on the list).
If there's a way to do this within the template itself, that'd be great, but it seems more likely that I'd need to do something in the backend in views.py. I've already tried this, to no effect:
class DetailView(generic.DetailView):
model = Survey
...
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
...
survey = get_object_or_404(Survey, survey_link__iexact=survey_link)
...
if randomize_choice_order:
survey.choice_set.order_by('?')
...
return context
Any idea how I can accomplish this? Maybe I need to instead develop a JS function to randomize the objects after they've already been placed?
You can create a custom template tag to shuffle your results.
# app/templatetags/shuffle.py
import random
from django import template
register = template.Library()
#register.filter
def shuffle(arg):
aux = list(arg)[:]
random.shuffle(aux)
return aux
Then in your template
{% load shuffle %}
{% for choice in survey.choice_set.all|shuffle %}
With Django 1.7+ you can use the Prefetch object:
survey = get_object_or_404(
Survey.objects.prefetch_related(
Prefetch('choice', queryset=Choice.objects.order_by('?'),
to_attr='random_choices')
),
survey_link__iexact=survey_link
)
You can then access the random set with survey.random_choices. The original choice set is still available with survey.choice_set.all().

Trouble Rendering Template Variables in Django

I am hitting a brick wall when it comes to solving this problem. I have a template that is being included in an other template, but I am unable to render any template variables in the included template. I have a separate template tag file for the included template. I am at a total loss right now how to resolve this problem.
I would like to be able to render the field values from my model in the template (which consist of an image, a short description, etc.) I am fairly certain that I am going about this in the wrong way.
Below is my code:
The model:
class AddOnItem(models.Model):
base_product = models.ForeignKey(Product)
base_price = models.DecimalField(max_digits=8, decimal_places=2, default=0.0)
addon_image = models.ImageField(upload_to='uploads/shop/product/images/',
default='uploads/shop/product/images/', help_text='Max width: 450px')
short_description = models.CharField(max_length=255, blank=True)
def __unicode__(self):
return self.base_product.name
The template:
{% load addon_tags %}
{% if items_to_add %}
{% for items in items_to_add %}
<div id="addon-container">
<div id="left-addon" class="addon-container">
<img src="#" class="addon-image">
<p class="addon-description">{{items.short_description}}</p>
</div>
</div>
{% endfor %}
{% endif %}
addon_tags.py:
from django import template
from sho.models import AddOnItem
register = template.Library()
#register.inclusion_tag("foo/templates/v/sho/addon.html", takes_context=True)
def add_on_render():
context['items_to_add'] = AddOnItem()
return context
I imagine I am doing either a lot wrong (my assumption at the moment) or I am missing some minor bit. I have been working on this for several days and have gone over the docs repeatedly. I am simply completely missing something and this has become a major blocker for me. Any help would be appreciated.
Django version 1.4
Edit:
I ended up rewriting the view and did away with the templatetag. Thanks to both Daniel Roseman and Odif Yltsaeb for their replies.
1) From your post you are adding empty, new item into the context in add_on_render templateag.
2) I cant see in your post, WHERE you are using {% add_on_render %} templatetag. You have created templattag, but do not seem to be using it anywhere.
It is bit hard to understand what exactly are you trying to do or why you even need templatetag there.
If you want to display models field value, you do not need templateag for this and all the stuff that you show in your "template" part of this post, could be very well in your main template, which i assume, is not shown in your post.
If you want to use templatetag, then this templatetag should probably recieve AddOnItem istance as parameter like this:
#register.inclusion_tag("foo/templates/v/sho/addon.html", takes_context=True)
def add_on_render(item):
context['items_to_add'] = item
return context
And You could use it in some template like this:
{% load addon_tags %}
{% if items_to_add %}
{% for items in items_to_add %}
<div id="addon-container">
<div id="left-addon" class="addon-container">
<img src="#" class="addon-image">
<p class="addon-description">{% add_on_render items %}</p>
</div>
</div>
{% endfor %}
{% endif %}
and your foo/templates/v/sho/addon.html
would like like this:
{{ items_to_add.short_description }}
But doing it this way seems very unnecessary as you could achieve all that without templatag by using the template code that you already have outside your "main" template.
You haven't posted the template that you are attempting to include the tag in. I suspect you're not calling it at all, because there are a couple of errors that would cause exceptions if you did try and use the tag. You need to do {% add_on_render %} somewhere in your main template.
As I say though there are a couple of errors. Firstly, you don't define context (as an empty dict) before adding the items_to_add key. You can shortcut this by just doing it in one go.
Secondly you've made items_to_add a single, blank, AddOnItem. So in your included template, iterating through items_to_add does nothing at all. Not sure what you are trying to do there. Perhaps you want to pass all AddOnItem instances?
context = {'items_to_add': AddOnItem.objects.all()}
Or maybe you want to filter them by some criteria, in which case you probably want to pass those criteria to the inclusion tag itself:
def add_on_render(product):
context = {'items_to_add': AddOnItem.objects.filter(base_product=product)}
and you would call it from the main template like this:
{% add_on_render my_product %}
if you set "takes_context=True" you should take context as the first argument in decorated function:
#register.inclusion_tag("foo/templates/v/sho/addon.html", takes_context=True)
def add_on_render(context):
context['items_to_add'] = AddOnItem()
....

Possible to limit filters ManyToMany/Foreign Key in Django admin for a model where the relationship is defined on the other model?

So the title is a bit obtuse, I know, but I couldn't think of a more succinct way to state it. Here's the issue:
I've created two proxy models for "user types", both inheriting from django.contrib.auth.User. Each has a custom manager limiting the queryset to items belonging to a particular Group. Specifically, there's a PressUser which is any user belonging to the "Press" group and StaffUser which is any user in any other group than "Press".
The issue is that when I add 'groups' to list_filters on my StaffUsers modeladmin, the resulting filter options are every group available, including "Press", and not just groups available to StaffUsers.
I've research a bit online and came up with a custom filterspec that should produce the behavior I want, but the problem is that the User model's 'groups' attribute is actually a related_name applied from the Group model. As a result, I can't attach my filterspec to 'groups' in my proxy model.
Is there any other way to apply the filterspec? Alternatively, is there a better approach to filtering the items returned by the default filterspec?
So, I was able to solve my own problem. For those that might run into a similar situation, here are the steps:
The approach I took is to modify the change_list.html template and manually filter out the items I didn't want to be included. There's quite a number of changes to make, though.
First, add a changelist_view method to your ModelAdmin:
# myproject/account/admin.py
class StaffUserAdmin(models.ModelAdmin):
...
def changelist_view(self, request, extra_context=None):
groups = Group.objects.exclude(name__in=['Press',]).values_list('name')
extra_context = {
'groups': [x[0] for x in groups],
}
return super(StaffUserAdmin, self).changelist_view(request,
extra_context=extra_context)
Basically, all we're doing here is passing in the filtered list of Groups we want to use into the context for the template.
Second, create a change_list.html template for your app.
# myproject/templates/admin/auth/staffuser/change_list.html
{% extends "admin/change_list.html" %}
{% load admin_list %}
{% load i18n %}
{% load account_admin %}
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}
{% ifequal spec.title 'group' %}
{% admin_list_group_filter cl spec groups %}
{% else %}
{% admin_list_filter cl spec %}
{% endifequal %}
{% endfor %}
</div>
{% endif %}
{% endblock filters %}
This one deserves a little explanation. First, the template tag loads: admin_list is used for the default Django template tag responsible for rendering the filters, admin_list_filter, i18n is used for trans, and account_admin is for my custom template tag (discussed in a sec), admin_list_group_filter.
The variable spec.title holds the title of the field that's being filtered on. Since I'm trying to alter how the Groups filter is displayed, I'm checking if it equals 'groups'. If it does, then I use my custom template tag, otherwise, it falls back to the default Django template tag.
Third, we create the template tag. I basically just copied the default Django template tag and made the necessary modifications.
# myproject/account/templatetags/account_admin.py
from django.template import Library
register = Library()
def admin_list_group_filter(cl, spec, groups):
return {'title': spec.title, 'choices' : list(spec.choices(cl)), 'groups': groups }
admin_list_group_filter = register.inclusion_tag('admin/auth/group_filter.html')(admin_list_group_filter)
The only things that I've changed here are adding a new argument to the method called 'groups' so I can pass in my filtered list of groups from before, as well as adding a new key to the dictionary to pass that list into the context for the template tag. I've also changed the template the tag uses to a new one that we're about to create now.
Fourth, create the template for the template tag.
# myproject/templates/admin/auth/group_filter.html
{% load i18n %}
<h3>{% blocktrans with title as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% for choice in choices %}
{% if choice.display in groups %}
<li{% if choice.selected %} class="selected"{% endif %}>
{{ choice.display }}</li>
{% endif %}
{% endfor %}
</ul>
No big surprises here. All we're doing is putting all the pieces together. Each choice is a dictionary with all the values needed to construct the filter link. Specifically, choice.display holds the actual name of the instance that will be filtered by. Obviously enough, I've set up a check to see if this value is in my filtered list of groups I want to show, and only render the link if it is.
So, it's a bit involved but works remarkably well. Just like that, you have a list of filters that is exactly what you want instead of the default ones generated by Django.
I'm going to tell you off the bat that I've never done this before myself, so take it with a grain of salt.
What I'd suggest would be to override get_changelist on your ModelAdmin, to return a custom ChangeList class, which you can define somewhere in your admin module.
Your custom ChangeList class would simply override get_filters, so you can map your custom FilterSpec for the group field.
Another thing that might interest you are patches from the feature request ticket for specifying custom filter specs. The latest patch doesn't work for Django 1.3rc1 yet, although #bendavis78 recently posted that he's working on a new one, but depending on your version of Django it may apply cleanly.
It looks like it barely missed the cut to get included into the 1.3 milestone, so I figure it's going to make it into the trunk as soon as work beings on Django 1.4.

Categories