Example #1
0
    def __init__(self, block_types, use_json_field=None, **kwargs):
        # extract kwargs that are to be passed on to the block, not handled by super
        block_opts = {}
        for arg in ["min_num", "max_num", "block_counts", "collapsed"]:
            if arg in kwargs:
                block_opts[arg] = kwargs.pop(arg)

        # for a top-level block, the 'blank' kwarg (defaulting to False) always overrides the
        # block's own 'required' meta attribute, even if not passed explicitly; this ensures
        # that the field and block have consistent definitions
        block_opts["required"] = not kwargs.get("blank", False)

        super().__init__(**kwargs)

        self.use_json_field = use_json_field

        if isinstance(block_types, Block):
            # use the passed block as the top-level block
            self.stream_block = block_types
        elif isinstance(block_types, type):
            # block passed as a class - instantiate it
            self.stream_block = block_types()
        else:
            # construct a top-level StreamBlock from the list of block types
            self.stream_block = StreamBlock(block_types)

        self.stream_block.set_meta_options(block_opts)
Example #2
0
class StreamPage(Page):
    body = StreamField(
        [
            ("text", CharBlock()),
            ("rich_text", RichTextBlock()),
            ("image", ExtendedImageChooserBlock()),
            (
                "product",
                StructBlock([
                    ("name", CharBlock()),
                    ("price", CharBlock()),
                ]),
            ),
            ("raw_html", RawHTMLBlock()),
            (
                "books",
                StreamBlock([
                    ("title", CharBlock()),
                    ("author", CharBlock()),
                ]),
            ),
        ],
        use_json_field=False,
    )

    api_fields = ("body", )

    content_panels = [
        FieldPanel("title"),
        FieldPanel("body"),
    ]

    preview_modes = []
Example #3
0
class StreamField(models.Field):
    def __init__(self, block_types, use_json_field=None, **kwargs):
        # extract kwargs that are to be passed on to the block, not handled by super
        block_opts = {}
        for arg in ["min_num", "max_num", "block_counts", "collapsed"]:
            if arg in kwargs:
                block_opts[arg] = kwargs.pop(arg)

        # for a top-level block, the 'blank' kwarg (defaulting to False) always overrides the
        # block's own 'required' meta attribute, even if not passed explicitly; this ensures
        # that the field and block have consistent definitions
        block_opts["required"] = not kwargs.get("blank", False)

        super().__init__(**kwargs)

        self.use_json_field = use_json_field

        if isinstance(block_types, Block):
            # use the passed block as the top-level block
            self.stream_block = block_types
        elif isinstance(block_types, type):
            # block passed as a class - instantiate it
            self.stream_block = block_types()
        else:
            # construct a top-level StreamBlock from the list of block types
            self.stream_block = StreamBlock(block_types)

        self.stream_block.set_meta_options(block_opts)

    @property
    def json_field(self):
        return models.JSONField(encoder=DjangoJSONEncoder)

    def _check_json_field(self):
        if type(self.use_json_field) is not bool:
            warnings.warn(
                f"StreamField must explicitly set use_json_field argument to True/False instead of {self.use_json_field}.",
                RemovedInWagtail40Warning,
                stacklevel=3,
            )

    def get_internal_type(self):
        return "JSONField" if self.use_json_field else "TextField"

    def get_lookup(self, lookup_name):
        if self.use_json_field:
            return self.json_field.get_lookup(lookup_name)
        return super().get_lookup(lookup_name)

    def get_transform(self, lookup_name):
        if self.use_json_field:
            return self.json_field.get_transform(lookup_name)
        return super().get_transform(lookup_name)

    def deconstruct(self):
        name, path, _, kwargs = super().deconstruct()
        block_types = list(self.stream_block.child_blocks.items())
        args = [block_types]
        kwargs["use_json_field"] = self.use_json_field
        return name, path, args, kwargs

    def to_python(self, value):
        if value is None or value == "":
            return StreamValue(self.stream_block, [])
        elif isinstance(value, StreamValue):
            return value
        elif isinstance(value, str):
            try:
                unpacked_value = json.loads(value)
            except ValueError:
                # value is not valid JSON; most likely, this field was previously a
                # rich text field before being migrated to StreamField, and the data
                # was left intact in the migration. Return an empty stream instead
                # (but keep the raw text available as an attribute, so that it can be
                # used to migrate that data to StreamField)
                return StreamValue(self.stream_block, [], raw_text=value)

            if unpacked_value is None:
                # we get here if value is the literal string 'null'. This should probably
                # never happen if the rest of the (de)serialization code is working properly,
                # but better to handle it just in case...
                return StreamValue(self.stream_block, [])

            return self.stream_block.to_python(unpacked_value)
        elif (self.use_json_field and value and isinstance(value, list)
              and isinstance(value[0], dict)):
            # The value is already unpacked since JSONField-based StreamField should
            # accept deserialised values (no need to call json.dumps() first).
            # In addition, the value is not a list of (block_name, value) tuples
            # handled in the `else` block.
            return self.stream_block.to_python(value)
        else:
            # See if it looks like the standard non-smart representation of a
            # StreamField value: a list of (block_name, value) tuples
            try:
                [None for (x, y) in value]
            except (TypeError, ValueError):
                # Give up trying to make sense of the value
                raise TypeError(
                    "Cannot handle %r (type %r) as a value of StreamField" %
                    (value, type(value)))

            # Test succeeded, so return as a StreamValue-ified version of that value
            return StreamValue(self.stream_block, value)

    def get_prep_value(self, value):
        if (isinstance(value, StreamValue) and not (value)
                and value.raw_text is not None):
            # An empty StreamValue with a nonempty raw_text attribute should have that
            # raw_text attribute written back to the db. (This is probably only useful
            # for reverse migrations that convert StreamField data back into plain text
            # fields.)
            return value.raw_text
        elif isinstance(value, StreamValue) or not self.use_json_field:
            # StreamValue instances must be prepared first.
            # Before use_json_field was implemented, this is also the value used in queries.
            return json.dumps(self.stream_block.get_prep_value(value),
                              cls=DjangoJSONEncoder)
        else:
            # When querying with JSONField features, the rhs might not be a StreamValue.
            return self.json_field.get_prep_value(value)

    def from_db_value(self, value, expression, connection):
        if self.use_json_field and isinstance(expression, KeyTransform):
            # This could happen when using JSONField key transforms,
            # e.g. Page.object.values('body__0').
            try:
                # We might be able to properly resolve to the appropriate StreamValue
                # based on `expression` and `self.stream_block`, but it might be too
                # complicated to do so. For now, just deserialise the value.
                return json.loads(value)
            except ValueError:
                # Just in case the extracted value is not valid JSON.
                return value

        return self.to_python(value)

    def formfield(self, **kwargs):
        """
        Override formfield to use a plain forms.Field so that we do no transformation on the value
        (as distinct from the usual fallback of forms.CharField, which transforms it into a string).
        """
        defaults = {"form_class": BlockField, "block": self.stream_block}
        defaults.update(kwargs)
        return super().formfield(**defaults)

    def value_to_string(self, obj):
        value = self.value_from_object(obj)
        return self.get_prep_value(value)

    def get_searchable_content(self, value):
        return self.stream_block.get_searchable_content(value)

    def check(self, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(self.stream_block.check(field=self, **kwargs))
        return errors

    def contribute_to_class(self, cls, name, **kwargs):
        super().contribute_to_class(cls, name, **kwargs)

        # Output deprecation warning on missing use_json_field argument, unless this is a fake model
        # for a migration
        if cls.__module__ != "__fake__":
            self._check_json_field()

        # Add Creator descriptor to allow the field to be set from a list or a
        # JSON string.
        setattr(cls, self.name, Creator(self))