##// END OF EJS Templates
notifications: insert 'References' mail headers to help MUA threading...
Mads Kiilerich -
r4384:05294985 default
parent child Browse files
Show More
@@ -1,506 +1,506 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.celerylib.tasks
15 kallithea.lib.celerylib.tasks
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Kallithea task modules, containing all task that suppose to be run
18 Kallithea task modules, containing all task that suppose to be run
19 by celery daemon
19 by celery daemon
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Oct 6, 2010
23 :created_on: Oct 6, 2010
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 from celery.decorators import task
29 from celery.decorators import task
30
30
31 import os
31 import os
32 import traceback
32 import traceback
33 import logging
33 import logging
34 from os.path import join as jn
34 from os.path import join as jn
35
35
36 from time import mktime
36 from time import mktime
37 from operator import itemgetter
37 from operator import itemgetter
38 from string import lower
38 from string import lower
39
39
40 from pylons import config, url
40 from pylons import config, url
41 from pylons.i18n.translation import _
41 from pylons.i18n.translation import _
42
42
43 from kallithea.lib.vcs import get_backend
43 from kallithea.lib.vcs import get_backend
44
44
45 from kallithea import CELERY_ON, CELERY_EAGER
45 from kallithea import CELERY_ON, CELERY_EAGER
46 from kallithea.lib.utils2 import safe_str
46 from kallithea.lib.utils2 import safe_str
47 from kallithea.lib.celerylib import run_task, locked_task, dbsession, \
47 from kallithea.lib.celerylib import run_task, locked_task, dbsession, \
48 str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
48 str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
49 from kallithea.lib.helpers import person
49 from kallithea.lib.helpers import person
50 from kallithea.lib.rcmail.smtp_mailer import SmtpMailer
50 from kallithea.lib.rcmail.smtp_mailer import SmtpMailer
51 from kallithea.lib.utils import add_cache, action_logger
51 from kallithea.lib.utils import add_cache, action_logger
52 from kallithea.lib.compat import json, OrderedDict
52 from kallithea.lib.compat import json, OrderedDict
53 from kallithea.lib.hooks import log_create_repository
53 from kallithea.lib.hooks import log_create_repository
54
54
55 from kallithea.model.db import Statistics, Repository, User
55 from kallithea.model.db import Statistics, Repository, User
56 from kallithea.model.scm import ScmModel
56 from kallithea.model.scm import ScmModel
57
57
58
58
59 add_cache(config) # pragma: no cover
59 add_cache(config) # pragma: no cover
60
60
61 __all__ = ['whoosh_index', 'get_commits_stats',
61 __all__ = ['whoosh_index', 'get_commits_stats',
62 'reset_user_password', 'send_email']
62 'reset_user_password', 'send_email']
63
63
64
64
65 def get_logger(cls):
65 def get_logger(cls):
66 if CELERY_ON:
66 if CELERY_ON:
67 try:
67 try:
68 log = cls.get_logger()
68 log = cls.get_logger()
69 except Exception:
69 except Exception:
70 log = logging.getLogger(__name__)
70 log = logging.getLogger(__name__)
71 else:
71 else:
72 log = logging.getLogger(__name__)
72 log = logging.getLogger(__name__)
73
73
74 return log
74 return log
75
75
76
76
77 @task(ignore_result=True)
77 @task(ignore_result=True)
78 @locked_task
78 @locked_task
79 @dbsession
79 @dbsession
80 def whoosh_index(repo_location, full_index):
80 def whoosh_index(repo_location, full_index):
81 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
81 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
82 log = get_logger(whoosh_index)
82 log = get_logger(whoosh_index)
83 DBS = get_session()
83 DBS = get_session()
84
84
85 index_location = config['index_dir']
85 index_location = config['index_dir']
86 WhooshIndexingDaemon(index_location=index_location,
86 WhooshIndexingDaemon(index_location=index_location,
87 repo_location=repo_location, sa=DBS)\
87 repo_location=repo_location, sa=DBS)\
88 .run(full_index=full_index)
88 .run(full_index=full_index)
89
89
90
90
91 @task(ignore_result=True)
91 @task(ignore_result=True)
92 @dbsession
92 @dbsession
93 def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100):
93 def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100):
94 log = get_logger(get_commits_stats)
94 log = get_logger(get_commits_stats)
95 DBS = get_session()
95 DBS = get_session()
96 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
96 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
97 ts_max_y)
97 ts_max_y)
98 lockkey_path = config['app_conf']['cache_dir']
98 lockkey_path = config['app_conf']['cache_dir']
99
99
100 log.info('running task with lockkey %s' % lockkey)
100 log.info('running task with lockkey %s' % lockkey)
101
101
102 try:
102 try:
103 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
103 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
104
104
105 # for js data compatibility cleans the key for person from '
105 # for js data compatibility cleans the key for person from '
106 akc = lambda k: person(k).replace('"', "")
106 akc = lambda k: person(k).replace('"', "")
107
107
108 co_day_auth_aggr = {}
108 co_day_auth_aggr = {}
109 commits_by_day_aggregate = {}
109 commits_by_day_aggregate = {}
110 repo = Repository.get_by_repo_name(repo_name)
110 repo = Repository.get_by_repo_name(repo_name)
111 if repo is None:
111 if repo is None:
112 return True
112 return True
113
113
114 repo = repo.scm_instance
114 repo = repo.scm_instance
115 repo_size = repo.count()
115 repo_size = repo.count()
116 # return if repo have no revisions
116 # return if repo have no revisions
117 if repo_size < 1:
117 if repo_size < 1:
118 lock.release()
118 lock.release()
119 return True
119 return True
120
120
121 skip_date_limit = True
121 skip_date_limit = True
122 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
122 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
123 last_rev = None
123 last_rev = None
124 last_cs = None
124 last_cs = None
125 timegetter = itemgetter('time')
125 timegetter = itemgetter('time')
126
126
127 dbrepo = DBS.query(Repository)\
127 dbrepo = DBS.query(Repository)\
128 .filter(Repository.repo_name == repo_name).scalar()
128 .filter(Repository.repo_name == repo_name).scalar()
129 cur_stats = DBS.query(Statistics)\
129 cur_stats = DBS.query(Statistics)\
130 .filter(Statistics.repository == dbrepo).scalar()
130 .filter(Statistics.repository == dbrepo).scalar()
131
131
132 if cur_stats is not None:
132 if cur_stats is not None:
133 last_rev = cur_stats.stat_on_revision
133 last_rev = cur_stats.stat_on_revision
134
134
135 if last_rev == repo.get_changeset().revision and repo_size > 1:
135 if last_rev == repo.get_changeset().revision and repo_size > 1:
136 # pass silently without any work if we're not on first revision or
136 # pass silently without any work if we're not on first revision or
137 # current state of parsing revision(from db marker) is the
137 # current state of parsing revision(from db marker) is the
138 # last revision
138 # last revision
139 lock.release()
139 lock.release()
140 return True
140 return True
141
141
142 if cur_stats:
142 if cur_stats:
143 commits_by_day_aggregate = OrderedDict(json.loads(
143 commits_by_day_aggregate = OrderedDict(json.loads(
144 cur_stats.commit_activity_combined))
144 cur_stats.commit_activity_combined))
145 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
145 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
146
146
147 log.debug('starting parsing %s' % parse_limit)
147 log.debug('starting parsing %s' % parse_limit)
148 lmktime = mktime
148 lmktime = mktime
149
149
150 last_rev = last_rev + 1 if last_rev >= 0 else 0
150 last_rev = last_rev + 1 if last_rev >= 0 else 0
151 log.debug('Getting revisions from %s to %s' % (
151 log.debug('Getting revisions from %s to %s' % (
152 last_rev, last_rev + parse_limit)
152 last_rev, last_rev + parse_limit)
153 )
153 )
154 for cs in repo[last_rev:last_rev + parse_limit]:
154 for cs in repo[last_rev:last_rev + parse_limit]:
155 log.debug('parsing %s' % cs)
155 log.debug('parsing %s' % cs)
156 last_cs = cs # remember last parsed changeset
156 last_cs = cs # remember last parsed changeset
157 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
157 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
158 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
158 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
159
159
160 if akc(cs.author) in co_day_auth_aggr:
160 if akc(cs.author) in co_day_auth_aggr:
161 try:
161 try:
162 l = [timegetter(x) for x in
162 l = [timegetter(x) for x in
163 co_day_auth_aggr[akc(cs.author)]['data']]
163 co_day_auth_aggr[akc(cs.author)]['data']]
164 time_pos = l.index(k)
164 time_pos = l.index(k)
165 except ValueError:
165 except ValueError:
166 time_pos = None
166 time_pos = None
167
167
168 if time_pos >= 0 and time_pos is not None:
168 if time_pos >= 0 and time_pos is not None:
169
169
170 datadict = \
170 datadict = \
171 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
171 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
172
172
173 datadict["commits"] += 1
173 datadict["commits"] += 1
174 datadict["added"] += len(cs.added)
174 datadict["added"] += len(cs.added)
175 datadict["changed"] += len(cs.changed)
175 datadict["changed"] += len(cs.changed)
176 datadict["removed"] += len(cs.removed)
176 datadict["removed"] += len(cs.removed)
177
177
178 else:
178 else:
179 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
179 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
180
180
181 datadict = {"time": k,
181 datadict = {"time": k,
182 "commits": 1,
182 "commits": 1,
183 "added": len(cs.added),
183 "added": len(cs.added),
184 "changed": len(cs.changed),
184 "changed": len(cs.changed),
185 "removed": len(cs.removed),
185 "removed": len(cs.removed),
186 }
186 }
187 co_day_auth_aggr[akc(cs.author)]['data']\
187 co_day_auth_aggr[akc(cs.author)]['data']\
188 .append(datadict)
188 .append(datadict)
189
189
190 else:
190 else:
191 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
191 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
192 co_day_auth_aggr[akc(cs.author)] = {
192 co_day_auth_aggr[akc(cs.author)] = {
193 "label": akc(cs.author),
193 "label": akc(cs.author),
194 "data": [{"time":k,
194 "data": [{"time":k,
195 "commits":1,
195 "commits":1,
196 "added":len(cs.added),
196 "added":len(cs.added),
197 "changed":len(cs.changed),
197 "changed":len(cs.changed),
198 "removed":len(cs.removed),
198 "removed":len(cs.removed),
199 }],
199 }],
200 "schema": ["commits"],
200 "schema": ["commits"],
201 }
201 }
202
202
203 #gather all data by day
203 #gather all data by day
204 if k in commits_by_day_aggregate:
204 if k in commits_by_day_aggregate:
205 commits_by_day_aggregate[k] += 1
205 commits_by_day_aggregate[k] += 1
206 else:
206 else:
207 commits_by_day_aggregate[k] = 1
207 commits_by_day_aggregate[k] = 1
208
208
209 overview_data = sorted(commits_by_day_aggregate.items(),
209 overview_data = sorted(commits_by_day_aggregate.items(),
210 key=itemgetter(0))
210 key=itemgetter(0))
211
211
212 if not co_day_auth_aggr:
212 if not co_day_auth_aggr:
213 co_day_auth_aggr[akc(repo.contact)] = {
213 co_day_auth_aggr[akc(repo.contact)] = {
214 "label": akc(repo.contact),
214 "label": akc(repo.contact),
215 "data": [0, 1],
215 "data": [0, 1],
216 "schema": ["commits"],
216 "schema": ["commits"],
217 }
217 }
218
218
219 stats = cur_stats if cur_stats else Statistics()
219 stats = cur_stats if cur_stats else Statistics()
220 stats.commit_activity = json.dumps(co_day_auth_aggr)
220 stats.commit_activity = json.dumps(co_day_auth_aggr)
221 stats.commit_activity_combined = json.dumps(overview_data)
221 stats.commit_activity_combined = json.dumps(overview_data)
222
222
223 log.debug('last revison %s' % last_rev)
223 log.debug('last revison %s' % last_rev)
224 leftovers = len(repo.revisions[last_rev:])
224 leftovers = len(repo.revisions[last_rev:])
225 log.debug('revisions to parse %s' % leftovers)
225 log.debug('revisions to parse %s' % leftovers)
226
226
227 if last_rev == 0 or leftovers < parse_limit:
227 if last_rev == 0 or leftovers < parse_limit:
228 log.debug('getting code trending stats')
228 log.debug('getting code trending stats')
229 stats.languages = json.dumps(__get_codes_stats(repo_name))
229 stats.languages = json.dumps(__get_codes_stats(repo_name))
230
230
231 try:
231 try:
232 stats.repository = dbrepo
232 stats.repository = dbrepo
233 stats.stat_on_revision = last_cs.revision if last_cs else 0
233 stats.stat_on_revision = last_cs.revision if last_cs else 0
234 DBS.add(stats)
234 DBS.add(stats)
235 DBS.commit()
235 DBS.commit()
236 except:
236 except:
237 log.error(traceback.format_exc())
237 log.error(traceback.format_exc())
238 DBS.rollback()
238 DBS.rollback()
239 lock.release()
239 lock.release()
240 return False
240 return False
241
241
242 # final release
242 # final release
243 lock.release()
243 lock.release()
244
244
245 # execute another task if celery is enabled
245 # execute another task if celery is enabled
246 if len(repo.revisions) > 1 and CELERY_ON and recurse_limit > 0:
246 if len(repo.revisions) > 1 and CELERY_ON and recurse_limit > 0:
247 recurse_limit -= 1
247 recurse_limit -= 1
248 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y,
248 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y,
249 recurse_limit)
249 recurse_limit)
250 if recurse_limit <= 0:
250 if recurse_limit <= 0:
251 log.debug('Breaking recursive mode due to reach of recurse limit')
251 log.debug('Breaking recursive mode due to reach of recurse limit')
252 return True
252 return True
253 except LockHeld:
253 except LockHeld:
254 log.info('LockHeld')
254 log.info('LockHeld')
255 return 'Task with key %s already running' % lockkey
255 return 'Task with key %s already running' % lockkey
256
256
257
257
258 @task(ignore_result=True)
258 @task(ignore_result=True)
259 @dbsession
259 @dbsession
260 def send_email(recipients, subject, body='', html_body=''):
260 def send_email(recipients, subject, body='', html_body='', headers=None):
261 """
261 """
262 Sends an email with defined parameters from the .ini files.
262 Sends an email with defined parameters from the .ini files.
263
263
264 :param recipients: list of recipients, if this is None, the defined email
264 :param recipients: list of recipients, if this is None, the defined email
265 address from field 'email_to' and all admins is used instead
265 address from field 'email_to' and all admins is used instead
266 :param subject: subject of the mail
266 :param subject: subject of the mail
267 :param body: body of the mail
267 :param body: body of the mail
268 :param html_body: html version of body
268 :param html_body: html version of body
269 """
269 """
270 log = get_logger(send_email)
270 log = get_logger(send_email)
271 DBS = get_session()
271 DBS = get_session()
272 assert isinstance(recipients, list), recipients
272 assert isinstance(recipients, list), recipients
273
273
274 email_config = config
274 email_config = config
275 email_prefix = email_config.get('email_prefix', '')
275 email_prefix = email_config.get('email_prefix', '')
276 if email_prefix:
276 if email_prefix:
277 subject = "%s %s" % (email_prefix, subject)
277 subject = "%s %s" % (email_prefix, subject)
278 if recipients is None:
278 if recipients is None:
279 # if recipients are not defined we send to email_config + all admins
279 # if recipients are not defined we send to email_config + all admins
280 admins = [u.email for u in User.query()
280 admins = [u.email for u in User.query()
281 .filter(User.admin == True).all()]
281 .filter(User.admin == True).all()]
282 recipients = [email_config.get('email_to')] + admins
282 recipients = [email_config.get('email_to')] + admins
283 log.warning("recipients not specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
283 log.warning("recipients not specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
284 elif not recipients:
284 elif not recipients:
285 log.error("No recipients specified")
285 log.error("No recipients specified")
286 return False
286 return False
287
287
288 mail_from = email_config.get('app_email_from', 'Kallithea')
288 mail_from = email_config.get('app_email_from', 'Kallithea')
289 user = email_config.get('smtp_username')
289 user = email_config.get('smtp_username')
290 passwd = email_config.get('smtp_password')
290 passwd = email_config.get('smtp_password')
291 mail_server = email_config.get('smtp_server')
291 mail_server = email_config.get('smtp_server')
292 mail_port = email_config.get('smtp_port')
292 mail_port = email_config.get('smtp_port')
293 tls = str2bool(email_config.get('smtp_use_tls'))
293 tls = str2bool(email_config.get('smtp_use_tls'))
294 ssl = str2bool(email_config.get('smtp_use_ssl'))
294 ssl = str2bool(email_config.get('smtp_use_ssl'))
295 debug = str2bool(email_config.get('debug'))
295 debug = str2bool(email_config.get('debug'))
296 smtp_auth = email_config.get('smtp_auth')
296 smtp_auth = email_config.get('smtp_auth')
297
297
298 if not mail_server:
298 if not mail_server:
299 log.error("SMTP mail server not configured - cannot send mail '%s' to %s", subject, ' '.join(recipients))
299 log.error("SMTP mail server not configured - cannot send mail '%s' to %s", subject, ' '.join(recipients))
300 log.warning("body:\n%s", body)
300 log.warning("body:\n%s", body)
301 log.warning("html:\n%s", html_body)
301 log.warning("html:\n%s", html_body)
302 return False
302 return False
303
303
304 try:
304 try:
305 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
305 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
306 mail_port, ssl, tls, debug=debug)
306 mail_port, ssl, tls, debug=debug)
307 m.send(recipients, subject, body, html_body)
307 m.send(recipients, subject, body, html_body, headers=headers)
308 except:
308 except:
309 log.error('Mail sending failed')
309 log.error('Mail sending failed')
310 log.error(traceback.format_exc())
310 log.error(traceback.format_exc())
311 return False
311 return False
312 return True
312 return True
313
313
314 @task(ignore_result=False)
314 @task(ignore_result=False)
315 @dbsession
315 @dbsession
316 def create_repo(form_data, cur_user):
316 def create_repo(form_data, cur_user):
317 from kallithea.model.repo import RepoModel
317 from kallithea.model.repo import RepoModel
318 from kallithea.model.user import UserModel
318 from kallithea.model.user import UserModel
319 from kallithea.model.db import Setting
319 from kallithea.model.db import Setting
320
320
321 log = get_logger(create_repo)
321 log = get_logger(create_repo)
322 DBS = get_session()
322 DBS = get_session()
323
323
324 cur_user = UserModel(DBS)._get_user(cur_user)
324 cur_user = UserModel(DBS)._get_user(cur_user)
325
325
326 owner = cur_user
326 owner = cur_user
327 repo_name = form_data['repo_name']
327 repo_name = form_data['repo_name']
328 repo_name_full = form_data['repo_name_full']
328 repo_name_full = form_data['repo_name_full']
329 repo_type = form_data['repo_type']
329 repo_type = form_data['repo_type']
330 description = form_data['repo_description']
330 description = form_data['repo_description']
331 private = form_data['repo_private']
331 private = form_data['repo_private']
332 clone_uri = form_data.get('clone_uri')
332 clone_uri = form_data.get('clone_uri')
333 repo_group = form_data['repo_group']
333 repo_group = form_data['repo_group']
334 landing_rev = form_data['repo_landing_rev']
334 landing_rev = form_data['repo_landing_rev']
335 copy_fork_permissions = form_data.get('copy_permissions')
335 copy_fork_permissions = form_data.get('copy_permissions')
336 copy_group_permissions = form_data.get('repo_copy_permissions')
336 copy_group_permissions = form_data.get('repo_copy_permissions')
337 fork_of = form_data.get('fork_parent_id')
337 fork_of = form_data.get('fork_parent_id')
338 state = form_data.get('repo_state', Repository.STATE_PENDING)
338 state = form_data.get('repo_state', Repository.STATE_PENDING)
339
339
340 # repo creation defaults, private and repo_type are filled in form
340 # repo creation defaults, private and repo_type are filled in form
341 defs = Setting.get_default_repo_settings(strip_prefix=True)
341 defs = Setting.get_default_repo_settings(strip_prefix=True)
342 enable_statistics = defs.get('repo_enable_statistics')
342 enable_statistics = defs.get('repo_enable_statistics')
343 enable_locking = defs.get('repo_enable_locking')
343 enable_locking = defs.get('repo_enable_locking')
344 enable_downloads = defs.get('repo_enable_downloads')
344 enable_downloads = defs.get('repo_enable_downloads')
345
345
346 try:
346 try:
347 repo = RepoModel(DBS)._create_repo(
347 repo = RepoModel(DBS)._create_repo(
348 repo_name=repo_name_full,
348 repo_name=repo_name_full,
349 repo_type=repo_type,
349 repo_type=repo_type,
350 description=description,
350 description=description,
351 owner=owner,
351 owner=owner,
352 private=private,
352 private=private,
353 clone_uri=clone_uri,
353 clone_uri=clone_uri,
354 repo_group=repo_group,
354 repo_group=repo_group,
355 landing_rev=landing_rev,
355 landing_rev=landing_rev,
356 fork_of=fork_of,
356 fork_of=fork_of,
357 copy_fork_permissions=copy_fork_permissions,
357 copy_fork_permissions=copy_fork_permissions,
358 copy_group_permissions=copy_group_permissions,
358 copy_group_permissions=copy_group_permissions,
359 enable_statistics=enable_statistics,
359 enable_statistics=enable_statistics,
360 enable_locking=enable_locking,
360 enable_locking=enable_locking,
361 enable_downloads=enable_downloads,
361 enable_downloads=enable_downloads,
362 state=state
362 state=state
363 )
363 )
364
364
365 action_logger(cur_user, 'user_created_repo',
365 action_logger(cur_user, 'user_created_repo',
366 form_data['repo_name_full'], '', DBS)
366 form_data['repo_name_full'], '', DBS)
367
367
368 DBS.commit()
368 DBS.commit()
369 # now create this repo on Filesystem
369 # now create this repo on Filesystem
370 RepoModel(DBS)._create_filesystem_repo(
370 RepoModel(DBS)._create_filesystem_repo(
371 repo_name=repo_name,
371 repo_name=repo_name,
372 repo_type=repo_type,
372 repo_type=repo_type,
373 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
373 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
374 clone_uri=clone_uri,
374 clone_uri=clone_uri,
375 )
375 )
376 repo = Repository.get_by_repo_name(repo_name_full)
376 repo = Repository.get_by_repo_name(repo_name_full)
377 log_create_repository(repo.get_dict(), created_by=owner.username)
377 log_create_repository(repo.get_dict(), created_by=owner.username)
378
378
379 # update repo changeset caches initially
379 # update repo changeset caches initially
380 repo.update_changeset_cache()
380 repo.update_changeset_cache()
381
381
382 # set new created state
382 # set new created state
383 repo.set_state(Repository.STATE_CREATED)
383 repo.set_state(Repository.STATE_CREATED)
384 DBS.commit()
384 DBS.commit()
385 except Exception, e:
385 except Exception, e:
386 log.warning('Exception %s occured when forking repository, '
386 log.warning('Exception %s occured when forking repository, '
387 'doing cleanup...' % e)
387 'doing cleanup...' % e)
388 # rollback things manually !
388 # rollback things manually !
389 repo = Repository.get_by_repo_name(repo_name_full)
389 repo = Repository.get_by_repo_name(repo_name_full)
390 if repo:
390 if repo:
391 Repository.delete(repo.repo_id)
391 Repository.delete(repo.repo_id)
392 DBS.commit()
392 DBS.commit()
393 RepoModel(DBS)._delete_filesystem_repo(repo)
393 RepoModel(DBS)._delete_filesystem_repo(repo)
394 raise
394 raise
395
395
396 # it's an odd fix to make celery fail task when exception occurs
396 # it's an odd fix to make celery fail task when exception occurs
397 def on_failure(self, *args, **kwargs):
397 def on_failure(self, *args, **kwargs):
398 pass
398 pass
399
399
400 return True
400 return True
401
401
402
402
403 @task(ignore_result=False)
403 @task(ignore_result=False)
404 @dbsession
404 @dbsession
405 def create_repo_fork(form_data, cur_user):
405 def create_repo_fork(form_data, cur_user):
406 """
406 """
407 Creates a fork of repository using interval VCS methods
407 Creates a fork of repository using interval VCS methods
408
408
409 :param form_data:
409 :param form_data:
410 :param cur_user:
410 :param cur_user:
411 """
411 """
412 from kallithea.model.repo import RepoModel
412 from kallithea.model.repo import RepoModel
413 from kallithea.model.user import UserModel
413 from kallithea.model.user import UserModel
414
414
415 log = get_logger(create_repo_fork)
415 log = get_logger(create_repo_fork)
416 DBS = get_session()
416 DBS = get_session()
417
417
418 base_path = Repository.base_path()
418 base_path = Repository.base_path()
419 cur_user = UserModel(DBS)._get_user(cur_user)
419 cur_user = UserModel(DBS)._get_user(cur_user)
420
420
421 repo_name = form_data['repo_name'] # fork in this case
421 repo_name = form_data['repo_name'] # fork in this case
422 repo_name_full = form_data['repo_name_full']
422 repo_name_full = form_data['repo_name_full']
423
423
424 repo_type = form_data['repo_type']
424 repo_type = form_data['repo_type']
425 owner = cur_user
425 owner = cur_user
426 private = form_data['private']
426 private = form_data['private']
427 clone_uri = form_data.get('clone_uri')
427 clone_uri = form_data.get('clone_uri')
428 repo_group = form_data['repo_group']
428 repo_group = form_data['repo_group']
429 landing_rev = form_data['landing_rev']
429 landing_rev = form_data['landing_rev']
430 copy_fork_permissions = form_data.get('copy_permissions')
430 copy_fork_permissions = form_data.get('copy_permissions')
431
431
432 try:
432 try:
433 fork_of = RepoModel(DBS)._get_repo(form_data.get('fork_parent_id'))
433 fork_of = RepoModel(DBS)._get_repo(form_data.get('fork_parent_id'))
434
434
435 fork_repo = RepoModel(DBS)._create_repo(
435 fork_repo = RepoModel(DBS)._create_repo(
436 repo_name=repo_name_full,
436 repo_name=repo_name_full,
437 repo_type=repo_type,
437 repo_type=repo_type,
438 description=form_data['description'],
438 description=form_data['description'],
439 owner=owner,
439 owner=owner,
440 private=private,
440 private=private,
441 clone_uri=clone_uri,
441 clone_uri=clone_uri,
442 repo_group=repo_group,
442 repo_group=repo_group,
443 landing_rev=landing_rev,
443 landing_rev=landing_rev,
444 fork_of=fork_of,
444 fork_of=fork_of,
445 copy_fork_permissions=copy_fork_permissions
445 copy_fork_permissions=copy_fork_permissions
446 )
446 )
447 action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full,
447 action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full,
448 fork_of.repo_name, '', DBS)
448 fork_of.repo_name, '', DBS)
449 DBS.commit()
449 DBS.commit()
450
450
451 update_after_clone = form_data['update_after_clone']
451 update_after_clone = form_data['update_after_clone']
452 source_repo_path = os.path.join(base_path, fork_of.repo_name)
452 source_repo_path = os.path.join(base_path, fork_of.repo_name)
453
453
454 # now create this repo on Filesystem
454 # now create this repo on Filesystem
455 RepoModel(DBS)._create_filesystem_repo(
455 RepoModel(DBS)._create_filesystem_repo(
456 repo_name=repo_name,
456 repo_name=repo_name,
457 repo_type=repo_type,
457 repo_type=repo_type,
458 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
458 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
459 clone_uri=source_repo_path,
459 clone_uri=source_repo_path,
460 )
460 )
461 repo = Repository.get_by_repo_name(repo_name_full)
461 repo = Repository.get_by_repo_name(repo_name_full)
462 log_create_repository(repo.get_dict(), created_by=owner.username)
462 log_create_repository(repo.get_dict(), created_by=owner.username)
463
463
464 # update repo changeset caches initially
464 # update repo changeset caches initially
465 repo.update_changeset_cache()
465 repo.update_changeset_cache()
466
466
467 # set new created state
467 # set new created state
468 repo.set_state(Repository.STATE_CREATED)
468 repo.set_state(Repository.STATE_CREATED)
469 DBS.commit()
469 DBS.commit()
470 except Exception, e:
470 except Exception, e:
471 log.warning('Exception %s occured when forking repository, '
471 log.warning('Exception %s occured when forking repository, '
472 'doing cleanup...' % e)
472 'doing cleanup...' % e)
473 #rollback things manually !
473 #rollback things manually !
474 repo = Repository.get_by_repo_name(repo_name_full)
474 repo = Repository.get_by_repo_name(repo_name_full)
475 if repo:
475 if repo:
476 Repository.delete(repo.repo_id)
476 Repository.delete(repo.repo_id)
477 DBS.commit()
477 DBS.commit()
478 RepoModel(DBS)._delete_filesystem_repo(repo)
478 RepoModel(DBS)._delete_filesystem_repo(repo)
479 raise
479 raise
480
480
481 # it's an odd fix to make celery fail task when exception occurs
481 # it's an odd fix to make celery fail task when exception occurs
482 def on_failure(self, *args, **kwargs):
482 def on_failure(self, *args, **kwargs):
483 pass
483 pass
484
484
485 return True
485 return True
486
486
487
487
488 def __get_codes_stats(repo_name):
488 def __get_codes_stats(repo_name):
489 from kallithea.config.conf import LANGUAGES_EXTENSIONS_MAP
489 from kallithea.config.conf import LANGUAGES_EXTENSIONS_MAP
490 repo = Repository.get_by_repo_name(repo_name).scm_instance
490 repo = Repository.get_by_repo_name(repo_name).scm_instance
491
491
492 tip = repo.get_changeset()
492 tip = repo.get_changeset()
493 code_stats = {}
493 code_stats = {}
494
494
495 def aggregate(cs):
495 def aggregate(cs):
496 for f in cs[2]:
496 for f in cs[2]:
497 ext = lower(f.extension)
497 ext = lower(f.extension)
498 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
498 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
499 if ext in code_stats:
499 if ext in code_stats:
500 code_stats[ext] += 1
500 code_stats[ext] += 1
501 else:
501 else:
502 code_stats[ext] = 1
502 code_stats[ext] = 1
503
503
504 map(aggregate, tip.walk('/'))
504 map(aggregate, tip.walk('/'))
505
505
506 return code_stats or {}
506 return code_stats or {}
@@ -1,105 +1,105 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.rcmail.smtp_mailer
15 kallithea.lib.rcmail.smtp_mailer
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Simple smtp mailer used in Kallithea
18 Simple smtp mailer used in Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Sep 13, 2010
22 :created_on: Sep 13, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import time
28 import time
29 import logging
29 import logging
30 import smtplib
30 import smtplib
31 from socket import sslerror
31 from socket import sslerror
32 from email.utils import formatdate
32 from email.utils import formatdate
33 from kallithea.lib.rcmail.message import Message
33 from kallithea.lib.rcmail.message import Message
34 from kallithea.lib.rcmail.utils import DNS_NAME
34 from kallithea.lib.rcmail.utils import DNS_NAME
35
35
36
36
37 class SmtpMailer(object):
37 class SmtpMailer(object):
38 """SMTP mailer class
38 """SMTP mailer class
39
39
40 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
40 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
41 mail_port, ssl, tls)
41 mail_port, ssl, tls)
42 mailer.send(recipients, subject, body, attachment_files)
42 mailer.send(recipients, subject, body, attachment_files)
43
43
44 :param recipients might be a list of string or single string
44 :param recipients might be a list of string or single string
45 :param attachment_files is a dict of {filename:location}
45 :param attachment_files is a dict of {filename:location}
46 it tries to guess the mimetype and attach the file
46 it tries to guess the mimetype and attach the file
47
47
48 """
48 """
49
49
50 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
50 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
51 mail_port=None, ssl=False, tls=False, debug=False):
51 mail_port=None, ssl=False, tls=False, debug=False):
52
52
53 self.mail_from = mail_from
53 self.mail_from = mail_from
54 self.mail_server = mail_server
54 self.mail_server = mail_server
55 self.mail_port = mail_port
55 self.mail_port = mail_port
56 self.user = user
56 self.user = user
57 self.passwd = passwd
57 self.passwd = passwd
58 self.ssl = ssl
58 self.ssl = ssl
59 self.tls = tls
59 self.tls = tls
60 self.debug = debug
60 self.debug = debug
61 self.auth = smtp_auth
61 self.auth = smtp_auth
62
62
63 def send(self, recipients=[], subject='', body='', html='',
63 def send(self, recipients=[], subject='', body='', html='',
64 attachment_files=None):
64 attachment_files=None, headers=None):
65
65
66 if isinstance(recipients, basestring):
66 if isinstance(recipients, basestring):
67 recipients = [recipients]
67 recipients = [recipients]
68 headers = {
68 if headers is None:
69 'Date': formatdate(time.time())
69 headers = {}
70 }
70 headers.setdefault('Date', formatdate(time.time()))
71 msg = Message(subject, recipients, body, html, self.mail_from,
71 msg = Message(subject, recipients, body, html, self.mail_from,
72 recipients_separator=", ", extra_headers=headers)
72 recipients_separator=", ", extra_headers=headers)
73 raw_msg = msg.to_message()
73 raw_msg = msg.to_message()
74
74
75 if self.ssl:
75 if self.ssl:
76 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port,
76 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port,
77 local_hostname=DNS_NAME.get_fqdn())
77 local_hostname=DNS_NAME.get_fqdn())
78 else:
78 else:
79 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port,
79 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port,
80 local_hostname=DNS_NAME.get_fqdn())
80 local_hostname=DNS_NAME.get_fqdn())
81
81
82 if self.tls:
82 if self.tls:
83 smtp_serv.ehlo()
83 smtp_serv.ehlo()
84 smtp_serv.starttls()
84 smtp_serv.starttls()
85
85
86 if self.debug:
86 if self.debug:
87 smtp_serv.set_debuglevel(1)
87 smtp_serv.set_debuglevel(1)
88
88
89 smtp_serv.ehlo()
89 smtp_serv.ehlo()
90 if self.auth:
90 if self.auth:
91 smtp_serv.esmtp_features["auth"] = self.auth
91 smtp_serv.esmtp_features["auth"] = self.auth
92
92
93 # if server requires authorization you must provide login and password
93 # if server requires authorization you must provide login and password
94 # but only if we have them
94 # but only if we have them
95 if self.user and self.passwd:
95 if self.user and self.passwd:
96 smtp_serv.login(self.user, self.passwd)
96 smtp_serv.login(self.user, self.passwd)
97
97
98 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
98 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
99 logging.info('MAIL SENT TO: %s' % recipients)
99 logging.info('MAIL SENT TO: %s' % recipients)
100
100
101 try:
101 try:
102 smtp_serv.quit()
102 smtp_serv.quit()
103 except sslerror:
103 except sslerror:
104 # sslerror is raised in tls connections on closing sometimes
104 # sslerror is raised in tls connections on closing sometimes
105 smtp_serv.close()
105 smtp_serv.close()
@@ -1,291 +1,301 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.comment
15 kallithea.model.comment
16 ~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 comments model for Kallithea
18 comments model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Nov 11, 2011
22 :created_on: Nov 11, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from sqlalchemy.util.compat import defaultdict
32 from sqlalchemy.util.compat import defaultdict
33
33
34 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
34 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
35 from kallithea.lib import helpers as h
35 from kallithea.lib import helpers as h
36 from kallithea.model import BaseModel
36 from kallithea.model import BaseModel
37 from kallithea.model.db import ChangesetComment, User, Repository, \
37 from kallithea.model.db import ChangesetComment, User, Repository, \
38 Notification, PullRequest
38 Notification, PullRequest
39 from kallithea.model.notification import NotificationModel
39 from kallithea.model.notification import NotificationModel
40 from kallithea.model.meta import Session
40 from kallithea.model.meta import Session
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class ChangesetCommentsModel(BaseModel):
45 class ChangesetCommentsModel(BaseModel):
46
46
47 cls = ChangesetComment
47 cls = ChangesetComment
48
48
49 def __get_changeset_comment(self, changeset_comment):
49 def __get_changeset_comment(self, changeset_comment):
50 return self._get_instance(ChangesetComment, changeset_comment)
50 return self._get_instance(ChangesetComment, changeset_comment)
51
51
52 def __get_pull_request(self, pull_request):
52 def __get_pull_request(self, pull_request):
53 return self._get_instance(PullRequest, pull_request)
53 return self._get_instance(PullRequest, pull_request)
54
54
55 def _extract_mentions(self, s):
55 def _extract_mentions(self, s):
56 user_objects = []
56 user_objects = []
57 for username in extract_mentioned_users(s):
57 for username in extract_mentioned_users(s):
58 user_obj = User.get_by_username(username, case_insensitive=True)
58 user_obj = User.get_by_username(username, case_insensitive=True)
59 if user_obj:
59 if user_obj:
60 user_objects.append(user_obj)
60 user_objects.append(user_obj)
61 return user_objects
61 return user_objects
62
62
63 def _get_notification_data(self, repo, comment, user, comment_text,
63 def _get_notification_data(self, repo, comment, user, comment_text,
64 line_no=None, revision=None, pull_request=None,
64 line_no=None, revision=None, pull_request=None,
65 status_change=None, closing_pr=False):
65 status_change=None, closing_pr=False):
66 """
66 """
67 Get notification data
67 Get notification data
68
68
69 :param comment_text:
69 :param comment_text:
70 :param line:
70 :param line:
71 :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
71 :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
72 """
72 """
73 # make notification
73 # make notification
74 body = comment_text # text of the comment
74 body = comment_text # text of the comment
75 line = ''
75 line = ''
76 if line_no:
76 if line_no:
77 line = _('on line %s') % line_no
77 line = _('on line %s') % line_no
78
78
79 #changeset
79 #changeset
80 if revision:
80 if revision:
81 notification_type = Notification.TYPE_CHANGESET_COMMENT
81 notification_type = Notification.TYPE_CHANGESET_COMMENT
82 cs = repo.scm_instance.get_changeset(revision)
82 cs = repo.scm_instance.get_changeset(revision)
83 desc = "%s" % (cs.short_id)
83 desc = "%s" % (cs.short_id)
84
84
85 _url = h.url('changeset_home',
85 revision_url = h.url('changeset_home',
86 repo_name=repo.repo_name,
87 revision=revision,
88 qualified=True,)
89 comment_url = h.url('changeset_home',
86 repo_name=repo.repo_name,
90 repo_name=repo.repo_name,
87 revision=revision,
91 revision=revision,
88 anchor='comment-%s' % comment.comment_id,
92 anchor='comment-%s' % comment.comment_id,
89 qualified=True,
93 qualified=True,
90 )
94 )
91 subj = safe_unicode(
95 subj = safe_unicode(
92 h.link_to('Re changeset: %(desc)s %(line)s' % \
96 h.link_to('Re changeset: %(desc)s %(line)s' % \
93 {'desc': desc, 'line': line},
97 {'desc': desc, 'line': line},
94 _url)
98 comment_url)
95 )
99 )
96 # get the current participants of this changeset
100 # get the current participants of this changeset
97 recipients = ChangesetComment.get_users(revision=revision)
101 recipients = ChangesetComment.get_users(revision=revision)
98 # add changeset author if it's in kallithea system
102 # add changeset author if it's in kallithea system
99 cs_author = User.get_from_cs_author(cs.author)
103 cs_author = User.get_from_cs_author(cs.author)
100 if not cs_author:
104 if not cs_author:
101 #use repo owner if we cannot extract the author correctly
105 #use repo owner if we cannot extract the author correctly
102 cs_author = repo.user
106 cs_author = repo.user
103 recipients += [cs_author]
107 recipients += [cs_author]
104 email_kwargs = {
108 email_kwargs = {
105 'status_change': status_change,
109 'status_change': status_change,
106 'cs_comment_user': h.person(user),
110 'cs_comment_user': h.person(user),
107 'cs_target_repo': h.url('summary_home', repo_name=repo.repo_name,
111 'cs_target_repo': h.url('summary_home', repo_name=repo.repo_name,
108 qualified=True),
112 qualified=True),
109 'cs_comment_url': _url,
113 'cs_comment_url': comment_url,
110 'raw_id': revision,
114 'raw_id': revision,
111 'message': cs.message,
115 'message': cs.message,
112 'repo_name': repo.repo_name,
116 'repo_name': repo.repo_name,
113 'short_id': h.short_id(revision),
117 'short_id': h.short_id(revision),
114 'branch': cs.branch,
118 'branch': cs.branch,
115 'comment_username': user.username,
119 'comment_username': user.username,
120 'threading': [revision_url, comment_url], # TODO: url to line number
116 }
121 }
117 #pull request
122 #pull request
118 elif pull_request:
123 elif pull_request:
119 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
124 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
120 desc = comment.pull_request.title
125 desc = comment.pull_request.title
121 _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
126 _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
122 _url = h.url('pullrequest_show',
127 pr_url = h.url('pullrequest_show',
128 repo_name=pull_request.other_repo.repo_name,
129 pull_request_id=pull_request.pull_request_id,
130 qualified=True,)
131 comment_url = h.url('pullrequest_show',
123 repo_name=pull_request.other_repo.repo_name,
132 repo_name=pull_request.other_repo.repo_name,
124 pull_request_id=pull_request.pull_request_id,
133 pull_request_id=pull_request.pull_request_id,
125 anchor='comment-%s' % comment.comment_id,
134 anchor='comment-%s' % comment.comment_id,
126 qualified=True,
135 qualified=True,
127 )
136 )
128 subj = safe_unicode(
137 subj = safe_unicode(
129 h.link_to('Re pull request #%(pr_id)s: %(desc)s %(line)s' % \
138 h.link_to('Re pull request #%(pr_id)s: %(desc)s %(line)s' % \
130 {'desc': desc,
139 {'desc': desc,
131 'pr_id': comment.pull_request.pull_request_id,
140 'pr_id': comment.pull_request.pull_request_id,
132 'line': line},
141 'line': line},
133 _url)
142 comment_url)
134 )
143 )
135 # get the current participants of this pull request
144 # get the current participants of this pull request
136 recipients = ChangesetComment.get_users(pull_request_id=
145 recipients = ChangesetComment.get_users(pull_request_id=
137 pull_request.pull_request_id)
146 pull_request.pull_request_id)
138 # add pull request author
147 # add pull request author
139 recipients += [pull_request.author]
148 recipients += [pull_request.author]
140
149
141 # add the reviewers to notification
150 # add the reviewers to notification
142 recipients += [x.user for x in pull_request.reviewers]
151 recipients += [x.user for x in pull_request.reviewers]
143
152
144 #set some variables for email notification
153 #set some variables for email notification
145 email_kwargs = {
154 email_kwargs = {
146 'pr_title': pull_request.title,
155 'pr_title': pull_request.title,
147 'pr_id': pull_request.pull_request_id,
156 'pr_id': pull_request.pull_request_id,
148 'status_change': status_change,
157 'status_change': status_change,
149 'closing_pr': closing_pr,
158 'closing_pr': closing_pr,
150 'pr_comment_url': _url,
159 'pr_comment_url': comment_url,
151 'pr_comment_user': h.person(user),
160 'pr_comment_user': h.person(user),
152 'pr_target_repo': h.url('summary_home',
161 'pr_target_repo': h.url('summary_home',
153 repo_name=pull_request.other_repo.repo_name,
162 repo_name=pull_request.other_repo.repo_name,
154 qualified=True),
163 qualified=True),
155 'repo_name': pull_request.other_repo.repo_name,
164 'repo_name': pull_request.other_repo.repo_name,
156 'ref': org_ref_name,
165 'ref': org_ref_name,
157 'comment_username': user.username,
166 'comment_username': user.username,
167 'threading': [pr_url, comment_url], # TODO: url to line number
158 }
168 }
159
169
160 return subj, body, recipients, notification_type, email_kwargs
170 return subj, body, recipients, notification_type, email_kwargs
161
171
162 def create(self, text, repo, user, revision=None, pull_request=None,
172 def create(self, text, repo, user, revision=None, pull_request=None,
163 f_path=None, line_no=None, status_change=None, closing_pr=False,
173 f_path=None, line_no=None, status_change=None, closing_pr=False,
164 send_email=True):
174 send_email=True):
165 """
175 """
166 Creates new comment for changeset or pull request.
176 Creates new comment for changeset or pull request.
167 If status_change is not None this comment is associated with a
177 If status_change is not None this comment is associated with a
168 status change of changeset or changesets associated with pull request
178 status change of changeset or changesets associated with pull request
169
179
170 :param text:
180 :param text:
171 :param repo:
181 :param repo:
172 :param user:
182 :param user:
173 :param revision:
183 :param revision:
174 :param pull_request: (for emails, not for comments)
184 :param pull_request: (for emails, not for comments)
175 :param f_path:
185 :param f_path:
176 :param line_no:
186 :param line_no:
177 :param status_change: (for emails, not for comments)
187 :param status_change: (for emails, not for comments)
178 :param closing_pr: (for emails, not for comments)
188 :param closing_pr: (for emails, not for comments)
179 :param send_email: also send email
189 :param send_email: also send email
180 """
190 """
181 if not text:
191 if not text:
182 log.warning('Missing text for comment, skipping...')
192 log.warning('Missing text for comment, skipping...')
183 return
193 return
184
194
185 repo = self._get_repo(repo)
195 repo = self._get_repo(repo)
186 user = self._get_user(user)
196 user = self._get_user(user)
187 comment = ChangesetComment()
197 comment = ChangesetComment()
188 comment.repo = repo
198 comment.repo = repo
189 comment.author = user
199 comment.author = user
190 comment.text = text
200 comment.text = text
191 comment.f_path = f_path
201 comment.f_path = f_path
192 comment.line_no = line_no
202 comment.line_no = line_no
193
203
194 if revision:
204 if revision:
195 comment.revision = revision
205 comment.revision = revision
196 elif pull_request:
206 elif pull_request:
197 pull_request = self.__get_pull_request(pull_request)
207 pull_request = self.__get_pull_request(pull_request)
198 comment.pull_request = pull_request
208 comment.pull_request = pull_request
199 else:
209 else:
200 raise Exception('Please specify revision or pull_request_id')
210 raise Exception('Please specify revision or pull_request_id')
201
211
202 Session().add(comment)
212 Session().add(comment)
203 Session().flush()
213 Session().flush()
204
214
205 if send_email:
215 if send_email:
206 (subj, body, recipients, notification_type,
216 (subj, body, recipients, notification_type,
207 email_kwargs) = self._get_notification_data(
217 email_kwargs) = self._get_notification_data(
208 repo, comment, user,
218 repo, comment, user,
209 comment_text=text,
219 comment_text=text,
210 line_no=line_no,
220 line_no=line_no,
211 revision=revision,
221 revision=revision,
212 pull_request=pull_request,
222 pull_request=pull_request,
213 status_change=status_change,
223 status_change=status_change,
214 closing_pr=closing_pr)
224 closing_pr=closing_pr)
215 # create notification objects, and emails
225 # create notification objects, and emails
216 NotificationModel().create(
226 NotificationModel().create(
217 created_by=user, subject=subj, body=body,
227 created_by=user, subject=subj, body=body,
218 recipients=recipients, type_=notification_type,
228 recipients=recipients, type_=notification_type,
219 email_kwargs=email_kwargs,
229 email_kwargs=email_kwargs,
220 )
230 )
221
231
222 mention_recipients = set(self._extract_mentions(body))\
232 mention_recipients = set(self._extract_mentions(body))\
223 .difference(recipients)
233 .difference(recipients)
224 if mention_recipients:
234 if mention_recipients:
225 email_kwargs.update({'pr_mention': True})
235 email_kwargs.update({'pr_mention': True})
226 subj = _('[Mention]') + ' ' + subj
236 subj = _('[Mention]') + ' ' + subj
227 NotificationModel().create(
237 NotificationModel().create(
228 created_by=user, subject=subj, body=body,
238 created_by=user, subject=subj, body=body,
229 recipients=mention_recipients,
239 recipients=mention_recipients,
230 type_=notification_type,
240 type_=notification_type,
231 email_kwargs=email_kwargs
241 email_kwargs=email_kwargs
232 )
242 )
233
243
234 return comment
244 return comment
235
245
236 def delete(self, comment):
246 def delete(self, comment):
237 """
247 """
238 Deletes given comment
248 Deletes given comment
239
249
240 :param comment_id:
250 :param comment_id:
241 """
251 """
242 comment = self.__get_changeset_comment(comment)
252 comment = self.__get_changeset_comment(comment)
243 Session().delete(comment)
253 Session().delete(comment)
244
254
245 return comment
255 return comment
246
256
247 def get_comments(self, repo_id, revision=None, pull_request=None):
257 def get_comments(self, repo_id, revision=None, pull_request=None):
248 """
258 """
249 Gets main comments based on revision or pull_request_id
259 Gets main comments based on revision or pull_request_id
250
260
251 :param repo_id:
261 :param repo_id:
252 :param revision:
262 :param revision:
253 :param pull_request:
263 :param pull_request:
254 """
264 """
255
265
256 q = ChangesetComment.query()\
266 q = ChangesetComment.query()\
257 .filter(ChangesetComment.repo_id == repo_id)\
267 .filter(ChangesetComment.repo_id == repo_id)\
258 .filter(ChangesetComment.line_no == None)\
268 .filter(ChangesetComment.line_no == None)\
259 .filter(ChangesetComment.f_path == None)
269 .filter(ChangesetComment.f_path == None)
260 if revision:
270 if revision:
261 q = q.filter(ChangesetComment.revision == revision)
271 q = q.filter(ChangesetComment.revision == revision)
262 elif pull_request:
272 elif pull_request:
263 pull_request = self.__get_pull_request(pull_request)
273 pull_request = self.__get_pull_request(pull_request)
264 q = q.filter(ChangesetComment.pull_request == pull_request)
274 q = q.filter(ChangesetComment.pull_request == pull_request)
265 else:
275 else:
266 raise Exception('Please specify revision or pull_request')
276 raise Exception('Please specify revision or pull_request')
267 q = q.order_by(ChangesetComment.created_on)
277 q = q.order_by(ChangesetComment.created_on)
268 return q.all()
278 return q.all()
269
279
270 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
280 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
271 q = Session().query(ChangesetComment)\
281 q = Session().query(ChangesetComment)\
272 .filter(ChangesetComment.repo_id == repo_id)\
282 .filter(ChangesetComment.repo_id == repo_id)\
273 .filter(ChangesetComment.line_no != None)\
283 .filter(ChangesetComment.line_no != None)\
274 .filter(ChangesetComment.f_path != None)\
284 .filter(ChangesetComment.f_path != None)\
275 .order_by(ChangesetComment.comment_id.asc())\
285 .order_by(ChangesetComment.comment_id.asc())\
276
286
277 if revision:
287 if revision:
278 q = q.filter(ChangesetComment.revision == revision)
288 q = q.filter(ChangesetComment.revision == revision)
279 elif pull_request:
289 elif pull_request:
280 pull_request = self.__get_pull_request(pull_request)
290 pull_request = self.__get_pull_request(pull_request)
281 q = q.filter(ChangesetComment.pull_request == pull_request)
291 q = q.filter(ChangesetComment.pull_request == pull_request)
282 else:
292 else:
283 raise Exception('Please specify revision or pull_request_id')
293 raise Exception('Please specify revision or pull_request_id')
284
294
285 comments = q.all()
295 comments = q.all()
286
296
287 paths = defaultdict(lambda: defaultdict(list))
297 paths = defaultdict(lambda: defaultdict(list))
288
298
289 for co in comments:
299 for co in comments:
290 paths[co.f_path][co.line_no].append(co)
300 paths[co.f_path][co.line_no].append(co)
291 return paths.items()
301 return paths.items()
@@ -1,316 +1,320 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.notification
15 kallithea.model.notification
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Model for notifications
18 Model for notifications
19
19
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Nov 20, 2011
23 :created_on: Nov 20, 2011
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29
29
30 import os
30 import os
31 import logging
31 import logging
32 import traceback
32 import traceback
33
33
34 from pylons import tmpl_context as c
34 from pylons import tmpl_context as c
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 import kallithea
37 import kallithea
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.model import BaseModel
39 from kallithea.model import BaseModel
40 from kallithea.model.db import Notification, User, UserNotification
40 from kallithea.model.db import Notification, User, UserNotification
41 from kallithea.model.meta import Session
41 from kallithea.model.meta import Session
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class NotificationModel(BaseModel):
46 class NotificationModel(BaseModel):
47
47
48 cls = Notification
48 cls = Notification
49
49
50 def __get_notification(self, notification):
50 def __get_notification(self, notification):
51 if isinstance(notification, Notification):
51 if isinstance(notification, Notification):
52 return notification
52 return notification
53 elif isinstance(notification, (int, long)):
53 elif isinstance(notification, (int, long)):
54 return Notification.get(notification)
54 return Notification.get(notification)
55 else:
55 else:
56 if notification:
56 if notification:
57 raise Exception('notification must be int, long or Instance'
57 raise Exception('notification must be int, long or Instance'
58 ' of Notification got %s' % type(notification))
58 ' of Notification got %s' % type(notification))
59
59
60 def create(self, created_by, subject, body, recipients=None,
60 def create(self, created_by, subject, body, recipients=None,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
62 email_kwargs={}):
62 email_kwargs={}):
63 """
63 """
64
64
65 Creates notification of given type
65 Creates notification of given type
66
66
67 :param created_by: int, str or User instance. User who created this
67 :param created_by: int, str or User instance. User who created this
68 notification
68 notification
69 :param subject:
69 :param subject:
70 :param body:
70 :param body:
71 :param recipients: list of int, str or User objects, when None
71 :param recipients: list of int, str or User objects, when None
72 is given send to all admins
72 is given send to all admins
73 :param type_: type of notification
73 :param type_: type of notification
74 :param with_email: send email with this notification
74 :param with_email: send email with this notification
75 :param email_kwargs: additional dict to pass as args to email template
75 :param email_kwargs: additional dict to pass as args to email template
76 """
76 """
77 from kallithea.lib.celerylib import tasks, run_task
77 from kallithea.lib.celerylib import tasks, run_task
78
78
79 if recipients and not getattr(recipients, '__iter__', False):
79 if recipients and not getattr(recipients, '__iter__', False):
80 raise Exception('recipients must be a list or iterable')
80 raise Exception('recipients must be a list or iterable')
81
81
82 created_by_obj = self._get_user(created_by)
82 created_by_obj = self._get_user(created_by)
83
83
84 recipients_objs = []
84 recipients_objs = []
85 if recipients:
85 if recipients:
86 for u in recipients:
86 for u in recipients:
87 obj = self._get_user(u)
87 obj = self._get_user(u)
88 if obj:
88 if obj:
89 recipients_objs.append(obj)
89 recipients_objs.append(obj)
90 else:
90 else:
91 # TODO: inform user that requested operation couldn't be completed
91 # TODO: inform user that requested operation couldn't be completed
92 log.error('cannot email unknown user %r', u)
92 log.error('cannot email unknown user %r', u)
93 recipients_objs = set(recipients_objs)
93 recipients_objs = set(recipients_objs)
94 log.debug('sending notifications %s to %s' % (
94 log.debug('sending notifications %s to %s' % (
95 type_, recipients_objs)
95 type_, recipients_objs)
96 )
96 )
97 elif recipients is None:
97 elif recipients is None:
98 # empty recipients means to all admins
98 # empty recipients means to all admins
99 recipients_objs = User.query().filter(User.admin == True).all()
99 recipients_objs = User.query().filter(User.admin == True).all()
100 log.debug('sending notifications %s to admins: %s' % (
100 log.debug('sending notifications %s to admins: %s' % (
101 type_, recipients_objs)
101 type_, recipients_objs)
102 )
102 )
103 #else: silently skip notification mails?
103 #else: silently skip notification mails?
104
104
105 # TODO: inform user who are notified
105 # TODO: inform user who are notified
106 notif = Notification.create(
106 notif = Notification.create(
107 created_by=created_by_obj, subject=subject,
107 created_by=created_by_obj, subject=subject,
108 body=body, recipients=recipients_objs, type_=type_
108 body=body, recipients=recipients_objs, type_=type_
109 )
109 )
110
110
111 if not with_email:
111 if not with_email:
112 return notif
112 return notif
113
113
114 #don't send email to person who created this comment
114 #don't send email to person who created this comment
115 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
115 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
116
116
117 headers = None
118 if 'threading' in email_kwargs:
119 headers = {'References': ' '.join('<%s>' % x for x in email_kwargs['threading'])}
120
117 # send email with notification to all other participants
121 # send email with notification to all other participants
118 for rec in rec_objs:
122 for rec in rec_objs:
119 email_body = None # we set body to none, we just send HTML emails
123 email_body = None # we set body to none, we just send HTML emails
120 ## this is passed into template
124 ## this is passed into template
121 kwargs = {'subject': subject,
125 kwargs = {'subject': subject,
122 'body': h.rst_w_mentions(body),
126 'body': h.rst_w_mentions(body),
123 'when': h.fmt_date(notif.created_on),
127 'when': h.fmt_date(notif.created_on),
124 'user': notif.created_by_user.username,
128 'user': notif.created_by_user.username,
125 }
129 }
126
130
127 kwargs.update(email_kwargs)
131 kwargs.update(email_kwargs)
128 email_subject = EmailNotificationModel()\
132 email_subject = EmailNotificationModel()\
129 .get_email_description(type_, **kwargs)
133 .get_email_description(type_, **kwargs)
130 email_body_html = EmailNotificationModel()\
134 email_body_html = EmailNotificationModel()\
131 .get_email_tmpl(type_, **kwargs)
135 .get_email_tmpl(type_, **kwargs)
132
136
133 run_task(tasks.send_email, [rec.email], email_subject, email_body,
137 run_task(tasks.send_email, [rec.email], email_subject, email_body,
134 email_body_html)
138 email_body_html, headers)
135
139
136 return notif
140 return notif
137
141
138 def delete(self, user, notification):
142 def delete(self, user, notification):
139 # we don't want to remove actual notification just the assignment
143 # we don't want to remove actual notification just the assignment
140 try:
144 try:
141 notification = self.__get_notification(notification)
145 notification = self.__get_notification(notification)
142 user = self._get_user(user)
146 user = self._get_user(user)
143 if notification and user:
147 if notification and user:
144 obj = UserNotification.query()\
148 obj = UserNotification.query()\
145 .filter(UserNotification.user == user)\
149 .filter(UserNotification.user == user)\
146 .filter(UserNotification.notification
150 .filter(UserNotification.notification
147 == notification)\
151 == notification)\
148 .one()
152 .one()
149 Session().delete(obj)
153 Session().delete(obj)
150 return True
154 return True
151 except Exception:
155 except Exception:
152 log.error(traceback.format_exc())
156 log.error(traceback.format_exc())
153 raise
157 raise
154
158
155 def get_for_user(self, user, filter_=None):
159 def get_for_user(self, user, filter_=None):
156 """
160 """
157 Get notifications for given user, filter them if filter dict is given
161 Get notifications for given user, filter them if filter dict is given
158
162
159 :param user:
163 :param user:
160 :param filter:
164 :param filter:
161 """
165 """
162 user = self._get_user(user)
166 user = self._get_user(user)
163
167
164 q = UserNotification.query()\
168 q = UserNotification.query()\
165 .filter(UserNotification.user == user)\
169 .filter(UserNotification.user == user)\
166 .join((Notification, UserNotification.notification_id ==
170 .join((Notification, UserNotification.notification_id ==
167 Notification.notification_id))
171 Notification.notification_id))
168
172
169 if filter_:
173 if filter_:
170 q = q.filter(Notification.type_.in_(filter_))
174 q = q.filter(Notification.type_.in_(filter_))
171
175
172 return q.all()
176 return q.all()
173
177
174 def mark_read(self, user, notification):
178 def mark_read(self, user, notification):
175 try:
179 try:
176 notification = self.__get_notification(notification)
180 notification = self.__get_notification(notification)
177 user = self._get_user(user)
181 user = self._get_user(user)
178 if notification and user:
182 if notification and user:
179 obj = UserNotification.query()\
183 obj = UserNotification.query()\
180 .filter(UserNotification.user == user)\
184 .filter(UserNotification.user == user)\
181 .filter(UserNotification.notification
185 .filter(UserNotification.notification
182 == notification)\
186 == notification)\
183 .one()
187 .one()
184 obj.read = True
188 obj.read = True
185 Session().add(obj)
189 Session().add(obj)
186 return True
190 return True
187 except Exception:
191 except Exception:
188 log.error(traceback.format_exc())
192 log.error(traceback.format_exc())
189 raise
193 raise
190
194
191 def mark_all_read_for_user(self, user, filter_=None):
195 def mark_all_read_for_user(self, user, filter_=None):
192 user = self._get_user(user)
196 user = self._get_user(user)
193 q = UserNotification.query()\
197 q = UserNotification.query()\
194 .filter(UserNotification.user == user)\
198 .filter(UserNotification.user == user)\
195 .filter(UserNotification.read == False)\
199 .filter(UserNotification.read == False)\
196 .join((Notification, UserNotification.notification_id ==
200 .join((Notification, UserNotification.notification_id ==
197 Notification.notification_id))
201 Notification.notification_id))
198 if filter_:
202 if filter_:
199 q = q.filter(Notification.type_.in_(filter_))
203 q = q.filter(Notification.type_.in_(filter_))
200
204
201 # this is a little inefficient but sqlalchemy doesn't support
205 # this is a little inefficient but sqlalchemy doesn't support
202 # update on joined tables :(
206 # update on joined tables :(
203 for obj in q.all():
207 for obj in q.all():
204 obj.read = True
208 obj.read = True
205 Session().add(obj)
209 Session().add(obj)
206
210
207 def get_unread_cnt_for_user(self, user):
211 def get_unread_cnt_for_user(self, user):
208 user = self._get_user(user)
212 user = self._get_user(user)
209 return UserNotification.query()\
213 return UserNotification.query()\
210 .filter(UserNotification.read == False)\
214 .filter(UserNotification.read == False)\
211 .filter(UserNotification.user == user).count()
215 .filter(UserNotification.user == user).count()
212
216
213 def get_unread_for_user(self, user):
217 def get_unread_for_user(self, user):
214 user = self._get_user(user)
218 user = self._get_user(user)
215 return [x.notification for x in UserNotification.query()\
219 return [x.notification for x in UserNotification.query()\
216 .filter(UserNotification.read == False)\
220 .filter(UserNotification.read == False)\
217 .filter(UserNotification.user == user).all()]
221 .filter(UserNotification.user == user).all()]
218
222
219 def get_user_notification(self, user, notification):
223 def get_user_notification(self, user, notification):
220 user = self._get_user(user)
224 user = self._get_user(user)
221 notification = self.__get_notification(notification)
225 notification = self.__get_notification(notification)
222
226
223 return UserNotification.query()\
227 return UserNotification.query()\
224 .filter(UserNotification.notification == notification)\
228 .filter(UserNotification.notification == notification)\
225 .filter(UserNotification.user == user).scalar()
229 .filter(UserNotification.user == user).scalar()
226
230
227 def make_description(self, notification, show_age=True):
231 def make_description(self, notification, show_age=True):
228 """
232 """
229 Creates a human readable description based on properties
233 Creates a human readable description based on properties
230 of notification object
234 of notification object
231 """
235 """
232 #alias
236 #alias
233 _n = notification
237 _n = notification
234 _map = {
238 _map = {
235 _n.TYPE_CHANGESET_COMMENT: _('%(user)s commented on changeset at %(when)s'),
239 _n.TYPE_CHANGESET_COMMENT: _('%(user)s commented on changeset at %(when)s'),
236 _n.TYPE_MESSAGE: _('%(user)s sent message at %(when)s'),
240 _n.TYPE_MESSAGE: _('%(user)s sent message at %(when)s'),
237 _n.TYPE_MENTION: _('%(user)s mentioned you at %(when)s'),
241 _n.TYPE_MENTION: _('%(user)s mentioned you at %(when)s'),
238 _n.TYPE_REGISTRATION: _('%(user)s registered in Kallithea at %(when)s'),
242 _n.TYPE_REGISTRATION: _('%(user)s registered in Kallithea at %(when)s'),
239 _n.TYPE_PULL_REQUEST: _('%(user)s opened new pull request at %(when)s'),
243 _n.TYPE_PULL_REQUEST: _('%(user)s opened new pull request at %(when)s'),
240 _n.TYPE_PULL_REQUEST_COMMENT: _('%(user)s commented on pull request at %(when)s')
244 _n.TYPE_PULL_REQUEST_COMMENT: _('%(user)s commented on pull request at %(when)s')
241 }
245 }
242 tmpl = _map[notification.type_]
246 tmpl = _map[notification.type_]
243
247
244 if show_age:
248 if show_age:
245 when = h.age(notification.created_on)
249 when = h.age(notification.created_on)
246 else:
250 else:
247 when = h.fmt_date(notification.created_on)
251 when = h.fmt_date(notification.created_on)
248
252
249 return tmpl % dict(
253 return tmpl % dict(
250 user=notification.created_by_user.username,
254 user=notification.created_by_user.username,
251 when=when,
255 when=when,
252 )
256 )
253
257
254
258
255 class EmailNotificationModel(BaseModel):
259 class EmailNotificationModel(BaseModel):
256
260
257 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
261 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
258 TYPE_MESSAGE = Notification.TYPE_MESSAGE # only used for testing
262 TYPE_MESSAGE = Notification.TYPE_MESSAGE # only used for testing
259 # Notification.TYPE_MENTION is not used
263 # Notification.TYPE_MENTION is not used
260 TYPE_PASSWORD_RESET = 'password_link'
264 TYPE_PASSWORD_RESET = 'password_link'
261 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
265 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
262 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
266 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
263 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
267 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
264 TYPE_DEFAULT = 'default'
268 TYPE_DEFAULT = 'default'
265
269
266 def __init__(self):
270 def __init__(self):
267 super(EmailNotificationModel, self).__init__()
271 super(EmailNotificationModel, self).__init__()
268 self._template_root = kallithea.CONFIG['pylons.paths']['templates'][0]
272 self._template_root = kallithea.CONFIG['pylons.paths']['templates'][0]
269 self._tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
273 self._tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
270 self.email_types = {
274 self.email_types = {
271 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
275 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
272 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
276 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
273 self.TYPE_REGISTRATION: 'email_templates/registration.html',
277 self.TYPE_REGISTRATION: 'email_templates/registration.html',
274 self.TYPE_DEFAULT: 'email_templates/default.html',
278 self.TYPE_DEFAULT: 'email_templates/default.html',
275 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
279 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
276 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
280 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
277 }
281 }
278 self._subj_map = {
282 self._subj_map = {
279 self.TYPE_CHANGESET_COMMENT: _('Comment on %(repo_name)s changeset %(short_id)s on %(branch)s by %(comment_username)s'),
283 self.TYPE_CHANGESET_COMMENT: _('Comment on %(repo_name)s changeset %(short_id)s on %(branch)s by %(comment_username)s'),
280 self.TYPE_MESSAGE: 'Test Message',
284 self.TYPE_MESSAGE: 'Test Message',
281 # self.TYPE_PASSWORD_RESET
285 # self.TYPE_PASSWORD_RESET
282 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
286 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
283 # self.TYPE_DEFAULT
287 # self.TYPE_DEFAULT
284 self.TYPE_PULL_REQUEST: _('Review request on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(pr_username)s'),
288 self.TYPE_PULL_REQUEST: _('Review request on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(pr_username)s'),
285 self.TYPE_PULL_REQUEST_COMMENT: _('Comment on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(comment_username)s'),
289 self.TYPE_PULL_REQUEST_COMMENT: _('Comment on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(comment_username)s'),
286 }
290 }
287
291
288 def get_email_description(self, type_, **kwargs):
292 def get_email_description(self, type_, **kwargs):
289 """
293 """
290 return subject for email based on given type
294 return subject for email based on given type
291 """
295 """
292 tmpl = self._subj_map[type_]
296 tmpl = self._subj_map[type_]
293 try:
297 try:
294 subj = tmpl % kwargs
298 subj = tmpl % kwargs
295 except KeyError, e:
299 except KeyError, e:
296 log.error('error generating email subject for %r from %s: %s', type_, ','.join(self._subj_map.keys()), e)
300 log.error('error generating email subject for %r from %s: %s', type_, ','.join(self._subj_map.keys()), e)
297 raise
301 raise
298 l = [str(x) for x in [kwargs.get('status_change'), kwargs.get('closing_pr') and _('Closing')] if x]
302 l = [str(x) for x in [kwargs.get('status_change'), kwargs.get('closing_pr') and _('Closing')] if x]
299 if l:
303 if l:
300 subj += ' (%s)' % (', '.join(l))
304 subj += ' (%s)' % (', '.join(l))
301 return subj
305 return subj
302
306
303 def get_email_tmpl(self, type_, **kwargs):
307 def get_email_tmpl(self, type_, **kwargs):
304 """
308 """
305 return generated template for email based on given type
309 return generated template for email based on given type
306 """
310 """
307
311
308 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
312 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
309 email_template = self._tmpl_lookup.get_template(base)
313 email_template = self._tmpl_lookup.get_template(base)
310 # translator and helpers inject
314 # translator and helpers inject
311 _kwargs = {'_': _,
315 _kwargs = {'_': _,
312 'h': h,
316 'h': h,
313 'c': c}
317 'c': c}
314 _kwargs.update(kwargs)
318 _kwargs.update(kwargs)
315 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
319 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
316 return email_template.render(**_kwargs)
320 return email_template.render(**_kwargs)
@@ -1,185 +1,186 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.pull_request
15 kallithea.model.pull_request
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull request model for Kallithea
18 pull request model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jun 6, 2012
22 :created_on: Jun 6, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from kallithea.model.meta import Session
33 from kallithea.model.meta import Session
34 from kallithea.lib import helpers as h
34 from kallithea.lib import helpers as h
35 from kallithea.model import BaseModel
35 from kallithea.model import BaseModel
36 from kallithea.model.db import PullRequest, PullRequestReviewers, Notification,\
36 from kallithea.model.db import PullRequest, PullRequestReviewers, Notification,\
37 ChangesetStatus
37 ChangesetStatus
38 from kallithea.model.notification import NotificationModel
38 from kallithea.model.notification import NotificationModel
39 from kallithea.lib.utils2 import safe_unicode
39 from kallithea.lib.utils2 import safe_unicode
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class PullRequestModel(BaseModel):
45 class PullRequestModel(BaseModel):
46
46
47 cls = PullRequest
47 cls = PullRequest
48
48
49 def __get_pull_request(self, pull_request):
49 def __get_pull_request(self, pull_request):
50 return self._get_instance(PullRequest, pull_request)
50 return self._get_instance(PullRequest, pull_request)
51
51
52 def get_pullrequest_cnt_for_user(self, user):
52 def get_pullrequest_cnt_for_user(self, user):
53 return PullRequest.query()\
53 return PullRequest.query()\
54 .join(PullRequestReviewers)\
54 .join(PullRequestReviewers)\
55 .filter(PullRequestReviewers.user_id == user)\
55 .filter(PullRequestReviewers.user_id == user)\
56 .filter(PullRequest.status != PullRequest.STATUS_CLOSED)\
56 .filter(PullRequest.status != PullRequest.STATUS_CLOSED)\
57 .count()
57 .count()
58
58
59 def get_all(self, repo_name, from_=False, closed=False):
59 def get_all(self, repo_name, from_=False, closed=False):
60 """Get all PRs for repo.
60 """Get all PRs for repo.
61 Default is all PRs to the repo, PRs from the repo if from_.
61 Default is all PRs to the repo, PRs from the repo if from_.
62 Closed PRs are only included if closed is true."""
62 Closed PRs are only included if closed is true."""
63 repo = self._get_repo(repo_name)
63 repo = self._get_repo(repo_name)
64 q = PullRequest.query()
64 q = PullRequest.query()
65 if from_:
65 if from_:
66 q = q.filter(PullRequest.org_repo == repo)
66 q = q.filter(PullRequest.org_repo == repo)
67 else:
67 else:
68 q = q.filter(PullRequest.other_repo == repo)
68 q = q.filter(PullRequest.other_repo == repo)
69 if not closed:
69 if not closed:
70 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
70 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
71 return q.order_by(PullRequest.created_on.desc()).all()
71 return q.order_by(PullRequest.created_on.desc()).all()
72
72
73 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
73 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
74 revisions, reviewers, title, description=None):
74 revisions, reviewers, title, description=None):
75 from kallithea.model.changeset_status import ChangesetStatusModel
75 from kallithea.model.changeset_status import ChangesetStatusModel
76
76
77 created_by_user = self._get_user(created_by)
77 created_by_user = self._get_user(created_by)
78 org_repo = self._get_repo(org_repo)
78 org_repo = self._get_repo(org_repo)
79 other_repo = self._get_repo(other_repo)
79 other_repo = self._get_repo(other_repo)
80
80
81 new = PullRequest()
81 new = PullRequest()
82 new.org_repo = org_repo
82 new.org_repo = org_repo
83 new.org_ref = org_ref
83 new.org_ref = org_ref
84 new.other_repo = other_repo
84 new.other_repo = other_repo
85 new.other_ref = other_ref
85 new.other_ref = other_ref
86 new.revisions = revisions
86 new.revisions = revisions
87 new.title = title
87 new.title = title
88 new.description = description
88 new.description = description
89 new.author = created_by_user
89 new.author = created_by_user
90 Session().add(new)
90 Session().add(new)
91 Session().flush()
91 Session().flush()
92
92
93 #reset state to under-review
93 #reset state to under-review
94 from kallithea.model.comment import ChangesetCommentsModel
94 from kallithea.model.comment import ChangesetCommentsModel
95 comment = ChangesetCommentsModel().create(
95 comment = ChangesetCommentsModel().create(
96 text=u'Auto status change to %s' % (ChangesetStatus.get_status_lbl(ChangesetStatus.STATUS_UNDER_REVIEW)),
96 text=u'Auto status change to %s' % (ChangesetStatus.get_status_lbl(ChangesetStatus.STATUS_UNDER_REVIEW)),
97 repo=org_repo,
97 repo=org_repo,
98 user=new.author,
98 user=new.author,
99 pull_request=new,
99 pull_request=new,
100 send_email=False
100 send_email=False
101 )
101 )
102 ChangesetStatusModel().set_status(
102 ChangesetStatusModel().set_status(
103 org_repo,
103 org_repo,
104 ChangesetStatus.STATUS_UNDER_REVIEW,
104 ChangesetStatus.STATUS_UNDER_REVIEW,
105 new.author,
105 new.author,
106 comment,
106 comment,
107 pull_request=new
107 pull_request=new
108 )
108 )
109 self.__add_reviewers(new, reviewers)
109 self.__add_reviewers(new, reviewers)
110 return new
110 return new
111
111
112 def __add_reviewers(self, pr, reviewers):
112 def __add_reviewers(self, pr, reviewers):
113 #members
113 #members
114 for member in set(reviewers):
114 for member in set(reviewers):
115 _usr = self._get_user(member)
115 _usr = self._get_user(member)
116 reviewer = PullRequestReviewers(_usr, pr)
116 reviewer = PullRequestReviewers(_usr, pr)
117 Session().add(reviewer)
117 Session().add(reviewer)
118
118
119 revision_data = [(x.raw_id, x.message)
119 revision_data = [(x.raw_id, x.message)
120 for x in map(pr.org_repo.get_changeset, pr.revisions)]
120 for x in map(pr.org_repo.get_changeset, pr.revisions)]
121
121
122 #notification to reviewers
122 #notification to reviewers
123 pr_url = h.url('pullrequest_show', repo_name=pr.other_repo.repo_name,
123 pr_url = h.url('pullrequest_show', repo_name=pr.other_repo.repo_name,
124 pull_request_id=pr.pull_request_id,
124 pull_request_id=pr.pull_request_id,
125 qualified=True)
125 qualified=True)
126 subject = safe_unicode(
126 subject = safe_unicode(
127 h.link_to(
127 h.link_to(
128 _('%(user)s wants you to review pull request #%(pr_id)s: %(pr_title)s') % \
128 _('%(user)s wants you to review pull request #%(pr_id)s: %(pr_title)s') % \
129 {'user': pr.author.username,
129 {'user': pr.author.username,
130 'pr_title': pr.title,
130 'pr_title': pr.title,
131 'pr_id': pr.pull_request_id},
131 'pr_id': pr.pull_request_id},
132 pr_url)
132 pr_url)
133 )
133 )
134 body = pr.description
134 body = pr.description
135 _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
135 _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
136 email_kwargs = {
136 email_kwargs = {
137 'pr_title': pr.title,
137 'pr_title': pr.title,
138 'pr_user_created': h.person(pr.author),
138 'pr_user_created': h.person(pr.author),
139 'pr_repo_url': h.url('summary_home', repo_name=pr.other_repo.repo_name,
139 'pr_repo_url': h.url('summary_home', repo_name=pr.other_repo.repo_name,
140 qualified=True,),
140 qualified=True,),
141 'pr_url': pr_url,
141 'pr_url': pr_url,
142 'pr_revisions': revision_data,
142 'pr_revisions': revision_data,
143 'repo_name': pr.other_repo.repo_name,
143 'repo_name': pr.other_repo.repo_name,
144 'pr_id': pr.pull_request_id,
144 'pr_id': pr.pull_request_id,
145 'ref': org_ref_name,
145 'ref': org_ref_name,
146 'pr_username': pr.author.username,
146 'pr_username': pr.author.username,
147 'threading': [pr_url],
147 }
148 }
148 NotificationModel().create(created_by=pr.author, subject=subject, body=body,
149 NotificationModel().create(created_by=pr.author, subject=subject, body=body,
149 recipients=reviewers,
150 recipients=reviewers,
150 type_=Notification.TYPE_PULL_REQUEST,
151 type_=Notification.TYPE_PULL_REQUEST,
151 email_kwargs=email_kwargs)
152 email_kwargs=email_kwargs)
152
153
153 def update_reviewers(self, pull_request, reviewers_ids):
154 def update_reviewers(self, pull_request, reviewers_ids):
154 reviewers_ids = set(reviewers_ids)
155 reviewers_ids = set(reviewers_ids)
155 pull_request = self.__get_pull_request(pull_request)
156 pull_request = self.__get_pull_request(pull_request)
156 current_reviewers = PullRequestReviewers.query()\
157 current_reviewers = PullRequestReviewers.query()\
157 .filter(PullRequestReviewers.pull_request==
158 .filter(PullRequestReviewers.pull_request==
158 pull_request)\
159 pull_request)\
159 .all()
160 .all()
160 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
161 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
161
162
162 to_add = reviewers_ids.difference(current_reviewers_ids)
163 to_add = reviewers_ids.difference(current_reviewers_ids)
163 to_remove = current_reviewers_ids.difference(reviewers_ids)
164 to_remove = current_reviewers_ids.difference(reviewers_ids)
164
165
165 log.debug("Adding %s reviewers" % to_add)
166 log.debug("Adding %s reviewers" % to_add)
166 self.__add_reviewers(pull_request, to_add)
167 self.__add_reviewers(pull_request, to_add)
167
168
168 log.debug("Removing %s reviewers" % to_remove)
169 log.debug("Removing %s reviewers" % to_remove)
169 for uid in to_remove:
170 for uid in to_remove:
170 reviewer = PullRequestReviewers.query()\
171 reviewer = PullRequestReviewers.query()\
171 .filter(PullRequestReviewers.user_id==uid,
172 .filter(PullRequestReviewers.user_id==uid,
172 PullRequestReviewers.pull_request==pull_request)\
173 PullRequestReviewers.pull_request==pull_request)\
173 .scalar()
174 .scalar()
174 if reviewer:
175 if reviewer:
175 Session().delete(reviewer)
176 Session().delete(reviewer)
176
177
177 def delete(self, pull_request):
178 def delete(self, pull_request):
178 pull_request = self.__get_pull_request(pull_request)
179 pull_request = self.__get_pull_request(pull_request)
179 Session().delete(pull_request)
180 Session().delete(pull_request)
180
181
181 def close_pull_request(self, pull_request):
182 def close_pull_request(self, pull_request):
182 pull_request = self.__get_pull_request(pull_request)
183 pull_request = self.__get_pull_request(pull_request)
183 pull_request.status = PullRequest.STATUS_CLOSED
184 pull_request.status = PullRequest.STATUS_CLOSED
184 pull_request.updated_on = datetime.datetime.now()
185 pull_request.updated_on = datetime.datetime.now()
185 Session().add(pull_request)
186 Session().add(pull_request)
General Comments 0
You need to be logged in to leave comments. Login now