django admin inline many to many custom fields
Asked Answered
R

3

17

Hi I am trying to customize my inlines in django admin.

Here are my models:

class Row(models.Model):
    name = models.CharField(max_length=255)

class Table(models.Model):
    rows = models.ManyToManyField(Row, blank=True)
    name = models.CharField(max_length=255)

    def __unicode__(self):
        return self.name

and my admin:

class RowInline(admin.TabularInline):
    model = Table.rows.through
    fields = ['name']


class TableAdmin(admin.ModelAdmin):
    inlines = [
        RowInline,
    ]
    exclude = ('rows',)

However I get this error

ImproperlyConfigured at /admin/table_app/table/1/

'RowInline.fields' refers to field 'name' that is missing from the form.

How is that possible ?

Rammer answered 6/11, 2013 at 9:3 Comment(5)
change model = Table.rows.through to model = RowSolemn
does not work Row has no foreign key to TableRammer
I'm coming across the exact same problem. I added a bounty to hopefully give it some more attention.Hocus
The RowInline class specifies its model as Table.rows.through (the autogenerated intermediate model for the Table.rows many-to-many relation) and then asks for its name field — but the intermediate model has no such field, it only has two foreign keys: one to Table (named from_) and one to Row (named to). TabularInline (or indeed any of the *Inline classes) will not be able to handle your needs; you'll likely need to implement a custom subclass.Azaleah
I put an updated question more inline with what I'm doing Here: #21563145Hocus
I
36
class RowInline(admin.TabularInline):
    model = Table.rows.through
    fields = ['name']

This presents a problem because Table.rows.through represents an intermediate model. If you would like to understand this better have a look at your database. You'll see an intermediate table which references this model. It is probably named something like apname_table_rows. This intermeditate model does not contain the field, name. It just has two foreign key fields: table and row. (And it has an id field.)

If you need the name it can be referenced as a readonly field through the rows relation.

class RowInline(admin.TabularInline):
    model = Table.rows.through
    fields = ['row_name']
    readonly_fields = ['row_name']

    def row_name(self, instance):
        return instance.row.name
    row_name.short_description = 'row name'


class TableAdmin(admin.ModelAdmin):
    inlines = [
        RowInline,
    ]
    exclude = ('rows',)
Inearth answered 5/2, 2014 at 7:12 Comment(5)
That seems like a great idea but it's not working for me. I even made the function return simply "test" to rule out problems with the instance but it still can't seem to find the field.Hocus
Can you post a stack trace?Inearth
That is a good approach for only displaying field values. readonly_fields only displayed as a label, not as a form field. So django can get the value and display it. But you can not use this approach to insert or update data through inlines.Malo
Edited the answer to have both fields and readonly_fields, which is what worked for me using recent django.Satterfield
Exactly what i was looking for. By showing only read only fields, we can prevent loading the whole table in <select> in inlinesEffeminacy
M
9

Django can not display it as you expected. Because there is an intermediary join table which joins your tables. In your example:

admin.py:

class RowInline(admin.TabularInline):
    model = Table.rows.through  # You are not addressing directly Row table but intermediary table
    fields = ['name']

As the above note, model in RowInline addressing following table in your database, not Your Row table and model

table: your-app-name_table_row
--------------------------------
id       | int not null
table_id | int
row_id   | int

You can think it like there is an imaginary table in your model that joins the two tables.

class Table_Row(Model):
    table = ForeignKey(Table)
    row = ForeignKey(Row)

So if you edit your Inline as following

class RowInline(admin.TabularInline):
    model = Table.rows.through  # You are not addressing directly Row table but intermediary table
    fields = ['row', 'table']

you will not see any error or exception. Because your model in RowInline addresses an intermediary table and that table do have those fields. Django can virtualize the imaginary table Table_Row up to here and can handle this.

But we can use relations in admin, with using __. If your code do have a ForeignKey relation instead of ManyToManyField relation, then following be valid in your admin

 class Row(models.Model):
    name = models.CharField(max_length=255)

class Table(models.Model):
    rows = models.ForeignKey(Row, blank=True)
    name = models.CharField(max_length=255)

    def __unicode__(self):
        return self.name

and your admin:

class RowInline(admin.TabularInline):
    model = Table
    fields = ['rows__name']

Because you will have real Models and djnago can evaluate __ relation on them

But if you try that in your structure:

 class Row(models.Model):
    name = models.CharField(max_length=255)

class Table(models.Model):
    rows = models.ManyToManyField(Row, blank=True)
    name = models.CharField(max_length=255)

    def __unicode__(self):
        return self.name

and your admin:

class RowInline(admin.TabularInline):
    model = Table.rows.through 
    fields = ['row__name']

it will raise Exception! Because you do not have real table in your model, and django can not evaluate __ relations on virtual models it designs on top of its head.

Conclusion:

In your Inlines addressing ManyToMany relations, you are dealing with an imaginary intermediary model and you can not use fields orexclude attributes on that because your imaginary model do not have those fields and django can not handle relations ober that imaginary table. Following will be acceptable

class RowInline(admin.TabularInline):
    model = Table.rows.through 
    # No fields or exclude declarations in here

and django will display combo boxes for your virtual intermediary table options and add a fancy green + sign to add new records, but you will can not have inline fields to add new records directly to your database within the same single page. Djnago can not handle this on a single page.

You can try creating real intermediary table and show it using through, but thats totally a longer job and I do not test it to see its results.

Update: There is also the reason why django do not let something like that. Consider following:

          Table   | Table_Row |   Row
       -----------+-----------+---------
Start      5      |           |          
Step 1     5      |           |    1
Step 2     5      |    5-1    |    1  

At the beginning, You have a table with no related Rows, you want to add a row to the table... For joining a row with a table, you must first create a row so you execute step 1. After You do create your row, you can create Table_Row record to join these two. So in contains more than a single database insertion. Django crew may avoid such usage since it contains multiple inserts and operation is related more tables.

But this is just an assumption on the reason of the behavior.

Malo answered 5/2, 2014 at 12:31 Comment(0)
S
0

In your admin.py try this

    class RowInline(admin.TabularInline):
        model = Table.rows.through
        list_display = ('name',)


    class TableAdmin(admin.ModelAdmin):
        inlines = [
            RowInline,
            ]
        readonly_fields = ('rows',)
Skell answered 5/2, 2014 at 10:29 Comment(1)
This solved my problem for when in the admin tool I don't want to display the original table and just the through relationship. Thanks!Gaily

© 2022 - 2024 — McMap. All rights reserved.