Django/AJAX - 链式模型选择 - 修复 'Select a Valid Choice' 错误而不预加载所有可能的选择

Django/AJAX - Chained modelform selects - Fixing 'Select a Valid Choice' error without preloading of all possible choices

我有这些型号:

class Subject(models.Model):
    title = models.CharField(max_length=255)

class Teacher(models.Model):
    name = models.CharField(max_length=255)
    subjects = models.ManyToManyField(Subject)

class Exam(models.Model):
    classroom = models.CharField(max_length=20)
    teacher = models.ForeignKey(Teacher)
    subjects = models.ManyToManyField(Subject)

因此,由于有几千个科目,但只有少数科目对特定教师有效,我禁用了模型中的科目字段,一旦选择了教师,重新启用它并加载可能的通过 AJAX 的选项。

编辑现有考试时,一切正常,因为我可以这样做:

if not self.instance._state.adding:
    self.fields['subjects'].queryset = self.instance.teacher.subject_set.all()

但是,当我想添加新考试时,加载变得非常缓慢,因为所有可能的选项都已加载,只有这样我的 javascript 才能用更窄的查询集替换它们。因此,我尝试像这样删除冗余加载:

if not self.instance._state.adding:
    self.fields['subjects'].queryset = self.instance.teacher.subject_set.all()
else:
    self.fields['subjects'].queryset = Subject.objects.none()

然后我得到一个 "Select a Valid Choice. XXX is not one of the available choices" 错误。

那么,有没有办法动态更改特定字段的查询集参数(或其他一些解决方法)并避免出现指定的错误?

编辑:

好吧,看来我的问题解释不通,没人回答,所以这里是一个简化的应用程序的完整代码,你可以自己尝试一下,看看问题是什么。

models.py

class Subject(models.Model):
    title = models.CharField(max_length=255)

    def __unicode__(self):
        return self.title

class Teacher(models.Model):
    name = models.CharField(max_length=255)
    subjects = models.ManyToManyField(Subject)

    def __unicode__(self):
        return self.name

class Exam(models.Model):
    classroom = models.CharField(max_length=20)
    teacher = models.ForeignKey(Teacher)
    subjects = models.ManyToManyField(Subject)

    def __unicode__(self):
        return u"{0} - {1}".format(self.classroom, self.teacher)

forms.py/admin.py

from .models import Subject, Teacher, Exam

class ExamForm(forms.ModelForm):
    class Meta:
        model = Exam
        fields = ['classroom','teacher','subjects']

    class Media:
        js = (
            'https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js',
            'js/exam_form.js',
        )

    def __init__(self, *args, **kwargs):
        super(ExamForm, self).__init__(*args, **kwargs)

        if not self.instance._state.adding:
            self.fields['subjects'].queryset = self.instance.teacher.subjects.all()
        else:
            self.fields['subjects'].queryset = Subject.objects.none()

@admin.register(Exam)
class ExamAdmin(admin.ModelAdmin):
    list_display = ['classroom','teacher']
    form = ExamForm

admin.site.register(Subject)
admin.site.register(Teacher)

views.py

from .models import Teacher


def query_teacher_subject(request):
    id = request.GET.get('id')
    if request.is_ajax():
        try:
            teacher = Teacher.objects.get(id=id)
        except Teacher.DoesNotExist:
            return HttpResponse(serializers.serialize('json', 'Teacher does not exist.'))
        else:
            return HttpResponse(serializers.serialize('json',teacher.subjects.all(), fields=('id','title',)))
    else:
        raise Http404

exam_form.js

$(function() {
    var t = $("#id_teacher");
    t.change(function(){
        if (t.val() != '') {
            $("#id_subjects").prop('disabled',false);

            var selected = [];
            $("#id_subjects :selected").each(function(i, sel){
                selected.push($(sel).val());
            });

            $.get('/ajax-call/', {id:t.val()}, function(data) {
                data = $.parseJSON(data);
                var options;
                $.each(data, function(index,item) {
                    options += '<option value="' + item.pk + '">' + item.fields.title + '</option>';
                });
                $("#id_subjects").html(options);

                $("#id_subjects option").each(function(i, sel) {
                    if ($.inArray(sel.value, selected) !== -1) {
                        $('#id_subjects option[value=' + sel.value + ']').attr('selected', true);
                    }
                });

            });
        } else {
            $("#id_subjects").prop('disabled',true);
        }
    });
    $("#id_teacher").trigger('change');
});

因此,如果您在 ExamForm 的 init 方法中省略 else 分支,则一切正常,除了所有主题最初都已加载这一事实。但是,如果将其留在那里,则会出现以下错误:"Select a valid choice. 1 is not one of the available choices."

那么,如何将最初为空的查询集放入稍后通过 AJAX 填充并仍然成功验证的模型中?

好吧,我终于解决了问题,但我仍然不确定为什么原来的想法不起作用。

所以,我单独留下了表单的 __init__ 方法,并将调整移动到管理员的 get_form 方法,如下所示:

    def get_form(self, request, obj=None, **kwargs):
        form = super(ExamAdmin, self).get_form(request, obj, **kwargs)
        if request.method == 'GET':
            if obj:
                form.base_fields['subjects'].queryset = Subject.objects.filter(pk__in=obj.subjects.all())
            else:
                form.base_fields['subjects'].queryset = Subject.objects.none()
        return form