##// END OF EJS Templates
pull requests: make it possible control display of closed PRs and whether it is PRs to or from repo
Mads Kiilerich -
r4024:73ef2a5d default
parent child Browse files
Show More
@@ -1,556 +1,558 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, tmpl_context as c, url
34 34 from pylons.controllers.util import 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.helpers import Page
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import diffs
44 44 from rhodecode.lib.utils import action_logger, jsonify
45 45 from rhodecode.lib.vcs.utils import safe_str
46 46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
47 47 from rhodecode.lib.diffs import LimitedDiffContainer
48 48 from rhodecode.model.db import PullRequest, ChangesetStatus, 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 from rhodecode.lib.utils2 import safe_int
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class PullrequestsController(BaseRepoController):
61 61
62 62 def __before__(self):
63 63 super(PullrequestsController, self).__before__()
64 64 repo_model = RepoModel()
65 65 c.users_array = repo_model.get_users_js()
66 66 c.users_groups_array = repo_model.get_users_groups_js()
67 67
68 68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
69 69 """return a structure with repo's interesting changesets, suitable for
70 70 the selectors in pullrequest.html
71 71
72 72 rev: a revision that must be in the list somehow and selected by default
73 73 branch: a branch that must be in the list and selected by default - even if closed
74 74 branch_rev: a revision of which peers should be preferred and available."""
75 75 # list named branches that has been merged to this named branch - it should probably merge back
76 76 peers = []
77 77
78 78 if rev:
79 79 rev = safe_str(rev)
80 80
81 81 if branch:
82 82 branch = safe_str(branch)
83 83
84 84 if branch_rev:
85 85 branch_rev = safe_str(branch_rev)
86 86 # not restricting to merge() would also get branch point and be better
87 87 # (especially because it would get the branch point) ... but is currently too expensive
88 88 otherbranches = {}
89 89 for i in repo._repo.revs(
90 90 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
91 91 branch_rev, branch_rev):
92 92 cs = repo.get_changeset(i)
93 93 otherbranches[cs.branch] = cs.raw_id
94 94 for abranch, node in otherbranches.iteritems():
95 95 selected = 'branch:%s:%s' % (abranch, node)
96 96 peers.append((selected, abranch))
97 97
98 98 selected = None
99 99
100 100 branches = []
101 101 for abranch, branchrev in repo.branches.iteritems():
102 102 n = 'branch:%s:%s' % (abranch, branchrev)
103 103 branches.append((n, abranch))
104 104 if rev == branchrev:
105 105 selected = n
106 106 if branch == abranch:
107 107 selected = n
108 108 branch = None
109 109 if branch: # branch not in list - it is probably closed
110 110 revs = repo._repo.revs('max(branch(%s))', branch)
111 111 if revs:
112 112 cs = repo.get_changeset(revs[0])
113 113 selected = 'branch:%s:%s' % (branch, cs.raw_id)
114 114 branches.append((selected, branch))
115 115
116 116 bookmarks = []
117 117 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
118 118 n = 'book:%s:%s' % (bookmark, bookmarkrev)
119 119 bookmarks.append((n, bookmark))
120 120 if rev == bookmarkrev:
121 121 selected = n
122 122
123 123 tags = []
124 124 for tag, tagrev in repo.tags.iteritems():
125 125 n = 'tag:%s:%s' % (tag, tagrev)
126 126 tags.append((n, tag))
127 127 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
128 128 selected = n
129 129
130 130 # prio 1: rev was selected as existing entry above
131 131
132 132 # prio 2: create special entry for rev; rev _must_ be used
133 133 specials = []
134 134 if rev and selected is None:
135 135 selected = 'rev:%s:%s' % (rev, rev)
136 136 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
137 137
138 138 # prio 3: most recent peer branch
139 139 if peers and not selected:
140 140 selected = peers[0][0][0]
141 141
142 142 # prio 4: tip revision
143 143 if not selected:
144 144 selected = 'tag:tip:%s' % repo.tags['tip']
145 145
146 146 groups = [(specials, _("Special")),
147 147 (peers, _("Peer branches")),
148 148 (bookmarks, _("Bookmarks")),
149 149 (branches, _("Branches")),
150 150 (tags, _("Tags")),
151 151 ]
152 152 return [g for g in groups if g[0]], selected
153 153
154 154 def _get_is_allowed_change_status(self, pull_request):
155 155 owner = self.rhodecode_user.user_id == pull_request.user_id
156 156 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
157 157 pull_request.reviewers]
158 158 return (self.rhodecode_user.admin or owner or reviewer)
159 159
160 160 def _load_compare_data(self, pull_request, enable_comments=True):
161 161 """
162 162 Load context data needed for generating compare diff
163 163
164 164 :param pull_request:
165 165 """
166 166 org_repo = pull_request.org_repo
167 167 (org_ref_type,
168 168 org_ref_name,
169 169 org_ref_rev) = pull_request.org_ref.split(':')
170 170
171 171 other_repo = org_repo
172 172 (other_ref_type,
173 173 other_ref_name,
174 174 other_ref_rev) = pull_request.other_ref.split(':')
175 175
176 176 # despite opening revisions for bookmarks/branches/tags, we always
177 177 # convert this to rev to prevent changes after bookmark or branch change
178 178 org_ref = ('rev', org_ref_rev)
179 179 other_ref = ('rev', other_ref_rev)
180 180
181 181 c.org_repo = org_repo
182 182 c.other_repo = other_repo
183 183
184 184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
185 185
186 186 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
187 187
188 188 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
189 189
190 190 c.org_ref = org_ref[1]
191 191 c.org_ref_type = org_ref[0]
192 192 c.other_ref = other_ref[1]
193 193 c.other_ref_type = other_ref[0]
194 194
195 195 diff_limit = self.cut_off_limit if not fulldiff else None
196 196
197 197 # we swap org/other ref since we run a simple diff on one repo
198 198 log.debug('running diff between %s and %s in %s'
199 199 % (other_ref, org_ref, org_repo.scm_instance.path))
200 200 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
201 201
202 202 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
203 203 diff_limit=diff_limit)
204 204 _parsed = diff_processor.prepare()
205 205
206 206 c.limited_diff = False
207 207 if isinstance(_parsed, LimitedDiffContainer):
208 208 c.limited_diff = True
209 209
210 210 c.files = []
211 211 c.changes = {}
212 212 c.lines_added = 0
213 213 c.lines_deleted = 0
214 214
215 215 for f in _parsed:
216 216 st = f['stats']
217 217 c.lines_added += st['added']
218 218 c.lines_deleted += st['deleted']
219 219 fid = h.FID('', f['filename'])
220 220 c.files.append([fid, f['operation'], f['filename'], f['stats']])
221 221 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
222 222 parsed_lines=[f])
223 223 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
224 224
225 225 @LoginRequired()
226 226 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
227 227 'repository.admin')
228 228 def show_all(self, repo_name):
229 c.pull_requests = PullRequestModel().get_all(repo_name)
229 c.from_ = request.GET.get('from_') or ''
230 c.closed = request.GET.get('closed') or ''
231 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
230 232 c.repo_name = repo_name
231 233 p = safe_int(request.GET.get('page', 1), 1)
232 234
233 235 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
234 236
235 237 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
236 238
237 239 if request.environ.get('HTTP_X_PARTIAL_XHR'):
238 240 return c.pullrequest_data
239 241
240 242 return render('/pullrequests/pullrequest_show_all.html')
241 243
242 244 @LoginRequired()
243 245 @NotAnonymous()
244 246 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
245 247 'repository.admin')
246 248 def index(self):
247 249 org_repo = c.rhodecode_db_repo
248 250
249 251 if org_repo.scm_instance.alias != 'hg':
250 252 log.error('Review not available for GIT REPOS')
251 253 raise HTTPNotFound
252 254
253 255 try:
254 256 org_repo.scm_instance.get_changeset()
255 257 except EmptyRepositoryError, e:
256 258 h.flash(h.literal(_('There are no changesets yet')),
257 259 category='warning')
258 260 redirect(url('summary_home', repo_name=org_repo.repo_name))
259 261
260 262 org_rev = request.GET.get('rev_end')
261 263 # rev_start is not directly useful - its parent could however be used
262 264 # as default for other and thus give a simple compare view
263 265 #other_rev = request.POST.get('rev_start')
264 266 branch = request.GET.get('branch')
265 267
266 268 c.org_repos = []
267 269 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
268 270 c.default_org_repo = org_repo.repo_name
269 271 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
270 272
271 273 c.other_repos = []
272 274 other_repos_info = {}
273 275
274 276 def add_other_repo(repo, branch_rev=None):
275 277 if repo.repo_name in other_repos_info: # shouldn't happen
276 278 return
277 279 c.other_repos.append((repo.repo_name, repo.repo_name))
278 280 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
279 281 other_repos_info[repo.repo_name] = {
280 282 'user': dict(user_id=repo.user.user_id,
281 283 username=repo.user.username,
282 284 firstname=repo.user.firstname,
283 285 lastname=repo.user.lastname,
284 286 gravatar_link=h.gravatar_url(repo.user.email, 14)),
285 287 'description': repo.description.split('\n', 1)[0],
286 288 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
287 289 }
288 290
289 291 # add org repo to other so we can open pull request against peer branches on itself
290 292 add_other_repo(org_repo, branch_rev=org_rev)
291 293 c.default_other_repo = org_repo.repo_name
292 294
293 295 # gather forks and add to this list ... even though it is rare to
294 296 # request forks to pull from their parent
295 297 for fork in org_repo.forks:
296 298 add_other_repo(fork)
297 299
298 300 # add parents of this fork also, but only if it's not empty
299 301 if org_repo.parent and org_repo.parent.scm_instance.revisions:
300 302 add_other_repo(org_repo.parent)
301 303 c.default_other_repo = org_repo.parent.repo_name
302 304
303 305 c.default_other_repo_info = other_repos_info[c.default_other_repo]
304 306 c.other_repos_info = json.dumps(other_repos_info)
305 307
306 308 return render('/pullrequests/pullrequest.html')
307 309
308 310 @LoginRequired()
309 311 @NotAnonymous()
310 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 313 'repository.admin')
312 314 def create(self, repo_name):
313 315 repo = RepoModel()._get_repo(repo_name)
314 316 try:
315 317 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
316 318 except formencode.Invalid, errors:
317 319 log.error(traceback.format_exc())
318 320 if errors.error_dict.get('revisions'):
319 321 msg = 'Revisions: %s' % errors.error_dict['revisions']
320 322 elif errors.error_dict.get('pullrequest_title'):
321 323 msg = _('Pull request requires a title with min. 3 chars')
322 324 else:
323 325 msg = _('Error creating pull request')
324 326
325 327 h.flash(msg, 'error')
326 328 return redirect(url('pullrequest_home', repo_name=repo_name))
327 329
328 330 org_repo = _form['org_repo']
329 331 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
330 332 other_repo = _form['other_repo']
331 333 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
332 334 revisions = [x for x in reversed(_form['revisions'])]
333 335 reviewers = _form['review_members']
334 336
335 337 title = _form['pullrequest_title']
336 338 description = _form['pullrequest_desc']
337 339 try:
338 340 pull_request = PullRequestModel().create(
339 341 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
340 342 other_ref, revisions, reviewers, title, description
341 343 )
342 344 Session().commit()
343 345 h.flash(_('Successfully opened new pull request'),
344 346 category='success')
345 347 except Exception:
346 348 h.flash(_('Error occurred during sending pull request'),
347 349 category='error')
348 350 log.error(traceback.format_exc())
349 351 return redirect(url('pullrequest_home', repo_name=repo_name))
350 352
351 353 return redirect(url('pullrequest_show', repo_name=other_repo,
352 354 pull_request_id=pull_request.pull_request_id))
353 355
354 356 @LoginRequired()
355 357 @NotAnonymous()
356 358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
357 359 'repository.admin')
358 360 @jsonify
359 361 def update(self, repo_name, pull_request_id):
360 362 pull_request = PullRequest.get_or_404(pull_request_id)
361 363 if pull_request.is_closed():
362 364 raise HTTPForbidden()
363 365 #only owner or admin can update it
364 366 owner = pull_request.author.user_id == c.rhodecode_user.user_id
365 367 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
366 368 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
367 369 request.POST.get('reviewers_ids', '').split(',')))
368 370
369 371 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
370 372 Session().commit()
371 373 return True
372 374 raise HTTPForbidden()
373 375
374 376 @LoginRequired()
375 377 @NotAnonymous()
376 378 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
377 379 'repository.admin')
378 380 @jsonify
379 381 def delete(self, repo_name, pull_request_id):
380 382 pull_request = PullRequest.get_or_404(pull_request_id)
381 383 #only owner can delete it !
382 384 if pull_request.author.user_id == c.rhodecode_user.user_id:
383 385 PullRequestModel().delete(pull_request)
384 386 Session().commit()
385 387 h.flash(_('Successfully deleted pull request'),
386 388 category='success')
387 389 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
388 390 raise HTTPForbidden()
389 391
390 392 @LoginRequired()
391 393 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
392 394 'repository.admin')
393 395 def show(self, repo_name, pull_request_id):
394 396 repo_model = RepoModel()
395 397 c.users_array = repo_model.get_users_js()
396 398 c.users_groups_array = repo_model.get_users_groups_js()
397 399 c.pull_request = PullRequest.get_or_404(pull_request_id)
398 400 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
399 401 cc_model = ChangesetCommentsModel()
400 402 cs_model = ChangesetStatusModel()
401 403 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
402 404 pull_request=c.pull_request,
403 405 with_revisions=True)
404 406
405 407 cs_statuses = defaultdict(list)
406 408 for st in _cs_statuses:
407 409 cs_statuses[st.author.username] += [st]
408 410
409 411 c.pull_request_reviewers = []
410 412 c.pull_request_pending_reviewers = []
411 413 for o in c.pull_request.reviewers:
412 414 st = cs_statuses.get(o.user.username, None)
413 415 if st:
414 416 sorter = lambda k: k.version
415 417 st = [(x, list(y)[0])
416 418 for x, y in (groupby(sorted(st, key=sorter), sorter))]
417 419 else:
418 420 c.pull_request_pending_reviewers.append(o.user)
419 421 c.pull_request_reviewers.append([o.user, st])
420 422
421 423 # pull_requests repo_name we opened it against
422 424 # ie. other_repo must match
423 425 if repo_name != c.pull_request.other_repo.repo_name:
424 426 raise HTTPNotFound
425 427
426 428 # load compare data into template context
427 429 enable_comments = not c.pull_request.is_closed()
428 430 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
429 431
430 432 # inline comments
431 433 c.inline_cnt = 0
432 434 c.inline_comments = cc_model.get_inline_comments(
433 435 c.rhodecode_db_repo.repo_id,
434 436 pull_request=pull_request_id)
435 437 # count inline comments
436 438 for __, lines in c.inline_comments:
437 439 for comments in lines.values():
438 440 c.inline_cnt += len(comments)
439 441 # comments
440 442 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
441 443 pull_request=pull_request_id)
442 444
443 445 try:
444 446 cur_status = c.statuses[c.pull_request.revisions[0]][0]
445 447 except Exception:
446 448 log.error(traceback.format_exc())
447 449 cur_status = 'undefined'
448 450 if c.pull_request.is_closed() and 0:
449 451 c.current_changeset_status = cur_status
450 452 else:
451 453 # changeset(pull-request) status calulation based on reviewers
452 454 c.current_changeset_status = cs_model.calculate_status(
453 455 c.pull_request_reviewers,
454 456 )
455 457 c.changeset_statuses = ChangesetStatus.STATUSES
456 458
457 459 c.as_form = False
458 460 c.ancestor = None # there is one - but right here we don't know which
459 461 return render('/pullrequests/pullrequest_show.html')
460 462
461 463 @LoginRequired()
462 464 @NotAnonymous()
463 465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 466 'repository.admin')
465 467 @jsonify
466 468 def comment(self, repo_name, pull_request_id):
467 469 pull_request = PullRequest.get_or_404(pull_request_id)
468 470 if pull_request.is_closed():
469 471 raise HTTPForbidden()
470 472
471 473 status = request.POST.get('changeset_status')
472 474 change_status = request.POST.get('change_changeset_status')
473 475 text = request.POST.get('text')
474 476 close_pr = request.POST.get('save_close')
475 477
476 478 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
477 479 if status and change_status and allowed_to_change_status:
478 480 _def = (_('Status change -> %s')
479 481 % ChangesetStatus.get_status_lbl(status))
480 482 if close_pr:
481 483 _def = _('Closing with') + ' ' + _def
482 484 text = text or _def
483 485 comm = ChangesetCommentsModel().create(
484 486 text=text,
485 487 repo=c.rhodecode_db_repo.repo_id,
486 488 user=c.rhodecode_user.user_id,
487 489 pull_request=pull_request_id,
488 490 f_path=request.POST.get('f_path'),
489 491 line_no=request.POST.get('line'),
490 492 status_change=(ChangesetStatus.get_status_lbl(status)
491 493 if status and change_status
492 494 and allowed_to_change_status else None),
493 495 closing_pr=close_pr
494 496 )
495 497
496 498 action_logger(self.rhodecode_user,
497 499 'user_commented_pull_request:%s' % pull_request_id,
498 500 c.rhodecode_db_repo, self.ip_addr, self.sa)
499 501
500 502 if allowed_to_change_status:
501 503 # get status if set !
502 504 if status and change_status:
503 505 ChangesetStatusModel().set_status(
504 506 c.rhodecode_db_repo.repo_id,
505 507 status,
506 508 c.rhodecode_user.user_id,
507 509 comm,
508 510 pull_request=pull_request_id
509 511 )
510 512
511 513 if close_pr:
512 514 if status in ['rejected', 'approved']:
513 515 PullRequestModel().close_pull_request(pull_request_id)
514 516 action_logger(self.rhodecode_user,
515 517 'user_closed_pull_request:%s' % pull_request_id,
516 518 c.rhodecode_db_repo, self.ip_addr, self.sa)
517 519 else:
518 520 h.flash(_('Closing pull request on other statuses than '
519 521 'rejected or approved forbidden'),
520 522 category='warning')
521 523
522 524 Session().commit()
523 525
524 526 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
525 527 return redirect(h.url('pullrequest_show', repo_name=repo_name,
526 528 pull_request_id=pull_request_id))
527 529
528 530 data = {
529 531 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
530 532 }
531 533 if comm:
532 534 c.co = comm
533 535 data.update(comm.get_dict())
534 536 data.update({'rendered_text':
535 537 render('changeset/changeset_comment_block.html')})
536 538
537 539 return data
538 540
539 541 @LoginRequired()
540 542 @NotAnonymous()
541 543 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
542 544 'repository.admin')
543 545 @jsonify
544 546 def delete_comment(self, repo_name, comment_id):
545 547 co = ChangesetComment.get(comment_id)
546 548 if co.pull_request.is_closed():
547 549 #don't allow deleting comments on closed pull request
548 550 raise HTTPForbidden()
549 551
550 552 owner = co.author.user_id == c.rhodecode_user.user_id
551 553 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
552 554 ChangesetCommentsModel().delete(comment=co)
553 555 Session().commit()
554 556 return True
555 557 else:
556 558 raise HTTPForbidden()
@@ -1,156 +1,163 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
29 29 from pylons.i18n.translation import _
30 30
31 31 from rhodecode.model.meta import Session
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.model import BaseModel
34 34 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\
35 35 ChangesetStatus
36 36 from rhodecode.model.notification import NotificationModel
37 37 from rhodecode.lib.utils2 import safe_unicode
38 38
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class PullRequestModel(BaseModel):
44 44
45 45 cls = PullRequest
46 46
47 47 def __get_pull_request(self, pull_request):
48 48 return self._get_instance(PullRequest, pull_request)
49 49
50 def get_all(self, repo):
51 repo = self._get_repo(repo)
52 return PullRequest.query()\
53 .filter(PullRequest.other_repo == repo)\
54 .order_by(PullRequest.created_on.desc())\
55 .all()
50 def get_all(self, repo_name, from_=False, closed=False):
51 """Get all PRs for repo.
52 Default is all PRs to the repo, PRs from the repo if from_.
53 Closed PRs are only included if closed is true."""
54 repo = self._get_repo(repo_name)
55 q = PullRequest.query()
56 if from_:
57 q = q.filter(PullRequest.org_repo == repo)
58 else:
59 q = q.filter(PullRequest.other_repo == repo)
60 if not closed:
61 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
62 return q.order_by(PullRequest.created_on.desc()).all()
56 63
57 64 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
58 65 revisions, reviewers, title, description=None):
59 66 from rhodecode.model.changeset_status import ChangesetStatusModel
60 67
61 68 created_by_user = self._get_user(created_by)
62 69 org_repo = self._get_repo(org_repo)
63 70 other_repo = self._get_repo(other_repo)
64 71
65 72 new = PullRequest()
66 73 new.org_repo = org_repo
67 74 new.org_ref = org_ref
68 75 new.other_repo = other_repo
69 76 new.other_ref = other_ref
70 77 new.revisions = revisions
71 78 new.title = title
72 79 new.description = description
73 80 new.author = created_by_user
74 81 Session().add(new)
75 82 Session().flush()
76 83 #members
77 84 for member in set(reviewers):
78 85 _usr = self._get_user(member)
79 86 reviewer = PullRequestReviewers(_usr, new)
80 87 Session().add(reviewer)
81 88
82 89 #reset state to under-review
83 90 ChangesetStatusModel().set_status(
84 91 repo=org_repo,
85 92 status=ChangesetStatus.STATUS_UNDER_REVIEW,
86 93 user=created_by_user,
87 94 pull_request=new
88 95 )
89 96 revision_data = [(x.raw_id, x.message)
90 97 for x in map(org_repo.get_changeset, revisions)]
91 98 #notification to reviewers
92 99 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
93 100 pull_request_id=new.pull_request_id,
94 101 qualified=True,
95 102 )
96 103 subject = safe_unicode(
97 104 h.link_to(
98 105 _('%(user)s wants you to review pull request #%(pr_id)s: %(pr_title)s') % \
99 106 {'user': created_by_user.username,
100 107 'pr_title': new.title,
101 108 'pr_id': new.pull_request_id},
102 109 pr_url
103 110 )
104 111 )
105 112 body = description
106 113 kwargs = {
107 114 'pr_title': title,
108 115 'pr_user_created': h.person(created_by_user.email),
109 116 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
110 117 qualified=True,),
111 118 'pr_url': pr_url,
112 119 'pr_revisions': revision_data
113 120 }
114 121
115 122 NotificationModel().create(created_by=created_by_user, subject=subject, body=body,
116 123 recipients=reviewers,
117 124 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
118 125 return new
119 126
120 127 def update_reviewers(self, pull_request, reviewers_ids):
121 128 reviewers_ids = set(reviewers_ids)
122 129 pull_request = self.__get_pull_request(pull_request)
123 130 current_reviewers = PullRequestReviewers.query()\
124 131 .filter(PullRequestReviewers.pull_request==
125 132 pull_request)\
126 133 .all()
127 134 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
128 135
129 136 to_add = reviewers_ids.difference(current_reviewers_ids)
130 137 to_remove = current_reviewers_ids.difference(reviewers_ids)
131 138
132 139 log.debug("Adding %s reviewers" % to_add)
133 140 log.debug("Removing %s reviewers" % to_remove)
134 141
135 142 for uid in to_add:
136 143 _usr = self._get_user(uid)
137 144 reviewer = PullRequestReviewers(_usr, pull_request)
138 145 Session().add(reviewer)
139 146
140 147 for uid in to_remove:
141 148 reviewer = PullRequestReviewers.query()\
142 149 .filter(PullRequestReviewers.user_id==uid,
143 150 PullRequestReviewers.pull_request==pull_request)\
144 151 .scalar()
145 152 if reviewer:
146 153 Session().delete(reviewer)
147 154
148 155 def delete(self, pull_request):
149 156 pull_request = self.__get_pull_request(pull_request)
150 157 Session().delete(pull_request)
151 158
152 159 def close_pull_request(self, pull_request):
153 160 pull_request = self.__get_pull_request(pull_request)
154 161 pull_request.status = PullRequest.STATUS_CLOSED
155 162 pull_request.updated_on = datetime.datetime.now()
156 163 Session().add(pull_request)
@@ -1,361 +1,361 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- HEADER -->
5 5 <div id="header-dd"></div>
6 6 <div id="header">
7 7 <div id="header-inner" class="title">
8 8 <div id="logo">
9 9 <h1><a href="${h.url('home')}">${c.rhodecode_name}</a></h1>
10 10 </div>
11 11 <!-- MENU -->
12 12 ${self.page_nav()}
13 13 <!-- END MENU -->
14 14 ${self.body()}
15 15 </div>
16 16 </div>
17 17 <!-- END HEADER -->
18 18
19 19 <!-- CONTENT -->
20 20 <div id="content">
21 21 <div class="flash_msg">
22 22 <% messages = h.flash.pop_messages() %>
23 23 % if messages:
24 24 <ul id="flash-messages">
25 25 % for message in messages:
26 26 <li class="${message.category}_msg">${message}</li>
27 27 % endfor
28 28 </ul>
29 29 % endif
30 30 </div>
31 31 <div id="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 <!-- FOOTER -->
38 38 <div id="footer">
39 39 <div id="footer-inner" class="title">
40 40 <div>
41 41 <p class="footer-link">
42 42 ${_('Server instance: %s') % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}
43 43 </p>
44 44 <p class="footer-link-right">
45 45 <a href="${h.url('rhodecode_official')}">
46 46 RhodeCode
47 47 %if c.visual.show_version:
48 48 ${c.rhodecode_version}
49 49 %endif
50 50 </a>
51 51 &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski and others
52 52 %if c.rhodecode_bugtracker:
53 53 &ndash; <a href="${c.rhodecode_bugtracker}">${_('Report a bug')}</a>
54 54 %endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63 <%def name="breadcrumbs()">
64 64 <div class="breadcrumbs">
65 65 ${self.breadcrumbs_links()}
66 66 </div>
67 67 </%def>
68 68
69 69 <%def name="admin_menu()">
70 70 <ul class="admin_menu">
71 71 <li>${h.link_to(_('Admin journal'),h.url('admin_home'),class_='journal ')}</li>
72 72 <li>${h.link_to(_('Repositories'),h.url('repos'),class_='repos')}</li>
73 73 <li>${h.link_to(_('Repository groups'),h.url('repos_groups'),class_='repos_groups')}</li>
74 74 <li>${h.link_to(_('Users'),h.url('users'),class_='users')}</li>
75 75 <li>${h.link_to(_('User groups'),h.url('users_groups'),class_='groups')}</li>
76 76 <li>${h.link_to(_('Permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li>
77 77 <li>${h.link_to(_('LDAP'),h.url('ldap_home'),class_='ldap')}</li>
78 78 <li>${h.link_to(_('Defaults'),h.url('defaults'),class_='defaults')}</li>
79 79 <li class="last">${h.link_to(_('Settings'),h.url('admin_settings'),class_='settings')}</li>
80 80 </ul>
81 81 </%def>
82 82
83 83 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
84 84 <ul>
85 85 %if repositories:
86 86 <li>${h.link_to(_('Repositories'),h.url('repos'),class_='repos')}</li>
87 87 %endif
88 88 %if repository_groups:
89 89 <li>${h.link_to(_('Repository groups'),h.url('repos_groups'),class_='repos_groups')}</li>
90 90 %endif
91 91 %if user_groups:
92 92 <li>${h.link_to(_('User groups'),h.url('users_groups'),class_='groups')}</li>
93 93 %endif
94 94 </ul>
95 95 </%def>
96 96
97 97 <%def name="repo_context_bar(current=None)">
98 98 <%
99 99 def follow_class():
100 100 if c.repository_following:
101 101 return h.literal('following')
102 102 else:
103 103 return h.literal('follow')
104 104 %>
105 105 <%
106 106 def is_current(selected):
107 107 if selected == current:
108 108 return h.literal('class="current"')
109 109 %>
110 110
111 111 <!--- CONTEXT BAR -->
112 112 <div id="context-bar" class="box">
113 113 <div id="breadcrumbs">
114 114 ${h.link_to(_(u'Repositories'),h.url('home'))}
115 115 &raquo;
116 116 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
117 117 </div>
118 118 <ul id="context-pages" class="horizontal-list">
119 119 <li ${is_current('summary')}><a href="${h.url('summary_home', repo_name=c.repo_name)}" class="summary">${_('Summary')}</a></li>
120 120 <li ${is_current('changelog')}><a href="${h.url('changelog_home', repo_name=c.repo_name)}" class="changelogs">${_('Changelog')}</a></li>
121 121 <li ${is_current('files')}><a href="${h.url('files_home', repo_name=c.repo_name)}" class="files"></span>${_('Files')}</a></li>
122 122 <li ${is_current('switch-to')}>
123 123 <a href="#" id="branch_tag_switcher_2" class="dropdown switch-to"></span>${_('Switch To')}</a>
124 124 <ul id="switch_to_list_2" class="switch_to submenu">
125 125 <li><a href="#">${_('Loading...')}</a></li>
126 126 </ul>
127 127 </li>
128 128 <li ${is_current('options')}>
129 129 <a href="#" class="dropdown options"></span>${_('Options')}</a>
130 130 <ul>
131 131 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
132 132 <li>${h.link_to(_('Settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}</li>
133 133 %endif
134 134 %if c.rhodecode_db_repo.fork:
135 135 <li>${h.link_to(_('Compare fork'),h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,org_ref_type='branch',org_ref='default',other_repo=c.repo_name,other_ref_type='branch',other_ref=request.GET.get('branch') or 'default', merge=1),class_='compare_request')}</li>
136 136 %endif
137 137 <li>${h.link_to(_('Search'),h.url('search_repo',repo_name=c.repo_name),class_='search')}</li>
138 138
139 139 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
140 140 %if c.rhodecode_db_repo.locked[0]:
141 141 <li>${h.link_to(_('Unlock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_del')}</li>
142 142 %else:
143 143 <li>${h.link_to(_('Lock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_add')}</li>
144 144 %endif
145 145 %endif
146 146 ## TODO: this check feels wrong, it would be better to have a check for permissions
147 147 ## also it feels like a job for the controller
148 148 %if c.rhodecode_user.username != 'default':
149 149 <li>
150 150 <a class="${follow_class()}" onclick="javascript:toggleFollowingRepo(this,${c.rhodecode_db_repo.repo_id},'${str(h.get_token())}');">
151 151 <span class="show-follow">${_('Follow')}</span>
152 152 <span class="show-following">${_('Unfollow')}</span>
153 153 </a>
154 154 </li>
155 155 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}" class="fork">${_('Fork')}</a></li>
156 156 %if h.is_hg(c.rhodecode_repo):
157 157 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="pull-request">${_('Create Pull Request')}</a></li>
158 158 %endif
159 159 %endif
160 160 </ul>
161 161 </li>
162 162 <li ${is_current('showpullrequest')}>
163 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests')}" class="pull-request">${_('Pull Requests')}
163 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}" class="pull-request">${_('Pull Requests')}
164 164 %if c.repository_pull_requests:
165 165 <span>${c.repository_pull_requests}</span>
166 166 %endif
167 167 </a>
168 168 </li>
169 169 </ul>
170 170 </div>
171 171 <script type="text/javascript">
172 172 YUE.on('branch_tag_switcher_2','mouseover',function(){
173 173 var loaded = YUD.hasClass('branch_tag_switcher_2','loaded');
174 174 if(!loaded){
175 175 YUD.addClass('branch_tag_switcher_2','loaded');
176 176 ypjax("${h.url('branch_tag_switcher',repo_name=c.repo_name)}",'switch_to_list_2',
177 177 function(o){},
178 178 function(o){YUD.removeClass('branch_tag_switcher_2','loaded');}
179 179 ,null);
180 180 }
181 181 return false;
182 182 });
183 183 </script>
184 184 <!--- END CONTEXT BAR -->
185 185 </%def>
186 186
187 187 <%def name="usermenu()">
188 188 ## USER MENU
189 189 <li>
190 190 <a class="menu_link childs" id="quick_login_link">
191 191 <span class="icon">
192 192 <img src="${h.gravatar_url(c.rhodecode_user.email,20)}" alt="avatar">
193 193 </span>
194 194 %if c.rhodecode_user.username != 'default':
195 195 <span class="menu_link_user">${c.rhodecode_user.username}</span>
196 196 %if c.unread_notifications != 0:
197 197 <span class="menu_link_notifications">${c.unread_notifications}</span>
198 198 %endif
199 199 %else:
200 200 <span>${_('Not logged in')}</span>
201 201 %endif
202 202 </a>
203 203
204 204 <div class="user-menu">
205 205 <div id="quick_login">
206 206 %if c.rhodecode_user.username == 'default':
207 207 <h4>${_('Login to your account')}</h4>
208 208 ${h.form(h.url('login_home',came_from=h.url.current()))}
209 209 <div class="form">
210 210 <div class="fields">
211 211 <div class="field">
212 212 <div class="label">
213 213 <label for="username">${_('Username')}:</label>
214 214 </div>
215 215 <div class="input">
216 216 ${h.text('username',class_='focus')}
217 217 </div>
218 218
219 219 </div>
220 220 <div class="field">
221 221 <div class="label">
222 222 <label for="password">${_('Password')}:</label>
223 223 </div>
224 224 <div class="input">
225 225 ${h.password('password',class_='focus')}
226 226 </div>
227 227
228 228 </div>
229 229 <div class="buttons">
230 230 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
231 231 <div class="register">
232 232 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
233 233 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
234 234 %endif
235 235 </div>
236 236 <div class="submit">
237 237 ${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
238 238 </div>
239 239 </div>
240 240 </div>
241 241 </div>
242 242 ${h.end_form()}
243 243 %else:
244 244 <div class="links_left">
245 245 <div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
246 246 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
247 247 <div class="email">${c.rhodecode_user.email}</div>
248 248 </div>
249 249 <div class="links_right">
250 250 <ol class="links">
251 251 <li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
252 252 <li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
253 253 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
254 254 </ol>
255 255 </div>
256 256 %endif
257 257 </div>
258 258 </div>
259 259
260 260 </li>
261 261 </%def>
262 262
263 263 <%def name="menu(current=None)">
264 264 <%
265 265 def is_current(selected):
266 266 if selected == current:
267 267 return h.literal('class="current"')
268 268 %>
269 269 <ul id="quick" class="horizontal-list">
270 270 <!-- repo switcher -->
271 271 <li ${is_current('repositories')}>
272 272 <a class="menu_link repo_switcher childs" id="repo_switcher" title="${_('Switch repository')}" href="${h.url('home')}">
273 273 ${_('Repositories')}
274 274 </a>
275 275 <ul id="repo_switcher_list" class="repo_switcher">
276 276 <li>
277 277 <a href="#">${_('Loading...')}</a>
278 278 </li>
279 279 </ul>
280 280 </li>
281 281 ##ROOT MENU
282 282 %if c.rhodecode_user.username != 'default':
283 283 <li ${is_current('journal')}>
284 284 <a class="menu_link journal" title="${_('Show recent activity')}" href="${h.url('journal')}">
285 285 ${_('Journal')}
286 286 </a>
287 287 </li>
288 288 %else:
289 289 <li ${is_current('journal')}>
290 290 <a class="menu_link journal" title="${_('Public journal')}" href="${h.url('public_journal')}">
291 291 ${_('Public journal')}
292 292 </a>
293 293 </li>
294 294 %endif
295 295 <li ${is_current('gists')}>
296 296 <a class="menu_link gists childs" title="${_('Show public gists')}" href="${h.url('gists')}">
297 297 ${_('Gists')}
298 298 </a>
299 299 <ul class="admin_menu">
300 300 <li>${h.link_to(_('Create new gist'),h.url('new_gist'),class_='gists-new ')}</li>
301 301 <li>${h.link_to(_('All public gists'),h.url('gists'),class_='gists ')}</li>
302 302 %if c.rhodecode_user.username != 'default':
303 303 <li>${h.link_to(_('My public gists'),h.url('gists', public=1),class_='gists')}</li>
304 304 <li>${h.link_to(_('My private gists'),h.url('gists', private=1),class_='gists-private ')}</li>
305 305 %endif
306 306 </ul>
307 307 </li>
308 308 <li ${is_current('search')}>
309 309 <a class="menu_link search" title="${_('Search in repositories')}" href="${h.url('search')}">
310 310 ${_('Search')}
311 311 </a>
312 312 </li>
313 313 % if h.HasPermissionAll('hg.admin')('access admin main page'):
314 314 <li ${is_current('admin')}>
315 315 <a class="menu_link admin childs" title="${_('Admin')}" href="${h.url('admin_home')}">
316 316 ${_('Admin')}
317 317 </a>
318 318 ${admin_menu()}
319 319 </li>
320 320 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
321 321 <li ${is_current('admin')}>
322 322 <a class="menu_link admin childs" title="${_('Admin')}">
323 323 ${_('Admin')}
324 324 </a>
325 325 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
326 326 c.rhodecode_user.repository_groups_admin,
327 327 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
328 328 </li>
329 329 % endif
330 330 ${usermenu()}
331 331 <script type="text/javascript">
332 332 YUE.on('repo_switcher','mouseover',function(){
333 333 var target = 'q_filter_rs';
334 334 var qfilter_activate = function(){
335 335 var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
336 336 var func = function(node){
337 337 return node.parentNode;
338 338 }
339 339 q_filter(target,nodes,func);
340 340 }
341 341
342 342 var loaded = YUD.hasClass('repo_switcher','loaded');
343 343 if(!loaded){
344 344 YUD.addClass('repo_switcher','loaded');
345 345 ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
346 346 function(o){qfilter_activate();YUD.get(target).focus()},
347 347 function(o){YUD.removeClass('repo_switcher','loaded');}
348 348 ,null);
349 349 }else{
350 350 YUD.get(target).focus();
351 351 }
352 352 return false;
353 353 });
354 354
355 355 YUE.on('header-dd', 'click',function(e){
356 356 YUD.addClass('header-inner', 'hover');
357 357 YUD.addClass('content', 'hover');
358 358 });
359 359
360 360 </script>
361 361 </%def>
@@ -1,24 +1,24 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 % for pr in c.pullrequests_pager:
4 4 <div class="pr ${'pr-closed' if pr.is_closed() else ''}">
5 5 <div class="pr-title">
6 6 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pr.last_review_status))}" />
7 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">
7 <a href="${h.url('pullrequest_show',repo_name=pr.other_repo.repo_name,pull_request_id=pr.pull_request_id)}">
8 8 ${_('Pull request #%s opened by %s on %s') % (pr.pull_request_id, pr.author.full_name, h.fmt_date(pr.created_on))}
9 9 </a>
10 10 %if pr.is_closed():
11 11 <span class="pr-closed-tag">${_('Closed')}</span>
12 12 %endif
13 13 </div>
14 14 <h5 style="border:0px;padding-bottom:0px">${_('Title')}: ${pr.title}</h5>
15 15 <div class="pr-desc">${pr.description}</div>
16 16 </div>
17 17 % endfor
18 18
19 19
20 20 <div class="notification-paginator">
21 21 <div class="pagination-wh pagination-left">
22 22 ${c.pullrequests_pager.pager('$link_previous ~2~ $link_next')}
23 23 </div>
24 24 </div>
@@ -1,26 +1,50 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Requests') % c.repo_name} &middot; ${c.rhodecode_name}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 ${_('Pull requests')}
8 %if c.from_:
9 ${_('Pull requests from %s') % c.repo_name}
10 %else:
11 ${_('Pull requests to %s') % c.repo_name}
12 %endif
9 13 </%def>
10 14
11 15 <%def name="page_nav()">
12 16 ${self.menu('repositories')}
13 17 </%def>
14 18
15 19 <%def name="main()">
16 20 ${self.repo_context_bar('showpullrequest')}
17 21
18 22 <div class="box">
19 23 <!-- box / title -->
20 24 <div class="title">
21 25 ${self.breadcrumbs()}
22 26 </div>
27
28 <div style="margin: 0 20px">
29 <div>
30 %if c.from_:
31 ${h.link_to(_('Instead, show pull requests to %s') % c.repo_name, h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed))}
32 %else:
33 ${h.link_to(_('Instead, show pull requests from %s') % c.repo_name, h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed,from_=1))}
34 %endif
35 </div>
36
37 <div>
38 %if c.closed:
39 ${h.link_to(_('Hide closed pull requests'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_))}
40 %else:
41 ${h.link_to(_('Show closed pull requests too'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_,closed=1))}
42 %endif
43 </div>
44 </div>
45
23 46 ${c.pullrequest_data}
47
24 48 </div>
25 49
26 50 </%def>
General Comments 0
You need to be logged in to leave comments. Login now