##// END OF EJS Templates
notifications changes...
marcink -
r3430:bbe21df7 beta
parent child Browse files
Show More
@@ -1,405 +1,404 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.changeset
3 rhodecode.controllers.changeset
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 changeset controller for pylons showoing changes beetween
6 changeset controller for pylons showoing changes beetween
7 revisions
7 revisions
8
8
9 :created_on: Apr 25, 2010
9 :created_on: Apr 25, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import logging
26 import logging
27 import traceback
27 import traceback
28 from collections import defaultdict
28 from collections import defaultdict
29 from webob.exc import HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPForbidden, HTTPBadRequest
30
30
31 from pylons import tmpl_context as c, url, request, response
31 from pylons import tmpl_context as c, url, request, response
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from rhodecode.lib.utils import jsonify
34 from rhodecode.lib.utils import jsonify
35
35
36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
37 ChangesetDoesNotExistError
37 ChangesetDoesNotExistError
38
38
39 import rhodecode.lib.helpers as h
39 import rhodecode.lib.helpers as h
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils import action_logger
43 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.compat import OrderedDict
44 from rhodecode.lib import diffs
44 from rhodecode.lib import diffs
45 from rhodecode.model.db import ChangesetComment, ChangesetStatus
45 from rhodecode.model.db import ChangesetComment, ChangesetStatus
46 from rhodecode.model.comment import ChangesetCommentsModel
46 from rhodecode.model.comment import ChangesetCommentsModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.lib.diffs import LimitedDiffContainer
50 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
51 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
52 from rhodecode.lib.vcs.backends.base import EmptyChangeset
52 from rhodecode.lib.vcs.backends.base import EmptyChangeset
53 from rhodecode.lib.utils2 import safe_unicode
53 from rhodecode.lib.utils2 import safe_unicode
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, GET):
58 def _update_with_GET(params, GET):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += GET.getall(k)
60 params[k] += GET.getall(k)
61
61
62
62
63 def anchor_url(revision, path, GET):
63 def anchor_url(revision, path, GET):
64 fid = h.FID(revision, path)
64 fid = h.FID(revision, path)
65 return h.url.current(anchor=fid, **dict(GET))
65 return h.url.current(anchor=fid, **dict(GET))
66
66
67
67
68 def get_ignore_ws(fid, GET):
68 def get_ignore_ws(fid, GET):
69 ig_ws_global = GET.get('ignorews')
69 ig_ws_global = GET.get('ignorews')
70 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
70 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
71 if ig_ws:
71 if ig_ws:
72 try:
72 try:
73 return int(ig_ws[0].split(':')[-1])
73 return int(ig_ws[0].split(':')[-1])
74 except:
74 except:
75 pass
75 pass
76 return ig_ws_global
76 return ig_ws_global
77
77
78
78
79 def _ignorews_url(GET, fileid=None):
79 def _ignorews_url(GET, fileid=None):
80 fileid = str(fileid) if fileid else None
80 fileid = str(fileid) if fileid else None
81 params = defaultdict(list)
81 params = defaultdict(list)
82 _update_with_GET(params, GET)
82 _update_with_GET(params, GET)
83 lbl = _('show white space')
83 lbl = _('show white space')
84 ig_ws = get_ignore_ws(fileid, GET)
84 ig_ws = get_ignore_ws(fileid, GET)
85 ln_ctx = get_line_ctx(fileid, GET)
85 ln_ctx = get_line_ctx(fileid, GET)
86 # global option
86 # global option
87 if fileid is None:
87 if fileid is None:
88 if ig_ws is None:
88 if ig_ws is None:
89 params['ignorews'] += [1]
89 params['ignorews'] += [1]
90 lbl = _('ignore white space')
90 lbl = _('ignore white space')
91 ctx_key = 'context'
91 ctx_key = 'context'
92 ctx_val = ln_ctx
92 ctx_val = ln_ctx
93 # per file options
93 # per file options
94 else:
94 else:
95 if ig_ws is None:
95 if ig_ws is None:
96 params[fileid] += ['WS:1']
96 params[fileid] += ['WS:1']
97 lbl = _('ignore white space')
97 lbl = _('ignore white space')
98
98
99 ctx_key = fileid
99 ctx_key = fileid
100 ctx_val = 'C:%s' % ln_ctx
100 ctx_val = 'C:%s' % ln_ctx
101 # if we have passed in ln_ctx pass it along to our params
101 # if we have passed in ln_ctx pass it along to our params
102 if ln_ctx:
102 if ln_ctx:
103 params[ctx_key] += [ctx_val]
103 params[ctx_key] += [ctx_val]
104
104
105 params['anchor'] = fileid
105 params['anchor'] = fileid
106 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
106 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
108
108
109
109
110 def get_line_ctx(fid, GET):
110 def get_line_ctx(fid, GET):
111 ln_ctx_global = GET.get('context')
111 ln_ctx_global = GET.get('context')
112 if fid:
112 if fid:
113 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
113 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
114 else:
114 else:
115 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
115 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
116 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
116 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
117 if ln_ctx:
117 if ln_ctx:
118 ln_ctx = [ln_ctx]
118 ln_ctx = [ln_ctx]
119
119
120 if ln_ctx:
120 if ln_ctx:
121 retval = ln_ctx[0].split(':')[-1]
121 retval = ln_ctx[0].split(':')[-1]
122 else:
122 else:
123 retval = ln_ctx_global
123 retval = ln_ctx_global
124
124
125 try:
125 try:
126 return int(retval)
126 return int(retval)
127 except:
127 except:
128 return 3
128 return 3
129
129
130
130
131 def _context_url(GET, fileid=None):
131 def _context_url(GET, fileid=None):
132 """
132 """
133 Generates url for context lines
133 Generates url for context lines
134
134
135 :param fileid:
135 :param fileid:
136 """
136 """
137
137
138 fileid = str(fileid) if fileid else None
138 fileid = str(fileid) if fileid else None
139 ig_ws = get_ignore_ws(fileid, GET)
139 ig_ws = get_ignore_ws(fileid, GET)
140 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
140 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
141
141
142 params = defaultdict(list)
142 params = defaultdict(list)
143 _update_with_GET(params, GET)
143 _update_with_GET(params, GET)
144
144
145 # global option
145 # global option
146 if fileid is None:
146 if fileid is None:
147 if ln_ctx > 0:
147 if ln_ctx > 0:
148 params['context'] += [ln_ctx]
148 params['context'] += [ln_ctx]
149
149
150 if ig_ws:
150 if ig_ws:
151 ig_ws_key = 'ignorews'
151 ig_ws_key = 'ignorews'
152 ig_ws_val = 1
152 ig_ws_val = 1
153
153
154 # per file option
154 # per file option
155 else:
155 else:
156 params[fileid] += ['C:%s' % ln_ctx]
156 params[fileid] += ['C:%s' % ln_ctx]
157 ig_ws_key = fileid
157 ig_ws_key = fileid
158 ig_ws_val = 'WS:%s' % 1
158 ig_ws_val = 'WS:%s' % 1
159
159
160 if ig_ws:
160 if ig_ws:
161 params[ig_ws_key] += [ig_ws_val]
161 params[ig_ws_key] += [ig_ws_val]
162
162
163 lbl = _('%s line context') % ln_ctx
163 lbl = _('%s line context') % ln_ctx
164
164
165 params['anchor'] = fileid
165 params['anchor'] = fileid
166 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
166 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
167 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
167 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
168
168
169
169
170 class ChangesetController(BaseRepoController):
170 class ChangesetController(BaseRepoController):
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
174 'repository.admin')
174 'repository.admin')
175 def __before__(self):
175 def __before__(self):
176 super(ChangesetController, self).__before__()
176 super(ChangesetController, self).__before__()
177 c.affected_files_cut_off = 60
177 c.affected_files_cut_off = 60
178 repo_model = RepoModel()
178 repo_model = RepoModel()
179 c.users_array = repo_model.get_users_js()
179 c.users_array = repo_model.get_users_js()
180 c.users_groups_array = repo_model.get_users_groups_js()
180 c.users_groups_array = repo_model.get_users_groups_js()
181
181
182 def index(self, revision, method='show'):
182 def index(self, revision, method='show'):
183 c.anchor_url = anchor_url
183 c.anchor_url = anchor_url
184 c.ignorews_url = _ignorews_url
184 c.ignorews_url = _ignorews_url
185 c.context_url = _context_url
185 c.context_url = _context_url
186 c.fulldiff = fulldiff = request.GET.get('fulldiff')
186 c.fulldiff = fulldiff = request.GET.get('fulldiff')
187 #get ranges of revisions if preset
187 #get ranges of revisions if preset
188 rev_range = revision.split('...')[:2]
188 rev_range = revision.split('...')[:2]
189 enable_comments = True
189 enable_comments = True
190 try:
190 try:
191 if len(rev_range) == 2:
191 if len(rev_range) == 2:
192 enable_comments = False
192 enable_comments = False
193 rev_start = rev_range[0]
193 rev_start = rev_range[0]
194 rev_end = rev_range[1]
194 rev_end = rev_range[1]
195 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
195 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
196 end=rev_end)
196 end=rev_end)
197 else:
197 else:
198 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
198 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
199
199
200 c.cs_ranges = list(rev_ranges)
200 c.cs_ranges = list(rev_ranges)
201 if not c.cs_ranges:
201 if not c.cs_ranges:
202 raise RepositoryError('Changeset range returned empty result')
202 raise RepositoryError('Changeset range returned empty result')
203
203
204 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
204 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
205 log.error(traceback.format_exc())
205 log.error(traceback.format_exc())
206 h.flash(str(e), category='warning')
206 h.flash(str(e), category='warning')
207 return redirect(url('home'))
207 return redirect(url('home'))
208
208
209 c.changes = OrderedDict()
209 c.changes = OrderedDict()
210
210
211 c.lines_added = 0 # count of lines added
211 c.lines_added = 0 # count of lines added
212 c.lines_deleted = 0 # count of lines removes
212 c.lines_deleted = 0 # count of lines removes
213
213
214 c.changeset_statuses = ChangesetStatus.STATUSES
214 c.changeset_statuses = ChangesetStatus.STATUSES
215 c.comments = []
215 c.comments = []
216 c.statuses = []
216 c.statuses = []
217 c.inline_comments = []
217 c.inline_comments = []
218 c.inline_cnt = 0
218 c.inline_cnt = 0
219
219
220 # Iterate over ranges (default changeset view is always one changeset)
220 # Iterate over ranges (default changeset view is always one changeset)
221 for changeset in c.cs_ranges:
221 for changeset in c.cs_ranges:
222 inlines = []
222 inlines = []
223 if method == 'show':
223 if method == 'show':
224 c.statuses.extend([ChangesetStatusModel().get_status(
224 c.statuses.extend([ChangesetStatusModel().get_status(
225 c.rhodecode_db_repo.repo_id, changeset.raw_id)])
225 c.rhodecode_db_repo.repo_id, changeset.raw_id)])
226
226
227 c.comments.extend(ChangesetCommentsModel()\
227 c.comments.extend(ChangesetCommentsModel()\
228 .get_comments(c.rhodecode_db_repo.repo_id,
228 .get_comments(c.rhodecode_db_repo.repo_id,
229 revision=changeset.raw_id))
229 revision=changeset.raw_id))
230
230
231 #comments from PR
231 #comments from PR
232 st = ChangesetStatusModel().get_statuses(
232 st = ChangesetStatusModel().get_statuses(
233 c.rhodecode_db_repo.repo_id, changeset.raw_id,
233 c.rhodecode_db_repo.repo_id, changeset.raw_id,
234 with_revisions=True)
234 with_revisions=True)
235 # from associated statuses, check the pull requests, and
235 # from associated statuses, check the pull requests, and
236 # show comments from them
236 # show comments from them
237
237
238 prs = set([x.pull_request for x in
238 prs = set([x.pull_request for x in
239 filter(lambda x: x.pull_request != None, st)])
239 filter(lambda x: x.pull_request != None, st)])
240
240
241 for pr in prs:
241 for pr in prs:
242 c.comments.extend(pr.comments)
242 c.comments.extend(pr.comments)
243 inlines = ChangesetCommentsModel()\
243 inlines = ChangesetCommentsModel()\
244 .get_inline_comments(c.rhodecode_db_repo.repo_id,
244 .get_inline_comments(c.rhodecode_db_repo.repo_id,
245 revision=changeset.raw_id)
245 revision=changeset.raw_id)
246 c.inline_comments.extend(inlines)
246 c.inline_comments.extend(inlines)
247
247
248 c.changes[changeset.raw_id] = []
248 c.changes[changeset.raw_id] = []
249
249
250 cs2 = changeset.raw_id
250 cs2 = changeset.raw_id
251 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
251 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
252 context_lcl = get_line_ctx('', request.GET)
252 context_lcl = get_line_ctx('', request.GET)
253 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
253 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
254
254
255 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
255 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
256 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
256 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
257 diff_limit = self.cut_off_limit if not fulldiff else None
257 diff_limit = self.cut_off_limit if not fulldiff else None
258 diff_processor = diffs.DiffProcessor(_diff,
258 diff_processor = diffs.DiffProcessor(_diff,
259 vcs=c.rhodecode_repo.alias,
259 vcs=c.rhodecode_repo.alias,
260 format='gitdiff',
260 format='gitdiff',
261 diff_limit=diff_limit)
261 diff_limit=diff_limit)
262 cs_changes = OrderedDict()
262 cs_changes = OrderedDict()
263 if method == 'show':
263 if method == 'show':
264 _parsed = diff_processor.prepare()
264 _parsed = diff_processor.prepare()
265 c.limited_diff = False
265 c.limited_diff = False
266 if isinstance(_parsed, LimitedDiffContainer):
266 if isinstance(_parsed, LimitedDiffContainer):
267 c.limited_diff = True
267 c.limited_diff = True
268 for f in _parsed:
268 for f in _parsed:
269 st = f['stats']
269 st = f['stats']
270 if st[0] != 'b':
270 if st[0] != 'b':
271 c.lines_added += st[0]
271 c.lines_added += st[0]
272 c.lines_deleted += st[1]
272 c.lines_deleted += st[1]
273 fid = h.FID(changeset.raw_id, f['filename'])
273 fid = h.FID(changeset.raw_id, f['filename'])
274 diff = diff_processor.as_html(enable_comments=enable_comments,
274 diff = diff_processor.as_html(enable_comments=enable_comments,
275 parsed_lines=[f])
275 parsed_lines=[f])
276 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
276 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
277 diff, st]
277 diff, st]
278 else:
278 else:
279 # downloads/raw we only need RAW diff nothing else
279 # downloads/raw we only need RAW diff nothing else
280 diff = diff_processor.as_raw()
280 diff = diff_processor.as_raw()
281 cs_changes[''] = [None, None, None, None, diff, None]
281 cs_changes[''] = [None, None, None, None, diff, None]
282 c.changes[changeset.raw_id] = cs_changes
282 c.changes[changeset.raw_id] = cs_changes
283
283
284 #sort comments by how they were generated
284 #sort comments by how they were generated
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286
286
287 # count inline comments
287 # count inline comments
288 for __, lines in c.inline_comments:
288 for __, lines in c.inline_comments:
289 for comments in lines.values():
289 for comments in lines.values():
290 c.inline_cnt += len(comments)
290 c.inline_cnt += len(comments)
291
291
292 if len(c.cs_ranges) == 1:
292 if len(c.cs_ranges) == 1:
293 c.changeset = c.cs_ranges[0]
293 c.changeset = c.cs_ranges[0]
294 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
294 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
295 for x in c.changeset.parents])
295 for x in c.changeset.parents])
296 if method == 'download':
296 if method == 'download':
297 response.content_type = 'text/plain'
297 response.content_type = 'text/plain'
298 response.content_disposition = 'attachment; filename=%s.diff' \
298 response.content_disposition = 'attachment; filename=%s.diff' \
299 % revision[:12]
299 % revision[:12]
300 return diff
300 return diff
301 elif method == 'patch':
301 elif method == 'patch':
302 response.content_type = 'text/plain'
302 response.content_type = 'text/plain'
303 c.diff = safe_unicode(diff)
303 c.diff = safe_unicode(diff)
304 return render('changeset/patch_changeset.html')
304 return render('changeset/patch_changeset.html')
305 elif method == 'raw':
305 elif method == 'raw':
306 response.content_type = 'text/plain'
306 response.content_type = 'text/plain'
307 return diff
307 return diff
308 elif method == 'show':
308 elif method == 'show':
309 if len(c.cs_ranges) == 1:
309 if len(c.cs_ranges) == 1:
310 return render('changeset/changeset.html')
310 return render('changeset/changeset.html')
311 else:
311 else:
312 return render('changeset/changeset_range.html')
312 return render('changeset/changeset_range.html')
313
313
314 def changeset_raw(self, revision):
314 def changeset_raw(self, revision):
315 return self.index(revision, method='raw')
315 return self.index(revision, method='raw')
316
316
317 def changeset_patch(self, revision):
317 def changeset_patch(self, revision):
318 return self.index(revision, method='patch')
318 return self.index(revision, method='patch')
319
319
320 def changeset_download(self, revision):
320 def changeset_download(self, revision):
321 return self.index(revision, method='download')
321 return self.index(revision, method='download')
322
322
323 @jsonify
323 @jsonify
324 def comment(self, repo_name, revision):
324 def comment(self, repo_name, revision):
325 status = request.POST.get('changeset_status')
325 status = request.POST.get('changeset_status')
326 change_status = request.POST.get('change_changeset_status')
326 change_status = request.POST.get('change_changeset_status')
327 text = request.POST.get('text')
327 text = request.POST.get('text')
328 if status and change_status:
328 if status and change_status:
329 text = text or (_('Status change -> %s')
329 text = text or (_('Status change -> %s')
330 % ChangesetStatus.get_status_lbl(status))
330 % ChangesetStatus.get_status_lbl(status))
331
331
332 comm = ChangesetCommentsModel().create(
332 c.co = comm = ChangesetCommentsModel().create(
333 text=text,
333 text=text,
334 repo=c.rhodecode_db_repo.repo_id,
334 repo=c.rhodecode_db_repo.repo_id,
335 user=c.rhodecode_user.user_id,
335 user=c.rhodecode_user.user_id,
336 revision=revision,
336 revision=revision,
337 f_path=request.POST.get('f_path'),
337 f_path=request.POST.get('f_path'),
338 line_no=request.POST.get('line'),
338 line_no=request.POST.get('line'),
339 status_change=(ChangesetStatus.get_status_lbl(status)
339 status_change=(ChangesetStatus.get_status_lbl(status)
340 if status and change_status else None)
340 if status and change_status else None)
341 )
341 )
342
342
343 # get status if set !
343 # get status if set !
344 if status and change_status:
344 if status and change_status:
345 # if latest status was from pull request and it's closed
345 # if latest status was from pull request and it's closed
346 # disallow changing status !
346 # disallow changing status !
347 # dont_allow_on_closed_pull_request = True !
347 # dont_allow_on_closed_pull_request = True !
348
348
349 try:
349 try:
350 ChangesetStatusModel().set_status(
350 ChangesetStatusModel().set_status(
351 c.rhodecode_db_repo.repo_id,
351 c.rhodecode_db_repo.repo_id,
352 status,
352 status,
353 c.rhodecode_user.user_id,
353 c.rhodecode_user.user_id,
354 comm,
354 comm,
355 revision=revision,
355 revision=revision,
356 dont_allow_on_closed_pull_request=True
356 dont_allow_on_closed_pull_request=True
357 )
357 )
358 except StatusChangeOnClosedPullRequestError:
358 except StatusChangeOnClosedPullRequestError:
359 log.error(traceback.format_exc())
359 log.error(traceback.format_exc())
360 msg = _('Changing status on a changeset associated with '
360 msg = _('Changing status on a changeset associated with '
361 'a closed pull request is not allowed')
361 'a closed pull request is not allowed')
362 h.flash(msg, category='warning')
362 h.flash(msg, category='warning')
363 return redirect(h.url('changeset_home', repo_name=repo_name,
363 return redirect(h.url('changeset_home', repo_name=repo_name,
364 revision=revision))
364 revision=revision))
365 action_logger(self.rhodecode_user,
365 action_logger(self.rhodecode_user,
366 'user_commented_revision:%s' % revision,
366 'user_commented_revision:%s' % revision,
367 c.rhodecode_db_repo, self.ip_addr, self.sa)
367 c.rhodecode_db_repo, self.ip_addr, self.sa)
368
368
369 Session().commit()
369 Session().commit()
370
370
371 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
371 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
372 return redirect(h.url('changeset_home', repo_name=repo_name,
372 return redirect(h.url('changeset_home', repo_name=repo_name,
373 revision=revision))
373 revision=revision))
374
374 #only ajax below
375 data = {
375 data = {
376 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
376 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
377 }
377 }
378 if comm:
378 if comm:
379 c.co = comm
380 data.update(comm.get_dict())
379 data.update(comm.get_dict())
381 data.update({'rendered_text':
380 data.update({'rendered_text':
382 render('changeset/changeset_comment_block.html')})
381 render('changeset/changeset_comment_block.html')})
383
382
384 return data
383 return data
385
384
386 @jsonify
385 @jsonify
387 def delete_comment(self, repo_name, comment_id):
386 def delete_comment(self, repo_name, comment_id):
388 co = ChangesetComment.get(comment_id)
387 co = ChangesetComment.get(comment_id)
389 owner = co.author.user_id == c.rhodecode_user.user_id
388 owner = co.author.user_id == c.rhodecode_user.user_id
390 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
389 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
391 ChangesetCommentsModel().delete(comment=co)
390 ChangesetCommentsModel().delete(comment=co)
392 Session().commit()
391 Session().commit()
393 return True
392 return True
394 else:
393 else:
395 raise HTTPForbidden()
394 raise HTTPForbidden()
396
395
397 @jsonify
396 @jsonify
398 def changeset_info(self, repo_name, revision):
397 def changeset_info(self, repo_name, revision):
399 if request.is_xhr:
398 if request.is_xhr:
400 try:
399 try:
401 return c.rhodecode_repo.get_changeset(revision)
400 return c.rhodecode_repo.get_changeset(revision)
402 except ChangesetDoesNotExistError, e:
401 except ChangesetDoesNotExistError, e:
403 return EmptyChangeset(message=str(e))
402 return EmptyChangeset(message=str(e))
404 else:
403 else:
405 raise HTTPBadRequest()
404 raise HTTPBadRequest()
@@ -1,479 +1,485 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import formencode
27 import formencode
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
30 from collections import defaultdict
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from pylons import request, response, session, tmpl_context as c, url
33 from pylons import request, response, session, tmpl_context as c, url
34 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.lib.compat import json
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 NotAnonymous
40 NotAnonymous
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib import diffs
42 from rhodecode.lib import diffs
43 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils import action_logger, jsonify
44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.diffs import LimitedDiffContainer
46 from rhodecode.lib.diffs import LimitedDiffContainer
47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
48 ChangesetComment
48 ChangesetComment
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.comment import ChangesetCommentsModel
52 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.forms import PullRequestForm
54 from rhodecode.model.forms import PullRequestForm
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class PullrequestsController(BaseRepoController):
59 class PullrequestsController(BaseRepoController):
60
60
61 @LoginRequired()
61 @LoginRequired()
62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
63 'repository.admin')
63 'repository.admin')
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66 repo_model = RepoModel()
66 repo_model = RepoModel()
67 c.users_array = repo_model.get_users_js()
67 c.users_array = repo_model.get_users_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
69
69
70 def _get_repo_refs(self, repo):
70 def _get_repo_refs(self, repo):
71 hist_l = []
71 hist_l = []
72
72
73 branches_group = ([('branch:%s:%s' % (k, v), k) for
73 branches_group = ([('branch:%s:%s' % (k, v), k) for
74 k, v in repo.branches.iteritems()], _("Branches"))
74 k, v in repo.branches.iteritems()], _("Branches"))
75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
77 tags_group = ([('tag:%s:%s' % (k, v), k) for
77 tags_group = ([('tag:%s:%s' % (k, v), k) for
78 k, v in repo.tags.iteritems()
78 k, v in repo.tags.iteritems()
79 if k != 'tip'], _("Tags"))
79 if k != 'tip'], _("Tags"))
80
80
81 tip = repo.tags['tip']
81 tip = repo.tags['tip']
82 tipref = 'tag:tip:%s' % tip
82 tipref = 'tag:tip:%s' % tip
83 colontip = ':' + tip
83 colontip = ':' + tip
84 tips = [x[1] for x in branches_group[0] + bookmarks_group[0] + tags_group[0]
84 tips = [x[1] for x in branches_group[0] + bookmarks_group[0] + tags_group[0]
85 if x[0].endswith(colontip)]
85 if x[0].endswith(colontip)]
86 tags_group[0].append((tipref, 'tip (%s)' % ', '.join(tips)))
86 tags_group[0].append((tipref, 'tip (%s)' % ', '.join(tips)))
87
87
88 hist_l.append(bookmarks_group)
88 hist_l.append(bookmarks_group)
89 hist_l.append(branches_group)
89 hist_l.append(branches_group)
90 hist_l.append(tags_group)
90 hist_l.append(tags_group)
91
91
92 return hist_l, tipref
92 return hist_l, tipref
93
93
94 def _get_is_allowed_change_status(self, pull_request):
94 def _get_is_allowed_change_status(self, pull_request):
95 owner = self.rhodecode_user.user_id == pull_request.user_id
95 owner = self.rhodecode_user.user_id == pull_request.user_id
96 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
96 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
97 pull_request.reviewers]
97 pull_request.reviewers]
98 return (self.rhodecode_user.admin or owner or reviewer)
98 return (self.rhodecode_user.admin or owner or reviewer)
99
99
100 def show_all(self, repo_name):
100 def show_all(self, repo_name):
101 c.pull_requests = PullRequestModel().get_all(repo_name)
101 c.pull_requests = PullRequestModel().get_all(repo_name)
102 c.repo_name = repo_name
102 c.repo_name = repo_name
103 return render('/pullrequests/pullrequest_show_all.html')
103 return render('/pullrequests/pullrequest_show_all.html')
104
104
105 @NotAnonymous()
105 @NotAnonymous()
106 def index(self):
106 def index(self):
107 org_repo = c.rhodecode_db_repo
107 org_repo = c.rhodecode_db_repo
108
108
109 if org_repo.scm_instance.alias != 'hg':
109 if org_repo.scm_instance.alias != 'hg':
110 log.error('Review not available for GIT REPOS')
110 log.error('Review not available for GIT REPOS')
111 raise HTTPNotFound
111 raise HTTPNotFound
112
112
113 try:
113 try:
114 org_repo.scm_instance.get_changeset()
114 org_repo.scm_instance.get_changeset()
115 except EmptyRepositoryError, e:
115 except EmptyRepositoryError, e:
116 h.flash(h.literal(_('There are no changesets yet')),
116 h.flash(h.literal(_('There are no changesets yet')),
117 category='warning')
117 category='warning')
118 redirect(url('summary_home', repo_name=org_repo.repo_name))
118 redirect(url('summary_home', repo_name=org_repo.repo_name))
119
119
120 other_repos_info = {}
120 other_repos_info = {}
121
121
122 c.org_repos = []
122 c.org_repos = []
123 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
123 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
124 c.default_org_repo = org_repo.repo_name
124 c.default_org_repo = org_repo.repo_name
125 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance)
125 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance)
126
126
127 c.other_repos = []
127 c.other_repos = []
128 # add org repo to other so we can open pull request against itself
128 # add org repo to other so we can open pull request against itself
129 c.other_repos.extend(c.org_repos)
129 c.other_repos.extend(c.org_repos)
130 c.default_other_repo = org_repo.repo_name
130 c.default_other_repo = org_repo.repo_name
131 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.scm_instance)
131 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.scm_instance)
132 usr_data = lambda usr: dict(user_id=usr.user_id,
132 usr_data = lambda usr: dict(user_id=usr.user_id,
133 username=usr.username,
133 username=usr.username,
134 firstname=usr.firstname,
134 firstname=usr.firstname,
135 lastname=usr.lastname,
135 lastname=usr.lastname,
136 gravatar_link=h.gravatar_url(usr.email, 14))
136 gravatar_link=h.gravatar_url(usr.email, 14))
137 other_repos_info[org_repo.repo_name] = {
137 other_repos_info[org_repo.repo_name] = {
138 'user': usr_data(org_repo.user),
138 'user': usr_data(org_repo.user),
139 'description': org_repo.description,
139 'description': org_repo.description,
140 'revs': h.select('other_ref', c.default_other_ref,
140 'revs': h.select('other_ref', c.default_other_ref,
141 c.default_other_refs, class_='refs')
141 c.default_other_refs, class_='refs')
142 }
142 }
143
143
144 # gather forks and add to this list ... even though it is rare to
144 # gather forks and add to this list ... even though it is rare to
145 # request forks to pull their parent
145 # request forks to pull their parent
146 for fork in org_repo.forks:
146 for fork in org_repo.forks:
147 c.other_repos.append((fork.repo_name, fork.repo_name))
147 c.other_repos.append((fork.repo_name, fork.repo_name))
148 refs, default_ref = self._get_repo_refs(fork.scm_instance)
148 refs, default_ref = self._get_repo_refs(fork.scm_instance)
149 other_repos_info[fork.repo_name] = {
149 other_repos_info[fork.repo_name] = {
150 'user': usr_data(fork.user),
150 'user': usr_data(fork.user),
151 'description': fork.description,
151 'description': fork.description,
152 'revs': h.select('other_ref', default_ref, refs, class_='refs')
152 'revs': h.select('other_ref', default_ref, refs, class_='refs')
153 }
153 }
154
154
155 # add parents of this fork also, but only if it's not empty
155 # add parents of this fork also, but only if it's not empty
156 if org_repo.parent and org_repo.parent.scm_instance.revisions:
156 if org_repo.parent and org_repo.parent.scm_instance.revisions:
157 c.default_other_repo = org_repo.parent.repo_name
157 c.default_other_repo = org_repo.parent.repo_name
158 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.parent.scm_instance)
158 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.parent.scm_instance)
159 c.other_repos.append((org_repo.parent.repo_name, org_repo.parent.repo_name))
159 c.other_repos.append((org_repo.parent.repo_name, org_repo.parent.repo_name))
160 other_repos_info[org_repo.parent.repo_name] = {
160 other_repos_info[org_repo.parent.repo_name] = {
161 'user': usr_data(org_repo.parent.user),
161 'user': usr_data(org_repo.parent.user),
162 'description': org_repo.parent.description,
162 'description': org_repo.parent.description,
163 'revs': h.select('other_ref', c.default_other_ref,
163 'revs': h.select('other_ref', c.default_other_ref,
164 c.default_other_refs, class_='refs')
164 c.default_other_refs, class_='refs')
165 }
165 }
166
166
167 c.other_repos_info = json.dumps(other_repos_info)
167 c.other_repos_info = json.dumps(other_repos_info)
168 # other repo owner
168 # other repo owner
169 c.review_members = []
169 c.review_members = []
170 return render('/pullrequests/pullrequest.html')
170 return render('/pullrequests/pullrequest.html')
171
171
172 @NotAnonymous()
172 @NotAnonymous()
173 def create(self, repo_name):
173 def create(self, repo_name):
174 repo = RepoModel()._get_repo(repo_name)
174 repo = RepoModel()._get_repo(repo_name)
175 try:
175 try:
176 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
176 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
177 except formencode.Invalid, errors:
177 except formencode.Invalid, errors:
178 log.error(traceback.format_exc())
178 log.error(traceback.format_exc())
179 if errors.error_dict.get('revisions'):
179 if errors.error_dict.get('revisions'):
180 msg = 'Revisions: %s' % errors.error_dict['revisions']
180 msg = 'Revisions: %s' % errors.error_dict['revisions']
181 elif errors.error_dict.get('pullrequest_title'):
181 elif errors.error_dict.get('pullrequest_title'):
182 msg = _('Pull request requires a title with min. 3 chars')
182 msg = _('Pull request requires a title with min. 3 chars')
183 else:
183 else:
184 msg = _('error during creation of pull request')
184 msg = _('error during creation of pull request')
185
185
186 h.flash(msg, 'error')
186 h.flash(msg, 'error')
187 return redirect(url('pullrequest_home', repo_name=repo_name))
187 return redirect(url('pullrequest_home', repo_name=repo_name))
188
188
189 org_repo = _form['org_repo']
189 org_repo = _form['org_repo']
190 org_ref = _form['org_ref']
190 org_ref = _form['org_ref']
191 other_repo = _form['other_repo']
191 other_repo = _form['other_repo']
192 other_ref = _form['other_ref']
192 other_ref = _form['other_ref']
193 revisions = _form['revisions']
193 revisions = _form['revisions']
194 reviewers = _form['review_members']
194 reviewers = _form['review_members']
195
195
196 # if we have cherry picked pull request we don't care what is in
196 # if we have cherry picked pull request we don't care what is in
197 # org_ref/other_ref
197 # org_ref/other_ref
198 rev_start = request.POST.get('rev_start')
198 rev_start = request.POST.get('rev_start')
199 rev_end = request.POST.get('rev_end')
199 rev_end = request.POST.get('rev_end')
200
200
201 if rev_start and rev_end:
201 if rev_start and rev_end:
202 # this is swapped to simulate that rev_end is a revision from
202 # this is swapped to simulate that rev_end is a revision from
203 # parent of the fork
203 # parent of the fork
204 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
204 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
205 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
205 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
206
206
207 title = _form['pullrequest_title']
207 title = _form['pullrequest_title']
208 description = _form['pullrequest_desc']
208 description = _form['pullrequest_desc']
209
209
210 try:
210 try:
211 pull_request = PullRequestModel().create(
211 pull_request = PullRequestModel().create(
212 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
212 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
213 other_ref, revisions, reviewers, title, description
213 other_ref, revisions, reviewers, title, description
214 )
214 )
215 Session().commit()
215 Session().commit()
216 h.flash(_('Successfully opened new pull request'),
216 h.flash(_('Successfully opened new pull request'),
217 category='success')
217 category='success')
218 except Exception:
218 except Exception:
219 h.flash(_('Error occurred during sending pull request'),
219 h.flash(_('Error occurred during sending pull request'),
220 category='error')
220 category='error')
221 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
222 return redirect(url('pullrequest_home', repo_name=repo_name))
222 return redirect(url('pullrequest_home', repo_name=repo_name))
223
223
224 return redirect(url('pullrequest_show', repo_name=other_repo,
224 return redirect(url('pullrequest_show', repo_name=other_repo,
225 pull_request_id=pull_request.pull_request_id))
225 pull_request_id=pull_request.pull_request_id))
226
226
227 @NotAnonymous()
227 @NotAnonymous()
228 @jsonify
228 @jsonify
229 def update(self, repo_name, pull_request_id):
229 def update(self, repo_name, pull_request_id):
230 pull_request = PullRequest.get_or_404(pull_request_id)
230 pull_request = PullRequest.get_or_404(pull_request_id)
231 if pull_request.is_closed():
231 if pull_request.is_closed():
232 raise HTTPForbidden()
232 raise HTTPForbidden()
233 #only owner or admin can update it
233 #only owner or admin can update it
234 owner = pull_request.author.user_id == c.rhodecode_user.user_id
234 owner = pull_request.author.user_id == c.rhodecode_user.user_id
235 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
235 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
236 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
236 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
237 request.POST.get('reviewers_ids', '').split(',')))
237 request.POST.get('reviewers_ids', '').split(',')))
238
238
239 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
239 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
240 Session().commit()
240 Session().commit()
241 return True
241 return True
242 raise HTTPForbidden()
242 raise HTTPForbidden()
243
243
244 @NotAnonymous()
244 @NotAnonymous()
245 @jsonify
245 @jsonify
246 def delete(self, repo_name, pull_request_id):
246 def delete(self, repo_name, pull_request_id):
247 pull_request = PullRequest.get_or_404(pull_request_id)
247 pull_request = PullRequest.get_or_404(pull_request_id)
248 #only owner can delete it !
248 #only owner can delete it !
249 if pull_request.author.user_id == c.rhodecode_user.user_id:
249 if pull_request.author.user_id == c.rhodecode_user.user_id:
250 PullRequestModel().delete(pull_request)
250 PullRequestModel().delete(pull_request)
251 Session().commit()
251 Session().commit()
252 h.flash(_('Successfully deleted pull request'),
252 h.flash(_('Successfully deleted pull request'),
253 category='success')
253 category='success')
254 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
254 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
255 raise HTTPForbidden()
255 raise HTTPForbidden()
256
256
257 def _load_compare_data(self, pull_request, enable_comments=True):
257 def _load_compare_data(self, pull_request, enable_comments=True):
258 """
258 """
259 Load context data needed for generating compare diff
259 Load context data needed for generating compare diff
260
260
261 :param pull_request:
261 :param pull_request:
262 :type pull_request:
262 :type pull_request:
263 """
263 """
264 rev_start = request.GET.get('rev_start')
264 rev_start = request.GET.get('rev_start')
265 rev_end = request.GET.get('rev_end')
265 rev_end = request.GET.get('rev_end')
266
266
267 org_repo = pull_request.org_repo
267 org_repo = pull_request.org_repo
268 (org_ref_type,
268 (org_ref_type,
269 org_ref_name,
269 org_ref_name,
270 org_ref_rev) = pull_request.org_ref.split(':')
270 org_ref_rev) = pull_request.org_ref.split(':')
271
271
272 other_repo = org_repo
272 other_repo = org_repo
273 (other_ref_type,
273 (other_ref_type,
274 other_ref_name,
274 other_ref_name,
275 other_ref_rev) = pull_request.other_ref.split(':')
275 other_ref_rev) = pull_request.other_ref.split(':')
276
276
277 # despite opening revisions for bookmarks/branches/tags, we always
277 # despite opening revisions for bookmarks/branches/tags, we always
278 # convert this to rev to prevent changes after book or branch change
278 # convert this to rev to prevent changes after book or branch change
279 org_ref = ('rev', org_ref_rev)
279 org_ref = ('rev', org_ref_rev)
280 other_ref = ('rev', other_ref_rev)
280 other_ref = ('rev', other_ref_rev)
281
281
282 c.org_repo = org_repo
282 c.org_repo = org_repo
283 c.other_repo = other_repo
283 c.other_repo = other_repo
284
284
285 c.fulldiff = fulldiff = request.GET.get('fulldiff')
285 c.fulldiff = fulldiff = request.GET.get('fulldiff')
286
286
287 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
287 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
288
288
289 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
289 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
290 if c.cs_ranges[0].parents
290 if c.cs_ranges[0].parents
291 else EmptyChangeset(), 'raw_id'))
291 else EmptyChangeset(), 'raw_id'))
292
292
293 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
293 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
294 # defines that we need hidden inputs with changesets
294 # defines that we need hidden inputs with changesets
295 c.as_form = request.GET.get('as_form', False)
295 c.as_form = request.GET.get('as_form', False)
296
296
297 c.org_ref = org_ref[1]
297 c.org_ref = org_ref[1]
298 c.org_ref_type = org_ref[0]
298 c.org_ref_type = org_ref[0]
299 c.other_ref = other_ref[1]
299 c.other_ref = other_ref[1]
300 c.other_ref_type = other_ref[0]
300 c.other_ref_type = other_ref[0]
301
301
302 diff_limit = self.cut_off_limit if not fulldiff else None
302 diff_limit = self.cut_off_limit if not fulldiff else None
303
303
304 #we swap org/other ref since we run a simple diff on one repo
304 #we swap org/other ref since we run a simple diff on one repo
305 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
305 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
306
306
307 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
307 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
308 diff_limit=diff_limit)
308 diff_limit=diff_limit)
309 _parsed = diff_processor.prepare()
309 _parsed = diff_processor.prepare()
310
310
311 c.limited_diff = False
311 c.limited_diff = False
312 if isinstance(_parsed, LimitedDiffContainer):
312 if isinstance(_parsed, LimitedDiffContainer):
313 c.limited_diff = True
313 c.limited_diff = True
314
314
315 c.files = []
315 c.files = []
316 c.changes = {}
316 c.changes = {}
317 c.lines_added = 0
317 c.lines_added = 0
318 c.lines_deleted = 0
318 c.lines_deleted = 0
319 for f in _parsed:
319 for f in _parsed:
320 st = f['stats']
320 st = f['stats']
321 if st[0] != 'b':
321 if st[0] != 'b':
322 c.lines_added += st[0]
322 c.lines_added += st[0]
323 c.lines_deleted += st[1]
323 c.lines_deleted += st[1]
324 fid = h.FID('', f['filename'])
324 fid = h.FID('', f['filename'])
325 c.files.append([fid, f['operation'], f['filename'], f['stats']])
325 c.files.append([fid, f['operation'], f['filename'], f['stats']])
326 diff = diff_processor.as_html(enable_comments=enable_comments,
326 diff = diff_processor.as_html(enable_comments=enable_comments,
327 parsed_lines=[f])
327 parsed_lines=[f])
328 c.changes[fid] = [f['operation'], f['filename'], diff]
328 c.changes[fid] = [f['operation'], f['filename'], diff]
329
329
330 def show(self, repo_name, pull_request_id):
330 def show(self, repo_name, pull_request_id):
331 repo_model = RepoModel()
331 repo_model = RepoModel()
332 c.users_array = repo_model.get_users_js()
332 c.users_array = repo_model.get_users_js()
333 c.users_groups_array = repo_model.get_users_groups_js()
333 c.users_groups_array = repo_model.get_users_groups_js()
334 c.pull_request = PullRequest.get_or_404(pull_request_id)
334 c.pull_request = PullRequest.get_or_404(pull_request_id)
335 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
335 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
336 cc_model = ChangesetCommentsModel()
336 cc_model = ChangesetCommentsModel()
337 cs_model = ChangesetStatusModel()
337 cs_model = ChangesetStatusModel()
338 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
338 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
339 pull_request=c.pull_request,
339 pull_request=c.pull_request,
340 with_revisions=True)
340 with_revisions=True)
341
341
342 cs_statuses = defaultdict(list)
342 cs_statuses = defaultdict(list)
343 for st in _cs_statuses:
343 for st in _cs_statuses:
344 cs_statuses[st.author.username] += [st]
344 cs_statuses[st.author.username] += [st]
345
345
346 c.pull_request_reviewers = []
346 c.pull_request_reviewers = []
347 c.pull_request_pending_reviewers = []
347 c.pull_request_pending_reviewers = []
348 for o in c.pull_request.reviewers:
348 for o in c.pull_request.reviewers:
349 st = cs_statuses.get(o.user.username, None)
349 st = cs_statuses.get(o.user.username, None)
350 if st:
350 if st:
351 sorter = lambda k: k.version
351 sorter = lambda k: k.version
352 st = [(x, list(y)[0])
352 st = [(x, list(y)[0])
353 for x, y in (groupby(sorted(st, key=sorter), sorter))]
353 for x, y in (groupby(sorted(st, key=sorter), sorter))]
354 else:
354 else:
355 c.pull_request_pending_reviewers.append(o.user)
355 c.pull_request_pending_reviewers.append(o.user)
356 c.pull_request_reviewers.append([o.user, st])
356 c.pull_request_reviewers.append([o.user, st])
357
357
358 # pull_requests repo_name we opened it against
358 # pull_requests repo_name we opened it against
359 # ie. other_repo must match
359 # ie. other_repo must match
360 if repo_name != c.pull_request.other_repo.repo_name:
360 if repo_name != c.pull_request.other_repo.repo_name:
361 raise HTTPNotFound
361 raise HTTPNotFound
362
362
363 # load compare data into template context
363 # load compare data into template context
364 enable_comments = not c.pull_request.is_closed()
364 enable_comments = not c.pull_request.is_closed()
365 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
365 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
366
366
367 # inline comments
367 # inline comments
368 c.inline_cnt = 0
368 c.inline_cnt = 0
369 c.inline_comments = cc_model.get_inline_comments(
369 c.inline_comments = cc_model.get_inline_comments(
370 c.rhodecode_db_repo.repo_id,
370 c.rhodecode_db_repo.repo_id,
371 pull_request=pull_request_id)
371 pull_request=pull_request_id)
372 # count inline comments
372 # count inline comments
373 for __, lines in c.inline_comments:
373 for __, lines in c.inline_comments:
374 for comments in lines.values():
374 for comments in lines.values():
375 c.inline_cnt += len(comments)
375 c.inline_cnt += len(comments)
376 # comments
376 # comments
377 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
377 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
378 pull_request=pull_request_id)
378 pull_request=pull_request_id)
379
379
380 try:
380 try:
381 cur_status = c.statuses[c.pull_request.revisions[0]][0]
381 cur_status = c.statuses[c.pull_request.revisions[0]][0]
382 except:
382 except:
383 log.error(traceback.format_exc())
383 log.error(traceback.format_exc())
384 cur_status = 'undefined'
384 cur_status = 'undefined'
385 if c.pull_request.is_closed() and 0:
385 if c.pull_request.is_closed() and 0:
386 c.current_changeset_status = cur_status
386 c.current_changeset_status = cur_status
387 else:
387 else:
388 # changeset(pull-request) status calulation based on reviewers
388 # changeset(pull-request) status calulation based on reviewers
389 c.current_changeset_status = cs_model.calculate_status(
389 c.current_changeset_status = cs_model.calculate_status(
390 c.pull_request_reviewers,
390 c.pull_request_reviewers,
391 )
391 )
392 c.changeset_statuses = ChangesetStatus.STATUSES
392 c.changeset_statuses = ChangesetStatus.STATUSES
393
393
394 return render('/pullrequests/pullrequest_show.html')
394 return render('/pullrequests/pullrequest_show.html')
395
395
396 @NotAnonymous()
396 @NotAnonymous()
397 @jsonify
397 @jsonify
398 def comment(self, repo_name, pull_request_id):
398 def comment(self, repo_name, pull_request_id):
399 pull_request = PullRequest.get_or_404(pull_request_id)
399 pull_request = PullRequest.get_or_404(pull_request_id)
400 if pull_request.is_closed():
400 if pull_request.is_closed():
401 raise HTTPForbidden()
401 raise HTTPForbidden()
402
402
403 status = request.POST.get('changeset_status')
403 status = request.POST.get('changeset_status')
404 change_status = request.POST.get('change_changeset_status')
404 change_status = request.POST.get('change_changeset_status')
405 text = request.POST.get('text')
405 text = request.POST.get('text')
406 close_pr = request.POST.get('save_close')
406
407
407 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
408 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
408 if status and change_status and allowed_to_change_status:
409 if status and change_status and allowed_to_change_status:
409 text = text or (_('Status change -> %s')
410 _def = (_('status change -> %s')
410 % ChangesetStatus.get_status_lbl(status))
411 % ChangesetStatus.get_status_lbl(status))
412 if close_pr:
413 _def = _('Closing with') + ' ' + _def
414 text = text or _def
411 comm = ChangesetCommentsModel().create(
415 comm = ChangesetCommentsModel().create(
412 text=text,
416 text=text,
413 repo=c.rhodecode_db_repo.repo_id,
417 repo=c.rhodecode_db_repo.repo_id,
414 user=c.rhodecode_user.user_id,
418 user=c.rhodecode_user.user_id,
415 pull_request=pull_request_id,
419 pull_request=pull_request_id,
416 f_path=request.POST.get('f_path'),
420 f_path=request.POST.get('f_path'),
417 line_no=request.POST.get('line'),
421 line_no=request.POST.get('line'),
418 status_change=(ChangesetStatus.get_status_lbl(status)
422 status_change=(ChangesetStatus.get_status_lbl(status)
419 if status and change_status and allowed_to_change_status else None)
423 if status and change_status
424 and allowed_to_change_status else None),
425 closing_pr=close_pr
420 )
426 )
421
427
422 action_logger(self.rhodecode_user,
428 action_logger(self.rhodecode_user,
423 'user_commented_pull_request:%s' % pull_request_id,
429 'user_commented_pull_request:%s' % pull_request_id,
424 c.rhodecode_db_repo, self.ip_addr, self.sa)
430 c.rhodecode_db_repo, self.ip_addr, self.sa)
425
431
426 if allowed_to_change_status:
432 if allowed_to_change_status:
427 # get status if set !
433 # get status if set !
428 if status and change_status:
434 if status and change_status:
429 ChangesetStatusModel().set_status(
435 ChangesetStatusModel().set_status(
430 c.rhodecode_db_repo.repo_id,
436 c.rhodecode_db_repo.repo_id,
431 status,
437 status,
432 c.rhodecode_user.user_id,
438 c.rhodecode_user.user_id,
433 comm,
439 comm,
434 pull_request=pull_request_id
440 pull_request=pull_request_id
435 )
441 )
436
442
437 if request.POST.get('save_close'):
443 if close_pr:
438 if status in ['rejected', 'approved']:
444 if status in ['rejected', 'approved']:
439 PullRequestModel().close_pull_request(pull_request_id)
445 PullRequestModel().close_pull_request(pull_request_id)
440 action_logger(self.rhodecode_user,
446 action_logger(self.rhodecode_user,
441 'user_closed_pull_request:%s' % pull_request_id,
447 'user_closed_pull_request:%s' % pull_request_id,
442 c.rhodecode_db_repo, self.ip_addr, self.sa)
448 c.rhodecode_db_repo, self.ip_addr, self.sa)
443 else:
449 else:
444 h.flash(_('Closing pull request on other statuses than '
450 h.flash(_('Closing pull request on other statuses than '
445 'rejected or approved forbidden'),
451 'rejected or approved forbidden'),
446 category='warning')
452 category='warning')
447
453
448 Session().commit()
454 Session().commit()
449
455
450 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
456 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
451 return redirect(h.url('pullrequest_show', repo_name=repo_name,
457 return redirect(h.url('pullrequest_show', repo_name=repo_name,
452 pull_request_id=pull_request_id))
458 pull_request_id=pull_request_id))
453
459
454 data = {
460 data = {
455 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
461 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
456 }
462 }
457 if comm:
463 if comm:
458 c.co = comm
464 c.co = comm
459 data.update(comm.get_dict())
465 data.update(comm.get_dict())
460 data.update({'rendered_text':
466 data.update({'rendered_text':
461 render('changeset/changeset_comment_block.html')})
467 render('changeset/changeset_comment_block.html')})
462
468
463 return data
469 return data
464
470
465 @NotAnonymous()
471 @NotAnonymous()
466 @jsonify
472 @jsonify
467 def delete_comment(self, repo_name, comment_id):
473 def delete_comment(self, repo_name, comment_id):
468 co = ChangesetComment.get(comment_id)
474 co = ChangesetComment.get(comment_id)
469 if co.pull_request.is_closed():
475 if co.pull_request.is_closed():
470 #don't allow deleting comments on closed pull request
476 #don't allow deleting comments on closed pull request
471 raise HTTPForbidden()
477 raise HTTPForbidden()
472
478
473 owner = co.author.user_id == c.rhodecode_user.user_id
479 owner = co.author.user_id == c.rhodecode_user.user_id
474 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
480 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
475 ChangesetCommentsModel().delete(comment=co)
481 ChangesetCommentsModel().delete(comment=co)
476 Session().commit()
482 Session().commit()
477 return True
483 return True
478 else:
484 else:
479 raise HTTPForbidden()
485 raise HTTPForbidden()
@@ -1,249 +1,284 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.comment
3 rhodecode.model.comment
4 ~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 comments model for RhodeCode
6 comments model for RhodeCode
7
7
8 :created_on: Nov 11, 2011
8 :created_on: Nov 11, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30 from sqlalchemy.util.compat import defaultdict
30 from sqlalchemy.util.compat import defaultdict
31
31
32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import ChangesetComment, User, Repository, \
35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 Notification, PullRequest
36 Notification, PullRequest
37 from rhodecode.model.notification import NotificationModel
37 from rhodecode.model.notification import NotificationModel
38 from rhodecode.model.meta import Session
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 class ChangesetCommentsModel(BaseModel):
43 class ChangesetCommentsModel(BaseModel):
43
44
44 cls = ChangesetComment
45 cls = ChangesetComment
45
46
46 def __get_changeset_comment(self, changeset_comment):
47 def __get_changeset_comment(self, changeset_comment):
47 return self._get_instance(ChangesetComment, changeset_comment)
48 return self._get_instance(ChangesetComment, changeset_comment)
48
49
49 def __get_pull_request(self, pull_request):
50 def __get_pull_request(self, pull_request):
50 return self._get_instance(PullRequest, pull_request)
51 return self._get_instance(PullRequest, pull_request)
51
52
52 def _extract_mentions(self, s):
53 def _extract_mentions(self, s):
53 user_objects = []
54 user_objects = []
54 for username in extract_mentioned_users(s):
55 for username in extract_mentioned_users(s):
55 user_obj = User.get_by_username(username, case_insensitive=True)
56 user_obj = User.get_by_username(username, case_insensitive=True)
56 if user_obj:
57 if user_obj:
57 user_objects.append(user_obj)
58 user_objects.append(user_obj)
58 return user_objects
59 return user_objects
59
60
61 def _get_notification_data(self, repo, comment, user, comment_text,
62 line_no=None, revision=None, pull_request=None,
63 status_change=None, closing_pr=False):
64 """
65 Get notification data
66
67 :param comment_text:
68 :param line:
69 :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
70 """
71 # make notification
72 body = comment_text # text of the comment
73 line = ''
74 if line_no:
75 line = _('on line %s') % line_no
76
77 #changeset
78 if revision:
79 notification_type = Notification.TYPE_CHANGESET_COMMENT
80 cs = repo.scm_instance.get_changeset(revision)
81 desc = "%s" % (cs.short_id)
82
83 _url = h.url('changeset_home',
84 repo_name=repo.repo_name,
85 revision=revision,
86 anchor='comment-%s' % comment.comment_id,
87 qualified=True,
88 )
89 subj = safe_unicode(
90 h.link_to('Re changeset: %(desc)s %(line)s' % \
91 {'desc': desc, 'line': line},
92 _url)
93 )
94 email_subject = 'User %s commented on changeset %s' % \
95 (user.username, h.short_id(revision))
96 # get the current participants of this changeset
97 recipients = ChangesetComment.get_users(revision=revision)
98 # add changeset author if it's in rhodecode system
99 cs_author = User.get_from_cs_author(cs.author)
100 if not cs_author:
101 #use repo owner if we cannot extract the author correctly
102 cs_author = repo.user
103 recipients += [cs_author]
104 email_kwargs = {
105 'status_change': status_change,
106 'cs_comment_user': h.person(user.email),
107 'cs_target_repo': h.url('summary_home', repo_name=repo.repo_name,
108 qualified=True),
109 'cs_comment_url': _url,
110 'raw_id': revision,
111 'message': cs.message
112 }
113 #pull request
114 elif pull_request:
115 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
116 desc = comment.pull_request.title
117 _url = h.url('pullrequest_show',
118 repo_name=pull_request.other_repo.repo_name,
119 pull_request_id=pull_request.pull_request_id,
120 anchor='comment-%s' % comment.comment_id,
121 qualified=True,
122 )
123 subj = safe_unicode(
124 h.link_to('Re pull request #%(pr_id)s: %(desc)s %(line)s' % \
125 {'desc': desc,
126 'pr_id': comment.pull_request.pull_request_id,
127 'line': line},
128 _url)
129 )
130 email_subject = 'User %s commented on pull request #%s' % \
131 (user.username, comment.pull_request.pull_request_id)
132 # get the current participants of this pull request
133 recipients = ChangesetComment.get_users(pull_request_id=
134 pull_request.pull_request_id)
135 # add pull request author
136 recipients += [pull_request.author]
137
138 # add the reviewers to notification
139 recipients += [x.user for x in pull_request.reviewers]
140
141 #set some variables for email notification
142 email_kwargs = {
143 'pr_id': pull_request.pull_request_id,
144 'status_change': status_change,
145 'closing_pr': closing_pr,
146 'pr_comment_url': _url,
147 'pr_comment_user': h.person(user.email),
148 'pr_target_repo': h.url('summary_home',
149 repo_name=pull_request.other_repo.repo_name,
150 qualified=True)
151 }
152
153 return subj, body, recipients, notification_type, email_kwargs, email_subject
154
60 def create(self, text, repo, user, revision=None, pull_request=None,
155 def create(self, text, repo, user, revision=None, pull_request=None,
61 f_path=None, line_no=None, status_change=None, send_email=True):
156 f_path=None, line_no=None, status_change=None, closing_pr=False,
157 send_email=True):
62 """
158 """
63 Creates new comment for changeset or pull request.
159 Creates new comment for changeset or pull request.
64 IF status_change is not none this comment is associated with a
160 IF status_change is not none this comment is associated with a
65 status change of changeset or changesets associated with pull request
161 status change of changeset or changesets associated with pull request
66
162
67 :param text:
163 :param text:
68 :param repo:
164 :param repo:
69 :param user:
165 :param user:
70 :param revision:
166 :param revision:
71 :param pull_request:
167 :param pull_request:
72 :param f_path:
168 :param f_path:
73 :param line_no:
169 :param line_no:
74 :param status_change:
170 :param status_change:
171 :param closing_pr:
75 :param send_email:
172 :param send_email:
76 """
173 """
77 if not text:
174 if not text:
175 log.warning('Missing text for comment, skipping...')
78 return
176 return
79
177
80 repo = self._get_repo(repo)
178 repo = self._get_repo(repo)
81 user = self._get_user(user)
179 user = self._get_user(user)
82 comment = ChangesetComment()
180 comment = ChangesetComment()
83 comment.repo = repo
181 comment.repo = repo
84 comment.author = user
182 comment.author = user
85 comment.text = text
183 comment.text = text
86 comment.f_path = f_path
184 comment.f_path = f_path
87 comment.line_no = line_no
185 comment.line_no = line_no
88
186
89 if revision:
187 if revision:
90 cs = repo.scm_instance.get_changeset(revision)
91 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
92 comment.revision = revision
188 comment.revision = revision
93 elif pull_request:
189 elif pull_request:
94 pull_request = self.__get_pull_request(pull_request)
190 pull_request = self.__get_pull_request(pull_request)
95 comment.pull_request = pull_request
191 comment.pull_request = pull_request
96 else:
192 else:
97 raise Exception('Please specify revision or pull_request_id')
193 raise Exception('Please specify revision or pull_request_id')
98
194
99 self.sa.add(comment)
195 Session().add(comment)
100 self.sa.flush()
196 Session().flush()
101
102 # make notification
103 line = ''
104 body = text
105
106 #changeset
107 if revision:
108 if line_no:
109 line = _('on line %s') % line_no
110 subj = safe_unicode(
111 h.link_to('Re commit: %(desc)s %(line)s' % \
112 {'desc': desc, 'line': line},
113 h.url('changeset_home', repo_name=repo.repo_name,
114 revision=revision,
115 anchor='comment-%s' % comment.comment_id,
116 qualified=True,
117 )
118 )
119 )
120 notification_type = Notification.TYPE_CHANGESET_COMMENT
121 # get the current participants of this changeset
122 recipients = ChangesetComment.get_users(revision=revision)
123 # add changeset author if it's in rhodecode system
124 cs_author = User.get_from_cs_author(cs.author)
125 if not cs_author:
126 #use repo owner if we cannot extract the author correctly
127 cs_author = repo.user
128 recipients += [cs_author]
129 email_kwargs = {
130 'status_change': status_change,
131 }
132 #pull request
133 elif pull_request:
134 _url = h.url('pullrequest_show',
135 repo_name=pull_request.other_repo.repo_name,
136 pull_request_id=pull_request.pull_request_id,
137 anchor='comment-%s' % comment.comment_id,
138 qualified=True,
139 )
140 subj = safe_unicode(
141 h.link_to('Re pull request #%(pr_id)s: %(desc)s %(line)s' % \
142 {'desc': comment.pull_request.title,
143 'pr_id': comment.pull_request.pull_request_id,
144 'line': line},
145 _url)
146 )
147
148 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
149 # get the current participants of this pull request
150 recipients = ChangesetComment.get_users(pull_request_id=
151 pull_request.pull_request_id)
152 # add pull request author
153 recipients += [pull_request.author]
154
155 # add the reviewers to notification
156 recipients += [x.user for x in pull_request.reviewers]
157
158 #set some variables for email notification
159 email_kwargs = {
160 'pr_id': pull_request.pull_request_id,
161 'status_change': status_change,
162 'pr_comment_url': _url,
163 'pr_comment_user': h.person(user.email),
164 'pr_target_repo': h.url('summary_home',
165 repo_name=pull_request.other_repo.repo_name,
166 qualified=True)
167 }
168
197
169 if send_email:
198 if send_email:
199 (subj, body, recipients, notification_type,
200 email_kwargs, email_subject) = self._get_notification_data(
201 repo, comment, user,
202 comment_text=text,
203 line_no=line_no,
204 revision=revision,
205 pull_request=pull_request,
206 status_change=status_change,
207 closing_pr=closing_pr)
170 # create notification objects, and emails
208 # create notification objects, and emails
171 NotificationModel().create(
209 NotificationModel().create(
172 created_by=user, subject=subj, body=body,
210 created_by=user, subject=subj, body=body,
173 recipients=recipients, type_=notification_type,
211 recipients=recipients, type_=notification_type,
174 email_kwargs=email_kwargs
212 email_kwargs=email_kwargs, email_subject=email_subject
175 )
213 )
176
214
177 mention_recipients = set(self._extract_mentions(body))\
215 mention_recipients = set(self._extract_mentions(body))\
178 .difference(recipients)
216 .difference(recipients)
179 if mention_recipients:
217 if mention_recipients:
180 email_kwargs.update({'pr_mention': True})
218 email_kwargs.update({'pr_mention': True})
181 subj = _('[Mention]') + ' ' + subj
219 subj = _('[Mention]') + ' ' + subj
182 NotificationModel().create(
220 NotificationModel().create(
183 created_by=user, subject=subj, body=body,
221 created_by=user, subject=subj, body=body,
184 recipients=mention_recipients,
222 recipients=mention_recipients,
185 type_=notification_type,
223 type_=notification_type,
186 email_kwargs=email_kwargs
224 email_kwargs=email_kwargs
187 )
225 )
188
226
189 return comment
227 return comment
190
228
191 def delete(self, comment):
229 def delete(self, comment):
192 """
230 """
193 Deletes given comment
231 Deletes given comment
194
232
195 :param comment_id:
233 :param comment_id:
196 """
234 """
197 comment = self.__get_changeset_comment(comment)
235 comment = self.__get_changeset_comment(comment)
198 self.sa.delete(comment)
236 Session().delete(comment)
199
237
200 return comment
238 return comment
201
239
202 def get_comments(self, repo_id, revision=None, pull_request=None):
240 def get_comments(self, repo_id, revision=None, pull_request=None):
203 """
241 """
204 Get's main comments based on revision or pull_request_id
242 Get's main comments based on revision or pull_request_id
205
243
206 :param repo_id:
244 :param repo_id:
207 :type repo_id:
208 :param revision:
245 :param revision:
209 :type revision:
210 :param pull_request:
246 :param pull_request:
211 :type pull_request:
212 """
247 """
213
248
214 q = ChangesetComment.query()\
249 q = ChangesetComment.query()\
215 .filter(ChangesetComment.repo_id == repo_id)\
250 .filter(ChangesetComment.repo_id == repo_id)\
216 .filter(ChangesetComment.line_no == None)\
251 .filter(ChangesetComment.line_no == None)\
217 .filter(ChangesetComment.f_path == None)
252 .filter(ChangesetComment.f_path == None)
218 if revision:
253 if revision:
219 q = q.filter(ChangesetComment.revision == revision)
254 q = q.filter(ChangesetComment.revision == revision)
220 elif pull_request:
255 elif pull_request:
221 pull_request = self.__get_pull_request(pull_request)
256 pull_request = self.__get_pull_request(pull_request)
222 q = q.filter(ChangesetComment.pull_request == pull_request)
257 q = q.filter(ChangesetComment.pull_request == pull_request)
223 else:
258 else:
224 raise Exception('Please specify revision or pull_request')
259 raise Exception('Please specify revision or pull_request')
225 q = q.order_by(ChangesetComment.created_on)
260 q = q.order_by(ChangesetComment.created_on)
226 return q.all()
261 return q.all()
227
262
228 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
263 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
229 q = self.sa.query(ChangesetComment)\
264 q = Session().query(ChangesetComment)\
230 .filter(ChangesetComment.repo_id == repo_id)\
265 .filter(ChangesetComment.repo_id == repo_id)\
231 .filter(ChangesetComment.line_no != None)\
266 .filter(ChangesetComment.line_no != None)\
232 .filter(ChangesetComment.f_path != None)\
267 .filter(ChangesetComment.f_path != None)\
233 .order_by(ChangesetComment.comment_id.asc())\
268 .order_by(ChangesetComment.comment_id.asc())\
234
269
235 if revision:
270 if revision:
236 q = q.filter(ChangesetComment.revision == revision)
271 q = q.filter(ChangesetComment.revision == revision)
237 elif pull_request:
272 elif pull_request:
238 pull_request = self.__get_pull_request(pull_request)
273 pull_request = self.__get_pull_request(pull_request)
239 q = q.filter(ChangesetComment.pull_request == pull_request)
274 q = q.filter(ChangesetComment.pull_request == pull_request)
240 else:
275 else:
241 raise Exception('Please specify revision or pull_request_id')
276 raise Exception('Please specify revision or pull_request_id')
242
277
243 comments = q.all()
278 comments = q.all()
244
279
245 paths = defaultdict(lambda: defaultdict(list))
280 paths = defaultdict(lambda: defaultdict(list))
246
281
247 for co in comments:
282 for co in comments:
248 paths[co.f_path][co.line_no].append(co)
283 paths[co.f_path][co.line_no].append(co)
249 return paths.items()
284 return paths.items()
@@ -1,280 +1,282 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.notification
3 rhodecode.model.notification
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Model for notifications
6 Model for notifications
7
7
8
8
9 :created_on: Nov 20, 2011
9 :created_on: Nov 20, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons import tmpl_context as c
31 from pylons import tmpl_context as c
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.db import Notification, User, UserNotification
38 from rhodecode.model.meta import Session
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 class NotificationModel(BaseModel):
43 class NotificationModel(BaseModel):
43
44
44 cls = Notification
45 cls = Notification
45
46
46 def __get_notification(self, notification):
47 def __get_notification(self, notification):
47 if isinstance(notification, Notification):
48 if isinstance(notification, Notification):
48 return notification
49 return notification
49 elif isinstance(notification, (int, long)):
50 elif isinstance(notification, (int, long)):
50 return Notification.get(notification)
51 return Notification.get(notification)
51 else:
52 else:
52 if notification:
53 if notification:
53 raise Exception('notification must be int, long or Instance'
54 raise Exception('notification must be int, long or Instance'
54 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
55
56
56 def create(self, created_by, subject, body, recipients=None,
57 def create(self, created_by, subject, body, recipients=None,
57 type_=Notification.TYPE_MESSAGE, with_email=True,
58 type_=Notification.TYPE_MESSAGE, with_email=True,
58 email_kwargs={}):
59 email_kwargs={}, email_subject=None):
59 """
60 """
60
61
61 Creates notification of given type
62 Creates notification of given type
62
63
63 :param created_by: int, str or User instance. User who created this
64 :param created_by: int, str or User instance. User who created this
64 notification
65 notification
65 :param subject:
66 :param subject:
66 :param body:
67 :param body:
67 :param recipients: list of int, str or User objects, when None
68 :param recipients: list of int, str or User objects, when None
68 is given send to all admins
69 is given send to all admins
69 :param type_: type of notification
70 :param type_: type of notification
70 :param with_email: send email with this notification
71 :param with_email: send email with this notification
71 :param email_kwargs: additional dict to pass as args to email template
72 :param email_kwargs: additional dict to pass as args to email template
73 :param email_subject: use given subject as email subject
72 """
74 """
73 from rhodecode.lib.celerylib import tasks, run_task
75 from rhodecode.lib.celerylib import tasks, run_task
74
76
75 if recipients and not getattr(recipients, '__iter__', False):
77 if recipients and not getattr(recipients, '__iter__', False):
76 raise Exception('recipients must be a list or iterable')
78 raise Exception('recipients must be a list or iterable')
77
79
78 created_by_obj = self._get_user(created_by)
80 created_by_obj = self._get_user(created_by)
79
81
80 if recipients:
82 if recipients:
81 recipients_objs = []
83 recipients_objs = []
82 for u in recipients:
84 for u in recipients:
83 obj = self._get_user(u)
85 obj = self._get_user(u)
84 if obj:
86 if obj:
85 recipients_objs.append(obj)
87 recipients_objs.append(obj)
86 recipients_objs = set(recipients_objs)
88 recipients_objs = set(recipients_objs)
87 log.debug('sending notifications %s to %s' % (
89 log.debug('sending notifications %s to %s' % (
88 type_, recipients_objs)
90 type_, recipients_objs)
89 )
91 )
90 else:
92 else:
91 # empty recipients means to all admins
93 # empty recipients means to all admins
92 recipients_objs = User.query().filter(User.admin == True).all()
94 recipients_objs = User.query().filter(User.admin == True).all()
93 log.debug('sending notifications %s to admins: %s' % (
95 log.debug('sending notifications %s to admins: %s' % (
94 type_, recipients_objs)
96 type_, recipients_objs)
95 )
97 )
96 notif = Notification.create(
98 notif = Notification.create(
97 created_by=created_by_obj, subject=subject,
99 created_by=created_by_obj, subject=subject,
98 body=body, recipients=recipients_objs, type_=type_
100 body=body, recipients=recipients_objs, type_=type_
99 )
101 )
100
102
101 if with_email is False:
103 if with_email is False:
102 return notif
104 return notif
103
105
104 #don't send email to person who created this comment
106 #don't send email to person who created this comment
105 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
107 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
106
108
107 # send email with notification to all other participants
109 # send email with notification to all other participants
108 for rec in rec_objs:
110 for rec in rec_objs:
109 email_subject = NotificationModel().make_description(notif, False)
111 if not email_subject:
112 email_subject = NotificationModel().make_description(notif, show_age=False)
110 type_ = type_
113 type_ = type_
111 email_body = body
114 email_body = body
112 ## this is passed into template
115 ## this is passed into template
113 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
116 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
114 kwargs.update(email_kwargs)
117 kwargs.update(email_kwargs)
115 email_body_html = EmailNotificationModel()\
118 email_body_html = EmailNotificationModel()\
116 .get_email_tmpl(type_, **kwargs)
119 .get_email_tmpl(type_, **kwargs)
117
120
118 run_task(tasks.send_email, rec.email, email_subject, email_body,
121 run_task(tasks.send_email, rec.email, email_subject, email_body,
119 email_body_html)
122 email_body_html)
120
123
121 return notif
124 return notif
122
125
123 def delete(self, user, notification):
126 def delete(self, user, notification):
124 # we don't want to remove actual notification just the assignment
127 # we don't want to remove actual notification just the assignment
125 try:
128 try:
126 notification = self.__get_notification(notification)
129 notification = self.__get_notification(notification)
127 user = self._get_user(user)
130 user = self._get_user(user)
128 if notification and user:
131 if notification and user:
129 obj = UserNotification.query()\
132 obj = UserNotification.query()\
130 .filter(UserNotification.user == user)\
133 .filter(UserNotification.user == user)\
131 .filter(UserNotification.notification
134 .filter(UserNotification.notification
132 == notification)\
135 == notification)\
133 .one()
136 .one()
134 self.sa.delete(obj)
137 Session().delete(obj)
135 return True
138 return True
136 except Exception:
139 except Exception:
137 log.error(traceback.format_exc())
140 log.error(traceback.format_exc())
138 raise
141 raise
139
142
140 def get_for_user(self, user, filter_=None):
143 def get_for_user(self, user, filter_=None):
141 """
144 """
142 Get mentions for given user, filter them if filter dict is given
145 Get mentions for given user, filter them if filter dict is given
143
146
144 :param user:
147 :param user:
145 :type user:
146 :param filter:
148 :param filter:
147 """
149 """
148 user = self._get_user(user)
150 user = self._get_user(user)
149
151
150 q = UserNotification.query()\
152 q = UserNotification.query()\
151 .filter(UserNotification.user == user)\
153 .filter(UserNotification.user == user)\
152 .join((Notification, UserNotification.notification_id ==
154 .join((Notification, UserNotification.notification_id ==
153 Notification.notification_id))
155 Notification.notification_id))
154
156
155 if filter_:
157 if filter_:
156 q = q.filter(Notification.type_.in_(filter_))
158 q = q.filter(Notification.type_.in_(filter_))
157
159
158 return q.all()
160 return q.all()
159
161
160 def mark_read(self, user, notification):
162 def mark_read(self, user, notification):
161 try:
163 try:
162 notification = self.__get_notification(notification)
164 notification = self.__get_notification(notification)
163 user = self._get_user(user)
165 user = self._get_user(user)
164 if notification and user:
166 if notification and user:
165 obj = UserNotification.query()\
167 obj = UserNotification.query()\
166 .filter(UserNotification.user == user)\
168 .filter(UserNotification.user == user)\
167 .filter(UserNotification.notification
169 .filter(UserNotification.notification
168 == notification)\
170 == notification)\
169 .one()
171 .one()
170 obj.read = True
172 obj.read = True
171 self.sa.add(obj)
173 Session().add(obj)
172 return True
174 return True
173 except Exception:
175 except Exception:
174 log.error(traceback.format_exc())
176 log.error(traceback.format_exc())
175 raise
177 raise
176
178
177 def mark_all_read_for_user(self, user, filter_=None):
179 def mark_all_read_for_user(self, user, filter_=None):
178 user = self._get_user(user)
180 user = self._get_user(user)
179 q = UserNotification.query()\
181 q = UserNotification.query()\
180 .filter(UserNotification.user == user)\
182 .filter(UserNotification.user == user)\
181 .filter(UserNotification.read == False)\
183 .filter(UserNotification.read == False)\
182 .join((Notification, UserNotification.notification_id ==
184 .join((Notification, UserNotification.notification_id ==
183 Notification.notification_id))
185 Notification.notification_id))
184 if filter_:
186 if filter_:
185 q = q.filter(Notification.type_.in_(filter_))
187 q = q.filter(Notification.type_.in_(filter_))
186
188
187 # this is a little inefficient but sqlalchemy doesn't support
189 # this is a little inefficient but sqlalchemy doesn't support
188 # update on joined tables :(
190 # update on joined tables :(
189 for obj in q.all():
191 for obj in q.all():
190 obj.read = True
192 obj.read = True
191 self.sa.add(obj)
193 Session().add(obj)
192
194
193 def get_unread_cnt_for_user(self, user):
195 def get_unread_cnt_for_user(self, user):
194 user = self._get_user(user)
196 user = self._get_user(user)
195 return UserNotification.query()\
197 return UserNotification.query()\
196 .filter(UserNotification.read == False)\
198 .filter(UserNotification.read == False)\
197 .filter(UserNotification.user == user).count()
199 .filter(UserNotification.user == user).count()
198
200
199 def get_unread_for_user(self, user):
201 def get_unread_for_user(self, user):
200 user = self._get_user(user)
202 user = self._get_user(user)
201 return [x.notification for x in UserNotification.query()\
203 return [x.notification for x in UserNotification.query()\
202 .filter(UserNotification.read == False)\
204 .filter(UserNotification.read == False)\
203 .filter(UserNotification.user == user).all()]
205 .filter(UserNotification.user == user).all()]
204
206
205 def get_user_notification(self, user, notification):
207 def get_user_notification(self, user, notification):
206 user = self._get_user(user)
208 user = self._get_user(user)
207 notification = self.__get_notification(notification)
209 notification = self.__get_notification(notification)
208
210
209 return UserNotification.query()\
211 return UserNotification.query()\
210 .filter(UserNotification.notification == notification)\
212 .filter(UserNotification.notification == notification)\
211 .filter(UserNotification.user == user).scalar()
213 .filter(UserNotification.user == user).scalar()
212
214
213 def make_description(self, notification, show_age=True):
215 def make_description(self, notification, show_age=True):
214 """
216 """
215 Creates a human readable description based on properties
217 Creates a human readable description based on properties
216 of notification object
218 of notification object
217 """
219 """
218 #alias
220 #alias
219 _n = notification
221 _n = notification
220 _map = {
222 _map = {
221 _n.TYPE_CHANGESET_COMMENT: _('commented on commit at %(when)s'),
223 _n.TYPE_CHANGESET_COMMENT: _('commented on changeset at %(when)s'),
222 _n.TYPE_MESSAGE: _('sent message at %(when)s'),
224 _n.TYPE_MESSAGE: _('sent message at %(when)s'),
223 _n.TYPE_MENTION: _('mentioned you at %(when)s'),
225 _n.TYPE_MENTION: _('mentioned you at %(when)s'),
224 _n.TYPE_REGISTRATION: _('registered in RhodeCode at %(when)s'),
226 _n.TYPE_REGISTRATION: _('registered in RhodeCode at %(when)s'),
225 _n.TYPE_PULL_REQUEST: _('opened new pull request at %(when)s'),
227 _n.TYPE_PULL_REQUEST: _('opened new pull request at %(when)s'),
226 _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request at %(when)s')
228 _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request at %(when)s')
227 }
229 }
228
230
229 # action == _map string
231 # action == _map string
230 tmpl = "%(user)s %(action)s "
232 tmpl = "%(user)s %(action)s "
231 if show_age:
233 if show_age:
232 when = h.age(notification.created_on)
234 when = h.age(notification.created_on)
233 else:
235 else:
234 when = h.fmt_date(notification.created_on)
236 when = h.fmt_date(notification.created_on)
235
237
236 data = dict(
238 data = dict(
237 user=notification.created_by_user.username,
239 user=notification.created_by_user.username,
238 action=_map[notification.type_] % {'when': when},
240 action=_map[notification.type_] % {'when': when},
239 )
241 )
240 return tmpl % data
242 return tmpl % data
241
243
242
244
243 class EmailNotificationModel(BaseModel):
245 class EmailNotificationModel(BaseModel):
244
246
245 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
247 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
246 TYPE_PASSWORD_RESET = 'password_link'
248 TYPE_PASSWORD_RESET = 'password_link'
247 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
249 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
248 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
250 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
249 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
251 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
250 TYPE_DEFAULT = 'default'
252 TYPE_DEFAULT = 'default'
251
253
252 def __init__(self):
254 def __init__(self):
253 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
255 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
254 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
256 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
255
257
256 self.email_types = {
258 self.email_types = {
257 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
259 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
258 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
260 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
259 self.TYPE_REGISTRATION: 'email_templates/registration.html',
261 self.TYPE_REGISTRATION: 'email_templates/registration.html',
260 self.TYPE_DEFAULT: 'email_templates/default.html',
262 self.TYPE_DEFAULT: 'email_templates/default.html',
261 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
263 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
262 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
264 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
263 }
265 }
264
266
265 def get_email_tmpl(self, type_, **kwargs):
267 def get_email_tmpl(self, type_, **kwargs):
266 """
268 """
267 return generated template for email based on given type
269 return generated template for email based on given type
268
270
269 :param type_:
271 :param type_:
270 """
272 """
271
273
272 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
274 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
273 email_template = self._tmpl_lookup.get_template(base)
275 email_template = self._tmpl_lookup.get_template(base)
274 # translator and helpers inject
276 # translator and helpers inject
275 _kwargs = {'_': _,
277 _kwargs = {'_': _,
276 'h': h,
278 'h': h,
277 'c': c}
279 'c': c}
278 _kwargs.update(kwargs)
280 _kwargs.update(kwargs)
279 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
281 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
280 return email_template.render(**_kwargs)
282 return email_template.render(**_kwargs)
@@ -1,258 +1,259 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.pull_request
3 rhodecode.model.pull_request
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull request model for RhodeCode
6 pull request model for RhodeCode
7
7
8 :created_on: Jun 6, 2012
8 :created_on: Jun 6, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import datetime
27 import datetime
28 import re
28 import re
29
29
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33 from rhodecode.lib import helpers as h, unionrepo
33 from rhodecode.lib import helpers as h, unionrepo
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\
35 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\
36 ChangesetStatus
36 ChangesetStatus
37 from rhodecode.model.notification import NotificationModel
37 from rhodecode.model.notification import NotificationModel
38 from rhodecode.lib.utils2 import safe_unicode
38 from rhodecode.lib.utils2 import safe_unicode
39
39
40 from rhodecode.lib.vcs.utils.hgcompat import scmutil
40 from rhodecode.lib.vcs.utils.hgcompat import scmutil
41 from rhodecode.lib.vcs.utils import safe_str
41 from rhodecode.lib.vcs.utils import safe_str
42 from rhodecode.lib.vcs.backends.base import EmptyChangeset
42 from rhodecode.lib.vcs.backends.base import EmptyChangeset
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class PullRequestModel(BaseModel):
47 class PullRequestModel(BaseModel):
48
48
49 cls = PullRequest
49 cls = PullRequest
50
50
51 def __get_pull_request(self, pull_request):
51 def __get_pull_request(self, pull_request):
52 return self._get_instance(PullRequest, pull_request)
52 return self._get_instance(PullRequest, pull_request)
53
53
54 def get_all(self, repo):
54 def get_all(self, repo):
55 repo = self._get_repo(repo)
55 repo = self._get_repo(repo)
56 return PullRequest.query()\
56 return PullRequest.query()\
57 .filter(PullRequest.other_repo == repo)\
57 .filter(PullRequest.other_repo == repo)\
58 .order_by(PullRequest.created_on.desc())\
58 .order_by(PullRequest.created_on.desc())\
59 .all()
59 .all()
60
60
61 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
61 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
62 revisions, reviewers, title, description=None):
62 revisions, reviewers, title, description=None):
63 from rhodecode.model.changeset_status import ChangesetStatusModel
63 from rhodecode.model.changeset_status import ChangesetStatusModel
64
64
65 created_by_user = self._get_user(created_by)
65 created_by_user = self._get_user(created_by)
66 org_repo = self._get_repo(org_repo)
66 org_repo = self._get_repo(org_repo)
67 other_repo = self._get_repo(other_repo)
67 other_repo = self._get_repo(other_repo)
68
68
69 new = PullRequest()
69 new = PullRequest()
70 new.org_repo = org_repo
70 new.org_repo = org_repo
71 new.org_ref = org_ref
71 new.org_ref = org_ref
72 new.other_repo = other_repo
72 new.other_repo = other_repo
73 new.other_ref = other_ref
73 new.other_ref = other_ref
74 new.revisions = revisions
74 new.revisions = revisions
75 new.title = title
75 new.title = title
76 new.description = description
76 new.description = description
77 new.author = created_by_user
77 new.author = created_by_user
78 self.sa.add(new)
78 Session().add(new)
79 Session().flush()
79 Session().flush()
80 #members
80 #members
81 for member in set(reviewers):
81 for member in set(reviewers):
82 _usr = self._get_user(member)
82 _usr = self._get_user(member)
83 reviewer = PullRequestReviewers(_usr, new)
83 reviewer = PullRequestReviewers(_usr, new)
84 self.sa.add(reviewer)
84 Session().add(reviewer)
85
85
86 #reset state to under-review
86 #reset state to under-review
87 ChangesetStatusModel().set_status(
87 ChangesetStatusModel().set_status(
88 repo=org_repo,
88 repo=org_repo,
89 status=ChangesetStatus.STATUS_UNDER_REVIEW,
89 status=ChangesetStatus.STATUS_UNDER_REVIEW,
90 user=created_by_user,
90 user=created_by_user,
91 pull_request=new
91 pull_request=new
92 )
92 )
93
93 revision_data = [(x.raw_id, x.message)
94 for x in map(org_repo.get_changeset, revisions)]
94 #notification to reviewers
95 #notification to reviewers
95 notif = NotificationModel()
96 notif = NotificationModel()
96
97
97 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
98 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
98 pull_request_id=new.pull_request_id,
99 pull_request_id=new.pull_request_id,
99 qualified=True,
100 qualified=True,
100 )
101 )
101 subject = safe_unicode(
102 subject = safe_unicode(
102 h.link_to(
103 h.link_to(
103 _('%(user)s wants you to review pull request #%(pr_id)s: %(pr_title)s') % \
104 _('%(user)s wants you to review pull request #%(pr_id)s: %(pr_title)s') % \
104 {'user': created_by_user.username,
105 {'user': created_by_user.username,
105 'pr_title': new.title,
106 'pr_title': new.title,
106 'pr_id': new.pull_request_id},
107 'pr_id': new.pull_request_id},
107 pr_url
108 pr_url
108 )
109 )
109 )
110 )
110 body = description
111 body = description
111 kwargs = {
112 kwargs = {
112 'pr_title': title,
113 'pr_title': title,
113 'pr_user_created': h.person(created_by_user.email),
114 'pr_user_created': h.person(created_by_user.email),
114 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
115 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
115 qualified=True,),
116 qualified=True,),
116 'pr_url': pr_url,
117 'pr_url': pr_url,
117 'pr_revisions': revisions
118 'pr_revisions': revision_data
118 }
119 }
119
120
120 notif.create(created_by=created_by_user, subject=subject, body=body,
121 notif.create(created_by=created_by_user, subject=subject, body=body,
121 recipients=reviewers,
122 recipients=reviewers,
122 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
123 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
123 return new
124 return new
124
125
125 def update_reviewers(self, pull_request, reviewers_ids):
126 def update_reviewers(self, pull_request, reviewers_ids):
126 reviewers_ids = set(reviewers_ids)
127 reviewers_ids = set(reviewers_ids)
127 pull_request = self.__get_pull_request(pull_request)
128 pull_request = self.__get_pull_request(pull_request)
128 current_reviewers = PullRequestReviewers.query()\
129 current_reviewers = PullRequestReviewers.query()\
129 .filter(PullRequestReviewers.pull_request==
130 .filter(PullRequestReviewers.pull_request==
130 pull_request)\
131 pull_request)\
131 .all()
132 .all()
132 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
133 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
133
134
134 to_add = reviewers_ids.difference(current_reviewers_ids)
135 to_add = reviewers_ids.difference(current_reviewers_ids)
135 to_remove = current_reviewers_ids.difference(reviewers_ids)
136 to_remove = current_reviewers_ids.difference(reviewers_ids)
136
137
137 log.debug("Adding %s reviewers" % to_add)
138 log.debug("Adding %s reviewers" % to_add)
138 log.debug("Removing %s reviewers" % to_remove)
139 log.debug("Removing %s reviewers" % to_remove)
139
140
140 for uid in to_add:
141 for uid in to_add:
141 _usr = self._get_user(uid)
142 _usr = self._get_user(uid)
142 reviewer = PullRequestReviewers(_usr, pull_request)
143 reviewer = PullRequestReviewers(_usr, pull_request)
143 self.sa.add(reviewer)
144 Session().add(reviewer)
144
145
145 for uid in to_remove:
146 for uid in to_remove:
146 reviewer = PullRequestReviewers.query()\
147 reviewer = PullRequestReviewers.query()\
147 .filter(PullRequestReviewers.user_id==uid,
148 .filter(PullRequestReviewers.user_id==uid,
148 PullRequestReviewers.pull_request==pull_request)\
149 PullRequestReviewers.pull_request==pull_request)\
149 .scalar()
150 .scalar()
150 if reviewer:
151 if reviewer:
151 self.sa.delete(reviewer)
152 Session().delete(reviewer)
152
153
153 def delete(self, pull_request):
154 def delete(self, pull_request):
154 pull_request = self.__get_pull_request(pull_request)
155 pull_request = self.__get_pull_request(pull_request)
155 Session().delete(pull_request)
156 Session().delete(pull_request)
156
157
157 def close_pull_request(self, pull_request):
158 def close_pull_request(self, pull_request):
158 pull_request = self.__get_pull_request(pull_request)
159 pull_request = self.__get_pull_request(pull_request)
159 pull_request.status = PullRequest.STATUS_CLOSED
160 pull_request.status = PullRequest.STATUS_CLOSED
160 pull_request.updated_on = datetime.datetime.now()
161 pull_request.updated_on = datetime.datetime.now()
161 self.sa.add(pull_request)
162 Session().add(pull_request)
162
163
163 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref):
164 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref):
164 """
165 """
165 Returns a list of changesets that can be merged from org_repo@org_ref
166 Returns a list of changesets that can be merged from org_repo@org_ref
166 to other_repo@other_ref ... and the ancestor that would be used for merge
167 to other_repo@other_ref ... and the ancestor that would be used for merge
167
168
168 :param org_repo:
169 :param org_repo:
169 :param org_ref:
170 :param org_ref:
170 :param other_repo:
171 :param other_repo:
171 :param other_ref:
172 :param other_ref:
172 :param tmp:
173 :param tmp:
173 """
174 """
174
175
175 ancestor = None
176 ancestor = None
176
177
177 if alias == 'hg':
178 if alias == 'hg':
178 # lookup up the exact node id
179 # lookup up the exact node id
179 _revset_predicates = {
180 _revset_predicates = {
180 'branch': 'branch',
181 'branch': 'branch',
181 'book': 'bookmark',
182 'book': 'bookmark',
182 'tag': 'tag',
183 'tag': 'tag',
183 'rev': 'id',
184 'rev': 'id',
184 }
185 }
185
186
186 org_rev_spec = "%s('%s')" % (_revset_predicates[org_ref[0]],
187 org_rev_spec = "%s('%s')" % (_revset_predicates[org_ref[0]],
187 safe_str(org_ref[1]))
188 safe_str(org_ref[1]))
188 if org_ref[1] == EmptyChangeset().raw_id:
189 if org_ref[1] == EmptyChangeset().raw_id:
189 org_rev = org_ref[1]
190 org_rev = org_ref[1]
190 else:
191 else:
191 org_rev = org_repo._repo[scmutil.revrange(org_repo._repo,
192 org_rev = org_repo._repo[scmutil.revrange(org_repo._repo,
192 [org_rev_spec])[-1]]
193 [org_rev_spec])[-1]]
193 other_rev_spec = "%s('%s')" % (_revset_predicates[other_ref[0]],
194 other_rev_spec = "%s('%s')" % (_revset_predicates[other_ref[0]],
194 safe_str(other_ref[1]))
195 safe_str(other_ref[1]))
195 if other_ref[1] == EmptyChangeset().raw_id:
196 if other_ref[1] == EmptyChangeset().raw_id:
196 other_rev = other_ref[1]
197 other_rev = other_ref[1]
197 else:
198 else:
198 other_rev = other_repo._repo[scmutil.revrange(other_repo._repo,
199 other_rev = other_repo._repo[scmutil.revrange(other_repo._repo,
199 [other_rev_spec])[-1]]
200 [other_rev_spec])[-1]]
200
201
201 #case two independent repos
202 #case two independent repos
202 if org_repo != other_repo:
203 if org_repo != other_repo:
203 hgrepo = unionrepo.unionrepository(other_repo.baseui,
204 hgrepo = unionrepo.unionrepository(other_repo.baseui,
204 other_repo.path,
205 other_repo.path,
205 org_repo.path)
206 org_repo.path)
206 # all the changesets we are looking for will be in other_repo,
207 # all the changesets we are looking for will be in other_repo,
207 # so rev numbers from hgrepo can be used in other_repo
208 # so rev numbers from hgrepo can be used in other_repo
208
209
209 #no remote compare do it on the same repository
210 #no remote compare do it on the same repository
210 else:
211 else:
211 hgrepo = other_repo._repo
212 hgrepo = other_repo._repo
212
213
213 revs = ["ancestors(id('%s')) and not ancestors(id('%s'))" %
214 revs = ["ancestors(id('%s')) and not ancestors(id('%s'))" %
214 (other_rev, org_rev)]
215 (other_rev, org_rev)]
215 changesets = [other_repo.get_changeset(cs)
216 changesets = [other_repo.get_changeset(cs)
216 for cs in scmutil.revrange(hgrepo, revs)]
217 for cs in scmutil.revrange(hgrepo, revs)]
217
218
218 if org_repo != other_repo:
219 if org_repo != other_repo:
219 ancestors = scmutil.revrange(hgrepo,
220 ancestors = scmutil.revrange(hgrepo,
220 ["ancestor(id('%s'), id('%s'))" % (org_rev, other_rev)])
221 ["ancestor(id('%s'), id('%s'))" % (org_rev, other_rev)])
221 if len(ancestors) == 1:
222 if len(ancestors) == 1:
222 ancestor = hgrepo[ancestors[0]].hex()
223 ancestor = hgrepo[ancestors[0]].hex()
223
224
224 elif alias == 'git':
225 elif alias == 'git':
225 assert org_repo == other_repo, (org_repo, other_repo) # no git support for different repos
226 assert org_repo == other_repo, (org_repo, other_repo) # no git support for different repos
226 so, se = org_repo.run_git_command(
227 so, se = org_repo.run_git_command(
227 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
228 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
228 other_ref[1])
229 other_ref[1])
229 )
230 )
230 changesets = [org_repo.get_changeset(cs)
231 changesets = [org_repo.get_changeset(cs)
231 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
232 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
232
233
233 return changesets, ancestor
234 return changesets, ancestor
234
235
235 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
236 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
236 """
237 """
237 Returns incoming changesets for mercurial repositories
238 Returns incoming changesets for mercurial repositories
238
239
239 :param org_repo:
240 :param org_repo:
240 :param org_ref:
241 :param org_ref:
241 :param other_repo:
242 :param other_repo:
242 :param other_ref:
243 :param other_ref:
243 """
244 """
244
245
245 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
246 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
246 raise Exception('org_ref must be a two element list/tuple')
247 raise Exception('org_ref must be a two element list/tuple')
247
248
248 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
249 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
249 raise Exception('other_ref must be a two element list/tuple')
250 raise Exception('other_ref must be a two element list/tuple')
250
251
251 org_repo_scm = org_repo.scm_instance
252 org_repo_scm = org_repo.scm_instance
252 other_repo_scm = other_repo.scm_instance
253 other_repo_scm = other_repo.scm_instance
253
254
254 alias = org_repo.scm_instance.alias
255 alias = org_repo.scm_instance.alias
255 cs_ranges, ancestor = self._get_changesets(alias,
256 cs_ranges, ancestor = self._get_changesets(alias,
256 org_repo_scm, org_ref,
257 org_repo_scm, org_ref,
257 other_repo_scm, other_ref)
258 other_repo_scm, other_ref)
258 return cs_ranges, ancestor
259 return cs_ranges, ancestor
@@ -1,12 +1,17 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="main.html"/>
2 <%inherit file="main.html"/>
3
3 ##message from user goes here
4 <h4>${subject}</h4>
4 <p>
5
5 ${cs_comment_user}: <br/>
6 ${body}
6 ${body}
7 </p>
8 %if status_change:
9 <span>${_('New status')} -&gt; ${status_change}</span>
10 %endif
11 <div>${_('View this comment here')}: ${cs_comment_url}</div>
7
12
8 % if status_change is not None:
13 <pre>
9 <div>
14 ${_('Repo')}: ${cs_target_repo}
10 ${_('New status')} -&gt; ${status_change}
15 ${_('Changeset')}: ${h.short_id(raw_id)}
11 </div>
16 ${_('desc')}: ${h.shorter(message, 256)}
12 % endif
17 </pre>
@@ -1,17 +1,19 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="main.html"/>
2 <%inherit file="main.html"/>
3
3
4 ${_('User %s opened pull request for repository %s and wants you to review changes.') % (('<b>%s</b>' % pr_user_created),pr_repo_url) |n}
4 ${_('User %s opened pull request for repository %s and wants you to review changes.') % (('<b>%s</b>' % pr_user_created),pr_repo_url) |n}
5 <div>${_('View this pull request here')}: ${pr_url}</div>
5 <div>${_('View this pull request here')}: ${pr_url}</div>
6 <div>${_('title')}: ${pr_title}</div>
6 <div>${_('title')}: ${pr_title}</div>
7 <div>${_('description')}:</div>
7 <div>${_('description')}:</div>
8 <p>
8 <p>
9 ${body}
9 ${body}
10 </p>
10 </p>
11
11
12 <div>${_('revisions for reviewing')}</div>
12 <div>${_('revisions for reviewing')}</div>
13 <ul>
13 <pre>
14 %for r in pr_revisions:
14 %for r,r_msg in pr_revisions:
15 <li>${r}</li>
15 ${h.short_id(r)}:
16 ${h.shorter(r_msg, 256)}
17
16 %endfor
18 %endfor
17 </ul>
19 </pre>
@@ -1,13 +1,18 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="main.html"/>
2 <%inherit file="main.html"/>
3
3 ${_('Pull request #%s for repository %s') % (pr_id, pr_target_repo) |n}
4 ${_('User %s commented on pull request #%s for repository %s') % ('<b>%s</b>' % pr_comment_user, pr_id, pr_target_repo) |n}
4 ##message from user goes here
5 <p>
6 ${pr_comment_user}: <br/>
7 ${body}
8 </p>
5 <div>${_('View this comment here')}: ${pr_comment_url}</div>
9 <div>${_('View this comment here')}: ${pr_comment_url}</div>
6
10
7 <p>
8 ${body}
9
10 %if status_change:
11 %if status_change:
12 %if closing_pr:
13 <span>${_('Closing pull request with status')} -&gt; ${status_change}</span>
14 %else:
11 <span>${_('New status')} -&gt; ${status_change}</span>
15 <span>${_('New status')} -&gt; ${status_change}</span>
12 %endif
16 %endif
17 %endif
13 </p>
18 </p>
@@ -1,201 +1,201 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(_(u'Home'),h.url('/'))}
8 ${h.link_to(_(u'Home'),h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
10 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
11 &raquo;
11 &raquo;
12 ${_('new pull request')}
12 ${_('new pull request')}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16
16
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22 ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
22 ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
23 <div style="float:left;padding:0px 30px 30px 30px">
23 <div style="float:left;padding:0px 30px 30px 30px">
24 <input type="hidden" name="rev_start" value="${request.GET.get('rev_start')}" />
24 <input type="hidden" name="rev_start" value="${request.GET.get('rev_start')}" />
25 <input type="hidden" name="rev_end" value="${request.GET.get('rev_end')}" />
25 <input type="hidden" name="rev_end" value="${request.GET.get('rev_end')}" />
26
26
27 ##ORG
27 ##ORG
28 <div style="float:left">
28 <div style="float:left">
29 <div>
29 <div>
30 <span style="font-size: 20px">
30 <span style="font-size: 20px">
31 ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref',c.default_org_ref,c.org_refs,class_='refs')}
31 ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref',c.default_org_ref,c.org_refs,class_='refs')}
32 </span>
32 </span>
33 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
33 <div style="padding:5px 3px 3px 20px;">${c.rhodecode_db_repo.description}</div>
34 </div>
34 </div>
35 <div style="clear:both;padding-top: 10px"></div>
35 <div style="clear:both;padding-top: 10px"></div>
36 </div>
36 </div>
37 <div style="float:left;font-size:24px;padding:0px 20px">
37 <div style="float:left;font-size:24px;padding:0px 20px">
38 <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
38 <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
39 </div>
39 </div>
40
40
41 ##OTHER, most Probably the PARENT OF THIS FORK
41 ##OTHER, most Probably the PARENT OF THIS FORK
42 <div style="float:left">
42 <div style="float:left">
43 <div>
43 <div>
44 <span style="font-size: 20px">
44 <span style="font-size: 20px">
45 ${h.select('other_repo',c.default_other_repo,c.other_repos,class_='refs')}:${h.select('other_ref',c.default_other_ref,c.default_other_refs,class_='refs')}
45 ${h.select('other_repo',c.default_other_repo,c.other_repos,class_='refs')}:${h.select('other_ref',c.default_other_ref,c.default_other_refs,class_='refs')}
46 </span>
46 </span>
47 <div id="other_repo_desc" style="padding:5px 3px 3px 42px;"></div>
47 <div id="other_repo_desc" style="padding:5px 3px 3px 20px;"></div>
48 </div>
48 </div>
49 <div style="clear:both;padding-top: 10px"></div>
49 <div style="clear:both;padding-top: 10px"></div>
50 </div>
50 </div>
51 <div style="clear:both;padding-top: 10px"></div>
51 <div style="clear:both;padding-top: 10px"></div>
52 ## overview pulled by ajax
52 ## overview pulled by ajax
53 <div style="float:left" id="pull_request_overview"></div>
53 <div style="float:left" id="pull_request_overview"></div>
54 <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
54 <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
55 <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
55 <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
56 </div>
56 </div>
57 </div>
57 </div>
58 <div style="float:left; border-left:1px dashed #eee">
58 <div style="float:left; border-left:1px dashed #eee">
59 <h4>${_('Pull request reviewers')}</h4>
59 <h4>${_('Pull request reviewers')}</h4>
60 <div id="reviewers" style="padding:0px 0px 0px 15px">
60 <div id="reviewers" style="padding:0px 0px 0px 15px">
61 ## members goes here !
61 ## members goes here !
62 <div class="group_members_wrap">
62 <div class="group_members_wrap">
63 <ul id="review_members" class="group_members">
63 <ul id="review_members" class="group_members">
64 %for member in c.review_members:
64 %for member in c.review_members:
65 <li id="reviewer_${member.user_id}">
65 <li id="reviewer_${member.user_id}">
66 <div class="reviewers_member">
66 <div class="reviewers_member">
67 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
67 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
68 <div style="float:left">${member.full_name} (${_('owner')})</div>
68 <div style="float:left">${member.full_name} (${_('owner')})</div>
69 <input type="hidden" value="${member.user_id}" name="review_members" />
69 <input type="hidden" value="${member.user_id}" name="review_members" />
70 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
70 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
71 </div>
71 </div>
72 </li>
72 </li>
73 %endfor
73 %endfor
74 </ul>
74 </ul>
75 </div>
75 </div>
76
76
77 <div class='ac'>
77 <div class='ac'>
78 <div class="reviewer_ac">
78 <div class="reviewer_ac">
79 ${h.text('user', class_='yui-ac-input')}
79 ${h.text('user', class_='yui-ac-input')}
80 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
80 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
81 <div id="reviewers_container"></div>
81 <div id="reviewers_container"></div>
82 </div>
82 </div>
83 </div>
83 </div>
84 </div>
84 </div>
85 </div>
85 </div>
86 <h3>${_('Create new pull request')}</h3>
86 <h3>${_('Create new pull request')}</h3>
87
87
88 <div class="form">
88 <div class="form">
89 <!-- fields -->
89 <!-- fields -->
90
90
91 <div class="fields">
91 <div class="fields">
92
92
93 <div class="field">
93 <div class="field">
94 <div class="label">
94 <div class="label">
95 <label for="pullrequest_title">${_('Title')}:</label>
95 <label for="pullrequest_title">${_('Title')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 ${h.text('pullrequest_title',size=30)}
98 ${h.text('pullrequest_title',size=30)}
99 </div>
99 </div>
100 </div>
100 </div>
101
101
102 <div class="field">
102 <div class="field">
103 <div class="label label-textarea">
103 <div class="label label-textarea">
104 <label for="pullrequest_desc">${_('description')}:</label>
104 <label for="pullrequest_desc">${_('description')}:</label>
105 </div>
105 </div>
106 <div class="textarea text-area editor">
106 <div class="textarea text-area editor">
107 ${h.textarea('pullrequest_desc',size=30)}
107 ${h.textarea('pullrequest_desc',size=30)}
108 </div>
108 </div>
109 </div>
109 </div>
110
110
111 <div class="buttons">
111 <div class="buttons">
112 ${h.submit('save',_('Send pull request'),class_="ui-btn large")}
112 ${h.submit('save',_('Send pull request'),class_="ui-btn large")}
113 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
113 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
114 </div>
114 </div>
115 </div>
115 </div>
116 </div>
116 </div>
117 ${h.end_form()}
117 ${h.end_form()}
118
118
119 </div>
119 </div>
120
120
121 <script type="text/javascript">
121 <script type="text/javascript">
122 var _USERS_AC_DATA = ${c.users_array|n};
122 var _USERS_AC_DATA = ${c.users_array|n};
123 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
123 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
124 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
124 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
125
125
126 var other_repos_info = ${c.other_repos_info|n};
126 var other_repos_info = ${c.other_repos_info|n};
127
127
128 var loadPreview = function(){
128 var loadPreview = function(){
129 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','none');
129 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','none');
130 //url template
130 //url template
131 var url = "${h.url('compare_url',
131 var url = "${h.url('compare_url',
132 repo_name='__other_repo__',
132 repo_name='__other_repo__',
133 org_ref_type='__other_ref_type__',
133 org_ref_type='__other_ref_type__',
134 org_ref='__other_ref__',
134 org_ref='__other_ref__',
135 other_repo='__org_repo__',
135 other_repo='__org_repo__',
136 other_ref_type='__org_ref_type__',
136 other_ref_type='__org_ref_type__',
137 other_ref='__org_ref__',
137 other_ref='__org_ref__',
138 as_form=True,
138 as_form=True,
139 rev_start=request.GET.get('rev_start',''),
139 rev_start=request.GET.get('rev_start',''),
140 rev_end=request.GET.get('rev_end',''))}";
140 rev_end=request.GET.get('rev_end',''))}";
141 var org_repo = YUQ('#pull_request_form #org_repo')[0].value;
141 var org_repo = YUQ('#pull_request_form #org_repo')[0].value;
142 var org_ref = YUQ('#pull_request_form #org_ref')[0].value.split(':');
142 var org_ref = YUQ('#pull_request_form #org_ref')[0].value.split(':');
143
143
144 var other_repo = YUQ('#pull_request_form #other_repo')[0].value;
144 var other_repo = YUQ('#pull_request_form #other_repo')[0].value;
145 var other_ref = YUQ('#pull_request_form #other_ref')[0].value.split(':');
145 var other_ref = YUQ('#pull_request_form #other_ref')[0].value.split(':');
146
146
147 var select_refs = YUQ('#pull_request_form select.refs')
147 var select_refs = YUQ('#pull_request_form select.refs')
148 var rev_data = {
148 var rev_data = {
149 'org_repo': org_repo,
149 'org_repo': org_repo,
150 'org_ref': org_ref[1],
150 'org_ref': org_ref[1],
151 'org_ref_type': org_ref[0],
151 'org_ref_type': org_ref[0],
152 'other_repo': other_repo,
152 'other_repo': other_repo,
153 'other_ref': other_ref[1],
153 'other_ref': other_ref[1],
154 'other_ref_type': other_ref[0],
154 'other_ref_type': other_ref[0],
155 }; // gather the org/other ref and repo here
155 }; // gather the org/other ref and repo here
156
156
157 for (k in rev_data){
157 for (k in rev_data){
158 url = url.replace('__'+k+'__',rev_data[k]);
158 url = url.replace('__'+k+'__',rev_data[k]);
159 }
159 }
160
160
161 ypjax(url,'pull_request_overview', function(data){
161 ypjax(url,'pull_request_overview', function(data){
162 var sel_box = YUQ('#pull_request_form #other_repo')[0];
162 var sel_box = YUQ('#pull_request_form #other_repo')[0];
163 var repo_name = sel_box.options[sel_box.selectedIndex].value;
163 var repo_name = sel_box.options[sel_box.selectedIndex].value;
164 var _data = other_repos_info[repo_name];
164 var _data = other_repos_info[repo_name];
165 YUD.get('pull_request_overview_url').href = url;
165 YUD.get('pull_request_overview_url').href = url;
166 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','');
166 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','');
167 YUD.get('other_repo_desc').innerHTML = other_repos_info[repo_name]['description'];
167 YUD.get('other_repo_desc').innerHTML = other_repos_info[repo_name]['description'];
168 YUD.get('other_ref').innerHTML = other_repos_info[repo_name]['revs'];
168 YUD.get('other_ref').innerHTML = other_repos_info[repo_name]['revs'];
169 // select back the revision that was just compared
169 // select back the revision that was just compared
170 setSelectValue(YUD.get('other_ref'), rev_data['other_ref']);
170 setSelectValue(YUD.get('other_ref'), rev_data['other_ref']);
171 // reset && add the reviewer based on selected repo
171 // reset && add the reviewer based on selected repo
172 YUD.get('review_members').innerHTML = '';
172 YUD.get('review_members').innerHTML = '';
173 addReviewMember(_data.user.user_id, _data.user.firstname,
173 addReviewMember(_data.user.user_id, _data.user.firstname,
174 _data.user.lastname, _data.user.username,
174 _data.user.lastname, _data.user.username,
175 _data.user.gravatar_link);
175 _data.user.gravatar_link);
176 })
176 })
177 }
177 }
178
178
179 ## refresh automatically when something changes (org_repo can't change)
179 ## refresh automatically when something changes (org_repo can't change)
180
180
181 YUE.on('org_ref', 'change', function(e){
181 YUE.on('org_ref', 'change', function(e){
182 loadPreview();
182 loadPreview();
183 });
183 });
184
184
185 YUE.on('other_repo', 'change', function(e){
185 YUE.on('other_repo', 'change', function(e){
186 var repo_name = e.currentTarget.value;
186 var repo_name = e.currentTarget.value;
187 // replace the <select> of changed repo
187 // replace the <select> of changed repo
188 YUD.get('other_ref').innerHTML = other_repos_info[repo_name]['revs'];
188 YUD.get('other_ref').innerHTML = other_repos_info[repo_name]['revs'];
189 loadPreview();
189 loadPreview();
190 });
190 });
191
191
192 YUE.on('other_ref', 'change', function(e){
192 YUE.on('other_ref', 'change', function(e){
193 loadPreview();
193 loadPreview();
194 });
194 });
195
195
196 //lazy load overview after 0.5s
196 //lazy load overview after 0.5s
197 setTimeout(loadPreview, 500)
197 setTimeout(loadPreview, 500)
198
198
199 </script>
199 </script>
200
200
201 </%def>
201 </%def>
General Comments 0
You need to be logged in to leave comments. Login now