Skip to content

Commit e67f42b

Browse files
cvxluoJesse-Box
authored andcommitted
ref(integrations): order GroupIntegrationDetails endpoint better (#102535)
Refs https://linear.app/getsentry/issue/RTC-1148/publish-groupintegrationdetailsendpoint-api In preparation for making this endpoint public, we want to rearrange the endpoint a little to make it easier to read (order GET -> POST -> PUT -> DELETE) and add some docstrings.
1 parent 68c690e commit e67f42b

File tree

1 file changed

+160
-149
lines changed

1 file changed

+160
-149
lines changed

src/sentry/issues/endpoints/group_integration_details.py

Lines changed: 160 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -73,60 +73,16 @@ def serialize(
7373
class GroupIntegrationDetailsEndpoint(GroupEndpoint):
7474
owner = ApiOwner.ECOSYSTEM
7575
publish_status = {
76-
"DELETE": ApiPublishStatus.UNKNOWN,
7776
"GET": ApiPublishStatus.UNKNOWN,
78-
"PUT": ApiPublishStatus.UNKNOWN,
7977
"POST": ApiPublishStatus.UNKNOWN,
78+
"PUT": ApiPublishStatus.UNKNOWN,
79+
"DELETE": ApiPublishStatus.UNKNOWN,
8080
}
8181

82-
def _has_issue_feature(self, organization, user) -> bool:
83-
has_issue_basic = features.has(
84-
"organizations:integrations-issue-basic", organization, actor=user
85-
)
86-
87-
has_issue_sync = features.has(
88-
"organizations:integrations-issue-sync", organization, actor=user
89-
)
90-
91-
return has_issue_sync or has_issue_basic
92-
93-
def _has_issue_feature_on_integration(self, integration: RpcIntegration) -> bool:
94-
return integration.has_feature(
95-
feature=IntegrationFeatures.ISSUE_BASIC
96-
) or integration.has_feature(feature=IntegrationFeatures.ISSUE_SYNC)
97-
98-
def _get_installation(
99-
self, integration: RpcIntegration, organization_id: int
100-
) -> IssueBasicIntegration:
101-
installation = integration.get_installation(organization_id=organization_id)
102-
if not isinstance(installation, IssueBasicIntegration):
103-
raise ValueError(installation)
104-
return installation
105-
106-
def create_issue_activity(
107-
self,
108-
request: Request,
109-
group: Group,
110-
installation: IssueBasicIntegration,
111-
external_issue: ExternalIssue,
112-
new: bool,
113-
):
114-
issue_information = {
115-
"title": external_issue.title,
116-
"provider": installation.model.get_provider().name,
117-
"location": installation.get_issue_url(external_issue.key),
118-
"label": installation.get_issue_display_name(external_issue) or external_issue.key,
119-
"new": new,
120-
}
121-
Activity.objects.create(
122-
project=group.project,
123-
group=group,
124-
type=ActivityType.CREATE_ISSUE.value,
125-
user_id=request.user.id,
126-
data=issue_information,
127-
)
128-
12982
def get(self, request: Request, group, integration_id) -> Response:
83+
"""
84+
Retrieves the config needed to either link or create an external issue for a group.
85+
"""
13086
if not request.user.is_authenticated:
13187
return Response(status=400)
13288
elif not self._has_issue_feature(group.organization, request.user):
@@ -175,8 +131,112 @@ def get(self, request: Request, group, integration_id) -> Response:
175131
)
176132
)
177133

178-
# was thinking put for link an existing issue, post for create new issue?
134+
def post(self, request: Request, group, integration_id) -> Response:
135+
"""
136+
Creates a new external issue and links it to a group.
137+
"""
138+
if not request.user.is_authenticated:
139+
return Response(status=400)
140+
elif not self._has_issue_feature(group.organization, request.user):
141+
return Response({"detail": MISSING_FEATURE_MESSAGE}, status=400)
142+
143+
organization_id = group.project.organization_id
144+
result = integration_service.organization_context(
145+
organization_id=organization_id, integration_id=integration_id
146+
)
147+
integration = result.integration
148+
org_integration = result.organization_integration
149+
if not integration or not org_integration:
150+
return Response(status=404)
151+
152+
if not self._has_issue_feature_on_integration(integration):
153+
return Response(
154+
{"detail": "This feature is not supported for this integration."}, status=400
155+
)
156+
157+
installation = self._get_installation(integration, organization_id)
158+
159+
with ProjectManagementEvent(
160+
action_type=ProjectManagementActionType.CREATE_EXTERNAL_ISSUE_VIA_ISSUE_DETAIL,
161+
integration=integration,
162+
).capture() as lifecycle:
163+
lifecycle.add_extras(
164+
{
165+
"provider": integration.provider,
166+
"integration_id": integration.id,
167+
}
168+
)
169+
170+
try:
171+
data = installation.create_issue(request.data)
172+
except IntegrationConfigurationError as exc:
173+
lifecycle.record_halt(exc)
174+
return Response({"non_field_errors": [str(exc)]}, status=400)
175+
except IntegrationFormError as exc:
176+
lifecycle.record_halt(exc)
177+
return Response(exc.field_errors, status=400)
178+
except IntegrationError as e:
179+
lifecycle.record_failure(e)
180+
return Response({"non_field_errors": [str(e)]}, status=400)
181+
except IntegrationProviderError as exc:
182+
lifecycle.record_halt(exc)
183+
return Response(
184+
{
185+
"detail": f"Something went wrong while communicating with {integration.provider}"
186+
},
187+
status=503,
188+
)
189+
190+
external_issue_key = installation.make_external_key(data)
191+
external_issue, created = ExternalIssue.objects.get_or_create(
192+
organization_id=organization_id,
193+
integration_id=integration.id,
194+
key=external_issue_key,
195+
defaults={
196+
"title": data.get("title"),
197+
"description": data.get("description"),
198+
"metadata": data.get("metadata"),
199+
},
200+
)
201+
202+
try:
203+
with transaction.atomic(router.db_for_write(GroupLink)):
204+
GroupLink.objects.create(
205+
group_id=group.id,
206+
project_id=group.project_id,
207+
linked_type=GroupLink.LinkedType.issue,
208+
linked_id=external_issue.id,
209+
relationship=GroupLink.Relationship.references,
210+
)
211+
except IntegrityError:
212+
return Response({"detail": "That issue is already linked"}, status=400)
213+
214+
if created:
215+
integration_issue_created.send_robust(
216+
integration=integration,
217+
organization=group.project.organization,
218+
user=request.user,
219+
sender=self.__class__,
220+
)
221+
installation.store_issue_last_defaults(group.project, request.user, request.data)
222+
223+
self.create_issue_activity(request, group, installation, external_issue, new=True)
224+
225+
# TODO(jess): return serialized issue
226+
url = data.get("url") or installation.get_issue_url(external_issue.key)
227+
context = {
228+
"id": external_issue.id,
229+
"key": external_issue.key,
230+
"url": url,
231+
"integrationId": external_issue.integration_id,
232+
"displayName": installation.get_issue_display_name(external_issue),
233+
}
234+
return Response(context, status=201)
235+
179236
def put(self, request: Request, group, integration_id) -> Response:
237+
"""
238+
Links an existing external issue to a group.
239+
"""
180240
if not request.user.is_authenticated:
181241
return Response(status=400)
182242
elif not self._has_issue_feature(group.organization, request.user):
@@ -276,106 +336,10 @@ def put(self, request: Request, group, integration_id) -> Response:
276336
}
277337
return Response(context, status=201)
278338

279-
def post(self, request: Request, group, integration_id) -> Response:
280-
if not request.user.is_authenticated:
281-
return Response(status=400)
282-
elif not self._has_issue_feature(group.organization, request.user):
283-
return Response({"detail": MISSING_FEATURE_MESSAGE}, status=400)
284-
285-
organization_id = group.project.organization_id
286-
result = integration_service.organization_context(
287-
organization_id=organization_id, integration_id=integration_id
288-
)
289-
integration = result.integration
290-
org_integration = result.organization_integration
291-
if not integration or not org_integration:
292-
return Response(status=404)
293-
294-
if not self._has_issue_feature_on_integration(integration):
295-
return Response(
296-
{"detail": "This feature is not supported for this integration."}, status=400
297-
)
298-
299-
installation = self._get_installation(integration, organization_id)
300-
301-
with ProjectManagementEvent(
302-
action_type=ProjectManagementActionType.CREATE_EXTERNAL_ISSUE_VIA_ISSUE_DETAIL,
303-
integration=integration,
304-
).capture() as lifecycle:
305-
lifecycle.add_extras(
306-
{
307-
"provider": integration.provider,
308-
"integration_id": integration.id,
309-
}
310-
)
311-
312-
try:
313-
data = installation.create_issue(request.data)
314-
except IntegrationConfigurationError as exc:
315-
lifecycle.record_halt(exc)
316-
return Response({"non_field_errors": [str(exc)]}, status=400)
317-
except IntegrationFormError as exc:
318-
lifecycle.record_halt(exc)
319-
return Response(exc.field_errors, status=400)
320-
except IntegrationError as e:
321-
lifecycle.record_failure(e)
322-
return Response({"non_field_errors": [str(e)]}, status=400)
323-
except IntegrationProviderError as exc:
324-
lifecycle.record_halt(exc)
325-
return Response(
326-
{
327-
"detail": f"Something went wrong while communicating with {integration.provider}"
328-
},
329-
status=503,
330-
)
331-
332-
external_issue_key = installation.make_external_key(data)
333-
external_issue, created = ExternalIssue.objects.get_or_create(
334-
organization_id=organization_id,
335-
integration_id=integration.id,
336-
key=external_issue_key,
337-
defaults={
338-
"title": data.get("title"),
339-
"description": data.get("description"),
340-
"metadata": data.get("metadata"),
341-
},
342-
)
343-
344-
try:
345-
with transaction.atomic(router.db_for_write(GroupLink)):
346-
GroupLink.objects.create(
347-
group_id=group.id,
348-
project_id=group.project_id,
349-
linked_type=GroupLink.LinkedType.issue,
350-
linked_id=external_issue.id,
351-
relationship=GroupLink.Relationship.references,
352-
)
353-
except IntegrityError:
354-
return Response({"detail": "That issue is already linked"}, status=400)
355-
356-
if created:
357-
integration_issue_created.send_robust(
358-
integration=integration,
359-
organization=group.project.organization,
360-
user=request.user,
361-
sender=self.__class__,
362-
)
363-
installation.store_issue_last_defaults(group.project, request.user, request.data)
364-
365-
self.create_issue_activity(request, group, installation, external_issue, new=True)
366-
367-
# TODO(jess): return serialized issue
368-
url = data.get("url") or installation.get_issue_url(external_issue.key)
369-
context = {
370-
"id": external_issue.id,
371-
"key": external_issue.key,
372-
"url": url,
373-
"integrationId": external_issue.integration_id,
374-
"displayName": installation.get_issue_display_name(external_issue),
375-
}
376-
return Response(context, status=201)
377-
378339
def delete(self, request: Request, group, integration_id) -> Response:
340+
"""
341+
Deletes a link between a group and an external issue.
342+
"""
379343
if not self._has_issue_feature(group.organization, request.user):
380344
return Response({"detail": MISSING_FEATURE_MESSAGE}, status=400)
381345

@@ -417,3 +381,50 @@ def delete(self, request: Request, group, integration_id) -> Response:
417381
external_issue.delete()
418382

419383
return Response(status=204)
384+
385+
def _has_issue_feature(self, organization, user) -> bool:
386+
has_issue_basic = features.has(
387+
"organizations:integrations-issue-basic", organization, actor=user
388+
)
389+
390+
has_issue_sync = features.has(
391+
"organizations:integrations-issue-sync", organization, actor=user
392+
)
393+
394+
return has_issue_sync or has_issue_basic
395+
396+
def _has_issue_feature_on_integration(self, integration: RpcIntegration) -> bool:
397+
return integration.has_feature(
398+
feature=IntegrationFeatures.ISSUE_BASIC
399+
) or integration.has_feature(feature=IntegrationFeatures.ISSUE_SYNC)
400+
401+
def _get_installation(
402+
self, integration: RpcIntegration, organization_id: int
403+
) -> IssueBasicIntegration:
404+
installation = integration.get_installation(organization_id=organization_id)
405+
if not isinstance(installation, IssueBasicIntegration):
406+
raise ValueError(installation)
407+
return installation
408+
409+
def create_issue_activity(
410+
self,
411+
request: Request,
412+
group: Group,
413+
installation: IssueBasicIntegration,
414+
external_issue: ExternalIssue,
415+
new: bool,
416+
):
417+
issue_information = {
418+
"title": external_issue.title,
419+
"provider": installation.model.get_provider().name,
420+
"location": installation.get_issue_url(external_issue.key),
421+
"label": installation.get_issue_display_name(external_issue) or external_issue.key,
422+
"new": new,
423+
}
424+
Activity.objects.create(
425+
project=group.project,
426+
group=group,
427+
type=ActivityType.CREATE_ISSUE.value,
428+
user_id=request.user.id,
429+
data=issue_information,
430+
)

0 commit comments

Comments
 (0)