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:
parent
a4c18b4957
commit
fe1a306285
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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.':
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user