Skip to content

Add form_rules, form_create_rules, form_edit_rules #779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading