4
4
import re
5
5
import uuid
6
6
7
+ from django .conf import settings
7
8
from django .contrib .contenttypes .fields import GenericForeignKey
8
9
from django .contrib .contenttypes .fields import GenericRelation
9
10
from django .contrib .contenttypes .models import ContentType
10
11
from django .db import models
11
12
from django .db import transaction
13
+ from django .urls import reverse
12
14
from django .utils .crypto import get_random_string
13
15
from django .utils .safestring import mark_safe
14
16
from django .utils .translation import gettext_lazy as _
@@ -240,7 +242,16 @@ def get(self, *args, **kwargs):
240
242
original = super ().get (* args , ** kwargs )
241
243
return self ._get_subclass_replacement (original )
242
244
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 ]
244
255
return self ._get_subclass_replacement (instance )
245
256
246
257
def create (self , ** kwargs ):
@@ -263,6 +274,7 @@ def create(self, **kwargs):
263
274
class Integration (TimeStampedModel ):
264
275
"""Inbound webhook integration for projects."""
265
276
277
+ GITHUBAPP = "githubapp"
266
278
GITHUB_WEBHOOK = "github_webhook"
267
279
BITBUCKET_WEBHOOK = "bitbucket_webhook"
268
280
GITLAB_WEBHOOK = "gitlab_webhook"
@@ -275,7 +287,9 @@ class Integration(TimeStampedModel):
275
287
(API_WEBHOOK , _ ("Generic API incoming webhook" )),
276
288
)
277
289
278
- INTEGRATIONS = WEBHOOK_INTEGRATIONS
290
+ REMOTE_ONLY_INTEGRATIONS = ((GITHUBAPP , _ ("GitHub App" )),)
291
+
292
+ INTEGRATIONS = WEBHOOK_INTEGRATIONS + REMOTE_ONLY_INTEGRATIONS
279
293
280
294
project = models .ForeignKey (
281
295
Project ,
@@ -307,6 +321,8 @@ class Integration(TimeStampedModel):
307
321
308
322
# Integration attributes
309
323
has_sync = False
324
+ is_remote_only = False
325
+ is_active = True
310
326
311
327
def __str__ (self ):
312
328
return self .get_integration_type_display ()
@@ -316,6 +332,9 @@ def save(self, *args, **kwargs):
316
332
self .secret = get_random_string (length = 32 )
317
333
super ().save (* args , ** kwargs )
318
334
335
+ def get_absolute_url (self ) -> str :
336
+ return reverse ("projects_integrations_detail" , args = (self .project .slug , self .pk ))
337
+
319
338
320
339
class GitHubWebhook (Integration ):
321
340
integration_type_id = Integration .GITHUB_WEBHOOK
@@ -332,6 +351,47 @@ def can_sync(self):
332
351
return False
333
352
334
353
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
+
335
395
class BitbucketWebhook (Integration ):
336
396
integration_type_id = Integration .BITBUCKET_WEBHOOK
337
397
has_sync = True
0 commit comments