Giter Club home page Giter Club logo

wagtail-factories's Introduction

wagtail-factories

Factory boy classes for Wagtail CMS

Join the Community at Wagtail Space!

Join us at Wagtail Space US this year! The Call for Participation and Registration for both Wagtail Space 2024 events is open. We would love to have you give a talk, or just us as an attendee in June.

Status

image image image

Installation

pip install wagtail-factories

Usage

Documentation is still in progress, but see the tests for more examples.

import wagtail_factories
from . import models


class MyCarouselItemFactory(wagtail_factories.StructBlockFactory):
    label = 'my-label'
    image = factory.SubFactory(
        wagtail_factories.ImageChooserBlockFactory)

    class Meta:
        model = models.MyBlockItem


class MyCarouselFactory(wagtail_factories.StructBlockFactory):
    title = "Carousel title"
    items = wagtail_factories.ListBlockFactory(
        MyCarouselItemFactory)

    class Meta:
        model = models.MyCarousel


class MyNewsPageFactory(wagtail_factories.PageFactory):
    class Meta:
        model = models.MyNewsPage


class MyNewsPageChooserBlockFactory(wagtail_factories.PageChooserBlockFactory):
    page = factory.SubFactory(MyNewsPageFactory)


class MyTestPageFactory(wagtail_factories.PageFactory):
    body = wagtail_factories.StreamFieldFactory({
        'carousel': factory.SubFactory(MyCarouselFactory),
        'news_page': factory.SubFactory(MyNewsPageChooserBlockFactory),
    })

    class Meta:
        model = models.MyTestPage


def test_my_page():
    root_page = wagtail_factories.PageFactory(parent=None)
    my_page = MyTestPageFactory(
        parent=root_page,
        body__0__carousel__items__0__label='Slide 1',
        body__0__carousel__items__0__image__image__title='Image Slide 1',
        body__0__carousel__items__1__label='Slide 2',
        body__0__carousel__items__1__image__image__title='Image Slide 2',
        body__0__carousel__items__2__label='Slide 3',
        body__0__carousel__items__2__image__image__title='Image Slide 3',
        body__1__news_page__page__title="News",
    )

Using StreamBlockFactory

StreamBlockFactory can be used in conjunction with the other block factory types to create complex, nested StreamValues, much like how StreamBlock can be used to declare the blocks for a complex StreamField.

First, define your StreamBlockFactory subclass, using factory.SubFactory to wrap child block declarations. Be sure to include your StreamBlock subclass as the model attribute on the inner Meta class.

class MyStreamBlockFactory(wagtail_factories.StreamBlockFactory):
    my_struct_block = factory.SubFactory(MyStructBlockFactory)

    class Meta:
        model = MyStreamBlock

Then include your StreamBlockFactory subclass on a model factory as the argument to a StreamFieldFactory.

class MyPageFactory(wagtail_factories.PageFactory):
    body = wagtail_factories.StreamFieldFactory(MyStreamBlockFactory)

    class Meta:
        model = MyPage

You can then use a modified version of factory_boy's deep object declaration syntax to build up StreamValues on the fly.

MyPageFactory(
    body__0__my_struct_block__some_field="some value",
    body__0__my_struct_block__some_other_field="some other value",
)

To generate the default value for a block factory, terminate your declaration at the index and provide the block name as the value.

MyPageFactory(body__0="my_struct_block")

Alternative StreamFieldFactory declaration syntax

Prior to version 3.0, StreamFieldFactory could only be used by providing a dict mapping block names to block factory classes as the single argument, for example:

class MyTestPageWithStreamFieldFactory(wagtail_factories.PageFactory):
    body = wagtail_factories.StreamFieldFactory(
        {
            "char_array": wagtail_factories.ListBlockFactory(
                wagtail_factories.CharBlockFactory
            ),
            "int_array": wagtail_factories.ListBlockFactory(
                wagtail_factories.IntegerBlockFactory
            ),
            "struct": MyBlockFactory,
            "image": wagtail_factories.ImageChooserBlockFactory,
        }
    )

    class Meta:
        model = models.MyTestPage

This style of declaration is still supported, with the caveat that nested stream blocks are not supported for this approach. From version 3.0, all BlockFactory values in a StreamFieldFactory definition of this style must be wrapped in factory_boy SubFactories. For example, the above example must be updated to the following for 3.0 compatibility.

class MyTestPageWithStreamFieldFactory(wagtail_factories.PageFactory):
    body = wagtail_factories.StreamFieldFactory(
        {
            "char_array": wagtail_factories.ListBlockFactory(
                wagtail_factories.CharBlockFactory
            ),
            "int_array": wagtail_factories.ListBlockFactory(
                wagtail_factories.IntegerBlockFactory
            ),
            "struct": factory.SubFactory(MyBlockFactory),
            "image": factory.SubFactory(wagtail_factories.ImageChooserBlockFactory),
        }
    )

    class Meta:
        model = models.MyTestPage

This requirement does not apply to ListBlockFactory, which is a subclass of SubFactory.

wagtail-factories's People

Contributors

bcdickinson avatar blurrah avatar brianxu20 avatar crimzon96 avatar engineervix avatar flipperpa avatar hammygoonan avatar jams2 avatar jeromelebleu avatar katdom13 avatar kevinhowbrook avatar lparsons396 avatar marteinn avatar mikedingjan avatar mvantellingen avatar nickmoreton avatar rinti avatar therefromhere avatar thibaudcolas avatar tijani-dia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wagtail-factories's Issues

Add automatic generation of stream block factories

Currently, to use stream field factories for non-trivial stream fields, a user needs to:

  • define each block as a class, avoiding the inline declaration syntax; and
  • create a block factory class for each distinct block class.

This means a typical Wagtail codebase may have tens of block factory classes that provide little other than boilerplate, and maybe some default values.

We should provide functionality for automatically generating the block factories, so that users of the library can avoid writing boilerplate for boilerplate's sake.

An ideal implementation would:

  • allow users to use stream field factories without declaring any block factory classes;
  • allow users to explicitly declare and use block factory classes, including them at the root of the tree (i.e. as the stream field factory entrypoint - matching current behaviour);
  • allow users to explicitly declare and use block factory classes, including them at arbitrary points in an otherwise automatically generated tree of block factories;
  • declare default values for block factories or their fields, at arbitrary points in the tree;
  • generate block factories lazily - only when they're required;
  • cache any generated block factories, so that effort isn't repeated;
  • continue to work with the existing syntax for specifying block values; and
  • include proper documentation.

Issue importing from blocks.py

Thanks for your work on this! I'm not sure if this was my fault, but I'm having trouble importing anything from blocks.py. Everything in factories.py works.

I was really hoping to find out what was wrong, but I haven't debugged it yet. Your init looks fine. Here's an example of the ImportError:

screen shot 2016-12-21 at 12 00 37 pm

Use of traits with StreamBlockFactory

Given a factory definition of

class LinkStreamBlockFactory(wagtail_factories.StreamBlockFactory):
    internal = factory.SubFactory(PageStructBlockFactory)
    document = factory.SubFactory(DocumentStructBlockFactory)
    external = factory.SubFactory(ExternalLinkStructBlockFactory)

    class Meta:
        model = blocks.LinkStreamBlock

    class Params:
        is_internal = factory.Trait(**{"0": "internal"})

it is not possible to use the is_internal trait, like LinkStreamBlockFactory(is_internal=True), as the is_internal keyword breaks StreamBlockFactory's parameter parsing. This particular use case is somewhat pointless (you could just call LinkStreamBlockFactory(**{"0": "internal"})), but there may be valid use cases for traits (maybe as defining a shorthand for a particular combination of nested options). I would like to hear opinions on this - would fixing this be useful?

ListBlockFactory is not working in StruckBockFactory

ListBlockFactory is not working in StruckBockFactory and this case hasn't been tested.

I added new item to items in test_custom_page_streamfield_data_complex

@pytest.mark.django_db
def test_custom_page_streamfield_data_complex():
    assert Image.objects.count() == 0

    root_page = wagtail_factories.PageFactory(parent=None)
    page = MyTestPageWithStreamFieldFactory(
        parent=root_page,
        body__0__char_array__0="foo",
        body__0__char_array__1="bar",
        body__2__int_array__0=100,
        body__1__struct__title="My Title",
        body__1__struct__item__value=100,
        body__1__struct__image__image=None,
        body__3__image__image__title="Blub",
        body__4__struct__items__0__label="Test",  # < Added
        body__4__struct__items__0__value=200  # < Added
    )
    assert Image.objects.count() == 1
    image = Image.objects.first()

    assert page.body[0].block_type == "char_array"
    assert page.body[0].value == ["foo", "bar"]

    assert page.body[1].block_type == "struct"
    assert page.body[1].value == StructValue(
        None,
        [
            ("title", "My Title"),
            (
                "item",
                StructValue(None, [("label", "my-label"), ("value", 100)]),
                ),
            ("items", []),
            ("image", None),
        ],
    )

    assert page.body[2].block_type == "int_array"
    assert page.body[2].value == [100]

    assert page.body[3].block_type == "image"
    assert page.body[3].value == image
    
    content = str(page.body)
    assert "block-image" in content

Error:

โžœ  wagtail-factories git:(master) โœ— make test
py.test --reuse-db
===================================================================== test session starts ======================================================================
platform darwin -- Python 3.9.0, pytest-6.0.1, py-1.10.0, pluggy-0.13.1
django: settings: tests.settings (from ini)
rootdir: /Users/muhtarcangoktas/Code/wagtail-factories, configfile: setup.cfg
plugins: django-3.9.0, Faker-8.11.0, cov-2.7.1, pythonpath-0.7.3
collected 22 items

tests/test_blocks.py ...F..                                                                                                                              [ 27%]
tests/test_factories.py ...............                                                                                                                  [ 95%]
tests/test_blocks.py .                                                                                                                                   [100%]

=========================================================================== FAILURES ===========================================================================
__________________________________________________________ test_custom_page_streamfield_data_complex ___________________________________________________________

    @pytest.mark.django_db
    def test_custom_page_streamfield_data_complex():
        assert Image.objects.count() == 0

        root_page = wagtail_factories.PageFactory(parent=None)
        page = MyTestPageWithStreamFieldFactory(
            parent=root_page,
            body__0__char_array__0="foo",
            body__0__char_array__1="bar",
            body__2__int_array__0=100,
            body__1__struct__title="My Title",
            body__1__struct__item__value=100,
            body__1__struct__image__image=None,
            body__3__image__image__title="Blub",
            body__4__struct__items__0__label="Test",
            body__4__struct__items__0__value=200
        )
>       assert Image.objects.count() == 1
E       assert 2 == 1
E        +  where 2 = <bound method BaseManager._get_queryset_methods.<locals>.create_method.<locals>.manager_method of <django.db.models.manager.ManagerFromImageQuerySet object at 0x107f99280>>()
E        +    where <bound method BaseManager._get_queryset_methods.<locals>.create_method.<locals>.manager_method of <django.db.models.manager.ManagerFromImageQuerySet object at 0x107f99280>> = <django.db.models.manager.ManagerFromImageQuerySet object at 0x107f99280>.count
E        +      where <django.db.models.manager.ManagerFromImageQuerySet object at 0x107f99280> = Image.objects

tests/test_blocks.py:100: AssertionError
======================================================================= warnings summary =======================================================================
tests/test_factories.py::test_page_multiple_roots
  /Users/muhtarcangoktas/Code/wagtail-factories/tests/urls.py:6: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path().
    urlpatterns += [url(r"^", include(wagtail_urls))]

-- Docs: https://docs.pytest.org/en/stable/warnings.html
=================================================================== short test summary info ====================================================================
FAILED tests/test_blocks.py::test_custom_page_streamfield_data_complex - assert 2 == 1
=========================================================== 1 failed, 21 passed, 1 warning in 2.39s ============================================================
make: *** [test] Error 1

Library is not compatible with factory-boy==3.2.0

While upgrading the CI test targets I noticed that all the tests related to ListBlockFactory started to fail, I tracked it down to be because of factory-boy and the latest version (3.2). It works well in factory-boy 3.0 and 3.1.

The error seems to be because of a breaking change in factory boy where the SubFactory generator has been rewritten.

Before: https://github.com/FactoryBoy/factory_boy/blob/73ee5cccd84f6c30edc46580c9a563a62bef6c6f/factory/declarations.py#L401
After: https://github.com/FactoryBoy/factory_boy/blob/master/factory/declarations.py#L357

I will start to work out a solution and hopefully will have something ready by the end of the week.

Feature request: A TestCase that automatically finds and tests PageFactory subclasses

Imagine a test case that magically find all of the PageFactory subclasses within a project, and for each class found, test that:

  • It can successfully create an instance of that page type
  • The created page doesn't error when viewed normally
  • The created page doesn't error when viewed in 'preview' mode

I think this could easily be achieved by keeping a 'reference' dict that gets populated via a metaclass when subclasses are defined (a bit like what Wagtail does with wagtail.core.models.PAGE_MODEL_CLASSES)

We could ignore any factories that do not specify a model value in their Meta class. And maybe even introduce a new/optional Meta option that could be used to explicitly opt-out of testing for a specific factory class.

Is help needed?

I am wondering who is still active in the contributors and have time to maintain this project. I think at least tests should be updated to ensure last Wagtail and Django versions are still supported. Feel free to ask if any help is needed.

Using real images

Is there a way to have the image factory use real images, rather than solid color fields? Wagtail has lots of different crop/focus/fill/etc rules for its {% image ... %} tag that can't be tested with solid colors, so being able to specify a real (set of) image(s), either static or from the actual CMS, would be incredibly valuable.

(maybe it's something that actually already works, only requiring a set of options to be passed into factory_boy?)

Support nested stream blocks

A streamfield can contain stream blocks.
A struct block can contain stream blocks.

possible solution here: https://github.com/nhsuk/wagtail-factories/blob/dev/src/wagtail_factories/blocks.py#L158

For blocks:

from wagtail.core import blocks

class MySubStreamBlock(blocks.StreamBlock):
    foo = blocks.CharBlock()
    bar = blocks.CharBlock()

class MyStructBlock(blocks.StructBlock):
    sub_stream = MySubStreamBlock()

class MyStreamBlock(blocks.StreamBlock):
    struct = MyStructBlock()

This requires a factory definition of:

import factory
import wagtail_factories
from . import blocks
from .models import HomePage

class MySubStreamBlockFactory(wagtail_factories.StreamBlockFactory):

    foo = "foo"
    bar = "bar"

    class Meta:
        model = blocks.MySubStreamBlock

class MyStructBlockFactory(wagtail_factories.StructBlockFactory):

    sub_stream = wagtail_factories.StreamBlockSubFactory(MySubStreamBlockFactory)

    class Meta:
        model = blocks.MyStructBlock

class MyStreamBlockFactory(wagtail_factories.StreamBlockFactory):

    struct = factory.SubFactory(MyStructBlockFactory)

    class Meta:
        model = blocks.MyStreamBlock

class HomePageFactory(wagtail_factories.PageFactory):

    content = wagtail_factories.StreamFieldFactory({
        "struct": MyStructBlockFactory,
    }, **{
        "0__struct__sub_stream__0__foo": "foo",
        "0__struct__sub_stream__1__bar": "bar",
        "0__struct__sub_stream__2__foo": "foo2",
    })

    class Meta:
        model = HomePage

I am only using stream blocks as a sub-block of struct blocks in my project, so I haven't tested other combinations of blocks. E.g stream block as a sub-block of a stream-block, stream block inside a list block.

How to add multiple streamfield items?

For the StreamFieldFactory, how do I generate more than 1 streamfield item?

For example:

    images = wagtail_factories.StreamFieldFactory(
        {"image": wagtail_factories.ImageChooserBlockFactory()},
    )

This will generate 1 image. How do I generate more?

Add factories documentation

The example in the README is useful, but a bit strained. Now, with new features starting to come in, we're definitely going to need proper docs with recipe examples to make sure this is useful to more than just people with time to read through the tests.

Wagtail 5.0 not supported

When attempting to upgrade to Wagtail 5.0:

wagtail-factories 4.0.0 depends on wagtail<5.0 and >=2.15

Feature request: Using page factories for large-scale tree population

For a lot of projects, it can be helpful to establish the general 'page structure' of a site early on.

In the past, I've used fixtures for this, but I find them cumbersome. Every time you decide to switch up your model classes or fields, the fixtures remain unaware and require either a painful 'manual update' process, or a painful 'manually populate the tree locally and re-dump everything' process.

In a recent project, I decided that, since we were mostly defining a factory for every page type, we could use factories instead! I came up with the following:

from typing import Optional, Sequence

from django.template.defaultfilters import slugify
from wagtail.core.models import Page

from my_app.standardpages.factories import GeneralPageFactory


class CreatePageNode:
    """
    Used to represent a 'Wagtail page' that must be created.
    """

    default_factory_class = GeneralPageFactory

    def __init__(
        self,
        title: str,
        factory_class: type = None,
        *,
        children: Optional[Sequence["CreatePageNode"]] = None,
        **kwargs,
    ):
        self.title = title
        self.create_kwargs = kwargs
        self.factory_class = factory_class or self.default_factory_class
        self.children = children or []

    def create(
        self, parent: Page = None, create_subpages: bool = True, **kwargs
    ) -> None:
        create_kwargs = {**self.create_kwargs, **kwargs}
        create_kwargs.setdefault("title", self.title)
        if parent is None:
            parent = create_kwargs.get("parent") or Page.objects.first()

        try:
            page = (
                parent.get_children()
                .get(slug=create_kwargs.get("slug", slugify(create_kwargs["title"])))
                .specific
            )
            created = False
        except Page.DoesNotExist:
            create_kwargs.setdefault("parent", parent)
            page = self.factory_class.create(**create_kwargs)
            created = True

        if create_subpages:
            for child in self.children:
                child.create(parent=page, create_subpages=True)

        return page, created

This can then be used to define a 'page tree to create', like so:

from .utils import CreatePageNode as p

PAGES_TO_CREATE = p(
    "Home",
    HomePageFactory,
    slug="home",
    children=(
        p(
            "About us",
            children=(
                p("Accessibility Statement", specific_field_value="foo"),
                p("Collection", factory_property_value="bar"),
                p("Contact Us", slug="contact"),
                p("Corporate Support"),
                p("Frequently Asked Questions", slug="faqs"),
                p("Governance"),
                p(
                    "Policies and Procedures",
                    slug="policies-and-procedures",
                    children=(
                        p("Privacy Policy"),
                        p("Terms and Conditions"),
                    ),
                ),
                p("Projects", ProjectIndexPageFactory),
                p("Working for us"),
            ),
        ),
        p(
            "Support us",
            slug="support-us",
            children=(
                p("Become a member"),
                p("Become a patron"),
                p("Donate"),
            ),
        ),
        p(
            "Press",
            PressReleaseIndexPageFactory
        ),
        p(
            "Whats On?",
            EventIndexPageFactory,
            children=(
                p("Workshops", EventCategoryPageFactory, children=(
                    p("Example workshop", EventPageFactory, event_style="workshop"),
                ), 
                p("Food & drink", EventCategoryPageFactory, children=(
                    p("Example cocktail evening", EventPageFactory, event_style="food"),
                ), 
                p("Music", EventCategoryPageFactory, children=(
                    p("Jazz club", EventPageFactory, event_style="music", music_style="jazz"),
                    p("Rock city", EventPageFactory, event_style="music", music_style="rock"),
                ),
            )
        ),
    ),
)

This can easily be incorporated into a management command that can trigger creation of all of these pages, like so:

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    def handle(self, *args, **options)
        PAGES_TO_CREATE.create(create_subpages=True)

The process safely avoids creating/overwriting pages that already exist - meaning it can be rerun at a later time to add new sections (as the page types are developed)

ImportError: cannot import name 'extract_dict' from 'factory.utils'

It seems that the new factory-boy version 2.12.0 is not longer compatible with wagtail-factories=1.1.0 due to import errors.

/usr/local/lib/python3.7/site-packages/wagtail_factories/__init__.py:1: in <module>
    from .blocks import *  # noqa
/usr/local/lib/python3.7/site-packages/wagtail_factories/blocks.py:13: in <module>
    from wagtail_factories.factories import ImageFactory
/usr/local/lib/python3.7/site-packages/wagtail_factories/factories.py:3: in <module>
    from factory.utils import extract_dict
ImportError: cannot import name 'extract_dict' from 'factory.utils' (/usr/local/lib/python3.7/site-packages/factory/utils.py)

Temporary fixed by fixing the factory-boy version: factory_boy<2.12

StreamField remains empty

Thanks for your work!

I'm trying to get the StreamFieldFactory to work. But the fields always remains empty (with master branch)

My code:

models.py

class ConsiderationPage(Page):
    body_1 = StreamField(
        ('paragraph', blocks.RichTextBlock()),
        ('images', blocks.ListBlock(ImageChooserBlock(), icon="image"))
    )
    body_2 = StreamField(
        ('paragraph', blocks.RichTextBlock()),
        ('images', blocks.ListBlock(ImageChooserBlock(), icon="image"))
    )

    content_panels = Page.content_panels + [
        StreamFieldPanel('body_1'),
        StreamFieldPanel('body_2'),
    ]

factories.py

class ConsiderationPageFactory(wagtail_factories.PageFactory):
    title = factory.LazyAttribute(lambda x: fake_de.sentence(nb_words=3))

    body_1 = wagtail_factories.StreamFieldFactory({
        'paragraph': wagtail_factories.CharBlockFactory,
        'images': wagtail_factories.ListBlockFactory(wagtail_factories.ImageChooserBlockFactory)
    })

    body_2 = wagtail_factories.StreamFieldFactory({
        'paragraph': wagtail_factories.CharBlockFactory,
        'images': wagtail_factories.ListBlockFactory(wagtail_factories.ImageChooserBlockFactory)

    })

    class Meta:
        model = models.ConsiderationPage

Then I call:

root_page = ConsiderationPageFactory.create(parent=None)
SiteFactory.create(root_page=root_page, is_default_site=False)
ConsiderationPageFactory.create(parent=root_page)

Add RedirectFactory

Wagtail provides a Redirect model so it would be useful to add a reusable RedirectFactory.

Example:

class RedirectFactory(factory.django.DjangoModelFactory):
    old_path = factory.Faker("slug")
    site = factory.SubFactory(wagtail_factories.SiteFactory)
    redirect_page = factory.SubFactory(wagtail_factories.PageFactory)
    redirect_link = factory.Faker("url")
    is_permanent = factory.Faker("boolean")

    class Meta:
        model = Redirect

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """
        Override _create() to ensure that Redirect.clean() is run in order to
        normalise `old_path`.
        @see https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/redirects/models.py#L191
        @see https://github.com/jamescooke/factory_djoy/blob/2744fa062f58f5eb13c68938faf844a0812a3ff6/factory_djoy/factories.py#L21
        """
        obj = model_class(*args, **kwargs)
        try:
            obj.clean()
        except ValidationError as ve:
            message = (
                f"Error building {model_class} with {cls.__name__}.\nBad values:\n"
            )
            for field in ve.error_dict.keys():
                if field == "__all__":
                    message += "  __all__: obj.clean() failed\n"
                else:
                    message += f'  {field}: "{getattr(obj, field)}"\n'
            raise RuntimeError(message) from ve
        obj.save()
        return obj

Needs tests adding :).

ListBlockFactory breaks when passing deeply nested arguments to list items

Trying to use deeply nested arguments to ListBlock items via ListBlockFactory results in the following error:

...
  File "/venv/lib/python3.8/site-packages/wagtail_factories/blocks.py", line 81, in generate
    prefix, label = key.split("__", 2)
ValueError: too many values to unpack (expected 2)

This happens if your list block item factories themselves contain SubFactory declarations:

class ListItemFactory(wagtail_factories.StructBlockFactory):
  class Meta:
    model = ListItem

  some_fk = factory.SubFactory(MyOtherModelFactory)

Then this sort of thing fails:

MyPageFactory(body__0__list__0__some_fk__some_attribute="foo")

I'm pretty sure that the 2 passed to str.split here should be a 1:

prefix, label = key.split("__", 2)

StreamFieldFactory not working

images = wagtail_factories.StreamFieldFactory( {"image": wagtail_factories.ImageChooserBlockFactory}, )

The images attribute is an empty list after I run this factory. What am I doing wrong?

New release

Hi, could we get a new release to pypi?
With wagtail 2 out the door it would be great!

Thanks!

Unit tests are failing when wagtail >= 2.12

Some of the unit tests are failing when a wagtail version of >=2.12 is installed. The failing tests are test_custom_page_streamfield_data_complex, test_custom_page_streamfield_default_blocks, test_custom_page_streamfield and test_custom_page_streamfield_data. I've tried running the tests against Wagtail 2.9.3 and 2.11.7 and they pass.
The issue seems to be that an id attribute has been added to the block in Wagtail >= 2.12.
For example the data returned by the MyTestPageWithStreaFieldFactory call, with Wagtail >= 2.12 looks like this (page.body.streamdata):

[('char_array', ['foo', 'bar'], '35741861-c5ea-45a9-a8f5-8f5de95fe6d8'), ('struct', StructValue([('title', 'My Title'), ('item', StructValue([('label', 'my-label'), ('value', 100)])), ('items', []), ('image', None)]), 'a99daf3d-9055-45f3-a0f2-3b61fd45f58a'), ('int_array', [100], 'b7b60e57-5df2-4996-9916-d8f132ba57be'), ('image', <Image: Blub>, '4138701b-c8fe-45b9-91f9-6e0cdbcce520')]

Note the additional guids which are not expected in the test data.

I will try to fix this issue, but I'm looking for some advise on the best way to proceed. Can we simply drop support for older versions of wagtail and modify the unit tests to expect the additional guids? Or do we need to maintain support for older versions of Wagtail and modify the tests to take account of different versions (EG check the Wagtail version)? Or some other option which I have not thought of?

ListBlockFactory creates non-lists when used in fixture with pytest-factoryboy

pytest-factoryboy allows you to register a factory so that you can use a fixture to get an instance generated by the factory. This doesn't work well with wagtail-factories when you use a ListBlockFactory. The following tests should, in my opinion, pass:

from pytest_factoryboy import register
from wagtail.core.blocks import CharBlock, ListBlock, StructBlock
from wagtail_factories import ListBlockFactory, StructBlockFactory


class InnerBlock(StructBlock):
    text = CharBlock()


@register
class InnerBlockFactory(StructBlockFactory):
    class Meta:
        model = InnerBlock

    text = 'foo'


class OuterBlock(StructBlock):
    foos = ListBlock(InnerBlock)


@register
class OuterBlockFactory(StructBlockFactory):
    class Meta:
        model = OuterBlock

    foos = ListBlockFactory(InnerBlockFactory)


def test_factory():
    outer_block = OuterBlockFactory()
    assert isinstance(outer_block['foos'], list)


def test_fixture(outer_block):
    assert isinstance(outer_block['foos'], list)

However, test_fixture fails. In test_factory, the value of outer_block['foos'] is [] as expected, but in test_fixture, the value of outer_block['foos'] is StructValue([('text', 'foo')]). That is, it is not a list but a list element.

When you register a factory with pytest-factoryboy, it creates a fixture that is supposed to give you an instance generated by the factory. When you register a factory containing a field x that is generated by a SubFactory, pytest-factoryboy will look up the instance corresponding to the fixture of the subfactory and plug that in as the value of x.

I suspect the reason for the test failing is that ListBlockFactory is a subclass of SubFactory. In the code above, pytest-factoryboy will take the instance of the fixture corresponding to ListBlockFactory(InnerBlockFactory) and set foos to this instance. However, due to the way ListBlockFactory is implemented, this instance is an InnerBlock, not a list of inner blocks.

One might argue that pytest-factoryboy is to blame, but I believe it's an issue with ListBlockFactory: As a subtype of SubFactory, it should be possible to safely use ListBlockFactory in any context in which a SubFactory is expected. A SubFactory is supposed generate an instance using its "wrapped factory". However, the wrapped factory of ListBlockFactory is in fact a factory for list elements. Given the assumed semantics of SubFactory, the wrapped factory should generate a list, not an element.

Support StaticBlock inside a streamfield

It seems that the StreamFieldFactory assumes that all blocks will have fields. However a StaticBlock has no fields.

I propose setting the value to True inside a streamfield definition should create the block with no parameters. This could also work for StructBlocks where the field values are already defined in the relevant StructBlockFactory.

    page = MyTestPageWithStreamFieldFactory(
        parent=root_page,
        body__0__char_array__0='foo',
        body__0__char_array__1='bar',
        body__2__int_array__0=100,
        body__1__struct__title='My Title',
        body__1__struct__item__value=100,
        body__1__struct__image__image=None,
        body__3__image__image__title='Blub',
        body__4__static_block=True,
    )

Support StructBlocks defined directly in the model

A struct block doesn't necessarily always have it's own class definition.

From Wagtail's docs (http://docs.wagtail.io/en/v2.0/topics/streamfield.html#structblock):

('person', blocks.StructBlock([
    ('first_name', blocks.CharBlock()),
    ('surname', blocks.CharBlock()),
    ('photo', ImageChooserBlock(required=False)),
    ('biography', blocks.RichTextBlock()),
], icon='user'))

This is an alternate to defining a class:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'

At the moment, wagtail-factories only supports the class-based definition:

class MyTestPageFactory(wagtail_factories.PageFactory):

    body = wagtail_factories.StreamFieldFactory({
        'carousel': MyCarouselFactory
    })

    class Meta:
        model = models.MyTestPage

MyCarouselFactory has to be a subclass of wagtail_factories.StructBlockFactory, which depends on there being a class definition of the struct block.


Hopefully I will get time to look into this more. Has anyone come across this before, or have any ideas about how to solve it?

Support Streamfield with a nested StructBlock

Similar to #12

I have a case like this:

StreamField(
        [
            ('section', StructBlock(
                [
                    ('section_heading_en', TextBlock(label='Heading [en]')),
                    ('pages', ListBlock(
                        PageChooserBlock(label="Page", page_type=[InformationPage, ServicePage]),
                        help_text='Select existing pages in the order you want them \
                        to display within each heading.\
                        Pages should be added only once to any single guide.')
                     ),
                ],
                label="Section"
            )),
        ],
        verbose_name='Add a section header and pages to each section',
        blank=True
    )

This is sorta puesdo code/bad formatting, but ideally I ought to be able to do something like:

    'section': wagtail_factories.StructBlockFactory({
        'section_heading_en': description = factory.Faker('text')
        'pages': ListBlockFactory({
            PageChooserBlockFactory
            })
        })
        }
    )

Not compatable with Factory Boy 3.0

Factory Boy 3.0 was released earlier today with a breaking change to imports.

I'm now getting this error: AttributeError: module 'factory' has no attribute 'DjangoModelFactory

here's the full stack trace:

/blocks/tests/media_block_tests.py:8: in <module>
    import wagtail_factories
/usr/local/lib/python3.8/site-packages/wagtail_factories/__init__.py:1: in <module>
    from .blocks import *
/usr/local/lib/python3.8/site-packages/wagtail_factories/blocks.py:8: in <module>
    from wagtail_factories.factories import ImageFactory
/usr/local/lib/python3.8/site-packages/wagtail_factories/factories.py:42: in <module>
    class MP_NodeFactory(factory.DjangoModelFactory):
E   AttributeError: module 'factory' has no attribute 'DjangoModelFactory'

bump for wagtail 2.14.1?

Looking at the tox.ini ti seems that the latest version of wagtail that's supported is 2.13, could 2.14 support be added?

Add factories for Wagtail's view restriction classes

Here's a barebones example - we may need to add factories for some more related models.

import factory
import factory.fuzzy
import wagtail_factories
from wagtail import models as wagtail_models


class BaseViewRestrictionFactory(factory.django.DjangoModelFactory):
    """
    Factory for Wagtail's BaseViewRestriction model.
    """

    restriction_type = factory.fuzzy.FuzzyChoice(
        wagtail_models.BaseViewRestriction.RESTRICTION_CHOICES
    )
    password = factory.Faker("text", max_nb_chars=20)

    class Meta:
        model = wagtail_models.BaseViewRestriction
        abstract = True


class PageViewRestrictionFactory(BaseViewRestrictionFactory):
    """
    Factory for Wagtail's PageViewRestriction model.
    """

    page = factory.SubFactory(wagtail_factories.PageFactory)

    class Meta:
        model = wagtail_models.PageViewRestriction


class PasswordPageRestrictionFactory(PageViewRestrictionFactory):
    """
    Factory for creating password restrictions for Wagtail pages.
    """

    restriction_type = wagtail_models.BaseViewRestriction.PASSWORD

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

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

No way to specify default values for StreamBlockFactory

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
    }

Future maintenance?

@mvantellingen there have been some PRs open on this project for awhile, as well as changes pushed to master but not tagged in a release.

Would you be interested in more collaboration on this project? I think there are ways the community could build on and flesh out its functionality, despite it not being a super active project.

StructBlockFactory does not support custom StructValue classes

When using the StructBlockFactory with a StructBlock subclass that defines a custom Meta.value_class, this value class is ignored.

I think this section is the cause for that. It seems to return a StructValue, regardless if the block_class defines a value_class.

@classmethod
def _construct_struct_value(cls, block_class, params):
return blocks.StructValue(
block_class(),
[(name, value) for name, value in params.items()],
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.