Skip to content

Commit 46ab3ea

Browse files
authored
triv: #26243 added per column cell format (#140)
* triv: #no-task fixing admin * triv: #26243 added per column cell format * triv: #26243 added per column cell format * triv: #26243 added per column cell format * triv: #26243 added SBAdminXLSXFormat support in cell_format * triv: #26243 added SBAdminXLSXFormat support in cell_format
1 parent b29e50f commit 46ab3ea

4 files changed

Lines changed: 85 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This document provides key patterns and gotchas for developers and AI assistants
1717
| [Selection Actions](#selection-actions-bulk-actions) | Modal forms for bulk operations, `ListActionModalView`, confirmation modals, `SBAdminCustomAction` params, per-action permissions, success/error handling |
1818
| [Row Actions](#row-actions-per-row-list-buttons) | Per-row icon buttons with `SBAdminRowAction`, `RowActionModalView`, and row-aware enablement |
1919
| [Field Formatters](#field-formatters) | Badge formatters, `array_badge_formatter`, `BadgeType` options |
20+
| [XLSX Export Field Formatting](#xlsx-export-field-formatting) | Per-column Excel cell formats via `XLSXFieldOptions.cell_format` (named, dict, or `SBAdminXLSXFormat`) |
2021
| [View on Site link in list](#view-on-site-link-in-list) | List column with "View on site" icon via admin method, redirect view, `view_on_site_link_formatter` |
2122
| [Performance Optimization](#performance-optimization) | `Subquery` patterns, `ArrayAgg`, avoiding N+1 queries |
2223
| [Common Errors](#common-errors) | Frequent errors and solutions |
@@ -44,6 +45,7 @@ This document provides key patterns and gotchas for developers and AI assistants
4445
- **Adding a column?**[SBAdminField](#sbadminfield---list-display-columns)
4546
- **When should I set `filter_field`?**[When to set `filter_field`](#when-to-set-filter_field)
4647
- **Extra data for formatters?**[sbadmin_list_display_data](#sbadmin_list_display_data---extra-data-fields)
48+
- **XLSX custom formatting per column?**[XLSX Export Field Formatting](#xlsx-export-field-formatting)
4749
- **Filtering by related model?**[Filter Widgets](#filter-widgets) (filter_query_lambda)
4850
- **Comma-separated tags input?**[Form Widgets](#form-widgets)
4951
- **Schema-driven JSON editor (array/object editing)?**[`SBAdminJsonEditorWidget`](#sbadminjsoneditorwidget--schema-driven-json-editor)
@@ -1830,6 +1832,54 @@ format_array(["Published", "Featured"], badge_type=BadgeType.SUCCESS)
18301832

18311833
---
18321834

1835+
## XLSX Export Field Formatting
1836+
1837+
Use `XLSXFieldOptions.cell_format` to apply an Excel format to one exported column.
1838+
1839+
Supported `cell_format` values:
1840+
- `str` — key from `SBAdminXLSXOptions.cell_formats`
1841+
- `dict` — raw xlsxwriter format props
1842+
- `SBAdminXLSXFormat` — class-based format definition
1843+
1844+
```python
1845+
from django_smartbase_admin.engine.field import SBAdminField, XLSXFieldOptions
1846+
from django_smartbase_admin.services.xlsx_export import SBAdminXLSXFormat
1847+
1848+
class ArticleAdmin(SBAdmin):
1849+
sbadmin_list_display = (
1850+
SBAdminField(
1851+
name="price",
1852+
title="Price",
1853+
xlsx_options=XLSXFieldOptions(
1854+
cell_format=SBAdminXLSXFormat(
1855+
num_format='#,##0.00 "€"',
1856+
align="right",
1857+
)
1858+
),
1859+
),
1860+
)
1861+
```
1862+
1863+
Named lookup via `cell_formats`:
1864+
1865+
```python
1866+
class ArticleAdmin(SBAdmin):
1867+
def get_sbadmin_xlsx_options(self, request):
1868+
options = super().get_sbadmin_xlsx_options(request)
1869+
options.cell_formats = {
1870+
"money": {"num_format": '#,##0.00 "€"', "align": "right"},
1871+
}
1872+
return options
1873+
1874+
sbadmin_list_display = (
1875+
SBAdminField(
1876+
name="price",
1877+
xlsx_options=XLSXFieldOptions(cell_format="money"),
1878+
),
1879+
)
1880+
```
1881+
---
1882+
18331883
## Performance Optimization
18341884

18351885
### Preventing N+1 Queries with Subqueries

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-smartbase-admin"
3-
version = "1.3.2"
3+
version = "1.3.3"
44
description = ""
55
authors = ["SmartBase <info@smartbase.sk>"]
66
readme = "README.md"

src/django_smartbase_admin/engine/field.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
AutocompleteFilterWidget,
2929
)
3030
from django_smartbase_admin.services.translations import SBAdminTranslationsService
31+
from django_smartbase_admin.services.xlsx_export import SBAdminXLSXFormat
3132
from django_smartbase_admin.utils import JSONSerializableMixin
3233

3334

@@ -77,19 +78,22 @@ class XLSXFieldOptions(JSONSerializableMixin):
7778
field: str | None = None
7879
formatter: Formatter | None = None
7980
python_formatter: Callable[[int, Any], Any] | None = None
81+
cell_format: str | dict | SBAdminXLSXFormat | None = None
8082

8183
def __init__(
8284
self,
8385
title: str | None = None,
8486
field: str | None = None,
8587
formatter: Formatter | None = None,
8688
python_formatter: Callable[[int, Any], Any] | None = None,
89+
cell_format: str | dict | SBAdminXLSXFormat | None = None,
8790
) -> None:
8891
super().__init__()
8992
self.title = title
9093
self.field = field
9194
self.formatter = formatter
9295
self.python_formatter = python_formatter
96+
self.cell_format = cell_format
9397

9498

9599
class SBAdminField(JSONSerializableMixin):

src/django_smartbase_admin/services/xlsx_export.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import io
3+
import numbers
34
import re
45
from copy import copy
56
from html import unescape
@@ -53,6 +54,15 @@ def write_workbook(cls, export_file, data, columns, options=None):
5354
cell_formats_dict = {}
5455
for cell_format_key, cell_format in cell_format_options.items():
5556
cell_formats_dict[cell_format_key] = workbook.add_format(cell_format)
57+
per_column_formats = {}
58+
for idx, column in enumerate(columns):
59+
cell_format = column.get("cell_format")
60+
if isinstance(cell_format, str):
61+
per_column_formats[idx] = cell_formats_dict[cell_format]
62+
elif isinstance(cell_format, dict):
63+
per_column_formats[idx] = workbook.add_format(cell_format)
64+
elif isinstance(cell_format, SBAdminXLSXFormat):
65+
per_column_formats[idx] = workbook.add_format(cell_format.to_json())
5666
for (
5767
conditional_format_range,
5868
conditional_format,
@@ -84,34 +94,39 @@ def write_workbook(cls, export_file, data, columns, options=None):
8494
data_col = data_row.get(column["field"], "")
8595
column_formatter = column.get("formatter", None)
8696
image_write = False
97+
is_datetime_like = (
98+
isinstance(data_col, datetime.datetime)
99+
or isinstance(data_col, datetime.date)
100+
or isinstance(data_col, datetime.time)
101+
or isinstance(data_col, datetime.timedelta)
102+
)
87103
if column_formatter == Formatter.IMAGE.value:
88104
if row >= header_rows_count:
89105
try:
90106
worksheet.write_formula(
91107
row,
92108
col,
93109
f'=_xlfn.IMAGE("{data_col}")',
94-
cell_format=default_cell_format,
110+
cell_format=per_column_formats.get(
111+
col, default_cell_format
112+
),
95113
)
96114
image_write = True
97115
except ValueError:
98116
pass
99117
if column_formatter == Formatter.HTML.value:
100-
# remove newlines
101-
data_col = re.sub(r"\n", "", str(data_col))
102-
data_col = re.sub(r"\r\n", "", str(data_col))
103-
# replace all possible variants of <br> with new line
104-
data_col = re.sub(r"<br\s*/?>", "\n", str(data_col))
105-
# unescape
106-
data_col = unescape(data_col)
107-
data_col = strip_tags(data_col).strip()
108-
if not image_write:
109118
if (
110-
isinstance(data_col, datetime.datetime)
111-
or isinstance(data_col, datetime.date)
112-
or isinstance(data_col, datetime.time)
113-
or isinstance(data_col, datetime.timedelta)
119+
data_col is not None
120+
and not is_datetime_like
121+
and not isinstance(data_col, numbers.Number)
114122
):
123+
data_col = re.sub(r"\n", "", str(data_col))
124+
data_col = re.sub(r"\r\n", "", str(data_col))
125+
data_col = re.sub(r"<br\s*/?>", "\n", str(data_col))
126+
data_col = unescape(data_col)
127+
data_col = strip_tags(data_col).strip()
128+
if not image_write:
129+
if is_datetime_like:
115130
worksheet.write_datetime(
116131
row, col, data_col, default_cell_datetime_format
117132
)
@@ -121,7 +136,7 @@ def write_workbook(cls, export_file, data, columns, options=None):
121136
col,
122137
data_col,
123138
(
124-
default_cell_format
139+
per_column_formats.get(col, default_cell_format)
125140
if row >= header_rows_count
126141
else header_cell_format
127142
),

0 commit comments

Comments
 (0)