From 0cb9d299b6d182f0ec209ebe13dc3af612bcadc2 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 15 Jan 2025 15:57:53 -0500 Subject: [PATCH 01/16] Initial commit --- graphql_api/tests/test_owner.py | 43 +++++++++++++++++++++++++++ graphql_api/types/owner/owner.graphql | 8 +++++ graphql_api/types/owner/owner.py | 30 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index b626a07da6..04cac63523 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1202,3 +1202,46 @@ def test_fetch_available_plans_is_enterprise_plan(self): ] } } + + @patch("services.self_hosted.get_config") + def test_ai_enabled_repositories(self, get_config_mock): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + query = """{ + owner(username: "%s") { + aiEnabledRepos + } + } + + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["aiEnabledRepos"] is None + + + @patch("services.self_hosted.get_config") + def test_ai_enabled_repositories_app_not_configured(self, get_config_mock): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + query = """{ + owner(username: "%s") { + aiEnabledRepos + } + } + + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["aiEnabledRepos"] is None diff --git a/graphql_api/types/owner/owner.graphql b/graphql_api/types/owner/owner.graphql index 0f3b7c7413..0bf2749b85 100644 --- a/graphql_api/types/owner/owner.graphql +++ b/graphql_api/types/owner/owner.graphql @@ -39,6 +39,14 @@ type Owner { yaml: String aiFeaturesEnabled: Boolean! aiEnabledRepos: [String] + aiEnabledRepositories( + ordering: RepositoryOrdering + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): RepositoryConnection! @cost(complexity: 25, multipliers: ["first", "last"]) uploadTokenRequired: Boolean activatedUserCount: Int } diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index a268472156..0ffe63b904 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -396,3 +396,33 @@ def resolve_upload_token_required( @require_shared_account_or_part_of_org def resolve_activated_user_count(owner: Owner, info: GraphQLResolveInfo) -> int: return owner.activated_user_count + +@owner_bindable.field("aiEnabledRepositories") +def resolve_ai_enabled_repositories( + owner: Owner, + info: GraphQLResolveInfo, + ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, + **kwargs: Any, +) -> Coroutine[Any, Any, Connection]: + ai_features_app_install = GithubAppInstallation.objects.filter( + app_id=AI_FEATURES_GH_APP_ID, owner=owner + ).first() + + if not ai_features_app_install: + return None + + current_owner = info.context["request"].current_owner + queryset = Repository.objects.filter(author=owner).viewable_repos(current_owner) + + if ai_features_app_install.repository_service_ids: + queryset = queryset.filter( + service_id__in=ai_features_app_install.repository_service_ids + ) + + return queryset_to_connection( + queryset, + ordering=(ordering, RepositoryOrdering.ID), + ordering_direction=ordering_direction, + **kwargs, + ) \ No newline at end of file From 7c0bf381fbefdd691e42dc93bab368060ccb8793 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 11 Feb 2025 11:28:28 -0500 Subject: [PATCH 02/16] update --- graphql_api/types/owner/owner.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index d6a445828e..87b38746c6 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -400,12 +400,6 @@ def resolve_activated_user_count(owner: Owner, info: GraphQLResolveInfo) -> int: return owner.activated_user_count -@owner_bindable.field("billing") -@sync_to_async -@require_part_of_org -def resolve_billing(owner: Owner, info: GraphQLResolveInfo) -> dict | None: - return owner - @owner_bindable.field("billing") @sync_to_async @require_part_of_org From 91fd9cda5cbf4569da3f46d098c84dc79e52e957 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 12:51:55 -0500 Subject: [PATCH 03/16] Update tests --- graphql_api/tests/test_owner.py | 40 +++++++++++++++++++-------- graphql_api/types/owner/owner.graphql | 2 +- graphql_api/types/owner/owner.py | 8 ++++-- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index 8186ce3b9c..e97977369c 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1196,25 +1196,35 @@ def test_fetch_available_plans_is_enterprise_plan(self): @patch("services.self_hosted.get_config") def test_ai_enabled_repositories(self, get_config_mock): - current_org = OwnerFactory( - username="random-plan-user", - service="github", - ) - get_config_mock.return_value = [ {"service": "github", "ai_features_app_id": 12345}, ] + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=self.owner, + repository_service_ids=[], + installation_id=12345, + ) + + ai_app_installation.save() + query = """{ owner(username: "%s") { - aiEnabledRepos + aiEnabledRepositories(first: 20) { + edges { + node { + name + } + } + } } } - """ % (current_org.username) - data = self.gql_request(query, owner=current_org) - assert data["owner"]["aiEnabledRepos"] is None - + """ % (self.owner.username) + data = self.gql_request(query, owner=self.owner) + reps = paginate_connection(data["owner"]["aiEnabledRepositories"]) + assert reps == [{'name': 'a'}, {'name': 'b'}] @patch("services.self_hosted.get_config") def test_ai_enabled_repositories_app_not_configured(self, get_config_mock): @@ -1229,13 +1239,19 @@ def test_ai_enabled_repositories_app_not_configured(self, get_config_mock): query = """{ owner(username: "%s") { - aiEnabledRepos + aiEnabledRepositories { + edges { + node { + name + } + } + } } } """ % (current_org.username) data = self.gql_request(query, owner=current_org) - assert data["owner"]["aiEnabledRepos"] is None + assert data["owner"]["aiEnabledRepositories"] is None def test_fetch_owner_with_no_service(self): current_org = OwnerFactory( diff --git a/graphql_api/types/owner/owner.graphql b/graphql_api/types/owner/owner.graphql index 7af2008f17..b575deadf4 100644 --- a/graphql_api/types/owner/owner.graphql +++ b/graphql_api/types/owner/owner.graphql @@ -47,7 +47,7 @@ type Owner { after: String last: Int before: String - ): RepositoryConnection! @cost(complexity: 25, multipliers: ["first", "last"]) + ): RepositoryConnection @cost(complexity: 25, multipliers: ["first", "last"]) uploadTokenRequired: Boolean activatedUserCount: Int } diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index 87b38746c6..5d3ce091f0 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -32,6 +32,7 @@ Connection, build_connection_graphql, queryset_to_connection, + queryset_to_connection_sync, ) from graphql_api.helpers.mutation import ( require_part_of_org, @@ -406,7 +407,9 @@ def resolve_activated_user_count(owner: Owner, info: GraphQLResolveInfo) -> int: def resolve_billing(owner: Owner, info: GraphQLResolveInfo) -> dict | None: return owner + @owner_bindable.field("aiEnabledRepositories") +@sync_to_async def resolve_ai_enabled_repositories( owner: Owner, info: GraphQLResolveInfo, @@ -423,15 +426,14 @@ def resolve_ai_enabled_repositories( current_owner = info.context["request"].current_owner queryset = Repository.objects.filter(author=owner).viewable_repos(current_owner) - if ai_features_app_install.repository_service_ids: queryset = queryset.filter( service_id__in=ai_features_app_install.repository_service_ids ) - return queryset_to_connection( + return queryset_to_connection_sync( queryset, ordering=(ordering, RepositoryOrdering.ID), ordering_direction=ordering_direction, **kwargs, - ) \ No newline at end of file + ) From d047c5bb6b9250230cc961f82176265244dfd306 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 12:52:34 -0500 Subject: [PATCH 04/16] Update --- graphql_api/tests/test_owner.py | 4 ++-- utils/test_utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index e97977369c..9340faa91c 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1223,8 +1223,8 @@ def test_ai_enabled_repositories(self, get_config_mock): """ % (self.owner.username) data = self.gql_request(query, owner=self.owner) - reps = paginate_connection(data["owner"]["aiEnabledRepositories"]) - assert reps == [{'name': 'a'}, {'name': 'b'}] + repos = paginate_connection(data["owner"]["aiEnabledRepositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] @patch("services.self_hosted.get_config") def test_ai_enabled_repositories_app_not_configured(self, get_config_mock): diff --git a/utils/test_utils.py b/utils/test_utils.py index 6cac27e04e..6deeb8251a 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -45,10 +45,10 @@ def app(self) -> str: migrate_to = None def setUp(self) -> None: - assert self.migrate_from and self.migrate_to, ( - "TestCase '{}' must define migrate_from and migrate_to properties".format( - type(self).__name__ - ) + assert ( + self.migrate_from and self.migrate_to + ), "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ ) self.migrate_from = [(self.app, self.migrate_from)] self.migrate_to = [(self.app, self.migrate_to)] From 7111b1b234b8dceec3e3f600cf3e5c4df3c9fcea Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 12:53:48 -0500 Subject: [PATCH 05/16] Update --- utils/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/test_utils.py b/utils/test_utils.py index 6deeb8251a..859c602f23 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -45,10 +45,10 @@ def app(self) -> str: migrate_to = None def setUp(self) -> None: - assert ( - self.migrate_from and self.migrate_to - ), "TestCase '{}' must define migrate_from and migrate_to properties".format( - type(self).__name__ + assert self.migrate_from and self.migrate_to, ( + "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ + ) ) self.migrate_from = [(self.app, self.migrate_from)] self.migrate_to = [(self.app, self.migrate_to)] @@ -68,4 +68,4 @@ def setUp(self) -> None: self.apps = executor.loader.project_state(self.migrate_to).apps def setUpBeforeMigration(self, apps: Any) -> None: - pass + pass \ No newline at end of file From 46013652385d0fe29efecddc416811274e851018 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 12:56:09 -0500 Subject: [PATCH 06/16] Lint --- utils/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/test_utils.py b/utils/test_utils.py index 859c602f23..6cac27e04e 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -68,4 +68,4 @@ def setUp(self) -> None: self.apps = executor.loader.project_state(self.migrate_to).apps def setUpBeforeMigration(self, apps: Any) -> None: - pass \ No newline at end of file + pass From 333ff4cc01c57e6fc509a05752d8a67e085d1252 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 13:00:20 -0500 Subject: [PATCH 07/16] Update return type --- graphql_api/types/owner/owner.py | 2 +- utils/test_utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index b8755b84fa..ee31fac42c 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -416,7 +416,7 @@ def resolve_ai_enabled_repositories( ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID, ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, **kwargs: Any, -) -> Coroutine[Any, Any, Connection]: +) -> Coroutine[Any, Any, Connection] | None: ai_features_app_install = GithubAppInstallation.objects.filter( app_id=AI_FEATURES_GH_APP_ID, owner=owner ).first() diff --git a/utils/test_utils.py b/utils/test_utils.py index 6cac27e04e..6deeb8251a 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -45,10 +45,10 @@ def app(self) -> str: migrate_to = None def setUp(self) -> None: - assert self.migrate_from and self.migrate_to, ( - "TestCase '{}' must define migrate_from and migrate_to properties".format( - type(self).__name__ - ) + assert ( + self.migrate_from and self.migrate_to + ), "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ ) self.migrate_from = [(self.app, self.migrate_from)] self.migrate_to = [(self.app, self.migrate_to)] From 3763fb7d48af685b2b53a2828b56d7e2edd822bc Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 18 Feb 2025 13:03:31 -0500 Subject: [PATCH 08/16] CI headaches --- utils/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/test_utils.py b/utils/test_utils.py index 6deeb8251a..859c602f23 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -45,10 +45,10 @@ def app(self) -> str: migrate_to = None def setUp(self) -> None: - assert ( - self.migrate_from and self.migrate_to - ), "TestCase '{}' must define migrate_from and migrate_to properties".format( - type(self).__name__ + assert self.migrate_from and self.migrate_to, ( + "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ + ) ) self.migrate_from = [(self.app, self.migrate_from)] self.migrate_to = [(self.app, self.migrate_to)] @@ -68,4 +68,4 @@ def setUp(self) -> None: self.apps = executor.loader.project_state(self.migrate_to).apps def setUpBeforeMigration(self, apps: Any) -> None: - pass + pass \ No newline at end of file From d987ef7bd7838e04c01462bf1fbba9c76f9457b0 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 11:02:26 -0500 Subject: [PATCH 09/16] Try new approach --- graphql_api/actions/repository.py | 40 +++++++++++-- graphql_api/tests/test_owner.py | 59 ------------------- .../inputs/repository_set_filters.graphql | 1 + graphql_api/types/owner/owner.graphql | 8 --- graphql_api/types/owner/owner.py | 35 +---------- 5 files changed, 37 insertions(+), 106 deletions(-) diff --git a/graphql_api/actions/repository.py b/graphql_api/actions/repository.py index 25dc3a2780..d864651353 100644 --- a/graphql_api/actions/repository.py +++ b/graphql_api/actions/repository.py @@ -3,14 +3,16 @@ import sentry_sdk from django.db.models import QuerySet -from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.codecov_auth.models import GithubAppInstallation, Owner from shared.django_apps.core.models import Repository +from graphql_api.types.owner.owner import AI_FEATURES_GH_APP_ID + log = logging.getLogger(__name__) def apply_filters_to_queryset( - queryset: QuerySet, filters: dict[str, Any] | None + queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner ) -> QuerySet: filters = filters or {} term = filters.get("term") @@ -18,6 +20,7 @@ def apply_filters_to_queryset( activated = filters.get("activated") repo_names = filters.get("repo_names") is_public = filters.get("is_public") + ai_enabled = filters.get("ai_enabled") if repo_names: queryset = queryset.filter(name__in=repo_names) @@ -29,6 +32,25 @@ def apply_filters_to_queryset( queryset = queryset.filter(active=active) if is_public is not None: queryset = queryset.filter(private=not is_public) + if is_public is not None: + queryset = queryset.filter(private=not is_public) + if ai_enabled is not None: + queryset = filter_queryset_by_ai_enabled_repos(queryset, owner) + return queryset + + +def filter_queryset_by_ai_enabled_repos(queryset: QuerySet, owner: Owner) -> QuerySet: + ai_features_app_install = GithubAppInstallation.objects.filter( + app_id=AI_FEATURES_GH_APP_ID, owner=owner + ).first() + + if not ai_features_app_install: + return Repository.objects.none() + + if ai_features_app_install.repository_service_ids: + queryset = queryset.filter( + service_id__in=ai_features_app_install.repository_service_ids + ) return queryset @@ -43,14 +65,20 @@ def list_repository_for_owner( ) -> QuerySet: queryset = Repository.objects.viewable_repos(current_owner) + ai_enabled_filter = filters.get("ai_enabled") + + if ai_enabled_filter: + return filter_queryset_by_ai_enabled_repos(queryset, owner) + if exclude_okta_enforced_repos: queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) - queryset = ( - queryset.with_recent_coverage().with_latest_commit_at().filter(author=owner) - ) + if not ai_enabled_filter: + queryset = ( + queryset.with_recent_coverage().with_latest_commit_at().filter(author=owner) + ) - queryset = apply_filters_to_queryset(queryset, filters) + queryset = apply_filters_to_queryset(queryset, filters, owner) return queryset diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index 78145dec52..097b880903 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1194,65 +1194,6 @@ def test_fetch_available_plans_is_enterprise_plan(self): } } - @patch("services.self_hosted.get_config") - def test_ai_enabled_repositories(self, get_config_mock): - get_config_mock.return_value = [ - {"service": "github", "ai_features_app_id": 12345}, - ] - - ai_app_installation = GithubAppInstallation( - name="ai-features", - owner=self.owner, - repository_service_ids=[], - installation_id=12345, - ) - - ai_app_installation.save() - - query = """{ - owner(username: "%s") { - aiEnabledRepositories(first: 20) { - edges { - node { - name - } - } - } - } - } - - """ % (self.owner.username) - data = self.gql_request(query, owner=self.owner) - repos = paginate_connection(data["owner"]["aiEnabledRepositories"]) - assert repos == [{"name": "a"}, {"name": "b"}] - - @patch("services.self_hosted.get_config") - def test_ai_enabled_repositories_app_not_configured(self, get_config_mock): - current_org = OwnerFactory( - username="random-plan-user", - service="github", - ) - - get_config_mock.return_value = [ - {"service": "github", "ai_features_app_id": 12345}, - ] - - query = """{ - owner(username: "%s") { - aiEnabledRepositories { - edges { - node { - name - } - } - } - } - } - - """ % (current_org.username) - data = self.gql_request(query, owner=current_org) - assert data["owner"]["aiEnabledRepositories"] is None - def test_fetch_owner_with_no_service(self): current_org = OwnerFactory( username="random-plan-user", diff --git a/graphql_api/types/inputs/repository_set_filters.graphql b/graphql_api/types/inputs/repository_set_filters.graphql index a3f195d500..4669688398 100644 --- a/graphql_api/types/inputs/repository_set_filters.graphql +++ b/graphql_api/types/inputs/repository_set_filters.graphql @@ -4,4 +4,5 @@ input RepositorySetFilters { active: Boolean activated: Boolean isPublic: Boolean + aiEnabled: Boolean } diff --git a/graphql_api/types/owner/owner.graphql b/graphql_api/types/owner/owner.graphql index b575deadf4..99715411f4 100644 --- a/graphql_api/types/owner/owner.graphql +++ b/graphql_api/types/owner/owner.graphql @@ -40,14 +40,6 @@ type Owner { yaml: String aiFeaturesEnabled: Boolean! aiEnabledRepos: [String] - aiEnabledRepositories( - ordering: RepositoryOrdering - orderingDirection: OrderingDirection - first: Int - after: String - last: Int - before: String - ): RepositoryConnection @cost(complexity: 25, multipliers: ["first", "last"]) uploadTokenRequired: Boolean activatedUserCount: Int } diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index ee31fac42c..708b8a8c0a 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -31,7 +31,6 @@ from graphql_api.helpers.connection import ( Connection, build_connection_graphql, - queryset_to_connection, queryset_to_connection_sync, ) from graphql_api.helpers.mutation import ( @@ -55,6 +54,7 @@ @owner_bindable.field("repositories") +@sync_to_async def resolve_repositories( owner: Owner, info: GraphQLResolveInfo, @@ -77,7 +77,7 @@ def resolve_repositories( current_owner, owner, filters, okta_account_auths, exclude_okta_enforced_repos ) - return queryset_to_connection( + return queryset_to_connection_sync( queryset, ordering=(ordering, RepositoryOrdering.ID), ordering_direction=ordering_direction, @@ -406,34 +406,3 @@ def resolve_activated_user_count(owner: Owner, info: GraphQLResolveInfo) -> int: @require_part_of_org def resolve_billing(owner: Owner, info: GraphQLResolveInfo) -> dict | None: return owner - - -@owner_bindable.field("aiEnabledRepositories") -@sync_to_async -def resolve_ai_enabled_repositories( - owner: Owner, - info: GraphQLResolveInfo, - ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID, - ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, - **kwargs: Any, -) -> Coroutine[Any, Any, Connection] | None: - ai_features_app_install = GithubAppInstallation.objects.filter( - app_id=AI_FEATURES_GH_APP_ID, owner=owner - ).first() - - if not ai_features_app_install: - return None - - current_owner = info.context["request"].current_owner - queryset = Repository.objects.filter(author=owner).viewable_repos(current_owner) - if ai_features_app_install.repository_service_ids: - queryset = queryset.filter( - service_id__in=ai_features_app_install.repository_service_ids - ) - - return queryset_to_connection_sync( - queryset, - ordering=(ordering, RepositoryOrdering.ID), - ordering_direction=ordering_direction, - **kwargs, - ) From de1f3de1dd75dac5d718c31fa38feda0aa5cdef3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 14:36:07 -0500 Subject: [PATCH 10/16] Tests --- graphql_api/actions/repository.py | 94 ++++++++++++------------------- graphql_api/tests/test_owner.py | 29 ++++++++++ utils/test_utils.py | 2 +- 3 files changed, 67 insertions(+), 58 deletions(-) diff --git a/graphql_api/actions/repository.py b/graphql_api/actions/repository.py index d864651353..fa7ebefb68 100644 --- a/graphql_api/actions/repository.py +++ b/graphql_api/actions/repository.py @@ -6,53 +6,44 @@ from shared.django_apps.codecov_auth.models import GithubAppInstallation, Owner from shared.django_apps.core.models import Repository -from graphql_api.types.owner.owner import AI_FEATURES_GH_APP_ID +from utils.config import get_config log = logging.getLogger(__name__) +AI_FEATURES_GH_APP_ID = get_config("github", "ai_features_app_id") -def apply_filters_to_queryset( - queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner -) -> QuerySet: +def basic_filters(queryset: QuerySet, filters: dict[str, Any]) -> QuerySet: filters = filters or {} - term = filters.get("term") - active = filters.get("active") - activated = filters.get("activated") - repo_names = filters.get("repo_names") - is_public = filters.get("is_public") - ai_enabled = filters.get("ai_enabled") - - if repo_names: + if repo_names := filters.get("repo_names"): queryset = queryset.filter(name__in=repo_names) - if term: + if term := filters.get("term"): queryset = queryset.filter(name__contains=term) - if activated is not None: - queryset = queryset.filter(activated=activated) - if active is not None: - queryset = queryset.filter(active=active) - if is_public is not None: - queryset = queryset.filter(private=not is_public) - if is_public is not None: - queryset = queryset.filter(private=not is_public) - if ai_enabled is not None: - queryset = filter_queryset_by_ai_enabled_repos(queryset, owner) + for field in ("activated", "active"): + if filters.get(field) is not None: + queryset = queryset.filter(**{field: filters[field]}) + if filters.get("is_public") is not None: + queryset = queryset.filter(private=not filters["is_public"]) return queryset def filter_queryset_by_ai_enabled_repos(queryset: QuerySet, owner: Owner) -> QuerySet: - ai_features_app_install = GithubAppInstallation.objects.filter( + install = GithubAppInstallation.objects.filter( app_id=AI_FEATURES_GH_APP_ID, owner=owner ).first() - - if not ai_features_app_install: + if not install: return Repository.objects.none() + if install.repository_service_ids: + queryset = queryset.filter(service_id__in=install.repository_service_ids) + return queryset - if ai_features_app_install.repository_service_ids: - queryset = queryset.filter( - service_id__in=ai_features_app_install.repository_service_ids - ) - return queryset +def apply_filters( + queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner +) -> QuerySet: + filters = filters or {} + if filters.get("ai_enabled"): + return filter_queryset_by_ai_enabled_repos(queryset, owner) + return basic_filters(queryset, filters) @sentry_sdk.trace @@ -63,23 +54,14 @@ def list_repository_for_owner( okta_account_auths: list[int], exclude_okta_enforced_repos: bool = True, ) -> QuerySet: - queryset = Repository.objects.viewable_repos(current_owner) - - ai_enabled_filter = filters.get("ai_enabled") - - if ai_enabled_filter: - return filter_queryset_by_ai_enabled_repos(queryset, owner) - + filters = filters or {} + qs = Repository.objects.viewable_repos(current_owner) + if filters.get("ai_enabled"): + return filter_queryset_by_ai_enabled_repos(qs, owner) if exclude_okta_enforced_repos: - queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) - - if not ai_enabled_filter: - queryset = ( - queryset.with_recent_coverage().with_latest_commit_at().filter(author=owner) - ) - - queryset = apply_filters_to_queryset(queryset, filters, owner) - return queryset + qs = qs.exclude_accounts_enforced_okta(okta_account_auths) + qs = qs.with_recent_coverage().with_latest_commit_at().filter(author=owner) + return basic_filters(qs, filters) @sentry_sdk.trace @@ -89,16 +71,14 @@ def search_repos( okta_account_auths: list[int], exclude_okta_enforced_repos: bool = True, ) -> QuerySet: - authors_from = [current_owner.ownerid] + (current_owner.organizations or []) - queryset = Repository.objects.viewable_repos(current_owner) - + filters = filters or {} + authors = [current_owner.ownerid] + (current_owner.organizations or []) + qs = Repository.objects.viewable_repos(current_owner) if exclude_okta_enforced_repos: - queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) - - queryset = ( - queryset.with_recent_coverage() + qs = qs.exclude_accounts_enforced_okta(okta_account_auths) + qs = ( + qs.with_recent_coverage() .with_latest_commit_at() - .filter(author__ownerid__in=authors_from) + .filter(author__ownerid__in=authors) ) - queryset = apply_filters_to_queryset(queryset, filters) - return queryset + return apply_filters(qs, filters, current_owner) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index 097b880903..ab61d545ad 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1209,3 +1209,32 @@ def test_fetch_owner_with_no_service(self): """ % (current_org.username) data = self.gql_request(query, owner=current_org, provider="", with_errors=True) assert data == {"data": {"owner": None}} + + def test_fetch_repositories_ai_features_enabled(self): + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=self.owner, + repository_service_ids=[], + installation_id=12345, + ) + + ai_app_installation.save() + query = query_repositories % ( + self.owner.username, + "(filters: { aiEnabled: true })", + "", + ) + + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] + + def test_fetch_repositories_ai_features_enabled_no_app_install(self): + query = query_repositories % ( + self.owner.username, + "(filters: { aiEnabled: true })", + "", + ) + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [] diff --git a/utils/test_utils.py b/utils/test_utils.py index 859c602f23..6cac27e04e 100644 --- a/utils/test_utils.py +++ b/utils/test_utils.py @@ -68,4 +68,4 @@ def setUp(self) -> None: self.apps = executor.loader.project_state(self.migrate_to).apps def setUpBeforeMigration(self, apps: Any) -> None: - pass \ No newline at end of file + pass From 1ef6d080a039afa8c27c857ce0b780d44cb2b3b1 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 14:41:18 -0500 Subject: [PATCH 11/16] Undo --- graphql_api/actions/repository.py | 89 +++++++++++++++++++------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/graphql_api/actions/repository.py b/graphql_api/actions/repository.py index fa7ebefb68..7274adcc37 100644 --- a/graphql_api/actions/repository.py +++ b/graphql_api/actions/repository.py @@ -5,45 +5,53 @@ from django.db.models import QuerySet from shared.django_apps.codecov_auth.models import GithubAppInstallation, Owner from shared.django_apps.core.models import Repository - from utils.config import get_config + log = logging.getLogger(__name__) AI_FEATURES_GH_APP_ID = get_config("github", "ai_features_app_id") -def basic_filters(queryset: QuerySet, filters: dict[str, Any]) -> QuerySet: +def apply_filters_to_queryset( + queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner +) -> QuerySet: filters = filters or {} - if repo_names := filters.get("repo_names"): + term = filters.get("term") + active = filters.get("active") + activated = filters.get("activated") + repo_names = filters.get("repo_names") + is_public = filters.get("is_public") + ai_enabled = filters.get("ai_enabled") + + if repo_names: queryset = queryset.filter(name__in=repo_names) - if term := filters.get("term"): + if term: queryset = queryset.filter(name__contains=term) - for field in ("activated", "active"): - if filters.get(field) is not None: - queryset = queryset.filter(**{field: filters[field]}) - if filters.get("is_public") is not None: - queryset = queryset.filter(private=not filters["is_public"]) + if activated is not None: + queryset = queryset.filter(activated=activated) + if active is not None: + queryset = queryset.filter(active=active) + if is_public is not None: + queryset = queryset.filter(private=not is_public) + if ai_enabled is not None: + queryset = filter_queryset_by_ai_enabled_repos(queryset, owner) return queryset def filter_queryset_by_ai_enabled_repos(queryset: QuerySet, owner: Owner) -> QuerySet: - install = GithubAppInstallation.objects.filter( + ai_features_app_install = GithubAppInstallation.objects.filter( app_id=AI_FEATURES_GH_APP_ID, owner=owner ).first() - if not install: + + if not ai_features_app_install: return Repository.objects.none() - if install.repository_service_ids: - queryset = queryset.filter(service_id__in=install.repository_service_ids) - return queryset + if ai_features_app_install.repository_service_ids: + queryset = queryset.filter( + service_id__in=ai_features_app_install.repository_service_ids + ) -def apply_filters( - queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner -) -> QuerySet: - filters = filters or {} - if filters.get("ai_enabled"): - return filter_queryset_by_ai_enabled_repos(queryset, owner) - return basic_filters(queryset, filters) + return queryset @sentry_sdk.trace @@ -54,14 +62,23 @@ def list_repository_for_owner( okta_account_auths: list[int], exclude_okta_enforced_repos: bool = True, ) -> QuerySet: + queryset = Repository.objects.viewable_repos(current_owner) filters = filters or {} - qs = Repository.objects.viewable_repos(current_owner) - if filters.get("ai_enabled"): - return filter_queryset_by_ai_enabled_repos(qs, owner) + ai_enabled_filter = filters.get("ai_enabled") + + if ai_enabled_filter: + return filter_queryset_by_ai_enabled_repos(queryset, owner) + if exclude_okta_enforced_repos: - qs = qs.exclude_accounts_enforced_okta(okta_account_auths) - qs = qs.with_recent_coverage().with_latest_commit_at().filter(author=owner) - return basic_filters(qs, filters) + queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) + + if not ai_enabled_filter: + queryset = ( + queryset.with_recent_coverage().with_latest_commit_at().filter(author=owner) + ) + + queryset = apply_filters_to_queryset(queryset, filters, owner) + return queryset @sentry_sdk.trace @@ -71,14 +88,16 @@ def search_repos( okta_account_auths: list[int], exclude_okta_enforced_repos: bool = True, ) -> QuerySet: - filters = filters or {} - authors = [current_owner.ownerid] + (current_owner.organizations or []) - qs = Repository.objects.viewable_repos(current_owner) + authors_from = [current_owner.ownerid] + (current_owner.organizations or []) + queryset = Repository.objects.viewable_repos(current_owner) + if exclude_okta_enforced_repos: - qs = qs.exclude_accounts_enforced_okta(okta_account_auths) - qs = ( - qs.with_recent_coverage() + queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) + + queryset = ( + queryset.with_recent_coverage() .with_latest_commit_at() - .filter(author__ownerid__in=authors) + .filter(author__ownerid__in=authors_from) ) - return apply_filters(qs, filters, current_owner) + queryset = apply_filters_to_queryset(queryset, filters) + return queryset From 4dd984ddda78be229bfd862dba7cd128873c8157 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 14:42:05 -0500 Subject: [PATCH 12/16] Sort --- graphql_api/actions/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql_api/actions/repository.py b/graphql_api/actions/repository.py index 7274adcc37..9257bb2487 100644 --- a/graphql_api/actions/repository.py +++ b/graphql_api/actions/repository.py @@ -5,8 +5,8 @@ from django.db.models import QuerySet from shared.django_apps.codecov_auth.models import GithubAppInstallation, Owner from shared.django_apps.core.models import Repository -from utils.config import get_config +from utils.config import get_config log = logging.getLogger(__name__) AI_FEATURES_GH_APP_ID = get_config("github", "ai_features_app_id") From c0fd454834d6cc2942c8e3ce45dd0d799eccaf3d Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 14:45:57 -0500 Subject: [PATCH 13/16] Update params --- graphql_api/actions/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql_api/actions/repository.py b/graphql_api/actions/repository.py index 9257bb2487..ed491e655b 100644 --- a/graphql_api/actions/repository.py +++ b/graphql_api/actions/repository.py @@ -13,7 +13,7 @@ def apply_filters_to_queryset( - queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner + queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner | None = None ) -> QuerySet: filters = filters or {} term = filters.get("term") From 662df84a6013e72ae78b6f5254da9718e5223465 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 19:46:55 -0500 Subject: [PATCH 14/16] Try fix flakes --- graphql_api/tests/test_owner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index ab61d545ad..a581d931c6 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -685,13 +685,13 @@ def test_owner_available_plans(self): } """ % (current_org.username) data = self.gql_request(query, owner=current_org) - assert data["owner"]["availablePlans"] == [ + assert sorted(data["owner"]["availablePlans"]) == sorted([ {"value": "users-pr-inappm"}, {"value": "users-pr-inappy"}, {"value": "users-teamm"}, {"value": "users-teamy"}, {"value": DEFAULT_FREE_PLAN}, - ] + ]) def test_owner_query_with_no_service(self): current_org = OwnerFactory( @@ -1142,7 +1142,7 @@ def test_fetch_available_plans_is_enterprise_plan(self): } """ % (current_org.username) data = self.gql_request(query, owner=current_org) - assert data == { + assert sorted(data) == sorted({ "owner": { "availablePlans": [ { @@ -1192,7 +1192,7 @@ def test_fetch_available_plans_is_enterprise_plan(self): }, ] } - } + }) def test_fetch_owner_with_no_service(self): current_org = OwnerFactory( From 0c82122791ca9c6d9fc11a6442c383a9f0d03c07 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 20:57:59 -0500 Subject: [PATCH 15/16] Update tests --- graphql_api/tests/test_owner.py | 108 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index a581d931c6..77012d8647 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -685,13 +685,15 @@ def test_owner_available_plans(self): } """ % (current_org.username) data = self.gql_request(query, owner=current_org) - assert sorted(data["owner"]["availablePlans"]) == sorted([ + expected_plans = [ {"value": "users-pr-inappm"}, {"value": "users-pr-inappy"}, {"value": "users-teamm"}, {"value": "users-teamy"}, {"value": DEFAULT_FREE_PLAN}, - ]) + ] + for plan in expected_plans: + self.assertIn(plan, data["owner"]["availablePlans"]) def test_owner_query_with_no_service(self): current_org = OwnerFactory( @@ -1120,13 +1122,13 @@ def test_fetch_activated_user_count_when_not_in_org_but_has_shared_account(self) data = self.gql_request(query, owner=owner) assert data["owner"]["activatedUserCount"] == 2 + def test_fetch_available_plans_is_enterprise_plan(self): current_org = OwnerFactory( username="random-plan-user", service="github", plan=DEFAULT_FREE_PLAN, ) - query = """{ owner(username: "%s") { availablePlans { @@ -1142,57 +1144,55 @@ def test_fetch_available_plans_is_enterprise_plan(self): } """ % (current_org.username) data = self.gql_request(query, owner=current_org) - assert sorted(data) == sorted({ - "owner": { - "availablePlans": [ - { - "value": "users-pr-inappm", - "isEnterprisePlan": False, - "isProPlan": True, - "isTeamPlan": False, - "isSentryPlan": False, - "isFreePlan": False, - "isTrialPlan": False, - }, - { - "value": "users-pr-inappy", - "isEnterprisePlan": False, - "isProPlan": True, - "isTeamPlan": False, - "isSentryPlan": False, - "isFreePlan": False, - "isTrialPlan": False, - }, - { - "value": "users-teamm", - "isEnterprisePlan": False, - "isProPlan": False, - "isTeamPlan": True, - "isSentryPlan": False, - "isFreePlan": False, - "isTrialPlan": False, - }, - { - "value": "users-teamy", - "isEnterprisePlan": False, - "isProPlan": False, - "isTeamPlan": True, - "isSentryPlan": False, - "isFreePlan": False, - "isTrialPlan": False, - }, - { - "value": DEFAULT_FREE_PLAN, - "isEnterprisePlan": False, - "isProPlan": False, - "isTeamPlan": True, - "isSentryPlan": False, - "isFreePlan": True, - "isTrialPlan": False, - }, - ] - } - }) + expected_plans = [ + { + "value": "users-pr-inappm", + "isEnterprisePlan": False, + "isProPlan": True, + "isTeamPlan": False, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-pr-inappy", + "isEnterprisePlan": False, + "isProPlan": True, + "isTeamPlan": False, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-teamm", + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-teamy", + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": DEFAULT_FREE_PLAN, + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": True, + "isTrialPlan": False, + }, + ] + for plan in expected_plans: + self.assertIn(plan, data["owner"]["availablePlans"]) def test_fetch_owner_with_no_service(self): current_org = OwnerFactory( From 82bbbc9f2ddfebf243f1fe525cb9d5b14f84013c Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 21 Feb 2025 09:28:07 -0500 Subject: [PATCH 16/16] lint --- graphql_api/tests/test_owner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql_api/tests/test_owner.py b/graphql_api/tests/test_owner.py index 77012d8647..a6f32b5e24 100644 --- a/graphql_api/tests/test_owner.py +++ b/graphql_api/tests/test_owner.py @@ -1122,7 +1122,6 @@ def test_fetch_activated_user_count_when_not_in_org_but_has_shared_account(self) data = self.gql_request(query, owner=owner) assert data["owner"]["activatedUserCount"] == 2 - def test_fetch_available_plans_is_enterprise_plan(self): current_org = OwnerFactory( username="random-plan-user",