0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Move to a select field for determining the way TableBlock will use headers

- Make the new table_header_choice select backwards compatible with tables stored before Wagtail 6.0
- Built on previous PRs #9673 & #6763
- Fixes #5989
This commit is contained in:
DevilsAutumn 2023-12-09 16:20:57 +05:30 committed by LB (Ben Johnston)
parent a4c18b4957
commit fe1a306285
7 changed files with 194 additions and 96 deletions

View File

@ -38,6 +38,7 @@ Changelog
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
* Add ability to customise a page's copy form (Neeraj Yetheendran)
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
* Switch the `TableBlock` header controls to a field that requires user input (Bhuvnesh Sharma, Aman Pandey, Cynthia Kiser)
* Fix: Update system check for overwriting storage backends to recognise the `STORAGES` setting introduced in Django 4.2 (phijma-leukeleu)
* Fix: Prevent password change form from raising a validation error when browser autocomplete fills in the "Old password" field (Chiemezuo Akujobi)
* Fix: Ensure that the legacy dropdown options, when closed, do not get accidentally clicked by other interactions wide viewports (CheesyPhoenix, Christer Jensen)

View File

@ -3,26 +3,23 @@
exports[`telepath: wagtail.widgets.TableInput it renders correctly 1`] = `
"<div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-header">Row header</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field="">
<div class="w-field__help" id="the-id-handsontable-header-helptext" data-field-help="">
<div class="help">Display the first row as a header.</div>
</div>
<div class="w-field__input" data-field-input="">
<input type="checkbox" id="the-id-handsontable-header" name="handsontable-header" aria-describedby="the-id-handsontable-header-helptext">
</div>
</div>
</div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-col-header">Column header</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field="">
<div class="w-field__help" id="the-id-handsontable-col-header-helptext" data-field-help="">
<div class="help">Display the first column as a header.</div>
</div>
<div class="w-field__input" data-field-input="">
<input type="checkbox" id="the-id-handsontable-col-header" name="handsontable-col-header" aria-describedby="the-id-handsontable-col-header-helptext">
</div>
</div>
<label class="w-field__label" for="the-id-table-header-choice">Table headers</label>
<select id="the-id-table-header-choice" name="table-header-choice">
<option value="">Select a header option</option>
<option value="row">
Display the first row as a header
</option>
<option value="column">
Display the first column as a header
</option>
<option value="both">
Display the first row AND first column as headers
</option>
<option value="neither">
No headers
</option>
</select>
<p class="help">Which cells should be displayed as headers?</p>
</div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-col-caption">Table caption</label>
@ -43,26 +40,23 @@ exports[`telepath: wagtail.widgets.TableInput it renders correctly 1`] = `
exports[`telepath: wagtail.widgets.TableInput translation 1`] = `
"<div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-header">En-tête de ligne</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field="">
<div class="w-field__help" id="the-id-handsontable-header-helptext" data-field-help="">
<div class="help">Affichez la première ligne sous forme d'en-tête.</div>
</div>
<div class="w-field__input" data-field-input="">
<input type="checkbox" id="the-id-handsontable-header" name="handsontable-header" aria-describedby="the-id-handsontable-header-helptext">
</div>
</div>
</div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-col-header">En-tête de colonne</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field="">
<div class="w-field__help" id="the-id-handsontable-col-header-helptext" data-field-help="">
<div class="help">Affichez la première colonne sous forme d'en-tête.</div>
</div>
<div class="w-field__input" data-field-input="">
<input type="checkbox" id="the-id-handsontable-col-header" name="handsontable-col-header" aria-describedby="the-id-handsontable-col-header-helptext">
</div>
</div>
<label class="w-field__label" for="the-id-table-header-choice">En-têtes de tableau</label>
<select id="the-id-table-header-choice" name="table-header-choice">
<option value="">Select a header option</option>
<option value="row">
Afficher la première ligne sous forme d'en-tête
</option>
<option value="column">
Afficher la première colonne sous forme d'en-tête
</option>
<option value="both">
Afficher la première ligne ET la première colonne sous forme d'en-têtes
</option>
<option value="neither">
Pas d'en-têtes
</option>
</select>
<p class="help">Quelles cellules doivent être affichées en tant qu'en-têtes?</p>
</div>
<div class="w-field__wrapper" data-field-wrapper="">
<label class="w-field__label" for="the-id-handsontable-col-caption">Légende du tableau</label>

View File

@ -7,12 +7,14 @@ import { hasOwn } from '../../../utils/hasOwn';
function initTable(id, tableOptions) {
const containerId = id + '-handsontable-container';
const tableHeaderCheckboxId = id + '-handsontable-header';
const colHeaderCheckboxId = id + '-handsontable-col-header';
var tableHeaderId = id + '-handsontable-header';
var colHeaderId = id + '-handsontable-col-header';
var headerChoiceId = id + '-table-header-choice';
const tableCaptionId = id + '-handsontable-col-caption';
const hiddenStreamInput = $('#' + id);
const tableHeaderCheckbox = $('#' + tableHeaderCheckboxId);
const colHeaderCheckbox = $('#' + colHeaderCheckboxId);
var tableHeader = $('#' + tableHeaderId);
var colHeader = $('#' + colHeaderId);
var headerChoice = $('#' + headerChoiceId);
const tableCaption = $('#' + tableCaptionId);
const finalOptions = {};
let hot = null;
@ -52,18 +54,12 @@ function initTable(id, tableOptions) {
}
if (dataForForm !== null) {
if (hasOwn(dataForForm, 'first_row_is_table_header')) {
tableHeaderCheckbox.prop(
'checked',
dataForForm.first_row_is_table_header,
);
}
if (hasOwn(dataForForm, 'first_col_is_header')) {
colHeaderCheckbox.prop('checked', dataForForm.first_col_is_header);
}
if (hasOwn(dataForForm, 'table_caption')) {
tableCaption.prop('value', dataForForm.table_caption);
}
if (hasOwn(dataForForm, 'table_header_choice')) {
headerChoice.prop('value', dataForForm.table_header_choice);
}
}
if (!hasOwn(tableOptions, 'width') || !hasOwn(tableOptions, 'height')) {
@ -123,8 +119,9 @@ function initTable(id, tableOptions) {
data: hot.getData(),
cell: cell,
mergeCells: mergeCells,
first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
first_col_is_header: colHeaderCheckbox.prop('checked'),
first_row_is_table_header: tableHeader.val(),
first_col_is_header: colHeader.val(),
table_header_choice: headerChoice.val(),
table_caption: tableCaption.val(),
}),
);
@ -169,11 +166,7 @@ function initTable(id, tableOptions) {
persist();
};
tableHeaderCheckbox.on('change', () => {
persist();
});
colHeaderCheckbox.on('change', () => {
headerChoice.on('change', () => {
persist();
});
@ -238,26 +231,23 @@ class TableInput {
const container = document.createElement('div');
container.innerHTML = `
<div class="w-field__wrapper" data-field-wrapper>
<label class="w-field__label" for="${id}-handsontable-header">${this.strings['Row header']}</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field>
<div class="w-field__help" id="${id}-handsontable-header-helptext" data-field-help>
<div class="help">${this.strings['Display the first row as a header.']}</div>
</div>
<div class="w-field__input" data-field-input>
<input type="checkbox" id="${id}-handsontable-header" name="handsontable-header" aria-describedby="${id}-handsontable-header-helptext" />
</div>
</div>
</div>
<div class="w-field__wrapper" data-field-wrapper>
<label class="w-field__label" for="${id}-handsontable-col-header">${this.strings['Column header']}</label>
<div class="w-field w-field--boolean_field w-field--checkbox_input" data-field>
<div class="w-field__help" id="${id}-handsontable-col-header-helptext" data-field-help>
<div class="help">${this.strings['Display the first column as a header.']}</div>
</div>
<div class="w-field__input" data-field-input>
<input type="checkbox" id="${id}-handsontable-col-header" name="handsontable-col-header" aria-describedby="${id}-handsontable-col-header-helptext" />
</div>
</div>
<label class="w-field__label" for="${id}-table-header-choice">${this.strings['Table headers']}</label>
<select id="${id}-table-header-choice" name="table-header-choice">
<option value="">Select a header option</option>
<option value="row">
${this.strings['Display the first row as a header']}
</option>
<option value="column">
${this.strings['Display the first column as a header']}
</option>
<option value="both">
${this.strings['Display the first row AND first column as headers']}
</option>
<option value="neither">
${this.strings['No headers']}
</option>
</select>
<p class="help">${this.strings['Which cells should be displayed as headers?']}</p>
</div>
<div class="w-field__wrapper" data-field-wrapper>
<label class="w-field__label" for="${id}-handsontable-col-caption">${this.strings['Table caption']}</label>

View File

@ -33,11 +33,15 @@ const TEST_OPTIONS = {
};
const TEST_STRINGS = {
'Row header': 'Row header',
'Display the first row as a header.': 'Display the first row as a header.',
'Column header': 'Column header',
'Display the first column as a header.':
'Display the first column as a 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',
'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.':
@ -155,12 +159,16 @@ describe('telepath: wagtail.widgets.TableInput', () => {
test('translation', () => {
testStrings = {
'Row header': 'En-tête de ligne',
'Display the first row as a header.':
"Affichez la première ligne sous forme d'en-tête.",
'Column header': 'En-tête de colonne',
'Display the first column as a header.':
"Affichez la première colonne sous forme d'en-tête.",
'Table headers': 'En-têtes de tableau',
'Display the first row as a header':
"Afficher la première ligne sous forme d'en-tête",
'Display the first column as a header':
"Afficher la première colonne sous forme d'en-tête",
'Display the first row AND first column as headers':
"Afficher la première ligne ET la première colonne sous forme d'en-têtes",
'No headers': "Pas d'en-têtes",
'Which cells should be displayed as headers?':
"Quelles cellules doivent être affichées en tant qu'en-têtes?",
'Table caption': 'Légende du tableau',
'A heading that identifies the overall topic of the table, and is useful for screen reader users.':

View File

@ -60,6 +60,7 @@ Thank you to Thibaud Colas and Badr Fourane for their work on this feature.
* Show character counts on RichTextBlock with `max_length` (Elhussein Almasri)
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
* Switch the `TableBlock` header controls to a field that requires user input (Bhuvnesh Sharma, Aman Pandey, Cynthia Kiser)
### Bug fixes

View File

@ -1,6 +1,9 @@
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
@ -68,12 +71,18 @@ class TableInputAdapter(WidgetAdapter):
def js_args(self, widget):
strings = {
"Row header": _("Row header"),
"Display the first row as a header.": _(
"Display the first row as a 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 column as a header.": _(
"Display the first column as a 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.": _(
@ -117,6 +126,44 @@ class TableBlock(FieldBlock):
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 woud be a breaking change.
"""
if 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

View File

@ -116,7 +116,7 @@ class TestTableBlock(TestCase):
"""
self.assertHTMLEqual(result, expected)
def test_do_not_render_html(self):
def test_do_not_render_html_by_default(self):
"""
Ensure that raw html doesn't render
by default.
@ -145,6 +145,36 @@ class TestTableBlock(TestCase):
result = block.render(value)
self.assertHTMLEqual(result, expected)
def test_does_render_html_if_allowed(self):
"""
Ensure html renders if table_options set renderer to allow html
"""
value = {
"first_row_is_table_header": False,
"first_col_is_header": False,
"data": [
["<p><strong>Test</strong></p>", None, None],
[None, None, None],
[None, None, None],
],
}
expected = """
<table>
<tbody>
<tr><td><p><strong>Test</strong></p></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td></tr>
</tbody>
</table>
"""
new_options = self.default_table_options.copy()
new_options["renderer"] = "html"
block = TableBlock(table_options=new_options)
result = block.render(value)
self.assertHTMLEqual(result, expected)
def test_row_headers(self):
"""
Ensure that row headers are properly rendered.
@ -226,6 +256,33 @@ class TestTableBlock(TestCase):
result = block.render(value)
self.assertHTMLEqual(result, expected)
def test_no_headers(self):
"""
Test table without headers.
"""
value = {
"first_row_is_table_header": False,
"first_col_is_header": False,
"data": [
["Foo", "Bar", "Baz"],
["one", "two", "three"],
["four", "five", "six"],
],
}
expected = """
<table>
<tbody>
<tr><td>Foo</td><td>Bar</td><td>Baz</td></tr>
<tr><td>one</td><td>two</td><td>three</td></tr>
<tr><td>four</td><td>five</td><td>six</td></tr>
</tbody>
</table>
"""
block = TableBlock()
result = block.render(value)
self.assertHTMLEqual(result, expected)
def test_value_for_and_from_form(self):
"""
Make sure we get back good json and make