MongoEngine `QueryAjaxModelLoader.format` crashes (HTTP 500) on unresolved `DBRef` values
## Summary
`QueryAjaxModelLoader.format()` in `flask_admin.contrib.mongoengine.ajax`
accesses `model.pk` unconditionally. When MongoEngine cannot dereference a
`ReferenceField` (e.g. the referenced document has been deleted, or the
reference points to a different collection / database), it returns a raw
`bson.dbref.DBRef` instead of a `Document`. `DBRef` has no `.pk` attribute,
so the AJAX endpoint that powers the admin autocomplete widget crashes with
an `AttributeError`, surfacing as **HTTP 500** and breaking the edit form.
This was originally reported in #2036 (Sep 2020). The PR there bundled an
unrelated `Content-Disposition` fix, went stale, and now conflicts with
`master`. Opening a focused issue so the fix can be discussed and landed on
its own.
## Expected behavior
When an AJAX reference loader encounters a broken/unresolved reference, the
widget should degrade gracefully — either display the `DBRef` id as the
label (so the user can see *what* is broken and clear it) or return `None`
and let the form render an empty selection — not return a 500.
## Actual behavior
```
AttributeError: 'DBRef' object has no attribute 'pk'
```
…rendered as `500 Internal Server Error` from the `/admin/<view>/ajax/lookup/`
endpoint. The edit form's reference field becomes unusable until the broken
reference is cleaned up directly in the database.
## Root cause
`flask_admin/contrib/mongoengine/ajax.py:52`:
```python
def format(self, model: Document | None) -> tuple[str, str] | None:
if not model:
return None
return (as_unicode(model.pk), as_unicode(model))
```
`model` here can be a `DBRef` when MongoEngine fails to dereference. There
is no type-check before `.pk` access.
## Notes on PR #2036
The original PR proposed:
```python
from bson.dbref import DBRef
def format(self, model):
if not model:
return None
if not isinstance(model, DBRef):
return (as_unicode(model.pk), as_unicode(model))
else:
return (as_unicode(model), as_unicode(model))
```
The shape is right, but a few things to settle before landing:
1. `as_unicode(dbref)` returns a string like `DBRef('coll', ObjectId('…'))`,
which is not a usable id for the autocomplete value. The first tuple
element should probably be `as_unicode(model.id)` (the `ObjectId` the
`DBRef` carries) so the form posts back something the persistence layer
can resolve.
2. Worth deciding whether the second tuple element (the user-facing label)
should be the raw `DBRef` repr or a clearer marker like
`"(broken reference)"` — the current `as_unicode(model)` exposes
internal types to end users.
3. The import is fine at module level (`bson` is already a transitive dep
of `mongoengine`), but a typing-friendly form is preferable.
## Possible fix sketch (for discussion, not part of this issue)
```python
from bson.dbref import DBRef
def format(self, model: Document | DBRef | None) -> tuple[str, str] | None:
if not model:
return None
if isinstance(model, DBRef):
return (as_unicode(model.id), f"(missing: {model.collection}/{model.id})")
return (as_unicode(model.pk), as_unicode(model))
```
…plus a unit test that constructs a `DBRef` pointing at a non-existent
document and asserts `format()` returns a 2-tuple instead of raising.
## Reproducer
A full MRE requires MongoDB. The minimal path:
1. Define two MongoEngine `Document` classes — `Author` and `Book`, where
`Book.author = ReferenceField(Author)`.
2. Register a `ModelView` for `Book` with `form_ajax_refs={"author": {...}}`.
3. Create an `Author`, create a `Book` referencing it, then delete the
`Author` document directly (bypassing cascade).
4. Open the `Book` edit view → AJAX call to populate the existing value
crashes with the `AttributeError` above.
## Environment
- `flask-admin` from `master` (commit at time of writing).
- `mongoengine` (any recent version).
- MongoDB.
## Related
- #2036 — original report and bundled PR (will be closed in favour of two
focused PRs).
---
> Disclosure: I drafted this issue with help from Claude Code while triaging
> stale PR #2036; the references above were verified manually.
0 条评论