Skip to content

Commit e80b0da

Browse files
authored
Merge pull request #6033 from saadmk11/user-communication
Send site notification on Build status reporting failure and follow DRY
2 parents 28a8303 + 7efd763 commit e80b0da

File tree

7 files changed

+106
-53
lines changed

7 files changed

+106
-53
lines changed

readthedocs/builds/models.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from readthedocs.projects.constants import (
2323
BITBUCKET_COMMIT_URL,
2424
BITBUCKET_URL,
25+
GITHUB_BRAND,
2526
GITHUB_COMMIT_URL,
2627
GITHUB_URL,
2728
GITHUB_PULL_REQUEST_URL,
@@ -843,19 +844,11 @@ def is_external(self):
843844
@property
844845
def external_version_name(self):
845846
if self.is_external:
846-
try:
847-
if self.project.remote_repository.account.provider == 'github':
848-
return GITHUB_EXTERNAL_VERSION_NAME
849-
# TODO: Add External Version Name for other Git Providers
850-
except RemoteRepository.DoesNotExist:
851-
log.info('Remote repository does not exist for %s', self.project)
852-
return GENERIC_EXTERNAL_VERSION_NAME
853-
except Exception:
854-
log.exception(
855-
'Unhandled exception raised for %s while getting external_version_name',
856-
self.project
857-
)
858-
return GENERIC_EXTERNAL_VERSION_NAME
847+
if self.project.git_provider_name == GITHUB_BRAND:
848+
return GITHUB_EXTERNAL_VERSION_NAME
849+
# TODO: Add External Version Name for other Git Providers
850+
851+
return GENERIC_EXTERNAL_VERSION_NAME
859852
return None
860853

861854
def using_latest_config(self):

readthedocs/oauth/notifications.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from messages_extends.constants import ERROR_PERSISTENT
55

66
from readthedocs.notifications import SiteNotification
7+
from readthedocs.notifications.constants import ERROR_NON_PERSISTENT
78

89

910
class AttachWebhookNotification(SiteNotification):
@@ -55,3 +56,22 @@ def get_context_data(self):
5556
),
5657
})
5758
return context
59+
60+
61+
class GitBuildStatusFailureNotification(SiteNotification):
62+
63+
context_object_name = 'project'
64+
failure_level = ERROR_NON_PERSISTENT
65+
failure_message = _(
66+
'Could not send {{ provider_name }} build status report for {{ project.name }}. '
67+
'Make sure you have the correct {{ provider_name }} repository permissions</a> and '
68+
'your <a href="{{ url_connect_account }}">{{ provider_name }} account</a> '
69+
'is connected to ReadtheDocs.'
70+
) # noqa
71+
72+
def get_context_data(self):
73+
context = super().get_context_data()
74+
context.update({
75+
'url_connect_account': reverse('socialaccount_connections'),
76+
})
77+
return context

readthedocs/projects/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,6 @@
355355
)
356356

357357
GITHUB_PR_PULL_PATTERN = 'pull/{id}/head:external-{id}'
358+
359+
# Git provider names
360+
GITHUB_BRAND = 'GitHub'

readthedocs/projects/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
from urllib.parse import urlparse
88

9+
from allauth.socialaccount.providers import registry as allauth_registry
910
from django.conf import settings
1011
from django.contrib.auth.models import User
1112
from django.core.files.storage import get_storage_class
@@ -839,6 +840,29 @@ def vcs_repo(
839840
)
840841
return repo
841842

843+
def git_service_class(self):
844+
"""Get the service class for project. e.g: GitHubService, GitLabService."""
845+
from readthedocs.oauth.services import registry
846+
847+
for service_cls in registry:
848+
if service_cls.is_project_service(self):
849+
service = service_cls
850+
break
851+
else:
852+
log.warning('There are no registered services in the application.')
853+
service = None
854+
855+
return service
856+
857+
@property
858+
def git_provider_name(self):
859+
"""Get the provider name for project. e.g: GitHub, GitLab, BitBucket."""
860+
service = self.git_service_class()
861+
if service:
862+
provider = allauth_registry.by_id(service.adapter.provider_id)
863+
return provider.name
864+
return None
865+
842866
def repo_nonblockinglock(self, version, max_lock_age=None):
843867
"""
844868
Return a ``NonBlockingLock`` to acquire the lock via context manager.

readthedocs/projects/tasks.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@
6363
from readthedocs.doc_builder.loader import get_builder_class
6464
from readthedocs.doc_builder.python_environments import Conda, Virtualenv
6565
from readthedocs.oauth.models import RemoteRepository
66-
from readthedocs.oauth.services import registry
66+
from readthedocs.oauth.notifications import GitBuildStatusFailureNotification
6767
from readthedocs.oauth.services.github import GitHubService
68+
from readthedocs.projects.constants import GITHUB_BRAND
6869
from readthedocs.projects.models import APIProject, Feature
6970
from readthedocs.search.utils import index_new_files, remove_indexed_files
7071
from readthedocs.sphinx_domains.models import SphinxDomain
@@ -1889,47 +1890,55 @@ def send_build_status(build_pk, commit, status):
18891890
:param status: build status failed, pending, or success to be sent.
18901891
"""
18911892
build = Build.objects.get(pk=build_pk)
1893+
provider_name = build.project.git_provider_name
18921894

1893-
try:
1894-
if build.project.remote_repository.account.provider == 'github':
1895-
service = GitHubService(
1895+
if provider_name == GITHUB_BRAND:
1896+
# get the service class for the project e.g: GitHubService.
1897+
service_class = build.project.git_service_class()
1898+
try:
1899+
service = service_class(
18961900
build.project.remote_repository.users.first(),
18971901
build.project.remote_repository.account
18981902
)
1899-
1900-
# send Status report using the API.
1903+
# Send status report using the API.
19011904
service.send_build_status(build, commit, status)
19021905

1903-
except RemoteRepository.DoesNotExist:
1904-
# Get the service provider for the project
1905-
for service_cls in registry:
1906-
if service_cls.is_project_service(build.project):
1907-
service = service_cls
1908-
break
1909-
else:
1910-
log.warning('There are no registered services in the application.')
1911-
return False
1906+
except RemoteRepository.DoesNotExist:
1907+
users = build.project.users.all()
1908+
1909+
# Try to loop through all project users to get their social accounts
1910+
for user in users:
1911+
user_accounts = service_class.for_user(user)
1912+
# Try to loop through users all social accounts to send a successful request
1913+
for account in user_accounts:
1914+
# Currently we only support GitHub Status API
1915+
if account.provider_name == provider_name:
1916+
success = account.send_build_status(build, commit, status)
1917+
if success:
1918+
return True
1919+
1920+
for user in users:
1921+
# Send Site notification about Build status reporting failure
1922+
# to all the users of the project.
1923+
notification = GitBuildStatusFailureNotification(
1924+
context_object=build.project,
1925+
extra_context={'provider_name': provider_name},
1926+
user=user,
1927+
success=False,
1928+
)
1929+
notification.send()
19121930

1913-
# Try to loop through all project users to get their social accounts
1914-
for user in build.project.users.all():
1915-
user_accounts = service.for_user(user)
1916-
# Try to loop through users all social accounts to send a successful request
1917-
for account in user_accounts:
1918-
# Currently we only support GitHub Status API
1919-
if account.provider_name == 'GitHub':
1920-
success = account.send_build_status(build, commit, status)
1921-
if success:
1922-
return True
1931+
log.info(
1932+
'No social account or repository permission available for %s',
1933+
build.project.slug
1934+
)
1935+
return False
19231936

1924-
log.info(
1925-
'No social account or repository permission available for %s',
1926-
build.project
1927-
)
1928-
return False
1937+
except Exception:
1938+
log.exception('Send build status task failed for %s', build.project.slug)
1939+
return False
19291940

1930-
except Exception:
1931-
log.exception('Send build status task failed for %s', build.project)
1932-
return False
1941+
return False
19331942

19341943
# TODO: Send build status for other providers.
19351944

readthedocs/rtd_tests/tests/test_builds.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -656,13 +656,8 @@ def test_no_external_version_name(self):
656656
self.assertEqual(build.external_version_name, None)
657657

658658
def test_external_version_name_github(self):
659-
social_account = get(SocialAccount, provider='github')
660-
remote_repo = get(
661-
RemoteRepository,
662-
account=social_account,
663-
project=self.project
664-
)
665-
remote_repo.users.add(self.eric)
659+
self.project.repo = 'https://github.com/test/test/'
660+
self.project.save()
666661

667662
external_version = get(Version, project=self.project, type=EXTERNAL)
668663
external_build = get(

readthedocs/rtd_tests/tests/test_celery.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from django.contrib.auth.models import User
77
from django_dynamic_fixture import get
8+
from messages_extends.models import Message
89
from mock import MagicMock, patch
910

1011
from allauth.socialaccount.models import SocialAccount
@@ -335,6 +336,9 @@ def test_fileify_logging_when_wrong_version_pk(self, mock_logger):
335336

336337
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
337338
def test_send_build_status_task_with_remote_repo(self, send_build_status):
339+
self.project.repo = 'https://github.com/test/test/'
340+
self.project.save()
341+
338342
social_account = get(SocialAccount, provider='github')
339343
remote_repo = get(RemoteRepository, account=social_account, project=self.project)
340344
remote_repo.users.add(self.eric)
@@ -350,6 +354,7 @@ def test_send_build_status_task_with_remote_repo(self, send_build_status):
350354
send_build_status.assert_called_once_with(
351355
external_build, external_build.commit, BUILD_STATUS_SUCCESS
352356
)
357+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
353358

354359
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
355360
def test_send_build_status_task_with_social_account(self, send_build_status):
@@ -369,9 +374,12 @@ def test_send_build_status_task_with_social_account(self, send_build_status):
369374
send_build_status.assert_called_once_with(
370375
external_build, external_build.commit, BUILD_STATUS_SUCCESS
371376
)
377+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
372378

373379
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
374380
def test_send_build_status_task_without_remote_repo_or_social_account(self, send_build_status):
381+
self.project.repo = 'https://github.com/test/test/'
382+
self.project.save()
375383
external_version = get(Version, project=self.project, type=EXTERNAL)
376384
external_build = get(
377385
Build, project=self.project, version=external_version
@@ -381,3 +389,4 @@ def test_send_build_status_task_without_remote_repo_or_social_account(self, send
381389
)
382390

383391
send_build_status.assert_not_called()
392+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 1)

0 commit comments

Comments
 (0)