wagtail-tinymce-table 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ recursive-include wagtailtinymce/static *
2
+ include README.md
3
+ include LICENSE
@@ -0,0 +1,429 @@
1
+ Metadata-Version: 2.4
2
+ Name: wagtail-tinymce-table
3
+ Version: 0.2.1
4
+ Summary: TinyMCE-powered StreamField blocks for Wagtail, including a table block with Excel paste support.
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/ogcio/wagtail-tinymce-table
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: wagtail>=4.0
10
+ Requires-Dist: django-tinymce>=5.0
11
+ Requires-Dist: bleach>=6.0
12
+ Requires-Dist: beautifulsoup4>=4.12
13
+ Requires-Dist: lxml>=4.9
14
+ Requires-Dist: wagtail-localize>=1.5
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: pytest-django; extra == "dev"
18
+
19
+ # wagtail-tinymce
20
+
21
+ A set of [Wagtail](https://wagtail.org/) StreamField blocks that embed a [TinyMCE](https://www.tiny.cloud/) editor directly inside the page editor. The headline feature is `TinyMCETableBlock`: a fully-featured table block that lets editors paste tables straight from Excel or Google Sheets, create header-only or footer-only tables, and merge cells — all without needing Wagtail's built-in table block.
22
+
23
+ ---
24
+
25
+ ## Features
26
+
27
+ - **Paste from Excel / Google Sheets** — TinyMCE's clipboard handling converts spreadsheet data to clean HTML tables automatically.
28
+ - **Header rows (`<thead>`) and footer rows (`<tfoot>`)** — a custom *Footer Row* toolbar button toggles the selected row between `<tbody>` and `<tfoot>`. Header rows use TinyMCE's built-in *Row Header* button.
29
+ - **Header-less tables** — nothing forces a header row; plain body tables work fine.
30
+ - **Cell merging and splitting** — `colspan`/`rowspan` via the built-in *Merge Cells* and *Split Cell* buttons.
31
+ - **`wagtail-localize` integration** — `TinyMCETableBlock` implements `get_translatable_segments` and `restore_translated_segments` so table content is fully translatable, including header cells (`<th>`), footer cells, empty cells, and merged cells.
32
+ - **HTML sanitization** — all output is passed through [bleach](https://bleach.readthedocs.io/) with a strict allowlist before being stored.
33
+ - **Customisable** — override `custom_mce_config`, `allowed_tags`, `allowed_attributes`, or pass `menubar_options` / `toolbar_options` per block instance.
34
+
35
+ ---
36
+
37
+ ## Requirements
38
+
39
+ | Package | Minimum version |
40
+ |---|---|
41
+ | Python | 3.9 |
42
+ | Django | 4.2 |
43
+ | Wagtail | 4.0 |
44
+ | django-tinymce | 5.0 |
45
+ | bleach | 6.0 |
46
+ | beautifulsoup4 | 4.12 |
47
+ | lxml | 4.9 |
48
+
49
+ `wagtail-localize` ≥ 1.5 is a **required** dependency and is installed automatically.
50
+
51
+ ---
52
+
53
+ ## Installation
54
+
55
+ `wagtail-localize` is a **required** dependency — it is always installed automatically. No extra flags are needed.
56
+
57
+ ### From PyPI
58
+
59
+ ```bash
60
+ pip install wagtail-tinymce
61
+ ```
62
+
63
+ ### From Git (`master` branch — TinyMCE 7, django-tinymce ≥ 5.0)
64
+
65
+ ```bash
66
+ pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@master
67
+ ```
68
+
69
+ In a `requirements.txt` file (PEP 508):
70
+
71
+ ```text
72
+ wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@master
73
+ ```
74
+
75
+ ### Legacy version (TinyMCE 6, django-tinymce ≥ 3.5)
76
+
77
+ If your project requires (TinyMCE 6), install the tag instead:
78
+
79
+ ```bash
80
+ pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0
81
+ ```
82
+
83
+ In a `requirements.txt` file (PEP 508):
84
+
85
+ ```text
86
+ wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0
87
+ ```
88
+
89
+ ---
90
+
91
+ Add `"wagtailtinymce"` and `"tinymce"` to `INSTALLED_APPS`:
92
+
93
+ ```python
94
+ INSTALLED_APPS = [
95
+ ...
96
+ "tinymce",
97
+ "wagtailtinymce",
98
+ ]
99
+ ```
100
+
101
+ Add a minimal TinyMCE configuration to your Django settings (required by `django-tinymce`):
102
+
103
+ ```python
104
+ TINYMCE_DEFAULT_CONFIG = {}
105
+ ```
106
+
107
+ Add `STATIC_URL` if it is not already set:
108
+
109
+ ```python
110
+ STATIC_URL = "/static/"
111
+ ```
112
+
113
+ Run `collectstatic` so the Wagtail telepath adapter script is served:
114
+
115
+ ```bash
116
+ python manage.py collectstatic
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Quick start
122
+
123
+ ### Complete working example
124
+
125
+ The snippets below show every file you need to add or edit in a standard Wagtail project to get a table block working end-to-end.
126
+
127
+ #### 1. `settings.py`
128
+
129
+ ```python
130
+ INSTALLED_APPS = [
131
+ # --- Wagtail core ---
132
+ "wagtail.contrib.forms",
133
+ "wagtail.contrib.redirects",
134
+ "wagtail.embeds",
135
+ "wagtail.sites",
136
+ "wagtail.users",
137
+ "wagtail.snippets",
138
+ "wagtail.documents",
139
+ "wagtail.images",
140
+ "wagtail.search",
141
+ "wagtail.admin",
142
+ "wagtail",
143
+ # --- Django ---
144
+ "django.contrib.admin",
145
+ "django.contrib.auth",
146
+ "django.contrib.contenttypes",
147
+ "django.contrib.sessions",
148
+ "django.contrib.messages",
149
+ "django.contrib.staticfiles",
150
+ # --- Third-party ---
151
+ "tinymce", # django-tinymce must come before wagtailtinymce
152
+ "wagtailtinymce", # this package
153
+ ]
154
+
155
+ # Required by django-tinymce (may be an empty dict if you rely on block-level config)
156
+ TINYMCE_DEFAULT_CONFIG = {}
157
+ ```
158
+
159
+ #### 2. `myapp/models.py`
160
+
161
+ ```python
162
+ from wagtail.models import Page
163
+ from wagtail.fields import StreamField
164
+ from wagtail.admin.panels import FieldPanel
165
+
166
+ from wagtailtinymce.core.table_block import TinyMCETableBlock
167
+
168
+
169
+ class TableDemoPage(Page):
170
+ """A page that contains one or more TinyMCE table blocks."""
171
+
172
+ body = StreamField(
173
+ [
174
+ ("table", TinyMCETableBlock()),
175
+ ],
176
+ blank=True,
177
+ use_json_field=True,
178
+ verbose_name="Page body",
179
+ )
180
+
181
+ content_panels = Page.content_panels + [
182
+ FieldPanel("body"),
183
+ ]
184
+
185
+ class Meta:
186
+ verbose_name = "Table demo page"
187
+ ```
188
+
189
+ #### 3. `myapp/templates/myapp/table_demo_page.html`
190
+
191
+ ```html
192
+ {% extends "base.html" %}
193
+ {% load wagtailcore_tags %}
194
+
195
+ {% block content %}
196
+ <article>
197
+ <h1>{{ page.title }}</h1>
198
+
199
+ {% for block in page.body %}
200
+ {% if block.block_type == "table" %}
201
+ {# The block value is already sanitised HTML — render it directly #}
202
+ <div class="table-wrapper">
203
+ {{ block.value }}
204
+ </div>
205
+ {% endif %}
206
+ {% endfor %}
207
+ </article>
208
+ {% endblock %}
209
+ ```
210
+
211
+ > **Tip:** use `{% include_block page.body %}` instead of the manual loop if you do not need to wrap individual blocks in extra markup.
212
+
213
+ #### 4. Optional: add basic table styles
214
+
215
+ The block stores a plain `<table>` element. Add CSS so it displays nicely:
216
+
217
+ ```css
218
+ /* static/css/content.css (load this in your base template) */
219
+ .table-wrapper {
220
+ overflow-x: auto; /* horizontal scroll on small screens */
221
+ }
222
+
223
+ .table-wrapper table {
224
+ border-collapse: collapse;
225
+ width: 100%;
226
+ }
227
+
228
+ .table-wrapper th,
229
+ .table-wrapper td {
230
+ border: 1px solid #d1d5db;
231
+ padding: 0.5rem 0.75rem;
232
+ text-align: left;
233
+ vertical-align: top;
234
+ }
235
+
236
+ .table-wrapper thead th {
237
+ background-color: #f3f4f6;
238
+ font-weight: 600;
239
+ }
240
+
241
+ .table-wrapper tfoot td {
242
+ background-color: #f9fafb;
243
+ font-style: italic;
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ### Table block (minimal)
250
+
251
+ ```python
252
+ from wagtail.models import Page
253
+ from wagtail.fields import StreamField
254
+ from wagtail.admin.panels import FieldPanel
255
+ from wagtailtinymce.core.table_block import TinyMCETableBlock
256
+
257
+ class MyPage(Page):
258
+ body = StreamField(
259
+ [("table", TinyMCETableBlock())],
260
+ blank=True,
261
+ use_json_field=True,
262
+ )
263
+
264
+ content_panels = Page.content_panels + [
265
+ FieldPanel("body"),
266
+ ]
267
+ ```
268
+
269
+ Render the block in a template as you would any other StreamField block:
270
+
271
+ ```html
272
+ {% load wagtailcore_tags %}
273
+ {% include_block page.body %}
274
+ ```
275
+
276
+ ### Generic TinyMCE block
277
+
278
+ `TinyMCEBlock` is the base class. Use it directly when you need a full rich-text editor without the table-specific toolbar:
279
+
280
+ ```python
281
+ from wagtailtinymce.blocks import TinyMCEBlock
282
+
283
+ class MyPage(Page):
284
+ body = StreamField(
285
+ [("rich_text", TinyMCEBlock())],
286
+ blank=True,
287
+ use_json_field=True,
288
+ )
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Customisation
294
+
295
+ ### Per-instance toolbar / menubar
296
+
297
+ Pass `toolbar_options` or `menubar_options` when declaring the block:
298
+
299
+ ```python
300
+ TinyMCETableBlock(
301
+ toolbar_options="bold italic | table tablemergecells",
302
+ menubar_options="", # hide the menubar
303
+ )
304
+ ```
305
+
306
+ ### Subclassing for a project-wide config
307
+
308
+ ```python
309
+ from wagtailtinymce.core.table_block import TinyMCETableBlock
310
+
311
+ class MyTableBlock(TinyMCETableBlock):
312
+ custom_mce_config = {
313
+ **TinyMCETableBlock.custom_mce_config,
314
+ "language": "ga", # Irish
315
+ "content_css": "/static/css/editor.css",
316
+ }
317
+
318
+ allowed_tags = TinyMCETableBlock.allowed_tags + ["figure", "figcaption"]
319
+ ```
320
+
321
+ ### Disabling sanitization
322
+
323
+ Set `sanitize_input=False` if you need to allow arbitrary HTML (only do this when the editor is trusted):
324
+
325
+ ```python
326
+ TinyMCETableBlock(sanitize_input=False)
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Toolbar reference
332
+
333
+ The default `TinyMCETableBlock` toolbar groups are:
334
+
335
+ | Group | Buttons |
336
+ |---|---|
337
+ | Formatting | `bold` `italic` `link` `unlink` |
338
+ | Table structure | `table` `tablecaption` `tablecolheader` `tablerowheader` `tablefooterrow`* |
339
+ | Cell operations | `tablecellprops` `tablemergecells` `tablesplitcells` |
340
+ | Row operations | `tableinsertrowbefore` `tableinsertrowafter` `tabledeleterow` |
341
+ | Column operations | `tableinsertcolbefore` `tableinsertcolafter` `tabledeletecol` |
342
+
343
+ \* `tablefooterrow` is a custom button added by this package. Clicking it moves the selected row into `<tfoot>` (or back to `<tbody>` if it is already a footer row).
344
+
345
+ ---
346
+
347
+ ## `wagtail-localize` integration
348
+
349
+ When `wagtail-localize` is installed, `TinyMCETableBlock` implements the segment protocol so translators see each non-empty cell as an individual string segment.
350
+
351
+ **Behaviour:**
352
+ - Empty cells are skipped and do not consume a segment index.
353
+ - Duplicate cell values are extracted once and restored to all matching cells.
354
+ - `<th>` (header) cells, `<tbody>` cells, and `<tfoot>` cells are all included.
355
+ - Cells that contain a nested table are skipped entirely.
356
+ - Merged cells (`colspan`/`rowspan`) are treated as a single cell.
357
+
358
+ No configuration is needed. `wagtail-localize` is installed automatically with the package, and Wagtail Localize will pick up the segments automatically.
359
+
360
+ ---
361
+
362
+ ## Project structure
363
+
364
+ ```
365
+ wagtailtinymce/
366
+ ├── __init__.py # version
367
+ ├── apps.py # Django AppConfig
368
+ ├── blocks.py # TinyMCEBlock (base class)
369
+ ├── widgets.py # WagtailTinyMCE widget + telepath adapter
370
+ ├── core/
371
+ │ └── table_block.py # TinyMCETableBlock
372
+ └── static/
373
+ └── wagtailtinymce/js/
374
+ └── tinymce-adapter.js # Wagtail telepath registration
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Running the tests
380
+
381
+ ```bash
382
+ pip install "wagtail-tinymce[dev]" # adds pytest + pytest-django
383
+ pytest
384
+ ```
385
+
386
+ The test suite has **75 tests** covering:
387
+
388
+ - `_replace_cell_text` helper (simple and compound cell paths)
389
+ - `TinyMCETableBlock` configuration (allowed tags, toolbar, TinyMCE `setup` callback)
390
+ - `get_translatable_segments` (empty cells, duplicates, `<th>`, `<tfoot>`, merged cells, round-trip)
391
+ - `restore_translated_segments` (index correctness, compound cells, `<tfoot>`, duplicates)
392
+ - `TinyMCEBlock.sanitize` (XSS, allowed tags/attributes, inline formatting)
393
+ - `TinyMCEBlock.value_from_form` (`SafeData`, bypass mode)
394
+ - `WagtailTinyMCE` widget (config merging and overrides)
395
+ - `WagtailTinyMCEAdapter` (telepath registration)
396
+
397
+ ---
398
+
399
+ ## Changelog
400
+
401
+ ### 0.2.1
402
+
403
+ #### Bug fixes
404
+ - **Header rows now produce `<th>` elements.** The default TinyMCE `table_header_type` value `"section"` moved rows into `<thead>` but kept `<td>` cells. Setting it to `"sectionCells"` correctly converts cells to `<th>` as well.
405
+ - **Table `<caption>` text is now included in translatable segments.** The caption is extracted before row cells and restored in the same position, so it appears correctly in the Wagtail Localize translation UI.
406
+
407
+ ### 0.2.0
408
+
409
+ #### Breaking changes
410
+ - **Requires `django-tinymce >= 5.0`** (previously 3.5). This bundles **TinyMCE 7.8** instead of TinyMCE 6.x. If you cannot upgrade `django-tinymce`, pin to the `v0.1.0` git tag.
411
+ - **`wagtail-localize >= 1.5` is now a required dependency** (previously optional).
412
+
413
+ #### Packaging
414
+ - Fixed build backend from `setuptools.backends.legacy:build` to `setuptools.build_meta` for compatibility with older setuptools versions.
415
+ - Fixed `package-dir` mapping so `pip install git+…` correctly installs the `wagtailtinymce` module (previously the package was silently installed empty).
416
+
417
+ ### 0.1.0
418
+
419
+ - Initial release.
420
+ - `TinyMCEBlock` and `TinyMCETableBlock` StreamField blocks.
421
+ - Excel / Google Sheets paste support via TinyMCE's clipboard plugin.
422
+ - Custom *Footer Row* toolbar button for `<tfoot>` support.
423
+ - `wagtail-localize` segment extraction and restoration, with fixes for empty cells, merged cells with rich content, and `<th>` cells.
424
+
425
+ ---
426
+
427
+ ## Licence
428
+
429
+ MIT