wagtail / django-modelcluster

Django extension to allow working with 'clusters' of models as a single unit, independently of the database
BSD 3-Clause "New" or "Revised" License
485 stars 66 forks source link

AttributeError when a ParentalKey related manager is used as the queryset for a ModelChoiceField #160

Open bcdickinson opened 2 years ago

bcdickinson commented 2 years ago

Bug

FakeQuerySet doesn't have a _prefetch_related_lookups member, so Django throws an attribute error when using it as the queryset kwarg to a ModelChoiceField, here: https://github.com/django/django/blob/3.2.12/django/forms/models.py#L1167

My guess at a solution at this moment in time is to just add _prefetch_related_lookups = () to FakeQuerySet.__init__(), but I'm not 100% sure that's the right answer.

Context

I'm creating a QuestionPage that has multiple choice questions on it. QuestionPages have many Questions which in turn have many Answers. I generate a Form to use on the page and that works fine, but it breaks when I'm previewing the page with the following error:

'FakeQuerySet' object has no attribute '_prefetch_related_lookups'

Here's a sketch:


class QuestionPage(Page):
  ...

  content_panels = [
    ...,
    InlinePanel('questions'),
  ]

class Question(ClusterableModel):
  question_page = ParentalKey(QuestionPage, related_name='questions')
  ...

  panels  = [
    InlinePanel('answers'),
    ...
  ]

class Answer(models.Model):
  question = ParentalKey(Question, related_name="answers")
  ...

class QuestionPageForm(forms.Form):
  def __init__(self, *args, question_page: QuestionPage, **kwargs):
    super().__init__(*args, **kwargs)

    for question in question_page.questions.all():
      self.fields[f"question-{question.pk}"] = forms.ModelChoiceField(
        queryset=question.answers.all(),  # This seems to be what causes the AttributeError during previews
      )
bcdickinson commented 2 years ago

While working around this in my project, I found that we're missing FakeQuerySet.iterator() too. Here's my hack du jour that seems to be working nicely for the time being:

def monkey_patch_fakequeryset():
    FakeQuerySet._prefetch_related_lookups = ()

    def fakequeryset_iterator(self):
        return self

    FakeQuerySet.iterator = fakequeryset_iterator

monkey_patch_fakequeryset()
wimfeijen commented 2 years ago

Hi, I just wanted to say that I got the same error, and the proposal by bcdickinson seems to be a good one!

I am using the latest version of Wagtail 2.16.2, Django 3.2.13 and getting this error when previewing certain wagtailpages which contain ClusterableModels and ParentalManyToManyFields: 'FakeQuerySet' object has no attribute '_prefetch_related_lookups'

wimfeijen commented 2 years ago

Hi, I created a patch: https://github.com/wagtail/django-modelcluster/pull/163

It only adds a _prefetch_related_lookups objects. I did not add an iterator because I did not run into that bug (and couldn't test)