Skip to content

Commit a2544f7

Browse files
committed
0.1.30: inlines, wysiwig editor and form_fields_widgets parameter.
1 parent 202cf1a commit a2544f7

33 files changed

Lines changed: 571 additions & 335 deletions

File tree

docs/index.html

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ <h4 class="title">FastAdmin</h4>
181181
<li class="nav-item">
182182
<a class="nav-link" href="#changelog">Changelog</a>
183183
<ul class="nav flex-column">
184+
<li class="nav-item">
185+
<a class="nav-link" href="#v0_1_30">v0.1.30</a>
186+
</li>
184187
<li class="nav-item">
185188
<a class="nav-link" href="#v0_1_29">v0.1.29</a>
186189
</li>
@@ -227,7 +230,7 @@ <h1>Documentation</h1>
227230
<div class="row">
228231
<div class="col-sm-6 col-lg-4">
229232
<ul class="list-unstyled">
230-
<li><strong>Version:</strong> 0.1.29</li>
233+
<li><strong>Version:</strong> 0.1.30</li>
231234
<li>
232235
<strong>Author:</strong>
233236
<a href="mailto:vsdudakov@gmail.com" target="_blank"
@@ -800,6 +803,14 @@ <h3>ModelAdmin options</h3>
800803
<p class="lead">Base options:</p>
801804
<pre>
802805
<code class="language-python">
806+
# Widgets for fields. Overload widgets in your ModelAdmin class using this parameter.
807+
# It contains widget type and default widget props for type.
808+
# Example of usage:
809+
# form_fields_widgets = {
810+
# "description": (WidgetType.RichTextArea, {})
811+
# }
812+
form_fields_widgets: dict[str, tuple[WidgetType, dict]] = {}
813+
803814
# A list of actions to make available on the change list page.
804815
# You have to implement methods with names like action_name in your ModelAdmin class and decorate them with @action decorator.
805816
# Example of usage:
@@ -1367,6 +1378,38 @@ <h2>Changelog</h2>
13671378
</p>
13681379
<hr class="small-divider" />
13691380
<!-- <p class="alert alert-info mb-5"> For Future Updates Follow Us <a target="_blank" href="http://themeforest.net/user/harnishdesign?ref=HarnishDesign">@themeforest</a> / <a target="_blank" href="http://facebook.com/harnishdesign">@facebook</a> / <a target="_blank" href="http://twitter.com/harnishdesign">@twitter</a> / <a target="_blank" href="https://dribbble.com/harnishdesign">@Dribbble</a></p> -->
1381+
<h3 id="v0_1_30">
1382+
Version 0.1.30
1383+
<small class="text-muted">(26 March, 2024)</small>
1384+
</h3>
1385+
<ul class="changelog">
1386+
<li>Fixed frontend part of inlines.</li>
1387+
<li>Added wysiwig editor and RichTextArea widget type.</li>
1388+
<li>
1389+
Added form_fields_widgets parameter to configure widgets fr
1390+
fields easily.
1391+
</li>
1392+
<li>Bug fixes.</li>
1393+
</ul>
1394+
<hr class="small-divider" />
1395+
1396+
<h3 id="v0_1_29">
1397+
Version 0.1.29
1398+
<small class="text-muted">(26 March, 2024)</small>
1399+
</h3>
1400+
<ul class="changelog">
1401+
<li>Bug fixes.</li>
1402+
</ul>
1403+
<hr class="small-divider" />
1404+
1405+
<h3 id="v0_1_28">
1406+
Version 0.1.28
1407+
<small class="text-muted">(26 March, 2024)</small>
1408+
</h3>
1409+
<ul class="changelog">
1410+
<li>Bug fixes.</li>
1411+
</ul>
1412+
<hr class="small-divider" />
13701413

13711414
<h3 id="v0_1_27">
13721415
Version 0.1.27
@@ -1383,7 +1426,9 @@ <h3 id="v0_1_26">
13831426
</h3>
13841427
<ul class="changelog">
13851428
<li>Fixed bugs.</li>
1386-
<li>Added RichTextArea WidgetType based on quill wysiwig editor.</li>
1429+
<li>
1430+
Added RichTextArea WidgetType based on quill wysiwig editor.
1431+
</li>
13871432
</ul>
13881433
<hr class="small-divider" />
13891434

fastadmin/api/frameworks/django/app/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,15 @@ async def export(request: HttpRequest, model: str) -> JsonResponse:
254254
sort_by = filters.get("sort_by", None)
255255
try:
256256
payload = ExportInputSchema(**json.loads(request.body))
257-
file_name, stream = await api_service.export(
257+
file_name, content_type, stream = await api_service.export(
258258
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
259259
model,
260260
payload,
261261
search=search,
262262
sort_by=sort_by,
263263
filters=filters,
264264
)
265-
response = StreamingHttpResponse(stream, content_type="text/csv")
265+
response = StreamingHttpResponse(stream, content_type=content_type)
266266
response.headers["Content-Disposition"] = f'attachment; filename="{file_name}"'
267267
return response
268268

fastadmin/api/frameworks/fastapi/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async def export(
203203
:return: A stream of export data.
204204
"""
205205
try:
206-
file_name, stream = await api_service.export(
206+
file_name, content_type, stream = await api_service.export(
207207
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
208208
model,
209209
payload,
@@ -215,7 +215,7 @@ async def export(
215215
return StreamingResponse(
216216
stream, # type: ignore
217217
headers=headers,
218-
media_type="text/csv",
218+
media_type=content_type,
219219
)
220220
except AdminApiException as e:
221221
raise HTTPException(e.status_code, detail=e.detail)

fastadmin/api/frameworks/flask/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,15 @@ async def export(model: str) -> Response:
207207
try:
208208
request_payload: dict = request.json
209209
payload: ExportInputSchema = ExportInputSchema(**request_payload)
210-
file_name, stream = await api_service.export(
210+
file_name, content_type, stream = await api_service.export(
211211
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
212212
model,
213213
payload,
214214
search=search,
215215
sort_by=sort_by,
216216
filters=filters,
217217
)
218-
response = Response(stream, mimetype="text/csv")
218+
response = Response(stream, mimetype=content_type)
219219
response.headers["Content-Disposition"] = f'attachment; filename="{file_name}"'
220220
return response
221221
except AdminApiException as e:

fastadmin/api/schemas.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class ExportFormat(str, Enum):
88
"""Export format"""
99

1010
CSV = "CSV"
11+
JSON = "JSON"
1112

1213

1314
class ListQuerySchema(BaseModel):
@@ -30,9 +31,9 @@ class SignInInputSchema(BaseModel):
3031
class ExportInputSchema(BaseModel):
3132
"""Export input schema"""
3233

33-
format: ExportFormat | None
34-
limit: int | None
35-
offset: int | None
34+
format: ExportFormat | None = ExportFormat.CSV
35+
limit: int | None = 1000
36+
offset: int | None = 0
3637

3738

3839
class ActionInputSchema(BaseModel):

fastadmin/api/service.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from fastadmin.api.exceptions import AdminApiException
1212
from fastadmin.api.helpers import get_user_id_from_session_id, sanitize
13-
from fastadmin.api.schemas import ActionInputSchema, ExportInputSchema, SignInInputSchema
13+
from fastadmin.api.schemas import ActionInputSchema, ExportFormat, ExportInputSchema, SignInInputSchema
1414
from fastadmin.models.base import InlineModelAdmin, ModelAdmin
1515
from fastadmin.models.helpers import (
1616
generate_models_schema,
@@ -181,7 +181,7 @@ async def export(
181181
search: str | None = None,
182182
sort_by: str | None = None,
183183
filters: dict = {},
184-
) -> tuple[str, StringIO | BytesIO | None]:
184+
) -> tuple[str, str, StringIO | BytesIO | None]:
185185
current_user_id = await get_user_id_from_session_id(session_id)
186186
if not current_user_id:
187187
raise AdminApiException(401, detail="User is not authenticated.")
@@ -191,14 +191,26 @@ async def export(
191191
raise AdminApiException(404, detail=f"{model} model is not registered.")
192192

193193
filters = {k: sanitize(v) for k, v in filters.items() if k not in ("search", "sort_by", "offset", "limit")}
194-
file_name = f"{model}.{(payload.format or 'csv').lower()}"
195-
return file_name, await admin_model.get_export(
196-
payload.format,
197-
search=search,
198-
sort_by=sort_by,
199-
filters=filters,
200-
offset=payload.offset,
201-
limit=payload.limit,
194+
195+
content_type = "text/plain"
196+
file_name = f"{model}.txt"
197+
if payload.format == ExportFormat.CSV:
198+
content_type = "text/csv"
199+
file_name = f"{model}.csv"
200+
elif payload.format == ExportFormat.JSON:
201+
content_type = "text/plain"
202+
file_name = f"{model}.json"
203+
return (
204+
file_name,
205+
content_type,
206+
await admin_model.get_export(
207+
payload.format,
208+
search=search,
209+
sort_by=sort_by,
210+
filters=filters,
211+
offset=payload.offset,
212+
limit=payload.limit,
213+
),
202214
)
203215

204216
async def delete(

fastadmin/models/base.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import csv
22
import inspect
3+
import json
34
from collections.abc import Sequence
45
from io import BytesIO, StringIO
56
from typing import Any
@@ -434,17 +435,27 @@ async def get_export(
434435
fields = self.get_model_fields_with_widget_types(with_m2m=False)
435436

436437
export_fields = [f.name for f in fields]
438+
output = StringIO()
437439
if not export_format or export_format == ExportFormat.CSV:
438-
output = StringIO()
439440
writer = csv.DictWriter(output, fieldnames=export_fields)
440441
writer.writeheader()
441442
for obj in objs:
442443
obj_dict = await self.serialize_obj(obj, list_view=True)
443444
obj_dict = {k: v for k, v in obj_dict.items() if k in export_fields}
444445
writer.writerow(obj_dict)
445-
output.seek(0)
446-
return output
447-
return None
446+
if not export_format or export_format == ExportFormat.JSON:
447+
448+
class JSONEncoder(json.JSONEncoder):
449+
def default(self, obj):
450+
try:
451+
return super().default(obj)
452+
except TypeError:
453+
return str(obj)
454+
455+
json.dump([await self.serialize_obj(obj, list_view=True) for obj in objs], output, cls=JSONEncoder)
456+
457+
output.seek(0)
458+
return output
448459

449460
def has_add_permission(self, user_id: UUID | int | None = None) -> bool:
450461
"""This method is used to check if user has permission to add new model instance.

fastadmin/models/helpers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ def generate_models_schema(
243243
)
244244
else:
245245
admin_model_obj = cast(InlineModelAdmin, admin_model_obj)
246+
# TODO: replace parent on dynamic identification of fk name in inline model
247+
fk_name = admin_model_obj.fk_name or "parent"
246248
models_schemas.append(
247249
InlineModelSchema(
248250
name=model_name,
@@ -259,7 +261,7 @@ def generate_models_schema(
259261
list_max_show_all=admin_model_obj.list_max_show_all,
260262
show_full_result_count=admin_model_obj.show_full_result_count,
261263
# specific inline model fields
262-
fk_name=admin_model_obj.fk_name,
264+
fk_name=fk_name,
263265
max_num=admin_model_obj.max_num,
264266
min_num=admin_model_obj.min_num,
265267
verbose_name=admin_model_obj.verbose_name,

fastadmin/models/orms/django.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ def get_model_fields_with_widget_types(
5454
is_pk or getattr(orm_model_field, "auto_now", False) or getattr(orm_model_field, "auto_now_add", False)
5555
) and field_name not in self.readonly_fields
5656

57-
required = (
58-
not getattr(orm_model_field, "null", False)
59-
and not getattr(orm_model_field, "default", False)
60-
and not is_m2m
61-
)
57+
has_default = getattr(orm_model_field, "default", False)
58+
if hasattr(has_default, "__name__") and has_default.__name__ == "NOT_PROVIDED":
59+
has_default = False
60+
61+
required = not getattr(orm_model_field, "null", False) and not has_default and not is_m2m
6262
choices = (
6363
{item[0]: item[1] for item in orm_model_field.choices}
6464
if getattr(orm_model_field, "choices", None)
@@ -97,6 +97,7 @@ def get_model_fields_with_widget_types(
9797
filter_widget_type = WidgetType.TextArea
9898
case "BooleanField":
9999
form_widget_type = WidgetType.Switch
100+
form_widget_props["required"] = False
100101
filter_widget_type = WidgetType.RadioGroup
101102
filter_widget_props["options"] = [
102103
{"label": "Yes", "value": True},

fastadmin/models/orms/ponyorm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def get_model_fields_with_widget_types(
8585
filter_widget_type = WidgetType.TextArea
8686
case "bool":
8787
form_widget_type = WidgetType.Switch
88+
form_widget_props["required"] = False
8889
filter_widget_type = WidgetType.RadioGroup
8990
filter_widget_props["options"] = [
9091
{"label": "Yes", "value": True},

0 commit comments

Comments
 (0)