Skip to content

Commit d86cb1c

Browse files
committed
Enhance examples
1 parent 0812185 commit d86cb1c

17 files changed

Lines changed: 1531 additions & 77 deletions

File tree

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ clean:
88
.PHONY: fix
99
fix:
1010
@echo "Run ruff"
11-
@exec poetry run ruff --fix fastadmin tests docs
11+
@exec poetry run ruff --fix fastadmin tests docs examples
1212
@echo "Run isort"
13-
@exec poetry run isort fastadmin tests docs
13+
@exec poetry run isort fastadmin tests docs examples
1414
@echo "Run black"
15-
@exec poetry run black fastadmin tests docs
15+
@exec poetry run black fastadmin tests docs examples
1616
@echo "Run mypy"
1717
@exec poetry run mypy -p fastadmin -p docs
1818
@echo "Run frontend linters"
@@ -21,11 +21,11 @@ fix:
2121
.PHONY: lint
2222
lint:
2323
@echo "Run ruff"
24-
@exec poetry run ruff fastadmin tests docs
24+
@exec poetry run ruff fastadmin tests docs examples
2525
@echo "Run isort"
26-
@exec poetry run isort --check-only fastadmin tests docs
26+
@exec poetry run isort --check-only fastadmin tests docs examples
2727
@echo "Run black"
28-
@exec poetry run black --check --diff fastadmin tests docs
28+
@exec poetry run black --check --diff fastadmin tests docs examples
2929
@echo "Run mypy"
3030
@exec poetry run mypy -p fastadmin -p docs
3131
@echo "Run frontend linters"

docs/build.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def read_cls_docstring(cls):
3939

4040
def get_versions():
4141
return [
42+
{
43+
"version": "0.2.8",
44+
"changes": [
45+
"Fix sqlalchemy delete functionality. Add more examples.",
46+
],
47+
},
4248
{
4349
"version": "0.2.7",
4450
"changes": [

docs/index.html

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
5+
os.environ["ADMIN_USER_MODEL"] = "User"
6+
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
7+
os.environ["ADMIN_SECRET_KEY"] = "secret"
8+
9+
sys.path.append(str(Path(__file__).resolve().parent))

examples/fastapi_sqlalchemy/example.py

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,86 @@
1-
from typing import List
2-
31
from fastapi import FastAPI
4-
from sqlalchemy import Boolean, Integer, String, select, ForeignKey
5-
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
6-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
2+
from models import Base, BaseEvent, Event, Tournament, User, sqlalchemy_engine, sqlalchemy_sessionmaker
3+
from sqlalchemy import select, update
74

8-
from fastadmin import SqlAlchemyModelAdmin, register, fastapi_app as admin_app
5+
from fastadmin import SqlAlchemyInlineModelAdmin, SqlAlchemyModelAdmin, action, display
6+
from fastadmin import fastapi_app as admin_app
7+
from fastadmin import register
98

109

11-
sqlalchemy_engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=True)
12-
sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False)
10+
@register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
11+
class UserModelAdmin(SqlAlchemyModelAdmin):
12+
exclude = ("password",)
13+
list_display = ("id", "username", "is_superuser")
14+
list_display_links = ("id", "username")
15+
list_filter = ("id", "username", "is_superuser")
16+
search_fields = ("username",)
1317

18+
async def authenticate(self, username, password):
19+
sessionmaker = self.get_sessionmaker()
20+
async with sessionmaker() as session:
21+
query = select(self.model_cls).filter_by(username=username, password=password, is_superuser=True)
22+
result = await session.scalars(query)
23+
obj = result.first()
24+
if not obj:
25+
return None
26+
return obj.id
27+
28+
async def change_password(self, user_id, password):
29+
sessionmaker = self.get_sessionmaker()
30+
async with sessionmaker() as session:
31+
# use hash password for real usage
32+
query = update(self.model_cls).where(User.id.in_([user_id])).values(password=password)
33+
await session.execute(query)
34+
await session.commit()
35+
36+
37+
class EventInlineModelAdmin(SqlAlchemyInlineModelAdmin):
38+
model = Event
1439

15-
class Base(DeclarativeBase):
16-
pass
1740

41+
@register(Tournament, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
42+
class TournamentModelAdmin(SqlAlchemyModelAdmin):
43+
list_display = ("id", "name")
44+
inlines = (EventInlineModelAdmin,)
1845

19-
class User(Base):
20-
__tablename__ = "user"
2146

22-
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
23-
username: Mapped[str] = mapped_column(String(length=255), nullable=False)
24-
password: Mapped[str] = mapped_column(String(length=255), nullable=False)
25-
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
26-
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
47+
@register(BaseEvent, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
48+
class BaseEventModelAdmin(SqlAlchemyModelAdmin):
49+
pass
2750

28-
transactions: Mapped[List["Transaction"]] = relationship(back_populates="user")
2951

30-
def __str__(self):
31-
return self.username
52+
@register(Event, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
53+
class EventModelAdmin(SqlAlchemyModelAdmin):
54+
actions = ("make_is_active", "make_is_not_active")
55+
list_display = ("id", "name_with_price", "rating", "event_type", "is_active", "started")
56+
raw_id_fields = ("base",)
3257

58+
@action(description="Make user active")
59+
async def make_is_active(self, ids):
60+
sessionmaker = self.get_sessionmaker()
61+
async with sessionmaker() as session:
62+
query = update(Event).where(Event.id.in_(ids)).values(is_active=True)
63+
await session.execute(query)
64+
await session.commit()
3365

34-
class Transaction(Base):
35-
__tablename__ = "transaction"
66+
@action
67+
async def make_is_not_active(self, ids):
68+
sessionmaker = self.get_sessionmaker()
69+
async with sessionmaker() as session:
70+
query = update(Event).where(Event.id.in_(ids)).values(is_active=False)
71+
await session.execute(query)
72+
await session.commit()
3673

37-
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
38-
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
74+
@display
75+
async def started(self, obj):
76+
return bool(obj.start_time)
3977

40-
user: Mapped["User"] = relationship(back_populates="transactions")
78+
@display()
79+
async def name_with_price(self, obj):
80+
return f"{obj.name} - {obj.price}"
81+
82+
83+
app = FastAPI()
4184

4285

4386
async def init_db():
@@ -52,43 +95,11 @@ async def create_superuser():
5295
username="admin",
5396
password="admin",
5497
is_superuser=True,
55-
is_active=True,
5698
)
5799
s.add(user)
58100
await s.commit()
59101

60102

61-
@register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
62-
class UserAdmin(SqlAlchemyModelAdmin):
63-
exclude = ("password",)
64-
list_display = ("id", "username", "is_superuser", "is_active")
65-
list_display_links = ("id", "username")
66-
list_filter = ("id", "username", "is_superuser", "is_active")
67-
search_fields = ("username",)
68-
69-
async def authenticate(self, username, password):
70-
sessionmaker = self.get_sessionmaker()
71-
async with sessionmaker() as session:
72-
query = select(User).filter_by(
73-
username=username, password=password, is_superuser=True
74-
)
75-
result = await session.scalars(query)
76-
user = result.first()
77-
if not user:
78-
return None
79-
if password != user.password:
80-
return None
81-
return user.id
82-
83-
@register(Transaction, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
84-
class TransactionAdmin(SqlAlchemyModelAdmin):
85-
list_display = ("id", "user")
86-
raw_id_fields = ("user",)
87-
88-
89-
app = FastAPI()
90-
91-
92103
@app.on_event("startup")
93104
async def startup():
94105
await init_db()
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import datetime
2+
import typing as tp
3+
from decimal import Decimal
4+
from enum import Enum
5+
6+
from sqlalchemy import JSON, Boolean, Column, Date, DateTime, Float, ForeignKey, Integer, String, Table, Text, Time
7+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
8+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
9+
10+
sqlalchemy_engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=True)
11+
sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False)
12+
13+
14+
class EventTypeEnum(str, Enum):
15+
PRIVATE = "PRIVATE"
16+
PUBLIC = "PUBLIC"
17+
18+
19+
class Base(DeclarativeBase):
20+
pass
21+
22+
23+
class BaseModel(Base):
24+
__abstract__ = True
25+
26+
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
27+
created_at: Mapped[datetime.datetime] = mapped_column(
28+
DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
29+
)
30+
updated_at: Mapped[datetime.datetime] = mapped_column(
31+
DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
32+
)
33+
34+
@classmethod
35+
def get_model_name(cls):
36+
return f"sqlalchemy.{cls.__name__}"
37+
38+
39+
user_m2m_event = Table(
40+
"event_participants",
41+
Base.metadata,
42+
Column("event_id", ForeignKey("event.id"), primary_key=True),
43+
Column("user_id", ForeignKey("user.id"), primary_key=True),
44+
)
45+
46+
47+
class User(BaseModel):
48+
__tablename__ = "user"
49+
50+
username: Mapped[str] = mapped_column(String(length=255), nullable=False)
51+
password: Mapped[str] = mapped_column(String(length=255), nullable=False)
52+
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
53+
54+
events: Mapped[list["Event"]] = relationship(secondary=user_m2m_event, back_populates="participants")
55+
56+
async def __str__(self):
57+
return self.username
58+
59+
60+
class Tournament(BaseModel):
61+
__tablename__ = "tournament"
62+
63+
name: Mapped[str] = mapped_column(String(length=255), nullable=False)
64+
65+
events: Mapped[list["Event"]] = relationship(back_populates="tournament")
66+
67+
async def __str__(self):
68+
return self.name
69+
70+
71+
class BaseEvent(BaseModel):
72+
__tablename__ = "base_event"
73+
74+
name: Mapped[str] = mapped_column(String(length=255), nullable=False)
75+
event: Mapped[tp.Optional["Event"]] = relationship(back_populates="base")
76+
77+
async def __str__(self):
78+
return self.name
79+
80+
81+
class Event(BaseModel):
82+
__tablename__ = "event"
83+
84+
base_id: Mapped[int | None] = mapped_column(ForeignKey("base_event.id"), nullable=True)
85+
base: Mapped[tp.Optional["BaseEvent"]] = relationship(back_populates="event")
86+
87+
name: Mapped[str] = mapped_column(String(length=255), nullable=False)
88+
89+
tournament_id: Mapped[int | None] = mapped_column(ForeignKey("tournament.id"), nullable=False)
90+
tournament: Mapped[tp.Optional["Tournament"]] = relationship(back_populates="events")
91+
92+
participants: Mapped[list["User"]] = relationship(secondary=user_m2m_event, back_populates="events")
93+
94+
rating: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
95+
description: Mapped[str | None] = mapped_column(Text, nullable=False)
96+
event_type: Mapped[EventTypeEnum] = mapped_column(default=EventTypeEnum.PUBLIC)
97+
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
98+
start_time: Mapped[datetime.time | None] = mapped_column(Time, nullable=True)
99+
date: Mapped[datetime.date | None] = mapped_column(Date, nullable=True)
100+
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
101+
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
102+
price: Mapped[Decimal | None] = mapped_column(
103+
Float(asdecimal=True), nullable=True
104+
) # max_digits=10, decimal_places=2
105+
106+
json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
107+
108+
async def __str__(self):
109+
return self.name
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
all: install run
2+
3+
.PHONY: fastapi
4+
run:
5+
poetry run fastapi dev --reload --port=8090 example.py
6+
7+
.PHONY: install
8+
install:
9+
poetry install
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## FastAPI + Tortoise ORM Example
2+
3+
- Uses in-memory SQLite 3 instance
4+
- Creates User mode
5+
- Creates "admin/admin" superuser
6+
7+
```bash
8+
make install # Creates virtualenv with Poetry
9+
make run # Runs fastapi dev
10+
```
11+
12+
So open `http://127.0.0.1:8090/admin/` and have a fun!
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
5+
os.environ["ADMIN_USER_MODEL"] = "User"
6+
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
7+
os.environ["ADMIN_SECRET_KEY"] = "secret"
8+
9+
sys.path.append(str(Path(__file__).resolve().parent))

0 commit comments

Comments
 (0)