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