44import re
55import uuid
66
7+ from django .conf import settings
78from django .contrib .contenttypes .fields import GenericForeignKey
89from django .contrib .contenttypes .fields import GenericRelation
910from django .contrib .contenttypes .models import ContentType
1011from django .db import models
1112from django .db import transaction
13+ from django .urls import reverse
1214from django .utils .crypto import get_random_string
1315from django .utils .safestring import mark_safe
1416from django .utils .translation import gettext_lazy as _
@@ -240,7 +242,16 @@ def get(self, *args, **kwargs):
240242 original = super ().get (* args , ** kwargs )
241243 return self ._get_subclass_replacement (original )
242244
243- def subclass (self , instance ):
245+ def subclass (self , instance = None ):
246+ """
247+ Return a subclass or list of subclasses integrations.
248+
249+ If an instance was passed in, return a single subclasses integration
250+ instance. If this is a queryset or manager, render the list as a list
251+ using the integration subsclasses.
252+ """
253+ if instance is None :
254+ return [self ._get_subclass_replacement (_instance ) for _instance in self ]
244255 return self ._get_subclass_replacement (instance )
245256
246257 def create (self , ** kwargs ):
@@ -263,6 +274,7 @@ def create(self, **kwargs):
263274class Integration (TimeStampedModel ):
264275 """Inbound webhook integration for projects."""
265276
277+ GITHUBAPP = "githubapp"
266278 GITHUB_WEBHOOK = "github_webhook"
267279 BITBUCKET_WEBHOOK = "bitbucket_webhook"
268280 GITLAB_WEBHOOK = "gitlab_webhook"
@@ -275,7 +287,9 @@ class Integration(TimeStampedModel):
275287 (API_WEBHOOK , _ ("Generic API incoming webhook" )),
276288 )
277289
278- INTEGRATIONS = WEBHOOK_INTEGRATIONS
290+ REMOTE_ONLY_INTEGRATIONS = ((GITHUBAPP , _ ("GitHub App" )),)
291+
292+ INTEGRATIONS = WEBHOOK_INTEGRATIONS + REMOTE_ONLY_INTEGRATIONS
279293
280294 project = models .ForeignKey (
281295 Project ,
@@ -307,6 +321,8 @@ class Integration(TimeStampedModel):
307321
308322 # Integration attributes
309323 has_sync = False
324+ is_remote_only = False
325+ is_active = True
310326
311327 def __str__ (self ):
312328 return self .get_integration_type_display ()
@@ -316,6 +332,9 @@ def save(self, *args, **kwargs):
316332 self .secret = get_random_string (length = 32 )
317333 super ().save (* args , ** kwargs )
318334
335+ def get_absolute_url (self ) -> str :
336+ return reverse ("projects_integrations_detail" , args = (self .project .slug , self .pk ))
337+
319338
320339class GitHubWebhook (Integration ):
321340 integration_type_id = Integration .GITHUB_WEBHOOK
@@ -332,6 +351,47 @@ def can_sync(self):
332351 return False
333352
334353
354+ class GitHubAppIntegration (Integration ):
355+ integration_type_id = Integration .GITHUBAPP
356+ has_sync = False
357+ is_remote_only = True
358+
359+ class Meta :
360+ proxy = True
361+
362+ def get_absolute_url (self ) -> str | None :
363+ """
364+ Get URL of the GHA installation page.
365+
366+ Instead of showing a link to the integration details page, for GHA
367+ projects we show a link in the UI to the GHA installation page for the
368+ installation used by the project.
369+ """
370+ # If the GHA is disconnected we'll disonnect the remote repository and
371+ # so we won't have a URL to the installation page the project should be
372+ # using. We might want to store this on the model later so a repository
373+ # that is removed from the installation can still link to the
374+ # installation the project was _previously_ using.
375+ try :
376+ installation_id = self .project .remote_repository .github_app_installation .installation_id
377+ return f"https://github.com/apps/{ settings .GITHUB_APP_NAME } /installations/{ installation_id } "
378+ except AttributeError :
379+ return None
380+
381+ @property
382+ def is_active (self ) -> bool :
383+ """
384+ Is the GHA connection active for this project?
385+
386+ This assumes that the status of the GHA connect will be reflected as
387+ soon as there is an event that might disconnect the GHA on GitHub's
388+ side -- uninstalling the app or revoking permission to the repository.
389+ We listen for these events and should disconnect the remote
390+ repository, but would leave this integration.
391+ """
392+ return self .project .is_github_app_project
393+
394+
335395class BitbucketWebhook (Integration ):
336396 integration_type_id = Integration .BITBUCKET_WEBHOOK
337397 has_sync = True
0 commit comments