Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Admin - Displaying Intermediary Fields for M2M Models

Tags:

python

django

m2m

We have a Django app which contains a list of newspaper articles. Each article has a m2m relationship with both a "spokesperson", as well as a "firm" (company mentioned in the article).

At the moment, the Add Article page for creating new Articles is quite close to what we want - it's just the stock Django Admin, and we're using filter_horizontal for setting the two m2m relationships.

The next step was to add a "rating" field as an intermediary field on each m2m relationship.

So, an example of our models.py

class Article(models.Model):
    title = models.CharField(max_length=100)
    publication_date = models.DateField()
    entry_date = models.DateField(auto_now_add=True)
    abstract = models.TextField() # Can we restrict this to 450 characters?
    category = models.ForeignKey(Category)
    subject = models.ForeignKey(Subject)
    weekly_summary = models.BooleanField(help_text = 'Should this article be included in the weekly summary?')
    source_publication = models.ForeignKey(Publication)
    page_number = models.CharField(max_length=30)
    article_softcopy = models.FileField(upload_to='article_scans', null=True, blank=True, help_text='Optionally upload a soft-copy (scan) of the article.')
    url = models.URLField(null=True, blank=True, help_text = 'Enter a URL for the article. Include the protocl (e.g. http)')
    firm = models.ManyToManyField(Firm, null=True, blank=True, through='FirmRating')
    spokesperson = models.ManyToManyField(Spokeperson, null=True, blank=True, through='SpokespersonRating')

    def __unicode__(self):
        return self.title

class Firm(models.Model):
    name = models.CharField(max_length=50, unique=True)
    homepage = models.URLField(verify_exists=False, help_text='Enter the homepage of the firm. Include the protocol (e.g. http)')

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ['name']

class Spokeperson(models.Model):
    title = models.CharField(max_length=100)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.first_name + ' ' + self.last_name

    class Meta:
        ordering = ['last_name', 'first_name']

class FirmRating(models.Model):
    firm = models.ForeignKey(Firm)
    article = models.ForeignKey(Article)
    rating = models.IntegerField()

class SpokespersonRating(models.Model):
    firm = models.ForeignKey(Spokesperson)
    article = models.ForeignKey(Article)
    rating = models.IntegerField()

The issue here is that once we change our Firm and Spokesperson field to "through" and use intermediaries, our Add Article page no longer has a filter_horizontal control to add Firms/Spokeperson relationships to the Article - they completely disappear. You can't see them at all. I have no idea why this is.

I was hoping for there to be some way to keep using the cool filter_horizontal widget to set the relationship, and somehow just embed another field below that for setting the rating. However, I'm not sure how to do this whilst still leveraging off the Django admin.

I saw a writeup here about overriding a single widget in Django admin:

http://www.fictitiousnonsense.com/archives/22

However, I'm not sure if that method is still valid, and I'm not sure about applying it to here, with a FK to a intermediary model (it's basically an inline then?).

Surely there's an easy way of doing all this?

Cheers, Victor

like image 833
victorhooi Avatar asked Feb 26 '23 19:02

victorhooi


1 Answers

The problem is that the admin's method formfield_for_manytomany in django.contrib.admin.options doesn't return a form field for manytomany fields with an intermediary model! http://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L157

You would have to override this method in your ModelAdmin:

def formfield_for_manytomany(self, db_field, request=None, **kwargs):
    """
    Get a form Field for a ManyToManyField.
    """
    # If it uses an intermediary model that isn't auto created, don't show
    # a field in admin.
    if not db_field.rel.through._meta.auto_created:
        return None    # return something suitable for your needs here!
    db = kwargs.get('using')

    if db_field.name in self.raw_id_fields:
        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
        kwargs['help_text'] = ''
    elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
        kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
like image 158
Bernhard Vallant Avatar answered Apr 29 '23 01:04

Bernhard Vallant