We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
`to_python` throws `AttributeError` on `TableBlock` when value is None
### Issue Summary

Similar to: https://github.com/wagtail/wagtail/issues/4822

After upgrading one of our sites to 6.0, attempting to update some of our pages' indices results in a server error.

```
  File "/venv/lib/python3.8/site-packages/wagtail/contrib/table_block/blocks.py", line 135, in to_python
    if not value.get("table_header_choice", ""):
AttributeError: 'NoneType' object has no attribute 'get'
```

This is because, in Wagtail 6.0, the following code has been added:
https://github.com/wagtail/wagtail/commit/fe1a306285da6c48b66506c589abc2c66b51668a#diff-f058d7a36746222950f678ed6b178205778e4e1e5b85f0299c2a7c3b9d6c8939R129

### Steps to Reproduce

1. Create a page with an empty TableBlock in its body
2. Before Wagtail 6.0, this should save and render OK
3. After upgrading to 6.0, editing the same page will throw an error.
4. Looking at the logs, you should see the error above

A workaround is to identify those pages and remove the empty TableBlocks, but this would prove to be a tedious task if there are many page instances.

A possible fix would be to check if the value is not None first:
```
if value and not value.get("table_header_choice", ""):
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/contrib/table_block/blocks.py`
Content:
```
1 import json
2 
3 from django import forms
4 from django.core.exceptions import ValidationError
5 from django.forms.fields import Field
6 from django.forms.utils import ErrorList
7 from django.template.loader import render_to_string
8 from django.utils import translation
9 from django.utils.functional import cached_property
10 from django.utils.translation import gettext as _
11 
12 from wagtail.admin.staticfiles import versioned_static
13 from wagtail.blocks import FieldBlock
14 from wagtail.telepath import register
15 from wagtail.widget_adapters import WidgetAdapter
16 
17 DEFAULT_TABLE_OPTIONS = {
18     "minSpareRows": 0,
19     "startRows": 3,
20     "startCols": 3,
21     "colHeaders": False,
22     "rowHeaders": False,
23     "contextMenu": [
24         "row_above",
25         "row_below",
26         "---------",
27         "col_left",
28         "col_right",
29         "---------",
30         "remove_row",
31         "remove_col",
32         "---------",
33         "undo",
34         "redo",
35     ],
36     "editor": "text",
37     "stretchH": "all",
38     "height": 108,
39     "renderer": "text",
40     "autoColumnSize": False,
41 }
42 
43 
44 class TableInput(forms.HiddenInput):
45     def __init__(self, table_options=None, attrs=None):
46         self.table_options = table_options
47         super().__init__(attrs=attrs)
48 
49     @cached_property
50     def media(self):
51         return forms.Media(
52             css={
53                 "all": [
54                     versioned_static(
55                         "table_block/css/vendor/handsontable-6.2.2.full.min.css"
56                     ),
57                 ]
58             },
59             js=[
60                 versioned_static(
61                     "table_block/js/vendor/handsontable-6.2.2.full.min.js"
62                 ),
63                 versioned_static("table_block/js/table.js"),
64             ],
65         )
66 
67 
68 class TableInputAdapter(WidgetAdapter):
69     js_constructor = "wagtail.widgets.TableInput"
70 
71     def js_args(self, widget):
72         strings = {
73             "Row header": _("Row header"),
74             "Table headers": _("Table headers"),
75             "Display the first row as a header": _("Display the first row as a header"),
76             "Display the first column as a header": _(
77                 "Display the first column as a header"
78             ),
79             "Column header": _("Column header"),
80             "Display the first row AND first column as headers": _(
81                 "Display the first row AND first column as headers"
82             ),
83             "No headers": _("No headers"),
84             "Which cells should be displayed as headers?": _(
85                 "Which cells should be displayed as headers?"
86             ),
87             "Table caption": _("Table caption"),
88             "A heading that identifies the overall topic of the table, and is useful for screen reader users.": _(
89                 "A heading that identifies the overall topic of the table, and is useful for screen reader users."
90             ),
91             "Table": _("Table"),
92         }
93 
94         return [
95             widget.table_options,
96             strings,
97         ]
98 
99 
100 register(TableInputAdapter(), TableInput)
101 
102 
103 class TableBlock(FieldBlock):
104     def __init__(self, required=True, help_text=None, table_options=None, **kwargs):
105         """
106         CharField's 'label' and 'initial' parameters are not exposed, as Block
107         handles that functionality natively (via 'label' and 'default')
108 
109         CharField's 'max_length' and 'min_length' parameters are not exposed as table
110         data needs to have arbitrary length
111         """
112         self.table_options = self.get_table_options(table_options=table_options)
113         self.field_options = {"required": required, "help_text": help_text}
114 
115         super().__init__(**kwargs)
116 
117     @cached_property
118     def field(self):
119         return forms.CharField(
120             widget=TableInput(table_options=self.table_options), **self.field_options
121         )
122 
123     def value_from_form(self, value):
124         return json.loads(value)
125 
126     def value_for_form(self, value):
127         return json.dumps(value)
128 
129     def to_python(self, value):
130         """
131         If value came from a table block stored before Wagtail 6.0, we need to set an appropriate
132         value for the header choice. I would really like to have this default to "" and force the
133         editor to reaffirm they don't want any headers, but that would be a breaking change.
134         """
135         if not value.get("table_header_choice", ""):
136             if value.get("first_row_is_table_header", False) and value.get(
137                 "first_col_is_header", False
138             ):
139                 value["table_header_choice"] = "both"
140             elif value.get("first_row_is_table_header", False):
141                 value["table_header_choice"] = "row"
142             elif value.get("first_col_is_header", False):
143                 value["table_header_choice"] = "col"
144             else:
145                 value["table_header_choice"] = "neither"
146         return value
147 
148     def clean(self, value):
149         if not value:
150             return value
151 
152         if value.get("table_header_choice", ""):
153             value["first_row_is_table_header"] = value["table_header_choice"] in [
154                 "row",
155                 "both",
156             ]
157             value["first_col_is_header"] = value["table_header_choice"] in [
158                 "column",
159                 "both",
160             ]
161         else:
162             # Ensure we have a choice for the table_header_choice
163             errors = ErrorList(Field.default_error_messages["required"])
164             raise ValidationError("Validation error in TableBlock", params=errors)
165         return self.value_from_form(self.field.clean(self.value_for_form(value)))
166 
167     def get_form_state(self, value):
168         # pass state to frontend as a JSON-ish dict - do not serialise to a JSON string
169         return value
170 
171     def is_html_renderer(self):
172         return self.table_options["renderer"] == "html"
173 
174     def get_searchable_content(self, value):
175         content = []
176         if value:
177             for row in value.get("data", []):
178                 content.extend([v for v in row if v])
179         return content
180 
181     def render(self, value, context=None):
182         template = getattr(self.meta, "template", None)
183         if template and value:
184             table_header = (
185                 value["data"][0]
186                 if value.get("data", None)
187                 and len(value["data"]) > 0
188                 and value.get("first_row_is_table_header", False)
189                 else None
190             )
191             first_col_is_header = value.get("first_col_is_header", False)
192 
193             if context is None:
194                 new_context = {}
195             else:
196                 new_context = dict(context)
197 
198             new_context.update(
199                 {
200                     "self": value,
201                     self.TEMPLATE_VAR: value,
202                     "table_header": table_header,
203                     "first_col_is_header": first_col_is_header,
204                     "html_renderer": self.is_html_renderer(),
205                     "table_caption": value.get("table_caption"),
206                     "data": value["data"][1:]
207                     if table_header
208                     else value.get("data", []),
209                 }
210             )
211 
212             if value.get("cell"):
213                 new_context["classnames"] = {}
214                 new_context["hidden"] = {}
215                 for meta in value["cell"]:
216                     if "className" in meta:
217                         new_context["classnames"][(meta["row"], meta["col"])] = meta[
218                             "className"
219                         ]
220                     if "hidden" in meta:
221                         new_context["hidden"][(meta["row"], meta["col"])] = meta[
222                             "hidden"
223                         ]
224 
225             if value.get("mergeCells"):
226                 new_context["spans"] = {}
227                 for merge in value["mergeCells"]:
228                     new_context["spans"][(merge["row"], merge["col"])] = {
229                         "rowspan": merge["rowspan"],
230                         "colspan": merge["colspan"],
231                     }
232 
233             return render_to_string(template, new_context)
234         else:
235             return self.render_basic(value or "", context=context)
236 
237     def get_table_options(self, table_options=None):
238         """
239         Return a dict of table options using the defaults unless custom options provided
240 
241         table_options can contain any valid handsontable options:
242         https://handsontable.com/docs/6.2.2/Options.html
243         contextMenu: if value from table_options is True, still use default
244         language: if value is not in table_options, attempt to get from environment
245         """
246 
247         collected_table_options = DEFAULT_TABLE_OPTIONS.copy()
248 
249         if table_options is not None:
250             if table_options.get("contextMenu", None) is True:
251                 # explicitly check for True, as value could also be array
252                 # delete to ensure the above default is kept for contextMenu
253                 del table_options["contextMenu"]
254             collected_table_options.update(table_options)
255 
256         if "language" not in collected_table_options:
257             # attempt to gather the current set language of not provided
258             language = translation.get_language()
259             collected_table_options["language"] = language
260 
261         return collected_table_options
262 
263     class Meta:
264         default = None
265         template = "table_block/blocks/table.html"
266         icon = "table"
267
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/wagtail/contrib/table_block/blocks.py b/wagtail/contrib/table_block/blocks.py
--- a/wagtail/contrib/table_block/blocks.py
+++ b/wagtail/contrib/table_block/blocks.py
@@ -132,7 +132,7 @@
         value for the header choice. I would really like to have this default to "" and force the
         editor to reaffirm they don't want any headers, but that would be a breaking change.
         """
-        if not value.get("table_header_choice", ""):
+        if value and not value.get("table_header_choice", ""):
             if value.get("first_row_is_table_header", False) and value.get(
                 "first_col_is_header", False
             ):

{"golden_diff": "diff --git a/wagtail/contrib/table_block/blocks.py b/wagtail/contrib/table_block/blocks.py\n--- a/wagtail/contrib/table_block/blocks.py\n+++ b/wagtail/contrib/table_block/blocks.py\n@@ -132,7 +132,7 @@\n         value for the header choice. I would really like to have this default to \"\" and force the\n         editor to reaffirm they don't want any headers, but that would be a breaking change.\n         \"\"\"\n-        if not value.get(\"table_header_choice\", \"\"):\n+        if value and not value.get(\"table_header_choice\", \"\"):\n             if value.get(\"first_row_is_table_header\", False) and value.get(\n                 \"first_col_is_header\", False\n             ):\n", "issue": "`to_python` throws `AttributeError` on `TableBlock` when value is None\n### Issue Summary\r\n\r\nSimilar to: https://github.com/wagtail/wagtail/issues/4822\r\n\r\nAfter upgrading one of our sites to 6.0, attempting to update some of our pages' indices results in a server error.\r\n\r\n```\r\n  File \"/venv/lib/python3.8/site-packages/wagtail/contrib/table_block/blocks.py\", line 135, in to_python\r\n    if not value.get(\"table_header_choice\", \"\"):\r\nAttributeError: 'NoneType' object has no attribute 'get'\r\n```\r\n\r\nThis is because, in Wagtail 6.0, the following code has been added:\r\nhttps://github.com/wagtail/wagtail/commit/fe1a306285da6c48b66506c589abc2c66b51668a#diff-f058d7a36746222950f678ed6b178205778e4e1e5b85f0299c2a7c3b9d6c8939R129\r\n\r\n### Steps to Reproduce\r\n\r\n1. Create a page with an empty TableBlock in its body\r\n2. Before Wagtail 6.0, this should save and render OK\r\n3. After upgrading to 6.0, editing the same page will throw an error.\r\n4. Looking at the logs, you should see the error above\r\n\r\nA workaround is to identify those pages and remove the empty TableBlocks, but this would prove to be a tedious task if there are many page instances.\r\n\r\nA possible fix would be to check if the value is not None first:\r\n```\r\nif value and not value.get(\"table_header_choice\", \"\"):\r\n```\n", "before_files": [{"content": "import json\n\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.forms.fields import Field\nfrom django.forms.utils import ErrorList\nfrom django.template.loader import render_to_string\nfrom django.utils import translation\nfrom django.utils.functional import cached_property\nfrom django.utils.translation import gettext as _\n\nfrom wagtail.admin.staticfiles import versioned_static\nfrom wagtail.blocks import FieldBlock\nfrom wagtail.telepath import register\nfrom wagtail.widget_adapters import WidgetAdapter\n\nDEFAULT_TABLE_OPTIONS = {\n    \"minSpareRows\": 0,\n    \"startRows\": 3,\n    \"startCols\": 3,\n    \"colHeaders\": False,\n    \"rowHeaders\": False,\n    \"contextMenu\": [\n        \"row_above\",\n        \"row_below\",\n        \"---------\",\n        \"col_left\",\n        \"col_right\",\n        \"---------\",\n        \"remove_row\",\n        \"remove_col\",\n        \"---------\",\n        \"undo\",\n        \"redo\",\n    ],\n    \"editor\": \"text\",\n    \"stretchH\": \"all\",\n    \"height\": 108,\n    \"renderer\": \"text\",\n    \"autoColumnSize\": False,\n}\n\n\nclass TableInput(forms.HiddenInput):\n    def __init__(self, table_options=None, attrs=None):\n        self.table_options = table_options\n        super().__init__(attrs=attrs)\n\n    @cached_property\n    def media(self):\n        return forms.Media(\n            css={\n                \"all\": [\n                    versioned_static(\n                        \"table_block/css/vendor/handsontable-6.2.2.full.min.css\"\n                    ),\n                ]\n            },\n            js=[\n                versioned_static(\n                    \"table_block/js/vendor/handsontable-6.2.2.full.min.js\"\n                ),\n                versioned_static(\"table_block/js/table.js\"),\n            ],\n        )\n\n\nclass TableInputAdapter(WidgetAdapter):\n    js_constructor = \"wagtail.widgets.TableInput\"\n\n    def js_args(self, widget):\n        strings = {\n            \"Row header\": _(\"Row header\"),\n            \"Table headers\": _(\"Table headers\"),\n            \"Display the first row as a header\": _(\"Display the first row as a header\"),\n            \"Display the first column as a header\": _(\n                \"Display the first column as a header\"\n            ),\n            \"Column header\": _(\"Column header\"),\n            \"Display the first row AND first column as headers\": _(\n                \"Display the first row AND first column as headers\"\n            ),\n            \"No headers\": _(\"No headers\"),\n            \"Which cells should be displayed as headers?\": _(\n                \"Which cells should be displayed as headers?\"\n            ),\n            \"Table caption\": _(\"Table caption\"),\n            \"A heading that identifies the overall topic of the table, and is useful for screen reader users.\": _(\n                \"A heading that identifies the overall topic of the table, and is useful for screen reader users.\"\n            ),\n            \"Table\": _(\"Table\"),\n        }\n\n        return [\n            widget.table_options,\n            strings,\n        ]\n\n\nregister(TableInputAdapter(), TableInput)\n\n\nclass TableBlock(FieldBlock):\n    def __init__(self, required=True, help_text=None, table_options=None, **kwargs):\n        \"\"\"\n        CharField's 'label' and 'initial' parameters are not exposed, as Block\n        handles that functionality natively (via 'label' and 'default')\n\n        CharField's 'max_length' and 'min_length' parameters are not exposed as table\n        data needs to have arbitrary length\n        \"\"\"\n        self.table_options = self.get_table_options(table_options=table_options)\n        self.field_options = {\"required\": required, \"help_text\": help_text}\n\n        super().__init__(**kwargs)\n\n    @cached_property\n    def field(self):\n        return forms.CharField(\n            widget=TableInput(table_options=self.table_options), **self.field_options\n        )\n\n    def value_from_form(self, value):\n        return json.loads(value)\n\n    def value_for_form(self, value):\n        return json.dumps(value)\n\n    def to_python(self, value):\n        \"\"\"\n        If value came from a table block stored before Wagtail 6.0, we need to set an appropriate\n        value for the header choice. I would really like to have this default to \"\" and force the\n        editor to reaffirm they don't want any headers, but that would be a breaking change.\n        \"\"\"\n        if not value.get(\"table_header_choice\", \"\"):\n            if value.get(\"first_row_is_table_header\", False) and value.get(\n                \"first_col_is_header\", False\n            ):\n                value[\"table_header_choice\"] = \"both\"\n            elif value.get(\"first_row_is_table_header\", False):\n                value[\"table_header_choice\"] = \"row\"\n            elif value.get(\"first_col_is_header\", False):\n                value[\"table_header_choice\"] = \"col\"\n            else:\n                value[\"table_header_choice\"] = \"neither\"\n        return value\n\n    def clean(self, value):\n        if not value:\n            return value\n\n        if value.get(\"table_header_choice\", \"\"):\n            value[\"first_row_is_table_header\"] = value[\"table_header_choice\"] in [\n                \"row\",\n                \"both\",\n            ]\n            value[\"first_col_is_header\"] = value[\"table_header_choice\"] in [\n                \"column\",\n                \"both\",\n            ]\n        else:\n            # Ensure we have a choice for the table_header_choice\n            errors = ErrorList(Field.default_error_messages[\"required\"])\n            raise ValidationError(\"Validation error in TableBlock\", params=errors)\n        return self.value_from_form(self.field.clean(self.value_for_form(value)))\n\n    def get_form_state(self, value):\n        # pass state to frontend as a JSON-ish dict - do not serialise to a JSON string\n        return value\n\n    def is_html_renderer(self):\n        return self.table_options[\"renderer\"] == \"html\"\n\n    def get_searchable_content(self, value):\n        content = []\n        if value:\n            for row in value.get(\"data\", []):\n                content.extend([v for v in row if v])\n        return content\n\n    def render(self, value, context=None):\n        template = getattr(self.meta, \"template\", None)\n        if template and value:\n            table_header = (\n                value[\"data\"][0]\n                if value.get(\"data\", None)\n                and len(value[\"data\"]) > 0\n                and value.get(\"first_row_is_table_header\", False)\n                else None\n            )\n            first_col_is_header = value.get(\"first_col_is_header\", False)\n\n            if context is None:\n                new_context = {}\n            else:\n                new_context = dict(context)\n\n            new_context.update(\n                {\n                    \"self\": value,\n                    self.TEMPLATE_VAR: value,\n                    \"table_header\": table_header,\n                    \"first_col_is_header\": first_col_is_header,\n                    \"html_renderer\": self.is_html_renderer(),\n                    \"table_caption\": value.get(\"table_caption\"),\n                    \"data\": value[\"data\"][1:]\n                    if table_header\n                    else value.get(\"data\", []),\n                }\n            )\n\n            if value.get(\"cell\"):\n                new_context[\"classnames\"] = {}\n                new_context[\"hidden\"] = {}\n                for meta in value[\"cell\"]:\n                    if \"className\" in meta:\n                        new_context[\"classnames\"][(meta[\"row\"], meta[\"col\"])] = meta[\n                            \"className\"\n                        ]\n                    if \"hidden\" in meta:\n                        new_context[\"hidden\"][(meta[\"row\"], meta[\"col\"])] = meta[\n                            \"hidden\"\n                        ]\n\n            if value.get(\"mergeCells\"):\n                new_context[\"spans\"] = {}\n                for merge in value[\"mergeCells\"]:\n                    new_context[\"spans\"][(merge[\"row\"], merge[\"col\"])] = {\n                        \"rowspan\": merge[\"rowspan\"],\n                        \"colspan\": merge[\"colspan\"],\n                    }\n\n            return render_to_string(template, new_context)\n        else:\n            return self.render_basic(value or \"\", context=context)\n\n    def get_table_options(self, table_options=None):\n        \"\"\"\n        Return a dict of table options using the defaults unless custom options provided\n\n        table_options can contain any valid handsontable options:\n        https://handsontable.com/docs/6.2.2/Options.html\n        contextMenu: if value from table_options is True, still use default\n        language: if value is not in table_options, attempt to get from environment\n        \"\"\"\n\n        collected_table_options = DEFAULT_TABLE_OPTIONS.copy()\n\n        if table_options is not None:\n            if table_options.get(\"contextMenu\", None) is True:\n                # explicitly check for True, as value could also be array\n                # delete to ensure the above default is kept for contextMenu\n                del table_options[\"contextMenu\"]\n            collected_table_options.update(table_options)\n\n        if \"language\" not in collected_table_options:\n            # attempt to gather the current set language of not provided\n            language = translation.get_language()\n            collected_table_options[\"language\"] = language\n\n        return collected_table_options\n\n    class Meta:\n        default = None\n        template = \"table_block/blocks/table.html\"\n        icon = \"table\"\n", "path": "wagtail/contrib/table_block/blocks.py"}], "after_files": [{"content": "import json\n\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.forms.fields import Field\nfrom django.forms.utils import ErrorList\nfrom django.template.loader import render_to_string\nfrom django.utils import translation\nfrom django.utils.functional import cached_property\nfrom django.utils.translation import gettext as _\n\nfrom wagtail.admin.staticfiles import versioned_static\nfrom wagtail.blocks import FieldBlock\nfrom wagtail.telepath import register\nfrom wagtail.widget_adapters import WidgetAdapter\n\nDEFAULT_TABLE_OPTIONS = {\n    \"minSpareRows\": 0,\n    \"startRows\": 3,\n    \"startCols\": 3,\n    \"colHeaders\": False,\n    \"rowHeaders\": False,\n    \"contextMenu\": [\n        \"row_above\",\n        \"row_below\",\n        \"---------\",\n        \"col_left\",\n        \"col_right\",\n        \"---------\",\n        \"remove_row\",\n        \"remove_col\",\n        \"---------\",\n        \"undo\",\n        \"redo\",\n    ],\n    \"editor\": \"text\",\n    \"stretchH\": \"all\",\n    \"height\": 108,\n    \"renderer\": \"text\",\n    \"autoColumnSize\": False,\n}\n\n\nclass TableInput(forms.HiddenInput):\n    def __init__(self, table_options=None, attrs=None):\n        self.table_options = table_options\n        super().__init__(attrs=attrs)\n\n    @cached_property\n    def media(self):\n        return forms.Media(\n            css={\n                \"all\": [\n                    versioned_static(\n                        \"table_block/css/vendor/handsontable-6.2.2.full.min.css\"\n                    ),\n                ]\n            },\n            js=[\n                versioned_static(\n                    \"table_block/js/vendor/handsontable-6.2.2.full.min.js\"\n                ),\n                versioned_static(\"table_block/js/table.js\"),\n            ],\n        )\n\n\nclass TableInputAdapter(WidgetAdapter):\n    js_constructor = \"wagtail.widgets.TableInput\"\n\n    def js_args(self, widget):\n        strings = {\n            \"Row header\": _(\"Row header\"),\n            \"Table headers\": _(\"Table headers\"),\n            \"Display the first row as a header\": _(\"Display the first row as a header\"),\n            \"Display the first column as a header\": _(\n                \"Display the first column as a header\"\n            ),\n            \"Column header\": _(\"Column header\"),\n            \"Display the first row AND first column as headers\": _(\n                \"Display the first row AND first column as headers\"\n            ),\n            \"No headers\": _(\"No headers\"),\n            \"Which cells should be displayed as headers?\": _(\n                \"Which cells should be displayed as headers?\"\n            ),\n            \"Table caption\": _(\"Table caption\"),\n            \"A heading that identifies the overall topic of the table, and is useful for screen reader users.\": _(\n                \"A heading that identifies the overall topic of the table, and is useful for screen reader users.\"\n            ),\n            \"Table\": _(\"Table\"),\n        }\n\n        return [\n            widget.table_options,\n            strings,\n        ]\n\n\nregister(TableInputAdapter(), TableInput)\n\n\nclass TableBlock(FieldBlock):\n    def __init__(self, required=True, help_text=None, table_options=None, **kwargs):\n        \"\"\"\n        CharField's 'label' and 'initial' parameters are not exposed, as Block\n        handles that functionality natively (via 'label' and 'default')\n\n        CharField's 'max_length' and 'min_length' parameters are not exposed as table\n        data needs to have arbitrary length\n        \"\"\"\n        self.table_options = self.get_table_options(table_options=table_options)\n        self.field_options = {\"required\": required, \"help_text\": help_text}\n\n        super().__init__(**kwargs)\n\n    @cached_property\n    def field(self):\n        return forms.CharField(\n            widget=TableInput(table_options=self.table_options), **self.field_options\n        )\n\n    def value_from_form(self, value):\n        return json.loads(value)\n\n    def value_for_form(self, value):\n        return json.dumps(value)\n\n    def to_python(self, value):\n        \"\"\"\n        If value came from a table block stored before Wagtail 6.0, we need to set an appropriate\n        value for the header choice. I would really like to have this default to \"\" and force the\n        editor to reaffirm they don't want any headers, but that would be a breaking change.\n        \"\"\"\n        if value and not value.get(\"table_header_choice\", \"\"):\n            if value.get(\"first_row_is_table_header\", False) and value.get(\n                \"first_col_is_header\", False\n            ):\n                value[\"table_header_choice\"] = \"both\"\n            elif value.get(\"first_row_is_table_header\", False):\n                value[\"table_header_choice\"] = \"row\"\n            elif value.get(\"first_col_is_header\", False):\n                value[\"table_header_choice\"] = \"col\"\n            else:\n                value[\"table_header_choice\"] = \"neither\"\n        return value\n\n    def clean(self, value):\n        if not value:\n            return value\n\n        if value.get(\"table_header_choice\", \"\"):\n            value[\"first_row_is_table_header\"] = value[\"table_header_choice\"] in [\n                \"row\",\n                \"both\",\n            ]\n            value[\"first_col_is_header\"] = value[\"table_header_choice\"] in [\n                \"column\",\n                \"both\",\n            ]\n        else:\n            # Ensure we have a choice for the table_header_choice\n            errors = ErrorList(Field.default_error_messages[\"required\"])\n            raise ValidationError(\"Validation error in TableBlock\", params=errors)\n        return self.value_from_form(self.field.clean(self.value_for_form(value)))\n\n    def get_form_state(self, value):\n        # pass state to frontend as a JSON-ish dict - do not serialise to a JSON string\n        return value\n\n    def is_html_renderer(self):\n        return self.table_options[\"renderer\"] == \"html\"\n\n    def get_searchable_content(self, value):\n        content = []\n        if value:\n            for row in value.get(\"data\", []):\n                content.extend([v for v in row if v])\n        return content\n\n    def render(self, value, context=None):\n        template = getattr(self.meta, \"template\", None)\n        if template and value:\n            table_header = (\n                value[\"data\"][0]\n                if value.get(\"data\", None)\n                and len(value[\"data\"]) > 0\n                and value.get(\"first_row_is_table_header\", False)\n                else None\n            )\n            first_col_is_header = value.get(\"first_col_is_header\", False)\n\n            if context is None:\n                new_context = {}\n            else:\n                new_context = dict(context)\n\n            new_context.update(\n                {\n                    \"self\": value,\n                    self.TEMPLATE_VAR: value,\n                    \"table_header\": table_header,\n                    \"first_col_is_header\": first_col_is_header,\n                    \"html_renderer\": self.is_html_renderer(),\n                    \"table_caption\": value.get(\"table_caption\"),\n                    \"data\": value[\"data\"][1:]\n                    if table_header\n                    else value.get(\"data\", []),\n                }\n            )\n\n            if value.get(\"cell\"):\n                new_context[\"classnames\"] = {}\n                new_context[\"hidden\"] = {}\n                for meta in value[\"cell\"]:\n                    if \"className\" in meta:\n                        new_context[\"classnames\"][(meta[\"row\"], meta[\"col\"])] = meta[\n                            \"className\"\n                        ]\n                    if \"hidden\" in meta:\n                        new_context[\"hidden\"][(meta[\"row\"], meta[\"col\"])] = meta[\n                            \"hidden\"\n                        ]\n\n            if value.get(\"mergeCells\"):\n                new_context[\"spans\"] = {}\n                for merge in value[\"mergeCells\"]:\n                    new_context[\"spans\"][(merge[\"row\"], merge[\"col\"])] = {\n                        \"rowspan\": merge[\"rowspan\"],\n                        \"colspan\": merge[\"colspan\"],\n                    }\n\n            return render_to_string(template, new_context)\n        else:\n            return self.render_basic(value or \"\", context=context)\n\n    def get_table_options(self, table_options=None):\n        \"\"\"\n        Return a dict of table options using the defaults unless custom options provided\n\n        table_options can contain any valid handsontable options:\n        https://handsontable.com/docs/6.2.2/Options.html\n        contextMenu: if value from table_options is True, still use default\n        language: if value is not in table_options, attempt to get from environment\n        \"\"\"\n\n        collected_table_options = DEFAULT_TABLE_OPTIONS.copy()\n\n        if table_options is not None:\n            if table_options.get(\"contextMenu\", None) is True:\n                # explicitly check for True, as value could also be array\n                # delete to ensure the above default is kept for contextMenu\n                del table_options[\"contextMenu\"]\n            collected_table_options.update(table_options)\n\n        if \"language\" not in collected_table_options:\n            # attempt to gather the current set language of not provided\n            language = translation.get_language()\n            collected_table_options[\"language\"] = language\n\n        return collected_table_options\n\n    class Meta:\n        default = None\n        template = \"table_block/blocks/table.html\"\n        icon = \"table\"\n", "path": "wagtail/contrib/table_block/blocks.py"}]}