Skip to content

Commit ae554de

Browse files
authored
Merge pull request #17 from jowilf/odmantic-example
Add null support for EnumField & Add odmantic example
2 parents 8fb42bb + 5f3edc8 commit ae554de

File tree

15 files changed

+252
-26
lines changed

15 files changed

+252
-26
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.2] - 2022-09-20
9+
10+
---
11+
12+
### Fixed
13+
14+
* Null support for EnumField by @jowilf in https://github.com/jowilf/starlette-admin/pull/17
15+
16+
**Full Changelog**: https://github.com/jowilf/starlette-admin/compare/0.2.1...0.2.2
17+
18+
819
## [0.2.1] - 2022-09-19
920

1021
---

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</a>
2121
</p>
2222

23-
![Starlette-Admin Promo Image](docs/images/promo.png)
23+
![Starlette-Admin Promo Image](https://github.com/jowilf/starlette-admin/raw/main/docs/images/promo.png)
2424

2525
The key features are:
2626

docs/changelog.md

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.2] - 2022-09-20
9+
10+
---
11+
12+
### Fixed
13+
14+
* Null support for EnumField in [#17](https://github.com/jowilf/starlette-admin/pull/17)
15+
16+
817
## [0.2.1] - 2022-09-19
918

1019
---

examples/basic/app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class PostView(BaseModelView):
5858
sortable_fields = ("id", "title", "content")
5959
search_builder = False
6060
page_size = 10
61-
page_size_options = [2, -1]
61+
page_size_options = [5, 10, -1]
6262

6363
async def count(
6464
self,

examples/odmantic/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
This example shows how you can easily extend BaseModelView to use *Starlette-Admin*
2+
with [odmantic](https://github.com/art049/odmantic)
3+
4+
To run this example:
5+
6+
1. Clone the repository::
7+
8+
```shell
9+
git clone https://github.com/jowilf/starlette-admin.git
10+
cd starlette-admin
11+
```
12+
13+
2. Create and activate a virtual environment::
14+
15+
```shell
16+
python3 -m venv env
17+
source env/bin/activate
18+
```
19+
20+
3. Install requirements
21+
22+
```shell
23+
pip install -r 'examples/odmantic/requirements.txt'
24+
```
25+
26+
4. Run the application:
27+
28+
```shell
29+
uvicorn examples.odmantic.app:app
30+
```

examples/odmantic/__init__.py

Whitespace-only changes.

examples/odmantic/app.py

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import Any, Dict, List, Optional, Union
2+
3+
from bson import ObjectId
4+
from odmantic import AIOEngine, Field, Model
5+
from pydantic import ValidationError
6+
from starlette.applications import Starlette
7+
from starlette.requests import Request
8+
from starlette.responses import HTMLResponse
9+
from starlette.routing import Route
10+
from starlette_admin import BaseAdmin as Admin
11+
from starlette_admin import BaseModelView, EnumField, IntegerField, StringField
12+
from starlette_admin.exceptions import FormValidationError
13+
14+
from examples.odmantic.helpers import build_raw_query
15+
16+
engine = AIOEngine()
17+
app = Starlette(
18+
routes=[
19+
Route(
20+
"/",
21+
lambda r: HTMLResponse('<a href="/admin/">Click me to get to Admin!</a>'),
22+
)
23+
]
24+
)
25+
26+
27+
class Author(Model):
28+
name: str = Field(min_length=3, max_length=100)
29+
age: int = Field(ge=5, lt=150)
30+
sex: Optional[str]
31+
32+
33+
def build_query(where: Union[Dict[str, Any], str, None] = None) -> Any:
34+
if where is None:
35+
return {}
36+
if isinstance(where, dict):
37+
return build_raw_query(where) # from mongoengine integration
38+
else:
39+
return Author.name.match(where)
40+
41+
42+
def build_order_clauses(order_list: List[str]):
43+
clauses = []
44+
for value in order_list:
45+
key, order = value.strip().split(maxsplit=1)
46+
clause = getattr(Author, key)
47+
clauses.append(clause.desc() if order.lower() == "desc" else clause)
48+
return tuple(clauses) if len(clauses) > 0 else None
49+
50+
51+
def pydantic_error_to_form_validation_errors(exc: ValidationError):
52+
errors: Dict[str, str] = dict()
53+
for pydantic_error in exc.errors():
54+
errors[str(pydantic_error["loc"][-1])] = pydantic_error["msg"]
55+
return FormValidationError(errors)
56+
57+
58+
class AuthorView(BaseModelView):
59+
identity = "author"
60+
name = "Author"
61+
label = "Authors"
62+
pk_attr = "id"
63+
fields = [
64+
StringField("id"),
65+
StringField("name"),
66+
IntegerField("age"),
67+
EnumField.from_choices("sex", [("male", "MALE"), ("female", "FEMALE")]),
68+
]
69+
70+
async def count(
71+
self,
72+
request: Request,
73+
where: Union[Dict[str, Any], str, None] = None,
74+
) -> int:
75+
return await engine.count(Author, build_query(where))
76+
77+
async def find_all(
78+
self,
79+
request: Request,
80+
skip: int = 0,
81+
limit: int = 100,
82+
where: Union[Dict[str, Any], str, None] = None,
83+
order_by: Optional[List[str]] = None,
84+
) -> List[Any]:
85+
return await engine.find(
86+
Author,
87+
build_query(where),
88+
sort=build_order_clauses(order_by),
89+
skip=skip,
90+
limit=limit,
91+
)
92+
93+
async def find_by_pk(self, request: Request, pk: Any) -> Any:
94+
return await engine.find_one(Author, Author.id == ObjectId(pk))
95+
96+
async def find_by_pks(self, request: Request, pks: List[Any]) -> List[Any]:
97+
return await engine.find(Author.id.in_(pks))
98+
99+
async def create(self, request: Request, data: Dict) -> Any:
100+
try:
101+
return await engine.save(Author(**data))
102+
except ValidationError as exc:
103+
raise pydantic_error_to_form_validation_errors(exc)
104+
105+
async def edit(self, request: Request, pk: Any, data: Dict[str, Any]) -> Any:
106+
try:
107+
author = await self.find_by_pk(request, pk)
108+
author.update(data)
109+
return await engine.save(author)
110+
except ValidationError as exc:
111+
raise pydantic_error_to_form_validation_errors(exc)
112+
113+
114+
admin = Admin()
115+
admin.add_view(AuthorView)
116+
admin.mount_to(app)

examples/odmantic/helpers.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from typing import Any, Dict
2+
3+
comparison_map = {
4+
"eq": "$eq",
5+
"neq": "$ne",
6+
"ge": "$gte",
7+
"gt": "$gt",
8+
"le": "$lte",
9+
"lt": "$lt",
10+
"in": "$in",
11+
"not_in": "$nin",
12+
}
13+
14+
15+
def build_raw_query(dt_query: Dict[str, Any]) -> Dict[str, Any]:
16+
raw_query: Dict[str, Any] = dict()
17+
for key in dt_query:
18+
if key == "or":
19+
raw_query["$or"] = [build_raw_query(q) for q in dt_query[key]]
20+
elif key == "and":
21+
raw_query["$and"] = [build_raw_query(q) for q in dt_query[key]]
22+
elif key == "not":
23+
raw_query["$not"] = build_raw_query(dt_query[key])
24+
elif key == "between":
25+
values = dt_query[key]
26+
raw_query = {"$gte": values[0], "$lte": values[1]}
27+
elif key == "not_between":
28+
values = dt_query[key]
29+
raw_query = {"$not": {"$gte": values[0], "$lte": values[1]}}
30+
elif key == "contains":
31+
raw_query = {"$regex": dt_query[key], "$options": "mi"}
32+
elif key == "startsWith":
33+
raw_query = {"$regex": "^%s" % dt_query[key], "$options": "mi"}
34+
elif key == "endsWith":
35+
raw_query = {"$regex": "%s$" % dt_query[key], "$options": "mi"}
36+
elif key in comparison_map:
37+
raw_query[comparison_map[key]] = dt_query[key]
38+
else:
39+
raw_query[key] = build_raw_query(dt_query[key])
40+
return raw_query

examples/odmantic/requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
starlette-admin
2+
odmantic
3+
uvicorn

pyproject.toml

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "starlette-admin"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = "Simple and extensible admin interface framework for Starlette/FastApi"
55
authors = ["Jocelin Hounon <hounonj@gmail.com>"]
66
license = "MIT"
@@ -34,11 +34,11 @@ python-multipart = "*"
3434
[tool.poetry.dev-dependencies]
3535
pytest = "^7.1.3"
3636
sqlmodel = "^0.0.8"
37-
fastapi = "^0.82.0"
37+
fastapi = "^0.85.0"
3838
uvicorn = "^0.18.3"
3939
PyMySQL = { extras = ["rsa"], version = "^1.0.2" }
4040
Pillow = "^9.2.0"
41-
fasteners = "^0.17.3"
41+
fasteners = "^0.18"
4242
mongoengine = "^0.24.2"
4343
black = "^22.6.0"
4444
isort = "^5.10.1"
@@ -49,15 +49,15 @@ flake8 = "^3.9.2"
4949
async-asgi-testclient = "^1.4.11"
5050
pytest-asyncio = "^0.19.0"
5151
psycopg2-binary = "^2.9.3"
52-
mkdocs-material = "^8.4.3"
52+
mkdocs-material = "^8.5.2"
5353
mkdocstrings = { extras = ["python"], version = "^0.19.0" }
5454
itsdangerous = "^2.1.2"
55-
autoflake = "^1.5.3"
55+
autoflake = "^1.6.0"
5656
SQLAlchemy = { extras = ["asyncio"], version = "^1.4.41" }
5757
aiosqlite = "^0.17.0"
5858
asyncpg = "^0.26.0"
5959
aiomysql = "^0.1.1"
60-
beanie = "^1.11.9"
60+
odmantic = "^0.8.0"
6161

6262
[tool.coverage.report]
6363
fail_under = 99

starlette_admin/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.2.1"
1+
__version__ = "0.2.2"
22

33
from starlette_admin.base import BaseAdmin
44
from starlette_admin.fields import *

starlette_admin/base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ async def _render_login(self, request: Request) -> Response:
285285
try:
286286
assert self.auth_provider is not None
287287
return await self.auth_provider.login(
288-
form.get("username"),
289-
form.get("password"),
288+
form.get("username"), # type: ignore
289+
form.get("password"), # type: ignore
290290
form.get("remember_me") == "on",
291291
request,
292292
RedirectResponse(

0 commit comments

Comments
 (0)