diff --git a/apps/showcase/examples/example_html_grid.py b/apps/showcase/examples/example_html_grid.py index 45345497b..52000459d 100644 --- a/apps/showcase/examples/example_html_grid.py +++ b/apps/showcase/examples/example_html_grid.py @@ -50,8 +50,8 @@ def example_html_grid(path=None): **grid_param ) - grid.formatters["thing.color"] = lambda color: I( - _class="fa fa-circle", _style="color:" + color + grid.columns[3].represent = lambda row: I( + _class="fa fa-circle", _style="color:" + row.color ) return dict(grid=grid) diff --git a/docs/chapter-16.rst b/docs/chapter-16.rst index f7881fc7b..1fd4aaf24 100644 --- a/docs/chapter-16.rst +++ b/docs/chapter-16.rst @@ -81,7 +81,7 @@ As en example of application of the above, Consider the case of wanting to send emails asynchronously from a background task. In this example we send them using SendGrid from Twilio (https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-python) -Here is an example of scheduler task to send the email: +Here is a possible scheduler task to send the email: .. code:: python diff --git a/py4web/utils/grid.py b/py4web/utils/grid.py index 4a78335f4..a981f5461 100644 --- a/py4web/utils/grid.py +++ b/py4web/utils/grid.py @@ -5,6 +5,7 @@ import base64 import copy import datetime +import functools from urllib.parse import urlparse from pydal.objects import Expression, Field, FieldVirtual @@ -49,15 +50,6 @@ def safe_int(text, default): return default -def join_styles(items): - return "".join(items) if isinstance(items, (list, tuple)) else " %s" % items - - -def clean_sc(**kwargs): - """returns a clean dict with _class and _style only if value""" - return {key: value for key, value in kwargs.items() if value} - - class GridClassStyle: """ Default grid style @@ -114,61 +106,10 @@ class GridClassStyle: "grid-footer-element": "grid-footer-element info", } - styles = { - "grid-wrapper": "", - "grid-header": "display: table; width: 100%;", - "grid-new-button": "margin-top:4px; height:34px; line-height:34px;", - "grid-search": "display: table-cell; float:right;", - "grid-table-wrapper": "overflow-x: auto; width:100%;", - "grid-table": "", - "grid-sorter-icon-up": "", - "grid-sorter-icon-down": "", - "grid-thead": "", - "grid-tr": "", - "grid-th": "text-align:left; white-space: nowrap; vertical-align: top;", - "grid-td": "text-align: left; vertical-align: top;", - "grid-td-buttons": "text-align: left; white-space: nowrap; vertical-align: top;", - "grid-button": "margin-bottom: 0;", - "grid-details-button": "margin-bottom: 0;", - "grid-edit-button": "margin-bottom: 0;", - "grid-delete-button": "margin-bottom: 0;", - "grid-search-button": "height: 34px;", - "grid-clear-button": "height: 34px;", - "grid-footer": "display: table; width:100%;", - "grid-info": "display: table-cell;", - "grid-pagination": "display: table-cell; text-align:right;", - "grid-pagination-button": "min-width: 20px;", - "grid-pagination-button-current": "min-width: 20px; pointer-events:none; opacity: 0.7;", - "grid-cell-type-string": "white-space: nowrap; vertical-align: top; text-align: left; text-overflow: ellipsis; max-width: 200px;", - "grid-cell-type-text": "vertical-align: top; text-align: left; text-overflow: ellipsis; max-width: 200px;", - "grid-cell-type-boolean": "white-space: nowrap; vertical-align: top; text-align: center;", - "grid-cell-type-float": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-decimal": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-int": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-date": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-time": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-datetime": "white-space: nowrap; vertical-align: top; text-align: right;", - "grid-cell-type-upload": "white-space: nowrap; vertical-align: top; text-align: center;", - "grid-cell-type-list": "white-space: nowrap; vertical-align: top; text-align: left;", - # specific for custom form - "grid-search-form": "", - "grid-search-form-table": "", - "grid-search-form-tr": "border-bottom: none;", - "grid-search-form-td": "", - "grid-search-form-input": "", - "grid-search-form-select": "", - "grid-search-boolean": "", - "grid-header-element": "margin-top:4px; height:34px; line-height:34px;", - "grid-footer-element": "margin-top:4px; height:34px; line-height:34px;", - } - @classmethod - def get(cls, element): + def get(cls, element, default=None): """returns a dict with _class and _style for the element name""" - return clean_sc( - _class=cls.classes.get(element), - _style=cls.styles.get(element), - ) + return cls.classes.get(element, element if default is None else default) class GridClassStyleBulma(GridClassStyle): @@ -223,55 +164,6 @@ class GridClassStyleBulma(GridClassStyle): "grid-footer-element": "grid-footer-element button", } - styles = { - "grid-wrapper": "", - "grid-header": "", - "grid-new-button": "", - "grid-search": "", - "grid-table-wrapper": "", - "grid-table": "", - "grid-sorter-icon-up": "", - "grid-sorter-icon-down": "", - "grid-thead": "", - "grid-tr": "", - "grid-th": "text-align: center; text-transform: uppercase; vertical-align: bottom;", - "grid-td": "", - "grid-td-buttons": "", - "grid-button": "", - "grid-details-button": "", - "grid-edit-button": "", - "grid-delete-button": "", - "grid-search-button": "", - "grid-clear-button": "", - "grid-footer": "padding-top: .5em; padding-bottom: 2em;", - "grid-info": "", - "grid-pagination": "", - "grid-pagination-button": "margin-left: .25em;", - "grid-pagination-button-current": "margin-left: .25em;", - "grid-cell-type-string": "vertical-align: top; text-overflow: ellipsis;", - "grid-cell-type-text": "vertical-align: top; text-overflow: ellipsis;", - "grid-cell-type-boolean": "vertical-align: top; text-align: center", - "grid-cell-type-float": "vertical-align: top; text-align: right", - "grid-cell-type-decimal": "vertical-align: top; text-align: right", - "grid-cell-type-int": "vertical-align: top; text-align: center;", - "grid-cell-type-date": "vertical-align: top; text-align: center;", - "grid-cell-type-time": "vertical-align: top; text-align: center;", - "grid-cell-type-datetime": "vertical-align: top; text-align: center;", - "grid-cell-type-upload": "vertical-align: top; text-align: center;", - "grid-cell-type-list": "vertical-align: top; text-align: left;", - "grid-cell-type-id": "", - # specific for custom form - "grid-search-form": "", - "grid-search-form-table": "", - "grid-search-form-tr": "", - "grid-search-form-td": "", - "grid-search-form-input": "", - "grid-search-form-select": "", - "grid-search-boolean": "padding-top: .5rem;", - "grid-header-element": "", - "grid-footer-element": "", - } - class GridClassStyleBootstrap5(GridClassStyle): """The Grid style for Bootstrap 5""" @@ -325,55 +217,6 @@ class GridClassStyleBootstrap5(GridClassStyle): "grid-footer-element": "grid-footer-element btn btn-sm", } - styles = { - "grid-wrapper": "", - "grid-header": "", - "grid-new-button": "", - "grid-search": "", - "grid-table-wrapper": "", - "grid-table": "", - "grid-sorter-icon-up": "", - "grid-sorter-icon-down": "", - "grid-thead": "", - "grid-tr": "", - "grid-th": "text-align: center; text-transform: uppercase; vertical-align: bottom; text-decoration: none;", - "grid-td": "", - "grid-td-buttons": "white-space: nowrap; width: 1%;", - "grid-button": "", - "grid-details-button": "border-radius: 0 !important;", - "grid-edit-button": "border-radius: 0 !important;", - "grid-delete-button": "border-radius: 0 !important;", - "grid-search-button": "border-radius: 0 !important;", - "grid-clear-button": "border-radius: 0 !important;", - "grid-footer": "padding-top: .5em; padding-bottom: 2em;", - "grid-info": "", - "grid-pagination": "", - "grid-pagination-button": "margin-left: .25em; border-radius: 0 !important;", - "grid-pagination-button-current": "margin-left: .25em; border-radius: 0 !important;", - "grid-cell-type-string": "vertical-align: top; text-overflow: ellipsis;", - "grid-cell-type-text": "vertical-align: top; text-overflow: ellipsis;", - "grid-cell-type-boolean": "vertical-align: top; text-align: center", - "grid-cell-type-float": "vertical-align: top; text-align: right", - "grid-cell-type-decimal": "vertical-align: top; text-align: right", - "grid-cell-type-int": "vertical-align: top; text-align: center;", - "grid-cell-type-date": "vertical-align: top; text-align: center;", - "grid-cell-type-time": "vertical-align: top; text-align: center;", - "grid-cell-type-datetime": "vertical-align: top; text-align: center;", - "grid-cell-type-upload": "vertical-align: top; text-align: center;", - "grid-cell-type-list": "vertical-align: top; text-align: left;", - "grid-cell-type-id": "", - # specific for custom form - "grid-search-form": "", - "grid-search-form-table": "", - "grid-search-form-tr": "", - "grid-search-form-td": "", - "grid-search-form-input": "", - "grid-search-form-select": "", - "grid-search-boolean": "padding-top: .5rem;", - "grid-header-element": "", - "grid-footer-element": "", - } - class Column: """class used to represent a column in a grid""" @@ -382,34 +225,27 @@ def __init__( self, name, represent, - required_fields=None, + key=None, + required_fields=None, # must be a list or none orderby=None, + col_type="string", td_class_style=None, ): self.name = name self.represent = represent self.orderby = orderby - self.required_fields = [] - if required_fields: - if isinstance(required_fields, list): - self.required_fields = required_fields - else: - self.required_fields = [required_fields] - + self.required_fields = required_fields or [] + self.key = key + self.type = (col_type,) self.td_class_style = td_class_style - def render(self, row, index=None): - """renders a row al position index (optional)""" - return self.represent(row) - class Grid: FORMATTERS_BY_TYPE = { - "boolean": lambda value: ( - INPUT(_type="checkbox", _checked=value, _disabled="disabled") - if value - else "" - ), + "NoneType": lambda value: "", + "bool": lambda value: "☑" if value else "☐" if value is False else "", + "float": lambda value: "%.2f" % value, + "double": lambda value: "%.2f" % value, "datetime": lambda value: ( XML( "" @@ -451,9 +287,7 @@ class Grid: if value else "" ), - "list:string": lambda value: ", ".join(str(x) for x in value) if value else "", - "list:integer": lambda value: ", ".join(x for x in value) if value else "", - "default": lambda value: str(value) if value is not None else "", + "list": lambda value: ", ".join(x for x in value) if value else "", } def __init__( @@ -583,8 +417,6 @@ def __init__( self.rows = None self.tablename = None self.total_number_of_rows = None - self.use_tablename = self.is_join() - self.formatters = {} self.formatters_by_type = copy.copy(Grid.FORMATTERS_BY_TYPE) self.attributes_plugin = AttributesPlugin(request) @@ -653,33 +485,75 @@ def process(self): self.record_id = safe_int(parts[1] if len(parts) > 1 else None, default=None) table = db[self.tablename] + # if no column specified use all fields if not self.param.columns: - # if no column specified use all fields self.param.columns = [field for field in table if field.readable] + # convert to column object + self.columns = [] + + def title(col): + return str(col).replace('"', "") + + def col2key(col): + return str(col).lower().replace(".", "-") + + for index, col in enumerate(self.param.columns): + if isinstance(col, Column): + if not col.key: + col.key = f"column-{index}" + self.columns.append(col) + elif isinstance(col, Field): + # what about represe + def compute(row, col=col): + value = row(str(col)) + return col.represent(value) if col.represent else value + + self.columns.append( + Column( + col.label, + compute, + orderby=col, + required_fields=[col], + key=col2key(col), + col_type=col.type, + ) + ) + elif isinstance(col, FieldVirtual): + + def compute(row, col=col): + return col.f(row) if "id" in row else col.f(row[col.tablename]) + + self.columns.append( + Column( + col.label, + compute, + orderby=None, + required_fields=db[col.tablename], + key=col2key(col), + ) + ) + elif isinstance(col, Expression): + + def compute(row, name=str(col)): + return row._extra(name) + + self.columns.append( + Column( + title(col), + compute, + orderby=None, + required_fields=[col], + key=f"column-{index}", + ) + ) + else: + raise RuntimeError(f"Column not support {col}") - if not self.param.columns: - self.needed_fields = self.param.columns[:] - else: - needed_fields = set() - for col in self.param.columns: - print("Column", col) - if isinstance(col, Column): - if col.required_fields: - needed_fields |= set(col.required_fields) - else: - needed_fields |= set(db[self.tablename]) - elif isinstance(col, FieldVirtual): - # if virtual fields are specified the fields may come from a join - needed_fields |= set(db[col.tablename]) - else: - needed_fields.add(col) - self.needed_fields = list(needed_fields) - - print(self.needed_fields) - - # except the primary key may be missing and must be fetched even if not displayed - if not any(getattr(col, "name", None) == table._id.name for col in self.needed_fields): - self.needed_fields.insert(0, table._id) + # join the set of all required fields + sets = (set(col.required_fields) for col in self.columns) + self.needed_fields = list( + functools.reduce(lambda a, b: a | b, sets) | set([table._id]) + ) self.referrer = None @@ -769,21 +643,6 @@ def process(self): request.url.encode("utf8") ).decode("utf8") - # find the primary key of the primary table - pt = db[self.tablename] - key_is_missing = True - for field in self.param.columns: - if ( - isinstance(field, Field) - and field.table._tablename == pt._tablename - and field.name == pt._id.name - ): - key_is_missing = False - if key_is_missing: - # primary key wasn't included, add it and set show_id to False so it doesn't display - self.param.columns.append(pt._id) - self.param.show_id = False - self.current_page_number = safe_int(request.query.get("page"), default=1) select_params = dict() @@ -836,8 +695,6 @@ def process(self): self.page_end = self.total_number_of_rows # get the data - print(self.needed_fields) - print(select_params) self.rows = db(query).select(*self.needed_fields, **select_params) self.number_of_pages = self.total_number_of_rows // self.param.rows_per_page @@ -851,8 +708,14 @@ def process(self): or self.param.deletable or self.param.post_action_buttons ): - self.param.columns.append( - Column("", self.make_action_buttons, td_class_style="grid-td-buttons") + key = f"column-{len(self.columns)}" + self.columns.append( + Column( + "", + self.make_action_buttons, + key=key, + td_class_style=self.param.grid_class_style.get("grid-td-buttons"), + ) ) def iter_pages( @@ -889,9 +752,7 @@ def _make_action_button( icon, icon_size="small", # deprecated additional_classes=None, - additional_styles=None, override_classes=None, - override_styles=None, message=None, onclick=None, # deprecated row_id=None, @@ -903,29 +764,18 @@ def _make_action_button( if row_id: url += "/%s" % row_id - classes = self.param.grid_class_style.classes.get(name, "") - styles = self.param.grid_class_style.styles.get(name, "") + classes = self.param.grid_class_style.get(name) if callable(additional_classes): additional_classes = additional_classes(row) - if callable(additional_styles): - additional_styles = additional_styles(row) - if callable(override_classes): override_classes = override_classes(row) - if callable(override_styles): - override_styles = override_styles(row) - if override_classes: classes = join_classes(override_classes) elif additional_classes: classes = join_classes(classes, additional_classes) - if override_styles: - styles = join_styles(override_styles) - elif additional_styles: - styles += join_styles(additional_styles) if callable(url): url = url(row) @@ -940,7 +790,7 @@ def _make_action_button( _role="button", _message=message, _title=button_text, - **clean_sc(_class=classes, _style=styles), + _class=classes, **attrs, ) if self.param.include_action_button_text: @@ -950,6 +800,12 @@ def _make_action_button( return link + def reformat(self, value): + type_name = type(value).__name__ + if type_name in self.formatters_by_type: + return self.formatters_by_type[type_name](value) + return value + def _make_default_form(self): search_type = safe_int(request.query.get("search_type", 0), default=0) search_string = request.query.get("search_string") @@ -964,51 +820,51 @@ def _make_default_form(self): ] attrs = self.attributes_plugin.link(url=self.endpoint) form = FORM(*hidden_fields, **attrs) - sc = self.param.grid_class_style.get("grid-search-form-select") - select = SELECT( - *options, - **dict( - _name="search_type", - ), - **sc, - ) - sc = self.param.grid_class_style.get("grid-search-form-input") + classes = self.param.grid_class_style.get("grid-search-form-select") + select = SELECT(*options, **dict(_name="search_type", _class=classes)) + classes = self.param.grid_class_style.get("grid-search-form-input") input = INPUT( _type="text", _name="search_string", _value=search_string, - **sc, + _class=classes, ) - sc = self.param.grid_class_style.get("grid-search-button") - submit = INPUT(_type="submit", _value=self.T("Search"), **sc) + classes = self.param.grid_class_style.get("grid-search-button") + submit = INPUT(_type="submit", _value=self.T("Search"), _class=classes) clear_script = "document.querySelector('[name=search_string]').value='';" - sc = self.param.grid_class_style.get("grid-clear-button") + classes = self.param.grid_class_style.get("grid-clear-button") clear = INPUT( - _type="submit", _value=self.T("Clear"), _onclick=clear_script, **sc + _type="submit", + _value=self.T("Clear"), + _onclick=clear_script, + _class=classes, + ) + div = DIV( + _id="grid-search", _classes=self.param.grid_class_style.get("grid-search") ) - div = DIV(_id="grid-search", **self.param.grid_class_style.get("grid-search")) - sc = self.param.grid_class_style.get("grid-search-form-tr") - tr = TR(**sc) - sc = self.param.grid_class_style.get("grid-search-form-td") + tr = TR(_class=self.param.grid_class_style.get("grid-search-form-tr")) + classes = self.param.grid_class_style.get("grid-search-form-td") if len(options) > 1: - tr.append(TD(select, **sc)) - tr.append(TD(input, **sc)) - tr.append(TD(submit, clear, **sc)) - sc = self.param.grid_class_style.get("grid-search-form-table") - form.append(TABLE(tr, **sc)) + tr.append(TD(select, _class=classes)) + tr.append(TD(input, _class=classes)) + tr.append(TD(submit, clear, _class=classes)) + classes = self.param.grid_class_style.get("grid-search-form-table") + form.append(TABLE(tr, _class=classes)) div.append(form) return div def _make_search_form(self): # TODO: Do we need this? - div = DIV(_id="grid-search", **self.param.grid_class_style.get("grid-search")) + div = DIV( + _id="grid-search", _class=self.param.grid_class_style.get("grid-search") + ) div.append(self.param.search_form.custom["begin"]) - tr = TR(**self.param.grid_class_style.get("grid-search-form-tr")) + tr = TR(_class=self.param.grid_class_style.get("grid-search-form-tr")) for field in self.param.search_form.table: - td = TD(**self.param.grid_class_style.get("grid-search-form-td")) + td = TD(_class=self.param.grid_class_style.get("grid-search-form-td")) if field.type == "boolean": - sb = DIV(**self.param.grid_class_style.get("grid-search-boolean")) + sb = DIV(_class=self.param.grid_class_style.get("grid-search-boolean")) sb.append(self.param.search_form.custom["widgets"][field.name]) sb.append(field.label) td.append(sb) @@ -1018,12 +874,7 @@ def _make_search_form(self): field.name in self.param.search_form.custom["errors"] and self.param.search_form.custom["errors"][field.name] ): - td.append( - DIV( - self.param.search_form.custom["errors"][field.name], - _style="color:#ff0000", - ) - ) + td.append(DIV(self.param.search_form.custom["errors"][field.name])) tr.append(td) if self.param.search_button_text: tr.append( @@ -1033,18 +884,18 @@ def _make_search_form(self): _type="submit", _value=self.T(self.param.search_button_text), ), - **self.param.grid_class_style.get("grid-search-form-td"), + _class=self.param.grid_class_style.get("grid-search-form-td"), ) ) else: tr.append( TD( self.param.search_form.custom["submit"], - **self.param.grid_class_style.get("grid-search-form-td"), + _class=self.param.grid_class_style.get("grid-search-form-td"), ) ) div.append( - TABLE(tr, **self.param.grid_class_style.get("grid-search-form-table")) + TABLE(tr, _class=self.param.grid_class_style.get("grid-search-form-table")) ) for hidden_widget in self.param.search_form.custom["hidden_widgets"].keys(): if hidden_widget not in ("formname", "formkey"): @@ -1059,173 +910,73 @@ def _make_search_form(self): def _make_table_header(self): sort_order = request.query.get("orderby", "") - thead = THEAD( - **clean_sc(_class=self.param.grid_class_style.classes.get("grid-thead", "")) - ) - for index, column in enumerate(self.param.columns): - col = None - key = None - if isinstance(column, (Field, FieldVirtual)): - field = column - if field.readable and (field.type != "id" or self.param.show_id): - key, col = self._make_field_header(column, index, sort_order) - elif isinstance(column, Column): - key = column.name.lower().replace(" ", "-") - col = column.name - if column.orderby: - key, col = self._make_field_header(column, index, sort_order) - else: - raise RuntimeError("Invalid Grid Column type") - if col is not None: - classes = join_classes( - self.param.grid_class_style.classes.get("grid-th"), - "grid-col-%s" % key, - ) - style = self.param.grid_class_style.styles.get("grid-th") - thead.append(TH(col, **clean_sc(_class=classes, _style=style))) + thead = THEAD(_class=self.param.grid_class_style.get("grid-thead")) + for index, col in enumerate(self.columns): + col_header = self._make_col_header(col, index, sort_order) + classes = join_classes( + self.param.grid_class_style.get("grid-th"), + "grid-col-%s" % col.key, + ) + thead.append(TH(col_header, _class=classes)) return thead - def _make_field_header(self, field, field_index, sort_order): - up = I(**self.param.grid_class_style.get("grid-sorter-icon-up")) - dw = I(**self.param.grid_class_style.get("grid-sorter-icon-down")) + def _make_col_header(self, col, index, sort_order): + up = I(_class=self.param.grid_class_style.get("grid-sorter-icon-up")) + dw = I(_class=self.param.grid_class_style.get("grid-sorter-icon-down")) - if isinstance(field, Column): - key = str(field.orderby) - else: - key = "%s.%s" % (field.tablename, field.name) + orderby = col.orderby and str(col.orderby) heading = ( - self.param.headings[field_index] - if field_index < len(self.param.headings) - else getattr(field, "label", title(field.name)) + self.param.headings[index] if index < len(self.param.headings) else col.name ) # add the sort order query parm sort_query_parms = dict(self.query_parms) attrs = {} - if isinstance(field, FieldVirtual): - col = SPAN(heading) - elif key == sort_order: - sort_query_parms["orderby"] = "~" + key - url = URL(self.endpoint, "select", vars=sort_query_parms) - attrs = self.attributes_plugin.link(url=url) - col = A(heading, up, **attrs) - else: - sort_query_parms["orderby"] = key - url = URL(self.endpoint, "select", vars=sort_query_parms) - attrs = self.attributes_plugin.link(url=url) - col = A(heading, dw if "~" + key == sort_order else "", **attrs) - return key, col - - def _make_field(self, row, field, field_index): - """ - Render a field - - if only 1 table in the query, the no table name needed when getting the row value - however, if there - are multiple tables in the query (self.use_tablename == True) then we need to use the tablename as well - when accessing the value in the row object - - the row object sent in can take - :param row: - :param field: - :return: - """ - if self.use_tablename: - row = row[field.tablename] - if isinstance(field, FieldVirtual): - field_value = field.f(row) - else: - field_value = row[field.name] - key = "%s.%s" % (field.tablename, field.name) - # custom formatter overwrites represent - if key in self.formatters: - formatter = self.formatters.get(key) - # else if represent provided use it - elif field.represent: - formatter = field.represent - # else fallback on the formatter for the type - elif field.type in self.formatters_by_type: - formatter = self.formatters_by_type.get(field.type) - # else fallback to default + if orderby: + if orderby == sort_order: + sort_query_parms["orderby"] = "~" + orderby + url = URL(self.endpoint, "select", vars=sort_query_parms) + attrs = self.attributes_plugin.link(url=url) + col_header = A(heading, up, **attrs) + else: + sort_query_parms["orderby"] = orderby + url = URL(self.endpoint, "select", vars=sort_query_parms) + attrs = self.attributes_plugin.link(url=url) + col_header = A( + heading, dw if "~" + orderby == sort_order else "", **attrs + ) else: - formatter = self.formatters_by_type.get("default") - # allow 1 (value) or 2 (value + row) args - try: - formatted_value = formatter(field_value, row) - except TypeError: - formatted_value = formatter(field_value) - - class_type = "grid-cell-type-%s" % str(field.type).split(":")[0].split("(")[0] - class_col = " grid-col-%s" % key.replace(".", "_") - classes = join_classes( - self.param.grid_class_style.classes.get("grid-td"), - self.param.grid_class_style.classes.get(class_type), - class_col, - ) - td = TD( - formatted_value, - **clean_sc( - _class=classes, - _style=( - self.param.grid_class_style.styles.get(class_type) - or self.param.grid_class_style.styles.get("grid-td") - ), - ), - ) - - return td + col_header = heading + return col_header def _make_table_body(self): tbody = TBODY() - for row in self.rows: + for index, row in enumerate(self.rows): # find the row id - there may be nested tables.... - if not (self.use_tablename and self.tablename in row and "id" not in row): - self.use_tablename = False - key = "%s.%s" % (self.tablename, "__row") - if self.formatters.get(key): - extra_class = self.formatters.get(key)(row)["_class"] - extra_style = self.formatters.get(key)(row)["_style"] - else: - extra_class = "" - extra_style = "" tr = TR( _role="row", - **clean_sc( - _class=join_classes( - self.param.grid_class_style.classes.get("grid-tr"), extra_class - ), - _style=join_styles( - [self.param.grid_class_style.styles.get("grid-tr"), extra_style] - ), - ), + _class=self.param.grid_class_style.get("grid-tr"), ) + # add all the fields to the row - for index, column in enumerate(self.param.columns): - if isinstance(column, (Field, FieldVirtual)): - field = column - if field.readable and (field.type != "id" or self.param.show_id): - tr.append(self._make_field(row, field, index)) - elif isinstance(column, Column): - classes = self.param.grid_class_style.classes.get( - column.td_class_style, - column.td_class_style(row) - if callable(column.td_class_style) - else self.param.grid_class_style.classes.get("grid-td"), - ) - style = self.param.grid_class_style.styles.get( - column.td_class_style, - column.td_class_style(row) - if callable(column.td_class_style) - else self.param.grid_class_style.styles.get("grid-td"), - ) - tr.append( - TD( - column.render(row, index), - **clean_sc(_class=classes, _style=style), - ) - ) + for col in self.columns: + classes = join_classes( + [ + self.param.grid_class_style.get( + col.td_class_style, + col.td_class_style(row) + if callable(col.td_class_style) + else self.param.grid_class_style.get("grid-td"), + ), + f"grid-cell-{col.key}", + ] + ) + value = col.represent(row) + reformatted_value = self.reformat(value) + tr.append(TD(reformatted_value, _class=classes)) tbody.append(tr) @@ -1256,9 +1007,7 @@ def make_action_buttons(self, row): button_text=self.T(btn.text), icon=btn.icon, additional_classes=btn.additional_classes, - additional_styles=btn.__dict__.get("additional_styles"), override_classes=btn.__dict__.get("override_classes"), - override_styles=btn.__dict__.get("override_styles"), message=btn.message, row_id=row_id if btn.append_id else None, name=btn.__dict__.get("name"), @@ -1344,9 +1093,7 @@ def make_action_buttons(self, row): button_text=self.T(btn.text), icon=btn.icon, additional_classes=btn.additional_classes, - additional_styles=btn.__dict__.get("override_styles"), override_classes=btn.__dict__.get("override_classes"), - override_styles=btn.__dict__.get("override_styles"), message=btn.message, row_id=row_id if btn.append_id else None, name=btn.__dict__.get("name"), @@ -1363,7 +1110,7 @@ def make_action_buttons(self, row): return cat def _make_table_pager(self): - pager = DIV(**self.param.grid_class_style.get("grid-pagination")) + pager = DIV(_class=self.param.grid_class_style.get("grid-pagination")) previous_page_number = None for page_number in self.iter_pages( self.current_page_number, self.number_of_pages @@ -1372,7 +1119,7 @@ def _make_table_pager(self): pager_query_parms["page"] = page_number # if there is a gat add a spacer if previous_page_number and page_number - previous_page_number > 1: - pager.append(SPAN("...", _style="margin:0 10px;")) + pager.append(SPAN("...")) is_current = self.current_page_number == page_number page_name = ( "grid-pagination-button-current" @@ -1385,7 +1132,7 @@ def _make_table_pager(self): pager.append( A( page_number, - **self.param.grid_class_style.get(page_name), + _class=self.param.grid_class_style.get(page_name), _role="button", **attrs, ) @@ -1394,8 +1141,8 @@ def _make_table_pager(self): return pager def _make_table(self): - html = DIV(**self.param.grid_class_style.get("grid-wrapper")) - grid_header = DIV(**self.param.grid_class_style.get("grid-header")) + html = DIV(_class=self.param.grid_class_style.get("grid-wrapper")) + grid_header = DIV(_class=self.param.grid_class_style.get("grid-header")) # build the New button if needed if self.param.create and self.param.create != "": @@ -1412,12 +1159,7 @@ def _make_table(self): self.T(self.param.new_action_button_text), "fa-plus", icon_size="normal", - override_classes=self.param.grid_class_style.classes.get( - "grid-new-button", "" - ), - override_styles=self.param.grid_class_style.styles.get( - "grid-new-button" - ), + override_classes=self.param.grid_class_style.get("grid-new-button"), ) ) if self.param.header_elements and len(self.param.header_elements) > 0: @@ -1429,19 +1171,9 @@ def _make_table(self): else: override_classes = element.__dict__.get("override_classes", None) if not override_classes: - override_classes = ( - self.param.grid_class_style.classes.get( - "grid-header-element", "" - ) - + f" {element.additional_classes}" - ) - override_styles = element.__dict__.get("override_styles", None) - if not override_styles: - override_styles = ( - self.param.grid_class_style.styles.get( - "grid-trailer-element", "" - ) - + f" {element.__dict__.get('additional_styles')}" + override_classes = join_classes( + self.param.grid_class_style.get("grid-header-element"), + element.additional_classes, ) grid_header.append( self._make_action_button( @@ -1450,9 +1182,7 @@ def _make_table(self): icon=element.icon, icon_size="normal", additional_classes=element.additional_classes, - additional_styles=element.__dict__.get("additional_styles"), override_classes=override_classes, - override_styles=override_styles, message=element.message, name=element.__dict__.get("name"), ignore_attribute_plugin=element.ignore_attribute_plugin, @@ -1468,7 +1198,7 @@ def _make_table(self): html.append(grid_header) - table = TABLE(**self.param.grid_class_style.get("grid-table")) + table = TABLE(_class=self.param.grid_class_style.get("grid-table")) # build the header table.append(self._make_table_header()) @@ -1477,12 +1207,14 @@ def _make_table(self): table.append(self._make_table_body()) # add the table to the html - html.append(DIV(table, **self.param.grid_class_style.get("grid-table-wrapper"))) + html.append( + DIV(table, _class=self.param.grid_class_style.get("grid-table-wrapper")) + ) # add the row counter information - footer = DIV(**self.param.grid_class_style.get("grid-footer")) + footer = DIV(_class=self.param.grid_class_style.get("grid-footer")) - row_count = DIV(**self.param.grid_class_style.get("grid-info")) + row_count = DIV(_class=self.param.grid_class_style.get("grid-info")) ( row_count.append( str(self.T("Displaying rows %s thru %s of %s")) @@ -1516,19 +1248,9 @@ def _make_table(self): else: override_classes = element.__dict__.get("override_classes", None) if not override_classes: - override_classes = ( - self.param.grid_class_style.classes.get( - "grid-footer-element", "" - ) - + f" {element.additional_classes}" - ) - override_styles = element.__dict__.get("override_styles", None) - if not override_styles: - override_styles = ( - self.param.grid_class_style.styles.get( - "grid-footer-element", "" - ) - + f" {element.__dict__.get('additional_styles')}" + override_classes = join_classes( + self.param.grid_class_style.get("grid-footer-element"), + element.additional_classes, ) html.append( self._make_action_button( @@ -1537,9 +1259,7 @@ def _make_table(self): icon=element.icon, icon_size="normal", additional_classes=element.additional_classes, - additional_styles=element.__dict__.get("additional_styles"), override_classes=override_classes, - override_styles=override_styles, message=element.message, name=element.__dict__.get("name"), ignore_attribute_plugin=element.ignore_attribute_plugin,