priestc
priestc

Reputation: 35170

raw_id_fields for modelforms

I have a modelform which has one field that is a ForeignKey value to a model which as 40,000 rows. The default modelform tries to create a select box with 40,000 options, which, to say the least is not ideal. Even more so when this modelform is used in a formset factory!

In the admin, this is easiely avoidable by using "raw_id_fields", but there doesn't seem to be a modelform equivalent. How can I do this?

Here is my modelform:

class OpBaseForm(ModelForm):

    base = forms.CharField()

    class Meta:
        model = OpBase
        exclude = ['operation', 'routes']
        extra = 0
        raw_id_fields = ('base', )   #does nothing

The first bolded line works by not creating the huge unwieldy selectbox, but when I try to save a fieldset of this form, I get the error: "OpBase.base" must be a "Base" instance. In order for the modelform to be saved, 'base' needs to be a Base instance. Apparently, a string representation of a Base primary key isn't enough (at least not automatically). I need some kind of mechanism to change the string that is given my the form, to a Base instance. And this mechanism has to work in a formset. Any ideas? If only raw_id_fields would work, this would be easy as cake. But as far as I can tell, it only is available in the admin.

Upvotes: 17

Views: 11886

Answers (3)

chriscauley
chriscauley

Reputation: 21001

To expand on Voltaire's comment above, the django 1.4 solution is:

from django.contrib import admin
admin.autodiscover()

from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django import forms

from .models import Post, Photo

class PostForm(forms.ModelForm):
    photo = forms.ModelChoiceField(
        Photo.objects.all(),
        widget=ForeignKeyRawIdWidget(Post._meta.get_field("photo").rel,admin.site)
    )

And the only additional javascript you should need is:

<script type="text/javascript" src="/static/admin/js/admin/RelatedObjectLookups.js"></script>

The important thing here is that you call autodiscover on the admin, otherwise your RawIdWidget won't have a link. Also the ModelChoiceField requires a queryset, which isn't actually used. ModelChoiceField is preferable to CharField because CharField doesn't validate properly (tries to save the id rather than looking up the Photo instance).

Update

Django 2.0 deprecated Field.rel in favor of Field.remote_field.

That widget= line will now need to be:

widget=ForeignKeyRawIdWidget(Post._meta.get_field("photo").remote_field, admin.site),

Upvotes: 14

Matt
Matt

Reputation: 141

You can also use the entire raw_id_field admin widget, complete with the handy js popup search that the admin page has. You don't even need a model form. Here's how:

import string
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django import forms
from models import MyModel

# Have to subclass widget b/c 
# django hardcodes a relative path to Admin Root URL: ../../..
class HardcodedURLForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
    def render(self, *args, **kwargs):
        original_render = super(HardcodedURLForeignKeyRawIdWidget, 
            self).render(*args, **kwargs)
        ADMIN_ROOT_URL = "/admin/"
        return string.replace(original_render,"../../../", ADMIN_ROOT_URL)


class FieldLookupForm(forms.Form):
    my_foreignkey_field = forms.CharField(max_length=10,
        widget=HardcodedURLForeignKeyRawIdWidget(
            MyModel._meta.get_field("foreignkey_field").rel))

Add the relevant admin js to your template, and viola

{% block header %}
<script type="text/javascript">window.__admin_media_prefix__ = "/static/admin/";</script>
<script type="text/javascript" src="/admin/jsi18n/"></script>
<script type="text/javascript" src="/static/admin/js/core.js"></script>
<script type="text/javascript" src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/admin/js/actions.min.js"></script>
{% endblock %}

Upvotes: 14

Daniel Roseman
Daniel Roseman

Reputation: 599610

You need to change the widget for the base field, not the field type. I think this would work:

class OpBaseForm(ModelForm):
    base = forms.ModelChoiceField(queryset=Base.objects.all(), 
                                  widget=forms.TextInput)

    class Meta:
        model = OpBase
        ... etc... 

Upvotes: 13

Related Questions