diff --git a/sentry_redmine/client.py b/sentry_redmine/client.py new file mode 100644 index 0000000..4170d81 --- /dev/null +++ b/sentry_redmine/client.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +from sentry import http +from sentry.utils import json + + +class RedmineClient(object): + def __init__(self, host, key): + self.host = host.rstrip('/') + self.key = key + + def request(self, method, path, data=None): + headers = { + 'X-Redmine-API-Key': self.key, + 'Content-Type': "application/json", + } + url = '{}{}'.format(self.host, path) + session = http.build_session() + req = getattr(session, method.lower())(url, json=data, headers=headers) + return json.loads(req.text) + + def get_projects(self): + response = self.request('GET', '/projects.json') + return response + + def get_trackers(self): + response = self.request('GET', '/trackers.json') + return response + + def get_priorities(self): + response = self.request('GET', '/enumerations/issue_priorities.json') + return response + + def create_issue(self, data): + response = self.request('POST', '/issues.json', data={ + 'issue': data, + }) + + if 'issue' not in response or 'id' not in response['issue']: + raise Exception('Unable to create redmine ticket') + + return response diff --git a/sentry_redmine/forms.py b/sentry_redmine/forms.py new file mode 100644 index 0000000..5ea447a --- /dev/null +++ b/sentry_redmine/forms.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .client import RedmineClient + + +class RedmineOptionsForm(forms.Form): + host = forms.URLField(help_text=_("e.g. http://bugs.redmine.org")) + key = forms.CharField( + widget=forms.TextInput(attrs={'class': 'span9'}), + help_text='Your API key is available on your account page after enabling the Rest API (Administration -> Settings -> Authentication)') + project_id = forms.TypedChoiceField( + label='Project', coerce=int) + tracker_id = forms.TypedChoiceField( + label='Tracker', coerce=int) + default_priority = forms.TypedChoiceField( + label='Default Priority', coerce=int) + + def __init__(self, data=None, *args, **kwargs): + super(RedmineOptionsForm, self).__init__(data=data, *args, **kwargs) + + initial = kwargs.get('initial') or {} + for key, value in self.data.items(): + initial[key.lstrip(self.prefix or '')] = value + + has_credentials = all(initial.get(k) for k in ('host', 'key')) + if has_credentials: + client = RedmineClient(initial['host'], initial['key']) + try: + projects = client.get_projects() + except Exception: + has_credentials = False + else: + project_choices = [ + (p['id'], '%s (%s)' % (p['name'], p['identifier'])) + for p in projects['projects'] + ] + self.fields['project_id'].choices = project_choices + + if has_credentials: + try: + trackers = client.get_trackers() + except Exception: + del self.fields['tracker_id'] + else: + tracker_choices = [ + (p['id'], p['name']) + for p in trackers['trackers'] + ] + self.fields['tracker_id'].choices = tracker_choices + + try: + priorities = client.get_priorities() + except Exception: + del self.fields['default_priority'] + else: + tracker_choices = [ + (p['id'], p['name']) + for p in priorities['issue_priorities'] + ] + self.fields['default_priority'].choices = tracker_choices + + if not has_credentials: + del self.fields['project_id'] + del self.fields['tracker_id'] + del self.fields['default_priority'] + + def clean(self): + cd = self.cleaned_data + client = RedmineClient(cd['host'], cd['key']) + try: + client.get_projects() + except Exception: + raise forms.ValidationError('There was an issue authenticating with Redmine') + return cd + + def clean_host(self): + """ + Strip forward slashes off any url passed through the form. + """ + url = self.cleaned_data.get('host') + if url: + return url.rstrip('/') + return url + + +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'})) diff --git a/sentry_redmine/models.py b/sentry_redmine/models.py index 16ffff2..e69de29 100644 --- a/sentry_redmine/models.py +++ b/sentry_redmine/models.py @@ -1,7 +0,0 @@ -""" -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 index 02f5d50..46286c8 100644 --- a/sentry_redmine/plugin.py +++ b/sentry_redmine/plugin.py @@ -1,37 +1,12 @@ -""" -sentry_redmine.plugin -~~~~~~~~~~~~~~~~~~~~~~~~~ +from __future__ import absolute_import -:copyright: (c) 2011 by the Sentry Team, see AUTHORS for more details. -:license: BSD, see LICENSE for more details. -""" - -from django import forms from django.utils.translation import ugettext_lazy as _ -from sentry import http -from sentry.utils import json from sentry.plugins.bases.issue import IssuePlugin +from sentry.utils.http import absolute_uri -import urlparse - - -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'})) +from .client import RedmineClient +from .forms import RedmineOptionsForm, RedmineNewIssueForm class RedminePlugin(IssuePlugin): @@ -52,7 +27,7 @@ class RedminePlugin(IssuePlugin): 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'))) + return all((self.get_option(k, project) for k in ('host', 'key', 'project_id'))) def get_new_issue_title(self, **kwargs): return 'Create Redmine Task' @@ -60,33 +35,47 @@ class RedminePlugin(IssuePlugin): 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), + 'title': self._get_group_title(request, group, event), } + def _get_group_description(self, request, group, event): + output = [ + absolute_uri(group.get_absolute_url()), + ] + body = self._get_group_body(request, group, event) + if body: + output.extend([ + '', + '
',
+                body,
+                '
', + ]) + return '\n'.join(output) + + def get_client(self, project): + return RedmineClient( + host=self.get_option('host', project), + key=self.get_option('key', project), + ) + 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 = { + """ + Create a Redmine issue + """ + client = self.get_client(group.project) + default_priority = self.get_option('default_priority', group.project) + if default_priority is None: + default_priority = 4 + + response = client.create_issue({ 'project_id': self.get_option('project_id', group.project), 'tracker_id': self.get_option('tracker_id', group.project), - 'status_id': '0', + 'priority_id': default_priority, 'subject': form_data['title'].encode('utf-8'), 'description': form_data['description'].encode('utf-8'), - } - - session = http.build_session() - r = session.post(url, data=json.dumps({'issue': payload}), headers=headers) - data = json.loads(r.text) - - if 'issue' not in data or 'id' not in data['issue']: - raise Exception('Unable to create redmine ticket') - - return data['issue']['id'] + }) + return response['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) + return '{}/issues/{}'.format(host.rstrip('/'), issue_id)