one-to-many inline select with django admin

Question:

I have a standard many-to-one relationship set up. There are a bunch of fields, but for our purposes here, the relevant model is:

class Class(models.Model):
    name = models.CharField(max_length=128)

class Student(models.Model):
    class = models.ForeignKey(Class)
    name = models.CharField(max_length=128)
    address = models.CharField(max_length=128)
    # ...etc

I created an admin, and it works great. it even automatically has the ability for me to set the Class when I am editing a Student. However, when I go to create/edit a Class, all I get is an input box for the name.

Is there a way to add a box/field where Students can be added as members of Class from the Class admin page? I can make a form inline, but that is to create new Students. I already have all my Students created and am just looking for a quick method to add multiple existing Students to different Class’.

Asked By: MrGlass

||

Answers:

There is! You want InlineModelAdmin (see InlineModelAdmin documentation here)

Sample code in brief:

class StudentAdminInline(admin.TabularInline):
    model = Student

class ClassAdmin(admin.ModelAdmin):
    inlines = (StudentAdminInline, )
admin.site.register(Class, ClassAdmin)
Answered By: Luke Sneeringer

Here is “custom form” solution as Luke Sneeringer suggested. Anyway, I’m suprised by absence of out-of-the-box Django solution to this (rather natural and probably common) problem. Am I missing something?

from django import forms
from django.db import models
from django.contrib import admin

class Foo(models.Model):
    pass

class Bar(models.Model):
    foo = models.ForeignKey(Foo)

class FooForm(forms.ModelForm):
    class Meta:
        model = Foo

    bars = forms.ModelMultipleChoiceField(queryset=Bar.objects.all())

    def __init__(self, *args, **kwargs):
        super(FooForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['bars'].initial = self.instance.bar_set.all()

    def save(self, *args, **kwargs):
        # FIXME: 'commit' argument is not handled
        # TODO: Wrap reassignments into transaction
        # NOTE: Previously assigned Foos are silently reset
        instance = super(FooForm, self).save(commit=False)
        self.fields['bars'].initial.update(foo=None)
        self.cleaned_data['bars'].update(foo=instance)
        return instance

class FooAdmin(admin.ModelAdmin):
    form = FooForm
Answered By: zag

You could also run the student names trough a second model so that they are a ForeignKey. For example after the original code post add:

class AssignedStudents(models.Model):
    assigned_to = models.ForeignKey(Class, on_delete=models.CASCADE)
    student = models.ForeignKey(Student, on_delete=models.CASCADE)

Then add the inline to the admin like Luke Sneeringer said. You end up with a drop down list that you can select the student names from. Although, there is still the option to create new students.

Answered By: Kris O

Probably, this will help:
I used the described approach, but changed methods save and save_m2m in the following way:

from django import forms
from django.db import models
from django.contrib import admin

class Foo(models.Model):
     pass

class Bar(models.Model):
     foo = models.ForeignKey(Foo)

class FooForm(forms.ModelForm):
    class Meta:
        model = Foo

    bars = forms.ModelMultipleChoiceField(queryset=Bar.objects.all())

    def __init__(self, *args, **kwargs):
        super(FooForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['bars'].initial = self.instance.bar_set.all()

    def save_m2m(self):
        pass

    def save(self, *args, **kwargs):
        self.fields['bars'].initial.update(foo=None)
        foo_instance = Foo()
        foo_instance.pk = self.instance.pk
        # Copy all other fields.
        # ... #
        foo_instance.save()
        self.cleaned_data['bars'].update(foo=instance)
        return instance

class FooAdmin(admin.ModelAdmin):
    form = FooForm
Answered By: Vladimir

If the intention is to have students exist independently from a class, and then be able to add or remove them from a class, then this sets up a different relationship:

  • Many students can be a part of one class.
  • One student can be a part of many classes

In reality this is describing a Many-To-Many relationship. Simply exchanging

class Student(models.Model):
    class = models.ForeignKey(Class) ...

for

class Student(models.Model):
    class = models.ManyToManyField(Class)... 

will give the desired effect in Django admin immediately

Django Many-to-many

Answered By: Zac Butko

In 2021, there is no straight solution to this problem.

What I did is using ManyToMany with symmetrical=False. Read more one Django official doc about symmetrical. Only in that case, you can select multiple entities in django-admin

parent_questions = models.ManyToManyField('self', related_name='parent_questions',
                                         blank=True, symmetrical=False)
Answered By: Jasurbek Nabijonov

Something that works for me in Django 3.0.9

In models.py

from django import forms
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)
    course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return self.name


class CourseForm(forms.ModelForm):

    students = forms.ModelMultipleChoiceField(
        queryset=Student.objects.all(), required=False
    )
    name = forms.CharField(max_length=100)

    class Meta:
        model = Student
        fields = ["students", "name"]

    def __init__(self, *args, **kwargs):
        super(CourseForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields["students"].initial = self.instance.student_set.all()

    def save_m2m(self):
        pass

    def save(self, *args, **kwargs):
        self.fields["students"].initial.update(course=None)
        course_instance = Course()
        course_instance.pk = self.instance.pk
        course_instance.name = self.instance.name
        course_instance.save()
        self.cleaned_data["students"].update(course=course_instance)
        return course_instance

In admin.py

from django.contrib import admin
from .models import Student, Course

class StudentAdmin(admin.ModelAdmin):
    pass

class CourseAdmin(admin.ModelAdmin):
    form = CourseForm

admin.site.register(Student, StudentAdmin)
admin.site.register(Course, CourseAdmin)
Answered By: Oliver Ye