django:many-to-many relationships and admin

A few weeks ago I started playing aroung with django Web framework in order to create some cool web applications. I was quite lucky as these days django version 1 was released, full of new functionalities and goodies that will definitely make more django sites appear!

Learning django proved to be an easy going experience, as most things are quite well documented and there's a healthy and enthusiastic community willing to help. I'm writing this post to assist other people face the following problem: how to enable edits on both sides on the admin site, for ManyToManyField models. On django-user I see it's quite a frequent question over there, in fact I have myself posted for this issue before I resulted in this solution!



Let's start with an example. Say you have two simple models, Software and Category. A software can belong to many categories, and a category can have many software, so you have a Many To Many relationship:

#models.py
class Software(models.Model):
    name = models.CharField(max_length=50)
    categories = models.ManyToManyField(Category, blank=True)

class Category(models.Model):
    name = models.CharField(max_length=50)


Now if you register these models on admin.py and visit the admin panel, when you create (or edit) a Software object, you get categories it belongs to, and can add the object to more categories, or remove it from existing ones. Nothing unusual here, just the powerful and lovely django admin interface :)

On the other side, when you create/edit a Category object, you might also want to add what Software belongs there, or remove existing entries. This is not possible at the moment out of the box, because you can have the objects of the ManyToMany relationship only on the side that contains the actual reference. If you're working on the django InteractiveConsole you can add/remove software objects on a category object (in this example) through the attribute software_set.all(). So I guess for django newcomers it might be frustrating why you can't do it on the admin, on the other side of the Many To Many relationship (at least it has been for myself)!

Since the model has all the information we need for a Many To Many relationship, we only have to find a way to pass it to the related form. In order to do so, we will use a forms.ModelForm to build the form for our model and add a forms.ModelMultipleChoiceField that represents the reverse relation.


In our Software-Category example this is:
 
#admin.py
#set up ModelForm for Category

class CategoryAdminForm(forms.ModelForm):
    software_set = forms.ModelMultipleChoiceField(label='...', queryset=Software.objects.all(), required=False, help_text='...')

    class Meta:
        model = Category

#set up ModelAdmin for Category
class CategoryAdmin(admin.ModelAdmin):
    form = CategoryAdminForm

    fields = ['name', 'software_set']

    def save_model(self, request, obj, form, change):  
        obj.software_set.clear()
        for software in form.cleaned_data['software_set']:
            obj.software_set.add(software)
        obj.save()

    def get_form(self, request, obj=None, **kwargs):
        if obj:
            self.form.base_fields['software_set'].initial = obj.software_set.all()
        return super(CategoryAdmin, self).get_form(request, obj)


#finally register Category
admin.site.register(Category, CategoryAdmin)

save_model() is called when Save button is pressed and makes sure we save whatever values we select.
get_form() sets the initial values for our custom widget with the existing values (if there are any). If we don't set the initial values, the widget will appear with no values selected, so depending on our code in save_model() we might lose any existing values!

Thus we can now edit many-to-many relationships from both sides on the admin, which is pretty handy in some cases!

As I don't think this is the only solution, I would be glad to hear of others!

 

 



Comments:

while setting initial values inside get_form, I think it's better to do some casting to list, since this is the type that might be expected in other parts:

self.form.base_fields['software_set'].initial = list(obj.software_set.all())

Posted by Markos on October 21, 2008 at 05:43 PM EEST #

Post a Comment:
  • HTML Syntax: NOT allowed