Skip to content

Commit 6c2028a

Browse files
committed
Fix sqlalchemy required fields. Fix CI.
1 parent f9d8f29 commit 6c2028a

4 files changed

Lines changed: 34 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Setup Node.js
2222
uses: actions/setup-node@v3
2323
with:
24-
node-version: v22.4.1
24+
node-version: v24.3.0
2525
cache: 'yarn'
2626
cache-dependency-path: 'frontend/yarn.lock'
2727
- name: Install Dependencies

docs/build.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ def read_cls_docstring(cls):
4242

4343
def get_versions():
4444
return [
45+
{
46+
"version": "0.3.1",
47+
"changes": [
48+
"Fix sqlalchemy required fields. Fix CI.",
49+
],
50+
},
4551
{
4652
"version": "0.3.0",
4753
"changes": [

fastadmin/models/orms/sqlalchemy.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,14 @@ def get_model_fields_with_widget_types(
7070
is_immutable = (
7171
is_pk or bool(getattr(orm_model_field, "onupdate", None))
7272
) and field_name not in self.readonly_fields
73-
required = (
74-
not getattr(orm_model_field, "nullable", False)
75-
and not getattr(orm_model_field, "default", False)
76-
and not is_m2m
77-
)
73+
# For columns, use the column's nullable; for relationships (MANYTOONE/ONETOONE),
74+
# the relationship object has no nullable — use the underlying FK column's nullable.
75+
nullable = getattr(orm_model_field, "nullable", False)
76+
if field_type in ("ONETOONE", "MANYTOONE"):
77+
fk_column = next((c for c in mapper.c if c.key == column_name), None)
78+
if fk_column is not None:
79+
nullable = getattr(fk_column, "nullable", False)
80+
required = not nullable and not getattr(orm_model_field, "default", False) and not is_m2m
7881
choices = (
7982
orm_model_field.type._object_lookup
8083
if hasattr(orm_model_field, "type") and hasattr(orm_model_field.type, "_object_lookup")

tests/models/test_orm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,22 @@ def test_get_form_widget_event(event):
322322
assert field.form_widget_props
323323
case _:
324324
raise ValueError(f"Unexpected field: {field.name}")
325+
326+
327+
def test_nullable_foreign_key_form_required(event):
328+
"""Nullable FK (base) must have required=False; non-nullable FK (tournament) required=True.
329+
330+
Suitable for all ORMs: Django/Tortoise use field.null, Pony uses is_required,
331+
SQLAlchemy uses the underlying FK column's nullable.
332+
"""
333+
admin_model = get_admin_model(event.__class__)
334+
fields = admin_model.get_model_fields_with_widget_types()
335+
by_name = {f.name: f for f in fields}
336+
assert "base" in by_name, "Event should have base (nullable FK) field"
337+
assert "tournament" in by_name, "Event should have tournament (required FK) field"
338+
assert (
339+
by_name["base"].form_widget_props["required"] is False
340+
), "nullable FK base_id should be optional in admin form"
341+
assert (
342+
by_name["tournament"].form_widget_props["required"] is True
343+
), "non-nullable FK tournament_id should be required in admin form"

0 commit comments

Comments
 (0)