diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b77e530 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include setup.py README.rst MANIFEST.in LICENSE +recursive-include sentry_redmine/templates * +global-exclude *~ diff --git a/README.md b/README.md deleted file mode 100644 index 7720ca4..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -sentry-redmine -============== - -Sentry integration for creating Redmine issues \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b8b6bcb --- /dev/null +++ b/README.rst @@ -0,0 +1,24 @@ +sentry-redmine +================== + +An extension for Sentry which integrates with Redmine. Specifically, it allows you to easily create +Redmine issues from events within Sentry. + + +Install +------- + +Install the package via ``pip``:: + + pip install sentry-redmine + +Configuration +------------- + +Create a user within your Redmine install (a system agent). This user will +be creating tickets on your behalf via Sentry. + +Go to your project's configuration page (Projects -> [Project]) and select the +Redmine tab. Enter the required credentials and click save changes. + +You'll now see a new action on groups which allows quick creation of issues. diff --git a/sentry_redmine/__init__.py b/sentry_redmine/__init__.py new file mode 100644 index 0000000..a4a71fd --- /dev/null +++ b/sentry_redmine/__init__.py @@ -0,0 +1,13 @@ +""" +sentry_redmine +~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2013 by Aaditya Sood, Idea Device +:license: BSD, see LICENSE for more details. +""" + +try: + VERSION = __import__('pkg_resources') \ + .get_distribution('sentry-redmine').version +except Exception, e: + VERSION = 'unknown' diff --git a/sentry_redmine/models.py b/sentry_redmine/models.py new file mode 100644 index 0000000..16ffff2 --- /dev/null +++ b/sentry_redmine/models.py @@ -0,0 +1,7 @@ +""" +sentry_redmine.models +~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2013 by Aaditya Sood, Idea Device +:license: BSD, see LICENSE for more details. +""" diff --git a/sentry_redmine/plugin.py b/sentry_redmine/plugin.py new file mode 100644 index 0000000..226135d --- /dev/null +++ b/sentry_redmine/plugin.py @@ -0,0 +1,129 @@ +""" +sentry_redmine.plugin +~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2011 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + +import sys +import logging +from pprint import pformat +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse + +from sentry.plugins.bases.issue import IssuePlugin + +import httplib +import urlparse +import requests +import simplejson as json + + +class RedmineOptionsForm(forms.Form): + host = forms.URLField(help_text=_("e.g. http://bugs.redmine.org")) + key = forms.CharField(widget=forms.TextInput(attrs={'class': 'span9'})) + project_id = forms.CharField(widget=forms.TextInput(attrs={'class': 'span9'})) + tracker_id = forms.CharField(widget=forms.TextInput(attrs={'class': 'span9'})) + + def clean(self): + config = self.cleaned_data + if not all(config.get(k) for k in ('host', 'key', 'project_id', 'tracker_id')): + raise forms.ValidationError('Missing required configuration value') + return config + + +class RedmineNewIssueForm(forms.Form): + title = forms.CharField(max_length=200, widget=forms.TextInput(attrs={'class': 'span9'})) + description = forms.CharField(widget=forms.Textarea(attrs={'class': 'span9'})) + + +class RedminePlugin(IssuePlugin): + author = 'Idea Device' + author_url = 'https://github.com/ideadevice/sentry-redmine' + version = '0.1.0' + description = "Integrate Redmine issue tracking by linking a user account to a project." + resource_links = [ + ('Bug Tracker', 'https://github.com/ideadevice/sentry-redmine/issues'), + ('Source', 'https://github.com/ideadevice/sentry-redmine'), + ] + + slug = 'redmine' + title = _('Redmine') + conf_title = 'Redmine' + conf_key = 'redmine' + project_conf_form = RedmineOptionsForm + new_issue_form = RedmineNewIssueForm + + def is_configured(self, project, **kwargs): + return all((self.get_option(k, project) for k in ('host', 'key', 'project_id', 'tracker_id'))) + + def get_new_issue_title(self, **kwargs): + return 'Create Redmine Task' + + def _get_group_description(self, request, group, event): + output = [ + 'Sentry: %s' % request.build_absolute_uri(reverse('sentry-group', kwargs={ + 'project_id': group.project.slug, + 'team_slug': group.team.slug, + 'group_id': group.id, + })), + ] + output.append('\n* Server: @%s@' % event.server_name) + output.append('* Logger: @%s@' % event.logger) + output.append('* Level: @%s@' % event.level) + + body = self._get_group_body(request, group, event) + if body: + output.extend([ + '', + '
',
+                body,
+                '
', + ]) + return '\n'.join(output) + + def get_initial_form_data(self, request, group, event, **kwargs): + return { + 'description': self._get_group_description(request, group, event), + 'title': 'Sentry:%s' % self._get_group_title(request, group, event), + } + + def create_issue(self, group, form_data, **kwargs): + """Create a Redmine issue""" + headers = { "X-Redmine-API-Key": self.get_option('key', group.project), + 'content-type': 'application/json' } + url = urlparse.urljoin(self.get_option('host', group.project), "/issues.json") + payload = { + 'project_id': self.get_option('project_id', group.project), + 'tracker_id': self.get_option('tracker_id', group.project), + 'status_id': '0', + 'subject': form_data['title'].encode('utf-8'), + 'description': form_data['description'].encode('utf-8'), + } + #print >> sys.stderr, "url:", url + #print >> sys.stderr, "payload:\n", pformat(payload) + #print >> sys.stderr, pformat(group) + #print >> sys.stderr, pformat(dir(group)) + + try: + r = requests.post(url, data=json.dumps({'issue': payload}), headers=headers) + except requests.exceptions.HTTPError, e: + raise forms.ValidationError('Unable to reach Redmine host: %s' % (e.reason,)) + + try: + data = json.loads(r.text) + except json.JSONDecodeError, e: + #print >> sys.stderr, "ERROR: %s" % e + #print >> sys.stderr, "RESP:", r.text + raise forms.ValidationError('Unable to reach Redmine host: %s' % (e.reason,)) + + if not 'issue' in data or not 'id' in data['issue']: + raise forms.ValidationError('Unable to create redmine ticket') + + return data['issue']['id'] + + def get_issue_url(self, group, issue_id, **kwargs): + host = self.get_option('host', group.project) + return urlparse.urljoin(host, '/issues/%s' % issue_id) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..888c32f --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +""" +sentry-redmine +================== + +An extension for Sentry which integrates with Redmine. Specifically, it allows you to easily create +Maniphest tasks from events within Sentry. + +:copyright: (c) 2011 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +from setuptools import setup, find_packages + + +tests_require = [ + 'nose', +] + +install_requires = [ + 'sentry>=4.9.8', + 'requests>=0.2.0', + 'simplejson' +] + +setup( + name='sentry-redmine', + version='0.1.0', + author='Aaditya Sood', + author_email='a@ideadevice.com', + url='http://github.com/ideadevice/sentry-redmine', + description='A Sentry extension which integrates with Redmine.', + long_description=__doc__, + license='BSD', + packages=find_packages(exclude=['tests']), + zip_safe=False, + install_requires=install_requires, + tests_require=tests_require, + extras_require={'test': tests_require}, + test_suite='runtests.runtests', + include_package_data=True, + entry_points={ + 'sentry.apps': [ + 'redmine = sentry_redmine', + ], + 'sentry.plugins': [ + 'redmine = sentry_redmine.plugin:RedminePlugin' + ], + }, + classifiers=[ + 'Framework :: Django', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Topic :: Software Development' + ], +)