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
53 changes: 51 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This document provides key patterns and gotchas for developers and AI assistants
| [SBAdminField](#sbadminfield---list-display-columns) | Defining list columns, annotations, `supporting_annotates`, admin methods, ordering with computed fields, `sbadmin_list_display_data` |
| [Configuration](#configuration) | `INSTALLED_APPS`, role config, menu items, queryset restrictions, custom permissions |
| [Filter Widgets](#filter-widgets) | Built-in widgets, custom filters, `filter_query_lambda` for M2M filtering |
| [Form Widgets](#form-widgets) | `SBAdminTextTagsWidget`, `Meta.widgets` initialization, required select placeholders, `SBAdminJsonEditorWidget` for schema-driven JSON |
| [Form Widgets](#form-widgets) | `SBAdminTextTagsWidget`, input prefix/suffix on text and number widgets, `Meta.widgets` initialization, required select placeholders, `SBAdminJsonEditorWidget` for schema-driven JSON |
| [Admin Registration](#admin-registration) | `@admin.register` with `sb_admin_site`, `sbadmin_list_filter` vs `list_filter` |
| [Selection Actions](#selection-actions-bulk-actions) | Modal forms for bulk operations, `ListActionModalView`, confirmation modals, `SBAdminCustomAction` params, per-action permissions, success/error handling |
| [Row Actions](#row-actions-per-row-list-buttons) | Per-row icon buttons with `SBAdminRowAction`, `RowActionModalView`, and row-aware enablement |
Expand Down Expand Up @@ -145,6 +145,24 @@ class ArticleAdmin(SBAdmin):
| `filter_disabled` | bool | Disable filtering for this field |
| `python_formatter` | callable | Format value: `(obj_id, value) -> formatted_value` |
| `list_visible` | bool | Show/hide column in list |
| `tabulator_options` | TabulatorFieldOptions | Per-column Tabulator settings (width, grow, max, custom SBAdmin options) |

### Tabulator Options (table)

| Option | Type | Description |
|--------|------|-------------|
| `sbadminKeepDataWidth` | bool | Keep column natural width (prevent stretch) when using `fitDataFillAvailableSpace`. Best for icon/utility columns. |

```python
from django_smartbase_admin.engine.field import SBAdminField, TabulatorFieldOptions

sbadmin_list_display = (
SBAdminField(
name="id",
tabulator_options=TabulatorFieldOptions(sbadminKeepDataWidth=True),
),
)
```

### Admin Methods (like Django admin)

Expand Down Expand Up @@ -1078,6 +1096,37 @@ class ArticleTagNamesForm(SBAdminBaseFormInit, forms.Form):
- Duplicate values are prevented client-side.
- Works with dynamically-added rows in SBAdmin formsets and wizard formsets.

### Input prefix and suffix (text and number widgets)

Pass optional `prefix` and/or `suffix` strings to `SBAdminTextInputWidget` or `SBAdminNumberWidget` (e.g. currency, units, URL stem). Omit both for a normal input.

```python
from django import forms

from django_smartbase_admin.admin.admin_base import SBAdminBaseForm
from django_smartbase_admin.admin.widgets import (
SBAdminNumberWidget,
SBAdminTextInputWidget,
)

from blog.models import Article


class ArticleForm(SBAdminBaseForm):
class Meta:
model = Article
fields = ("slug", "price", "discount")
widgets = {
"slug": SBAdminTextInputWidget(prefix="https://blog.example.com/"),
"price": SBAdminNumberWidget(suffix="€"),
"discount": SBAdminNumberWidget(prefix="-", suffix="%"),
}
```

**Key points:**
- Addons are display-only; they do not change the stored field value.
- Other widgets can support the same pattern via `SBAdminInputAffixMixin` (mix in and forward `prefix` / `suffix` in `__init__`).

### `Meta.widgets` are initialized automatically in `SBAdminBaseForm`

When a form inherits from `SBAdminBaseForm`, widgets defined in `Meta.widgets` are initialized even if the field is not re-declared on the form class.
Expand Down Expand Up @@ -1532,7 +1581,7 @@ class ArticleAdmin(SBAdmin):
title=lambda row: _("Archive %(title)s") % {"title": row.get("title", "")},
icon="Delete",
view=self,
css_class=lambda row: "btn-icon btn-destructive",
css_class=lambda row: "btn btn-small btn-only-icon btn-destructive",
enabled_if=lambda row: row.get("status") != "archived",
),
# Mode 3: plain link. MODIFIER_OBJECT_ID is replaced with the row pk.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-smartbase-admin"
version = "1.2.0"
version = "1.3.0"
description = ""
authors = ["SmartBase <info@smartbase.sk>"]
readme = "README.md"
Expand Down
5 changes: 3 additions & 2 deletions src/django_smartbase_admin/admin/admin_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,10 +943,11 @@ def render_change_form(
):
if context.get("sbadmin_is_modal"):
media = context["media"]
js_assets = [str(asset) for asset in getattr(media, "_js", [])]
media_json = {
"js": list(getattr(media, "_js", [])),
"js": js_assets,
"css": {
medium: list(paths)
medium: [str(path) for path in paths]
for medium, paths in getattr(media, "_css", {}).items()
},
}
Expand Down
47 changes: 41 additions & 6 deletions src/django_smartbase_admin/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,41 @@ def get_context(self, name, value, attrs):
return context


class SBAdminTextInputWidget(SBAdminBaseWidget, forms.TextInput):
class SBAdminInputAffixMixin:
def __init__(self, *args, prefix=None, suffix=None, **kwargs):
self.prefix = prefix
self.suffix = suffix
super().__init__(*args, **kwargs)

def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["widget"]["prefix"] = self.prefix
context["widget"]["suffix"] = self.suffix
return context

def get_attrs_with_affix_classes(self, attrs, prefix=None, suffix=None):
attrs = dict(attrs or {})
classes = attrs.get("class", "")
if prefix:
classes = f"{classes} rounded-l-none".strip()
if suffix:
classes = f"{classes} rounded-r-none".strip()
attrs["class"] = classes
return attrs


class SBAdminTextInputWidget(
SBAdminInputAffixMixin, SBAdminBaseWidget, forms.TextInput
):
template_name = "sb_admin/widgets/text.html"

def __init__(self, form_field=None, attrs=None):
super().__init__(form_field, attrs={"class": "input", **(attrs or {})})
def __init__(self, form_field=None, attrs=None, prefix=None, suffix=None):
attrs = self.get_attrs_with_affix_classes(
{"class": "input", **(attrs or {})},
prefix=prefix,
suffix=suffix,
)
super().__init__(form_field, attrs=attrs, prefix=prefix, suffix=suffix)


class SBAdminTextTagsWidget(SBAdminBaseWidget, forms.TextInput):
Expand Down Expand Up @@ -159,12 +189,17 @@ def __init__(self, form_field=None, attrs=None):
super().__init__(form_field, attrs={"class": "input", **(attrs or {})})


class SBAdminNumberWidget(SBAdminBaseWidget, forms.NumberInput):
class SBAdminNumberWidget(SBAdminInputAffixMixin, SBAdminBaseWidget, forms.NumberInput):
class_name = "input"
template_name = "sb_admin/widgets/number.html"

def __init__(self, form_field=None, attrs=None):
super().__init__(form_field, attrs={"class": self.class_name, **(attrs or {})})
def __init__(self, form_field=None, attrs=None, prefix=None, suffix=None):
attrs = self.get_attrs_with_affix_classes(
{"class": self.class_name, **(attrs or {})},
prefix=prefix,
suffix=suffix,
)
super().__init__(form_field, attrs=attrs, prefix=prefix, suffix=suffix)


class SBAdminCheckboxWidget(SBAdminBaseWidget, forms.CheckboxInput):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def test_row_actions_are_processed_into_table_column_and_row_descriptors(self):
"url": "/actions/PublishArticleView/7/",
"title": "Publish Draft Article",
"icon": "Check-correct",
"css_class": "btn btn-tiny btn-icon",
"css_class": "btn btn-small btn-only-icon",
"open_in_modal": True,
"is_method_action": False,
"open_in_new_tab": False,
Expand All @@ -197,7 +197,7 @@ def test_row_actions_are_processed_into_table_column_and_row_descriptors(self):
"url": "/articles/7/",
"title": "Open",
"icon": "Preview-open",
"css_class": "btn btn-tiny btn-icon",
"css_class": "btn btn-small btn-only-icon",
"open_in_modal": False,
"is_method_action": False,
"open_in_new_tab": True,
Expand Down
26 changes: 26 additions & 0 deletions src/django_smartbase_admin/audit/tests/test_modal_media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Regression tests for modal media serialization in SBAdmin change form."""

import json
from unittest.mock import patch

from django.test import RequestFactory, TestCase
from js_asset import JS

from ckeditor.widgets import CKEditorWidget

from django_smartbase_admin.admin.admin_base import SBAdmin


class ModalMediaSerializationTests(TestCase):
def setUp(self):
self.request = RequestFactory().get("/")
self.admin = SBAdmin.__new__(SBAdmin)

@patch("django.contrib.admin.options.ModelAdmin.render_change_form")
def test_modal_ckeditor_widget_media_is_json_serializable(
self, mock_render_change_form
):
media = CKEditorWidget().media
context = {"sbadmin_is_modal": True, "media": media}
self.admin.render_change_form(self.request, context)
json.dumps(context["media_json"])
2 changes: 1 addition & 1 deletion src/django_smartbase_admin/engine/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class SBAdminRowAction(SBAdminCustomAction):
"""

target_view = None
css_class = "btn btn-tiny btn-icon"
css_class = "btn btn-small btn-only-icon"
open_in_new_tab = False
enabled_if = None
enabled_field = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
.column-widget-columns {
& > li {
@apply relative px-12 py-8 flex items-center;
.checkbox.checkbox-icon:not(:checked) + label {
.checkbox.checkbox-icon:not(:checked) + :where(label, .label) {
@media (pointer:fine) {
opacity: 0;
}
}
&:hover {
.checkbox.checkbox-icon + label {
.checkbox.checkbox-icon + :where(label, .label) {
opacity: 1;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@
}
}

.btn-only-icon {
@apply px-0 aspect-square !justify-center;
>span {
@apply hidden;
}
>svg {
@apply mx-0;
}
}

.btn-group {
@apply flex;
> * {
Expand Down
Loading
Loading