Skip to content

Rebase on origin/master + other minors#79

Merged
hasansezertasan merged 16 commits into
hasansezertasan:masterfrom
ElLorans:feat/replace-xeditable-with-htmx-oren
Jun 1, 2026
Merged

Rebase on origin/master + other minors#79
hasansezertasan merged 16 commits into
hasansezertasan:masterfrom
ElLorans:feat/replace-xeditable-with-htmx-oren

Conversation

@ElLorans
Copy link
Copy Markdown

@ElLorans ElLorans commented May 24, 2026

Closes pallets-eco#2909.
TODO: Errors are not rendered. E.g.: if the user submits a non unique field on a unique constrained column, only a console error is registered.
image

High-level PR Summary

This PR replaces the unmaintained x-editable library with a custom HTMX-based implementation for inline editing in Flask-Admin list views. The XEditableWidget is now deprecated (with a backward compatibility alias) and replaced by HTMXEditableWidget, which uses HTMX to fetch edit forms and submit updates via dedicated ajax_edit and ajax_update endpoints. The change removes dependencies on x-editable and its associated CSS/JS files, replacing them with HTMX and custom inline styling. A comprehensive example covering all supported field types (StringField, TextAreaField, IntegerField, FloatField, BooleanField, DateField, TimeField, DateTimeField, Enum, and ForeignKey) is added. Tests are updated to verify the new HTMX-based editing flow, including validation error handling and widget restoration. The TODO notes that error rendering still requires work for constraint violations like unique field constraints.

⏱️ Estimated Review Time: 1-3 hours

💡 Review Order Suggestion
Order File Path
1 doc/changelog.rst
2 examples/sqla_column_editable/README.md
3 examples/sqla_column_editable/pyproject.toml
4 examples/sqla_column_editable/.python-version
5 examples/sqla_column_editable/__init__.py
6 examples/sqla_column_editable/main.py
7 flask_admin/model/widgets.py
8 flask_admin/_types.py
9 flask_admin/model/base.py
10 flask_admin/static/admin/js/form.js
11 flask_admin/templates/bootstrap4/admin/lib.html
12 flask_admin/templates/bootstrap4/admin/model/editable_cell_edit.html
13 flask_admin/templates/bootstrap4/admin/model/list.html
14 flask_admin/model/form.py
15 flask_admin/contrib/sqla/view.py
16 flask_admin/contrib/mongoengine/view.py
17 flask_admin/contrib/peewee/view.py
18 flask_admin/static/vendor/htmx/htmx.min.js
19 flask_admin/tests/sqla/test_basic.py
20 flask_admin/tests/peeweemodel/test_basic.py
21 flask_admin/tests/mongoengine/test_basic.py

Need help? Join our Discord

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 24, 2026

Reviewer's Guide

Replaces the legacy x-editable-based inline editing with a new HTMX-powered editable cell system across Flask-Admin, including widget, AJAX endpoints, JS behavior, templates, and tests, and adds a comprehensive SQLAlchemy example for column_editable_list.

Sequence diagram for HTMX-powered inline cell editing flow

sequenceDiagram
    actor User
    participant Browser as Browser_HTMX
    participant View as BaseModelView
    participant DB as Database

    User->>Browser: Click editable cell (editable_cell_display.html span)
    Browser->>View: GET /ajax/edit/ (ajax_edit)
    View->>View: list_form(obj=record)
    View->>View: _restore_original_widget(form, field_name)
    View-->>Browser: render(editable_cell_edit.html)

    User->>Browser: Submit inline edit form
    Browser->>View: POST /ajax/update/ (ajax_update)
    View->>View: list_form()
    View->>View: validate_form(form)
    alt valid
        View->>DB: update_model(form, record)
        alt update success
            View->>DB: get_one(pk)
            View->>View: get_list_value(None, record, field_name)
            View-->>Browser: render(editable_cell_display.html)
        else update failed
            View-->>Browser: render(editable_cell_edit.html, errors)
        end
    else invalid
        View-->>Browser: render(editable_cell_edit.html, errors), 500
    end

    Browser->>Browser: htmx:beforeSwap (allow 4xx/5xx swap for editable cells)
Loading

File-Level Changes

Change Details Files
Replace XEditableWidget with HTMXEditableWidget and adjust editable list form scaffolding and type aliases.
  • Introduce HTMXEditableWidget that renders clickable spans with hx-get for fetching edit forms and keep a deprecated XEditableWidget shim.
  • Update create_editable_list_form to swap original widgets with HTMXEditableWidget while storing originals in a _original_widgets map for later restoration.
  • Rename type aliases and default widget references in core, contrib views, and type hints from XEditableWidget to HTMXEditableWidget.
flask_admin/model/widgets.py
flask_admin/model/form.py
flask_admin/model/base.py
flask_admin/contrib/sqla/view.py
flask_admin/contrib/mongoengine/view.py
flask_admin/contrib/peewee/view.py
flask_admin/_types.py
Implement HTMX-based inline edit workflow via new ajax_edit/ajax_update behavior and editable cell templates.
  • Add _restore_original_widget helper and new GET /ajax/edit/ endpoint that returns an edit form fragment using the original widget for a single field.
  • Rewrite POST /ajax/update/ to validate and update a single editable field, handle BooleanField absence semantics, and return either a display fragment or an edit fragment with errors.
  • Introduce editable_cell_edit.html and editable_cell_display.html partials and update list.html to wrap editable cells with stable IDs used as HTMX targets.
flask_admin/model/base.py
flask_admin/templates/bootstrap4/admin/model/editable_cell_edit.html
flask_admin/templates/bootstrap4/admin/model/editable_cell_display.html
flask_admin/templates/bootstrap4/admin/model/list.html
Replace x-editable frontend integration with HTMX-powered popover UX and error handling.
  • Remove all x-editable-specific JS wiring and parameter translation from form.js.
  • Wire up HTMX lifecycle events to allow swapping error responses, managing popover open/close, handling keyboard and outside-click dismissal, and reapplying faForm widgets.
  • Add CSS and JS includes for HTMX and editable cell popover styling in the Bootstrap 4 admin lib template, removing x-editable assets.
flask_admin/static/admin/js/form.js
flask_admin/templates/bootstrap4/admin/lib.html
flask_admin/static/vendor/htmx/htmx.min.js
flask_admin/static/vendor/x-editable/css/bootstrap4-editable.css
flask_admin/static/vendor/x-editable/js/bootstrap4-editable.min.js
Expand and adapt tests for SQLA, Peewee, and MongoEngine to the new HTMX inline editing behavior, including additional field-type coverage.
  • Update existing column_editable_list tests to assert HTMX markers (hx-get, editable-cell, hx-post) and status codes instead of x-editable messages.
  • Add new tests for ajax_edit behavior, error re-rendering, and inline editing of multiple field types (textarea, numeric, boolean, date/time/datetime) in SQLA.
  • Add a new MongoEngine column_editable_list test that exercises basic inline editing, ajax_edit, non-editable field handling, and relations.
flask_admin/tests/sqla/test_basic.py
flask_admin/tests/peeweemodel/test_basic.py
flask_admin/tests/mongoengine/test_basic.py
Provide a new SQLAlchemy example app demonstrating column_editable_list with HTMX inline editing across many field types.
  • Add a small Flask app with Dish and Cuisine models showcasing inline editing for strings, text, numbers, booleans, dates/times, enums, and relations.
  • Include a README with instructions for running the example via uv and describing the covered field types.
  • Add a pyproject and Python version file to define the example’s environment and dependency on the editable Flask-Admin checkout.
examples/sqla_column_editable/main.py
examples/sqla_column_editable/README.md
examples/sqla_column_editable/pyproject.toml
examples/sqla_column_editable/.python-version

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The HTMX editable cell markup is now defined both in HTMXEditableWidget.__call__ and in editable_cell_display.html; consider consolidating this into a single rendering path to avoid future drift between the two.
  • The inline styles for .editable-popover and .editable-cell in bootstrap4/admin/lib.html make the admin look harder to customize; moving these rules into a dedicated CSS file under static (and including it only when editable_columns is set) would keep them more maintainable and theme-friendly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The HTMX editable cell markup is now defined both in `HTMXEditableWidget.__call__` and in `editable_cell_display.html`; consider consolidating this into a single rendering path to avoid future drift between the two.
- The inline styles for `.editable-popover` and `.editable-cell` in `bootstrap4/admin/lib.html` make the admin look harder to customize; moving these rules into a dedicated CSS file under `static` (and including it only when `editable_columns` is set) would keep them more maintainable and theme-friendly.

## Individual Comments

### Comment 1
<location path="flask_admin/static/admin/js/form.js" line_range="625-626" />
<code_context>
+document.addEventListener('htmx:afterSwap', function(event) {
+    var target = event.detail.target;
+
+    // Initialize all data-role widgets (select2, timepicker, datepicker, etc.)
+    faForm.applyGlobalStyles(target);
+
+    // Position and focus the popover
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `faForm` here will fail because it is not defined in this global scope.

Because this listener is outside the IIFE where `faForm` is defined, `faForm` will be undefined at runtime and trigger a `ReferenceError` when popovers are swapped in. Either attach `faForm` to `window` inside the IIFE and reference `window.faForm` here, or move these HTMX handlers into the IIFE so they share the same scope.
</issue_to_address>

### Comment 2
<location path="flask_admin/model/base.py" line_range="2760-2768" />
<code_context>
+        if not field_name or field_name not in self.column_editable_list:
+            abort(404)
+
+        # Prevent validation issues: delete all fields except the submitted
+        # field, primary key, and csrf token
+        keep = {field_name, "list_form_pk", "csrf_token"}
         for field in list(form):
-            if (field.name in request.form) or (field.name == "csrf_token"):
-                pass
-            else:
+            if field.name not in keep and field.name not in request.form:
                 form.__delitem__(field.name)

</code_context>
<issue_to_address>
**suggestion (bug_risk):** Field-pruning logic could keep unexpected fields and subtly affect validation.

Because of `if field.name not in keep and field.name not in request.form`, any field submitted in `request.form` but not in `keep` will remain on the form. That contradicts the intent to keep only the edited field, PK, and CSRF, and could let unintended fields affect validation. Consider basing the pruning solely on membership in `keep` (or an explicit allowlist) to enforce the inline-edit contract more strictly.

```suggestion
        if not field_name or field_name not in self.column_editable_list:
            abort(404)

        # Prevent validation issues: delete all fields except the submitted
        # field, primary key, and csrf token
        keep = {field_name, "list_form_pk", "csrf_token"}
        for field in list(form):
            if field.name not in keep:
                form.__delitem__(field.name)
```
</issue_to_address>

### Comment 3
<location path="flask_admin/tests/sqla/test_basic.py" line_range="929-939" />
<code_context>
+        assert 'name="int_field"' in data
+        assert "42" in data
+
+        # -- FloatField: edit and save --
+        rv = client.post(
+            "/admin/model2/ajax/update/",
+            data={
+                "list_form_pk": "1",
+                "float_field": "3.14",
+            },
+        )
+        data = rv.data.decode("utf-8")
+        assert rv.status_code == 200
+        assert 'class="editable-cell"' in data
+
+        # -- FloatField: edit form renders input --
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen FloatField test by asserting the updated value is rendered

For the FloatField "edit and save" case, you currently only assert the 200 status and presence of `editable-cell`. Please also assert that `"3.14"` appears in the response (as with the IntegerField case) to confirm the updated float value is actually rendered in the list view.

```suggestion
        # -- FloatField: edit and save --
        rv = client.post(
            "/admin/model2/ajax/update/",
            data={
                "list_form_pk": "1",
                "float_field": "3.14",
            },
        )
        data = rv.data.decode("utf-8")
        assert rv.status_code == 200
        assert 'class="editable-cell"' in data
        assert "3.14" in data
```
</issue_to_address>

### Comment 4
<location path="flask_admin/tests/mongoengine/test_basic.py" line_range="243" />
<code_context>
+    rv = client.get(f"/admin/editable_model/ajax/edit/?pk={obj1.pk}&field=test2")
+    assert rv.status_code == 404
+
+    # Test relation editing
+    rv = client.post(
+        "/admin/editable_related/ajax/update/",
</code_context>
<issue_to_address>
**suggestion (testing):** Extend relation editing test to assert the related object actually changed

This test currently only checks for the presence of `editable-cell` in the response. To verify the relation edit actually applies, please also assert that the updated related object is reflected—for example, by checking for `obj2`’s name in the response or by reloading `RelatedModel` from MongoDB and asserting its `ref` is `obj2`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +625 to +626
// Initialize all data-role widgets (select2, timepicker, datepicker, etc.)
faForm.applyGlobalStyles(target);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using faForm here will fail because it is not defined in this global scope.

Because this listener is outside the IIFE where faForm is defined, faForm will be undefined at runtime and trigger a ReferenceError when popovers are swapped in. Either attach faForm to window inside the IIFE and reference window.faForm here, or move these HTMX handlers into the IIFE so they share the same scope.

Comment thread flask_admin/model/base.py
Comment thread flask_admin/tests/sqla/test_basic.py
rv = client.get(f"/admin/editable_model/ajax/edit/?pk={obj1.pk}&field=test2")
assert rv.status_code == 404

# Test relation editing
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend relation editing test to assert the related object actually changed

This test currently only checks for the presence of editable-cell in the response. To verify the relation edit actually applies, please also assert that the updated related object is reflected—for example, by checking for obj2’s name in the response or by reloading RelatedModel from MongoDB and asserting its ref is obj2.

@ElLorans ElLorans changed the title Feat/replace xeditable with htmx oren Rebase on origin/master May 24, 2026
@ElLorans ElLorans changed the title Rebase on origin/master Rebase on origin/master + other minors May 24, 2026
@ElLorans ElLorans closed this May 25, 2026
@ElLorans ElLorans deleted the feat/replace-xeditable-with-htmx-oren branch May 25, 2026 01:02
@ElLorans ElLorans restored the feat/replace-xeditable-with-htmx-oren branch May 25, 2026 01:03
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

Comment thread flask_admin/static/admin/js/form.js Outdated
@ElLorans ElLorans reopened this May 25, 2026
@ElLorans
Copy link
Copy Markdown
Author

Now err msgs appear
image

@ElLorans
Copy link
Copy Markdown
Author

New security issues found

Where? Is it fixed now?

ElLorans and others added 5 commits May 25, 2026 23:10
…o#1724) (pallets-eco#2902)

* fix(sqla): coerce ARRAY element type in `conv_ARRAY` (closes pallets-eco#1724)

`AdminModelConverter.conv_ARRAY` returned a `Select2TagsField` with no
`coerce` callable, so every submitted value was cast via the default
`text_type` and the resulting Python list was always `list[str]`. For a
Postgres `ARRAY(Integer)` column that round-trip fails on save with

    column "x" is of type integer[] but expression is of type text[]

(reported in pallets-eco#1724 in 2018 and still reproducible on `master` as of
v2.2.0; the long-standing workaround in that thread is to subclass
`Select2TagsField` and override `process_formdata` to call `int()` on
each entry).

---------

Co-authored-by: ElLorans <lorenzo.cerreta@gmail.com>
@hasansezertasan hasansezertasan merged commit e1af998 into hasansezertasan:master Jun 1, 2026
1 check passed
@hasansezertasan
Copy link
Copy Markdown
Owner

Hey @ElLorans, thanks for the work. I just merged it. Should I close pallets-eco#2847 and open a fresh PR (pallets-eco/flask-admin@master...hasansezertasan:flask-admin:master) or would you like to proceed by yourself? I am OK both ways.

@ElLorans
Copy link
Copy Markdown
Author

ElLorans commented Jun 1, 2026

Hey @ElLorans, thanks for the work. I just merged it. Should I close pallets-eco#2847 and open a fresh PR (pallets-eco/flask-admin@master...hasansezertasan:flask-admin:master) or would you like to proceed by yourself? I am OK both ways.

Urgh, I opened a PR to the wrong branch. I will open a PR vs the right one, my bad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants