##// END OF EJS Templates
templateContext: improve the context object idea
ergo -
r395:52964495 default
parent child Browse files
Show More
@@ -1,849 +1,849 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24
25 25 import formencode
26 26 import logging
27 27
28 28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 29 from pylons import request, tmpl_context as c, url
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32 from sqlalchemy.sql import func
33 33 from sqlalchemy.sql.expression import or_
34 34
35 35 from rhodecode.lib import auth, diffs, helpers as h
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.base import (
38 38 BaseRepoController, render, vcs_operation_context)
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
41 41 HasAcceptedRepoType, XHRRequired)
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
47 47 from rhodecode.lib.diffs import LimitedDiffContainer
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import ChangesetCommentsModel
50 50 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
51 51 Repository
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class PullrequestsController(BaseRepoController):
60 60 def __before__(self):
61 61 super(PullrequestsController, self).__before__()
62 62
63 63 def _load_compare_data(self, pull_request, enable_comments=True):
64 64 """
65 65 Load context data needed for generating compare diff
66 66
67 67 :param pull_request: object related to the request
68 68 :param enable_comments: flag to determine if comments are included
69 69 """
70 70 source_repo = pull_request.source_repo
71 71 source_ref_id = pull_request.source_ref_parts.commit_id
72 72
73 73 target_repo = pull_request.target_repo
74 74 target_ref_id = pull_request.target_ref_parts.commit_id
75 75
76 76 # despite opening commits for bookmarks/branches/tags, we always
77 77 # convert this to rev to prevent changes after bookmark or branch change
78 78 c.source_ref_type = 'rev'
79 79 c.source_ref = source_ref_id
80 80
81 81 c.target_ref_type = 'rev'
82 82 c.target_ref = target_ref_id
83 83
84 84 c.source_repo = source_repo
85 85 c.target_repo = target_repo
86 86
87 87 c.fulldiff = bool(request.GET.get('fulldiff'))
88 88
89 89 # diff_limit is the old behavior, will cut off the whole diff
90 90 # if the limit is applied otherwise will just hide the
91 91 # big files from the front-end
92 92 diff_limit = self.cut_off_limit_diff
93 93 file_limit = self.cut_off_limit_file
94 94
95 95 pre_load = ["author", "branch", "date", "message"]
96 96
97 97 c.commit_ranges = []
98 98 source_commit = EmptyCommit()
99 99 target_commit = EmptyCommit()
100 100 c.missing_requirements = False
101 101 try:
102 102 c.commit_ranges = [
103 103 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
104 104 for rev in pull_request.revisions]
105 105
106 106 c.statuses = source_repo.statuses(
107 107 [x.raw_id for x in c.commit_ranges])
108 108
109 109 target_commit = source_repo.get_commit(
110 110 commit_id=safe_str(target_ref_id))
111 111 source_commit = source_repo.get_commit(
112 112 commit_id=safe_str(source_ref_id))
113 113 except RepositoryRequirementError:
114 114 c.missing_requirements = True
115 115
116 116 c.missing_commits = False
117 117 if (c.missing_requirements or
118 118 isinstance(source_commit, EmptyCommit) or
119 119 source_commit == target_commit):
120 120 _parsed = []
121 121 c.missing_commits = True
122 122 else:
123 123 vcs_diff = PullRequestModel().get_diff(pull_request)
124 124 diff_processor = diffs.DiffProcessor(
125 125 vcs_diff, format='gitdiff', diff_limit=diff_limit,
126 126 file_limit=file_limit, show_full_diff=c.fulldiff)
127 127 _parsed = diff_processor.prepare()
128 128
129 129 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
130 130
131 131 c.files = []
132 132 c.changes = {}
133 133 c.lines_added = 0
134 134 c.lines_deleted = 0
135 135 c.included_files = []
136 136 c.deleted_files = []
137 137
138 138 for f in _parsed:
139 139 st = f['stats']
140 140 c.lines_added += st['added']
141 141 c.lines_deleted += st['deleted']
142 142
143 143 fid = h.FID('', f['filename'])
144 144 c.files.append([fid, f['operation'], f['filename'], f['stats']])
145 145 c.included_files.append(f['filename'])
146 146 html_diff = diff_processor.as_html(enable_comments=enable_comments,
147 147 parsed_lines=[f])
148 148 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
149 149
150 150 def _extract_ordering(self, request):
151 151 column_index = safe_int(request.GET.get('order[0][column]'))
152 152 order_dir = request.GET.get('order[0][dir]', 'desc')
153 153 order_by = request.GET.get(
154 154 'columns[%s][data][sort]' % column_index, 'name_raw')
155 155 return order_by, order_dir
156 156
157 157 @LoginRequired()
158 158 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
159 159 'repository.admin')
160 160 @HasAcceptedRepoType('git', 'hg')
161 161 def show_all(self, repo_name):
162 162 # filter types
163 163 c.active = 'open'
164 164 c.source = str2bool(request.GET.get('source'))
165 165 c.closed = str2bool(request.GET.get('closed'))
166 166 c.my = str2bool(request.GET.get('my'))
167 167 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
168 168 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
169 169 c.repo_name = repo_name
170 170
171 171 opened_by = None
172 172 if c.my:
173 173 c.active = 'my'
174 174 opened_by = [c.rhodecode_user.user_id]
175 175
176 176 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
177 177 if c.closed:
178 178 c.active = 'closed'
179 179 statuses = [PullRequest.STATUS_CLOSED]
180 180
181 181 if c.awaiting_review and not c.source:
182 182 c.active = 'awaiting'
183 183 if c.source and not c.awaiting_review:
184 184 c.active = 'source'
185 185 if c.awaiting_my_review:
186 186 c.active = 'awaiting_my'
187 187
188 188 data = self._get_pull_requests_list(
189 189 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
190 190 if not request.is_xhr:
191 191 c.data = json.dumps(data['data'])
192 192 c.records_total = data['recordsTotal']
193 193 return render('/pullrequests/pullrequests.html')
194 194 else:
195 195 return json.dumps(data)
196 196
197 197 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
198 198 # pagination
199 199 start = safe_int(request.GET.get('start'), 0)
200 200 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
201 201 order_by, order_dir = self._extract_ordering(request)
202 202
203 203 if c.awaiting_review:
204 204 pull_requests = PullRequestModel().get_awaiting_review(
205 205 repo_name, source=c.source, opened_by=opened_by,
206 206 statuses=statuses, offset=start, length=length,
207 207 order_by=order_by, order_dir=order_dir)
208 208 pull_requests_total_count = PullRequestModel(
209 209 ).count_awaiting_review(
210 210 repo_name, source=c.source, statuses=statuses,
211 211 opened_by=opened_by)
212 212 elif c.awaiting_my_review:
213 213 pull_requests = PullRequestModel().get_awaiting_my_review(
214 214 repo_name, source=c.source, opened_by=opened_by,
215 215 user_id=c.rhodecode_user.user_id, statuses=statuses,
216 216 offset=start, length=length, order_by=order_by,
217 217 order_dir=order_dir)
218 218 pull_requests_total_count = PullRequestModel(
219 219 ).count_awaiting_my_review(
220 220 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
221 221 statuses=statuses, opened_by=opened_by)
222 222 else:
223 223 pull_requests = PullRequestModel().get_all(
224 224 repo_name, source=c.source, opened_by=opened_by,
225 225 statuses=statuses, offset=start, length=length,
226 226 order_by=order_by, order_dir=order_dir)
227 227 pull_requests_total_count = PullRequestModel().count_all(
228 228 repo_name, source=c.source, statuses=statuses,
229 229 opened_by=opened_by)
230 230
231 231 from rhodecode.lib.utils import PartialRenderer
232 232 _render = PartialRenderer('data_table/_dt_elements.html')
233 233 data = []
234 234 for pr in pull_requests:
235 235 comments = ChangesetCommentsModel().get_all_comments(
236 236 c.rhodecode_db_repo.repo_id, pull_request=pr)
237 237
238 238 data.append({
239 239 'name': _render('pullrequest_name',
240 240 pr.pull_request_id, pr.target_repo.repo_name),
241 241 'name_raw': pr.pull_request_id,
242 242 'status': _render('pullrequest_status',
243 243 pr.calculated_review_status()),
244 244 'title': _render(
245 245 'pullrequest_title', pr.title, pr.description),
246 246 'description': h.escape(pr.description),
247 247 'updated_on': _render('pullrequest_updated_on',
248 248 h.datetime_to_time(pr.updated_on)),
249 249 'updated_on_raw': h.datetime_to_time(pr.updated_on),
250 250 'created_on': _render('pullrequest_updated_on',
251 251 h.datetime_to_time(pr.created_on)),
252 252 'created_on_raw': h.datetime_to_time(pr.created_on),
253 253 'author': _render('pullrequest_author',
254 254 pr.author.full_contact, ),
255 255 'author_raw': pr.author.full_name,
256 256 'comments': _render('pullrequest_comments', len(comments)),
257 257 'comments_raw': len(comments),
258 258 'closed': pr.is_closed(),
259 259 })
260 260 # json used to render the grid
261 261 data = ({
262 262 'data': data,
263 263 'recordsTotal': pull_requests_total_count,
264 264 'recordsFiltered': pull_requests_total_count,
265 265 })
266 266 return data
267 267
268 268 @LoginRequired()
269 269 @NotAnonymous()
270 270 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
271 271 'repository.admin')
272 272 @HasAcceptedRepoType('git', 'hg')
273 273 def index(self):
274 274 source_repo = c.rhodecode_db_repo
275 275
276 276 try:
277 277 source_repo.scm_instance().get_commit()
278 278 except EmptyRepositoryError:
279 279 h.flash(h.literal(_('There are no commits yet')),
280 280 category='warning')
281 281 redirect(url('summary_home', repo_name=source_repo.repo_name))
282 282
283 283 commit_id = request.GET.get('commit')
284 284 branch_ref = request.GET.get('branch')
285 285 bookmark_ref = request.GET.get('bookmark')
286 286
287 287 try:
288 288 source_repo_data = PullRequestModel().generate_repo_data(
289 289 source_repo, commit_id=commit_id,
290 290 branch=branch_ref, bookmark=bookmark_ref)
291 291 except CommitDoesNotExistError as e:
292 292 log.exception(e)
293 293 h.flash(_('Commit does not exist'), 'error')
294 294 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
295 295
296 296 default_target_repo = source_repo
297 297 if (source_repo.parent and
298 298 not source_repo.parent.scm_instance().is_empty()):
299 299 # change default if we have a parent repo
300 300 default_target_repo = source_repo.parent
301 301
302 302 target_repo_data = PullRequestModel().generate_repo_data(
303 303 default_target_repo)
304 304
305 305 selected_source_ref = source_repo_data['refs']['selected_ref']
306 306
307 307 title_source_ref = selected_source_ref.split(':', 2)[1]
308 308 c.default_title = PullRequestModel().generate_pullrequest_title(
309 309 source=source_repo.repo_name,
310 310 source_ref=title_source_ref,
311 311 target=default_target_repo.repo_name
312 312 )
313 313
314 314 c.default_repo_data = {
315 315 'source_repo_name': source_repo.repo_name,
316 316 'source_refs_json': json.dumps(source_repo_data),
317 317 'target_repo_name': default_target_repo.repo_name,
318 318 'target_refs_json': json.dumps(target_repo_data),
319 319 }
320 320 c.default_source_ref = selected_source_ref
321 321
322 322 return render('/pullrequests/pullrequest.html')
323 323
324 324 @LoginRequired()
325 325 @NotAnonymous()
326 326 @XHRRequired()
327 327 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
328 328 'repository.admin')
329 329 @jsonify
330 330 def get_repo_refs(self, repo_name, target_repo_name):
331 331 repo = Repository.get_by_repo_name(target_repo_name)
332 332 if not repo:
333 333 raise HTTPNotFound
334 334 return PullRequestModel().generate_repo_data(repo)
335 335
336 336 @LoginRequired()
337 337 @NotAnonymous()
338 338 @XHRRequired()
339 339 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 340 'repository.admin')
341 341 @jsonify
342 342 def get_repo_destinations(self, repo_name):
343 343 repo = Repository.get_by_repo_name(repo_name)
344 344 if not repo:
345 345 raise HTTPNotFound
346 346 filter_query = request.GET.get('query')
347 347
348 348 query = Repository.query() \
349 349 .order_by(func.length(Repository.repo_name)) \
350 350 .filter(or_(
351 351 Repository.repo_name == repo.repo_name,
352 352 Repository.fork_id == repo.repo_id))
353 353
354 354 if filter_query:
355 355 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
356 356 query = query.filter(
357 357 Repository.repo_name.ilike(ilike_expression))
358 358
359 359 add_parent = False
360 360 if repo.parent:
361 361 if filter_query in repo.parent.repo_name:
362 362 if not repo.parent.scm_instance().is_empty():
363 363 add_parent = True
364 364
365 365 limit = 20 - 1 if add_parent else 20
366 366 all_repos = query.limit(limit).all()
367 367 if add_parent:
368 368 all_repos += [repo.parent]
369 369
370 370 repos = []
371 371 for obj in self.scm_model.get_repos(all_repos):
372 372 repos.append({
373 373 'id': obj['name'],
374 374 'text': obj['name'],
375 375 'type': 'repo',
376 376 'obj': obj['dbrepo']
377 377 })
378 378
379 379 data = {
380 380 'more': False,
381 381 'results': [{
382 382 'text': _('Repositories'),
383 383 'children': repos
384 384 }] if repos else []
385 385 }
386 386 return data
387 387
388 388 @LoginRequired()
389 389 @NotAnonymous()
390 390 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
391 391 'repository.admin')
392 392 @HasAcceptedRepoType('git', 'hg')
393 393 @auth.CSRFRequired()
394 394 def create(self, repo_name):
395 395 repo = Repository.get_by_repo_name(repo_name)
396 396 if not repo:
397 397 raise HTTPNotFound
398 398
399 399 try:
400 400 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
401 401 except formencode.Invalid as errors:
402 402 if errors.error_dict.get('revisions'):
403 403 msg = 'Revisions: %s' % errors.error_dict['revisions']
404 404 elif errors.error_dict.get('pullrequest_title'):
405 405 msg = _('Pull request requires a title with min. 3 chars')
406 406 else:
407 407 msg = _('Error creating pull request: {}').format(errors)
408 408 log.exception(msg)
409 409 h.flash(msg, 'error')
410 410
411 411 # would rather just go back to form ...
412 412 return redirect(url('pullrequest_home', repo_name=repo_name))
413 413
414 414 source_repo = _form['source_repo']
415 415 source_ref = _form['source_ref']
416 416 target_repo = _form['target_repo']
417 417 target_ref = _form['target_ref']
418 418 commit_ids = _form['revisions'][::-1]
419 419 reviewers = _form['review_members']
420 420
421 421 # find the ancestor for this pr
422 422 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
423 423 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
424 424
425 425 source_scm = source_db_repo.scm_instance()
426 426 target_scm = target_db_repo.scm_instance()
427 427
428 428 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
429 429 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
430 430
431 431 ancestor = source_scm.get_common_ancestor(
432 432 source_commit.raw_id, target_commit.raw_id, target_scm)
433 433
434 434 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
435 435 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
436 436
437 437 pullrequest_title = _form['pullrequest_title']
438 438 title_source_ref = source_ref.split(':', 2)[1]
439 439 if not pullrequest_title:
440 440 pullrequest_title = PullRequestModel().generate_pullrequest_title(
441 441 source=source_repo,
442 442 source_ref=title_source_ref,
443 443 target=target_repo
444 444 )
445 445
446 446 description = _form['pullrequest_desc']
447 447 try:
448 448 pull_request = PullRequestModel().create(
449 449 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
450 450 target_ref, commit_ids, reviewers, pullrequest_title,
451 451 description
452 452 )
453 453 Session().commit()
454 454 h.flash(_('Successfully opened new pull request'),
455 455 category='success')
456 456 except Exception as e:
457 457 msg = _('Error occurred during sending pull request')
458 458 log.exception(msg)
459 459 h.flash(msg, category='error')
460 460 return redirect(url('pullrequest_home', repo_name=repo_name))
461 461
462 462 return redirect(url('pullrequest_show', repo_name=target_repo,
463 463 pull_request_id=pull_request.pull_request_id))
464 464
465 465 @LoginRequired()
466 466 @NotAnonymous()
467 467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 468 'repository.admin')
469 469 @auth.CSRFRequired()
470 470 @jsonify
471 471 def update(self, repo_name, pull_request_id):
472 472 pull_request_id = safe_int(pull_request_id)
473 473 pull_request = PullRequest.get_or_404(pull_request_id)
474 474 # only owner or admin can update it
475 475 allowed_to_update = PullRequestModel().check_user_update(
476 476 pull_request, c.rhodecode_user)
477 477 if allowed_to_update:
478 478 if 'reviewers_ids' in request.POST:
479 479 self._update_reviewers(pull_request_id)
480 480 elif str2bool(request.POST.get('update_commits', 'false')):
481 481 self._update_commits(pull_request)
482 482 elif str2bool(request.POST.get('close_pull_request', 'false')):
483 483 self._reject_close(pull_request)
484 484 elif str2bool(request.POST.get('edit_pull_request', 'false')):
485 485 self._edit_pull_request(pull_request)
486 486 else:
487 487 raise HTTPBadRequest()
488 488 return True
489 489 raise HTTPForbidden()
490 490
491 491 def _edit_pull_request(self, pull_request):
492 492 try:
493 493 PullRequestModel().edit(
494 494 pull_request, request.POST.get('title'),
495 495 request.POST.get('description'))
496 496 except ValueError:
497 497 msg = _(u'Cannot update closed pull requests.')
498 498 h.flash(msg, category='error')
499 499 return
500 500 else:
501 501 Session().commit()
502 502
503 503 msg = _(u'Pull request title & description updated.')
504 504 h.flash(msg, category='success')
505 505 return
506 506
507 507 def _update_commits(self, pull_request):
508 508 try:
509 509 if PullRequestModel().has_valid_update_type(pull_request):
510 510 updated_version, changes = PullRequestModel().update_commits(
511 511 pull_request)
512 512 if updated_version:
513 513 msg = _(
514 514 u'Pull request updated to "{source_commit_id}" with '
515 515 u'{count_added} added, {count_removed} removed '
516 516 u'commits.'
517 517 ).format(
518 518 source_commit_id=pull_request.source_ref_parts.commit_id,
519 519 count_added=len(changes.added),
520 520 count_removed=len(changes.removed))
521 521 h.flash(msg, category='success')
522 522 else:
523 523 h.flash(_("Nothing changed in pull request."),
524 524 category='warning')
525 525 else:
526 526 msg = _(
527 527 u"Skipping update of pull request due to reference "
528 528 u"type: {reference_type}"
529 529 ).format(reference_type=pull_request.source_ref_parts.type)
530 530 h.flash(msg, category='warning')
531 531 except CommitDoesNotExistError:
532 532 h.flash(
533 533 _(u'Update failed due to missing commits.'), category='error')
534 534
535 535 @auth.CSRFRequired()
536 536 @LoginRequired()
537 537 @NotAnonymous()
538 538 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
539 539 'repository.admin')
540 540 def merge(self, repo_name, pull_request_id):
541 541 """
542 542 POST /{repo_name}/pull-request/{pull_request_id}
543 543
544 544 Merge will perform a server-side merge of the specified
545 545 pull request, if the pull request is approved and mergeable.
546 546 After succesfull merging, the pull request is automatically
547 547 closed, with a relevant comment.
548 548 """
549 549 pull_request_id = safe_int(pull_request_id)
550 550 pull_request = PullRequest.get_or_404(pull_request_id)
551 551 user = c.rhodecode_user
552 552
553 553 if self._meets_merge_pre_conditions(pull_request, user):
554 554 log.debug("Pre-conditions checked, trying to merge.")
555 555 extras = vcs_operation_context(
556 556 request.environ, repo_name=pull_request.target_repo.repo_name,
557 557 username=user.username, action='push',
558 558 scm=pull_request.target_repo.repo_type)
559 559 self._merge_pull_request(pull_request, user, extras)
560 560
561 561 return redirect(url(
562 562 'pullrequest_show',
563 563 repo_name=pull_request.target_repo.repo_name,
564 564 pull_request_id=pull_request.pull_request_id))
565 565
566 566 def _meets_merge_pre_conditions(self, pull_request, user):
567 567 if not PullRequestModel().check_user_merge(pull_request, user):
568 568 raise HTTPForbidden()
569 569
570 570 merge_status, msg = PullRequestModel().merge_status(pull_request)
571 571 if not merge_status:
572 572 log.debug("Cannot merge, not mergeable.")
573 573 h.flash(msg, category='error')
574 574 return False
575 575
576 576 if (pull_request.calculated_review_status()
577 577 is not ChangesetStatus.STATUS_APPROVED):
578 578 log.debug("Cannot merge, approval is pending.")
579 579 msg = _('Pull request reviewer approval is pending.')
580 580 h.flash(msg, category='error')
581 581 return False
582 582 return True
583 583
584 584 def _merge_pull_request(self, pull_request, user, extras):
585 585 merge_resp = PullRequestModel().merge(
586 586 pull_request, user, extras=extras)
587 587
588 588 if merge_resp.executed:
589 589 log.debug("The merge was successful, closing the pull request.")
590 590 PullRequestModel().close_pull_request(
591 591 pull_request.pull_request_id, user)
592 592 Session().commit()
593 593 msg = _('Pull request was successfully merged and closed.')
594 594 h.flash(msg, category='success')
595 595 else:
596 596 log.debug(
597 597 "The merge was not successful. Merge response: %s",
598 598 merge_resp)
599 599 msg = PullRequestModel().merge_status_message(
600 600 merge_resp.failure_reason)
601 601 h.flash(msg, category='error')
602 602
603 603 def _update_reviewers(self, pull_request_id):
604 604 reviewers_ids = map(int, filter(
605 605 lambda v: v not in [None, ''],
606 606 request.POST.get('reviewers_ids', '').split(',')))
607 607 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
608 608 Session().commit()
609 609
610 610 def _reject_close(self, pull_request):
611 611 if pull_request.is_closed():
612 612 raise HTTPForbidden()
613 613
614 614 PullRequestModel().close_pull_request_with_comment(
615 615 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
616 616 Session().commit()
617 617
618 618 @LoginRequired()
619 619 @NotAnonymous()
620 620 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
621 621 'repository.admin')
622 622 @auth.CSRFRequired()
623 623 @jsonify
624 624 def delete(self, repo_name, pull_request_id):
625 625 pull_request_id = safe_int(pull_request_id)
626 626 pull_request = PullRequest.get_or_404(pull_request_id)
627 627 # only owner can delete it !
628 628 if pull_request.author.user_id == c.rhodecode_user.user_id:
629 629 PullRequestModel().delete(pull_request)
630 630 Session().commit()
631 631 h.flash(_('Successfully deleted pull request'),
632 632 category='success')
633 633 return redirect(url('my_account_pullrequests'))
634 634 raise HTTPForbidden()
635 635
636 636 @LoginRequired()
637 637 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
638 638 'repository.admin')
639 639 def show(self, repo_name, pull_request_id):
640 640 pull_request_id = safe_int(pull_request_id)
641 641 c.pull_request = PullRequest.get_or_404(pull_request_id)
642 642
643 if hasattr(c, 'pylons_dispatch_info'):
644 c.pylons_dispatch_info['extra']['pull_request'] = pull_request_id
643 c.template_context['pull_request_data']['pull_request_id'] = \
644 pull_request_id
645 645
646 646 # pull_requests repo_name we opened it against
647 647 # ie. target_repo must match
648 648 if repo_name != c.pull_request.target_repo.repo_name:
649 649 raise HTTPNotFound
650 650
651 651 c.allowed_to_change_status = PullRequestModel(). \
652 652 check_user_change_status(c.pull_request, c.rhodecode_user)
653 653 c.allowed_to_update = PullRequestModel().check_user_update(
654 654 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
655 655 c.allowed_to_merge = PullRequestModel().check_user_merge(
656 656 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
657 657
658 658 cc_model = ChangesetCommentsModel()
659 659
660 660 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
661 661
662 662 c.pull_request_review_status = c.pull_request.calculated_review_status()
663 663 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
664 664 c.pull_request)
665 665 c.approval_msg = None
666 666 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
667 667 c.approval_msg = _('Reviewer approval is pending.')
668 668 c.pr_merge_status = False
669 669 # load compare data into template context
670 670 enable_comments = not c.pull_request.is_closed()
671 671 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
672 672
673 673 # this is a hack to properly display links, when creating PR, the
674 674 # compare view and others uses different notation, and
675 675 # compare_commits.html renders links based on the target_repo.
676 676 # We need to swap that here to generate it properly on the html side
677 677 c.target_repo = c.source_repo
678 678
679 679 # inline comments
680 680 c.inline_cnt = 0
681 681 c.inline_comments = cc_model.get_inline_comments(
682 682 c.rhodecode_db_repo.repo_id,
683 683 pull_request=pull_request_id).items()
684 684 # count inline comments
685 685 for __, lines in c.inline_comments:
686 686 for comments in lines.values():
687 687 c.inline_cnt += len(comments)
688 688
689 689 # outdated comments
690 690 c.outdated_cnt = 0
691 691 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
692 692 c.outdated_comments = cc_model.get_outdated_comments(
693 693 c.rhodecode_db_repo.repo_id,
694 694 pull_request=c.pull_request)
695 695 # Count outdated comments and check for deleted files
696 696 for file_name, lines in c.outdated_comments.iteritems():
697 697 for comments in lines.values():
698 698 c.outdated_cnt += len(comments)
699 699 if file_name not in c.included_files:
700 700 c.deleted_files.append(file_name)
701 701 else:
702 702 c.outdated_comments = {}
703 703
704 704 # comments
705 705 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
706 706 pull_request=pull_request_id)
707 707
708 708 if c.allowed_to_update:
709 709 force_close = ('forced_closed', _('Close Pull Request'))
710 710 statuses = ChangesetStatus.STATUSES + [force_close]
711 711 else:
712 712 statuses = ChangesetStatus.STATUSES
713 713 c.commit_statuses = statuses
714 714
715 715 c.ancestor = None # TODO: add ancestor here
716 716
717 717 return render('/pullrequests/pullrequest_show.html')
718 718
719 719 @LoginRequired()
720 720 @NotAnonymous()
721 721 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
722 722 'repository.admin')
723 723 @auth.CSRFRequired()
724 724 @jsonify
725 725 def comment(self, repo_name, pull_request_id):
726 726 pull_request_id = safe_int(pull_request_id)
727 727 pull_request = PullRequest.get_or_404(pull_request_id)
728 728 if pull_request.is_closed():
729 729 raise HTTPForbidden()
730 730
731 731 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
732 732 # as a changeset status, still we want to send it in one value.
733 733 status = request.POST.get('changeset_status', None)
734 734 text = request.POST.get('text')
735 735 if status and '_closed' in status:
736 736 close_pr = True
737 737 status = status.replace('_closed', '')
738 738 else:
739 739 close_pr = False
740 740
741 741 forced = (status == 'forced')
742 742 if forced:
743 743 status = 'rejected'
744 744
745 745 allowed_to_change_status = PullRequestModel().check_user_change_status(
746 746 pull_request, c.rhodecode_user)
747 747
748 748 if status and allowed_to_change_status:
749 749 message = (_('Status change %(transition_icon)s %(status)s')
750 750 % {'transition_icon': '>',
751 751 'status': ChangesetStatus.get_status_lbl(status)})
752 752 if close_pr:
753 753 message = _('Closing with') + ' ' + message
754 754 text = text or message
755 755 comm = ChangesetCommentsModel().create(
756 756 text=text,
757 757 repo=c.rhodecode_db_repo.repo_id,
758 758 user=c.rhodecode_user.user_id,
759 759 pull_request=pull_request_id,
760 760 f_path=request.POST.get('f_path'),
761 761 line_no=request.POST.get('line'),
762 762 status_change=(ChangesetStatus.get_status_lbl(status)
763 763 if status and allowed_to_change_status else None),
764 764 closing_pr=close_pr
765 765 )
766 766
767 767 if allowed_to_change_status:
768 768 old_calculated_status = pull_request.calculated_review_status()
769 769 # get status if set !
770 770 if status:
771 771 ChangesetStatusModel().set_status(
772 772 c.rhodecode_db_repo.repo_id,
773 773 status,
774 774 c.rhodecode_user.user_id,
775 775 comm,
776 776 pull_request=pull_request_id
777 777 )
778 778
779 779 Session().flush()
780 780 # we now calculate the status of pull request, and based on that
781 781 # calculation we set the commits status
782 782 calculated_status = pull_request.calculated_review_status()
783 783 if old_calculated_status != calculated_status:
784 784 PullRequestModel()._trigger_pull_request_hook(
785 785 pull_request, c.rhodecode_user, 'review_status_change')
786 786
787 787 calculated_status_lbl = ChangesetStatus.get_status_lbl(
788 788 calculated_status)
789 789
790 790 if close_pr:
791 791 status_completed = (
792 792 calculated_status in [ChangesetStatus.STATUS_APPROVED,
793 793 ChangesetStatus.STATUS_REJECTED])
794 794 if forced or status_completed:
795 795 PullRequestModel().close_pull_request(
796 796 pull_request_id, c.rhodecode_user)
797 797 else:
798 798 h.flash(_('Closing pull request on other statuses than '
799 799 'rejected or approved is forbidden. '
800 800 'Calculated status from all reviewers '
801 801 'is currently: %s') % calculated_status_lbl,
802 802 category='warning')
803 803
804 804 Session().commit()
805 805
806 806 if not request.is_xhr:
807 807 return redirect(h.url('pullrequest_show', repo_name=repo_name,
808 808 pull_request_id=pull_request_id))
809 809
810 810 data = {
811 811 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
812 812 }
813 813 if comm:
814 814 c.co = comm
815 815 data.update(comm.get_dict())
816 816 data.update({'rendered_text':
817 817 render('changeset/changeset_comment_block.html')})
818 818
819 819 return data
820 820
821 821 @LoginRequired()
822 822 @NotAnonymous()
823 823 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
824 824 'repository.admin')
825 825 @auth.CSRFRequired()
826 826 @jsonify
827 827 def delete_comment(self, repo_name, comment_id):
828 828 return self._delete_comment(comment_id)
829 829
830 830 def _delete_comment(self, comment_id):
831 831 comment_id = safe_int(comment_id)
832 832 co = ChangesetComment.get_or_404(comment_id)
833 833 if co.pull_request.is_closed():
834 834 # don't allow deleting comments on closed pull request
835 835 raise HTTPForbidden()
836 836
837 837 is_owner = co.author.user_id == c.rhodecode_user.user_id
838 838 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
839 839 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
840 840 old_calculated_status = co.pull_request.calculated_review_status()
841 841 ChangesetCommentsModel().delete(comment=co)
842 842 Session().commit()
843 843 calculated_status = co.pull_request.calculated_review_status()
844 844 if old_calculated_status != calculated_status:
845 845 PullRequestModel()._trigger_pull_request_hook(
846 846 co.pull_request, c.rhodecode_user, 'review_status_change')
847 847 return True
848 848 else:
849 849 raise HTTPForbidden()
@@ -1,557 +1,577 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31
32 32 from paste.auth.basic import AuthBasicAuthenticator
33 33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 35 from pylons import config, tmpl_context as c, request, session, url
36 36 from pylons.controllers import WSGIController
37 37 from pylons.controllers.util import redirect
38 38 from pylons.i18n import translation
39 39 # marcink: don't remove this import
40 40 from pylons.templating import render_mako as render # noqa
41 41 from pylons.i18n.translation import _
42 42 from webob.exc import HTTPFound
43 43
44 44
45 45 import rhodecode
46 46 from rhodecode.authentication.base import VCS_TYPE
47 47 from rhodecode.lib import auth, utils2
48 48 from rhodecode.lib import helpers as h
49 49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 50 from rhodecode.lib.exceptions import UserCreationError
51 51 from rhodecode.lib.utils import (
52 52 get_repo_slug, set_rhodecode_config, password_changed,
53 53 get_enabled_hook_classes)
54 54 from rhodecode.lib.utils2 import (
55 55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 57 from rhodecode.model import meta
58 58 from rhodecode.model.db import Repository, User
59 59 from rhodecode.model.notification import NotificationModel
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 62
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 def _filter_proxy(ip):
68 68 """
69 69 Passed in IP addresses in HEADERS can be in a special format of multiple
70 70 ips. Those comma separated IPs are passed from various proxies in the
71 71 chain of request processing. The left-most being the original client.
72 72 We only care about the first IP which came from the org. client.
73 73
74 74 :param ip: ip string from headers
75 75 """
76 76 if ',' in ip:
77 77 _ips = ip.split(',')
78 78 _first_ip = _ips[0].strip()
79 79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 80 return _first_ip
81 81 return ip
82 82
83 83
84 84 def _filter_port(ip):
85 85 """
86 86 Removes a port from ip, there are 4 main cases to handle here.
87 87 - ipv4 eg. 127.0.0.1
88 88 - ipv6 eg. ::1
89 89 - ipv4+port eg. 127.0.0.1:8080
90 90 - ipv6+port eg. [::1]:8080
91 91
92 92 :param ip:
93 93 """
94 94 def is_ipv6(ip_addr):
95 95 if hasattr(socket, 'inet_pton'):
96 96 try:
97 97 socket.inet_pton(socket.AF_INET6, ip_addr)
98 98 except socket.error:
99 99 return False
100 100 else:
101 101 # fallback to ipaddress
102 102 try:
103 103 ipaddress.IPv6Address(ip_addr)
104 104 except Exception:
105 105 return False
106 106 return True
107 107
108 108 if ':' not in ip: # must be ipv4 pure ip
109 109 return ip
110 110
111 111 if '[' in ip and ']' in ip: # ipv6 with port
112 112 return ip.split(']')[0][1:].lower()
113 113
114 114 # must be ipv6 or ipv4 with port
115 115 if is_ipv6(ip):
116 116 return ip
117 117 else:
118 118 ip, _port = ip.split(':')[:2] # means ipv4+port
119 119 return ip
120 120
121 121
122 122 def get_ip_addr(environ):
123 123 proxy_key = 'HTTP_X_REAL_IP'
124 124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 125 def_key = 'REMOTE_ADDR'
126 126 _filters = lambda x: _filter_port(_filter_proxy(x))
127 127
128 128 ip = environ.get(proxy_key)
129 129 if ip:
130 130 return _filters(ip)
131 131
132 132 ip = environ.get(proxy_key2)
133 133 if ip:
134 134 return _filters(ip)
135 135
136 136 ip = environ.get(def_key, '0.0.0.0')
137 137 return _filters(ip)
138 138
139 139
140 140 def get_server_ip_addr(environ, log_errors=True):
141 141 hostname = environ.get('SERVER_NAME')
142 142 try:
143 143 return socket.gethostbyname(hostname)
144 144 except Exception as e:
145 145 if log_errors:
146 146 # in some cases this lookup is not possible, and we don't want to
147 147 # make it an exception in logs
148 148 log.exception('Could not retrieve server ip address: %s', e)
149 149 return hostname
150 150
151 151
152 152 def get_server_port(environ):
153 153 return environ.get('SERVER_PORT')
154 154
155 155
156 156 def get_access_path(environ):
157 157 path = environ.get('PATH_INFO')
158 158 org_req = environ.get('pylons.original_request')
159 159 if org_req:
160 160 path = org_req.environ.get('PATH_INFO')
161 161 return path
162 162
163 163
164 164 def vcs_operation_context(
165 165 environ, repo_name, username, action, scm, check_locking=True):
166 166 """
167 167 Generate the context for a vcs operation, e.g. push or pull.
168 168
169 169 This context is passed over the layers so that hooks triggered by the
170 170 vcs operation know details like the user, the user's IP address etc.
171 171
172 172 :param check_locking: Allows to switch of the computation of the locking
173 173 data. This serves mainly the need of the simplevcs middleware to be
174 174 able to disable this for certain operations.
175 175
176 176 """
177 177 # Tri-state value: False: unlock, None: nothing, True: lock
178 178 make_lock = None
179 179 locked_by = [None, None, None]
180 180 is_anonymous = username == User.DEFAULT_USER
181 181 if not is_anonymous and check_locking:
182 182 log.debug('Checking locking on repository "%s"', repo_name)
183 183 user = User.get_by_username(username)
184 184 repo = Repository.get_by_repo_name(repo_name)
185 185 make_lock, __, locked_by = repo.get_locking_state(
186 186 action, user.user_id)
187 187
188 188 settings_model = VcsSettingsModel(repo=repo_name)
189 189 ui_settings = settings_model.get_ui_settings()
190 190
191 191 extras = {
192 192 'ip': get_ip_addr(environ),
193 193 'username': username,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'make_lock': make_lock,
199 199 'locked_by': locked_by,
200 200 'server_url': utils2.get_server_url(environ),
201 201 'hooks': get_enabled_hook_classes(ui_settings),
202 202 }
203 203 return extras
204 204
205 205
206 206 class BasicAuth(AuthBasicAuthenticator):
207 207
208 208 def __init__(self, realm, authfunc, auth_http_code=None,
209 209 initial_call_detection=False):
210 210 self.realm = realm
211 211 self.initial_call = initial_call_detection
212 212 self.authfunc = authfunc
213 213 self._rc_auth_http_code = auth_http_code
214 214
215 215 def _get_response_from_code(self, http_code):
216 216 try:
217 217 return get_exception(safe_int(http_code))
218 218 except Exception:
219 219 log.exception('Failed to fetch response for code %s' % http_code)
220 220 return HTTPForbidden
221 221
222 222 def build_authentication(self):
223 223 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
224 224 if self._rc_auth_http_code and not self.initial_call:
225 225 # return alternative HTTP code if alternative http return code
226 226 # is specified in RhodeCode config, but ONLY if it's not the
227 227 # FIRST call
228 228 custom_response_klass = self._get_response_from_code(
229 229 self._rc_auth_http_code)
230 230 return custom_response_klass(headers=head)
231 231 return HTTPUnauthorized(headers=head)
232 232
233 233 def authenticate(self, environ):
234 234 authorization = AUTHORIZATION(environ)
235 235 if not authorization:
236 236 return self.build_authentication()
237 237 (authmeth, auth) = authorization.split(' ', 1)
238 238 if 'basic' != authmeth.lower():
239 239 return self.build_authentication()
240 240 auth = auth.strip().decode('base64')
241 241 _parts = auth.split(':', 1)
242 242 if len(_parts) == 2:
243 243 username, password = _parts
244 244 if self.authfunc(
245 245 username, password, environ, VCS_TYPE):
246 246 return username
247 247 if username and password:
248 248 # we mark that we actually executed authentication once, at
249 249 # that point we can use the alternative auth code
250 250 self.initial_call = False
251 251
252 252 return self.build_authentication()
253 253
254 254 __call__ = authenticate
255 255
256 256
257 257 def attach_context_attributes(context):
258 258 rc_config = SettingsModel().get_all_settings(cache=True)
259 259
260 260 context.rhodecode_version = rhodecode.__version__
261 261 context.rhodecode_edition = config.get('rhodecode.edition')
262 262 # unique secret + version does not leak the version but keep consistency
263 263 context.rhodecode_version_hash = md5(
264 264 config.get('beaker.session.secret', '') +
265 265 rhodecode.__version__)[:8]
266 266
267 267 # Default language set for the incoming request
268 268 context.language = translation.get_lang()[0]
269 269
270 270 # Visual options
271 271 context.visual = AttributeDict({})
272 272
273 273 # DB store
274 274 context.visual.show_public_icon = str2bool(
275 275 rc_config.get('rhodecode_show_public_icon'))
276 276 context.visual.show_private_icon = str2bool(
277 277 rc_config.get('rhodecode_show_private_icon'))
278 278 context.visual.stylify_metatags = str2bool(
279 279 rc_config.get('rhodecode_stylify_metatags'))
280 280 context.visual.dashboard_items = safe_int(
281 281 rc_config.get('rhodecode_dashboard_items', 100))
282 282 context.visual.admin_grid_items = safe_int(
283 283 rc_config.get('rhodecode_admin_grid_items', 100))
284 284 context.visual.repository_fields = str2bool(
285 285 rc_config.get('rhodecode_repository_fields'))
286 286 context.visual.show_version = str2bool(
287 287 rc_config.get('rhodecode_show_version'))
288 288 context.visual.use_gravatar = str2bool(
289 289 rc_config.get('rhodecode_use_gravatar'))
290 290 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
291 291 context.visual.default_renderer = rc_config.get(
292 292 'rhodecode_markup_renderer', 'rst')
293 293 context.visual.rhodecode_support_url = \
294 294 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
295 295
296 296 context.pre_code = rc_config.get('rhodecode_pre_code')
297 297 context.post_code = rc_config.get('rhodecode_post_code')
298 298 context.rhodecode_name = rc_config.get('rhodecode_title')
299 299 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
300 300 # if we have specified default_encoding in the request, it has more
301 301 # priority
302 302 if request.GET.get('default_encoding'):
303 303 context.default_encodings.insert(0, request.GET.get('default_encoding'))
304 304 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
305 305
306 306 # INI stored
307 307 context.labs_active = str2bool(
308 308 config.get('labs_settings_active', 'false'))
309 309 context.visual.allow_repo_location_change = str2bool(
310 310 config.get('allow_repo_location_change', True))
311 311 context.visual.allow_custom_hooks_settings = str2bool(
312 312 config.get('allow_custom_hooks_settings', True))
313 313 context.debug_style = str2bool(config.get('debug_style', False))
314 314
315 315 context.rhodecode_instanceid = config.get('instance_id')
316 316
317 317 # AppEnlight
318 318 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
319 319 context.appenlight_api_public_key = config.get(
320 320 'appenlight.api_public_key', '')
321 321 context.appenlight_server_url = config.get('appenlight.server_url', '')
322 322
323 323 # END CONFIG VARS
324 324
325 325 # TODO: This dosn't work when called from pylons compatibility tween.
326 326 # Fix this and remove it from base controller.
327 327 # context.repo_name = get_repo_slug(request) # can be empty
328 328
329 329 context.csrf_token = auth.get_csrf_token()
330 330 context.backends = rhodecode.BACKENDS.keys()
331 331 context.backends.sort()
332 332 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
333 333 context.rhodecode_user.user_id)
334 334
335 335
336 336 def get_auth_user(environ):
337 337 ip_addr = get_ip_addr(environ)
338 338 # make sure that we update permissions each time we call controller
339 339 _auth_token = (request.GET.get('auth_token', '') or
340 340 request.GET.get('api_key', ''))
341 341
342 342 if _auth_token:
343 343 # when using API_KEY we are sure user exists.
344 344 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
345 345 authenticated = False
346 346 else:
347 347 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
348 348 try:
349 349 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
350 350 ip_addr=ip_addr)
351 351 except UserCreationError as e:
352 352 h.flash(e, 'error')
353 353 # container auth or other auth functions that create users
354 354 # on the fly can throw this exception signaling that there's
355 355 # issue with user creation, explanation should be provided
356 356 # in Exception itself. We then create a simple blank
357 357 # AuthUser
358 358 auth_user = AuthUser(ip_addr=ip_addr)
359 359
360 360 if password_changed(auth_user, session):
361 361 session.invalidate()
362 362 cookie_store = CookieStoreWrapper(
363 363 session.get('rhodecode_user'))
364 364 auth_user = AuthUser(ip_addr=ip_addr)
365 365
366 366 authenticated = cookie_store.get('is_authenticated')
367 367
368 368 if not auth_user.is_authenticated and auth_user.is_user_object:
369 369 # user is not authenticated and not empty
370 370 auth_user.set_authenticated(authenticated)
371 371
372 372 return auth_user
373 373
374 374
375 375 class BaseController(WSGIController):
376 376
377 377 def __before__(self):
378 378 """
379 379 __before__ is called before controller methods and after __call__
380 380 """
381 381 # on each call propagate settings calls into global settings.
382 382 set_rhodecode_config(config)
383 383 attach_context_attributes(c)
384 384
385 385 # TODO: Remove this when fixed in attach_context_attributes()
386 386 c.repo_name = get_repo_slug(request) # can be empty
387 387
388 388 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
389 389 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
390 390 self.sa = meta.Session
391 391 self.scm_model = ScmModel(self.sa)
392 392
393 393 default_lang = c.language
394 394 user_lang = c.language
395 395 try:
396 396 user_obj = self._rhodecode_user.get_instance()
397 397 if user_obj:
398 398 user_lang = user_obj.user_data.get('language')
399 399 except Exception:
400 400 log.exception('Failed to fetch user language for user %s',
401 401 self._rhodecode_user)
402 402
403 403 if user_lang and user_lang != default_lang:
404 404 log.debug('set language to %s for user %s', user_lang,
405 405 self._rhodecode_user)
406 406 translation.set_lang(user_lang)
407 407
408 408 def _dispatch_redirect(self, with_url, environ, start_response):
409 409 resp = HTTPFound(with_url)
410 410 environ['SCRIPT_NAME'] = '' # handle prefix middleware
411 411 environ['PATH_INFO'] = with_url
412 412 return resp(environ, start_response)
413 413
414 414 def __call__(self, environ, start_response):
415 415 """Invoke the Controller"""
416 416 # WSGIController.__call__ dispatches to the Controller method
417 417 # the request is routed to. This routing information is
418 418 # available in environ['pylons.routes_dict']
419 419 from rhodecode.lib import helpers as h
420 420
421 421 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
422 422 if environ.get('debugtoolbar.wants_pylons_context', False):
423 423 environ['debugtoolbar.pylons_context'] = c._current_obj()
424 424
425 425 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
426 426 environ['pylons.routes_dict']['action']])
427 427
428 c.pylons_dispatch_info = {
429 'controller': environ['pylons.routes_dict']['controller'],
430 'action': environ['pylons.routes_dict']['action'],
428 c.template_context = {
429 'repo_name': None,
430 'repo_type': None,
431 'repo_landing_commit': None,
432 'rhodecode_user': {
433 'username': None,
434 'email': None,
435 },
436 'visual': {
437 'default_renderer': None
438 },
439 'commit_data': {
440 'commit_id': None
441 },
442 'pull_request_data': {'pull_request_id': None},
443 'timeago': {
444 'refresh_time': 120 * 1000,
445 'cutoff_limit': 1000*60*60*24*7
446 },
447 'pylons_dispatch':{
448 'controller': environ['pylons.routes_dict']['controller'],
449 'action': environ['pylons.routes_dict']['action'],
450 },
431 451 'extra': {'plugins': {}}
432 452 }
433 453
434 454 self.rc_config = SettingsModel().get_all_settings(cache=True)
435 455 self.ip_addr = get_ip_addr(environ)
436 456
437 457 # The rhodecode auth user is looked up and passed through the
438 458 # environ by the pylons compatibility tween in pyramid.
439 459 # So we can just grab it from there.
440 460 auth_user = environ['rc_auth_user']
441 461
442 462 # set globals for auth user
443 463 request.user = auth_user
444 464 c.rhodecode_user = self._rhodecode_user = auth_user
445 465
446 466 log.info('IP: %s User: %s accessed %s [%s]' % (
447 467 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
448 468 _route_name)
449 469 )
450 470
451 471 # TODO: Maybe this should be move to pyramid to cover all views.
452 472 # check user attributes for password change flag
453 473 user_obj = auth_user.get_instance()
454 474 if user_obj and user_obj.user_data.get('force_password_change'):
455 475 h.flash('You are required to change your password', 'warning',
456 476 ignore_duplicate=True)
457 477
458 478 skip_user_check_urls = [
459 479 'error.document', 'login.logout', 'login.index',
460 480 'admin/my_account.my_account_password',
461 481 'admin/my_account.my_account_password_update'
462 482 ]
463 483 if _route_name not in skip_user_check_urls:
464 484 return self._dispatch_redirect(
465 485 url('my_account_password'), environ, start_response)
466 486
467 487 return WSGIController.__call__(self, environ, start_response)
468 488
469 489
470 490 class BaseRepoController(BaseController):
471 491 """
472 492 Base class for controllers responsible for loading all needed data for
473 493 repository loaded items are
474 494
475 495 c.rhodecode_repo: instance of scm repository
476 496 c.rhodecode_db_repo: instance of db
477 497 c.repository_requirements_missing: shows that repository specific data
478 498 could not be displayed due to the missing requirements
479 499 c.repository_pull_requests: show number of open pull requests
480 500 """
481 501
482 502 def __before__(self):
483 503 super(BaseRepoController, self).__before__()
484 504 if c.repo_name: # extracted from routes
485 505 db_repo = Repository.get_by_repo_name(c.repo_name)
486 506 if not db_repo:
487 507 return
488 508
489 509 log.debug(
490 510 'Found repository in database %s with state `%s`',
491 511 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
492 512 route = getattr(request.environ.get('routes.route'), 'name', '')
493 513
494 514 # allow to delete repos that are somehow damages in filesystem
495 515 if route in ['delete_repo']:
496 516 return
497 517
498 518 if db_repo.repo_state in [Repository.STATE_PENDING]:
499 519 if route in ['repo_creating_home']:
500 520 return
501 521 check_url = url('repo_creating_home', repo_name=c.repo_name)
502 522 return redirect(check_url)
503 523
504 524 self.rhodecode_db_repo = db_repo
505 525
506 526 missing_requirements = False
507 527 try:
508 528 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
509 529 except RepositoryRequirementError as e:
510 530 missing_requirements = True
511 531 self._handle_missing_requirements(e)
512 532
513 533 if self.rhodecode_repo is None and not missing_requirements:
514 534 log.error('%s this repository is present in database but it '
515 535 'cannot be created as an scm instance', c.repo_name)
516 536
517 537 h.flash(_(
518 538 "The repository at %(repo_name)s cannot be located.") %
519 539 {'repo_name': c.repo_name},
520 540 category='error', ignore_duplicate=True)
521 541 redirect(url('home'))
522 542
523 543 # update last change according to VCS data
524 544 if not missing_requirements:
525 545 commit = db_repo.get_commit(
526 546 pre_load=["author", "date", "message", "parents"])
527 547 db_repo.update_commit_cache(commit)
528 548
529 549 # Prepare context
530 550 c.rhodecode_db_repo = db_repo
531 551 c.rhodecode_repo = self.rhodecode_repo
532 552 c.repository_requirements_missing = missing_requirements
533 553
534 554 self._update_global_counters(self.scm_model, db_repo)
535 555
536 556 def _update_global_counters(self, scm_model, db_repo):
537 557 """
538 558 Base variables that are exposed to every page of repository
539 559 """
540 560 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
541 561
542 562 def _handle_missing_requirements(self, error):
543 563 self.rhodecode_repo = None
544 564 log.error(
545 565 'Requirements are missing for repository %s: %s',
546 566 c.repo_name, error.message)
547 567
548 568 summary_url = url('summary_home', repo_name=c.repo_name)
549 569 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
550 570 settings_update_url = url('repo', repo_name=c.repo_name)
551 571 path = request.path
552 572 should_redirect = (
553 573 path not in (summary_url, settings_update_url)
554 574 and '/settings' not in path or path == statistics_url
555 575 )
556 576 if should_redirect:
557 577 redirect(summary_url)
@@ -1,171 +1,135 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 <%def name="get_template_context()" filter="n, trim">{
5
6 ## repo data
7 repo_name: "${getattr(c, 'repo_name', '')}",
8 % if hasattr(c, 'rhodecode_db_repo'):
9 repo_type: "${c.rhodecode_db_repo.repo_type}",
10 repo_landing_commit: "${c.rhodecode_db_repo.landing_rev[1]}",
11 % else:
12 repo_type: null,
13 repo_landing_commit: null,
14 % endif
4 <%
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
15 6
16 ## user data
17 % if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 rhodecode_user: {
19 username: "${c.rhodecode_user.username}",
20 email: "${c.rhodecode_user.email}",
21 },
22 % else:
23 rhodecode_user: {
24 username: null,
25 email: null,
26 },
27 % endif
7 if hasattr(c, 'rhodecode_db_repo'):
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
28 10
29 ## visual settings
30 visual: {
31 default_renderer: "${h.get_visual_attr(c, 'default_renderer')}"
32 },
33
34 ## current commit context, filled inside templates that expose that
35 commit_data: {
36 commit_id: null,
37 },
11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
38 14
39 ## current pr context, filled inside templates that expose that
40 pull_request_data: {
41 pull_request_id: null,
42 },
43
44 ## timeago settings, can be overwritten by custom user settings later
45 timeago: {
46 refresh_time: ${120 * 1000},
47 cutoff_limit: ${1000*60*60*24*7}
48 },
49 dispatch_info: ${h.json.dumps(getattr(c, 'pylons_dispatch_info', {}))|n}
50 }
51
52 </%def>
15 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
16 %>
53 17
54 18 <html xmlns="http://www.w3.org/1999/xhtml">
55 19 <head>
56 20 <title>${self.title()}</title>
57 21 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
58 22 <%def name="robots()">
59 23 <meta name="robots" content="index, nofollow"/>
60 24 </%def>
61 25 ${self.robots()}
62 26 <link rel="icon" href="${h.url('/images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
63 27
64 28 ## CSS definitions
65 29 <%def name="css()">
66 30 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
67 31 <!--[if lt IE 9]>
68 32 <link rel="stylesheet" type="text/css" href="${h.url('/css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
69 33 <![endif]-->
70 34 ## EXTRA FOR CSS
71 35 ${self.css_extra()}
72 36 </%def>
73 37 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
74 38 <%def name="css_extra()">
75 39 </%def>
76 40
77 41 ${self.css()}
78 42
79 43 ## JAVASCRIPT
80 44 <%def name="js()">
81 45 <script src="${h.url('/js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
82 46 <script type="text/javascript">
83 47 // register templateContext to pass template variables to JS
84 var templateContext = ${get_template_context()};
48 var templateContext = ${h.json.dumps(c.template_context)|n};
85 49
86 50 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
87 51 %if hasattr(c, 'rhodecode_db_repo'):
88 52 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
89 53 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
90 54 %else:
91 55 var REPO_LANDING_REV = '';
92 56 var REPO_TYPE = '';
93 57 %endif
94 58 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
95 59 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
96 60 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
97 61 % if getattr(c, 'rhodecode_user', None):
98 62 var USER = {name:'${c.rhodecode_user.username}'};
99 63 % else:
100 64 var USER = {name:null};
101 65 % endif
102 66
103 67 var APPENLIGHT = {
104 68 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
105 69 key: '${getattr(c, "appenlight_api_public_key", "")}',
106 70 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
107 71 requestInfo: {
108 72 % if getattr(c, 'rhodecode_user', None):
109 73 ip: '${c.rhodecode_user.ip_addr}',
110 74 username: '${c.rhodecode_user.username}'
111 75 % endif
112 76 }
113 77 };
114 78 </script>
115 79
116 80 <!--[if lt IE 9]>
117 81 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
118 82 <![endif]-->
119 83 <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
120 84 <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
121 85 <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
122 86
123 87 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
124 88 ${self.js_extra()}
125 89
126 90 <script type="text/javascript">
127 91 $(document).ready(function(){
128 92 show_more_event();
129 93 timeagoActivate();
130 94 })
131 95 </script>
132 96
133 97 </%def>
134 98
135 99 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
136 100 <%def name="js_extra()"></%def>
137 101 ${self.js()}
138 102
139 103 <%def name="head_extra()"></%def>
140 104 ${self.head_extra()}
141 105
142 106 <%include file="/base/plugins_base.html"/>
143 107
144 108 ## extra stuff
145 109 %if c.pre_code:
146 110 ${c.pre_code|n}
147 111 %endif
148 112 </head>
149 113 <body id="body">
150 114 <noscript>
151 115 <div class="noscript-error">
152 116 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
153 117 </div>
154 118 </noscript>
155 119 ## IE hacks
156 120 <!--[if IE 7]>
157 121 <script>$(document.body).addClass('ie7')</script>
158 122 <![endif]-->
159 123 <!--[if IE 8]>
160 124 <script>$(document.body).addClass('ie8')</script>
161 125 <![endif]-->
162 126 <!--[if IE 9]>
163 127 <script>$(document.body).addClass('ie9')</script>
164 128 <![endif]-->
165 129
166 130 ${next.body()}
167 131 %if c.post_code:
168 132 ${c.post_code|n}
169 133 %endif
170 134 </body>
171 135 </html>
General Comments 0
You need to be logged in to leave comments. Login now