Populating choices fields from model

3,135 views
Skip to first unread message

Joss Ingram

unread,
Aug 14, 2014, 9:59:57 AM8/14/14
to wag...@googlegroups.com
Hi,

I'd like a drop down field in the admin on a certain page that gets it's values from the model, another class.

What's the best way to do this? I have one working that populates from a JSON feed but when I set a function to populate a choices tuple from the model, it says

relation "taggit_tag" does not exist
LINE 1: ...NCT "taggit_tag"."slug", "taggit_tag"."name" FROM "taggit_ta...

Trying to get a dropdown menu of used tags on the site.

currently have :

def get_taglist():
    taglist = NewsPage.objects.all().only("tags__name").order_by('tags__name')     
        
    tagchoices = taglist.values_list('tags__name', 'tags__name').distinct()

    return tagchoices

which is after the NewsPage in the model?

Probably I'm doing something stupid in django terms!

Thanks

Joss

Joss Ingram

unread,
Aug 14, 2014, 11:11:46 AM8/14/14
to wag...@googlegroups.com
ok, I've just realized I don't want to be doing it this way. The choices field will only get populated when it creates the database, right? 

Is there anyway to have a dynamically populated drop down menu in wagtail that get's it's values when the admin page is loaded?

Matthew Westcott

unread,
Aug 15, 2014, 6:03:29 AM8/15/14
to wag...@googlegroups.com
Hi Joss,

There's a relevant comment in the Django docs at the end of https://docs.djangoproject.com/en/1.6/ref/models/fields/#choices :

> Finally, note that choices can be any iterable object – not necessarily a list or tuple. This lets you construct choices dynamically. But if you find yourself hacking choices to be dynamic, you’re probably better off using a proper database table with a ForeignKey. choices is meant for static data that doesn’t change much, if ever.

To get an iterable object that returns the up-to-date list every time you access it, you could do something like this (I'm sure there must be a less long-winded way of turning a function into an iterable in Python, but I can't immediately find it):

class TagListIterable(object):
def __iter__():
taglist = NewsPage.objects.all().only("tags__name").order_by('tags__name')
tagchoices = taglist.values_list('tags__name', 'tags__name').distinct()
return tagchoices.__iter__()

tag_list = TagListIterable()

Then in your model, the field declaration would look like:
tag = models.CharField(max_length=255, choices=tag_list)


However, I would second the suggestion of using a foreign key rather than a 'choices' list. In this case you would make your 'tag' field a ForeignKey to 'taggit.Tag' - possibly with a limit_choices_to argument set so that it only returns tags that are used on NewsPage. Then, if you give it an ordinary FieldPanel in the content_panels list, it will appear as a dropdown.

Cheers,
- Matt

Joss Ingram

unread,
Aug 15, 2014, 7:07:19 AM8/15/14
to wag...@googlegroups.com
Thanks Matt, that's given me a lot to think about. The values will change often so I think I'll try the separate table method!

Joss

Joss Ingram

unread,
Aug 19, 2014, 8:05:57 AM8/19/14
to wag...@googlegroups.com
Matt,

I'm trying to go with the foreignkey method, i'm using this 
    subject_news_tag = models.ForeignKey('taggit.Tag',blank=True, null=True, on_delete=models.SET_NULL) as a new field in a class for a certain page in my model.

also tried     subject_news_tag = models.ForeignKey(Tag,blank=True, null=True, on_delete=models.SET_NULL) as a new field in a class for a certain page in my model.


but I'm getting this below when I apply migrations :

column "subject_tag_id" cannot be cast automatically to type integer
HINT:  Specify a USING expression to perform the conversion.

I don't really want to be messing with the migrations, I'm doing something wrong obviously but any ideas?

Thanks

Joss

Joss Ingram

unread,
Aug 19, 2014, 9:22:50 AM8/19/14
to wag...@googlegroups.com
sorry, ignore my last post, i was doing something stupid!

Joss Ingram

unread,
Aug 19, 2014, 11:20:40 AM8/19/14
to wag...@googlegroups.com
I've got it working now, so thanks Matt,but I can't seem to get it to limit on only tags used on news pages though, is that def possible? Can't find any example on line.

Ive got     

    news_tag = models.ForeignKey('taggit.Tag',blank=True, null=True, on_delete=models.SET_NULL,limit_choices_to=Q(tag__id=newspage__tags__tag_id))

but 'newspage__tags__tag_id' is not defined

In the absence of any online example, I'm just guessing really being a django dunce

Joss

Matthew Westcott

unread,
Aug 19, 2014, 6:06:07 PM8/19/14
to wag...@googlegroups.com
Hi Joss,
I think the best way to do this would be to follow the 'tagged item' relation backwards. Using the wagtaildemo models as an example: you can retrieve all tags used on blog pages with:
Tag.objects.filter(demo_blogpagetag_items__isnull=False).distinct()
which would make the limit_choices_to clause:
limit_choices_to={'demo_blogpagetag_items__isnull': False}
- although I'm not sure whether this will return duplicate results, or whether the lack of distinct() clause will somehow fix itself. If that fails, the following *might* work, although at this point my brain starts to tie itself in knots!
limit_choices_to=Q(id__in=Tag.objects.filter(demo_blogpagetag_items__isnull=False).values_list('id', flat=True))

(In the above examples, you'll need to replace 'demo' and 'blogpagetag' with your app and model name respectively - if in doubt, you can find out the relation name by running the following within ./manage.py shell:
from taggit.models import Tag
dir(Tag.objects.first())
- and looking for something in that list that references 'newspage'.)


It's interesting to note that Django 1.7 adds the ability to pass a callable function as the limit_choices_to parameter <https://docs.djangoproject.com/en/1.7/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to>, which suggests to me that someone else ran into the same kind of problem, and fixed it by patching Django. If only that had happened a version or two earlier!

- Matt

Joss Ingram

unread,
Aug 20, 2014, 7:29:08 AM8/20/14
to wag...@googlegroups.com
Matt,

The first one does return duplicates, but the second one works a treat! Thanks so much!

Joss

John Ryding

unread,
Jun 16, 2015, 11:08:53 AM6/16/15
to wag...@googlegroups.com
So this post helped me a lot with a similar use case I encountered w/r/t dynamically populating a Select form in Wagtail. My use case was that whenever an edit page was loaded, I wanted to look at the file system to retrieve the list of templates in a directory and display them as choices for my model. On top of this, this folder may be updated at any point, so I wanted to look at the file system for every request.

To achieve part 1 of this, I had to implement a custom iterable method like Matthew noted in his reply. However, that did not fix the entire issue as the __iter__ function continued to only be called once.

```
class FileListIterable(object): 
    def __iter__(): 
        files = [ 
            # retrieve list of files from directory
        ]
        return files.__iter__() 

file_list = FileListIterable() 

MyModel.content_panels = [
    TemplateFieldPanel('template', widget=Select(choices= file_list)) # still results in __iter__ being called only once.
]
```

To get the __iter__ function calling every single time, Matthew helped point out that I had to bypass the the Select constructor because Django forcibly casts the iterable into a list: https://github.com/django/django/blob/master/django/forms/widgets.py#L519-L522

The working code looks like:

```
class FileListIterable(object): 
    def __iter__(): 
        files = [ 
            # retrieve list of files from directory
        ]
        return files.__iter__() 

file_list = FileListIterable() 
templates_select = Select()
templates_select.choices = file_list

MyModel.content_panels = [
    TemplateFieldPanel('template', widget= templates_select) # now __iter__ is called every time.
]
```

Thanks for the help on this Matthew!

- John Ryding

Joss Ingram

unread,
Jun 16, 2015, 12:40:08 PM6/16/15
to wag...@googlegroups.com
Hi John,

I think your example might be useful for something I'm trying to do, but I can't figure out the 'TemplateFieldPanel' bit, i'm getting  name 'TemplateFieldPanel' is not defined, I can't find any reference to it in the Wagtail docs either.

Thanks

Joss

Joss Ingram

unread,
Jun 16, 2015, 12:43:43 PM6/16/15
to wag...@googlegroups.com
Ok, should it just be FieldPanel? and also the widget= bit will only work in 1.0> of wagtail?

Joss Ingram

unread,
Aug 5, 2015, 11:09:34 AM8/5/15
to Wagtail support
ok, in case this will help someone else, I got it working like this:

class SubjectsIterable(object): 
    def __iter__(self): 
        subs = (define your choices tuple here, mine was generating from some remote JSON)
        return subs.__iter__() 
        
subs_list = SubjectsIterable() 
templates_select = forms.Select()
templates_select.choices = subs_list

my field panel bit is :

    FieldPanel('subject_area', widget=templates_select),    

my field in the model is :

    subject_area = models.IntegerField(null=False)

Richard Allen

unread,
Mar 10, 2021, 8:23:19 AM3/10/21
to Wagtail support
Resurrecting a very old thread here, but this was the only place I found that helped me get dynamic dropdowns working so thanks to everyone, huge help :). 

Here was my version - I liked the result of defining the choice list in the field definition, it's a more obvious dropdown box and also doesn't populate the field until you select rather than going to the first option without any user input:

@register_snippet 
class ParentSnippet(TranslatableMixin, ClusterableModel): 
    code = models.CharField(blank=False, null=True, max_length=10) 
    title = models.CharField(blank=False, null=True, max_length=50)

class ChoiceListIterator(object): 
    def __iter__(self): 
        dropdown_list = list(ChoiceListClass.objects.values_list('code','title')) 
        return dropdown_list.__iter__() 

class ChildOrderable:
    p_key= ParentalKey( "ParentSnippet", related_name="child_items", ) 
    choice_list=ChoiceListIterator() 
    submenu_selector=Select() 
    submenu_selector.choices = choice_list 

    dropdown_test = models.CharField( blank=False, null=True, max_length=10, choices=choice_list ) 

    panels = [ FieldPanel("dropdown_test"), ]

This leaves the last challenge - access the instance of the parent snippet so that I can dynamically filter the list based on properties in that instance. ParentalKey is None until the oderable is saved - anyone know how to access the parent instance before that point?
Reply all
Reply to author
Forward
0 new messages