Skip to content

Commit

Permalink
Add form_rules, form_create_rules, form_edit_rules (#779)
Browse files Browse the repository at this point in the history
  • Loading branch information
aminalaee authored Jun 12, 2024
1 parent b51c952 commit c2dd278
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 44 deletions.
5 changes: 4 additions & 1 deletion docs/api_reference/model_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@
- form_include_pk
- form_ajax_refs
- form_converter
- form_edit_query
- form_rules
- form_create_rules
- form_edit_rules
- column_type_formatters
- list_query
- count_query
- search_query
- sort_query
- edit_form_query
- on_model_change
- after_model_change
- on_model_delete
Expand Down
7 changes: 6 additions & 1 deletion docs/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ The forms are based on `WTForms` package and include the following options:
* `form_include_pk`: Control if primary key column should be included in create/edit forms. Default is `False`.
* `form_ajax_refs`: Use Ajax with Select2 for loading relationship models async. This is use ful when the related model has a lot of records.
* `form_converter`: Allow adding custom converters to support additional column types.
* `edit_form_query`: A method with the signature of `(request) -> stmt` which can customize the edit form data.
* `form_edit_query`: A method with the signature of `(request) -> stmt` which can customize the edit form data.
* `form_rules`: List of form rules to manage rendering and behaviour of form.
* `form_create_rules`: List of form rules to manage rendering and behaviour of form in create page.
* `form_edit_rules`: List of form rules to manage rendering and behaviour of form in edit page.

!!! example

Expand All @@ -217,6 +220,8 @@ The forms are based on `WTForms` package and include the following options:
"order_by": ("id",),
}
}
form_create_rules = ["name", "password"]
form_edit_rules = ["name"]
```

## Export options
Expand Down
2 changes: 2 additions & 0 deletions sqladmin/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ async def create(self, request: Request) -> Response:
model_view = self._find_model_view(identity)

Form = await model_view.scaffold_form()
model_view._validate_form_class(model_view._form_create_rules, Form)
form_data = await self._handle_form_data(request)
form = Form(form_data)

Expand Down Expand Up @@ -559,6 +560,7 @@ async def edit(self, request: Request) -> Response:
raise HTTPException(status_code=404)

Form = await model_view.scaffold_form()
model_view._validate_form_class(model_view._form_edit_rules, Form)
context = {
"obj": model,
"model_view": model_view,
Expand Down
58 changes: 58 additions & 0 deletions sqladmin/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from __future__ import annotations

import time
import warnings
from enum import Enum
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -29,6 +32,7 @@
from starlette.requests import Request
from starlette.responses import StreamingResponse
from wtforms import Field, Form
from wtforms.fields.core import UnboundField

from sqladmin._queries import Query
from sqladmin._types import MODEL_ATTR
Expand Down Expand Up @@ -598,6 +602,28 @@ class UserAdmin(ModelAdmin, model=User):
```
"""

form_rules: ClassVar[list[str]] = []
"""List of rendering rules for model creation and edit form.
This property changes default form rendering behavior and to rearrange
order of rendered fields, add some text between fields, group them, etc.
If not set, will use default Flask-Admin form rendering logic.
???+ example
```python
class UserAdmin(ModelAdmin, model=User):
form_rules = [
"first_name",
"last_name",
]
```
"""

form_create_rules: ClassVar[list[str]] = []
"""Customized rules for the create form. Cannot be specified with `form_rules`."""

form_edit_rules: ClassVar[list[str]] = []
"""Customized rules for the edit form. Cannot be specified with `form_rules`."""

# General options
column_labels: ClassVar[Dict[MODEL_ATTR, str]] = {}
"""A mapping of column labels, used to map column names to new names.
Expand Down Expand Up @@ -685,6 +711,8 @@ def __init__(self) -> None:
model_admin=self, name=name, options=options
)

self._refresh_form_rules_cache()

self._custom_actions_in_list: Dict[str, str] = {}
self._custom_actions_in_detail: Dict[str, str] = {}
self._custom_actions_confirmation: Dict[str, str] = {}
Expand Down Expand Up @@ -1054,6 +1082,13 @@ def list_query(self, request: Request) -> Select:
return select(self.model)

def edit_form_query(self, request: Request) -> Select:
msg = (
"Overriding 'edit_form_query' is deprecated. Use 'form_edit_query' instead."
)
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return self.form_edit_query(request)

def form_edit_query(self, request: Request) -> Select:
"""
The SQLAlchemy select expression used for the edit form page which can be
customized. By default it will select the object by primary key(s) without any
Expand Down Expand Up @@ -1143,3 +1178,26 @@ async def generate(writer: Writer) -> AsyncGenerator[Any, None]:
media_type="text/csv",
headers={"Content-Disposition": f"attachment;filename={filename}"},
)

def _refresh_form_rules_cache(self) -> None:
if self.form_rules:
self._form_create_rules = self.form_rules
self._form_edit_rules = self.form_rules
else:
self._form_create_rules = self.form_create_rules
self._form_edit_rules = self.form_edit_rules

def _validate_form_class(self, ruleset: List[Any], form_class: Type[Form]) -> None:
form_fields = []
for name, obj in form_class.__dict__.items():
if isinstance(obj, UnboundField):
form_fields.append(name)

missing_fields = []
if ruleset:
for field_name in form_fields:
if field_name not in ruleset:
missing_fields.append(field_name)

for field_name in missing_fields:
delattr(form_class, field_name)
33 changes: 33 additions & 0 deletions sqladmin/templates/sqladmin/_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,36 @@
{% endfor %}
</div>
{% endmacro %}

{% macro render_field(field, kwargs={}) %}
<div class="mb-3 form-group row">
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
<div class="col-sm-10">
{% if field.errors %}
{{ field(class_="form-control is-invalid") }}
{% else %}
{{ field() }}
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% if field.description %}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
</div>
</div>
{% endmacro %}

{% macro render_form_fields(form, form_opts=None) %}
{% if form.hidden_tag is defined %}
{{ form.hidden_tag() }}
{% else %}
{% for f in form if f.widget.input_type == 'hidden' %}
{{ f }}
{% endfor %}
{% endif %}

{% for f in form if f.widget.input_type != 'hidden' %}
{{ render_field(f, kwargs) }}
{% endfor %}
{% endmacro %}
20 changes: 2 additions & 18 deletions sqladmin/templates/sqladmin/create.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "sqladmin/layout.html" %}
{% from 'sqladmin/_macros.html' import render_form_fields %}
{% block content %}
<div class="col-12">
<div class="card">
Expand All @@ -14,24 +15,7 @@ <h3 class="card-title">New {{ model_view.name }}</h3>
{% endif %}
</div>
<fieldset class="form-fieldset">
{% for field in form %}
<div class="mb-3 form-group row">
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
<div class="col-sm-10">
{% if field.errors %}
{{ field(class_="form-control is-invalid") }}
{% else %}
{{ field() }}
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% if field.description %}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
</div>
</div>
{% endfor %}
{{ render_form_fields(form, form_opts=form_opts) }}
</fieldset>
<div class="row">
<div class="col-md-2">
Expand Down
32 changes: 8 additions & 24 deletions sqladmin/templates/sqladmin/edit.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "sqladmin/layout.html" %}
{% from 'sqladmin/_macros.html' import render_form_fields %}
{% block content %}
<div class="col-12">
<div class="card">
Expand All @@ -14,24 +15,7 @@ <h3 class="card-title">Edit {{ model_view.name }}</h3>
{% endif %}
</div>
<fieldset class="form-fieldset">
{% for field in form %}
<div class="mb-3 form-group row">
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
<div class="col-sm-10">
{% if field.errors %}
{{ field(class_="form-control is-invalid") }}
{% else %}
{{ field() }}
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% if field.description %}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
</div>
</div>
{% endfor %}
{{ render_form_fields(form, form_opts=form_opts) }}
</fieldset>
<div class="row">
<div class="col-md-2">
Expand All @@ -44,11 +28,11 @@ <h3 class="card-title">Edit {{ model_view.name }}</h3>
<input type="submit" name="save" value="Save" class="btn">
<input type="submit" name="save" value="Save and continue editing" class="btn">
{% if model_view.can_create %}
{% if model_view.save_as %}
<input type="submit" name="save" value="Save as new" class="btn">
{% else %}
<input type="submit" name="save" value="Save and add another" class="btn">
{% endif %}
{% if model_view.save_as %}
<input type="submit" name="save" value="Save as new" class="btn">
{% else %}
<input type="submit" name="save" value="Save and add another" class="btn">
{% endif %}
{% endif %}
</div>
</div>
Expand All @@ -57,4 +41,4 @@ <h3 class="card-title">Edit {{ model_view.name }}</h3>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
6 changes: 6 additions & 0 deletions tests/test_views/test_view_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ class UserAdmin(ModelView, model=User):
User.profile_formattable: lambda m, a: f"Formatted {m.profile_formattable}",
}
save_as = True
form_create_rules = ["name", "email", "addresses", "profile", "birthdate", "status"]
form_edit_rules = ["name", "email", "addresses", "profile", "birthdate"]


class AddressAdmin(ModelView, model=Address):
Expand Down Expand Up @@ -442,6 +444,7 @@ def test_create_endpoint_get_form(client: TestClient) -> None:
'<input class="form-control" id="email" name="email" type="text" value="">'
in response.text
)
assert '<select class="form-control" id="status" name="status">' in response.text


def test_create_endpoint_post_form(client: TestClient) -> None:
Expand Down Expand Up @@ -591,6 +594,9 @@ def test_update_get_page(client: TestClient) -> None:
assert (
'id="name" maxlength="16" name="name" type="text" value="Joe">' in response.text
)
assert (
'<select class="form-control" id="status" name="status">' not in response.text
)

response = client.get("/admin/address/edit/1")

Expand Down

0 comments on commit c2dd278

Please sign in to comment.