Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions sqladmin/_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
if TYPE_CHECKING:
from sqladmin.application import BaseView, ModelView

from sqladmin.helpers import local_url_for


class ItemMenu:
def __init__(self, name: str, icon: str | None = None) -> None:
Expand Down Expand Up @@ -73,8 +75,8 @@ def is_active(self, request: Request) -> bool:

def url(self, request: Request) -> str | URL:
if self.view.is_model:
return request.url_for("admin:list", identity=self.view.identity)
return request.url_for(f"admin:{self.view.identity}")
return local_url_for(request, "list", identity=self.view.identity)
return local_url_for(request, self.view.identity)

@property
def display_name(self) -> str:
Expand Down
19 changes: 10 additions & 9 deletions sqladmin/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from sqladmin.helpers import (
get_object_identifier,
is_async_session_maker,
local_url_for,
slugify_action_name,
)
from sqladmin.models import BaseView, ModelView
Expand Down Expand Up @@ -236,13 +237,12 @@ class UserAdmin(ModelView, model=User):

view._admin_ref = self
# Set database engine from Admin instance
view.session_maker = self.session_maker
view.is_async = self.is_async
view.ajax_lookup_url = urljoin(
self.base_url + "/", f"{view.identity}/ajax/lookup"
)
view.templates = self.templates
view_instance = view()
view_instance = view(self.session_maker)

self._find_decorated_funcs(
view, view_instance, self._handle_action_decorated_func
Expand Down Expand Up @@ -367,6 +367,7 @@ def __init__( # type: ignore[no-any-unimported]
debug: bool = False,
templates_dir: str = "templates",
authentication_backend: AuthenticationBackend | None = None,
mount_name: str = "admin",
) -> None:
"""
Args:
Expand Down Expand Up @@ -446,7 +447,7 @@ async def http_exception(
self.admin.router.routes = routes
self.admin.exception_handlers = {HTTPException: http_exception}
self.admin.debug = debug
self.app.mount(base_url, app=self.admin, name="admin")
self.app.mount(base_url, app=self.admin, name=mount_name)

@login_required
async def index(self, request: Request) -> Response:
Expand Down Expand Up @@ -519,7 +520,7 @@ async def delete(self, request: Request) -> Response:

referer_url = URL(request.headers.get("referer", ""))
referer_params = MultiDict(parse_qsl(referer_url.query))
url = URL(str(request.url_for("admin:list", identity=identity)))
url = URL(str(local_url_for(request, "list", identity=identity)))
url = url.include_query_params(**referer_params)
return PlainTextResponse(content=str(url))

Expand Down Expand Up @@ -658,7 +659,7 @@ async def login(self, request: Request) -> Response:
request, "sqladmin/login.html", context, status_code=400
)

return RedirectResponse(request.url_for("admin:index"), status_code=302)
return RedirectResponse(local_url_for(request, "index"), status_code=302)

async def logout(self, request: Request) -> Response:
if self.authentication_backend is None:
Expand All @@ -672,7 +673,7 @@ async def logout(self, request: Request) -> Response:
if isinstance(response, Response):
return response

return RedirectResponse(request.url_for("admin:index"), status_code=302)
return RedirectResponse(local_url_for(request, "index"), status_code=302)

async def ajax_lookup(self, request: Request) -> Response:
"""Ajax lookup route."""
Expand Down Expand Up @@ -706,14 +707,14 @@ def get_save_redirect_url(
identifier = get_object_identifier(obj)

if form.get("save") == "Save":
return request.url_for("admin:list", identity=identity)
return local_url_for(request, "list", identity=identity)

if form.get("save") == "Save and continue editing" or (
form.get("save") == "Save as new" and model_view.save_as_continue
):
return request.url_for("admin:edit", identity=identity, pk=identifier)
return local_url_for(request, "edit", identity=identity, pk=identifier)

return request.url_for("admin:create", identity=identity)
return local_url_for(request, "create", identity=identity)

async def _handle_form_data(self, request: Request, obj: Any = None) -> FormData:
"""
Expand Down
6 changes: 5 additions & 1 deletion sqladmin/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response

from sqladmin.helpers import local_url_for


class AuthenticationBackend:
"""Base class for implementing the Authentication into SQLAdmin.
Expand Down Expand Up @@ -66,7 +68,9 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> Any:
if isinstance(response, Response):
return response
if not bool(response):
return RedirectResponse(request.url_for("admin:login"), status_code=302)
return RedirectResponse(
local_url_for(request, "login"), status_code=302
)

if inspect.iscoroutinefunction(func):
return await func(*args, **kwargs)
Expand Down
58 changes: 58 additions & 0 deletions sqladmin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from sqlalchemy import Column, inspect
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import RelationshipProperty, sessionmaker
from starlette.datastructures import URL
from starlette.requests import Request
from starlette.routing import Mount, Router

from sqladmin._types import MODEL_PROPERTY

Expand Down Expand Up @@ -322,3 +325,58 @@ def choice_coerce(value: Any) -> Any:

def is_async_session_maker(session_maker: sessionmaker) -> bool:
return AsyncSession in session_maker.class_.__mro__


def local_url_for(request: Request, name: str, **kwargs: Any) -> URL:
"""
Generate a URL for the specified route, taking router hierarchy into account.

If the current router matches the target router or is not defined, uses the standard
`request.url_for()` method. Otherwise, adds a router name prefix to the route name.

Args:
request (Request): HTTP request object.
name (str): route name for URL generation.
**kwargs (Any): additional parameters for `request.url_for()`
(e.g., path parameters).

Returns:
URL: generated URL for the specified route.
"""
target_router = request.app.router
start_router = request.scope.get("router")
if not start_router or start_router == target_router:
return request.url_for(name, **kwargs)
router_name = get_current_router_name(start_router, target_router)
name_prefix = f"{router_name}:" if router_name else ""
return request.url_for(f"{name_prefix}{name}", **kwargs)


def get_current_router_name(start_router: Any, target_router: Router) -> str | None:
"""
Find the router name in the hierarchy that corresponds to the target router.

Traverses the routes of the start router and its nested applications
(of type `Mount`) to find a match with the target router.

Args:
start_router (Any): starting router from which the search begins.
target_router (Router): target router whose name needs to be found.

Returns:
str | None: router name (possibly composite in the format `parent:child`)
if the target router is found.`None` if the router is not found
in the hierarchy.
"""
for router in getattr(start_router, "routes", []):
if router == target_router:
return router.name
if not isinstance(router, Mount):
continue
app_router = getattr(router.app, "router", None)
if app_router == target_router:
return router.name
sub_name = get_current_router_name(router, target_router)
if sub_name is not None:
return f"{router.name}:{sub_name}"
return None
23 changes: 11 additions & 12 deletions sqladmin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Writer,
get_object_identifier,
get_primary_keys,
local_url_for,
object_identifier_values,
prettify_class_name,
secure_filename,
Expand Down Expand Up @@ -210,12 +211,6 @@ class UserAdmin(ModelView, model=User):

# Internals
pk_columns: ClassVar[Tuple[Column]]
session_maker: ClassVar[ # type: ignore[no-any-unimported]
Union[
sessionmaker,
"async_sessionmaker",
]
]
is_async: ClassVar[bool] = False
is_model: ClassVar[bool] = True
ajax_lookup_url: ClassVar[str] = ""
Expand Down Expand Up @@ -703,7 +698,10 @@ class UserAdmin(ModelView, model=User):
```
"""

def __init__(self) -> None:
def __init__(
self, session_maker: Union[sessionmaker, "async_sessionmaker"]
) -> None:
self.session_maker = session_maker
self._mapper = inspect(self.model)
self._prop_names = [attr.key for attr in self._mapper.attrs]
self._relations = [
Expand Down Expand Up @@ -795,22 +793,23 @@ async def _run_query(self, stmt: ClauseElement) -> Any:
def _url_for_delete(self, request: Request, obj: Any) -> str:
pk = get_object_identifier(obj)
query_params = urlencode({"pks": pk})
url = request.url_for(
"admin:delete", identity=slugify_class_name(obj.__class__.__name__)
url = local_url_for(
request, "delete", identity=slugify_class_name(obj.__class__.__name__)
)
return str(url) + "?" + query_params

def _url_for_details_with_prop(self, request: Request, obj: Any, prop: str) -> URL:
target = getattr(obj, prop, None)
if target is None:
return URL()
return self._build_url_for("admin:details", request, target)
return self._build_url_for("details", request, target)

def _url_for_action(self, request: Request, action_name: str) -> str:
return str(request.url_for(f"admin:action-{self.identity}-{action_name}"))
return str(local_url_for(request, f"action-{self.identity}-{action_name}"))

def _build_url_for(self, name: str, request: Request, obj: Any) -> URL:
return request.url_for(
return local_url_for(
request,
name,
identity=slugify_class_name(obj.__class__.__name__),
pk=get_object_identifier(obj),
Expand Down
26 changes: 13 additions & 13 deletions sqladmin/templates/sqladmin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/tabler.min.css') }}">
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/tabler-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/select2.min.css') }}">
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/flatpickr.min.css') }}">
<link rel="stylesheet" href="{{ url_for('admin:statics', path='css/main.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/tabler.min.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/tabler-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/select2.min.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/flatpickr.min.css') }}">
<link rel="stylesheet" href="{{ url_for('statics', path='css/main.css') }}">
{% if admin.favicon_url %}
<link rel="icon" href="{{ admin.favicon_url }}">
{% endif %}
Expand All @@ -25,13 +25,13 @@
{% endblock %}
</main>
{% endblock %}
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/tabler.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/popper.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/select2.full.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/flatpickr.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('admin:statics', path='js/main.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/tabler.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/popper.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/select2.full.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/flatpickr.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('statics', path='js/main.js') }}"></script>
{% block tail %}
{% endblock %}
</body>
Expand Down
4 changes: 2 additions & 2 deletions sqladmin/templates/sqladmin/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<h3 class="card-title">New {{ model_view.name }}</h3>
</div>
<div class="card-body border-bottom py-3">
<form action="{{ url_for('admin:create', identity=model_view.identity) }}" method="POST"
<form action="{{ url_for('create', identity=model_view.identity) }}" method="POST"
enctype="multipart/form-data">
<div class="row">
{% if error %}
Expand All @@ -19,7 +19,7 @@ <h3 class="card-title">New {{ model_view.name }}</h3>
</fieldset>
<div class="row">
<div class="col-md-2">
<a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
<a href="{{ url_for('list', identity=model_view.identity) }}" class="btn">
Cancel
</a>
</div>
Expand Down
8 changes: 4 additions & 4 deletions sqladmin/templates/sqladmin/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ <h3 class="card-title">
<td>
{% for elem, formatted_elem in zip(value, formatted_value) %}
{% if model_view.show_compact_lists %}
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
<a href="{{ model_view._build_url_for('details', request, elem) }}">({{ formatted_elem }})</a>
{% else %}
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">{{ formatted_elem }}</a><br/>
<a href="{{ model_view._build_url_for('details', request, elem) }}">{{ formatted_elem }}</a><br/>
{% endif %}
{% endfor %}
</td>
Expand All @@ -50,7 +50,7 @@ <h3 class="card-title">
<div class="card-footer container">
<div class="row row-gap-2">
<div class="col-auto">
<a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
<a href="{{ url_for('list', identity=model_view.identity) }}" class="btn">
Go Back
</a>
</div>
Expand All @@ -65,7 +65,7 @@ <h3 class="card-title">
{% endif %}
{% if model_view.can_edit %}
<div class="col-auto">
<a href="{{ model_view._build_url_for('admin:edit', request, model) }}" class="btn btn-primary">
<a href="{{ model_view._build_url_for('edit', request, model) }}" class="btn btn-primary">
Edit
</a>
</div>
Expand Down
4 changes: 2 additions & 2 deletions sqladmin/templates/sqladmin/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<h3 class="card-title">Edit {{ model_view.name }}</h3>
</div>
<div class="card-body border-bottom py-3">
<form action="{{ model_view._build_url_for('admin:edit', request, obj) }}" method="POST"
<form action="{{ model_view._build_url_for('edit', request, obj) }}" method="POST"
enctype="multipart/form-data">
<div class="row">
{% if error %}
Expand All @@ -19,7 +19,7 @@ <h3 class="card-title">Edit {{ model_view.name }}</h3>
</fieldset>
<div class="row">
<div class="col-md-2">
<a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
<a href="{{ url_for('list', identity=model_view.identity) }}" class="btn">
Cancel
</a>
</div>
Expand Down
4 changes: 2 additions & 2 deletions sqladmin/templates/sqladmin/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<aside class="navbar navbar-expand-lg navbar-vertical navbar-expand-md navbar-dark">
<div class="container-fluid">
<h1 class="navbar-brand navbar-brand-autodark">
<a href="{{ url_for('admin:index') }}">
<a href="{{ url_for('index') }}">
{% if admin.logo_url %}
<img src="{{ admin.logo_url }}" width="64" height="64" alt="{{ admin.title }}" />
{% else %}
Expand All @@ -23,7 +23,7 @@ <h3>{{ admin.title }}</h3>
</div>
</nav>
{% if admin.authentication_backend %}
<a href="{{ request.url_for('admin:logout') }}" class="btn btn-secondary btn-icon m-2 px-2">
<a href="{{ url_for('logout') }}" class="btn btn-secondary btn-icon m-2 px-2">
<span class="me-2">Logout</span>
<i class="fa fa-sign-out"></i>
</a>
Expand Down
Loading
Loading