django admin many-to-many intermediary models using through= and filter_horizontal

There are ways to do this

  • As provided by @obsoleter in the comment below : set QuestionTagM2M._meta.auto_created = True and deal w/ syncdb matters.
  • Dynamically add date_added field to the M2M model of Question model in models.py

    class Question(models.Model):
        # use auto-created M2M model
        tags = models.ManyToMany(Tag, related_name='questions')
    
    
    # add date_added field to the M2M model
    models.DateTimeField(auto_now_add=True).contribute_to_class(
             Question.tags.through, 'date_added')
    

    Then you could use it in admin as normal ManyToManyField.
    In Python shell, use Question.tags.through to refer the M2M model.

    Note, If you don't use South, then syncdb is enough; If you do, South does not like this way and will not freeze date_added field, you need to manually write migration to add/remove the corresponding column.

  • Customize ModelAdmin:

    1. Don't define fields inside customized ModelAdmin, only define filter_horizontal. This will bypass the field validation mentioned in Irfan's answer.
    2. Customize formfield_for_dbfield() or formfield_for_manytomany() to make Django admin to use widgets.FilteredSelectMultiple for the tags field.
    3. Customize save_related() method inside your ModelAdmin class, like

def save_related(self, request, form, *args, **kwargs):
    tags = form.cleaned_data.pop('tags', ())
    question = form.instance
    for tag in tags:
        QuestionTagM2M.objects.create(tag=tag, question=question)
    super(QuestionAdmin, self).save_related(request, form, *args, **kwargs)
  • Also, you could patch __set__() of the ReverseManyRelatedObjectsDescriptor field descriptor of ManyToManyField for date_added to save M2M instance w/o raise exception.

From https://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-intermediary-models

When you specify an intermediary model using the through argument to a ManyToManyField, the admin will not display a widget by default. This is because each instance of that intermediary model requires more information than could be displayed in a single widget, and the layout required for multiple widgets will vary depending on the intermediate model.

However, you can try including the tags field explicitly by using fields = ('tags',) in admin. This will cause this validation exception

'QuestionAdmin.fields' can't include the ManyToManyField field 'tags' because 'tags' manually specifies a 'through' model.

This validation is implemented in https://github.com/django/django/blob/master/django/contrib/admin/validation.py#L256

        if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
            raise ImproperlyConfigured("'%s.%s' "
                "can't include the ManyToManyField field '%s' because "
                "'%s' manually specifies a 'through' model." % (
                    cls.__name__, label, field, field))

I don't think that you can bypass this validation unless you implement your own custom field to be used as ManyToManyField.


The docs may have changed since the previous answers were posted. I took a look at the django docs link that @Irfan mentioned and it seems to be a more straight forward then it used to be.

Add an inline class to your admin.py and set the model to your M2M model

class QuestionTagM2MInline(admin.TabularInline):
    model = QuestionTagM2M
    extra = 1

set inlines in your admin class to contain the Inline you just defined

class QuestionAdmin(admin.ModelAdmin):
    #...other stuff here
    inlines = (QuestionTagM2MInline,)

Don't forget to register this admin class

admin.site.register(Question, QuestionAdmin)

After doing the above when I click on a question I have the form to do all the normal edits on it and below that are a list of the elements in my m2m relationship where I can add entries or edit existing ones.