Update support for modern Redmine
This commit is contained in:
parent
79046df5b3
commit
813c824485
|
@ -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
|
|
@ -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'}))
|
|
@ -1,7 +0,0 @@
|
|||
"""
|
||||
sentry_redmine.models
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:copyright: (c) 2013 by Aaditya Sood, Idea Device
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
|
@ -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([
|
||||
'',
|
||||
'<pre>',
|
||||
body,
|
||||
'</pre>',
|
||||
])
|
||||
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)
|
||||
|
|
Reference in New Issue