Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 1 addition & 2 deletions sqladmin/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ def __init__(
self.session_maker.configure(autoflush=False, autocommit=False)
self.is_async = is_async_session_maker(self.session_maker)

middlewares = middlewares or []
middlewares = list(middlewares or [])
self.authentication_backend = authentication_backend
if authentication_backend:
middlewares = list(middlewares)
middlewares.extend(authentication_backend.middlewares)

self.admin = Starlette(middleware=middlewares)
Expand Down
15 changes: 10 additions & 5 deletions sqladmin/templates/sqladmin/_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,22 @@
</div>
{% endmacro %}

{% macro render_field(field, kwargs={}) %}
{% macro render_field(field, kwargs={}, ajax_lookup_url="") %}
<div class="mb-3 form-group row">
{{ field.label(
class_="form-label col-sm-2 col-form-label" + (' required-label' if field.flags.required else ''),
**({'title': "This is a required field"} if field.flags.required else {})
) }}
<div class="col-sm-10">
{% if ajax_lookup_url and field.loader is defined %}
{% set extra = {"data_url": ajax_lookup_url} %}
{% else %}
{% set extra = {} %}
{% endif %}
{% if field.errors %}
{{ field(class_="form-control is-invalid") }}
{{ field(class_="form-control is-invalid", **extra) }}
{% else %}
{{ field() }}
{{ field(**extra) }}
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback">{{ error }}</div>
Expand All @@ -76,7 +81,7 @@
</div>
{% endmacro %}

{% macro render_form_fields(form, form_opts=None) %}
{% macro render_form_fields(form, form_opts=None, ajax_lookup_url="") %}
{% if form.hidden_tag is defined %}
{{ form.hidden_tag() }}
{% else %}
Expand All @@ -86,6 +91,6 @@
{% endif %}

{% for f in form if f.widget.input_type != 'hidden' %}
{{ render_field(f, kwargs) }}
{{ render_field(f, kwargs, ajax_lookup_url=ajax_lookup_url) }}
{% endfor %}
{% endmacro %}
2 changes: 1 addition & 1 deletion sqladmin/templates/sqladmin/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h3 class="card-title">New {{ model_view.name }}</h3>
{% endif %}
</div>
<fieldset class="form-fieldset">
{{ render_form_fields(form, form_opts=form_opts) }}
{{ render_form_fields(form, form_opts=form_opts, ajax_lookup_url=url_for('admin:ajax_lookup', identity=model_view.identity)) }}
</fieldset>
<div class="row">
<div class="col-md-2">
Expand Down
2 changes: 1 addition & 1 deletion sqladmin/templates/sqladmin/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h3 class="card-title">Edit {{ model_view.name }}</h3>
{% endif %}
</div>
<fieldset class="form-fieldset">
{{ render_form_fields(form, form_opts=form_opts) }}
{{ render_form_fields(form, form_opts=form_opts, ajax_lookup_url=url_for('admin:ajax_lookup', identity=model_view.identity)) }}
</fieldset>
<div class="row">
<div class="col-md-2">
Expand Down
8 changes: 4 additions & 4 deletions tests/test_ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ async def test_create_page_template(client: AsyncClient) -> None:

assert 'data-json="[]"' in response.text
assert 'data-role="select2-ajax"' in response.text
assert 'data-url="/admin/user/ajax/lookup"' in response.text
assert 'data-url="http://testserver/admin/user/ajax/lookup"' in response.text

response = await client.get("/admin/address/create")

assert 'data-role="select2-ajax"' in response.text
assert 'data-url="/admin/address/ajax/lookup"' in response.text
assert 'data-url="http://testserver/admin/address/ajax/lookup"' in response.text


async def test_edit_page_template(client: AsyncClient) -> None:
Expand All @@ -259,15 +259,15 @@ async def test_edit_page_template(client: AsyncClient) -> None:
in response.text
)
assert 'data-role="select2-ajax"' in response.text
assert 'data-url="/admin/user/ajax/lookup"' in response.text
assert 'data-url="http://testserver/admin/user/ajax/lookup"' in response.text

response = await client.get("/admin/address/edit/1")
assert (
'data-json="[{&#34;id&#34;: &#34;1&#34;, &#34;text&#34;: &#34;User 1&#34;}]"'
in response.text
)
assert 'data-role="select2-ajax"' in response.text
assert 'data-url="/admin/address/ajax/lookup"' in response.text
assert 'data-url="http://testserver/admin/address/ajax/lookup"' in response.text


async def test_create_and_edit_forms(client: AsyncClient) -> None:
Expand Down
4 changes: 1 addition & 3 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ class Shipment(Base):
destination_address_id = Column(Integer, ForeignKey("addresses.id"))

origin_address = relationship("Address", foreign_keys=[origin_address_id])
destination_address = relationship(
"Address", foreign_keys=[destination_address_id]
)
destination_address = relationship("Address", foreign_keys=[destination_address_id])


@pytest.fixture(autouse=True)
Expand Down
84 changes: 84 additions & 0 deletions tests/test_root_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from typing import Generator

import pytest
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from starlette.applications import Starlette
from starlette.testclient import TestClient

from sqladmin import Admin, ModelView
from tests.common import sync_engine as engine

session_maker = sessionmaker(bind=engine)

Base = declarative_base()


class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True)
name = Column(String(32), default="SQLAdmin")

addresses = relationship("Address", back_populates="user")


class Address(Base):
__tablename__ = "addresses"

id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))

user = relationship("User", back_populates="addresses")


@pytest.fixture(autouse=True)
def prepare_database() -> Generator[None, None, None]:
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)


def test_root_path_ajax_lookup_url_includes_root_path() -> None:
"""AJAX data-url for Select2 relationship fields must include root_path."""
app = Starlette()
admin = Admin(app=app, engine=engine)

class UserAdmin(ModelView, model=User):
form_ajax_refs = {
"addresses": {
"fields": ("id",),
}
}

class AddressAdmin(ModelView, model=Address): ...

admin.add_view(UserAdmin)
admin.add_view(AddressAdmin)

with TestClient(app, root_path="/api/v1") as client:
# Simulate uvicorn --root-path: requests arrive with prefix in path
response = client.get("/api/v1/admin/user/create")
assert response.status_code == 200
assert (
'data-url="http://testserver/api/v1/admin/user/ajax/lookup"'
in response.text
)

# The template passes data_url (underscore) as a kwarg. WTForms'
# clean_key converts it to data-url (dash) before the widget sees it.
# Verify the underscore variant doesn't leak as a raw HTML attribute.
assert "data_url=" not in response.text

# Edit page should also have ajax data-url with root_path
with session_maker() as session:
session.add(User(id=1, name="test"))
session.commit()

response = client.get("/api/v1/admin/user/edit/1")
assert response.status_code == 200
assert (
'data-url="http://testserver/api/v1/admin/user/ajax/lookup"'
in response.text
)
assert "data_url=" not in response.text
Loading