浏览代码

feat: pickle support for StreamField (#10654)

Fixes #1988
Antoni Martyniuk 1 年之前
父节点
当前提交
eadf9a6d9c
共有 6 个文件被更改,包括 65 次插入0 次删除
  1. 1 0
      CHANGELOG.txt
  2. 1 0
      CONTRIBUTORS.md
  3. 1 0
      docs/releases/5.2.md
  4. 26 0
      wagtail/blocks/stream_block.py
  5. 11 0
      wagtail/fields.py
  6. 25 0
      wagtail/tests/test_streamfield.py

+ 1 - 0
CHANGELOG.txt

@@ -9,6 +9,7 @@ Changelog
  * Move `SnippetViewSet` menu registration mechanism to base `ViewSet` class (Sage Abdullah)
  * Enable reference index tracking for models registered with `ModelViewSet` (Sage Abdullah)
  * When copying a page or creating an alias, copy its view restrictions to the destination (Sandeep Choudhary, Suyash Singh)
+ * Support pickling of StreamField values (pySilver)
  * Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
  * Docs: Document `WAGTAILADMIN_BASE_URL` on "Integrating Wagtail into a Django project" page (Shreshth Srivastava)
  * Maintenance: Fix snippet search test to work on non-fallback database backends (Matt Westcott)

+ 1 - 0
CONTRIBUTORS.md

@@ -725,6 +725,7 @@
 * Florent Lebreton
 * Shreshth Srivastava
 * Sandeep Choudhary
+* Antoni Martyniuk
 
 ## Translators
 

+ 1 - 0
docs/releases/5.2.md

@@ -19,6 +19,7 @@ depth: 1
  * Move `SnippetViewSet` menu registration mechanism to base `ViewSet` class (Sage Abdullah)
  * Enable reference index tracking for models registered with `ModelViewSet` (Sage Abdullah)
  * When copying a page or creating an alias, copy its view restrictions to the destination (Sandeep Choudhary, Suyash Singh)
+ * Support pickling of StreamField values (pySilver)
 
 ### Bug fixes
 

+ 26 - 0
wagtail/blocks/stream_block.py

@@ -2,9 +2,11 @@ import itertools
 import uuid
 from collections import OrderedDict, defaultdict
 from collections.abc import Mapping, MutableSequence
+from pickle import PickleError
 
 from django import forms
 from django.core.exceptions import ValidationError
+from django.db.models.fields import _load_field
 from django.forms.utils import ErrorList
 from django.utils.functional import cached_property
 from django.utils.html import format_html_join
@@ -720,6 +722,30 @@ class StreamValue(MutableSequence):
     def __str__(self):
         return self.__html__()
 
+    @staticmethod
+    def _deserialize_pickle_value(app_label, model_name, field_name, field_value):
+        """Returns StreamValue from pickled data"""
+        field = _load_field(app_label, model_name, field_name)
+        return field.to_python(field_value)
+
+    def __reduce__(self):
+        try:
+            stream_field = self._stream_field
+        except AttributeError:
+            raise PickleError(
+                "StreamValue can only be pickled if it is associated with a StreamField"
+            )
+
+        return (
+            self._deserialize_pickle_value,
+            (
+                stream_field.model._meta.app_label,
+                stream_field.model._meta.object_name,
+                stream_field.name,
+                self.get_prep_value(),
+            ),
+        )
+
 
 class StreamBlockAdapter(Adapter):
     js_constructor = "wagtail.blocks.StreamBlock"

+ 11 - 0
wagtail/fields.py

@@ -139,6 +139,17 @@ class StreamField(models.Field):
         return name, path, args, kwargs
 
     def to_python(self, value):
+        value = self._to_python(value)
+
+        # The top-level StreamValue is passed a reference to the StreamField, to support
+        # pickling. This is necessary because unpickling needs access to the StreamBlock
+        # definition, which cannot itself be pickled; instead we store a pointer to the
+        # field within the model, which gives us a path to retrieve the StreamBlock definition.
+
+        value._stream_field = self
+        return value
+
+    def _to_python(self, value):
         if value is None or value == "":
             return StreamValue(self.stream_block, [])
         elif isinstance(value, StreamValue):

+ 25 - 0
wagtail/tests/test_streamfield.py

@@ -1,4 +1,5 @@
 import json
+import pickle
 
 from django.apps import apps
 from django.db import connection, models
@@ -11,12 +12,14 @@ from wagtail.blocks import StreamBlockValidationError, StreamValue
 from wagtail.fields import StreamField
 from wagtail.images.models import Image
 from wagtail.images.tests.utils import get_test_image_file
+from wagtail.models import Page
 from wagtail.rich_text import RichText
 from wagtail.signal_handlers import disable_reference_index_auto_update
 from wagtail.test.testapp.models import (
     JSONBlockCountsStreamModel,
     JSONMinMaxCountStreamModel,
     JSONStreamModel,
+    StreamPage,
 )
 
 
@@ -600,3 +603,25 @@ class TestJSONStreamField(TestCase):
         instance = JSONStreamModel.objects.filter(body__contains=value).first()
         self.assertIsNotNone(instance)
         self.assertEqual(instance.id, self.instance.id)
+
+
+class TestStreamFieldPickleSupport(TestCase):
+    def setUp(self):
+        # Find root page
+        self.root_page = Page.objects.get(id=2)
+
+    def test_pickle_support(self):
+        stream_page = StreamPage(title="stream page", body=[("text", "hello")])
+        self.root_page.add_child(instance=stream_page)
+
+        # check that page can be serialized / deserialized
+        serialized = pickle.dumps(stream_page)
+        deserialized = pickle.loads(serialized)
+
+        # check that serialized page can be serialized / deserialized again
+        serialized2 = pickle.dumps(deserialized)
+        deserialized2 = pickle.loads(serialized2)
+
+        # check that page data is not corrupted
+        self.assertEqual(stream_page.body, deserialized.body)
+        self.assertEqual(stream_page.body, deserialized2.body)