Skip to content

Commit e8dd150

Browse files
authored
Merge pull request #12 from borhanst/fix/uuid-pk-support
Fix/UUID pk support
2 parents dd5f377 + 362f934 commit e8dd150

17 files changed

Lines changed: 161 additions & 57 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pip-delete-this-directory.txt
2828
# Virtual environments
2929
.venv/
3030
venv/
31+
list/
3132
ENV/
3233
env/
3334
.env

fastapi_admin_kit/api/roles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ async def update_role(
106106
)
107107

108108

109-
@router.delete("/{role_id}", status_code=204)
109+
@router.delete("/{role_id}", status_code=204, response_model=None)
110110
async def delete_role(
111111
request: Request,
112112
role_id: int,

fastapi_admin_kit/api/search.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async def get_search_suggestions(
5656
if registry is None or not q.strip():
5757
return {"suggestions": [], "query": q}
5858

59+
admin_path = request.app.state.admin_config["admin_path"]
5960
query_lower = q.strip().lower()
6061
suggestions: list[dict[str, Any]] = []
6162

@@ -79,7 +80,7 @@ async def get_search_suggestions(
7980
"model": table_name,
8081
"label": verbose_name_plural,
8182
"sublabel": table_name,
82-
"url": f"/admin/{table_name}",
83+
"url": f"{admin_path}/{table_name}",
8384
}
8485
)
8586

@@ -120,7 +121,7 @@ async def get_search_suggestions(
120121
"field": field_name,
121122
"label": f"{verbose_name_plural}{field_label}",
122123
"sublabel": f"{table_name}.{field_name}",
123-
"url": f"/admin/{table_name}?q={q}",
124+
"url": f"{admin_path}/{table_name}?q={q}",
124125
}
125126
)
126127

fastapi_admin_kit/auth/csrf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,10 @@ async def auth_redirect_handler(request: Request, exc: HTTPException) -> Respons
224224
if exc.status_code == 401:
225225
accept = request.headers.get("accept", "")
226226
if "text/html" in accept:
227-
login_url = "/admin/login"
227+
admin_path = request.app.state.admin_config["admin_path"]
228+
login_url = f"{admin_path}/login"
228229
current_path = request.url.path
229-
if current_path != "/admin/login":
230+
if current_path != f"{admin_path}/login":
230231
if request.url.query:
231232
login_url += f"?next={current_path}%3F{request.url.query}"
232233
else:

fastapi_admin_kit/auth/views.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ async def login_get(
4545
) -> HTMLResponse:
4646
"""GET /admin/login — show login page, redirect if already logged in."""
4747
if session_payload is not None:
48-
target = next if _is_safe_url(next) else "/admin/"
48+
admin_path = request.app.state.admin_config["admin_path"]
49+
target = next if _is_safe_url(next) else f"{admin_path}/"
4950
return RedirectResponse(url=target, status_code=status.HTTP_302_FOUND)
5051

5152
jinja_env = request.app.state.admin_jinja_env
@@ -90,7 +91,8 @@ async def login_post(
9091
if next and _is_safe_url(next):
9192
redirect_url = next
9293
else:
93-
redirect_url = "/admin/"
94+
admin_path = request.app.state.admin_config["admin_path"]
95+
redirect_url = f"{admin_path}/"
9496

9597
samesite = getattr(
9698
request.app.state.admin_state, "session_samesite", "strict"
@@ -149,7 +151,8 @@ async def logout_post(
149151
request.app.state.admin_state, "session_samesite", "strict"
150152
)
151153
response = RedirectResponse(
152-
url="/admin/login", status_code=status.HTTP_302_FOUND
154+
url=f"{request.app.state.admin_config['admin_path']}/login",
155+
status_code=status.HTTP_302_FOUND
153156
)
154157
response.delete_cookie(
155158
key=session_backend.cookie_name,

fastapi_admin_kit/form/pipeline.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ def build_form_context(
2727
fieldsets: list[FieldsetContext] = [FieldsetContext(fields=[])]
2828

2929
for field_meta in registered.form_fields:
30-
col = next((c for c in registered.columns if c.name == field_meta.name), None)
31-
rel = next((r for r in registered.relationships if r.name == field_meta.name), None)
30+
col = next(
31+
(c for c in registered.columns if c.name == field_meta.name), None
32+
)
33+
rel = next(
34+
(r for r in registered.relationships if r.name == field_meta.name),
35+
None,
36+
)
3237
widget = registered.get_widget(field_meta.name)
3338

3439
value = values.get(field_meta.name)
@@ -39,6 +44,7 @@ def build_form_context(
3944
if value is None and rel is not None:
4045
try:
4146
from sqlalchemy import inspect as sa_inspect
47+
4248
mapper = sa_inspect(type(obj))
4349
rel_prop = mapper.relationships.get(rel.name)
4450
if rel_prop is not None:
@@ -62,9 +68,13 @@ def build_form_context(
6268
):
6369
try:
6470
from sqlalchemy import inspect as sa_inspect
71+
6572
mapper = sa_inspect(type(obj))
6673
rel_prop = mapper.relationships.get(rel.name)
67-
if rel_prop is not None and rel_prop.direction.name == "MANYTOMANY":
74+
if (
75+
rel_prop is not None
76+
and rel_prop.direction.name == "MANYTOMANY"
77+
):
6878
value = [str(item.id) for item in value]
6979
except Exception:
7080
pass
@@ -74,6 +84,13 @@ def build_form_context(
7484
widget_macro = widget.macro_name
7585
widget_ctx = widget.render_context(field_meta, value)
7686
widget_ctx["is_create"] = is_create
87+
if request is not None:
88+
admin_path = request.app.state.admin_config["admin_path"]
89+
widget_ctx["admin_path"] = admin_path
90+
if "search_url" in widget_ctx:
91+
widget_ctx["search_url"] = widget_ctx["search_url"].replace(
92+
"/admin/", f"{admin_path}/"
93+
)
7794
if obj is not None:
7895
widget_ctx["obj_id"] = getattr(obj, "id", "")
7996
if rel is not None and rel_labels:

fastapi_admin_kit/inspection/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ def get_pk_field(model: type) -> str | None:
6767
return tuple(col.key for col in pk_cols)
6868

6969

70+
def cast_pk_value(model: type, value: Any) -> Any:
71+
"""Cast a string primary key value to the correct Python type.
72+
73+
Inspects the model's primary key column type and converts the value
74+
accordingly. Supports Integer, BigInteger, String, and UUID types.
75+
Returns the original value if type cannot be determined.
76+
"""
77+
if value is None:
78+
return None
79+
mapper = inspect(model)
80+
pk_cols = mapper.primary_key
81+
if not pk_cols or len(pk_cols) != 1:
82+
return value
83+
pk_col = pk_cols[0]
84+
from sqlalchemy import BigInteger, Integer
85+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
86+
from sqlalchemy.types import Uuid
87+
88+
col_type = type(pk_col.type)
89+
if col_type in (Integer, BigInteger):
90+
return int(value)
91+
if col_type in (PG_UUID, Uuid):
92+
from uuid import UUID
93+
94+
return UUID(str(value))
95+
return value
96+
97+
7098
def auto_label(name: str) -> str:
7199
"""Auto-generate a human-readable label from a field name.
72100

fastapi_admin_kit/router.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,14 @@ async def inline_edit_form(
150150
from sqlalchemy import select
151151
from sqlalchemy.orm import selectinload
152152

153+
from fastapi_admin_kit.inspection import cast_pk_value
153154
mapper = sa_inspect(registered.model)
154155
options = [
155156
selectinload(getattr(registered.model, r.key))
156157
for r in mapper.relationships
157158
]
158159
stmt = select(registered.model).options(*options).where(
159-
getattr(registered.model, registered.pk_field) == int(id)
160+
getattr(registered.model, registered.pk_field) == cast_pk_value(registered.model, id)
160161
)
161162
result = await session.execute(stmt)
162163
obj = result.scalar_one_or_none()
@@ -209,13 +210,14 @@ async def inline_edit_save(
209210
from sqlalchemy import select
210211
from sqlalchemy.orm import selectinload
211212

213+
from fastapi_admin_kit.inspection import cast_pk_value
212214
mapper = sa_inspect(registered.model)
213215
options = [
214216
selectinload(getattr(registered.model, r.key))
215217
for r in mapper.relationships
216218
]
217219
stmt = select(registered.model).options(*options).where(
218-
getattr(registered.model, registered.pk_field) == int(id)
220+
getattr(registered.model, registered.pk_field) == cast_pk_value(registered.model, id)
219221
)
220222
result = await session.execute(stmt)
221223
obj = result.scalar_one_or_none()
@@ -377,7 +379,8 @@ async def execute_row_action(
377379
if not action_obj:
378380
raise HTTPException(status_code=404, detail=f"Unknown action: {action_name}")
379381

380-
obj = await session.get(registered.model, int(id))
382+
from fastapi_admin_kit.inspection import cast_pk_value
383+
obj = await session.get(registered.model, cast_pk_value(registered.model, id))
381384
if not obj:
382385
raise HTTPException(status_code=404, detail="Not found")
383386

@@ -484,7 +487,8 @@ async def update_field(
484487

485488
session = get_db_session(request)
486489
try:
487-
obj = await session.get(registered.model, int(id))
490+
from fastapi_admin_kit.inspection import cast_pk_value
491+
obj = await session.get(registered.model, cast_pk_value(registered.model, id))
488492
if obj is None:
489493
raise HTTPException(status_code=404, detail="Object not found")
490494

fastapi_admin_kit/static/js/admin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ document.addEventListener('alpine:init', () => {
565565
return;
566566
}
567567
try {
568-
const resp = await fetch(`/admin/search/suggestions?q=${encodeURIComponent(q)}`);
568+
const resp = await fetch(`${window.__ADMIN_PATH__}/search/suggestions?q=${encodeURIComponent(q)}`);
569569
if (resp.ok) {
570570
const data = await resp.json();
571571
this.results = data.suggestions || [];

fastapi_admin_kit/templates/base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ <h3 class="modal__title" x-text="title"></h3>
136136
<script>{{ _ui2.custom_js }}</script>
137137
{% endif %}
138138

139+
<script>
140+
window.__ADMIN_PATH__ = {{ admin_path | default('/admin') | tojson }};
141+
</script>
139142
<script src="/static/js/htmx-config.js"></script>
140143
<script src="/static/js/admin.js?v={{ static_version }}"></script>
141144
</body>

0 commit comments

Comments
 (0)