You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/07/20 15:21:49 UTC

(tooling-trusted-releases) branch sbp updated: Consolidate release queries for the navigation menu

This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 7019111f Consolidate release queries for the navigation menu
7019111f is described below

commit 7019111f25f0f69bc846a578069427fa1e693fa9
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Jul 20 16:19:27 2026 +0100

    Consolidate release queries for the navigation menu
---
 atr/db/interaction.py     |  71 +++++++++----------
 atr/server.py             |   5 +-
 tests/unit/test_topnav.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 210 insertions(+), 37 deletions(-)

diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 6fd8d1b6..ef760a65 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -946,38 +946,6 @@ def trusted_vote_round(release: sql.Release) -> int | None:
     return None
 
 
-async def unfinished_releases(asfuid: str, is_member: bool) -> list[tuple[str, str, list[sql.Release]]]:
-    releases: list[tuple[str, str, list[sql.Release]]] = []
-    async with db.session() as data:
-        user_projects = await user.projects(asfuid)
-        user_projects.sort(key=lambda p: p.display_name)
-
-        active_phases = [
-            sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
-            sql.ReleasePhase.RELEASE_CANDIDATE,
-            sql.ReleasePhase.RELEASE_PREVIEW,
-        ]
-        for project in user_projects:
-            stmt = (
-                sqlmodel.select(sql.Release)
-                .where(
-                    sql.Release.project_key == project.key,
-                    sql.validate_instrumented_attribute(sql.Release.phase).in_(active_phases),
-                )
-                .options(db.select_in_load(sql.Release.project))
-                .order_by(sql.validate_instrumented_attribute(sql.Release.created).desc())
-            )
-            result = await data.execute(stmt)
-            active_releases = list(result.scalars().all())
-            if not user.can_view_embargoed_release(project.committee, asfuid, is_member=is_member):
-                active_releases = [r for r in active_releases if (not r.is_embargoed)]
-            if active_releases:
-                active_releases.sort(key=lambda r: r.created, reverse=True)
-                releases.append((project.short_display_name, project.key, active_releases))
-
-    return releases
-
-
 async def user_committees(asf_uid: str) -> list[tuple[str, str]]:
     results = []
     for committee in await user_committees_participant(asf_uid):
@@ -1003,9 +971,42 @@ async def user_committees_participant(asf_uid: str, caller_data: db.Session | No
         return await data.committee(has_participant=asf_uid).all()
 
 
-async def user_projects(asf_uid: str, caller_data: db.Session | None = None) -> list[tuple[str, str]]:
-    projects = await user.projects(asf_uid)
-    return [(p.key, p.display_name) for p in projects]
+async def user_topnav(
+    asf_uid: str, is_member: bool
+) -> tuple[list[tuple[str, str, list[sql.Release]]], list[tuple[str, str]]]:
+    via = sql.validate_instrumented_attribute
+    user_projects = await user.projects(asf_uid)
+    user_projects.sort(key=lambda p: p.display_name)
+
+    active_phases = [
+        sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+        sql.ReleasePhase.RELEASE_CANDIDATE,
+        sql.ReleasePhase.RELEASE_PREVIEW,
+    ]
+    async with db.session() as data:
+        stmt = (
+            sqlmodel.select(sql.Release)
+            .where(
+                via(sql.Release.project_key).in_([p.key for p in user_projects]),
+                via(sql.Release.phase).in_(active_phases),
+            )
+            .order_by(via(sql.Release.created).desc())
+        )
+        result = await data.execute(stmt)
+        releases_by_project: dict[str, list[sql.Release]] = {}
+        for release in result.scalars().all():
+            releases_by_project.setdefault(release.project_key, []).append(release)
+
+    unfinished: list[tuple[str, str, list[sql.Release]]] = []
+    for project in user_projects:
+        active_releases = releases_by_project.get(project.key, [])
+        for release in active_releases:
+            release.project = project
+        if not user.can_view_embargoed_release(project.committee, asf_uid, is_member=is_member):
+            active_releases = [r for r in active_releases if (not r.is_embargoed)]
+        if active_releases:
+            unfinished.append((project.short_display_name, project.key, active_releases))
+    return unfinished, [(p.key, p.display_name) for p in user_projects]
 
 
 async def validate_trusted_jwt(publisher: str, jwt: str) -> tuple[github.TrustedPublisherPayload, str | None]:
diff --git a/atr/server.py b/atr/server.py
index af86e43e..54738fe3 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -285,8 +285,9 @@ def _app_setup_context(app: base.QuartApp) -> None:
         user_notifications: list[sql.Notification] = []
         if current_user is not None:
             current_uid = current_user.uid
-            topnav_unfinished_releases = await interaction.unfinished_releases(current_uid, current_user.is_member)
-            topnav_user_projects = await interaction.user_projects(current_uid)
+            topnav_unfinished_releases, topnav_user_projects = await interaction.user_topnav(
+                current_uid, current_user.is_member
+            )
             async with db.session() as data:
                 db_user = await data.user(asf_uid=current_uid).get()
                 if db_user:
diff --git a/tests/unit/test_topnav.py b/tests/unit/test_topnav.py
new file mode 100644
index 00000000..c7b43bd9
--- /dev/null
+++ b/tests/unit/test_topnav.py
@@ -0,0 +1,171 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+from collections.abc import AsyncIterator
+
+import pytest
+import sqlalchemy
+import sqlalchemy.event
+import sqlalchemy.ext.asyncio
+import sqlmodel
+
+import atr.db as db
+import atr.db.interaction as interaction
+import atr.models.sql as sql
+import atr.user as user
+
+
+class MockApp:
+    def __init__(self):
+        self.extensions: dict[str, object] = {}
+
+
+class MockConfig:
+    ADMIN_USERS_ADDITIONAL = ""
+
+
+@pytest.fixture
+def mock_app(monkeypatch: pytest.MonkeyPatch) -> MockApp:
+    app = MockApp()
+    monkeypatch.setattr("asfquart.APP", app)
+    return app
+
+
+@pytest.fixture
+async def sqlite_global_db() -> AsyncIterator[sqlalchemy.ext.asyncio.AsyncEngine]:
+    engine = sqlalchemy.ext.asyncio.create_async_engine(
+        "sqlite+aiosqlite:///:memory:",
+        connect_args={"check_same_thread": False},
+        poolclass=sqlalchemy.pool.StaticPool,
+    )
+    async with engine.begin() as conn:
+        await conn.run_sync(sqlmodel.SQLModel.metadata.create_all)
+    db._global_atr_sessionmaker = sqlalchemy.ext.asyncio.async_sessionmaker(
+        bind=engine, class_=db.Session, expire_on_commit=False
+    )
+    yield engine
+    await engine.dispose()
+    db._global_atr_sessionmaker = None
+
+
+@pytest.mark.asyncio
+async def test_user_topnav_embargo_and_grouping(
+    sqlite_global_db: sqlalchemy.ext.asyncio.AsyncEngine, mock_app: MockApp, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    user._get_additional_admin_users.cache_clear()
+    monkeypatch.setattr("atr.config.get", lambda: MockConfig())
+    mock_app.extensions["admins"] = frozenset()
+    async with db.session() as data:
+        data.add(sql.Committee(key="example", name="Example", committee_members=["alice"], committers=["bob"]))
+        data.add(sql.Project(key="example", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+        data.add(sql.Project(key="example-two", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+        data.add(
+            sql.Release(
+                key="example-1.0.0",
+                phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+                project_key="example",
+                version="1.0.0",
+                created=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC),
+            )
+        )
+        data.add(
+            sql.Release(
+                key="example-2.0.0",
+                phase=sql.ReleasePhase.RELEASE_CANDIDATE,
+                project_key="example",
+                version="2.0.0",
+                created=datetime.datetime(2026, 2, 1, tzinfo=datetime.UTC),
+                expedited=True,
+            )
+        )
+        data.add(
+            sql.Release(
+                key="example-0.9.0",
+                phase=sql.ReleasePhase.RELEASE,
+                project_key="example",
+                version="0.9.0",
+                created=datetime.datetime(2025, 12, 1, tzinfo=datetime.UTC),
+            )
+        )
+        await data.commit()
+
+    unfinished, projects = await interaction.user_topnav("alice", False)
+    assert projects == [("example", "example"), ("example-two", "example-two")]
+    assert [(name, key) for name, key, _releases in unfinished] == [("example", "example")]
+    releases = unfinished[0][2]
+    assert [r.version for r in releases] == ["2.0.0", "1.0.0"]
+    assert releases[0].project.key == "example"
+
+    unfinished, _projects = await interaction.user_topnav("bob", False)
+    assert [r.version for r in unfinished[0][2]] == ["1.0.0"]
+
+    unfinished, _projects = await interaction.user_topnav("bob", True)
+    assert [r.version for r in unfinished[0][2]] == ["2.0.0", "1.0.0"]
+
+
+@pytest.mark.asyncio
+async def test_user_topnav_statement_count_constant(
+    sqlite_global_db: sqlalchemy.ext.asyncio.AsyncEngine, mock_app: MockApp, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    user._get_additional_admin_users.cache_clear()
+    monkeypatch.setattr("atr.config.get", lambda: MockConfig())
+    mock_app.extensions["admins"] = frozenset()
+    statements: list[str] = []
+    sqlalchemy.event.listen(
+        sqlite_global_db.sync_engine,
+        "before_cursor_execute",
+        lambda conn, cursor, statement, parameters, context, executemany: statements.append(statement),
+    )
+
+    async with db.session() as data:
+        data.add(sql.Committee(key="example", name="Example", committee_members=["alice"]))
+        data.add(sql.Project(key="example", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+        data.add(
+            sql.Release(
+                key="example-1.0.0",
+                phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+                project_key="example",
+                version="1.0.0",
+                created=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC),
+            )
+        )
+        await data.commit()
+
+    statements.clear()
+    await interaction.user_topnav("alice", False)
+    baseline = len(statements)
+
+    async with db.session() as data:
+        for number in (2, 3):
+            data.add(sql.Project(key=f"example-{number}", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+            data.add(
+                sql.Release(
+                    key=f"example-{number}-1.0.0",
+                    phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+                    project_key=f"example-{number}",
+                    version="1.0.0",
+                    created=datetime.datetime(2026, 1, number, tzinfo=datetime.UTC),
+                )
+            )
+        await data.commit()
+
+    statements.clear()
+    unfinished, projects = await interaction.user_topnav("alice", False)
+    assert len(projects) == 3
+    assert len(unfinished) == 3
+    assert len(statements) == baseline


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org