Skip to content

Commit b79c998

Browse files
committed
feat: Migrate organizations list to control
1 parent 8246cf3 commit b79c998

File tree

2 files changed

+167
-114
lines changed

2 files changed

+167
-114
lines changed

src/sentry/api/endpoints/organization_index.py

Lines changed: 16 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from django.conf import settings
44
from django.db import IntegrityError
5-
from django.db.models import Count, Q
65
from drf_spectacular.utils import extend_schema
76
from rest_framework import serializers, status
87
from rest_framework.request import Request
@@ -13,7 +12,6 @@
1312
from sentry.api.api_publish_status import ApiPublishStatus
1413
from sentry.api.base import Endpoint, region_silo_endpoint
1514
from sentry.api.bases.organization import OrganizationPermission
16-
from sentry.api.paginator import DateTimePaginator, OffsetPaginator
1715
from sentry.api.serializers import serialize
1816
from sentry.api.serializers.models.organization import (
1917
BaseOrganizationSerializer,
@@ -24,21 +22,16 @@
2422
from sentry.apidocs.parameters import CursorQueryParam, OrganizationParams
2523
from sentry.apidocs.utils import inline_sentry_response_serializer
2624
from sentry.auth.superuser import is_active_superuser
27-
from sentry.db.models.query import in_iexact
2825
from sentry.hybridcloud.rpc import IDEMPOTENCY_KEY_LENGTH
29-
from sentry.models.organization import Organization, OrganizationStatus
30-
from sentry.models.organizationmember import OrganizationMember
31-
from sentry.models.projectplatform import ProjectPlatform
32-
from sentry.search.utils import tokenize_query
26+
from sentry.models.organization import Organization
27+
from sentry.organizations.services.organization import organization_service
3328
from sentry.services.organization import (
3429
OrganizationOptions,
3530
OrganizationProvisioningOptions,
3631
PostProvisionOptions,
3732
)
3833
from sentry.services.organization.provisioning import organization_provisioning_service
3934
from sentry.signals import org_setup_complete, terms_accepted
40-
from sentry.users.services.user.service import user_service
41-
from sentry.utils.pagination_factory import PaginatorLike
4235

4336
logger = logging.getLogger(__name__)
4437

@@ -95,113 +88,22 @@ def get(self, request: Request) -> Response:
9588
Return a list of organizations available to the authenticated session in a region.
9689
This is particularly useful for requests with a user bound context. For API key-based requests this will only return the organization that belongs to the key.
9790
"""
98-
owner_only = request.GET.get("owner") in ("1", "true")
99-
100-
queryset = Organization.objects.distinct()
101-
102-
if request.auth and not request.user.is_authenticated:
103-
if hasattr(request.auth, "project"):
104-
queryset = queryset.filter(id=request.auth.project.organization_id)
105-
elif request.auth.organization_id is not None:
106-
queryset = queryset.filter(id=request.auth.organization_id)
107-
108-
elif owner_only and request.user.is_authenticated:
109-
# This is used when closing an account
110-
111-
# also fetches organizations in which you are a member of an owner team
112-
queryset = Organization.objects.get_organizations_where_user_is_owner(
113-
user_id=request.user.id
114-
)
115-
org_results = []
116-
for org in sorted(queryset, key=lambda x: x.name):
117-
# O(N) query
118-
org_results.append(
119-
{"organization": serialize(org), "singleOwner": org.has_single_owner()}
120-
)
121-
122-
return Response(org_results)
123-
124-
elif not (is_active_superuser(request) and request.GET.get("show") == "all"):
125-
queryset = queryset.filter(
126-
id__in=OrganizationMember.objects.filter(user_id=request.user.id).values(
127-
"organization"
128-
)
129-
)
130-
if request.auth and request.auth.organization_id is not None and queryset.count() > 1:
131-
# If a token is limited to one organization, this endpoint should only return that one organization
132-
queryset = queryset.filter(id=request.auth.organization_id)
133-
134-
query = request.GET.get("query")
135-
if query:
136-
tokens = tokenize_query(query)
137-
for key, value in tokens.items():
138-
if key == "query":
139-
query_value = " ".join(value)
140-
user_ids = {
141-
u.id
142-
for u in user_service.get_many_by_email(
143-
emails=[query_value], is_verified=False
144-
)
145-
}
146-
queryset = queryset.filter(
147-
Q(name__icontains=query_value)
148-
| Q(slug__icontains=query_value)
149-
| Q(member_set__user_id__in=user_ids)
150-
)
151-
elif key == "slug":
152-
queryset = queryset.filter(in_iexact("slug", value))
153-
elif key == "email":
154-
user_ids = {
155-
u.id
156-
for u in user_service.get_many_by_email(emails=value, is_verified=False)
157-
}
158-
queryset = queryset.filter(Q(member_set__user_id__in=user_ids))
159-
elif key == "platform":
160-
queryset = queryset.filter(
161-
project__in=ProjectPlatform.objects.filter(platform__in=value).values(
162-
"project_id"
163-
)
164-
)
165-
elif key == "id":
166-
queryset = queryset.filter(id__in=value)
167-
elif key == "status":
168-
try:
169-
queryset = queryset.filter(
170-
status__in=[OrganizationStatus[v.upper()] for v in value]
171-
)
172-
except KeyError:
173-
queryset = queryset.none()
174-
elif key == "member_id":
175-
queryset = queryset.filter(
176-
id__in=OrganizationMember.objects.filter(id__in=value).values(
177-
"organization"
178-
)
179-
)
180-
else:
181-
queryset = queryset.none()
182-
183-
sort_by = request.GET.get("sortBy")
184-
paginator_cls: type[PaginatorLike]
185-
if sort_by == "members":
186-
queryset = queryset.annotate(member_count=Count("member_set"))
187-
order_by = "-member_count"
188-
paginator_cls = OffsetPaginator
189-
elif sort_by == "projects":
190-
queryset = queryset.annotate(project_count=Count("project"))
191-
order_by = "-project_count"
192-
paginator_cls = OffsetPaginator
193-
else:
194-
order_by = "-date_added"
195-
paginator_cls = DateTimePaginator
196-
197-
return self.paginate(
198-
request=request,
199-
queryset=queryset,
200-
order_by=order_by,
201-
on_results=lambda x: serialize(x, request.user),
202-
paginator_cls=paginator_cls,
91+
result = organization_service.list_organizations(
92+
actor_user_id=request.user.id,
93+
owner_only=request.GET.get("owner") in ("1", "true"),
94+
query=request.GET.get("query"),
95+
sort_by=request.GET.get("sortBy"),
96+
show=request.GET.get("show"),
97+
actor_organization_id=request.auth.organization_id if request.auth else None,
98+
actor_project_id=request.auth.project_id if request.auth else None,
99+
actor_is_active_superuser=is_active_superuser(request),
100+
as_user=request.user,
203101
)
204102

103+
response = Response(result["results"])
104+
self.add_cursor_headers(request, response, result["cursor"])
105+
return response
106+
205107
# XXX: endpoint useless for end-users as it needs user context.
206108
def post(self, request: Request) -> Response:
207109
"""
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import logging
2+
from enum import Enum
3+
4+
from django.db.models import Count, Q
5+
from rest_framework.response import Response
6+
7+
from sentry.api.paginator import DateTimePaginator, OffsetPaginator
8+
from sentry.api.serializers import serialize
9+
from sentry.db.models.query import in_iexact
10+
from sentry.models.organization import Organization, OrganizationStatus
11+
from sentry.models.organizationmember import OrganizationMember
12+
from sentry.models.projectplatform import ProjectPlatform
13+
from sentry.search.utils import tokenize_query
14+
from sentry.users.services.user.service import user_service
15+
from sentry.utils.cursors import Cursor
16+
from sentry.utils.pagination_factory import PaginatorLike
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class SortBy(Enum):
22+
MEMBERS = "members"
23+
PROJECTS = "projects"
24+
DATE_ADDED = "date"
25+
26+
27+
class Show(Enum):
28+
ALL = "all"
29+
30+
31+
def list_organizations(
32+
*,
33+
actor_user_id: int,
34+
owner_only: bool = False,
35+
query: str | None = None,
36+
show: Show | None = None,
37+
sort_by: SortBy | None = "date",
38+
cursor: Cursor | None = None,
39+
per_page: int = 100,
40+
# actor specific stuff
41+
actor_is_active_superuser: bool = False,
42+
actor_organization_id: int | None = None,
43+
actor_project_id: int | None = None,
44+
):
45+
"""
46+
Return a list of organizations available to the authenticated session in a region.
47+
This is particularly useful for requests with a user bound context. For API key-based requests this will only return the organization that belongs to the key.
48+
"""
49+
queryset = Organization.objects.distinct()
50+
51+
if actor_project_id is not None:
52+
queryset = queryset.filter(id=actor_project_id.organization_id)
53+
elif actor_organization_id is not None:
54+
queryset = queryset.filter(id=actor_organization_id)
55+
56+
if owner_only:
57+
# This is used when closing an account
58+
# also fetches organizations in which you are a member of an owner team
59+
queryset = Organization.objects.get_organizations_where_user_is_owner(user_id=actor_user_id)
60+
org_results = []
61+
for org in sorted(queryset, key=lambda x: x.name):
62+
# O(N) query
63+
org_results.append(
64+
{"organization": serialize(org), "singleOwner": org.has_single_owner()}
65+
)
66+
67+
return Response(org_results)
68+
69+
elif not (actor_is_active_superuser and show == "all"):
70+
queryset = queryset.filter(
71+
id__in=OrganizationMember.objects.filter(user_id=actor_user_id).values("organization")
72+
)
73+
if actor_organization_id is not None and queryset.count() > 1:
74+
# If a token is limited to one organization, this endpoint should only return that one organization
75+
queryset = queryset.filter(id=actor_organization_id)
76+
77+
if query:
78+
tokens = tokenize_query(query)
79+
for key, value in tokens.items():
80+
if key == "query":
81+
query_value = " ".join(value)
82+
user_ids = {
83+
u.id
84+
for u in user_service.get_many_by_email(emails=[query_value], is_verified=False)
85+
}
86+
queryset = queryset.filter(
87+
Q(name__icontains=query_value)
88+
| Q(slug__icontains=query_value)
89+
| Q(member_set__user_id__in=user_ids)
90+
)
91+
elif key == "slug":
92+
queryset = queryset.filter(in_iexact("slug", value))
93+
elif key == "email":
94+
user_ids = {
95+
u.id for u in user_service.get_many_by_email(emails=value, is_verified=False)
96+
}
97+
queryset = queryset.filter(Q(member_set__user_id__in=user_ids))
98+
elif key == "platform":
99+
queryset = queryset.filter(
100+
project__in=ProjectPlatform.objects.filter(platform__in=value).values(
101+
"project_id"
102+
)
103+
)
104+
elif key == "id":
105+
queryset = queryset.filter(id__in=value)
106+
elif key == "status":
107+
try:
108+
queryset = queryset.filter(
109+
status__in=[OrganizationStatus[v.upper()] for v in value]
110+
)
111+
except KeyError:
112+
queryset = queryset.none()
113+
elif key == "member_id":
114+
queryset = queryset.filter(
115+
id__in=OrganizationMember.objects.filter(id__in=value).values("organization")
116+
)
117+
else:
118+
queryset = queryset.none()
119+
120+
paginator_cls: type[PaginatorLike]
121+
if sort_by == "members":
122+
queryset = queryset.annotate(member_count=Count("member_set"))
123+
order_by = "-member_count"
124+
paginator_cls = OffsetPaginator
125+
elif sort_by == "projects":
126+
queryset = queryset.annotate(project_count=Count("project"))
127+
order_by = "-project_count"
128+
paginator_cls = OffsetPaginator
129+
else:
130+
order_by = "-date_added"
131+
paginator_cls = DateTimePaginator
132+
133+
paginator = paginator_cls()
134+
cursor_result = paginator.get_result(
135+
limit=per_page,
136+
cursor=cursor,
137+
order_by=order_by,
138+
)
139+
140+
# TODO: missing user to serialize
141+
results = [serialize(org) for org in cursor_result.results]
142+
143+
return {
144+
"results": results,
145+
"cursor": {
146+
"next": cursor_result.next,
147+
"prev": cursor_result.prev,
148+
"hits": cursor_result.hits,
149+
"max_hits": cursor_result.max_hits,
150+
},
151+
}

0 commit comments

Comments
 (0)