How to create block from list of choices in page admin, outputs list of model instance URLS?

1,038 views
Skip to first unread message

Brendan Hayward

unread,
Oct 19, 2016, 11:10:56 PM10/19/16
to Wagtail support
My goal: to give page creators the ability to output a list of courses available per faculty, where faculty is chosen from a list in the page admin.

I'm basically trying to create a block to put in a stream field which does this:
    1. user selects courselist block from stream panel
        - CourseListBlock inherits from ChoiceBlock or ChooserBlock
    2. user selects faculty from drop down list in block
    3. page outputs list in html of these URLs

Each URL already exists for each course, returned by award.get_absolute_url():

businesslist   = [award.get_absolute_url() for award in Award.objects.filter(school__name__contains='Business')]
educationlist    = [award.get_absolute_url() for award in Award.objects.filter(school__name__contains='Education')]
sciencelist = [award.get_absolute_url() for award in Award.objects.filter(school__name__contains='Science')]

In the shell, these list comprehensions output a list of URLs, exactly as I want them, eg:

((['/awards/doctor-philosophy-phd/',
   '/awards/master-arts/',
   '/awards/master-philosophy-mphil/',
   '/award/bachelor-science/',
))

award: what we call courses, or degrees here. Is a regular django model.
school: another name for faculty, or department. Is also a regular django model

I've tried these resources from SO so far:
from SO:

Overriding value_for_form and value_from_form in ChoiceBlock and ChooserBlock haven't helped me yet.
Has anyone done this yet?

Here's the gist of what I've got:

class CourseListBlock(blocks.ChoiceBlock): #or blocks.ChooserBlock, not sure
    choices = (('Business', 'Business'), ('Education', 'Education'), ('Science', 'Science'), ('Other faculties etc', 'Other faculties etc'))
    class Meta:
        verbose_name = 'Course List'
        icon = 'list-ul'

    def value_from_form(self, value):
        try:
            return [Award.get_absolute_url() for award in Award.objects.filter(school__name__contains=str(value))]
        except:
            return value

    def value_for_form(self, value)
        return value

class StandardPage(Page):
    body = StreamField([
        ...
        ('courselist', CourseListBlock(required=False)),
        ...
    ])
    ... the rest of the page class ...

Many thanks,
Brendan Hayward

Brendan Hayward

unread,
Oct 20, 2016, 8:10:51 PM10/20/16
to Wagtail support

Nikola Dang

unread,
Dec 6, 2016, 10:52:32 PM12/6/16
to Wagtail support
Is there any results from this? I encouter the same situation.

Matthew Westcott

unread,
Dec 14, 2016, 7:22:48 AM12/14/16
to wag...@googlegroups.com
Hi Brendan,

Better late than never... Wagtail 1.8 adds the ability to pass a callable as the `choices` parameter to a ChoiceBlock, so you'll be able to do this:

def get_school_choices():
return [(school.id, school.name) for school in School.objects.all()]

then in the StreamField definition:
ChoiceBlock(choices=get_school_choices)

Note that currently this only works when `choices` is passed to the ChoiceBlock constructor, not as a property of a subclass, so you can't write:

class CourseListBlock(ChoiceBlock):
choices = get_school_choices


For the second half of the problem, returning a list of course URLs, I suspect you don't really want to return that from value_from_form - that would mean that the list of course URLs is what gets stored in the database (and you'd have to somehow turn that back into a school ID when re-displaying the edit form). Instead, we probably want the stored value to be the school ID (which is what the get_school_choices code above does), and to perform the lookup of courses at the point of rendering the block on the page. This is best done with a `get_context` method:


def get_school_choices():
return [(school.id, school.name) for school in School.objects.all()]

class CourseListBlock(ChoiceBlock):
def get_context(self, value):
context = super(CourseListBlock, self).get_context(value)
context['award_urls'] = [award.get_absolute_url() for award in Award.objects.filter(school__id=value)]

class Meta:
template = 'myapp/blocks/course_list.html'
# this template will be used when you invoke {% include_block %} on the page template;
# you can refer to the 'award_urls' variable here

# in the StreamField definition:
CourseListBlock(choices=get_school_choices)


Cheers,
- Matt
> --
> You received this message because you are subscribed to the Google Groups "Wagtail support" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to wagtail+u...@googlegroups.com.
> To post to this group, send email to wag...@googlegroups.com.
> Visit this group at https://groups.google.com/group/wagtail.
> To view this discussion on the web, visit https://groups.google.com/d/msgid/wagtail/91f236de-d5c6-483b-8d00-03da5e701898%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Brendan Hayward

unread,
Dec 21, 2016, 5:53:26 AM12/21/16
to wag...@googlegroups.com
This is excellent, thanks Matt.
I only just moved our dev instance to 1.8 yesterday, so your email was a
few days head of me.

I did end up setting a property on the School and Awards models, and
referring to it in the view. It works nicely, but this might be better.

Brendan Hayward

unread,
Dec 21, 2016, 7:47:37 PM12/21/16
to Wagtail support
See Matthew Westcott's reply below for the update to ChoiceBlock in 1.8.

I sub-classed the ChooserBlock, here's what I did:

1. $django-admin startapp custom_utils

2. Create file custom_blocks.py  ( I have all my custom blocks in here, not just this CourseList one )

3. I created this custom block:

from wagtail.wagtailcore import blocks
from django import forms

class CourseBlock(blocks.ChooserBlock):
    target_model
= School  # Award is our name for a degree, or university course
    widget
= forms.Select

   
class Meta:
        icon
= 'list-ul'
       
template = 'course_block.html'
        verbose_name
= 'Course List'

4. I added this method to the target model `School`:

    def awardlist(self):
        awdlist
= OrderedDict()
       
for awd in Award.objects.order_by('aqf_level'):
            awdlist
.update({awd.name:awd.get_absolute_url()})
       
return awdlist

    The "@property" or "@cached_property" decorators can be used on this method if desired

5. Then in the template for CourseBlock, "course_block.html", use the method like this:
   
{% load wagtailcore_tags %}

<div class="courselist">
 
<ul>
   
{% for awardname, awardurl in value.awardlist.items %}
   
<li><a class="ac-link" href="{{ awardurl }}">{{ awardname }}</a></li>
   
{% endfor %}
 
</ul>
</
div>

6. Finally, add the block to whichever Page model you use:

class CommonPage(Page):
    body
= StreamField([
       
('heading',       blocks.CharBlock(classname='full title', icon='openquote')),
       
('paragraph',     blocks.RichTextBlock(classname='full')),
       
('image',         ImageChooserBlock()),
       
('document',      DocumentChooserBlock(required=False)),
       
('courselist',    CourseBlock(required=False)),
       
... other blocks ...,
   
])
   
... rest of the model stuff ...

And all that gives me a nice CourseList button in the Streamfield editor, and outputs a nice list of model instances wherever people want it.
Reply all
Reply to author
Forward
0 new messages