views:

801

answers:

3

The Django admin happily supports many-to-one and many-to-many relationships through an HTML <SELECT> form field, allowing selection of one or many options respectively. There's even a nice Javascript filter_horizontal widget to help.

I'm trying to do the same from the one-to-many side through related_name. I don't see how it's much different from many-to-many as far as displaying it in the form is concerned, I just need a multi-select SELECT list. But I cannot simply add the related_name value to my ModelAdmin-derived field list.

Does Django support one-to-many fields in this way?

My Django model something like this (contrived to simplify the example):

class Person(models.Model):
    ...
    manager = models.ForeignKey('self', related_name='staff',
                                null=True, blank=True, )

From the Person admin page, I can easily get a <SELECT> list showing all possible staff to choose this person's manager from. I also want to display a multiple-selection <SELECT> list of all the manager's staff.

I don't want to use inlines, as I don't want to edit the subordinates details; I do want to be able to add/remove people from the list.

(I'm trying to use django-ajax-selects to replace the SELECT widget, but that's by-the-by.)

A: 

This might end up being one of those limitations. Here's what I managed to come up with. It's possible to create a custom Form and pass that to the admin site to use for rendering a page. However, what I couldn't get was for it to properly show the initial value properly. Perhaps someone better at Forms can come along with some uber-cool-secret-meta API that I don't know about and fix this.

models.py

class Person(models.Model):
    # ...
    manager = models.ForeignKey('self', related_name='staff', null=True, blank=True)

    def manager_staff(self):
        return self.manager.staff.all()

admin.py

class PersonAdminForm(forms.ModelForm):    
    manager_staff = forms.ModelMultipleChoiceField(
                        initial='manager_staff',       # Can't get this to work
                        queryset=Person.objects.all(),
                        required=False,
                        )

    class Meta:
        model = Person

class PersonAdmin(admin.ModelAdmin):    
    form = PersonAdminForm

    def save_model(self, request, obj, form, change):
        for id in form.data.getlist('manager_staff'):
            # This is kind of a weak way to do this, but you get the idea...
            p = Person.objects.get(id=id)
            p.manager = obj.manager
            p.save()

        super(PersonAdmin, self).save_model(request, obj, form, change)

admin.site.register(Person, PersonAdmin)
T. Stone
+1  A: 

you can set the initial values in the init method of the form.

Refer to them as self.fields['manager_staff'].initial.

Like so:

class PersonAdminForm(forms.ModelForm):    
    manager_staff = forms.ModelMultipleChoiceField(
                        queryset=Person.objects.all(),
                        required=False,
                        )

    def __init__(self, *args, **kwargs):
        super(PersonAdminForm, self).__init__(*args, **kwargs)
        if self.instance.id is not None:
            selected_items = [ values[0] for values in Person.objects.filter(
                               #whatever your filter criteria is
                              ) ]
            self.fields['manager_staff'].initial = selected_items
nicoechaniz
A: 

Here's what I came up with based on the answers from T. Stone and nicoechaniz. The initial data is provided in much the same way as nicoechaniz does it. For the saving you have to be careful do deal with the case of a completely new Person as well as editing an existing Person. That's what makes the save() method complicated, depending on non-publicized parts of the ModelForm API. Really this should be built into Django. I imagine it's a commonly sought after feature.

class PersonAdminForm(forms.ModelForm):
    staff = forms.ModelMultipleChoiceField(
                        queryset=Person.objects.all(),
                        required=False,
                        )
    class Meta:
        model = Person

    def __init__(self, *args, **kwargs):
        super(PersonAdminForm, self).__init__(*args,**kwargs)

        if self.instance.pk is not None:
            self.initial['staff'] = [values[0] for values in self.instance.staff.values_list('pk')]

    def save(self, commit=True):
        instance = super(PersonAdminForm, self).save(commit)

        def save_m2m():
            instance.staff = self.cleaned_data['staff']

        if commit:
            save_m2m()
        elif hasattr(self, 'save_m2m'):
            save_old_m2m = self.save_m2m

            def save_both():
                save_old_m2m()
                save_m2m()

            self.save_m2m = save_both
        else:
            self.save_m2m = save_m2m

        return instance

    save.alters_data = True

class PersonAdmin(admin.ModelAdmin):
    form = PersonAdminForm
Rowan
Bug filed: http://code.djangoproject.com/ticket/13369
Rowan