Skip to content
10 changes: 10 additions & 0 deletions examples/peewee_simple/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Post(BaseModel):
title = CharField(max_length=120)
text = peewee.TextField(null=False)
date = peewee.DateTimeField()
status = CharField(max_length=20, default="draft")

user = peewee.ForeignKeyField(User)

Expand Down Expand Up @@ -70,6 +71,15 @@ class PostAdmin(ModelView):

form_ajax_refs = {"user": {"fields": (User.username, "email")}}

form_choices = {
"status": [
("draft", "Draft"),
("review", "In Review"),
("published", "Published"),
("archived", "Archived"),
]
}


class Book(BaseModel):
isbn = CharField(max_length=13, primary_key=True)
Expand Down
25 changes: 25 additions & 0 deletions flask_admin/contrib/peewee/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,31 @@ def __init__(self, view: t.Any, additional: t.Any = None) -> None:

self.overrides = getattr(self.view, "form_overrides", None) or {}

def convert(self, model: t.Any, field: t.Any, field_args: t.Any) -> t.Any:
info = super().convert(model, field, field_args)

# Override field type if necessary - form_overrides take priority
if field.name in self.overrides:
return info

# Check if a list of 'form_choices' are specified
form_choices = getattr(self.view, "form_choices", None)
if form_choices:
choices = form_choices.get(field.name)
if choices:
kwargs = dict(info.field.kwargs)
for k in ("choices", "allow_blank", "coerce"):
kwargs.pop(k, None)
return info._replace(
field=form.Select2Field(
choices=choices,
allow_blank=field.null,
**kwargs,
)
)

return info

def handle_foreign_key(
self, model: t.Any, field: t.Any, **kwargs: t.Any
) -> tuple[str, AjaxSelectField] | None:
Expand Down
117 changes: 117 additions & 0 deletions flask_admin/tests/peeweemodel/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,123 @@ class Model(BaseModel):
assert len(edit_form.test.validators) == 2 # type: ignore[attr-defined]


def test_form_choices(app: Flask, db: peewee.SqliteDatabase, admin: Admin) -> None:
class BaseModel(peewee.Model):
class Meta:
database = db

class Model(BaseModel):
title = peewee.CharField(max_length=200)
status = peewee.CharField(max_length=20, default="draft")
priority = peewee.CharField(max_length=20, null=True)

Model.create_table()

status_choices = [
("draft", "Draft"),
("review", "In Review"),
("published", "Published"),
]
priority_choices = [
("low", "Low"),
("high", "High"),
]
view = CustomModelView(
Model,
form_choices={
"status": status_choices,
"priority": priority_choices,
},
)
admin.add_view(view)

form_obj = view.create_form()

# Check that select field is rendered with correct choices
assert type(form_obj.status).__name__ == "Select2Field" # type: ignore[attr-defined]
assert form_obj.status.choices == status_choices # type: ignore[attr-defined]

# Nullable field should allow blank
assert type(form_obj.priority).__name__ == "Select2Field" # type: ignore[attr-defined]
assert form_obj.priority.choices == priority_choices # type: ignore[attr-defined]
assert form_obj.priority.allow_blank is True # type: ignore[attr-defined]

# Non-nullable field should not allow blank
assert form_obj.status.allow_blank is False # type: ignore[attr-defined]

# Fields not in form_choices should convert normally
assert type(form_obj.title).__name__ == "StringField" # type: ignore[attr-defined]


def test_form_choices_persists_and_prepopulates(
app: Flask, db: peewee.SqliteDatabase, admin: Admin
) -> None:
class BaseModel(peewee.Model):
class Meta:
database = db

class Model(BaseModel):
title = peewee.CharField(max_length=200)
status = peewee.CharField(max_length=20, default="draft")

Model.create_table()

view = CustomModelView(
Model,
form_choices={
"status": [
("draft", "Draft"),
("published", "Published"),
],
},
)
admin.add_view(view)

client = app.test_client()

rv = client.post(
"/admin/model/new/",
data={"title": "Hello", "status": "published"},
)
assert rv.status_code == 302

model = Model.select().get() # type: ignore[no-untyped-call]
assert model.status == "published"

rv = client.get(f"/admin/model/edit/?id={model.id}")
assert rv.status_code == 200
assert b"published" in rv.data
edit_form = view.edit_form(obj=model)
assert edit_form.status.data == "published" # type: ignore[attr-defined]


def test_form_choices_with_form_overrides(
app: Flask, db: peewee.SqliteDatabase, admin: Admin
) -> None:
class BaseModel(peewee.Model):
class Meta:
database = db

class Model(BaseModel):
status = peewee.CharField(max_length=20, default="draft")

Model.create_table()

view = CustomModelView(
Model,
form_choices={
"status": [("draft", "Draft"), ("published", "Published")],
},
form_overrides={"status": fields.TextAreaField},
)
admin.add_view(view)

form_obj = view.create_form()

# form_overrides should take priority over form_choices
assert type(form_obj.status).__name__ == "TextAreaField" # type: ignore[attr-defined]


def test_ajax_fk(app: Flask, db: peewee.SqliteDatabase, admin: Admin) -> None:
class BaseModel(peewee.Model):
class Meta:
Expand Down