wagtail / wagtail-factories

Factory boy classes for wagtail
http://wagtail-factories.readthedocs.io/en/latest/
MIT License
102 stars 41 forks source link

Tests fail vs Wagtail 4.1 with "AttributeError: 'list' object has no attribute 'bound_blocks'" #65

Closed johncarter-phntm closed 1 year ago

johncarter-phntm commented 1 year ago

I'm hitting a similar error with my own factories, it looks like passing a list into a ListBlock is the problem.

Example error:

tests/test_factories.py:95 (test_custom_page_streamfield_data)
@pytest.mark.django_db
    def test_custom_page_streamfield_data():
        root_page = wagtail_factories.PageFactory(parent=None)
>       page = MyTestPageFactory(
            parent=root_page, body=[("char_array", ["bla-1", "bla-2"])]
        )

test_factories.py:99: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/base.py:40: in __call__
    return cls.create(**kwargs)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/base.py:528: in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/django.py:117: in _generate
    return super()._generate(strategy, params)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/base.py:465: in _generate
    return step.build()
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/builder.py:262: in build
    instance = self.factory_meta.instantiate(
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/factory/base.py:317: in instantiate
    return self.factory._create(model, *args, **kwargs)
../src/wagtail_factories/factories.py:66: in _create
    instance = cls._create_instance(model_class, parent, kwargs)
../src/wagtail_factories/factories.py:74: in _create_instance
    parent.add_child(instance=instance)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/treebeard/mp_tree.py:1083: in add_child
    return MP_AddChildHandler(self, **kwargs).process()
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/treebeard/mp_tree.py:387: in process
    newobj.save()
../../../.pyenv/versions/3.11.0/lib/python3.11/contextlib.py:81: in inner
    return func(*args, **kwds)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/models/__init__.py:1164: in save
    result = super().save(**kwargs)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/modelcluster/models.py:201: in save
    super().save(update_fields=real_update_fields, **kwargs)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/django/db/models/base.py:812: in save
    self.save_base(
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/django/db/models/base.py:878: in save_base
    post_save.send(
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/django/dispatch/dispatcher.py:176: in send
    return [
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/django/dispatch/dispatcher.py:177: in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/signal_handlers.py:89: in update_reference_index_on_save
    ReferenceIndex.create_or_update_for_object(instance)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/models/reference_index.py:352: in create_or_update_for_object
    references = set(cls._extract_references_from_object(object))
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/models/reference_index.py:285: in _extract_references_from_object
    yield from (
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/models/reference_index.py:285: in <genexpr>
    yield from (
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/fields.py:249: in extract_references
    yield from self.stream_block.extract_references(value)
../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/blocks/stream_block.py:334: in extract_references
    for (
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <wagtail.blocks.list_block.ListBlock object at 0x7f025b895bd0>
value = ['bla-1', 'bla-2']

    def extract_references(self, value):
>       for child in value.bound_blocks:
E       AttributeError: 'list' object has no attribute 'bound_blocks'

../../../.virtualenvs/wagtail-factories/lib/python3.11/site-packages/wagtail/blocks/list_block.py:323: AttributeError
zerolab commented 1 year ago

fwiw, my workaround for this was https://github.com/torchbox/wagtail-grapple/pull/270/commits/b3e6883d5e9154b137f803ce2a58c60e4ebbc9d9#diff-0fda90953a8234a646f51996c7a14f3d7976713a8a27f3815701cf98c299c883R46, but I did not have the chance to dive deeper

ychab commented 1 year ago

Same error while upgrading from Wagtail 4.0.4 to 4.1 ! Obviously, wagtail_factories.ListBlockFactory is now buggy... (list instead of bound_blocks as expected)

@zerolab thanks for the workaround ! In my custom wagtail_factories.StructBlockFactory factory, I'm now defining attribute with ListValue(ListBlock(MyCustomBlock()), values=[{.. fields...}]) instead of ListBlockFactory...

This isn't dynamic (factory boy way) but at least, we could jump to wagtail 4.1...

Hope a real fix could be done quickly!

ychab commented 1 year ago

For instance, here is the models :

class SkillLinkBlock(blocks.StructBlock):
    title = blocks.CharBlock(max_length=128)
    page = blocks.PageChooserBlock(label='Page', required=False)

    class Meta:
        icon = 'link'
        value_class = SkillLinkStructValue

class SkillBlock(blocks.StructBlock):
    heading = blocks.CharBlock(max_length=128)
    links = blocks.ListBlock(SkillLinkBlock(), collapsed=True)

    class Meta:
        template = 'portfolio/blocks/skill_grid.html'
        icon = 'code'

class SkillsBlock(blocks.StreamBlock):
    skill = SkillBlock()

    class Meta:
        template = 'portfolio/blocks/skills.html'
        icon = 'code'

and the factories:

class SkillLinkBlockFactory(wagtail_factories.StructBlockFactory):

    class Meta:
        model = SkillLinkBlock

    title = factory.Faker('sentence', nb_words=3, locale=current_locale)
    page = factory.SubFactory(wagtail_factories.PageChooserBlockFactory)

class SkillBlockFactory(wagtail_factories.StructBlockFactory):

    class Meta:
        model = SkillBlock

    heading = factory.Faker('sentence', nb_words=3, locale=current_locale)

    # BROKEN ListBlockFactory... worked with Wagtail 4.0.4
    # links = wagtail_factories.ListBlockFactory(SkillLinkBlockFactory)

    # Unfortunetly, cannot use SkillLinkBlockFactory...
    links = ListValue(ListBlock(SkillLinkBlock()), values=[
        {
            'title': fake.sentence(nb_words=3),
            'page': wagtail_factories.PageChooserBlockFactory(),
        },
    ])

class SkillsBlockFactory(wagtail_factories.StreamBlockFactory):

    class Meta:
        model = SkillsBlock

    skill = factory.SubFactory(SkillBlockFactory)

But afterall, maybe I was a lucky guy and didn't use well ListBlock... ?

johncarter-phntm commented 1 year ago

I've added a WIP fix in #66 , not sure if this is the right approach or not though.

bglendenning commented 1 year ago

FWIW, as a quick fix I implemented a subclassed version of the solution @johncarter-phntm proposes (thanks) for generating development data. I'm not thrilled about introducing the tech debt.

from wagtail.blocks import list_block

class CustomListBlockFactory(wagtail_factories.ListBlockFactory):
    def evaluate(self, instance, step, extra):
        retval = super().evaluate(instance, step, extra)
        block_model = self.get_factory()._meta.model
        return list_block.ListValue(list_block.ListBlock(block_model), values=retval)