mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2025-08-14 18:58:13 +08:00
update
This commit is contained in:
@ -41,6 +41,8 @@ Or pro version online demo [here](https://fastapi-admin-pro.long2ice.cn/admin/lo
|
||||
|
||||

|
||||
|
||||
## Run examples in local
|
||||
|
||||
## Documentation
|
||||
|
||||
See documentation at [https://fastapi-admin.github.io/fastapi-admin](https://fastapi-admin.github.io/fastapi-admin).
|
||||
|
@ -11,10 +11,10 @@ from tortoise.contrib.fastapi import register_tortoise
|
||||
from examples import settings
|
||||
from examples.constants import BASE_DIR
|
||||
from examples.models import Admin
|
||||
from examples.providers import LoginProvider
|
||||
from fastapi_admin.app import app as admin_app
|
||||
from fastapi_admin.providers.login import UsernamePasswordProvider
|
||||
|
||||
login_provider = UsernamePasswordProvider(admin_model=Admin)
|
||||
login_provider = LoginProvider(admin_model=Admin)
|
||||
|
||||
|
||||
def create_app():
|
||||
|
7
examples/providers.py
Normal file
7
examples/providers.py
Normal file
@ -0,0 +1,7 @@
|
||||
from fastapi_admin.models import AbstractAdmin
|
||||
from fastapi_admin.providers.login import UsernamePasswordProvider
|
||||
|
||||
|
||||
class LoginProvider(UsernamePasswordProvider):
|
||||
async def update_password(self, admin: AbstractAdmin, password: str):
|
||||
pass
|
@ -1,11 +1,12 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from examples import enums
|
||||
from examples.constants import BASE_DIR
|
||||
from examples.models import Admin, Category, Config, Product
|
||||
from fastapi_admin.app import app
|
||||
from fastapi_admin.providers.file_upload import FileUploadProvider
|
||||
from fastapi_admin.resources import Dropdown, Field, Link, Model
|
||||
from fastapi_admin.resources import Action, Dropdown, Field, Link, Model
|
||||
from fastapi_admin.widgets import displays, filters, inputs
|
||||
|
||||
upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))
|
||||
@ -14,7 +15,7 @@ upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static"
|
||||
@app.register
|
||||
class Home(Link):
|
||||
label = "Home"
|
||||
icon = "ti ti-home"
|
||||
icon = "fas fa-home"
|
||||
url = "/admin"
|
||||
|
||||
|
||||
@ -22,7 +23,7 @@ class Home(Link):
|
||||
class AdminResource(Model):
|
||||
label = "Admin"
|
||||
model = Admin
|
||||
icon = "ti ti-user"
|
||||
icon = "fas fa-user"
|
||||
page_pre_title = "admin list"
|
||||
page_title = "admin model"
|
||||
filters = [
|
||||
@ -49,6 +50,13 @@ class AdminResource(Model):
|
||||
),
|
||||
"created_at",
|
||||
]
|
||||
can_create = False
|
||||
|
||||
def get_actions(self) -> List[Action]:
|
||||
return []
|
||||
|
||||
def get_bulk_actions(self) -> List[Action]:
|
||||
return []
|
||||
|
||||
|
||||
@app.register
|
||||
@ -78,7 +86,7 @@ class Content(Dropdown):
|
||||
]
|
||||
|
||||
label = "Content"
|
||||
icon = "ti ti-package"
|
||||
icon = "fas fa-bars"
|
||||
resources = [ProductResource, CategoryResource]
|
||||
|
||||
|
||||
@ -86,7 +94,7 @@ class Content(Dropdown):
|
||||
class ConfigResource(Model):
|
||||
label = "Config"
|
||||
model = Config
|
||||
icon = "ti ti-settings"
|
||||
icon = "fas fa-cogs"
|
||||
filters = [
|
||||
filters.Enum(enum=enums.Status, name="status", label="Status"),
|
||||
filters.Search(name="key", label="Key", search_mode="equal"),
|
||||
@ -107,8 +115,8 @@ class ConfigResource(Model):
|
||||
@app.register
|
||||
class GithubLink(Link):
|
||||
label = "Github"
|
||||
url = "https://github.com/long2ice"
|
||||
icon = "ti ti-brand-github"
|
||||
url = "https://github.com/fastapi-admin/fastapi-admin"
|
||||
icon = "fab fa-github"
|
||||
target = "_blank"
|
||||
|
||||
|
||||
@ -116,5 +124,5 @@ class GithubLink(Link):
|
||||
class DocumentationLink(Link):
|
||||
label = "Documentation"
|
||||
url = "https://long2ice.github.io/fastadmin"
|
||||
icon = "ti ti-file-text"
|
||||
icon = "fas fa-file-code"
|
||||
target = "_blank"
|
||||
|
37
examples/templates/password.html
Normal file
37
examples/templates/password.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block page_body %}
|
||||
{% include "components/alert_error.html" %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ _('update_password') }} (This will not take effect because is overwrite in examples)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ request.app.admin_path }}/password">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">
|
||||
{{ _('old_password') }}
|
||||
</label>
|
||||
<input class="form-control" type="password" name="old_password"
|
||||
placeholder="{{ _('old_password_placeholder') }}"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">
|
||||
{{ _('new_password') }}
|
||||
</label>
|
||||
<input class="form-control" type="password" name="new_password"
|
||||
placeholder="{{ _('new_password_placeholder') }}"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">
|
||||
{{ _('re_new_password') }}
|
||||
</label>
|
||||
<input class="form-control" type="password" name="re_new_password"
|
||||
placeholder="{{ _('re_new_password_placeholder') }}"/>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary">{{ _('submit') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -21,7 +21,7 @@ class FastAdmin(FastAPI):
|
||||
admin_path: str
|
||||
resources: List[Type[Resource]] = []
|
||||
model_resources: Dict[Type[Model], Type[Resource]] = {}
|
||||
login_provider: Optional[Type[LoginProvider]] = LoginProvider
|
||||
login_provider: Optional[LoginProvider]
|
||||
redis: Redis
|
||||
|
||||
def configure(
|
||||
@ -77,7 +77,7 @@ class FastAdmin(FastAPI):
|
||||
self.resources.append(resource)
|
||||
|
||||
def get_model_resource(self, model: Type[Model]):
|
||||
return self.model_resources[model]
|
||||
return self.model_resources[model]()
|
||||
|
||||
|
||||
app = FastAdmin(
|
||||
|
@ -19,7 +19,12 @@ def get_model(resource: Optional[str] = Path(...)):
|
||||
|
||||
|
||||
def get_model_resource(request: Request, model=Depends(get_model)):
|
||||
return request.app.get_model_resource(model)
|
||||
model_resource = request.app.get_model_resource(model) # type:Model
|
||||
actions = model_resource.get_actions()
|
||||
bulk_actions = model_resource.get_bulk_actions()
|
||||
model_resource.actions = actions
|
||||
model_resource.bulk_actions = bulk_actions
|
||||
return model_resource
|
||||
|
||||
|
||||
def _get_resources(resources: List[Type[Resource]]):
|
||||
|
@ -2,7 +2,7 @@ from typing import Callable
|
||||
|
||||
from starlette.requests import Request
|
||||
|
||||
from fastapi_admin import i18n, template
|
||||
from fastapi_admin import i18n
|
||||
|
||||
|
||||
async def language_processor(request: Request, call_next: Callable):
|
||||
|
@ -1,14 +1,14 @@
|
||||
from typing import List, Optional, Type, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from tortoise import ForeignKeyFieldInstance
|
||||
from starlette.datastructures import FormData
|
||||
from tortoise import ForeignKeyFieldInstance, ManyToManyFieldInstance
|
||||
from tortoise import Model as TortoiseModel
|
||||
from tortoise.fields import BooleanField, DateField, DatetimeField, JSONField
|
||||
from tortoise.fields.data import CharEnumFieldInstance, IntEnumFieldInstance, IntField, TextField
|
||||
|
||||
from fastapi_admin.exceptions import NoSuchFieldFound
|
||||
from fastapi_admin.i18n import _
|
||||
from fastapi_admin.utils import ClassProperty
|
||||
from fastapi_admin.widgets import Widget, displays, inputs
|
||||
from fastapi_admin.widgets.filters import Filter
|
||||
|
||||
@ -67,20 +67,16 @@ class Model(Resource):
|
||||
page_pre_title: Optional[str] = None
|
||||
page_title: Optional[str] = None
|
||||
filters: Optional[List[Union[str, Filter]]] = []
|
||||
can_edit: bool = True
|
||||
can_delete: bool = True
|
||||
can_create: bool = True
|
||||
enctype = "application/x-www-form-urlencoded"
|
||||
|
||||
@ClassProperty
|
||||
def actions(self) -> List[Action]:
|
||||
def get_actions(self) -> List[Action]:
|
||||
return [
|
||||
Action(label=_("update"), icon="ti ti-edit", name="update", ajax=False),
|
||||
Action(label=_("delete"), icon="ti ti-trash", name="delete", method="DELETE"),
|
||||
]
|
||||
|
||||
@ClassProperty
|
||||
def bulk_actions(self) -> List[Action]:
|
||||
def get_bulk_actions(self) -> List[Action]:
|
||||
return [
|
||||
Action(label=_("delete_selected"), icon="ti ti-trash", name="delete", method="DELETE"),
|
||||
]
|
||||
@ -109,16 +105,23 @@ class Model(Resource):
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
async def resolve_data(cls, data: dict):
|
||||
async def resolve_data(cls, data: FormData):
|
||||
ret = {}
|
||||
m2m_ret = {}
|
||||
for field in cls.get_fields(is_display=False):
|
||||
input_ = field.input
|
||||
if input_.context.get("disabled") or isinstance(input_, inputs.DisplayOnly):
|
||||
continue
|
||||
name = input_.context.get("name")
|
||||
if isinstance(input_, inputs.ManyToMany):
|
||||
v = data.getlist(name)
|
||||
value = await input_.parse_value(v)
|
||||
m2m_ret[name] = await input_.model.filter(pk__in=value)
|
||||
else:
|
||||
v = data.get(name)
|
||||
ret[name] = await input_.parse_value(v)
|
||||
return ret
|
||||
value = await input_.parse_value(v)
|
||||
ret[name] = value
|
||||
return ret, m2m_ret
|
||||
|
||||
@classmethod
|
||||
async def get_filters(cls, values: Optional[dict] = None):
|
||||
@ -191,7 +194,8 @@ class Model(Resource):
|
||||
field.related_model, null=null, default=field.default
|
||||
)
|
||||
field_name = field.source_field
|
||||
|
||||
elif isinstance(field, ManyToManyFieldInstance):
|
||||
display, input_ = displays.InputOnly(), inputs.ManyToMany(field.related_model)
|
||||
return Field(name=field_name, label=label.title(), display=display, input_=input_)
|
||||
|
||||
@classmethod
|
||||
@ -213,6 +217,14 @@ class Model(Resource):
|
||||
def get_fields_label(cls, display: bool = True):
|
||||
return cls._get_fields_attr("label", display)
|
||||
|
||||
@classmethod
|
||||
def get_m2m_field(cls):
|
||||
ret = []
|
||||
for field in cls.fields or cls.model._meta.fields:
|
||||
if field in cls.model._meta.m2m_fields:
|
||||
ret.append(field)
|
||||
return ret
|
||||
|
||||
|
||||
class Dropdown(Resource):
|
||||
resources: List[Type[Resource]]
|
||||
|
@ -1,5 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, Form
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from fastapi_admin.depends import get_current_admin, get_resources
|
||||
|
@ -1,10 +1,11 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from jinja2 import TemplateNotFound
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse
|
||||
from starlette.status import HTTP_303_SEE_OTHER
|
||||
from tortoise import Model
|
||||
from tortoise.fields import ManyToManyRelation
|
||||
from tortoise.transactions import in_transaction
|
||||
|
||||
from fastapi_admin.depends import get_model, get_model_resource, get_resources
|
||||
from fastapi_admin.resources import Model as ModelResource
|
||||
@ -38,8 +39,6 @@ async def list_view(
|
||||
qs = qs.offset((page_num - 1) * page_size)
|
||||
values = await qs.values_list(*fields_name)
|
||||
values = await render_values(fields, values)
|
||||
return templates.TemplateResponse(
|
||||
"list.html",
|
||||
context = {
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
@ -57,7 +56,16 @@ async def list_view(
|
||||
"to": page_size * page_num,
|
||||
"page_title": model_resource.page_title,
|
||||
"page_pre_title": model_resource.page_pre_title,
|
||||
},
|
||||
}
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
f"{resource}/list.html",
|
||||
context=context,
|
||||
)
|
||||
except TemplateNotFound:
|
||||
return templates.TemplateResponse(
|
||||
"list.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
@ -71,13 +79,29 @@ async def update(
|
||||
model=Depends(get_model),
|
||||
):
|
||||
form = await request.form()
|
||||
data = await model_resource.resolve_data(dict(form))
|
||||
obj = await model.get(pk=pk)
|
||||
await obj.update_from_dict(data).save()
|
||||
data, m2m_data = await model_resource.resolve_data(form)
|
||||
async with in_transaction() as conn:
|
||||
obj = (
|
||||
await model.filter(pk=pk)
|
||||
.using_db(conn)
|
||||
.select_for_update()
|
||||
.get()
|
||||
.prefetch_related(*model_resource.get_m2m_field())
|
||||
)
|
||||
await obj.update_from_dict(data).save(using_db=conn)
|
||||
for k, items in m2m_data.items():
|
||||
m2m_obj = getattr(obj, k)
|
||||
await m2m_obj.clear()
|
||||
if items:
|
||||
await m2m_obj.add(*items)
|
||||
obj = (
|
||||
await model.filter(pk=pk)
|
||||
.using_db(conn)
|
||||
.get()
|
||||
.prefetch_related(*model_resource.get_m2m_field())
|
||||
)
|
||||
inputs = await model_resource.get_inputs(obj)
|
||||
if "save" in form.keys():
|
||||
return templates.TemplateResponse(
|
||||
"update.html",
|
||||
context = {
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
@ -88,7 +112,16 @@ async def update(
|
||||
"pk": pk,
|
||||
"page_title": model_resource.page_title,
|
||||
"page_pre_title": model_resource.page_pre_title,
|
||||
},
|
||||
}
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
f"{resource}/update.html",
|
||||
context=context,
|
||||
)
|
||||
except TemplateNotFound:
|
||||
return templates.TemplateResponse(
|
||||
"update.html",
|
||||
context=context,
|
||||
)
|
||||
return redirect(request, "list_view", resource=resource)
|
||||
|
||||
@ -104,8 +137,6 @@ async def update_view(
|
||||
):
|
||||
obj = await model.get(pk=pk)
|
||||
inputs = await model_resource.get_inputs(obj)
|
||||
return templates.TemplateResponse(
|
||||
"update.html",
|
||||
context = {
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
@ -116,7 +147,16 @@ async def update_view(
|
||||
"model_resource": model_resource,
|
||||
"page_title": model_resource.page_title,
|
||||
"page_pre_title": model_resource.page_pre_title,
|
||||
},
|
||||
}
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
f"{resource}/update.html",
|
||||
context=context,
|
||||
)
|
||||
except TemplateNotFound:
|
||||
return templates.TemplateResponse(
|
||||
"update.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
@ -128,8 +168,6 @@ async def create_view(
|
||||
model_resource: ModelResource = Depends(get_model_resource),
|
||||
):
|
||||
inputs = await model_resource.get_inputs()
|
||||
return templates.TemplateResponse(
|
||||
"create.html",
|
||||
context = {
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
@ -139,7 +177,16 @@ async def create_view(
|
||||
"model_resource": model_resource,
|
||||
"page_title": model_resource.page_title,
|
||||
"page_pre_title": model_resource.page_pre_title,
|
||||
},
|
||||
}
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
f"{resource}/create.html",
|
||||
context=context,
|
||||
)
|
||||
except TemplateNotFound:
|
||||
return templates.TemplateResponse(
|
||||
"create.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
@ -153,12 +200,14 @@ async def create(
|
||||
):
|
||||
inputs = await model_resource.get_inputs()
|
||||
form = await request.form()
|
||||
data = await model_resource.resolve_data(dict(form))
|
||||
await model.create(**data)
|
||||
data, m2m_data = await model_resource.resolve_data(form)
|
||||
async with in_transaction() as conn:
|
||||
obj = await model.create(**data, using_db=conn)
|
||||
for k, items in m2m_data.items():
|
||||
m2m_obj = getattr(obj, k) # type:ManyToManyRelation
|
||||
await m2m_obj.add(*items, using_db=conn)
|
||||
if "save" in form.keys():
|
||||
return redirect(request, "list_view", resource=resource)
|
||||
return templates.TemplateResponse(
|
||||
"create.html",
|
||||
context = {
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
@ -168,7 +217,16 @@ async def create(
|
||||
"model_resource": model_resource,
|
||||
"page_title": model_resource.page_title,
|
||||
"page_pre_title": model_resource.page_pre_title,
|
||||
},
|
||||
}
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
f"{resource}/create.html",
|
||||
context=context,
|
||||
)
|
||||
except TemplateNotFound:
|
||||
return templates.TemplateResponse(
|
||||
"create.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@ def set_global_env(name: str, value: Any):
|
||||
|
||||
def add_template_folder(*folders: str):
|
||||
for folder in folders:
|
||||
templates.env.loader.searchpath.append(folder)
|
||||
templates.env.loader.searchpath.insert(0, folder)
|
||||
|
||||
|
||||
async def render_values(
|
||||
|
@ -4,10 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@latest/dist/css/tabler.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons@latest/iconfont/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@latest/dist/css/tabler-flags.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@latest/dist/css/tabler-payments.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@latest/dist/css/tabler-vendors.min.css">
|
||||
<script src="https://kit.fontawesome.com/65694932fa.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.js"></script>
|
||||
<script src="https://unpkg.com/@tabler/core@latest/dist/js/tabler.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
|
||||
|
@ -116,7 +116,8 @@
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#" class="link-secondary">License</a>
|
||||
<a href="https://github.com/fastapi-admin/fastapi-admin/blob/dev/LICENSE"
|
||||
class="link-secondary">License</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
@ -160,8 +161,8 @@
|
||||
<ul class="list-inline list-inline-dots mb-0">
|
||||
<li class="list-inline-item">
|
||||
Copyright © {{ 2021 }} - {{ NOW_YEAR }}
|
||||
<a href="http://github.com/fastapi-admin" class="link-secondary"
|
||||
>FastAPI-Admin</a
|
||||
<a href="http://github.com/long2ice" class="link-secondary"
|
||||
>long2ice</a
|
||||
>. All rights reserved.
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
|
@ -16,7 +16,7 @@
|
||||
type="text"
|
||||
class="form-control"
|
||||
value="{{ page_size }}"
|
||||
size="1"
|
||||
size="2"
|
||||
name="page_size"
|
||||
aria-label="entries count"
|
||||
/>
|
||||
@ -89,6 +89,7 @@
|
||||
<table class="table card-table table-vcenter text-nowrap datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if model_resource.bulk_actions %}
|
||||
<th class="w-1">
|
||||
<input
|
||||
class="form-check-input m-0 align-middle"
|
||||
@ -96,15 +97,19 @@
|
||||
id="checkbox-select-all"
|
||||
/>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% for label in fields_label %}
|
||||
<th>{{ label }}</th>
|
||||
{% endfor %}
|
||||
{% if model_resource.actions %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for value in values %}
|
||||
<tr>
|
||||
{% if model_resource.bulk_actions %}
|
||||
<td>
|
||||
<input
|
||||
data-id="{{ value[0]|int }}"
|
||||
@ -112,6 +117,7 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% for v in value %}
|
||||
<td>{{ v|safe }}</td>
|
||||
{% endfor %}
|
||||
|
21
fastapi_admin/templates/widgets/inputs/many_to_many.html
Normal file
21
fastapi_admin/templates/widgets/inputs/many_to_many.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% with id = 'form-select-' + name %}
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-label">{{ label }}</div>
|
||||
<select multiple class="form-select" name="{{ name }}" id="{{ id }}">
|
||||
</select>
|
||||
</div>
|
||||
<script>
|
||||
new Choices(el = document.getElementById('{{ id }}'), {
|
||||
classNames: {
|
||||
containerInner: el.className,
|
||||
listDropdown: 'dropdown-menu',
|
||||
itemChoice: 'dropdown-item',
|
||||
activeState: 'show',
|
||||
selectedState: 'active',
|
||||
inputCloned: 'bg-white',
|
||||
},
|
||||
removeItemButton: true,
|
||||
choices:{{ options|safe }}
|
||||
});
|
||||
</script>
|
||||
{% endwith %}
|
@ -1,9 +0,0 @@
|
||||
class ClassProperty(property):
|
||||
def __get__(self, obj, obj_type=None):
|
||||
return super(ClassProperty, self).__get__(obj_type)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
super(ClassProperty, self).__set__(type(obj), value)
|
||||
|
||||
def __delete__(self, obj):
|
||||
super(ClassProperty, self).__delete__(type(obj))
|
@ -50,5 +50,5 @@ class Json(Display):
|
||||
|
||||
async def render(self, value: dict):
|
||||
return self.templates.get_template(self.template).render(
|
||||
value=json.dumps(value, indent=4, sort_keys=True),
|
||||
value=json.dumps(value),
|
||||
)
|
||||
|
@ -96,6 +96,35 @@ class ForeignKey(Select):
|
||||
return await self.model.all()
|
||||
|
||||
|
||||
class ManyToMany(Select):
|
||||
template = "widgets/inputs/many_to_many.html"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: Type[Model],
|
||||
disabled: bool = False,
|
||||
):
|
||||
super().__init__(disabled=disabled)
|
||||
self.model = model
|
||||
|
||||
async def get_options(self):
|
||||
ret = await self.get_queryset()
|
||||
options = [dict(label=str(x), value=x.pk) for x in ret]
|
||||
return options
|
||||
|
||||
async def get_queryset(self):
|
||||
return await self.model.all()
|
||||
|
||||
async def render(self, value: Any):
|
||||
options = await self.get_options()
|
||||
selected = list(map(lambda x: x.pk, value.related_objects if value else []))
|
||||
for option in options:
|
||||
if option.get("value") in selected:
|
||||
option["selected"] = True
|
||||
self.context.update(options=json.dumps(options))
|
||||
return await super(Input, self).render(value)
|
||||
|
||||
|
||||
class Enum(Select):
|
||||
def __init__(
|
||||
self,
|
||||
|
26
poetry.lock
generated
26
poetry.lock
generated
@ -428,7 +428,7 @@ tornado = ">=5.0"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-git-revision-date-localized-plugin"
|
||||
version = "0.9"
|
||||
version = "0.9.2"
|
||||
description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -593,7 +593,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.8.1"
|
||||
version = "2.9.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -722,7 +722,7 @@ testing = ["filelock"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -853,7 +853,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
version = "3.10.0.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1249,8 +1249,8 @@ mkdocs = [
|
||||
{file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
|
||||
]
|
||||
mkdocs-git-revision-date-localized-plugin = [
|
||||
{file = "mkdocs-git-revision-date-localized-plugin-0.9.tar.gz", hash = "sha256:49e59396f1e83264b8f54fcb339a9137925a1af19e639f03dc59dae7f22e914f"},
|
||||
{file = "mkdocs_git_revision_date_localized_plugin-0.9-py3-none-any.whl", hash = "sha256:5d319398e9ce325d02df1cba232a92b215f8d0c0ffd3810b6a61d6c5eb6306e6"},
|
||||
{file = "mkdocs-git-revision-date-localized-plugin-0.9.2.tar.gz", hash = "sha256:c15c76d5baa1f8f37e3a4146b9a6f1b00c5a361ea959ada0703fd9ec462afbe3"},
|
||||
{file = "mkdocs_git_revision_date_localized_plugin-0.9.2-py3-none-any.whl", hash = "sha256:d4b21eeb9f212efd314a05e771cf69f2bab1f51bdaa5c9955d09410a396a069b"},
|
||||
]
|
||||
mkdocs-material = [
|
||||
{file = "mkdocs-material-7.1.3.tar.gz", hash = "sha256:e34bba93ad1a0e6f9afc371f4ef55bedabbf13b9a786b013b0ce26ac55ec2932"},
|
||||
@ -1345,8 +1345,8 @@ pyflakes = [
|
||||
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
|
||||
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
|
||||
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
|
||||
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
|
||||
]
|
||||
pylint = [
|
||||
{file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"},
|
||||
@ -1385,8 +1385,8 @@ pytest-xdist = [
|
||||
{file = "pytest_xdist-2.2.1-py3-none-any.whl", hash = "sha256:2447a1592ab41745955fb870ac7023026f20a5f0bfccf1b52a879bd193d46450"},
|
||||
]
|
||||
python-dotenv = [
|
||||
{file = "python-dotenv-0.17.0.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"},
|
||||
{file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"},
|
||||
{file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"},
|
||||
{file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"},
|
||||
]
|
||||
python-multipart = [
|
||||
{file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
|
||||
@ -1569,9 +1569,9 @@ typed-ast = [
|
||||
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
|
||||
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
|
||||
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
|
||||
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
|
||||
]
|
||||
uvicorn = [
|
||||
{file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"},
|
||||
|
Reference in New Issue
Block a user