Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api_reference/model_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
- form_create_rules
- form_edit_rules
- column_type_formatters
- column_type_formatters_detail
- non_link_related_fields
- list_query
- count_query
Expand Down
79 changes: 63 additions & 16 deletions docs/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ The filter UI provides a dropdown for operation selection and a text input for t

- **AllUniqueStringValuesFilter/StaticValuesFilter/ForeignKeyFilter**: Shows all possible values as links (good for columns with few unique values)
- **OperationColumnFilter**: Provides operation dropdown + text input (good for columns with many possible values or numeric/date operations)

Choose OperationColumnFilter when you want users to type custom search terms with operation flexibility, and AllUniqueStringValuesFilter when you want to show all available options as clickable links.


Expand Down Expand Up @@ -319,8 +319,6 @@ The pagination options in the list page can be configured. The available options
There are a few options which apply to both List and Detail pages. They include:

- `column_labels`: A mapping of column labels, used to map column names to new names in all places.
- `column_type_formatters`: A mapping of type keys and callable values to format in all places.
For example you can add custom date formatter to be used in both list and detail pages.
- `save_as`: A boolean to enable "save as new" option when editing an object.
- `save_as_continue`: A boolean to control the redirect URL if `save_as` is enabled.

Expand All @@ -336,6 +334,60 @@ There are a few options which apply to both List and Detail pages. They include:
save_as = True
```

## Type formatters

You can create formatters for data types without specifying field names in both `column_formatters` and `column_formatters_detail`. The following options are suitable for this:

- `column_type_formatters`: Mapping type keys to callable values for formatting on list pages.
- `column_type_formatters_detail`: A mapping of type keys and callable values to format in details pages.

!!! example
```python
class UserAdmin(ModelView, model=User):
column_type_formatters = {
type(None): lambda x: 'Empty',
str: lambda x: x[:10]
}
column_type_formatters_detail = {
type(None): lambda x: 'Null',
str: lambda x: x.title()
}
```

!!! tip

If `column_type_formatters_detail` is not explicitly specified, the `column_type_formatters` mapping is used for the detail page.

??? example "Example with build-in formatters"

```python
import enum
import datetime
import uuid

from enum import StrEnum
# from sqladmin._types import StrEnum # for python <3.11
from sqladmin.formatters import (
str_enum_formatter,
datetime_formatter,
copy_to_clipboard_formatter,
)


custom_column_type_formatters_detail = ModelView.column_type_formatters_detail.copy()
custom_column_type_formatters_detail.update(
{
StrEnum: str_enum_formatter,
datetime.datetime: datetime_formatter,
uuid.UUID: copy_to_clipboard_formatter,
}
)


class UserAdmin(ModelView, model=User):
column_type_formatters_detail = custom_column_type_formatters_detail
```

## Form options

SQLAdmin allows customizing how forms work with your models.
Expand Down Expand Up @@ -393,13 +445,11 @@ The export options can be set per model and includes the following options:
## Pretty CSV Export
- `ModelView.use_pretty_export`: Default value is `False`

Enables exporting CSV files with user-friendly column labels and formatted cell values
matching the UI list view.
When enabled, exports utilize the `column_formatters` and `column_labels` defined in the admin view,
improving readability and ensuring consistency between the UI and exported data.
Enables exporting CSV files with user-friendly column labels and formatted cell values matching the UI list view.
When enabled, exports utilize the `column_formatters` and `column_labels` defined in the admin view,
improving readability and ensuring consistency between the UI and exported data.

Custom cell formatting can be implemented in the ModelView class by overriding the async method
`custom_export_cell`, otherwise basic cell formatting is used by default.
Custom cell formatting can be implemented in the ModelView class by overriding the async method custom_export_cell`, otherwise basic cell formatting is used by default.

Example of usage:
```python
Expand All @@ -408,10 +458,10 @@ class ExamResultAdmin(ModelView, model=ExamResult):

column_list = ["score", "instructors", "course.title", "course.instructors", "created_at"]
column_labels = {
"score": "Score",
"instructors": "Exam Instructors",
"course.title": "Course Title",
"course.instructors": "Course Instructors",
"score": "Score",
"instructors": "Exam Instructors",
"course.title": "Course Title",
"course.instructors": "Course Instructors",
"created_at": "Exam Time",
}
column_formatters = {
Expand All @@ -435,8 +485,6 @@ class ExamResultAdmin(ModelView, model=ExamResult):
return None
```



## Import options

SQLAdmin supports importing data from a UTF-8 CSV file on the list page.
Expand Down Expand Up @@ -632,7 +680,6 @@ The available options for `action` are:
- `add_in_detail`: A boolean indicating if this action should be available in detail page.
- `confirmation_message`: A string message that if defined, will open a modal to ask for confirmation before calling the action method.


### Toast Notifications

You can display toast notifications after a custom action completes using
Expand Down
21 changes: 21 additions & 0 deletions sqladmin/_types.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import sys
from enum import Enum
from typing import (
Any,
AnyStr,
Callable,
Dict,
Iterable,
List,
Protocol,
Tuple,
Type,
TypeVar,
Union,
runtime_checkable,
)

from markupsafe import Markup
from sqlalchemy.engine import Engine
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker
from sqlalchemy.orm import (
Expand All @@ -19,6 +26,15 @@
)
from sqlalchemy.sql.expression import Select
from starlette.requests import Request
from typing_extensions import TypeAlias

if sys.version_info < (3, 11):

class StrEnum(str, Enum):
__str__ = str.__str__
__repr__ = Enum.__repr__
else:
from enum import StrEnum as StrEnum # noqa: F401

MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
ENGINE_TYPE = Union[Engine, AsyncEngine]
Expand Down Expand Up @@ -79,3 +95,8 @@ async def get_filtered_query(


ColumnFilter = Union[SimpleColumnFilter, OperationColumnFilter]

BASE_FORMATTERS_TYPE: TypeAlias = Dict[
Type[Any],
Callable[[Any], Union[Markup, Iterable[Markup], AnyStr, Iterable[AnyStr]]],
]
70 changes: 66 additions & 4 deletions sqladmin/formatters.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,82 @@
import datetime
from typing import Any

from markupsafe import Markup

from sqladmin._types import BASE_FORMATTERS_TYPE, StrEnum

def empty_formatter(value: Any) -> str:

def empty_formatter(value: Any) -> Markup:
"""Return empty string for `None` value"""
return ""

return Markup("") # nosec


def bool_formatter(value: bool) -> Markup:
"""Return check icon if value is `True` or X otherwise."""

icon_class = "fa-check text-success" if value else "fa-times text-danger"
return Markup("<i class='fa {}'></i>").format(icon_class)
return Markup("<i class='fa {}'></i>").format(icon_class) # nosec


def str_enum_formatter(value: StrEnum) -> Markup:
"""Return badge for value and list of available StrEnum values in tooltip."""

title = ""
if hasattr(value, "_member_names_") and len(value._member_names_) > 0:
title = f'title="Available values: {", ".join(value._member_names_)}">'

return Markup(
f'<span class="my-1 py-1 px-2 badge bg-secondary '
f'text-light lead d-inline-block text-truncate" '
f'data-bs-toggle="tooltip" '
f'data-bs-html="true" '
f'data-bs-placement="bottom" '
f"{title}"
f"{value}"
f"</span>"
) # nosec


def datetime_formatter(value: datetime.datetime) -> Markup:
"""Return badge for easy viewing of datetime."""

return Markup(
f"<span "
f'class="my-1 py-1 px-2 badge bg-secondary text-light '
f'lead d-inline-block text-truncate" '
f'data-bs-toggle="tooltip" '
f'data-bs-html="true" '
f'data-bs-placement="bottom" '
f'title="{value}"'
f">"
f'<i class="fa-solid fa-calendar-days"></i> '
f"{value.strftime('%d %B %Y %H:%M:%S')}"
f"</span>"
) # nosec


def copy_to_clipboard_formatter(value: Any) -> Markup:
"""Return value with copy to clipboard button and alert."""

escaped_value = Markup.escape(str(value))

return Markup(
f'<div class="d-flex justify-content-start align-items-center">'
f'<div class="me-2">{escaped_value}</div>'
f"<button "
f'class="btn btn-link p-2 me-2" '
f'data-copy-value="{escaped_value}" '
f'onclick="copyToClipboard(this, this.dataset.copyValue)"'
f">"
f'<i class="fas fa-copy"></i>'
f"</button>"
f'<div class="alert alert-primary fade mb-0 p-1">Copied!</div>'
f"</div>"
) # nosec


BASE_FORMATTERS = {
BASE_FORMATTERS: BASE_FORMATTERS_TYPE = {
type(None): empty_formatter,
bool: bool_formatter,
}
Loading
Loading