torchbox / wagtail-grapple

A Wagtail app that makes building GraphQL endpoints a breeze!
https://wagtail-grapple.readthedocs.io/en/latest/
Other
152 stars 57 forks source link

StructValue' object has no attribute 'value' #78

Closed timmysmalls closed 4 years ago

timmysmalls commented 4 years ago

I'm getting an error when I work with a nested StructBlock, and try to retrieve the fields using Grapple. In my Streamfield I have a StrucBlock that, among other things, contains a ListBlock with a ButtonBlock (a StructBlock) inside. I hit this exception: StructValue' object has no attribute 'value'. It still returns all the other fields from my query correctly, but the two fields on the button block I request (button_text, and button_link) are nulls. I checked where this exception is raised. It's line 285 of actions.py in grapple. That line is: value = instance.value[field_name]. That seems to work in most cases, but not on the StructValue, which I think is some derivative of an OrderedDict. I found a very simple solution, but I'm sure there's more to it than what I did. Anyway, for starters, here's the code in my app.

The definition for the Page:

from django.db import models
from wagtail.core import blocks

from modelcluster.models import ClusterableModel

from wagtail.admin.edit_handlers import MultiFieldPanel, StreamFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.core.models import Page
from wagtail.core.fields import StreamField

from ..blocks import ImageAndTextBlock

from grapple.models import(
    GraphQLImage,
    GraphQLStreamfield,
)

class StaticPage(Page, ClusterableModel):
    """Model for Static page"""
    banner_image = models.ForeignKey(
        'wagtailimages.Image',
        null=False,
        blank=False,
        on_delete=models.PROTECT,
        related_name='+',
        verbose_name='banner image',
    )
    content = StreamField(
        [
            ("rich_text", blocks.RichTextBlock()),
            ("image_and_text", ImageAndTextBlock()),
        ],
        null=True,
        blank=True,
    )

    content_panels = Page.content_panels + [
        MultiFieldPanel(
            [
                ImageChooserPanel('banner_image'),
            ],
            heading='banner'
        ),
        MultiFieldPanel(
            [
                StreamFieldPanel('content'),
            ],
            heading='content'
        )
    ]

    graphql_fields = [
        GraphQLImage('banner_image'),
        GraphQLStreamfield('content'),
    ]

    def __str__(self):
        return self.id

    def __repr__(self):
        return str(self)

    class Meta:
        verbose_name = 'static page'
        verbose_name_plural = 'static pages'

The top StructBlock, that contains the ButtonBlock that doesn't seem to work with Grapple:

from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock
from grapple.helpers import register_streamfield_block

from .button_block import ButtonBlock

from grapple.models import(
    GraphQLImage,
    GraphQLString,
    GraphQLStreamfield,
)

@register_streamfield_block
class ImageAndTextBlock(blocks.StructBlock):
    """Block that contains image and text."""
    image = ImageChooserBlock(
        required=True,
        label='Image',
    )   
    title_blue = blocks.CharBlock(
        max_length=50,
        required=True,
        label='Title (blue)',
    )   
    title_orange = blocks.CharBlock(
        max_length=50,
        required=False,
        label='Title (orange)',
    )
    text = blocks.TextBlock(
        required=True,
        label='Text',
        rows=4,
    )
    buttons = blocks.ListBlock(
        ButtonBlock()
    )

    graphql_fields = [
        GraphQLImage('image'),
        GraphQLString('title_blue'),
        GraphQLString('title_orange'),
        GraphQLString('text'),
        GraphQLStreamfield(
            'buttons',
        )
    ]

    class Meta:
        icon = "edit"
        label = "Image and Text"

And lastly, the button I'm trying to get through GraphQL:

from wagtail.core import blocks

from grapple.helpers import register_streamfield_block

from grapple.models import(
    GraphQLString,
)

@register_streamfield_block
class ButtonBlock(blocks.StructBlock):
    button_text = blocks.CharBlock(
        required=True,
        max_length=50,
        label='Text',
    )
    button_link = blocks.CharBlock(
        required=True,
        max_length=255,
        label='Link',
    )

    graphql_fields = [
        GraphQLString('button_text'),
        GraphQLString('button_link'),
    ]

    class Meta:
        icon = "radio-full" 
        label = "Button"

Lastly, here's what I changed in actions.py that fixed the problem. I check if the instance is an instance of StructValue. If not, it just does what it's always done: value = instance.value[field_name]. If it is a StructValue, it just calls the key directly on the StructValue. So:

value = instance.value[field_name]

becomes:

from wagtail.core.blocks import StructValue

if (isinstance(instance, StructValue)):                                                
    value = instance[field_name]                                                       
else:                                                                                  
    value = instance.value[field_name]

Did I do something wrong here to create this error, or is this a bug? Thanks.

timmysmalls commented 4 years ago

It seems that this part does get tested, but it uses StreamValues in the test, and from what I can see the testcases should use StructValues instead, which have a different interface.

zerolab commented 4 years ago

@timmysmalls what is the query you were running this with? Was trying to write a test to reproduce the issue but with little luck so far

edit: found it! 1f5c7ecaeb922756c1df04c2549cff222dba8a1b