← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~ack/maas-site-manager:pending-sites-endpoint into maas-site-manager:main

 

Alberto Donato has proposed merging ~ack/maas-site-manager:pending-sites-endpoint into maas-site-manager:main.

Commit message:
add /requests endpoint for listing pending sites



Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~ack/maas-site-manager/+git/site-manager/+merge/442427
-- 
Your team MAAS Committers is requested to review the proposed merge of ~ack/maas-site-manager:pending-sites-endpoint into maas-site-manager:main.
diff --git a/backend/msm/db/models.py b/backend/msm/db/models.py
index a04bc3e..ec3d032 100644
--- a/backend/msm/db/models.py
+++ b/backend/msm/db/models.py
@@ -38,6 +38,15 @@ class Site(BaseModel):
     stats: SiteData | None
 
 
+class PendingSite(BaseModel):
+    """A pending MAAS site."""
+
+    id: int
+    name: str
+    url: str
+    created: datetime
+
+
 class Token(BaseModel):
     """A registration token for a site."""
 
diff --git a/backend/msm/db/queries.py b/backend/msm/db/queries.py
index 9248559..3e51fbb 100644
--- a/backend/msm/db/queries.py
+++ b/backend/msm/db/queries.py
@@ -28,6 +28,7 @@ from ._tables import (
     User,
 )
 from .models import (
+    PendingSite as PendingSiteSchema,
     Site as SiteSchema,
     Token as TokenSchema,
     UserWithPassword as UserWithPasswordSchema,
@@ -97,7 +98,7 @@ def filters_from_arguments(
     ]
 
 
-async def get_filtered_sites(
+async def get_sites(
     session: AsyncSession,
     offset: int = 0,
     limit: int = MAX_PAGE_SIZE,
@@ -109,7 +110,6 @@ async def get_filtered_sites(
     street: list[str] | None = None,
     timezone: list[str] | None = None,
     url: list[str] | None = None,
-    accepted: bool | None = None,
 ) -> tuple[int, Iterable[SiteSchema]]:
     filters = filters_from_arguments(
         Site,
@@ -122,8 +122,7 @@ async def get_filtered_sites(
         timezone=timezone,
         url=url,
     )
-    if accepted is not None:
-        filters.append(Site.c.accepted == accepted)
+    filters.append(Site.c.accepted == True)  # noqa
     count = (
         await session.execute(
             select(func.count())
@@ -175,6 +174,34 @@ async def get_filtered_sites(
     return count, [SiteSchema(**row._asdict()) for row in result.all()]
 
 
+async def get_pending_sites(
+    session: AsyncSession,
+    offset: int = 0,
+    limit: int = MAX_PAGE_SIZE,
+) -> tuple[int, Iterable[PendingSiteSchema]]:
+    filters = [Site.c.accepted == False]  # noqa
+    count = (
+        await session.execute(
+            select(func.count()).select_from(Site).where(*filters)
+        )
+    ).scalar() or 0
+    stmt = (
+        select(
+            Site.c.id,
+            Site.c.name,
+            Site.c.url,
+            Site.c.created,
+        )
+        .select_from(Site)
+        .where(*filters)
+        .order_by(Site.c.id)
+        .limit(limit)
+        .offset(offset)
+    )
+    result = await session.execute(stmt)
+    return count, [PendingSiteSchema(**row._asdict()) for row in result.all()]
+
+
 async def get_tokens(
     session: AsyncSession,
     offset: int = 0,
diff --git a/backend/msm/user_api/_handlers.py b/backend/msm/user_api/_handlers.py
index c1dcd0a..7d0bfac 100644
--- a/backend/msm/user_api/_handlers.py
+++ b/backend/msm/user_api/_handlers.py
@@ -32,6 +32,7 @@ from ._schema import (
     CreateTokensRequest,
     CreateTokensResponse,
     JSONWebToken,
+    PaginatedPendingSites,
     PaginatedSites,
     PaginatedTokens,
     UserLoginRequest,
@@ -49,12 +50,11 @@ async def sites(
     pagination_params: PaginationParams = Depends(pagination_params),
     filter_params: SiteFilterParams = Depends(site_filter_parameters),
 ) -> PaginatedSites:
-    """Return all sites."""
-    total, results = await queries.get_filtered_sites(
+    """Return accepted sites."""
+    total, results = await queries.get_sites(
         session,
         offset=pagination_params.offset,
         limit=pagination_params.size,
-        accepted=True,  # only list accepted sites here
         **filter_params._asdict(),
     )
     return PaginatedSites(
@@ -65,6 +65,25 @@ async def sites(
     )
 
 
+async def pending_sites(
+    authenticated_user: Annotated[User, Depends(get_authenticated_user)],
+    session: AsyncSession = Depends(db_session),
+    pagination_params: PaginationParams = Depends(pagination_params),
+) -> PaginatedPendingSites:
+    """Return pending sites."""
+    total, results = await queries.get_pending_sites(
+        session,
+        offset=pagination_params.offset,
+        limit=pagination_params.size,
+    )
+    return PaginatedPendingSites(
+        total=total,
+        page=pagination_params.page,
+        size=pagination_params.size,
+        items=list(results),
+    )
+
+
 async def tokens(
     authenticated_user: Annotated[User, Depends(get_authenticated_user)],
     session: AsyncSession = Depends(db_session),
diff --git a/backend/msm/user_api/_schema.py b/backend/msm/user_api/_schema.py
index ee6c3f8..af1da4f 100644
--- a/backend/msm/user_api/_schema.py
+++ b/backend/msm/user_api/_schema.py
@@ -7,6 +7,7 @@ from uuid import UUID
 from pydantic import BaseModel
 
 from ..db.models import (
+    PendingSite,
     Site,
     Token,
 )
@@ -34,6 +35,10 @@ class PaginatedSites(PaginatedResults):
     items: list[Site]
 
 
+class PaginatedPendingSites(PaginatedResults):
+    items: list[PendingSite]
+
+
 class PaginatedTokens(PaginatedResults):
     items: list[Token]
 
diff --git a/backend/msm/user_api/_setup.py b/backend/msm/user_api/_setup.py
index 6efd588..d9e976d 100644
--- a/backend/msm/user_api/_setup.py
+++ b/backend/msm/user_api/_setup.py
@@ -37,15 +37,18 @@ def create_app(db_dsn: str | None = None) -> FastAPI:
     )
     app.state.db = db
     app.router.add_api_route("/", _handlers.root, methods=["GET"])
+    app.router.add_api_route(
+        "/login", _handlers.login_for_access_token, methods=["POST"]
+    )
+    app.router.add_api_route(
+        "/requests", _handlers.pending_sites, methods=["GET"]
+    )
     app.router.add_api_route("/sites", _handlers.sites, methods=["GET"])
     app.router.add_api_route("/tokens", _handlers.tokens, methods=["GET"])
     app.router.add_api_route(
         "/tokens", _handlers.tokens_post, methods=["POST"]
     )
     app.router.add_api_route(
-        "/login", _handlers.login_for_access_token, methods=["POST"]
-    )
-    app.router.add_api_route(
         "/users/me", _handlers.read_users_me, methods=["GET"]
     )
     return app
diff --git a/backend/tests/user_api/test_handlers.py b/backend/tests/user_api/test_handlers.py
index 8a5d97c..a1301f0 100644
--- a/backend/tests/user_api/test_handlers.py
+++ b/backend/tests/user_api/test_handlers.py
@@ -244,6 +244,52 @@ async def test_list_sites_with_stats(
 
 
 @pytest.mark.asyncio
+async def test_list_pending_sites(
+    authenticated_user_app_client: AuthAsyncClient, fixture: Fixture
+) -> None:
+    site1 = {
+        "name": "LondonHQ",
+        "city": "London",
+        "country": "gb",
+        "latitude": "51.509865",
+        "longitude": "-0.118092",
+        "note": "the first site",
+        "region": "Blue Fin Bldg",
+        "street": "110 Southwark St",
+        "timezone": "Europe/London",
+        "url": "https://londoncalling.example.com";,
+        "accepted": True,
+    }
+    site2 = site1.copy()
+    site2.update(
+        {
+            "name": "BerlinHQ",
+            "timezone": "Europe/Berlin",
+            "city": "Berlin",
+            "country": "de",
+            "accepted": False,
+        }
+    )
+    _, pending_site = await fixture.create("site", [site1, site2])
+
+    response = await authenticated_user_app_client.get("/requests")
+    assert response.status_code == 200
+    assert response.json() == {
+        "page": 1,
+        "size": 20,
+        "total": 1,
+        "items": [
+            {
+                "id": pending_site["id"],
+                "name": pending_site["name"],
+                "url": pending_site["url"],
+                "created": pending_site["created"].isoformat(),
+            },
+        ],
+    }
+
+
+@pytest.mark.asyncio
 @pytest.mark.parametrize("time_format", ["ISO 8601", "Float"])
 async def test_token_time_format(
     time_format: str, authenticated_user_app_client: AuthAsyncClient

Follow ups