├── .gitignore
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── sentry_teamwork
├── __init__.py
├── client.py
├── plugin.py
└── templates
│ ├── create_issue.html
│ └── sentry_teamwork
│ └── create_issue.html
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pyo
3 | *.egg
4 | *.egg-info
5 | dist
6 | build
7 | eggs
8 | parts
9 | bin
10 | sdist
11 | develop-eggs
12 | pip-log.txt
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2016 Sentry
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include setup.py README.md MANIFEST.in LICENSE
2 | global-exclude *~
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # https://gist.github.com/3697234
2 | publish:
3 | python setup.py sdist upload
4 |
5 | clean:
6 | rm -rf *.egg-info
7 | rm -rf dist
8 | rm -rf build
9 |
10 | .PHONY: publish clean
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | sentry-teamwork
2 | ===============
3 | A [Sentry](https://www.getsentry.com/) plugin to integrate with [Teamwork](https://teamwork.com).
4 |
5 |
6 | DEPRECATED: This project now lives in [sentry](https://github.com/getsentry/sentry/tree/master/src/sentry_plugins/teamwork)
7 |
--------------------------------------------------------------------------------
/sentry_teamwork/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | VERSION = __import__('pkg_resources').get_distribution(__name__) \
3 | .version
4 | except Exception, e:
5 | VERSION = 'Unknown'
6 |
--------------------------------------------------------------------------------
/sentry_teamwork/client.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from sentry import http
4 | from sentry.utils import json
5 |
6 |
7 | class TeamworkClient(object):
8 | def __init__(self, base_url, token, timeout=5):
9 | self.base_url = base_url
10 | self._token = token
11 | self._timeout = timeout
12 |
13 | def _request(self, path, method="GET", params=None, data=None):
14 | path = path.lstrip('/')
15 | url = '%s/%s' % (self.base_url, path)
16 |
17 | if not params:
18 | params = {}
19 |
20 | session = http.build_session()
21 | resp = getattr(session, method.lower())(
22 | url,
23 | auth=(self._token, ''),
24 | params=params,
25 | json=data,
26 | timeout=self._timeout,
27 | )
28 | resp.raise_for_status()
29 | return json.loads(resp.content)
30 |
31 | def list_projects(self):
32 | return self._request(
33 | path='/projects.json',
34 | )['projects']
35 |
36 | def list_tasklists(self, project_id):
37 | return self._request(
38 | path='/projects/{0}/tasklists.json'.format(project_id),
39 | )['tasklists']
40 |
41 | def create_task(self, tasklist_id, **kwargs):
42 | return self._request(
43 | method='POST',
44 | path='/tasklists/{0}/tasks.json'.format(tasklist_id),
45 | data={
46 | 'todo-item': kwargs,
47 | },
48 | )['id']
49 |
--------------------------------------------------------------------------------
/sentry_teamwork/plugin.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import sentry_teamwork
4 |
5 | from django import forms
6 | from django.utils.translation import ugettext_lazy as _
7 | from requests.exceptions import RequestException
8 | from sentry.plugins.base import JSONResponse
9 | from sentry.plugins.bases.issue import IssuePlugin, NewIssueForm
10 | from sentry.utils.http import absolute_uri
11 |
12 | from .client import TeamworkClient
13 |
14 | ISSUES_URL = 'https://github.com/getsentry/sentry-teamwork/issues'
15 |
16 |
17 | class TeamworkSettingsForm(forms.Form):
18 | url = forms.URLField(
19 | label=_('Teamwork URL'),
20 | help_text=('i.e. http://sentry.teamwork.com'),
21 | )
22 | token = forms.CharField(label=_('Teamwork API Token'))
23 |
24 |
25 | class TeamworkTaskForm(NewIssueForm):
26 | title = forms.CharField(
27 | label=_('Title'), max_length=200,
28 | widget=forms.TextInput(attrs={'class': 'span9'}))
29 | description = forms.CharField(
30 | label=_('Description'),
31 | widget=forms.Textarea(attrs={'class': 'span9'}))
32 | project = forms.ChoiceField(label=_('Project'), choices=())
33 | tasklist = forms.ChoiceField(label=_('Task List'), choices=())
34 |
35 | create_issue_template = 'sentry_teamwork/create_issue.html'
36 |
37 | def __init__(self, client, data=None, initial=None):
38 | super(TeamworkTaskForm, self).__init__(data=data, initial=initial)
39 |
40 | try:
41 | project_list = client.list_projects()
42 | except RequestException as e:
43 | raise forms.ValidationError(
44 | _('Error contacting Teamwork API: %s') % str(e))
45 |
46 | self.fields['project'].choices = [
47 | (str(i['id']), i['name']) for i in project_list
48 | ]
49 | self.fields['project'].widget.choices = self.fields['project'].choices
50 |
51 | if self.data.get('project'):
52 | try:
53 | tasklist_list = client.list_tasklists(data['project'])
54 | except RequestException as e:
55 | raise forms.ValidationError(
56 | _('Error contacting Teamwork API: %s') % str(e))
57 | self.fields['tasklist'].choices = [
58 | (str(i['id']), i['name']) for i in tasklist_list
59 | ]
60 | self.fields['tasklist'].widget.choices = self.fields['tasklist'].choices
61 |
62 |
63 | class TeamworkTaskPlugin(IssuePlugin):
64 | author = "Sentry Team"
65 | author_url = "https://github.com/getsentry/sentry-teamwork"
66 | title = _('Teamwork')
67 | description = _('Create Teamwork Tasks.')
68 | slug = 'teamwork'
69 |
70 | resource_links = [
71 | (_('Bug Tracker'), ISSUES_URL),
72 | (_('Source'), 'https://github.com/getsentry/sentry-teamwork'),
73 | ]
74 |
75 | conf_title = title
76 | conf_key = slug
77 |
78 | version = sentry_teamwork.VERSION
79 | project_conf_form = TeamworkSettingsForm
80 |
81 | new_issue_form = TeamworkTaskForm
82 | create_issue_template = 'sentry_teamwork/create_issue.html'
83 |
84 | def _get_group_description(self, request, group, event):
85 | """
86 | Return group description in markdown-compatible format.
87 |
88 | This overrides an internal method to IssuePlugin.
89 | """
90 | output = [
91 | absolute_uri(group.get_absolute_url()),
92 | ]
93 | body = self._get_group_body(request, group, event)
94 | if body:
95 | output.extend([
96 | '',
97 | '\n'.join(' ' + line for line in body.splitlines()),
98 | ])
99 | return '\n'.join(output)
100 |
101 | def is_configured(self, request, project, **kwargs):
102 | return all((
103 | self.get_option(key, project)
104 | for key in ('url', 'token')
105 | ))
106 |
107 | def get_client(self, project):
108 | return TeamworkClient(
109 | base_url=self.get_option('url', project),
110 | token=self.get_option('token', project),
111 | )
112 |
113 | def get_new_issue_form(self, request, group, event, **kwargs):
114 | """
115 | Return a Form for the "Create new issue" page.
116 | """
117 | return self.new_issue_form(
118 | client=self.get_client(group.project),
119 | data=request.POST or None,
120 | initial=self.get_initial_form_data(request, group, event),
121 | )
122 |
123 | def get_issue_url(self, group, issue_id, **kwargs):
124 | url = self.get_option('url', group.project)
125 | return '%s/tasks/%s' % (url.rstrip('/'), issue_id)
126 |
127 | def get_new_issue_title(self, **kwargs):
128 | return _('Create Teamwork Task')
129 |
130 | def create_issue(self, request, group, form_data, **kwargs):
131 | client = self.get_client(group.project)
132 | try:
133 | task_id = client.create_task(
134 | content=form_data['title'],
135 | description=form_data['description'],
136 | tasklist_id=form_data['tasklist'],
137 | )
138 | except RequestException as e:
139 | raise forms.ValidationError(
140 | _('Error creating Teamwork task: %s') % str(e))
141 |
142 | return task_id
143 |
144 | def view(self, request, group, **kwargs):
145 | op = request.GET.get('op')
146 | # TODO(dcramer): add caching
147 | if op == 'getTaskLists':
148 | project_id = request.GET.get('pid')
149 | if not project_id:
150 | return HttpResponse(status=400)
151 |
152 | client = self.get_client(group.project)
153 | task_list = client.list_tasklists(project_id)
154 | return JSONResponse([
155 | {'id': i['id'], 'text': i['name']} for i in task_list
156 | ])
157 |
158 | return super(TeamworkTaskPlugin, self).view(request, group, **kwargs)
159 |
--------------------------------------------------------------------------------
/sentry_teamwork/templates/create_issue.html:
--------------------------------------------------------------------------------
1 | {% extends "sentry/plugins/bases/issue/create_issue.html" %}
2 |
3 | {% block meta %}
4 | {{ block.super }}
5 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/sentry_teamwork/templates/sentry_teamwork/create_issue.html:
--------------------------------------------------------------------------------
1 | {% extends "sentry/plugins/bases/issue/create_issue.html" %}
2 |
3 | {% block meta %}
4 | {{ block.super }}
5 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | sentry-teamwork
5 | ===============
6 |
7 | A Sentry plugin to integrate with Teamwork.
8 |
9 | License
10 | -------
11 | Copyright 2015 Sentry
12 | """
13 |
14 | from setuptools import setup, find_packages
15 |
16 | install_requires = [
17 | ]
18 |
19 | setup(
20 | name='sentry-teamwork',
21 | version='0.1.0',
22 | author='David Cramer',
23 | author_email='dcramer@gmail.com',
24 | url='https://github.com/getsentry/sentry-teamwork',
25 | description='A Sentry plugin that integrates with Teamwork',
26 | long_description=__doc__,
27 | license='Apache 2.0',
28 | packages=find_packages(exclude=['tests']),
29 | install_requires=install_requires,
30 | entry_points={
31 | 'sentry.plugins': [
32 | 'sentry_teamwork = sentry_teamwork.plugin:TeamworkTaskPlugin'
33 | ],
34 | 'sentry.apps': [
35 | 'sentry_teamwork = sentry_teamwork'
36 | ],
37 | },
38 | include_package_data=True,
39 | )
40 |
--------------------------------------------------------------------------------