wagtail / wagtail-factories

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

No way to specify default values for StreamBlockFactory #57

Open bcdickinson opened 2 years ago

bcdickinson commented 2 years ago

This is a pre-emptive issue ahead of the merge of #55 to document a missing feature without unnecessarily holding up that PR. The PR adds a new StreamBlockFactory class that can be passed as the argument to a StreamFieldFactory declaration. The missing feature is the ability to specify the default value of a StreamBlockFactory subclass, either when it's passed to StreamFieldFactory or on the class definition itself.

The relationship between StreamFieldFactory and StreamBlockFactory is a bit like the relationship between SubFactory and Factory, so it'd be nice to be able to do something similar to the **kwargs that SubFactory.__call__ accepts to set the default value of the generated stream data. However, the values will always start with block indexes (e.g. 0__my_block_type__some_attribute="foo") which aren't legal Python kwarg names, so my proposal for this API would be to accept an optional dict-like object that defines the defaults, something like:

class MyPageFactory(wagtail_factories.PageFactory):
  body = wagtail_factories.StreamFieldFactory(
    MyStreamBlockFactory,
    {
      "0": "my_block_type",  # Default value for "my_block_type"
      "1__my_block_type__some_field": "foo",  # Explicit value for a nested field
    },
  )

The above should work well for defining default data for a given use of a StreamBlockFactory, it would also be useful to be able to specify default values as part of the StreamBlockFactory definition as well, so that the same data could be used in multiple places without being redefined. I'm less sure how the API for this should look, but one idea might be adding a new default_data member to the Meta class, something like this:

class MyStreamBlockFactory(wagtail_factories.StreamBlockFactory):
  my_block_type = factory.SubFactory(MyBlockTypeFactory)

  class Meta:
    model = MyStreamBlock
    default_data = {
      "0": "my_block_type",  # Default value for "my_block_type"
      "1__my_block_type__some_field": "foo",  # Explicit value for a nested field
    }
tbrlpld commented 2 years ago

@bcdickinson I am confused by the "0": "my_block_type", # Default value for "my_block_type". Is "0" the value?

tbrlpld commented 2 years ago

If the issue we are trying to solve is the fact that the block "descriptor" (not sure what to call it) starts with a number (because we don't know the name of the field on the parent), could we just use something generic as the start for the string like "self__0__my_block_type__some_field": ...?

jams2 commented 2 years ago

@tbrlpld

I am confused by the "0": "my_block_type", # Default value for "my_block_type". Is "0" the value?

In this case, this means that the 0th item in the generated StreamValue would get the default value for the my_block_type declaration - in this case it will be the default value of MyBlockTypeFactory.

@bcdickinson it might be worth changing StreamFieldFactory so it is a SubFactory subclass - that way one could provide a defaults dict in the declaration. From a high level this wouldn't be too difficult.

To allow specifying StreamBlockFactory defaults on the Meta class, I think we'd have to further extend StreamBlockStepBuilder. One way would be to update its __init__ method to merge extras and factory_meta.default_data before doing the work that generates the actual factory class that's used for object generation.

Question: given a StreamBlockFactory definition like this:

class BodyFactory(StreamBlockFactory):
    struct_1 = SubFactory(Struct1Factory)
    struct_2 = SubFactory(Struct2Factory)

    class Meta:
        default_data = {"0": "struct_1", "1": "struct_1"}

and a call like MyPageFactory(body__1="struct_2"), would you expect the declarations to be merged such that you effectively end up generating data from

{
    "0": "struct_1",  # from meta.default_data,
    "1": "struct_2",  # from extras at page factory instantiation
}

?

jams2 commented 2 years ago

Potentially related: #60

tbrlpld commented 1 year ago

I found, you can use factory boy's post_generation decorator to create a default set of blocks. Of course, this is only on the page where the block is used and not the block itself, but it's something.

class InformationPageFactory(wagtail_factories.PageFactory):
    class Meta:
        model = std_models.InformationPage

    title = factory.Faker("text", max_nb_chars=25)
    introduction = factory.Faker("text", max_nb_chars=200)

    @factory.post_generation
    def body(obj, create, extracted, **kwargs):
        blocks = kwargs or {"0": "paragraph"}
        obj.body = utils_factories.StoryBlockFactory(**blocks)