##// END OF EJS Templates
Implemented #670 Implementation of Roles in Pull Request...
marcink -
r3104:c77d5c63 beta
parent child Browse files
Show More
@@ -1,477 +1,487 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()], _("Tags"))
79 79
80 80 hist_l.append(bookmarks_group)
81 81 hist_l.append(branches_group)
82 82 hist_l.append(tags_group)
83 83
84 84 return hist_l
85 85
86 86 def _get_default_rev(self, repo):
87 87 """
88 88 Get's default revision to do compare on pull request
89 89
90 90 :param repo:
91 91 """
92 92 repo = repo.scm_instance
93 93 if 'default' in repo.branches:
94 94 return 'default'
95 95 else:
96 96 #if repo doesn't have default branch return first found
97 97 return repo.branches.keys()[0]
98 98
99 def _get_is_allowed_change_status(self, pull_request):
100 owner = self.rhodecode_user.user_id == pull_request.user_id
101 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
102 pull_request.reviewers]
103 return (self.rhodecode_user.admin or owner or reviewer)
104
99 105 def show_all(self, repo_name):
100 106 c.pull_requests = PullRequestModel().get_all(repo_name)
101 107 c.repo_name = repo_name
102 108 return render('/pullrequests/pullrequest_show_all.html')
103 109
104 110 @NotAnonymous()
105 111 def index(self):
106 112 org_repo = c.rhodecode_db_repo
107 113
108 114 if org_repo.scm_instance.alias != 'hg':
109 115 log.error('Review not available for GIT REPOS')
110 116 raise HTTPNotFound
111 117
112 118 try:
113 119 org_repo.scm_instance.get_changeset()
114 120 except EmptyRepositoryError, e:
115 121 h.flash(h.literal(_('There are no changesets yet')),
116 122 category='warning')
117 123 redirect(url('summary_home', repo_name=org_repo.repo_name))
118 124
119 125 other_repos_info = {}
120 126
121 127 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
122 128 c.org_repos = []
123 129 c.other_repos = []
124 130 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
125 131 org_repo.user.username, c.repo_name))
126 132 )
127 133
128 134 # add org repo to other so we can open pull request agains itself
129 135 c.other_repos.extend(c.org_repos)
130 136
131 137 c.default_pull_request = org_repo.repo_name # repo name pre-selected
132 138 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
133 139 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
134 140 #add orginal repo
135 141 other_repos_info[org_repo.repo_name] = {
136 142 'gravatar': h.gravatar_url(org_repo.user.email, 24),
137 143 'description': org_repo.description,
138 144 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
139 145 }
140 146
141 147 #gather forks and add to this list
142 148 for fork in org_repo.forks:
143 149 c.other_repos.append((fork.repo_name, '%s/%s' % (
144 150 fork.user.username, fork.repo_name))
145 151 )
146 152 other_repos_info[fork.repo_name] = {
147 153 'gravatar': h.gravatar_url(fork.user.email, 24),
148 154 'description': fork.description,
149 155 'revs': h.select('other_ref', '',
150 156 self._get_repo_refs(fork.scm_instance),
151 157 class_='refs')
152 158 }
153 159 #add parents of this fork also, but only if it's not empty
154 160 if org_repo.parent and org_repo.parent.scm_instance.revisions:
155 161 c.default_pull_request = org_repo.parent.repo_name
156 162 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
157 163 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
158 164 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
159 165 org_repo.parent.user.username,
160 166 org_repo.parent.repo_name))
161 167 )
162 168 other_repos_info[org_repo.parent.repo_name] = {
163 169 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
164 170 'description': org_repo.parent.description,
165 171 'revs': h.select('other_ref', '',
166 172 self._get_repo_refs(org_repo.parent.scm_instance),
167 173 class_='refs')
168 174 }
169 175
170 176 c.other_repos_info = json.dumps(other_repos_info)
171 177 c.review_members = [org_repo.user]
172 178 return render('/pullrequests/pullrequest.html')
173 179
174 180 @NotAnonymous()
175 181 def create(self, repo_name):
176 182 repo = RepoModel()._get_repo(repo_name)
177 183 try:
178 184 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
179 185 except formencode.Invalid, errors:
180 186 log.error(traceback.format_exc())
181 187 if errors.error_dict.get('revisions'):
182 188 msg = 'Revisions: %s' % errors.error_dict['revisions']
183 189 elif errors.error_dict.get('pullrequest_title'):
184 190 msg = _('Pull request requires a title with min. 3 chars')
185 191 else:
186 192 msg = _('error during creation of pull request')
187 193
188 194 h.flash(msg, 'error')
189 195 return redirect(url('pullrequest_home', repo_name=repo_name))
190 196
191 197 org_repo = _form['org_repo']
192 198 org_ref = _form['org_ref']
193 199 other_repo = _form['other_repo']
194 200 other_ref = _form['other_ref']
195 201 revisions = _form['revisions']
196 202 reviewers = _form['review_members']
197 203
198 204 # if we have cherry picked pull request we don't care what is in
199 205 # org_ref/other_ref
200 206 rev_start = request.POST.get('rev_start')
201 207 rev_end = request.POST.get('rev_end')
202 208
203 209 if rev_start and rev_end:
204 210 # this is swapped to simulate that rev_end is a revision from
205 211 # parent of the fork
206 212 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
207 213 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
208 214
209 215 title = _form['pullrequest_title']
210 216 description = _form['pullrequest_desc']
211 217
212 218 try:
213 219 pull_request = PullRequestModel().create(
214 220 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
215 221 other_ref, revisions, reviewers, title, description
216 222 )
217 223 Session().commit()
218 224 h.flash(_('Successfully opened new pull request'),
219 225 category='success')
220 226 except Exception:
221 227 h.flash(_('Error occurred during sending pull request'),
222 228 category='error')
223 229 log.error(traceback.format_exc())
224 230 return redirect(url('pullrequest_home', repo_name=repo_name))
225 231
226 232 return redirect(url('pullrequest_show', repo_name=other_repo,
227 233 pull_request_id=pull_request.pull_request_id))
228 234
229 235 @NotAnonymous()
230 236 @jsonify
231 237 def update(self, repo_name, pull_request_id):
232 238 pull_request = PullRequest.get_or_404(pull_request_id)
233 239 if pull_request.is_closed():
234 240 raise HTTPForbidden()
235 241 #only owner or admin can update it
236 242 owner = pull_request.author.user_id == c.rhodecode_user.user_id
237 243 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
238 244 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
239 245 request.POST.get('reviewers_ids', '').split(',')))
240 246
241 247 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
242 248 Session().commit()
243 249 return True
244 250 raise HTTPForbidden()
245 251
246 252 @NotAnonymous()
247 253 @jsonify
248 254 def delete(self, repo_name, pull_request_id):
249 255 pull_request = PullRequest.get_or_404(pull_request_id)
250 256 #only owner can delete it !
251 257 if pull_request.author.user_id == c.rhodecode_user.user_id:
252 258 PullRequestModel().delete(pull_request)
253 259 Session().commit()
254 260 h.flash(_('Successfully deleted pull request'),
255 261 category='success')
256 262 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
257 263 raise HTTPForbidden()
258 264
259 265 def _load_compare_data(self, pull_request, enable_comments=True):
260 266 """
261 267 Load context data needed for generating compare diff
262 268
263 269 :param pull_request:
264 270 :type pull_request:
265 271 """
266 272 rev_start = request.GET.get('rev_start')
267 273 rev_end = request.GET.get('rev_end')
268 274
269 275 org_repo = pull_request.org_repo
270 276 (org_ref_type,
271 277 org_ref_name,
272 278 org_ref_rev) = pull_request.org_ref.split(':')
273 279
274 280 other_repo = org_repo
275 281 (other_ref_type,
276 282 other_ref_name,
277 283 other_ref_rev) = pull_request.other_ref.split(':')
278 284
279 285 # despite opening revisions for bookmarks/branches/tags, we always
280 286 # convert this to rev to prevent changes after book or branch change
281 287 org_ref = ('rev', org_ref_rev)
282 288 other_ref = ('rev', other_ref_rev)
283 289
284 290 c.org_repo = org_repo
285 291 c.other_repo = other_repo
286 292
287 293 c.fulldiff = fulldiff = request.GET.get('fulldiff')
288 294
289 295 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
290 296
291 297 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
292 298 if c.cs_ranges[0].parents
293 299 else EmptyChangeset(), 'raw_id'))
294 300
295 301 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
296 302 c.target_repo = c.repo_name
297 303 # defines that we need hidden inputs with changesets
298 304 c.as_form = request.GET.get('as_form', False)
299 305
300 306 c.org_ref = org_ref[1]
301 307 c.other_ref = other_ref[1]
302 308
303 309 diff_limit = self.cut_off_limit if not fulldiff else None
304 310
305 311 #we swap org/other ref since we run a simple diff on one repo
306 312 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
307 313
308 314 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
309 315 diff_limit=diff_limit)
310 316 _parsed = diff_processor.prepare()
311 317
312 318 c.limited_diff = False
313 319 if isinstance(_parsed, LimitedDiffContainer):
314 320 c.limited_diff = True
315 321
316 322 c.files = []
317 323 c.changes = {}
318 324 c.lines_added = 0
319 325 c.lines_deleted = 0
320 326 for f in _parsed:
321 327 st = f['stats']
322 328 if st[0] != 'b':
323 329 c.lines_added += st[0]
324 330 c.lines_deleted += st[1]
325 331 fid = h.FID('', f['filename'])
326 332 c.files.append([fid, f['operation'], f['filename'], f['stats']])
327 333 diff = diff_processor.as_html(enable_comments=enable_comments,
328 334 parsed_lines=[f])
329 335 c.changes[fid] = [f['operation'], f['filename'], diff]
330 336
331 337 def show(self, repo_name, pull_request_id):
332 338 repo_model = RepoModel()
333 339 c.users_array = repo_model.get_users_js()
334 340 c.users_groups_array = repo_model.get_users_groups_js()
335 341 c.pull_request = PullRequest.get_or_404(pull_request_id)
336 342 c.target_repo = c.pull_request.org_repo.repo_name
337
343 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
338 344 cc_model = ChangesetCommentsModel()
339 345 cs_model = ChangesetStatusModel()
340 346 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
341 347 pull_request=c.pull_request,
342 348 with_revisions=True)
343 349
344 350 cs_statuses = defaultdict(list)
345 351 for st in _cs_statuses:
346 352 cs_statuses[st.author.username] += [st]
347 353
348 354 c.pull_request_reviewers = []
349 355 c.pull_request_pending_reviewers = []
350 356 for o in c.pull_request.reviewers:
351 357 st = cs_statuses.get(o.user.username, None)
352 358 if st:
353 359 sorter = lambda k: k.version
354 360 st = [(x, list(y)[0])
355 361 for x, y in (groupby(sorted(st, key=sorter), sorter))]
356 362 else:
357 363 c.pull_request_pending_reviewers.append(o.user)
358 364 c.pull_request_reviewers.append([o.user, st])
359 365
360 366 # pull_requests repo_name we opened it against
361 367 # ie. other_repo must match
362 368 if repo_name != c.pull_request.other_repo.repo_name:
363 369 raise HTTPNotFound
364 370
365 371 # load compare data into template context
366 372 enable_comments = not c.pull_request.is_closed()
367 373 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
368 374
369 375 # inline comments
370 376 c.inline_cnt = 0
371 377 c.inline_comments = cc_model.get_inline_comments(
372 378 c.rhodecode_db_repo.repo_id,
373 379 pull_request=pull_request_id)
374 380 # count inline comments
375 381 for __, lines in c.inline_comments:
376 382 for comments in lines.values():
377 383 c.inline_cnt += len(comments)
378 384 # comments
379 385 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
380 386 pull_request=pull_request_id)
381 387
382 388 try:
383 389 cur_status = c.statuses[c.pull_request.revisions[0]][0]
384 390 except:
385 391 log.error(traceback.format_exc())
386 392 cur_status = 'undefined'
387 393 if c.pull_request.is_closed() and 0:
388 394 c.current_changeset_status = cur_status
389 395 else:
390 396 # changeset(pull-request) status calulation based on reviewers
391 397 c.current_changeset_status = cs_model.calculate_status(
392 398 c.pull_request_reviewers,
393 399 )
394 400 c.changeset_statuses = ChangesetStatus.STATUSES
395 401
396 402 return render('/pullrequests/pullrequest_show.html')
397 403
398 404 @NotAnonymous()
399 405 @jsonify
400 406 def comment(self, repo_name, pull_request_id):
401 407 pull_request = PullRequest.get_or_404(pull_request_id)
402 408 if pull_request.is_closed():
403 409 raise HTTPForbidden()
404 410
405 411 status = request.POST.get('changeset_status')
406 412 change_status = request.POST.get('change_changeset_status')
407 413 text = request.POST.get('text')
408 if status and change_status:
414
415 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
416 if status and change_status and allowed_to_change_status:
409 417 text = text or (_('Status change -> %s')
410 418 % ChangesetStatus.get_status_lbl(status))
411 419 comm = ChangesetCommentsModel().create(
412 420 text=text,
413 421 repo=c.rhodecode_db_repo.repo_id,
414 422 user=c.rhodecode_user.user_id,
415 423 pull_request=pull_request_id,
416 424 f_path=request.POST.get('f_path'),
417 425 line_no=request.POST.get('line'),
418 426 status_change=(ChangesetStatus.get_status_lbl(status)
419 if status and change_status else None)
427 if status and change_status and allowed_to_change_status else None)
420 428 )
421 429
422 # get status if set !
423 if status and change_status:
424 ChangesetStatusModel().set_status(
425 c.rhodecode_db_repo.repo_id,
426 status,
427 c.rhodecode_user.user_id,
428 comm,
429 pull_request=pull_request_id
430 )
431 430 action_logger(self.rhodecode_user,
432 431 'user_commented_pull_request:%s' % pull_request_id,
433 432 c.rhodecode_db_repo, self.ip_addr, self.sa)
434 433
435 if request.POST.get('save_close'):
436 if status in ['rejected', 'approved']:
437 PullRequestModel().close_pull_request(pull_request_id)
438 action_logger(self.rhodecode_user,
439 'user_closed_pull_request:%s' % pull_request_id,
440 c.rhodecode_db_repo, self.ip_addr, self.sa)
441 else:
442 h.flash(_('Closing pull request on other statuses than '
443 'rejected or approved forbidden'),
444 category='warning')
434 if allowed_to_change_status:
435 # get status if set !
436 if status and change_status:
437 ChangesetStatusModel().set_status(
438 c.rhodecode_db_repo.repo_id,
439 status,
440 c.rhodecode_user.user_id,
441 comm,
442 pull_request=pull_request_id
443 )
444
445 if request.POST.get('save_close'):
446 if status in ['rejected', 'approved']:
447 PullRequestModel().close_pull_request(pull_request_id)
448 action_logger(self.rhodecode_user,
449 'user_closed_pull_request:%s' % pull_request_id,
450 c.rhodecode_db_repo, self.ip_addr, self.sa)
451 else:
452 h.flash(_('Closing pull request on other statuses than '
453 'rejected or approved forbidden'),
454 category='warning')
445 455
446 456 Session().commit()
447 457
448 458 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
449 459 return redirect(h.url('pullrequest_show', repo_name=repo_name,
450 460 pull_request_id=pull_request_id))
451 461
452 462 data = {
453 463 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
454 464 }
455 465 if comm:
456 466 c.co = comm
457 467 data.update(comm.get_dict())
458 468 data.update({'rendered_text':
459 469 render('changeset/changeset_comment_block.html')})
460 470
461 471 return data
462 472
463 473 @NotAnonymous()
464 474 @jsonify
465 475 def delete_comment(self, repo_name, comment_id):
466 476 co = ChangesetComment.get(comment_id)
467 477 if co.pull_request.is_closed():
468 478 #don't allow deleting comments on closed pull request
469 479 raise HTTPForbidden()
470 480
471 481 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
472 482 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
473 483 ChangesetCommentsModel().delete(comment=co)
474 484 Session().commit()
475 485 return True
476 486 else:
477 487 raise HTTPForbidden()
@@ -1,172 +1,176 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(co)}
5 5 ##
6 6 <%def name="comment_block(co)">
7 7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 8 <div class="comment-wrapp">
9 9 <div class="meta">
10 10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 11 <div class="user">
12 12 ${co.author.username}
13 13 </div>
14 14 <div class="date">
15 15 ${h.age(co.modified_at)} <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
16 16 </div>
17 17 %if co.status_change:
18 18 <div style="float:left" class="changeset-status-container">
19 19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change[0].status_lbl}</div>
21 21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change[0].status))}" /></div>
22 22 </div>
23 23 %endif
24 24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 25 <div class="buttons">
26 26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 27 </div>
28 28 %endif
29 29 </div>
30 30 <div class="text">
31 31 ${h.rst_w_mentions(co.text)|n}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 </%def>
36 36
37 37
38 38 <%def name="comment_inline_form()">
39 39 <div id='comment-inline-form-template' style="display:none">
40 40 <div class="comment-inline-form ac">
41 41 %if c.rhodecode_user.username != 'default':
42 42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 43 ${h.form('#', class_='inline-form')}
44 44 <div class="clearfix">
45 45 <div class="comment-help">${_('Commenting on line {1}.')}
46 46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 49 )
50 50 )|n
51 51 }
52 52 </div>
53 53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 55 </div>
56 56 <div class="comment-button">
57 57 <input type="hidden" name="f_path" value="{0}">
58 58 <input type="hidden" name="line" value="{1}">
59 59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 61 </div>
62 62 ${h.end_form()}
63 63 %else:
64 64 ${h.form('')}
65 65 <div class="clearfix">
66 66 <div class="comment-help">
67 67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button">
71 71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 72 </div>
73 73 ${h.end_form()}
74 74 %endif
75 75 </div>
76 76 </div>
77 77 </%def>
78 78
79 79
80 80 ## generates inlines taken from c.comments var
81 81 <%def name="inlines()">
82 82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
83 83 %for path, lines in c.inline_comments:
84 84 % for line,comments in lines.iteritems():
85 85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
86 86 %for co in comments:
87 87 ${comment_block(co)}
88 88 %endfor
89 89 </div>
90 90 %endfor
91 91 %endfor
92 92
93 93 </%def>
94 94
95 95 ## generate inline comments and the main ones
96 96 <%def name="generate_comments()">
97 97 <div class="comments">
98 98 <div id="inline-comments-container">
99 99 ## generate inlines for this changeset
100 100 ${inlines()}
101 101 </div>
102 102
103 103 %for co in c.comments:
104 104 <div id="comment-tr-${co.comment_id}">
105 105 ${comment_block(co)}
106 106 </div>
107 107 %endfor
108 108 </div>
109 109 </%def>
110 110
111 111 ## MAIN COMMENT FORM
112 <%def name="comments(post_url, cur_status, close_btn=False)">
112 <%def name="comments(post_url, cur_status, close_btn=False, change_status=True)">
113 113
114 114 <div class="comments">
115 115 %if c.rhodecode_user.username != 'default':
116 116 <div class="comment-form ac">
117 117 ${h.form(post_url)}
118 118 <strong>${_('Leave a comment')}</strong>
119 119 <div class="clearfix">
120 120 <div class="comment-help">
121 121 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
122 122 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
123 123 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
124 %if change_status:
124 125 | <label for="show_changeset_status_box" class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}</label>
125 126 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
127 %endif
126 128 </div>
129 %if change_status:
127 130 <div id="status_block_container" class="status-block" style="display:none">
128 131 %for status,lbl in c.changeset_statuses:
129 132 <div class="">
130 133 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
131 134 <label for="${status}">${lbl}</label>
132 135 </div>
133 136 %endfor
134 137 </div>
138 %endif
135 139 <div class="mentions-container" id="mentions_container"></div>
136 140 ${h.textarea('text')}
137 141 </div>
138 142 <div class="comment-button">
139 143 ${h.submit('save', _('Comment'), class_="ui-btn large")}
140 %if close_btn:
141 ${h.submit('save_close', _('Comment and close'), class_='ui-btn blue large %s' % 'hidden' if cur_status in ['not_reviewd','under_review'] else '')}
144 %if close_btn and change_status:
145 ${h.submit('save_close', _('Comment and close'), class_='ui-btn blue large %s' % ('hidden' if cur_status in ['not_reviewed','under_review'] else ''))}
142 146 %endif
143 147 </div>
144 148 ${h.end_form()}
145 149 </div>
146 150 %endif
147 151 </div>
148 152 <script>
149 153 YUE.onDOMReady(function () {
150 154 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
151 155
152 156 // changeset status box listener
153 157 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
154 158 if(e.currentTarget.checked){
155 159 YUD.setStyle('status_block_container','display','');
156 160 }
157 161 else{
158 162 YUD.setStyle('status_block_container','display','none');
159 163 }
160 164 })
161 165 YUE.on(YUQ('.status_change_radio'), 'change',function(e){
162 166 var val = e.currentTarget.value;
163 167 if (val == 'approved' || val == 'rejected') {
164 168 YUD.removeClass('save_close', 'hidden');
165 169 }else{
166 170 YUD.addClass('save_close', 'hidden');
167 171 }
168 172 })
169 173
170 174 });
171 175 </script>
172 176 </%def>
@@ -1,217 +1,217 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
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.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
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 %if c.pull_request.is_closed():
23 23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 24 %endif
25 25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
26 26
27 27 <div class="form">
28 28 <div id="summary" class="fields">
29 29 <div class="field">
30 30 <div class="label-summary">
31 31 <label>${_('Status')}:</label>
32 32 </div>
33 33 <div class="input">
34 34 <div class="changeset-status-container" style="float:none;clear:both">
35 35 %if c.current_changeset_status:
36 36 <div title="${_('Pull request status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
37 37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
38 38 %endif
39 39 </div>
40 40 </div>
41 41 </div>
42 42 <div class="field">
43 43 <div class="label-summary">
44 44 <label>${_('Still not reviewed by')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 % if len(c.pull_request_pending_reviewers) > 0:
48 48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
49 49 %else:
50 50 <div>${_('pull request was reviewed by all reviewers')}</div>
51 51 %endif
52 52 </div>
53 53 </div>
54 54 </div>
55 55 </div>
56 56 <div style="white-space:pre-wrap;padding:3px 3px 5px 20px">${h.literal(c.pull_request.description)}</div>
57 57 <div style="padding:4px 4px 10px 20px">
58 58 <div>${_('Created on')}: ${h.fmt_date(c.pull_request.created_on)}</div>
59 59 </div>
60 60
61 61 <div style="overflow: auto;">
62 62 ##DIFF
63 63 <div class="table" style="float:left;clear:none">
64 64 <div id="body" class="diffblock">
65 65 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
66 66 </div>
67 67 <div id="changeset_compare_view_content">
68 68 ##CS
69 69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
70 70 <%include file="/compare/compare_cs.html" />
71 71
72 72 ## FILES
73 73 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
74 74
75 75 % if c.limited_diff:
76 76 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
77 77 % else:
78 78 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
79 79 %endif
80 80
81 81 </div>
82 82 <div class="cs_files">
83 83 %if not c.files:
84 84 <span class="empty_data">${_('No files')}</span>
85 85 %endif
86 86 %for fid, change, f, stat in c.files:
87 87 <div class="cs_${change}">
88 88 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
89 89 <div class="changes">${h.fancy_file_stats(stat)}</div>
90 90 </div>
91 91 %endfor
92 92 </div>
93 93 % if c.limited_diff:
94 94 <h5>${_('Changeset was too big and was cut off...')}</h5>
95 95 % endif
96 96 </div>
97 97 </div>
98 98 ## REVIEWERS
99 99 <div style="float:left; border-left:1px dashed #eee">
100 100 <h4>${_('Pull request reviewers')}</h4>
101 101 <div id="reviewers" style="padding:0px 0px 5px 10px">
102 102 ## members goes here !
103 103 <div class="group_members_wrap" style="min-height:45px">
104 104 <ul id="review_members" class="group_members">
105 105 %for member,status in c.pull_request_reviewers:
106 106 <li id="reviewer_${member.user_id}">
107 107 <div class="reviewers_member">
108 108 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
109 109 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
110 110 </div>
111 111 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
112 112 <div style="float:left">${member.full_name} (${_('owner')})</div>
113 113 <input type="hidden" value="${member.user_id}" name="review_members" />
114 114 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id):
115 115 <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
116 116 %endif
117 117 </div>
118 118 </li>
119 119 %endfor
120 120 </ul>
121 121 </div>
122 122 %if not c.pull_request.is_closed():
123 123 <div class='ac'>
124 124 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
125 125 <div class="reviewer_ac">
126 126 ${h.text('user', class_='yui-ac-input')}
127 127 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
128 128 <div id="reviewers_container"></div>
129 129 </div>
130 130 <div style="padding:0px 10px">
131 131 <span id="update_pull_request" class="ui-btn xsmall">${_('save')}</span>
132 132 </div>
133 133 %endif
134 134 </div>
135 135 %endif
136 136 </div>
137 137 </div>
138 138 </div>
139 139 <script>
140 140 var _USERS_AC_DATA = ${c.users_array|n};
141 141 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
142 142 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
143 143 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
144 144 AJAX_UPDATE_PULLREQUEST = "${url('pullrequest_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}"
145 145 </script>
146 146
147 147 ## diff block
148 148 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
149 149 %for fid, change, f, stat in c.files:
150 150 ${diff_block.diff_block_simple([c.changes[fid]])}
151 151 %endfor
152 152 % if c.limited_diff:
153 153 <h4>${_('Changeset was too big and was cut off...')}</h4>
154 154 % endif
155 155
156 156
157 157 ## template for inline comment form
158 158 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
159 159 ${comment.comment_inline_form()}
160 160
161 161 ## render comments and inlines
162 162 ${comment.generate_comments()}
163 163
164 164 % if not c.pull_request.is_closed():
165 165 ## main comment form and it status
166 166 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
167 167 pull_request_id=c.pull_request.pull_request_id),
168 168 c.current_changeset_status,
169 close_btn=True)}
169 close_btn=True, change_status=c.allowed_to_change_status)}
170 170 %endif
171 171
172 172 <script type="text/javascript">
173 173 YUE.onDOMReady(function(){
174 174 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
175 175
176 176 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
177 177 var show = 'none';
178 178 var target = e.currentTarget;
179 179 if(target.checked){
180 180 var show = ''
181 181 }
182 182 var boxid = YUD.getAttribute(target,'id_for');
183 183 var comments = YUQ('#{0} .inline-comments'.format(boxid));
184 184 for(c in comments){
185 185 YUD.setStyle(comments[c],'display',show);
186 186 }
187 187 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
188 188 for(c in btns){
189 189 YUD.setStyle(btns[c],'display',show);
190 190 }
191 191 })
192 192
193 193 YUE.on(YUQ('.line'),'click',function(e){
194 194 var tr = e.currentTarget;
195 195 injectInlineForm(tr);
196 196 });
197 197
198 198 // inject comments into they proper positions
199 199 var file_comments = YUQ('.inline-comment-placeholder');
200 200 renderInlineComments(file_comments);
201 201
202 202 YUE.on(YUD.get('update_pull_request'),'click',function(e){
203 203
204 204 var reviewers_ids = [];
205 205 var ids = YUQ('#review_members input');
206 206 for(var i=0; i<ids.length;i++){
207 207 var id = ids[i].value
208 208 reviewers_ids.push(id);
209 209 }
210 210 updateReviewers(reviewers_ids);
211 211 })
212 212 })
213 213 </script>
214 214
215 215 </div>
216 216
217 217 </%def>
General Comments 0
You need to be logged in to leave comments. Login now