From 47947b631672262ddbd812151c9e94130a00dca7 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Sun, 3 Jul 2016 22:56:24 +0200 Subject: [PATCH] Implement GitHub Issues service (closes #3) --- flash_services/__init__.py | 5 +- flash_services/github.py | 38 +++++++++- flash_services/static/scripts/services.js | 9 +++ .../templates/partials/gh-issues-section.html | 18 +++++ tests/test_github_issues_service.py | 73 +++++++++++++++++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 flash_services/templates/partials/gh-issues-section.html create mode 100644 tests/test_github_issues_service.py diff --git a/flash_services/__init__.py b/flash_services/__init__.py index dfe48ac..3215580 100644 --- a/flash_services/__init__.py +++ b/flash_services/__init__.py @@ -8,12 +8,12 @@ from .codeship import Codeship from .coveralls import Coveralls -from .github import GitHub +from .github import GitHub, GitHubIssues from .tracker import Tracker from .travis import TravisOS, TravisPro __author__ = 'Jonathan Sharpe' -__version__ = '0.3.2' +__version__ = '0.3.3' blueprint = Blueprint( 'services', @@ -28,6 +28,7 @@ codeship=Codeship, coveralls=Coveralls, github=GitHub, + gh_issues=GitHubIssues, tracker=Tracker, travis=TravisOS, travis_pro=TravisPro, diff --git a/flash_services/github.py b/flash_services/github.py index 9e53b0d..9d9032c 100644 --- a/flash_services/github.py +++ b/flash_services/github.py @@ -1,7 +1,7 @@ """Defines the GitHub service integration.""" import logging -from collections import OrderedDict +from collections import defaultdict, OrderedDict import requests @@ -112,3 +112,39 @@ def format_commit(cls, commit): committed=occurred(commit.get('committer', {}).get('date')), message=commit.get('message', ''), )) + + +class GitHubIssues(GitHub): + """Show the current status of GitHub issues and pull requests.""" + + FRIENDLY_NAME = 'GitHub Issues' + TEMPLATE = 'gh-issues-section' + + def update(self): + logger.debug('fetching GitHub issue data') + url_params = OrderedDict(state='all') + response = requests.get( + self.url_builder( + '/repos/{repo}/issues', + params={'repo': self.repo_name}, + url_params=url_params, + ), + headers=self.headers, + ) + if response.status_code == 200: + return self.format_data(self.name, response.json()) + logger.error('failed to update GitHub issue data') + return {} + + @classmethod + def format_data(cls, name, data): + counts = defaultdict(int) + for issue in data: + if issue.get('pull_request') is not None: + counts['{}-pull-requests'.format(issue['state'])] += 1 + else: + counts['{}-issues'.format(issue['state'])] += 1 + return dict( + issues=counts, + name=name, + ) diff --git a/flash_services/static/scripts/services.js b/flash_services/static/scripts/services.js index 0fcb616..fc28d7c 100644 --- a/flash_services/static/scripts/services.js +++ b/flash_services/static/scripts/services.js @@ -15,6 +15,15 @@ var SERVICES = { }); } }, + gh_issues: function (pane, data) { + if (data.issues) { + var states = ['open-issues', 'closed-issues', 'open-pull-requests', + 'closed-pull-requests']; + states.forEach(function (state) { + pane.find('.' + state).text(data.issues[state] || 0); + }); + } + }, github: function (pane, data) { if (data.commits) { updateItems(pane, data.commits, '.commit', updateCommit); diff --git a/flash_services/templates/partials/gh-issues-section.html b/flash_services/templates/partials/gh-issues-section.html new file mode 100644 index 0000000..4120cde --- /dev/null +++ b/flash_services/templates/partials/gh-issues-section.html @@ -0,0 +1,18 @@ +
+
+ Open issues: + +
+
+ Closed issues: + +
+
+ Open PRs: + +
+
+ Closed PRs: + +
+
diff --git a/tests/test_github_issues_service.py b/tests/test_github_issues_service.py new file mode 100644 index 0000000..0391ff9 --- /dev/null +++ b/tests/test_github_issues_service.py @@ -0,0 +1,73 @@ +from unittest import mock + +import pytest + +from flash_services.core import Service +from flash_services.github import GitHubIssues + + +@pytest.fixture +def service(): + return GitHubIssues(api_token='foobar', account='foo', repo='bar') + + +def test_tracker_service_type(): + assert issubclass(GitHubIssues, Service) + + +def test_correct_config(): + assert GitHubIssues.AUTH_PARAM == 'access_token' + assert GitHubIssues.FRIENDLY_NAME == 'GitHub Issues' + assert GitHubIssues.REQUIRED == {'api_token', 'account', 'repo'} + assert GitHubIssues.ROOT == 'https://api.github.com' + assert GitHubIssues.TEMPLATE == 'gh-issues-section' + + +@mock.patch('flash_services.github.logger.debug') +@mock.patch('flash_services.github.requests.get', **{ + 'return_value.status_code': 200, + 'return_value.json.return_value': [], +}) +def test_update_success(get, debug, service): + result = service.update() + + get.assert_called_once_with( + 'https://api.github.com/repos/foo/bar/issues?state=all&access_token=foobar', + headers={'User-Agent': 'bar'} + ) + debug.assert_called_once_with('fetching GitHub issue data') + assert result == {'issues': {}, 'name': 'foo/bar'} + + +@mock.patch('flash_services.github.logger.error') +@mock.patch('flash_services.github.requests.get', **{ + 'return_value.status_code': 401, +}) +def test_update_failure(get, error, service): + result = service.update() + + get.assert_called_once_with( + 'https://api.github.com/repos/foo/bar/issues?state=all&access_token=foobar', + headers={'User-Agent': 'bar'} + ) + error.assert_called_once_with('failed to update GitHub issue data') + assert result == {} + + +@pytest.mark.parametrize('input_, expected', [ + (('hello', []), dict(name='hello', issues={})), + ( + ('hello', [{'state': 'open'}, {'state': 'open'}]), + dict(name='hello', issues={'open-issues': 2}), + ), + ( + ('hello', [{'state': 'open'}, {'state': 'closed'}]), + dict(name='hello', issues={'open-issues': 1, 'closed-issues': 1}), + ), + ( + ('hello', [{'state': 'open'}, {'state': 'open', 'pull_request': {}}]), + dict(name='hello', issues={'open-issues': 1, 'open-pull-requests': 1}), + ), +]) +def test_format_data(input_, expected): + assert GitHubIssues.format_data(*input_) == expected