##// END OF EJS Templates
integrations: add email integration, fixes #4159
dan -
r640:6c88399f default
parent child Browse files
Show More
@@ -0,0 +1,127 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from __future__ import unicode_literals
22
23 import deform
24 import logging
25 import colander
26
27 from mako.template import Template
28
29 from rhodecode import events
30 from rhodecode.translation import _, lazy_ugettext
31 from rhodecode.lib.celerylib import run_task
32 from rhodecode.lib.celerylib import tasks
33 from rhodecode.integrations.types.base import IntegrationTypeBase
34 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
35
36
37 log = logging.getLogger()
38
39
40
41 class EmailSettingsSchema(IntegrationSettingsSchemaBase):
42 @colander.instantiate(validator=colander.Length(min=1))
43 class recipients(colander.SequenceSchema):
44 title = lazy_ugettext('Recipients')
45 description = lazy_ugettext('Email addresses to send push events to')
46 widget = deform.widget.SequenceWidget(min_len=1)
47
48 recipient = colander.SchemaNode(
49 colander.String(),
50 title=lazy_ugettext('Email address'),
51 description=lazy_ugettext('Email address'),
52 default='',
53 validator=colander.Email(),
54 widget=deform.widget.TextInputWidget(
55 placeholder='user@domain.com',
56 ),
57 )
58
59
60 class EmailIntegrationType(IntegrationTypeBase):
61 key = 'email'
62 display_name = lazy_ugettext('Email')
63 SettingsSchema = EmailSettingsSchema
64
65 def settings_schema(self):
66 schema = EmailSettingsSchema()
67 return schema
68
69 def send_event(self, event):
70 data = event.as_dict()
71 log.debug('got event: %r', event)
72
73 if isinstance(event, events.RepoPushEvent):
74 repo_push_handler(data, self.settings)
75 else:
76 log.debug('ignoring event: %r', event)
77
78
79 def repo_push_handler(data, settings):
80 for commit in data['push']['commits']:
81 email_body_plaintext = repo_push_template_plaintext.render(
82 data=data,
83 commit=commit,
84 commit_msg=commit['message'],
85 )
86 email_body_html = repo_push_template_html.render(
87 data=data,
88 commit=commit,
89 commit_msg=commit['message_html'],
90 )
91
92 subject = '[%(repo_name)s] %(commit_id)s: %(commit_msg)s' % {
93 'repo_name': data['repo']['repo_name'],
94 'commit_id': commit['short_id'],
95 'commit_msg': commit['message'].split('\n')[0][:150]
96 }
97 for email_address in settings['recipients']:
98 task = run_task(
99 tasks.send_email, email_address, subject,
100 email_body_plaintext, email_body_html)
101
102
103 # TODO: dan: add changed files, make html pretty
104 repo_push_template_plaintext = Template('''
105 User: ${data['actor']['username']}
106 Branches: ${', '.join(branch['name'] for branch in data['push']['branches'])}
107 Repository: ${data['repo']['url']}
108 Commit: ${commit['raw_id']}
109 URL: ${commit['url']}
110 Author: ${commit['author']}
111 Date: ${commit['date']}
112 Commit Message:
113
114 ${commit_msg}
115 ''')
116
117 repo_push_template_html = Template('''
118 User: ${data['actor']['username']}<br>
119 Branches: ${', '.join(branch['name'] for branch in data['push']['branches'])}<br>
120 Repository: ${data['repo']['url']}<br>
121 Commit: ${commit['raw_id']}<br>
122 URL: ${commit['url']}<br>
123 Author: ${commit['author']}<br>
124 Date: ${commit['date']}<br>
125 Commit Message:<br>
126 <p>${commit_msg}</p>
127 ''')
@@ -1,60 +1,62 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.integrations.registry import IntegrationTypeRegistry
24 from rhodecode.integrations.types import webhook, slack, hipchat
24 from rhodecode.integrations.types import webhook, slack, hipchat, email
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 # TODO: dan: This is currently global until we figure out what to do about
30 30 # VCS's not having a pyramid context - move it to pyramid app configuration
31 31 # includeme level later to allow per instance integration setup
32 32 integration_type_registry = IntegrationTypeRegistry()
33 33
34 34 integration_type_registry.register_integration_type(
35 35 webhook.WebhookIntegrationType)
36 36 integration_type_registry.register_integration_type(
37 37 slack.SlackIntegrationType)
38 38 integration_type_registry.register_integration_type(
39 39 hipchat.HipchatIntegrationType)
40 integration_type_registry.register_integration_type(
41 email.EmailIntegrationType)
40 42
41 43
42 44 def integrations_event_handler(event):
43 45 """
44 46 Takes an event and passes it to all enabled integrations
45 47 """
46 48 from rhodecode.model.integration import IntegrationModel
47 49
48 50 integration_model = IntegrationModel()
49 51 integrations = integration_model.get_for_event(event)
50 52 for integration in integrations:
51 53 try:
52 54 integration_model.send_event(integration, event)
53 55 except Exception:
54 56 log.exception(
55 57 'failure occured when sending event %s to integration %s' % (
56 58 event, integration))
57 59
58 60
59 61 def includeme(config):
60 62 config.include('rhodecode.integrations.routes')
@@ -1,284 +1,284 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode task modules, containing all task that suppose to be run
23 23 by celery daemon
24 24 """
25 25
26 26
27 27 import os
28 28 import logging
29 29
30 30 from celery.task import task
31 31 from pylons import config
32 32
33 33 import rhodecode
34 34 from rhodecode.lib.celerylib import (
35 35 run_task, dbsession, __get_lockkey, LockHeld, DaemonLock,
36 36 get_session, vcsconnection, RhodecodeCeleryTask)
37 37 from rhodecode.lib.hooks_base import log_create_repository
38 38 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
39 39 from rhodecode.lib.utils import add_cache, action_logger
40 40 from rhodecode.lib.utils2 import safe_int, str2bool
41 41 from rhodecode.model.db import Repository, User
42 42
43 43
44 44 add_cache(config) # pragma: no cover
45 45
46 46
47 47 def get_logger(cls):
48 48 if rhodecode.CELERY_ENABLED:
49 49 try:
50 50 log = cls.get_logger()
51 51 except Exception:
52 52 log = logging.getLogger(__name__)
53 53 else:
54 54 log = logging.getLogger(__name__)
55 55
56 56 return log
57 57
58 58
59 59 @task(ignore_result=True, base=RhodecodeCeleryTask)
60 60 @dbsession
61 61 def send_email(recipients, subject, body='', html_body='', email_config=None):
62 62 """
63 63 Sends an email with defined parameters from the .ini files.
64 64
65 65 :param recipients: list of recipients, it this is empty the defined email
66 66 address from field 'email_to' is used instead
67 67 :param subject: subject of the mail
68 68 :param body: body of the mail
69 69 :param html_body: html version of body
70 70 """
71 71 log = get_logger(send_email)
72 72
73 email_config = email_config or config
73 email_config = email_config or rhodecode.CONFIG
74 74 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
75 75 if not recipients:
76 76 # if recipients are not defined we send to email_config + all admins
77 77 admins = [
78 78 u.email for u in User.query().filter(User.admin == True).all()]
79 79 recipients = [email_config.get('email_to')] + admins
80 80
81 81 mail_server = email_config.get('smtp_server') or None
82 82 if mail_server is None:
83 83 log.error("SMTP server information missing. Sending email failed. "
84 84 "Make sure that `smtp_server` variable is configured "
85 85 "inside the .ini file")
86 86 return False
87 87
88 88 mail_from = email_config.get('app_email_from', 'RhodeCode')
89 89 user = email_config.get('smtp_username')
90 90 passwd = email_config.get('smtp_password')
91 91 mail_port = email_config.get('smtp_port')
92 92 tls = str2bool(email_config.get('smtp_use_tls'))
93 93 ssl = str2bool(email_config.get('smtp_use_ssl'))
94 94 debug = str2bool(email_config.get('debug'))
95 95 smtp_auth = email_config.get('smtp_auth')
96 96
97 97 try:
98 98 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
99 99 mail_port, ssl, tls, debug=debug)
100 100 m.send(recipients, subject, body, html_body)
101 101 except Exception:
102 102 log.exception('Mail sending failed')
103 103 return False
104 104 return True
105 105
106 106
107 107 @task(ignore_result=True, base=RhodecodeCeleryTask)
108 108 @dbsession
109 109 @vcsconnection
110 110 def create_repo(form_data, cur_user):
111 111 from rhodecode.model.repo import RepoModel
112 112 from rhodecode.model.user import UserModel
113 113 from rhodecode.model.settings import SettingsModel
114 114
115 115 log = get_logger(create_repo)
116 116 DBS = get_session()
117 117
118 118 cur_user = UserModel(DBS)._get_user(cur_user)
119 119 owner = cur_user
120 120
121 121 repo_name = form_data['repo_name']
122 122 repo_name_full = form_data['repo_name_full']
123 123 repo_type = form_data['repo_type']
124 124 description = form_data['repo_description']
125 125 private = form_data['repo_private']
126 126 clone_uri = form_data.get('clone_uri')
127 127 repo_group = safe_int(form_data['repo_group'])
128 128 landing_rev = form_data['repo_landing_rev']
129 129 copy_fork_permissions = form_data.get('copy_permissions')
130 130 copy_group_permissions = form_data.get('repo_copy_permissions')
131 131 fork_of = form_data.get('fork_parent_id')
132 132 state = form_data.get('repo_state', Repository.STATE_PENDING)
133 133
134 134 # repo creation defaults, private and repo_type are filled in form
135 135 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
136 136 enable_statistics = form_data.get(
137 137 'enable_statistics', defs.get('repo_enable_statistics'))
138 138 enable_locking = form_data.get(
139 139 'enable_locking', defs.get('repo_enable_locking'))
140 140 enable_downloads = form_data.get(
141 141 'enable_downloads', defs.get('repo_enable_downloads'))
142 142
143 143 try:
144 144 RepoModel(DBS)._create_repo(
145 145 repo_name=repo_name_full,
146 146 repo_type=repo_type,
147 147 description=description,
148 148 owner=owner,
149 149 private=private,
150 150 clone_uri=clone_uri,
151 151 repo_group=repo_group,
152 152 landing_rev=landing_rev,
153 153 fork_of=fork_of,
154 154 copy_fork_permissions=copy_fork_permissions,
155 155 copy_group_permissions=copy_group_permissions,
156 156 enable_statistics=enable_statistics,
157 157 enable_locking=enable_locking,
158 158 enable_downloads=enable_downloads,
159 159 state=state
160 160 )
161 161
162 162 action_logger(cur_user, 'user_created_repo',
163 163 repo_name_full, '', DBS)
164 164 DBS.commit()
165 165
166 166 # now create this repo on Filesystem
167 167 RepoModel(DBS)._create_filesystem_repo(
168 168 repo_name=repo_name,
169 169 repo_type=repo_type,
170 170 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
171 171 clone_uri=clone_uri,
172 172 )
173 173 repo = Repository.get_by_repo_name(repo_name_full)
174 174 log_create_repository(created_by=owner.username, **repo.get_dict())
175 175
176 176 # update repo commit caches initially
177 177 repo.update_commit_cache()
178 178
179 179 # set new created state
180 180 repo.set_state(Repository.STATE_CREATED)
181 181 DBS.commit()
182 182 except Exception as e:
183 183 log.warning('Exception %s occurred when creating repository, '
184 184 'doing cleanup...', e)
185 185 # rollback things manually !
186 186 repo = Repository.get_by_repo_name(repo_name_full)
187 187 if repo:
188 188 Repository.delete(repo.repo_id)
189 189 DBS.commit()
190 190 RepoModel(DBS)._delete_filesystem_repo(repo)
191 191 raise
192 192
193 193 # it's an odd fix to make celery fail task when exception occurs
194 194 def on_failure(self, *args, **kwargs):
195 195 pass
196 196
197 197 return True
198 198
199 199
200 200 @task(ignore_result=True, base=RhodecodeCeleryTask)
201 201 @dbsession
202 202 @vcsconnection
203 203 def create_repo_fork(form_data, cur_user):
204 204 """
205 205 Creates a fork of repository using internal VCS methods
206 206
207 207 :param form_data:
208 208 :param cur_user:
209 209 """
210 210 from rhodecode.model.repo import RepoModel
211 211 from rhodecode.model.user import UserModel
212 212
213 213 log = get_logger(create_repo_fork)
214 214 DBS = get_session()
215 215
216 216 cur_user = UserModel(DBS)._get_user(cur_user)
217 217 owner = cur_user
218 218
219 219 repo_name = form_data['repo_name'] # fork in this case
220 220 repo_name_full = form_data['repo_name_full']
221 221 repo_type = form_data['repo_type']
222 222 description = form_data['description']
223 223 private = form_data['private']
224 224 clone_uri = form_data.get('clone_uri')
225 225 repo_group = safe_int(form_data['repo_group'])
226 226 landing_rev = form_data['landing_rev']
227 227 copy_fork_permissions = form_data.get('copy_permissions')
228 228 fork_id = safe_int(form_data.get('fork_parent_id'))
229 229
230 230 try:
231 231 fork_of = RepoModel(DBS)._get_repo(fork_id)
232 232 RepoModel(DBS)._create_repo(
233 233 repo_name=repo_name_full,
234 234 repo_type=repo_type,
235 235 description=description,
236 236 owner=owner,
237 237 private=private,
238 238 clone_uri=clone_uri,
239 239 repo_group=repo_group,
240 240 landing_rev=landing_rev,
241 241 fork_of=fork_of,
242 242 copy_fork_permissions=copy_fork_permissions
243 243 )
244 244 action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full,
245 245 fork_of.repo_name, '', DBS)
246 246 DBS.commit()
247 247
248 248 base_path = Repository.base_path()
249 249 source_repo_path = os.path.join(base_path, fork_of.repo_name)
250 250
251 251 # now create this repo on Filesystem
252 252 RepoModel(DBS)._create_filesystem_repo(
253 253 repo_name=repo_name,
254 254 repo_type=repo_type,
255 255 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
256 256 clone_uri=source_repo_path,
257 257 )
258 258 repo = Repository.get_by_repo_name(repo_name_full)
259 259 log_create_repository(created_by=owner.username, **repo.get_dict())
260 260
261 261 # update repo commit caches initially
262 262 config = repo._config
263 263 config.set('extensions', 'largefiles', '')
264 264 repo.update_commit_cache(config=config)
265 265
266 266 # set new created state
267 267 repo.set_state(Repository.STATE_CREATED)
268 268 DBS.commit()
269 269 except Exception as e:
270 270 log.warning('Exception %s occurred when forking repository, '
271 271 'doing cleanup...', e)
272 272 # rollback things manually !
273 273 repo = Repository.get_by_repo_name(repo_name_full)
274 274 if repo:
275 275 Repository.delete(repo.repo_id)
276 276 DBS.commit()
277 277 RepoModel(DBS)._delete_filesystem_repo(repo)
278 278 raise
279 279
280 280 # it's an odd fix to make celery fail task when exception occurs
281 281 def on_failure(self, *args, **kwargs):
282 282 pass
283 283
284 284 return True
@@ -1,35 +1,35 b''
1 1 <div tal:omit-tag="field.widget.hidden"
2 2 tal:define="hidden hidden|field.widget.hidden;
3 3 error_class error_class|field.widget.error_class;
4 4 description description|field.description;
5 5 title title|field.title;
6 6 oid oid|field.oid"
7 7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
8 8 i18n:domain="deform">
9 9 <div class="deform-seq-item-group">
10 10 <span tal:replace="structure field.serialize(cstruct)"/>
11 11 <tal:errors condition="field.error and not hidden"
12 12 define="errstr 'error-%s' % oid"
13 13 repeat="msg field.error.messages()">
14 14 <p tal:condition="msg"
15 15 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 class="${error_class} help-block"
16 class="${error_class} help-block error-block"
17 17 i18n:translate="">${msg}</p>
18 18 </tal:errors>
19 19 </div>
20 20 <div class="deform-seq-item-handle" style="padding:0">
21 21 <!-- sequence_item -->
22 22 <span class="deform-order-button close glyphicon glyphicon-resize-vertical"
23 23 id="${oid}-order"
24 24 tal:condition="not hidden"
25 25 title="Reorder (via drag and drop)"
26 26 i18n:attributes="title"></span>
27 27 <a class="deform-close-button close"
28 28 id="${oid}-close"
29 29 tal:condition="not field.widget.hidden"
30 30 title="Remove"
31 31 i18n:attributes="title"
32 32 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
33 33 </div>
34 34 <!-- /sequence_item -->
35 35 </div>
General Comments 0
You need to be logged in to leave comments. Login now