diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index adbc36064306..bda8caacc680 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -76,7 +76,12 @@ OrganizationInvitationFactory, OrganizationRoleFactory, ) -from ...common.db.packaging import ProjectFactory, RoleFactory, RoleInvitationFactory +from ...common.db.packaging import ( + ProjectFactory, + ReleaseFactory, + RoleFactory, + RoleInvitationFactory, +) class TestFailedLoginView: @@ -146,6 +151,36 @@ def test_returns_user(self, db_request): user = UserFactory.create() assert views.profile(user, db_request) == {"user": user, "projects": []} + def test_user_profile_queries_once_for_all_projects( + self, db_request, query_recorder + ): + user = UserFactory.create() + projects = ProjectFactory.create_batch(3) + for project in projects: + # associate the user to each project as role: owner + RoleFactory.create(user=user, project=project) + # Add some releases, with time skew to ensure the ordering is correct + ReleaseFactory.create( + project=project, created=project.created + datetime.timedelta(minutes=1) + ) + ReleaseFactory.create( + project=project, created=project.created + datetime.timedelta(minutes=2) + ) + # Add a prerelease, shouldn't affect any results + ReleaseFactory.create( + project=project, + created=project.created + datetime.timedelta(minutes=3), + is_prerelease=True, + ) + + with query_recorder: + response = views.profile(user, db_request) + + assert response["user"] == user + assert len(response["projects"]) == 3 + # Two queries, one for the user (via context), one for their projects + assert len(query_recorder.queries) == 2 + class TestAccountsSearch: def test_unauthenticated_raises_401(self): diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index b7cab36cf2da..64cab34b8e0a 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -30,6 +30,7 @@ from pyramid.interfaces import ISecurityPolicy from pyramid.security import forget, remember from pyramid.view import view_config, view_defaults +from sqlalchemy import and_, func, select from sqlalchemy.exc import NoResultFound from webauthn.helpers import bytes_to_base64url from webob.multidict import MultiDict @@ -168,14 +169,53 @@ def profile(user, request): if user.username != request.matchdict.get("username", user.username): return HTTPMovedPermanently(request.current_route_path(username=user.username)) - projects = ( - request.db.query(Project) - .filter(Project.users.contains(user)) - .join(Project.releases) + # Query only for the necessary data that the template needs + # Subquery to get the latest release date for each project associated with the user + latest_releases_subquery = ( + select( + Release.project_id, func.max(Release.created).label("latest_release_date") + ) + .join(Role, Release.project_id == Role.project_id) + .where(Role.user_id == user.id) + .group_by(Release.project_id) + .subquery() + ) + # Main query to select the latest releases + stmt = ( + select( + Project.name, + Project.normalized_name, + Release.created, + Release.summary, + ) + .join(Role, Project.id == Role.project_id) + .outerjoin( + latest_releases_subquery, + Project.id == latest_releases_subquery.c.project_id, + ) + .outerjoin( + Release, + and_( + Release.project_id == latest_releases_subquery.c.project_id, + Release.created == latest_releases_subquery.c.latest_release_date, + ), + ) + .where(Role.user_id == user.id) + .distinct() .order_by(Release.created.desc()) - .all() ) + # Construct the list of projects with their latest releases from query results + projects = [ + { + "name": row.name, + "normalized_name": row.normalized_name, + "created": row.created, + "summary": row.summary, + } + for row in request.db.execute(stmt) + ] + return {"user": user, "projects": projects} diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 72cc7f6e449b..31ff0daa0472 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -119,194 +119,194 @@ msgstr "" msgid "The username isn't valid. Try again." msgstr "" -#: warehouse/accounts/views.py:119 +#: warehouse/accounts/views.py:120 msgid "" "There have been too many unsuccessful login attempts. You have been " "locked out for {}. Please try again later." msgstr "" -#: warehouse/accounts/views.py:136 +#: warehouse/accounts/views.py:137 msgid "" "Too many emails have been added to this account without verifying them. " "Check your inbox and follow the verification links. (IP: ${ip})" msgstr "" -#: warehouse/accounts/views.py:148 +#: warehouse/accounts/views.py:149 msgid "" "Too many password resets have been requested for this account without " "completing them. Check your inbox and follow the verification links. (IP:" " ${ip})" msgstr "" -#: warehouse/accounts/views.py:330 warehouse/accounts/views.py:399 -#: warehouse/accounts/views.py:401 warehouse/accounts/views.py:430 -#: warehouse/accounts/views.py:432 warehouse/accounts/views.py:538 +#: warehouse/accounts/views.py:370 warehouse/accounts/views.py:439 +#: warehouse/accounts/views.py:441 warehouse/accounts/views.py:470 +#: warehouse/accounts/views.py:472 warehouse/accounts/views.py:578 msgid "Invalid or expired two factor login." msgstr "" -#: warehouse/accounts/views.py:393 +#: warehouse/accounts/views.py:433 msgid "Already authenticated" msgstr "" -#: warehouse/accounts/views.py:473 +#: warehouse/accounts/views.py:513 msgid "Successful WebAuthn assertion" msgstr "" -#: warehouse/accounts/views.py:569 warehouse/manage/views/__init__.py:873 +#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:873 msgid "Recovery code accepted. The supplied code cannot be used again." msgstr "" -#: warehouse/accounts/views.py:661 +#: warehouse/accounts/views.py:701 msgid "" "New user registration temporarily disabled. See https://pypi.org/help" "#admin-intervention for details." msgstr "" -#: warehouse/accounts/views.py:799 +#: warehouse/accounts/views.py:839 msgid "Expired token: request a new password reset link" msgstr "" -#: warehouse/accounts/views.py:801 +#: warehouse/accounts/views.py:841 msgid "Invalid token: request a new password reset link" msgstr "" -#: warehouse/accounts/views.py:803 warehouse/accounts/views.py:904 -#: warehouse/accounts/views.py:1008 warehouse/accounts/views.py:1177 +#: warehouse/accounts/views.py:843 warehouse/accounts/views.py:944 +#: warehouse/accounts/views.py:1048 warehouse/accounts/views.py:1217 msgid "Invalid token: no token supplied" msgstr "" -#: warehouse/accounts/views.py:807 +#: warehouse/accounts/views.py:847 msgid "Invalid token: not a password reset token" msgstr "" -#: warehouse/accounts/views.py:812 +#: warehouse/accounts/views.py:852 msgid "Invalid token: user not found" msgstr "" -#: warehouse/accounts/views.py:822 +#: warehouse/accounts/views.py:862 msgid "Invalid token: user has logged in since this token was requested" msgstr "" -#: warehouse/accounts/views.py:840 +#: warehouse/accounts/views.py:880 msgid "" "Invalid token: password has already been changed since this token was " "requested" msgstr "" -#: warehouse/accounts/views.py:872 +#: warehouse/accounts/views.py:912 msgid "You have reset your password" msgstr "" -#: warehouse/accounts/views.py:900 +#: warehouse/accounts/views.py:940 msgid "Expired token: request a new email verification link" msgstr "" -#: warehouse/accounts/views.py:902 +#: warehouse/accounts/views.py:942 msgid "Invalid token: request a new email verification link" msgstr "" -#: warehouse/accounts/views.py:908 +#: warehouse/accounts/views.py:948 msgid "Invalid token: not an email verification token" msgstr "" -#: warehouse/accounts/views.py:917 +#: warehouse/accounts/views.py:957 msgid "Email not found" msgstr "" -#: warehouse/accounts/views.py:920 +#: warehouse/accounts/views.py:960 msgid "Email already verified" msgstr "" -#: warehouse/accounts/views.py:937 +#: warehouse/accounts/views.py:977 msgid "You can now set this email as your primary address" msgstr "" -#: warehouse/accounts/views.py:941 +#: warehouse/accounts/views.py:981 msgid "This is your primary address" msgstr "" -#: warehouse/accounts/views.py:946 +#: warehouse/accounts/views.py:986 msgid "Email address ${email_address} verified. ${confirm_message}." msgstr "" -#: warehouse/accounts/views.py:1004 +#: warehouse/accounts/views.py:1044 msgid "Expired token: request a new organization invitation" msgstr "" -#: warehouse/accounts/views.py:1006 +#: warehouse/accounts/views.py:1046 msgid "Invalid token: request a new organization invitation" msgstr "" -#: warehouse/accounts/views.py:1012 +#: warehouse/accounts/views.py:1052 msgid "Invalid token: not an organization invitation token" msgstr "" -#: warehouse/accounts/views.py:1016 +#: warehouse/accounts/views.py:1056 msgid "Organization invitation is not valid." msgstr "" -#: warehouse/accounts/views.py:1025 +#: warehouse/accounts/views.py:1065 msgid "Organization invitation no longer exists." msgstr "" -#: warehouse/accounts/views.py:1076 +#: warehouse/accounts/views.py:1116 msgid "Invitation for '${organization_name}' is declined." msgstr "" -#: warehouse/accounts/views.py:1139 +#: warehouse/accounts/views.py:1179 msgid "You are now ${role} of the '${organization_name}' organization." msgstr "" -#: warehouse/accounts/views.py:1173 +#: warehouse/accounts/views.py:1213 msgid "Expired token: request a new project role invitation" msgstr "" -#: warehouse/accounts/views.py:1175 +#: warehouse/accounts/views.py:1215 msgid "Invalid token: request a new project role invitation" msgstr "" -#: warehouse/accounts/views.py:1181 +#: warehouse/accounts/views.py:1221 msgid "Invalid token: not a collaboration invitation token" msgstr "" -#: warehouse/accounts/views.py:1185 +#: warehouse/accounts/views.py:1225 msgid "Role invitation is not valid." msgstr "" -#: warehouse/accounts/views.py:1200 +#: warehouse/accounts/views.py:1240 msgid "Role invitation no longer exists." msgstr "" -#: warehouse/accounts/views.py:1231 +#: warehouse/accounts/views.py:1271 msgid "Invitation for '${project_name}' is declined." msgstr "" -#: warehouse/accounts/views.py:1297 +#: warehouse/accounts/views.py:1337 msgid "You are now ${role} of the '${project_name}' project." msgstr "" -#: warehouse/accounts/views.py:1548 warehouse/accounts/views.py:1791 +#: warehouse/accounts/views.py:1588 warehouse/accounts/views.py:1831 #: warehouse/manage/views/__init__.py:1409 msgid "" "Trusted publishing is temporarily disabled. See https://pypi.org/help" "#admin-intervention for details." msgstr "" -#: warehouse/accounts/views.py:1569 +#: warehouse/accounts/views.py:1609 msgid "disabled. See https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/accounts/views.py:1585 +#: warehouse/accounts/views.py:1625 msgid "" "You must have a verified email in order to register a pending trusted " "publisher. See https://pypi.org/help#openid-connect for details." msgstr "" -#: warehouse/accounts/views.py:1598 +#: warehouse/accounts/views.py:1638 msgid "You can't register more than 3 pending trusted publishers at once." msgstr "" -#: warehouse/accounts/views.py:1614 warehouse/manage/views/__init__.py:1464 +#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1464 #: warehouse/manage/views/__init__.py:1577 #: warehouse/manage/views/__init__.py:1689 #: warehouse/manage/views/__init__.py:1799 @@ -315,29 +315,29 @@ msgid "" "again later." msgstr "" -#: warehouse/accounts/views.py:1625 warehouse/manage/views/__init__.py:1478 +#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1478 #: warehouse/manage/views/__init__.py:1591 #: warehouse/manage/views/__init__.py:1703 #: warehouse/manage/views/__init__.py:1813 msgid "The trusted publisher could not be registered" msgstr "" -#: warehouse/accounts/views.py:1639 +#: warehouse/accounts/views.py:1679 msgid "" "This trusted publisher has already been registered. Please contact PyPI's" " admins if this wasn't intentional." msgstr "" -#: warehouse/accounts/views.py:1666 +#: warehouse/accounts/views.py:1706 msgid "Registered a new pending publisher to create " msgstr "" -#: warehouse/accounts/views.py:1805 warehouse/accounts/views.py:1818 -#: warehouse/accounts/views.py:1825 +#: warehouse/accounts/views.py:1845 warehouse/accounts/views.py:1858 +#: warehouse/accounts/views.py:1865 msgid "Invalid publisher ID" msgstr "" -#: warehouse/accounts/views.py:1831 +#: warehouse/accounts/views.py:1871 msgid "Removed trusted publisher for project " msgstr "" diff --git a/warehouse/templates/accounts/profile.html b/warehouse/templates/accounts/profile.html index e23bc952ef26..9b1ac8437d66 100644 --- a/warehouse/templates/accounts/profile.html +++ b/warehouse/templates/accounts/profile.html @@ -80,9 +80,9 @@
{{ project.releases[0].summary }}
+{{ project.summary }}
{% endfor %} {% else %}