Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/contrib/table_block/blocks.py
2024-08-27 20:33:44 +02:00

267 lines
9.3 KiB
Python

import json
from django import forms
from django.core.exceptions import ValidationError
from django.forms.fields import Field
from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from wagtail.admin.staticfiles import versioned_static
from wagtail.blocks import FieldBlock
from wagtail.telepath import register
from wagtail.widget_adapters import WidgetAdapter
DEFAULT_TABLE_OPTIONS = {
"minSpareRows": 0,
"startRows": 3,
"startCols": 3,
"colHeaders": False,
"rowHeaders": False,
"contextMenu": [
"row_above",
"row_below",
"---------",
"col_left",
"col_right",
"---------",
"remove_row",
"remove_col",
"---------",
"undo",
"redo",
],
"editor": "text",
"stretchH": "all",
"height": 108,
"renderer": "text",
"autoColumnSize": False,
}
class TableInput(forms.HiddenInput):
def __init__(self, table_options=None, attrs=None):
self.table_options = table_options
super().__init__(attrs=attrs)
@cached_property
def media(self):
return forms.Media(
css={
"all": [
versioned_static(
"table_block/css/vendor/handsontable-6.2.2.full.min.css"
),
]
},
js=[
versioned_static(
"table_block/js/vendor/handsontable-6.2.2.full.min.js"
),
versioned_static("table_block/js/table.js"),
],
)
class TableInputAdapter(WidgetAdapter):
js_constructor = "wagtail.widgets.TableInput"
def js_args(self, widget):
strings = {
"Row header": _("Row header"),
"Table headers": _("Table headers"),
"Display the first row as a header": _("Display the first row as a header"),
"Display the first column as a header": _(
"Display the first column as a header"
),
"Column header": _("Column header"),
"Display the first row AND first column as headers": _(
"Display the first row AND first column as headers"
),
"No headers": _("No headers"),
"Which cells should be displayed as headers?": _(
"Which cells should be displayed as headers?"
),
"Table caption": _("Table caption"),
"A heading that identifies the overall topic of the table, and is useful for screen reader users.": _(
"A heading that identifies the overall topic of the table, and is useful for screen reader users."
),
"Table": _("Table"),
}
return [
widget.table_options,
strings,
]
register(TableInputAdapter(), TableInput)
class TableBlock(FieldBlock):
def __init__(self, required=True, help_text=None, table_options=None, **kwargs):
"""
CharField's 'label' and 'initial' parameters are not exposed, as Block
handles that functionality natively (via 'label' and 'default')
CharField's 'max_length' and 'min_length' parameters are not exposed as table
data needs to have arbitrary length
"""
self.table_options = self.get_table_options(table_options=table_options)
self.field_options = {"required": required, "help_text": help_text}
super().__init__(**kwargs)
@cached_property
def field(self):
return forms.CharField(
widget=TableInput(table_options=self.table_options), **self.field_options
)
def value_from_form(self, value):
return json.loads(value)
def value_for_form(self, value):
return json.dumps(value)
def to_python(self, value):
"""
If value came from a table block stored before Wagtail 6.0, we need to set an appropriate
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 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
):
value["table_header_choice"] = "both"
elif value.get("first_row_is_table_header", False):
value["table_header_choice"] = "row"
elif value.get("first_col_is_header", False):
value["table_header_choice"] = "col"
else:
value["table_header_choice"] = "neither"
return value
def clean(self, value):
if not value:
return value
if value.get("table_header_choice", ""):
value["first_row_is_table_header"] = value["table_header_choice"] in [
"row",
"both",
]
value["first_col_is_header"] = value["table_header_choice"] in [
"column",
"both",
]
else:
# Ensure we have a choice for the table_header_choice
errors = ErrorList(Field.default_error_messages["required"])
raise ValidationError("Validation error in TableBlock", params=errors)
return self.value_from_form(self.field.clean(self.value_for_form(value)))
def get_form_state(self, value):
# pass state to frontend as a JSON-ish dict - do not serialise to a JSON string
return value
def is_html_renderer(self):
return self.table_options["renderer"] == "html"
def get_searchable_content(self, value):
content = []
if value:
for row in value.get("data", []):
content.extend([v for v in row if v])
return content
def render(self, value, context=None):
template = getattr(self.meta, "template", None)
if template and value:
table_header = (
value["data"][0]
if value.get("data", None)
and len(value["data"]) > 0
and value.get("first_row_is_table_header", False)
else None
)
first_col_is_header = value.get("first_col_is_header", False)
if context is None:
new_context = {}
else:
new_context = dict(context)
new_context.update(
{
"self": value,
self.TEMPLATE_VAR: value,
"table_header": table_header,
"first_col_is_header": first_col_is_header,
"html_renderer": self.is_html_renderer(),
"table_caption": value.get("table_caption"),
"data": value["data"][1:]
if table_header
else value.get("data", []),
}
)
if value.get("cell"):
new_context["classnames"] = {}
new_context["hidden"] = {}
for meta in value["cell"]:
if "className" in meta:
new_context["classnames"][(meta["row"], meta["col"])] = meta[
"className"
]
if "hidden" in meta:
new_context["hidden"][(meta["row"], meta["col"])] = meta[
"hidden"
]
if value.get("mergeCells"):
new_context["spans"] = {}
for merge in value["mergeCells"]:
new_context["spans"][(merge["row"], merge["col"])] = {
"rowspan": merge["rowspan"],
"colspan": merge["colspan"],
}
return render_to_string(template, new_context)
else:
return self.render_basic(value or "", context=context)
def get_table_options(self, table_options=None):
"""
Return a dict of table options using the defaults unless custom options provided
table_options can contain any valid handsontable options:
https://handsontable.com/docs/6.2.2/Options.html
contextMenu: if value from table_options is True, still use default
language: if value is not in table_options, attempt to get from environment
"""
collected_table_options = DEFAULT_TABLE_OPTIONS.copy()
if table_options is not None:
if table_options.get("contextMenu", None) is True:
# explicitly check for True, as value could also be array
# delete to ensure the above default is kept for contextMenu
del table_options["contextMenu"]
collected_table_options.update(table_options)
if "language" not in collected_table_options:
# attempt to gather the current set language of not provided
language = translation.get_language()
collected_table_options["language"] = language
return collected_table_options
class Meta:
default = None
template = "table_block/blocks/table.html"
icon = "table"