##// END OF EJS Templates
pull-request: introduced new merge-checks....
marcink -
r1334:68703a99 default
parent child Browse files
Show More
@@ -0,0 +1,37 b''
1
2 <div class="pull-request-wrap">
3
4 <ul>
5 % for pr_check_type, pr_check_msg in c.pr_merge_checks:
6 <li>
7 <span class="merge-message ${pr_check_type}" data-role="merge-message">
8 % if pr_check_type in ['success']:
9 <i class="icon-true"></i>
10 % else:
11 <i class="icon-false"></i>
12 % endif
13 ${pr_check_msg}
14 </span>
15 </li>
16 % endfor
17 </ul>
18
19 <div class="pull-request-merge-actions">
20 % if c.allowed_to_merge:
21 <div class="pull-right">
22 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
23 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
24 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
25 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
26 ${h.end_form()}
27 </div>
28 % elif c.rhodecode_user.username != h.DEFAULT_USER:
29 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
30 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
31 % else:
32 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
33 % endif
34 </div>
35
36 </div>
37
@@ -1,1018 +1,1046 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = CommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636
637 todos = CommentsModel().get_unresolved_todos(pull_request)
638 if todos:
639 log.debug("Cannot merge, unresolved todos left.")
640 if len(todos) == 1:
641 msg = _('Cannot merge, {} todo still not resolved.').format(
642 len(todos))
643 else:
644 msg = _('Cannot merge, {} todos still not resolved.').format(
645 len(todos))
646 h.flash(msg, category='error')
647 return False
636 648 return True
637 649
638 650 def _merge_pull_request(self, pull_request, user, extras):
639 651 merge_resp = PullRequestModel().merge(
640 652 pull_request, user, extras=extras)
641 653
642 654 if merge_resp.executed:
643 655 log.debug("The merge was successful, closing the pull request.")
644 656 PullRequestModel().close_pull_request(
645 657 pull_request.pull_request_id, user)
646 658 Session().commit()
647 659 msg = _('Pull request was successfully merged and closed.')
648 660 h.flash(msg, category='success')
649 661 else:
650 662 log.debug(
651 663 "The merge was not successful. Merge response: %s",
652 664 merge_resp)
653 665 msg = PullRequestModel().merge_status_message(
654 666 merge_resp.failure_reason)
655 667 h.flash(msg, category='error')
656 668
657 669 def _update_reviewers(self, pull_request_id, review_members):
658 670 reviewers = [
659 671 (int(r['user_id']), r['reasons']) for r in review_members]
660 672 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 673 Session().commit()
662 674
663 675 def _reject_close(self, pull_request):
664 676 if pull_request.is_closed():
665 677 raise HTTPForbidden()
666 678
667 679 PullRequestModel().close_pull_request_with_comment(
668 680 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 681 Session().commit()
670 682
671 683 @LoginRequired()
672 684 @NotAnonymous()
673 685 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 686 'repository.admin')
675 687 @auth.CSRFRequired()
676 688 @jsonify
677 689 def delete(self, repo_name, pull_request_id):
678 690 pull_request_id = safe_int(pull_request_id)
679 691 pull_request = PullRequest.get_or_404(pull_request_id)
680 692 # only owner can delete it !
681 693 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 694 PullRequestModel().delete(pull_request)
683 695 Session().commit()
684 696 h.flash(_('Successfully deleted pull request'),
685 697 category='success')
686 698 return redirect(url('my_account_pullrequests'))
687 699 raise HTTPForbidden()
688 700
689 701 def _get_pr_version(self, pull_request_id, version=None):
690 702 pull_request_id = safe_int(pull_request_id)
691 703 at_version = None
692 704
693 705 if version and version == 'latest':
694 706 pull_request_ver = PullRequest.get(pull_request_id)
695 707 pull_request_obj = pull_request_ver
696 708 _org_pull_request_obj = pull_request_obj
697 709 at_version = 'latest'
698 710 elif version:
699 711 pull_request_ver = PullRequestVersion.get_or_404(version)
700 712 pull_request_obj = pull_request_ver
701 713 _org_pull_request_obj = pull_request_ver.pull_request
702 714 at_version = pull_request_ver.pull_request_version_id
703 715 else:
704 716 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 717
706 718 pull_request_display_obj = PullRequest.get_pr_display_object(
707 719 pull_request_obj, _org_pull_request_obj)
708 720 return _org_pull_request_obj, pull_request_obj, \
709 721 pull_request_display_obj, at_version
710 722
711 723 def _get_pr_version_changes(self, version, pull_request_latest):
712 724 """
713 725 Generate changes commits, and diff data based on the current pr version
714 726 """
715 727
716 728 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 729
718 730 # fake the version to add the "initial" state object
719 731 pull_request_initial = PullRequest.get_pr_display_object(
720 732 pull_request_latest, pull_request_latest,
721 733 internal_methods=['get_commit', 'versions'])
722 734 pull_request_initial.revisions = []
723 735 pull_request_initial.source_repo.get_commit = types.MethodType(
724 736 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 737 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 738 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 739
728 740 _changes_versions = [pull_request_latest] + \
729 741 list(reversed(c.versions)) + \
730 742 [pull_request_initial]
731 743
732 744 if version == 'latest':
733 745 index = 0
734 746 else:
735 747 for pos, prver in enumerate(_changes_versions):
736 748 ver = getattr(prver, 'pull_request_version_id', -1)
737 749 if ver == safe_int(version):
738 750 index = pos
739 751 break
740 752 else:
741 753 index = 0
742 754
743 755 cur_obj = _changes_versions[index]
744 756 prev_obj = _changes_versions[index + 1]
745 757
746 758 old_commit_ids = set(prev_obj.revisions)
747 759 new_commit_ids = set(cur_obj.revisions)
748 760
749 761 changes = PullRequestModel()._calculate_commit_id_changes(
750 762 old_commit_ids, new_commit_ids)
751 763
752 764 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 765 cur_obj, prev_obj)
754 766 file_changes = PullRequestModel()._calculate_file_changes(
755 767 old_diff_data, new_diff_data)
756 768 return changes, file_changes
757 769
758 770 @LoginRequired()
759 771 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 772 'repository.admin')
761 773 def show(self, repo_name, pull_request_id):
762 774 pull_request_id = safe_int(pull_request_id)
763 775 version = request.GET.get('version')
776 merge_checks = request.GET.get('merge_checks')
764 777
765 778 (pull_request_latest,
766 779 pull_request_at_ver,
767 780 pull_request_display_obj,
768 781 at_version) = self._get_pr_version(pull_request_id, version=version)
769 782
770 783 c.template_context['pull_request_data']['pull_request_id'] = \
771 784 pull_request_id
772 785
773 786 # pull_requests repo_name we opened it against
774 787 # ie. target_repo must match
775 788 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 789 raise HTTPNotFound
777 790
778 791 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 792 pull_request_at_ver)
780 793
794 c.ancestor = None # TODO: add ancestor here
795 c.pull_request = pull_request_display_obj
796 c.pull_request_latest = pull_request_latest
797
781 798 pr_closed = pull_request_latest.is_closed()
782 799 if at_version and not at_version == 'latest':
783 800 c.allowed_to_change_status = False
784 801 c.allowed_to_update = False
785 802 c.allowed_to_merge = False
786 803 c.allowed_to_delete = False
787 804 c.allowed_to_comment = False
788 805 else:
789 806 c.allowed_to_change_status = PullRequestModel(). \
790 807 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 808 c.allowed_to_update = PullRequestModel().check_user_update(
792 809 pull_request_latest, c.rhodecode_user) and not pr_closed
793 810 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 811 pull_request_latest, c.rhodecode_user) and not pr_closed
795 812 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 813 pull_request_latest, c.rhodecode_user) and not pr_closed
797 814 c.allowed_to_comment = not pr_closed
798 815
799 816 cc_model = CommentsModel()
800 817
801 818 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 819 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
809 820
810 821 c.versions = pull_request_display_obj.versions()
811 822 c.at_version = at_version
812 823 c.at_version_num = at_version if at_version and at_version != 'latest' else None
813 824 c.at_version_pos = ChangesetComment.get_index_from_version(
814 825 c.at_version_num, c.versions)
815 826
816 827 # GENERAL COMMENTS with versions #
817 828 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
818 829 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
819 830
820 831 # pick comments we want to render at current version
821 832 c.comment_versions = cc_model.aggregate_comments(
822 833 general_comments, c.versions, c.at_version_num)
823 834 c.comments = c.comment_versions[c.at_version_num]['until']
824 835
825 836 # INLINE COMMENTS with versions #
826 837 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
827 838 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
828 839 c.inline_versions = cc_model.aggregate_comments(
829 840 inline_comments, c.versions, c.at_version_num, inline=True)
830 841
831 842 # if we use version, then do not show later comments
832 843 # than current version
833 paths = collections.defaultdict(lambda: collections.defaultdict(list))
844 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
834 845 for co in inline_comments:
835 846 if c.at_version_num:
836 847 # pick comments that are at least UPTO given version, so we
837 848 # don't render comments for higher version
838 849 should_render = co.pull_request_version_id and \
839 850 co.pull_request_version_id <= c.at_version_num
840 851 else:
841 852 # showing all, for 'latest'
842 853 should_render = True
843 854
844 855 if should_render:
845 paths[co.f_path][co.line_no].append(co)
846 inline_comments = paths
856 display_inline_comments[co.f_path][co.line_no].append(co)
857
858 c.pr_merge_checks = []
859 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
860 pull_request_at_ver)
861 c.pr_merge_checks.append(['warning' if not c.pr_merge_status else 'success', c.pr_merge_msg])
862
863 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
864 approval_msg = _('Reviewer approval is pending.')
865 c.pr_merge_status = False
866 c.pr_merge_checks.append(['warning', approval_msg])
867
868 todos = cc_model.get_unresolved_todos(pull_request_latest)
869 if todos:
870 c.pr_merge_status = False
871 if len(todos) == 1:
872 msg = _('{} todo still not resolved.').format(len(todos))
873 else:
874 msg = _('{} todos still not resolved.').format(len(todos))
875 c.pr_merge_checks.append(['warning', msg])
876
877 if merge_checks:
878 return render('/pullrequests/pullrequest_merge_checks.mako')
847 879
848 880 # load compare data into template context
849 self._load_compare_data(pull_request_at_ver, inline_comments)
881 self._load_compare_data(pull_request_at_ver, display_inline_comments)
850 882
851 883 # this is a hack to properly display links, when creating PR, the
852 884 # compare view and others uses different notation, and
853 885 # compare_commits.mako renders links based on the target_repo.
854 886 # We need to swap that here to generate it properly on the html side
855 887 c.target_repo = c.source_repo
856 888
857 889 if c.allowed_to_update:
858 890 force_close = ('forced_closed', _('Close Pull Request'))
859 891 statuses = ChangesetStatus.STATUSES + [force_close]
860 892 else:
861 893 statuses = ChangesetStatus.STATUSES
862 894 c.commit_statuses = statuses
863 895
864 c.ancestor = None # TODO: add ancestor here
865 c.pull_request = pull_request_display_obj
866 c.pull_request_latest = pull_request_latest
867
868 896 c.changes = None
869 897 c.file_changes = None
870 898
871 899 c.show_version_changes = 1 # control flag, not used yet
872 900
873 901 if at_version and c.show_version_changes:
874 902 c.changes, c.file_changes = self._get_pr_version_changes(
875 903 version, pull_request_latest)
876 904
877 905 return render('/pullrequests/pullrequest_show.mako')
878 906
879 907 @LoginRequired()
880 908 @NotAnonymous()
881 909 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
882 910 'repository.admin')
883 911 @auth.CSRFRequired()
884 912 @jsonify
885 913 def comment(self, repo_name, pull_request_id):
886 914 pull_request_id = safe_int(pull_request_id)
887 915 pull_request = PullRequest.get_or_404(pull_request_id)
888 916 if pull_request.is_closed():
889 917 raise HTTPForbidden()
890 918
891 919 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
892 920 # as a changeset status, still we want to send it in one value.
893 921 status = request.POST.get('changeset_status', None)
894 922 text = request.POST.get('text')
895 923 comment_type = request.POST.get('comment_type')
896 924 resolves_comment_id = request.POST.get('resolves_comment_id', None)
897 925
898 926 if status and '_closed' in status:
899 927 close_pr = True
900 928 status = status.replace('_closed', '')
901 929 else:
902 930 close_pr = False
903 931
904 932 forced = (status == 'forced')
905 933 if forced:
906 934 status = 'rejected'
907 935
908 936 allowed_to_change_status = PullRequestModel().check_user_change_status(
909 937 pull_request, c.rhodecode_user)
910 938
911 939 if status and allowed_to_change_status:
912 940 message = (_('Status change %(transition_icon)s %(status)s')
913 941 % {'transition_icon': '>',
914 942 'status': ChangesetStatus.get_status_lbl(status)})
915 943 if close_pr:
916 944 message = _('Closing with') + ' ' + message
917 945 text = text or message
918 946 comm = CommentsModel().create(
919 947 text=text,
920 948 repo=c.rhodecode_db_repo.repo_id,
921 949 user=c.rhodecode_user.user_id,
922 950 pull_request=pull_request_id,
923 951 f_path=request.POST.get('f_path'),
924 952 line_no=request.POST.get('line'),
925 953 status_change=(ChangesetStatus.get_status_lbl(status)
926 954 if status and allowed_to_change_status else None),
927 955 status_change_type=(status
928 956 if status and allowed_to_change_status else None),
929 957 closing_pr=close_pr,
930 958 comment_type=comment_type,
931 959 resolves_comment_id=resolves_comment_id
932 960 )
933 961
934 962 if allowed_to_change_status:
935 963 old_calculated_status = pull_request.calculated_review_status()
936 964 # get status if set !
937 965 if status:
938 966 ChangesetStatusModel().set_status(
939 967 c.rhodecode_db_repo.repo_id,
940 968 status,
941 969 c.rhodecode_user.user_id,
942 970 comm,
943 971 pull_request=pull_request_id
944 972 )
945 973
946 974 Session().flush()
947 975 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
948 976 # we now calculate the status of pull request, and based on that
949 977 # calculation we set the commits status
950 978 calculated_status = pull_request.calculated_review_status()
951 979 if old_calculated_status != calculated_status:
952 980 PullRequestModel()._trigger_pull_request_hook(
953 981 pull_request, c.rhodecode_user, 'review_status_change')
954 982
955 983 calculated_status_lbl = ChangesetStatus.get_status_lbl(
956 984 calculated_status)
957 985
958 986 if close_pr:
959 987 status_completed = (
960 988 calculated_status in [ChangesetStatus.STATUS_APPROVED,
961 989 ChangesetStatus.STATUS_REJECTED])
962 990 if forced or status_completed:
963 991 PullRequestModel().close_pull_request(
964 992 pull_request_id, c.rhodecode_user)
965 993 else:
966 994 h.flash(_('Closing pull request on other statuses than '
967 995 'rejected or approved is forbidden. '
968 996 'Calculated status from all reviewers '
969 997 'is currently: %s') % calculated_status_lbl,
970 998 category='warning')
971 999
972 1000 Session().commit()
973 1001
974 1002 if not request.is_xhr:
975 1003 return redirect(h.url('pullrequest_show', repo_name=repo_name,
976 1004 pull_request_id=pull_request_id))
977 1005
978 1006 data = {
979 1007 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
980 1008 }
981 1009 if comm:
982 1010 c.co = comm
983 1011 c.inline_comment = True if comm.line_no else False
984 1012 data.update(comm.get_dict())
985 1013 data.update({'rendered_text':
986 1014 render('changeset/changeset_comment_block.mako')})
987 1015
988 1016 return data
989 1017
990 1018 @LoginRequired()
991 1019 @NotAnonymous()
992 1020 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
993 1021 'repository.admin')
994 1022 @auth.CSRFRequired()
995 1023 @jsonify
996 1024 def delete_comment(self, repo_name, comment_id):
997 1025 return self._delete_comment(comment_id)
998 1026
999 1027 def _delete_comment(self, comment_id):
1000 1028 comment_id = safe_int(comment_id)
1001 1029 co = ChangesetComment.get_or_404(comment_id)
1002 1030 if co.pull_request.is_closed():
1003 1031 # don't allow deleting comments on closed pull request
1004 1032 raise HTTPForbidden()
1005 1033
1006 1034 is_owner = co.author.user_id == c.rhodecode_user.user_id
1007 1035 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1008 1036 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1009 1037 old_calculated_status = co.pull_request.calculated_review_status()
1010 1038 CommentsModel().delete(comment=co)
1011 1039 Session().commit()
1012 1040 calculated_status = co.pull_request.calculated_review_status()
1013 1041 if old_calculated_status != calculated_status:
1014 1042 PullRequestModel()._trigger_pull_request_hook(
1015 1043 co.pull_request, c.rhodecode_user, 'review_status_change')
1016 1044 return True
1017 1045 else:
1018 1046 raise HTTPForbidden()
@@ -1,600 +1,612 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47 from rhodecode.model.validation_schema.schemas import comment_schema
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class CommentsModel(BaseModel):
54 54
55 55 cls = ChangesetComment
56 56
57 57 DIFF_CONTEXT_BEFORE = 3
58 58 DIFF_CONTEXT_AFTER = 3
59 59
60 60 def __get_commit_comment(self, changeset_comment):
61 61 return self._get_instance(ChangesetComment, changeset_comment)
62 62
63 63 def __get_pull_request(self, pull_request):
64 64 return self._get_instance(PullRequest, pull_request)
65 65
66 66 def _extract_mentions(self, s):
67 67 user_objects = []
68 68 for username in extract_mentioned_users(s):
69 69 user_obj = User.get_by_username(username, case_insensitive=True)
70 70 if user_obj:
71 71 user_objects.append(user_obj)
72 72 return user_objects
73 73
74 74 def _get_renderer(self, global_renderer='rst'):
75 75 try:
76 76 # try reading from visual context
77 77 from pylons import tmpl_context
78 78 global_renderer = tmpl_context.visual.default_renderer
79 79 except AttributeError:
80 80 log.debug("Renderer not set, falling back "
81 81 "to default renderer '%s'", global_renderer)
82 82 except Exception:
83 83 log.error(traceback.format_exc())
84 84 return global_renderer
85 85
86 86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 87 # group by versions, and count until, and display objects
88 88
89 89 comment_groups = collections.defaultdict(list)
90 90 [comment_groups[
91 91 _co.pull_request_version_id].append(_co) for _co in comments]
92 92
93 93 def yield_comments(pos):
94 94 for co in comment_groups[pos]:
95 95 yield co
96 96
97 97 comment_versions = collections.defaultdict(
98 98 lambda: collections.defaultdict(list))
99 99 prev_prvid = -1
100 100 # fake last entry with None, to aggregate on "latest" version which
101 101 # doesn't have an pull_request_version_id
102 102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 103 prvid = ver.pull_request_version_id
104 104 if prev_prvid == -1:
105 105 prev_prvid = prvid
106 106
107 107 for co in yield_comments(prvid):
108 108 comment_versions[prvid]['at'].append(co)
109 109
110 110 # save until
111 111 current = comment_versions[prvid]['at']
112 112 prev_until = comment_versions[prev_prvid]['until']
113 113 cur_until = prev_until + current
114 114 comment_versions[prvid]['until'].extend(cur_until)
115 115
116 116 # save outdated
117 117 if inline:
118 118 outdated = [x for x in cur_until
119 119 if x.outdated_at_version(show_version)]
120 120 else:
121 121 outdated = [x for x in cur_until
122 122 if x.older_than_version(show_version)]
123 123 display = [x for x in cur_until if x not in outdated]
124 124
125 125 comment_versions[prvid]['outdated'] = outdated
126 126 comment_versions[prvid]['display'] = display
127 127
128 128 prev_prvid = prvid
129 129
130 130 return comment_versions
131 131
132 def get_unresolved_todos(self, pull_request):
133
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO) \
139 .filter(coalesce(ChangesetComment.display_state, '') !=
140 ChangesetComment.COMMENT_OUTDATED).all()
141
142 return todos
143
132 144 def create(self, text, repo, user, commit_id=None, pull_request=None,
133 145 f_path=None, line_no=None, status_change=None,
134 146 status_change_type=None, comment_type=None,
135 147 resolves_comment_id=None, closing_pr=False, send_email=True,
136 148 renderer=None):
137 149 """
138 150 Creates new comment for commit or pull request.
139 151 IF status_change is not none this comment is associated with a
140 152 status change of commit or commit associated with pull request
141 153
142 154 :param text:
143 155 :param repo:
144 156 :param user:
145 157 :param commit_id:
146 158 :param pull_request:
147 159 :param f_path:
148 160 :param line_no:
149 161 :param status_change: Label for status change
150 162 :param comment_type: Type of comment
151 163 :param status_change_type: type of status change
152 164 :param closing_pr:
153 165 :param send_email:
154 166 :param renderer: pick renderer for this comment
155 167 """
156 168 if not text:
157 169 log.warning('Missing text for comment, skipping...')
158 170 return
159 171
160 172 if not renderer:
161 173 renderer = self._get_renderer()
162 174
163 175 repo = self._get_repo(repo)
164 176 user = self._get_user(user)
165 177
166 178 schema = comment_schema.CommentSchema()
167 179 validated_kwargs = schema.deserialize(dict(
168 180 comment_body=text,
169 181 comment_type=comment_type,
170 182 comment_file=f_path,
171 183 comment_line=line_no,
172 184 renderer_type=renderer,
173 185 status_change=status_change_type,
174 186 resolves_comment_id=resolves_comment_id,
175 187 repo=repo.repo_id,
176 188 user=user.user_id,
177 189 ))
178 190
179 191 comment = ChangesetComment()
180 192 comment.renderer = validated_kwargs['renderer_type']
181 193 comment.text = validated_kwargs['comment_body']
182 194 comment.f_path = validated_kwargs['comment_file']
183 195 comment.line_no = validated_kwargs['comment_line']
184 196 comment.comment_type = validated_kwargs['comment_type']
185 197
186 198 comment.repo = repo
187 199 comment.author = user
188 200 comment.resolved_comment = self.__get_commit_comment(
189 201 validated_kwargs['resolves_comment_id'])
190 202
191 203 pull_request_id = pull_request
192 204
193 205 commit_obj = None
194 206 pull_request_obj = None
195 207
196 208 if commit_id:
197 209 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
198 210 # do a lookup, so we don't pass something bad here
199 211 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
200 212 comment.revision = commit_obj.raw_id
201 213
202 214 elif pull_request_id:
203 215 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
204 216 pull_request_obj = self.__get_pull_request(pull_request_id)
205 217 comment.pull_request = pull_request_obj
206 218 else:
207 219 raise Exception('Please specify commit or pull_request_id')
208 220
209 221 Session().add(comment)
210 222 Session().flush()
211 223 kwargs = {
212 224 'user': user,
213 225 'renderer_type': renderer,
214 226 'repo_name': repo.repo_name,
215 227 'status_change': status_change,
216 228 'status_change_type': status_change_type,
217 229 'comment_body': text,
218 230 'comment_file': f_path,
219 231 'comment_line': line_no,
220 232 }
221 233
222 234 if commit_obj:
223 235 recipients = ChangesetComment.get_users(
224 236 revision=commit_obj.raw_id)
225 237 # add commit author if it's in RhodeCode system
226 238 cs_author = User.get_from_cs_author(commit_obj.author)
227 239 if not cs_author:
228 240 # use repo owner if we cannot extract the author correctly
229 241 cs_author = repo.user
230 242 recipients += [cs_author]
231 243
232 244 commit_comment_url = self.get_url(comment)
233 245
234 246 target_repo_url = h.link_to(
235 247 repo.repo_name,
236 248 h.url('summary_home',
237 249 repo_name=repo.repo_name, qualified=True))
238 250
239 251 # commit specifics
240 252 kwargs.update({
241 253 'commit': commit_obj,
242 254 'commit_message': commit_obj.message,
243 255 'commit_target_repo': target_repo_url,
244 256 'commit_comment_url': commit_comment_url,
245 257 })
246 258
247 259 elif pull_request_obj:
248 260 # get the current participants of this pull request
249 261 recipients = ChangesetComment.get_users(
250 262 pull_request_id=pull_request_obj.pull_request_id)
251 263 # add pull request author
252 264 recipients += [pull_request_obj.author]
253 265
254 266 # add the reviewers to notification
255 267 recipients += [x.user for x in pull_request_obj.reviewers]
256 268
257 269 pr_target_repo = pull_request_obj.target_repo
258 270 pr_source_repo = pull_request_obj.source_repo
259 271
260 272 pr_comment_url = h.url(
261 273 'pullrequest_show',
262 274 repo_name=pr_target_repo.repo_name,
263 275 pull_request_id=pull_request_obj.pull_request_id,
264 276 anchor='comment-%s' % comment.comment_id,
265 277 qualified=True,)
266 278
267 279 # set some variables for email notification
268 280 pr_target_repo_url = h.url(
269 281 'summary_home', repo_name=pr_target_repo.repo_name,
270 282 qualified=True)
271 283
272 284 pr_source_repo_url = h.url(
273 285 'summary_home', repo_name=pr_source_repo.repo_name,
274 286 qualified=True)
275 287
276 288 # pull request specifics
277 289 kwargs.update({
278 290 'pull_request': pull_request_obj,
279 291 'pr_id': pull_request_obj.pull_request_id,
280 292 'pr_target_repo': pr_target_repo,
281 293 'pr_target_repo_url': pr_target_repo_url,
282 294 'pr_source_repo': pr_source_repo,
283 295 'pr_source_repo_url': pr_source_repo_url,
284 296 'pr_comment_url': pr_comment_url,
285 297 'pr_closing': closing_pr,
286 298 })
287 299 if send_email:
288 300 # pre-generate the subject for notification itself
289 301 (subject,
290 302 _h, _e, # we don't care about those
291 303 body_plaintext) = EmailNotificationModel().render_email(
292 304 notification_type, **kwargs)
293 305
294 306 mention_recipients = set(
295 307 self._extract_mentions(text)).difference(recipients)
296 308
297 309 # create notification objects, and emails
298 310 NotificationModel().create(
299 311 created_by=user,
300 312 notification_subject=subject,
301 313 notification_body=body_plaintext,
302 314 notification_type=notification_type,
303 315 recipients=recipients,
304 316 mention_recipients=mention_recipients,
305 317 email_kwargs=kwargs,
306 318 )
307 319
308 320 action = (
309 321 'user_commented_pull_request:{}'.format(
310 322 comment.pull_request.pull_request_id)
311 323 if comment.pull_request
312 324 else 'user_commented_revision:{}'.format(comment.revision)
313 325 )
314 326 action_logger(user, action, comment.repo)
315 327
316 328 registry = get_current_registry()
317 329 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
318 330 channelstream_config = rhodecode_plugins.get('channelstream', {})
319 331 msg_url = ''
320 332 if commit_obj:
321 333 msg_url = commit_comment_url
322 334 repo_name = repo.repo_name
323 335 elif pull_request_obj:
324 336 msg_url = pr_comment_url
325 337 repo_name = pr_target_repo.repo_name
326 338
327 339 if channelstream_config.get('enabled'):
328 340 message = '<strong>{}</strong> {} - ' \
329 341 '<a onclick="window.location=\'{}\';' \
330 342 'window.location.reload()">' \
331 343 '<strong>{}</strong></a>'
332 344 message = message.format(
333 345 user.username, _('made a comment'), msg_url,
334 346 _('Show it now'))
335 347 channel = '/repo${}$/pr/{}'.format(
336 348 repo_name,
337 349 pull_request_id
338 350 )
339 351 payload = {
340 352 'type': 'message',
341 353 'timestamp': datetime.utcnow(),
342 354 'user': 'system',
343 355 'exclude_users': [user.username],
344 356 'channel': channel,
345 357 'message': {
346 358 'message': message,
347 359 'level': 'info',
348 360 'topic': '/notifications'
349 361 }
350 362 }
351 363 channelstream_request(channelstream_config, [payload],
352 364 '/message', raise_exc=False)
353 365
354 366 return comment
355 367
356 368 def delete(self, comment):
357 369 """
358 370 Deletes given comment
359 371
360 372 :param comment_id:
361 373 """
362 374 comment = self.__get_commit_comment(comment)
363 375 Session().delete(comment)
364 376
365 377 return comment
366 378
367 379 def get_all_comments(self, repo_id, revision=None, pull_request=None):
368 380 q = ChangesetComment.query()\
369 381 .filter(ChangesetComment.repo_id == repo_id)
370 382 if revision:
371 383 q = q.filter(ChangesetComment.revision == revision)
372 384 elif pull_request:
373 385 pull_request = self.__get_pull_request(pull_request)
374 386 q = q.filter(ChangesetComment.pull_request == pull_request)
375 387 else:
376 388 raise Exception('Please specify commit or pull_request')
377 389 q = q.order_by(ChangesetComment.created_on)
378 390 return q.all()
379 391
380 392 def get_url(self, comment):
381 393 comment = self.__get_commit_comment(comment)
382 394 if comment.pull_request:
383 395 return h.url(
384 396 'pullrequest_show',
385 397 repo_name=comment.pull_request.target_repo.repo_name,
386 398 pull_request_id=comment.pull_request.pull_request_id,
387 399 anchor='comment-%s' % comment.comment_id,
388 400 qualified=True,)
389 401 else:
390 402 return h.url(
391 403 'changeset_home',
392 404 repo_name=comment.repo.repo_name,
393 405 revision=comment.revision,
394 406 anchor='comment-%s' % comment.comment_id,
395 407 qualified=True,)
396 408
397 409 def get_comments(self, repo_id, revision=None, pull_request=None):
398 410 """
399 411 Gets main comments based on revision or pull_request_id
400 412
401 413 :param repo_id:
402 414 :param revision:
403 415 :param pull_request:
404 416 """
405 417
406 418 q = ChangesetComment.query()\
407 419 .filter(ChangesetComment.repo_id == repo_id)\
408 420 .filter(ChangesetComment.line_no == None)\
409 421 .filter(ChangesetComment.f_path == None)
410 422 if revision:
411 423 q = q.filter(ChangesetComment.revision == revision)
412 424 elif pull_request:
413 425 pull_request = self.__get_pull_request(pull_request)
414 426 q = q.filter(ChangesetComment.pull_request == pull_request)
415 427 else:
416 428 raise Exception('Please specify commit or pull_request')
417 429 q = q.order_by(ChangesetComment.created_on)
418 430 return q.all()
419 431
420 432 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
421 433 q = self._get_inline_comments_query(repo_id, revision, pull_request)
422 434 return self._group_comments_by_path_and_line_number(q)
423 435
424 436 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
425 437 version=None):
426 438 inline_cnt = 0
427 439 for fname, per_line_comments in inline_comments.iteritems():
428 440 for lno, comments in per_line_comments.iteritems():
429 441 for comm in comments:
430 442 if not comm.outdated_at_version(version) and skip_outdated:
431 443 inline_cnt += 1
432 444
433 445 return inline_cnt
434 446
435 447 def get_outdated_comments(self, repo_id, pull_request):
436 448 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
437 449 # of a pull request.
438 450 q = self._all_inline_comments_of_pull_request(pull_request)
439 451 q = q.filter(
440 452 ChangesetComment.display_state ==
441 453 ChangesetComment.COMMENT_OUTDATED
442 454 ).order_by(ChangesetComment.comment_id.asc())
443 455
444 456 return self._group_comments_by_path_and_line_number(q)
445 457
446 458 def _get_inline_comments_query(self, repo_id, revision, pull_request):
447 459 # TODO: johbo: Split this into two methods: One for PR and one for
448 460 # commit.
449 461 if revision:
450 462 q = Session().query(ChangesetComment).filter(
451 463 ChangesetComment.repo_id == repo_id,
452 464 ChangesetComment.line_no != null(),
453 465 ChangesetComment.f_path != null(),
454 466 ChangesetComment.revision == revision)
455 467
456 468 elif pull_request:
457 469 pull_request = self.__get_pull_request(pull_request)
458 470 if not CommentsModel.use_outdated_comments(pull_request):
459 471 q = self._visible_inline_comments_of_pull_request(pull_request)
460 472 else:
461 473 q = self._all_inline_comments_of_pull_request(pull_request)
462 474
463 475 else:
464 476 raise Exception('Please specify commit or pull_request_id')
465 477 q = q.order_by(ChangesetComment.comment_id.asc())
466 478 return q
467 479
468 480 def _group_comments_by_path_and_line_number(self, q):
469 481 comments = q.all()
470 482 paths = collections.defaultdict(lambda: collections.defaultdict(list))
471 483 for co in comments:
472 484 paths[co.f_path][co.line_no].append(co)
473 485 return paths
474 486
475 487 @classmethod
476 488 def needed_extra_diff_context(cls):
477 489 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
478 490
479 491 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
480 492 if not CommentsModel.use_outdated_comments(pull_request):
481 493 return
482 494
483 495 comments = self._visible_inline_comments_of_pull_request(pull_request)
484 496 comments_to_outdate = comments.all()
485 497
486 498 for comment in comments_to_outdate:
487 499 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
488 500
489 501 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
490 502 diff_line = _parse_comment_line_number(comment.line_no)
491 503
492 504 try:
493 505 old_context = old_diff_proc.get_context_of_line(
494 506 path=comment.f_path, diff_line=diff_line)
495 507 new_context = new_diff_proc.get_context_of_line(
496 508 path=comment.f_path, diff_line=diff_line)
497 509 except (diffs.LineNotInDiffException,
498 510 diffs.FileNotInDiffException):
499 511 comment.display_state = ChangesetComment.COMMENT_OUTDATED
500 512 return
501 513
502 514 if old_context == new_context:
503 515 return
504 516
505 517 if self._should_relocate_diff_line(diff_line):
506 518 new_diff_lines = new_diff_proc.find_context(
507 519 path=comment.f_path, context=old_context,
508 520 offset=self.DIFF_CONTEXT_BEFORE)
509 521 if not new_diff_lines:
510 522 comment.display_state = ChangesetComment.COMMENT_OUTDATED
511 523 else:
512 524 new_diff_line = self._choose_closest_diff_line(
513 525 diff_line, new_diff_lines)
514 526 comment.line_no = _diff_to_comment_line_number(new_diff_line)
515 527 else:
516 528 comment.display_state = ChangesetComment.COMMENT_OUTDATED
517 529
518 530 def _should_relocate_diff_line(self, diff_line):
519 531 """
520 532 Checks if relocation shall be tried for the given `diff_line`.
521 533
522 534 If a comment points into the first lines, then we can have a situation
523 535 that after an update another line has been added on top. In this case
524 536 we would find the context still and move the comment around. This
525 537 would be wrong.
526 538 """
527 539 should_relocate = (
528 540 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
529 541 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
530 542 return should_relocate
531 543
532 544 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
533 545 candidate = new_diff_lines[0]
534 546 best_delta = _diff_line_delta(diff_line, candidate)
535 547 for new_diff_line in new_diff_lines[1:]:
536 548 delta = _diff_line_delta(diff_line, new_diff_line)
537 549 if delta < best_delta:
538 550 candidate = new_diff_line
539 551 best_delta = delta
540 552 return candidate
541 553
542 554 def _visible_inline_comments_of_pull_request(self, pull_request):
543 555 comments = self._all_inline_comments_of_pull_request(pull_request)
544 556 comments = comments.filter(
545 557 coalesce(ChangesetComment.display_state, '') !=
546 558 ChangesetComment.COMMENT_OUTDATED)
547 559 return comments
548 560
549 561 def _all_inline_comments_of_pull_request(self, pull_request):
550 562 comments = Session().query(ChangesetComment)\
551 563 .filter(ChangesetComment.line_no != None)\
552 564 .filter(ChangesetComment.f_path != None)\
553 565 .filter(ChangesetComment.pull_request == pull_request)
554 566 return comments
555 567
556 568 def _all_general_comments_of_pull_request(self, pull_request):
557 569 comments = Session().query(ChangesetComment)\
558 570 .filter(ChangesetComment.line_no == None)\
559 571 .filter(ChangesetComment.f_path == None)\
560 572 .filter(ChangesetComment.pull_request == pull_request)
561 573 return comments
562 574
563 575 @staticmethod
564 576 def use_outdated_comments(pull_request):
565 577 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
566 578 settings = settings_model.get_general_settings()
567 579 return settings.get('rhodecode_use_outdated_comments', False)
568 580
569 581
570 582 def _parse_comment_line_number(line_no):
571 583 """
572 584 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
573 585 """
574 586 old_line = None
575 587 new_line = None
576 588 if line_no.startswith('o'):
577 589 old_line = int(line_no[1:])
578 590 elif line_no.startswith('n'):
579 591 new_line = int(line_no[1:])
580 592 else:
581 593 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
582 594 return diffs.DiffLineNumber(old_line, new_line)
583 595
584 596
585 597 def _diff_to_comment_line_number(diff_line):
586 598 if diff_line.new is not None:
587 599 return u'n{}'.format(diff_line.new)
588 600 elif diff_line.old is not None:
589 601 return u'o{}'.format(diff_line.old)
590 602 return u''
591 603
592 604
593 605 def _diff_line_delta(a, b):
594 606 if None not in (a.new, b.new):
595 607 return abs(a.new - b.new)
596 608 elif None not in (a.old, b.old):
597 609 return abs(a.old - b.old)
598 610 else:
599 611 raise ValueError(
600 612 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3842 +1,3846 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def builtin_token_roles(self):
606 606 return map(UserApiKeys._get_role_name, [
607 607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 608 ])
609 609
610 610 @property
611 611 def ip_addresses(self):
612 612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 613 return [x.ip_addr for x in ret]
614 614
615 615 @property
616 616 def username_and_name(self):
617 617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618 618
619 619 @property
620 620 def username_or_name_or_email(self):
621 621 full_name = self.full_name if self.full_name is not ' ' else None
622 622 return self.username or full_name or self.email
623 623
624 624 @property
625 625 def full_name(self):
626 626 return '%s %s' % (self.firstname, self.lastname)
627 627
628 628 @property
629 629 def full_name_or_username(self):
630 630 return ('%s %s' % (self.firstname, self.lastname)
631 631 if (self.firstname and self.lastname) else self.username)
632 632
633 633 @property
634 634 def full_contact(self):
635 635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636 636
637 637 @property
638 638 def short_contact(self):
639 639 return '%s %s' % (self.firstname, self.lastname)
640 640
641 641 @property
642 642 def is_admin(self):
643 643 return self.admin
644 644
645 645 @property
646 646 def AuthUser(self):
647 647 """
648 648 Returns instance of AuthUser for this user
649 649 """
650 650 from rhodecode.lib.auth import AuthUser
651 651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 652 username=self.username)
653 653
654 654 @hybrid_property
655 655 def user_data(self):
656 656 if not self._user_data:
657 657 return {}
658 658
659 659 try:
660 660 return json.loads(self._user_data)
661 661 except TypeError:
662 662 return {}
663 663
664 664 @user_data.setter
665 665 def user_data(self, val):
666 666 if not isinstance(val, dict):
667 667 raise Exception('user_data must be dict, got %s' % type(val))
668 668 try:
669 669 self._user_data = json.dumps(val)
670 670 except Exception:
671 671 log.error(traceback.format_exc())
672 672
673 673 @classmethod
674 674 def get_by_username(cls, username, case_insensitive=False,
675 675 cache=False, identity_cache=False):
676 676 session = Session()
677 677
678 678 if case_insensitive:
679 679 q = cls.query().filter(
680 680 func.lower(cls.username) == func.lower(username))
681 681 else:
682 682 q = cls.query().filter(cls.username == username)
683 683
684 684 if cache:
685 685 if identity_cache:
686 686 val = cls.identity_cache(session, 'username', username)
687 687 if val:
688 688 return val
689 689 else:
690 690 q = q.options(
691 691 FromCache("sql_cache_short",
692 692 "get_user_by_name_%s" % _hash_key(username)))
693 693
694 694 return q.scalar()
695 695
696 696 @classmethod
697 697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 698 q = cls.query().filter(cls.api_key == auth_token)
699 699
700 700 if cache:
701 701 q = q.options(FromCache("sql_cache_short",
702 702 "get_auth_token_%s" % auth_token))
703 703 res = q.scalar()
704 704
705 705 if fallback and not res:
706 706 #fallback to additional keys
707 707 _res = UserApiKeys.query()\
708 708 .filter(UserApiKeys.api_key == auth_token)\
709 709 .filter(or_(UserApiKeys.expires == -1,
710 710 UserApiKeys.expires >= time.time()))\
711 711 .first()
712 712 if _res:
713 713 res = _res.user
714 714 return res
715 715
716 716 @classmethod
717 717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718 718
719 719 if case_insensitive:
720 720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721 721
722 722 else:
723 723 q = cls.query().filter(cls.email == email)
724 724
725 725 if cache:
726 726 q = q.options(FromCache("sql_cache_short",
727 727 "get_email_key_%s" % _hash_key(email)))
728 728
729 729 ret = q.scalar()
730 730 if ret is None:
731 731 q = UserEmailMap.query()
732 732 # try fetching in alternate email map
733 733 if case_insensitive:
734 734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 735 else:
736 736 q = q.filter(UserEmailMap.email == email)
737 737 q = q.options(joinedload(UserEmailMap.user))
738 738 if cache:
739 739 q = q.options(FromCache("sql_cache_short",
740 740 "get_email_map_key_%s" % email))
741 741 ret = getattr(q.scalar(), 'user', None)
742 742
743 743 return ret
744 744
745 745 @classmethod
746 746 def get_from_cs_author(cls, author):
747 747 """
748 748 Tries to get User objects out of commit author string
749 749
750 750 :param author:
751 751 """
752 752 from rhodecode.lib.helpers import email, author_name
753 753 # Valid email in the attribute passed, see if they're in the system
754 754 _email = email(author)
755 755 if _email:
756 756 user = cls.get_by_email(_email, case_insensitive=True)
757 757 if user:
758 758 return user
759 759 # Maybe we can match by username?
760 760 _author = author_name(author)
761 761 user = cls.get_by_username(_author, case_insensitive=True)
762 762 if user:
763 763 return user
764 764
765 765 def update_userdata(self, **kwargs):
766 766 usr = self
767 767 old = usr.user_data
768 768 old.update(**kwargs)
769 769 usr.user_data = old
770 770 Session().add(usr)
771 771 log.debug('updated userdata with ', kwargs)
772 772
773 773 def update_lastlogin(self):
774 774 """Update user lastlogin"""
775 775 self.last_login = datetime.datetime.now()
776 776 Session().add(self)
777 777 log.debug('updated user %s lastlogin', self.username)
778 778
779 779 def update_lastactivity(self):
780 780 """Update user lastactivity"""
781 781 usr = self
782 782 old = usr.user_data
783 783 old.update({'last_activity': time.time()})
784 784 usr.user_data = old
785 785 Session().add(usr)
786 786 log.debug('updated user %s lastactivity', usr.username)
787 787
788 788 def update_password(self, new_password, change_api_key=False):
789 789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790 790
791 791 self.password = get_crypt_password(new_password)
792 792 if change_api_key:
793 793 self.api_key = generate_auth_token(self.username)
794 794 Session().add(self)
795 795
796 796 @classmethod
797 797 def get_first_super_admin(cls):
798 798 user = User.query().filter(User.admin == true()).first()
799 799 if user is None:
800 800 raise Exception('FATAL: Missing administrative account!')
801 801 return user
802 802
803 803 @classmethod
804 804 def get_all_super_admins(cls):
805 805 """
806 806 Returns all admin accounts sorted by username
807 807 """
808 808 return User.query().filter(User.admin == true())\
809 809 .order_by(User.username.asc()).all()
810 810
811 811 @classmethod
812 812 def get_default_user(cls, cache=False):
813 813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 814 if user is None:
815 815 raise Exception('FATAL: Missing default account!')
816 816 return user
817 817
818 818 def _get_default_perms(self, user, suffix=''):
819 819 from rhodecode.model.permission import PermissionModel
820 820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821 821
822 822 def get_default_perms(self, suffix=''):
823 823 return self._get_default_perms(self, suffix)
824 824
825 825 def get_api_data(self, include_secrets=False, details='full'):
826 826 """
827 827 Common function for generating user related data for API
828 828
829 829 :param include_secrets: By default secrets in the API data will be replaced
830 830 by a placeholder value to prevent exposing this data by accident. In case
831 831 this data shall be exposed, set this flag to ``True``.
832 832
833 833 :param details: details can be 'basic|full' basic gives only a subset of
834 834 the available user information that includes user_id, name and emails.
835 835 """
836 836 user = self
837 837 user_data = self.user_data
838 838 data = {
839 839 'user_id': user.user_id,
840 840 'username': user.username,
841 841 'firstname': user.name,
842 842 'lastname': user.lastname,
843 843 'email': user.email,
844 844 'emails': user.emails,
845 845 }
846 846 if details == 'basic':
847 847 return data
848 848
849 849 api_key_length = 40
850 850 api_key_replacement = '*' * api_key_length
851 851
852 852 extras = {
853 853 'api_key': api_key_replacement,
854 854 'api_keys': [api_key_replacement],
855 855 'active': user.active,
856 856 'admin': user.admin,
857 857 'extern_type': user.extern_type,
858 858 'extern_name': user.extern_name,
859 859 'last_login': user.last_login,
860 860 'ip_addresses': user.ip_addresses,
861 861 'language': user_data.get('language')
862 862 }
863 863 data.update(extras)
864 864
865 865 if include_secrets:
866 866 data['api_key'] = user.api_key
867 867 data['api_keys'] = user.auth_tokens
868 868 return data
869 869
870 870 def __json__(self):
871 871 data = {
872 872 'full_name': self.full_name,
873 873 'full_name_or_username': self.full_name_or_username,
874 874 'short_contact': self.short_contact,
875 875 'full_contact': self.full_contact,
876 876 }
877 877 data.update(self.get_api_data())
878 878 return data
879 879
880 880
881 881 class UserApiKeys(Base, BaseModel):
882 882 __tablename__ = 'user_api_keys'
883 883 __table_args__ = (
884 884 Index('uak_api_key_idx', 'api_key'),
885 885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 886 UniqueConstraint('api_key'),
887 887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 889 )
890 890 __mapper_args__ = {}
891 891
892 892 # ApiKey role
893 893 ROLE_ALL = 'token_role_all'
894 894 ROLE_HTTP = 'token_role_http'
895 895 ROLE_VCS = 'token_role_vcs'
896 896 ROLE_API = 'token_role_api'
897 897 ROLE_FEED = 'token_role_feed'
898 898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899 899
900 900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 904 expires = Column('expires', Float(53), nullable=False)
905 905 role = Column('role', String(255), nullable=True)
906 906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907 907
908 908 user = relationship('User', lazy='joined')
909 909
910 910 @classmethod
911 911 def _get_role_name(cls, role):
912 912 return {
913 913 cls.ROLE_ALL: _('all'),
914 914 cls.ROLE_HTTP: _('http/web interface'),
915 915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 916 cls.ROLE_API: _('api calls'),
917 917 cls.ROLE_FEED: _('feed access'),
918 918 }.get(role, role)
919 919
920 920 @property
921 921 def expired(self):
922 922 if self.expires == -1:
923 923 return False
924 924 return time.time() > self.expires
925 925
926 926 @property
927 927 def role_humanized(self):
928 928 return self._get_role_name(self.role)
929 929
930 930
931 931 class UserEmailMap(Base, BaseModel):
932 932 __tablename__ = 'user_email_map'
933 933 __table_args__ = (
934 934 Index('uem_email_idx', 'email'),
935 935 UniqueConstraint('email'),
936 936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 938 )
939 939 __mapper_args__ = {}
940 940
941 941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 944 user = relationship('User', lazy='joined')
945 945
946 946 @validates('_email')
947 947 def validate_email(self, key, email):
948 948 # check if this email is not main one
949 949 main_email = Session().query(User).filter(User.email == email).scalar()
950 950 if main_email is not None:
951 951 raise AttributeError('email %s is present is user table' % email)
952 952 return email
953 953
954 954 @hybrid_property
955 955 def email(self):
956 956 return self._email
957 957
958 958 @email.setter
959 959 def email(self, val):
960 960 self._email = val.lower() if val else None
961 961
962 962
963 963 class UserIpMap(Base, BaseModel):
964 964 __tablename__ = 'user_ip_map'
965 965 __table_args__ = (
966 966 UniqueConstraint('user_id', 'ip_addr'),
967 967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 969 )
970 970 __mapper_args__ = {}
971 971
972 972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 977 user = relationship('User', lazy='joined')
978 978
979 979 @classmethod
980 980 def _get_ip_range(cls, ip_addr):
981 981 net = ipaddress.ip_network(ip_addr, strict=False)
982 982 return [str(net.network_address), str(net.broadcast_address)]
983 983
984 984 def __json__(self):
985 985 return {
986 986 'ip_addr': self.ip_addr,
987 987 'ip_range': self._get_ip_range(self.ip_addr),
988 988 }
989 989
990 990 def __unicode__(self):
991 991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 992 self.user_id, self.ip_addr)
993 993
994 994 class UserLog(Base, BaseModel):
995 995 __tablename__ = 'user_logs'
996 996 __table_args__ = (
997 997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 999 )
1000 1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008 1008
1009 1009 def __unicode__(self):
1010 1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 1011 self.repository_name,
1012 1012 self.action)
1013 1013
1014 1014 @property
1015 1015 def action_as_day(self):
1016 1016 return datetime.date(*self.action_date.timetuple()[:3])
1017 1017
1018 1018 user = relationship('User')
1019 1019 repository = relationship('Repository', cascade='')
1020 1020
1021 1021
1022 1022 class UserGroup(Base, BaseModel):
1023 1023 __tablename__ = 'users_groups'
1024 1024 __table_args__ = (
1025 1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 1027 )
1028 1028
1029 1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037 1037
1038 1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044 1044
1045 1045 user = relationship('User')
1046 1046
1047 1047 @hybrid_property
1048 1048 def group_data(self):
1049 1049 if not self._group_data:
1050 1050 return {}
1051 1051
1052 1052 try:
1053 1053 return json.loads(self._group_data)
1054 1054 except TypeError:
1055 1055 return {}
1056 1056
1057 1057 @group_data.setter
1058 1058 def group_data(self, val):
1059 1059 try:
1060 1060 self._group_data = json.dumps(val)
1061 1061 except Exception:
1062 1062 log.error(traceback.format_exc())
1063 1063
1064 1064 def __unicode__(self):
1065 1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 1066 self.users_group_id,
1067 1067 self.users_group_name)
1068 1068
1069 1069 @classmethod
1070 1070 def get_by_group_name(cls, group_name, cache=False,
1071 1071 case_insensitive=False):
1072 1072 if case_insensitive:
1073 1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 1074 func.lower(group_name))
1075 1075
1076 1076 else:
1077 1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 1078 if cache:
1079 1079 q = q.options(FromCache(
1080 1080 "sql_cache_short",
1081 1081 "get_group_%s" % _hash_key(group_name)))
1082 1082 return q.scalar()
1083 1083
1084 1084 @classmethod
1085 1085 def get(cls, user_group_id, cache=False):
1086 1086 user_group = cls.query()
1087 1087 if cache:
1088 1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 1089 "get_users_group_%s" % user_group_id))
1090 1090 return user_group.get(user_group_id)
1091 1091
1092 1092 def permissions(self, with_admins=True, with_owner=True):
1093 1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 1095 joinedload(UserUserGroupToPerm.user),
1096 1096 joinedload(UserUserGroupToPerm.permission),)
1097 1097
1098 1098 # get owners and admins and permissions. We do a trick of re-writing
1099 1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 1100 # has a global reference and changing one object propagates to all
1101 1101 # others. This means if admin is also an owner admin_row that change
1102 1102 # would propagate to both objects
1103 1103 perm_rows = []
1104 1104 for _usr in q.all():
1105 1105 usr = AttributeDict(_usr.user.get_dict())
1106 1106 usr.permission = _usr.permission.permission_name
1107 1107 perm_rows.append(usr)
1108 1108
1109 1109 # filter the perm rows by 'default' first and then sort them by
1110 1110 # admin,write,read,none permissions sorted again alphabetically in
1111 1111 # each group
1112 1112 perm_rows = sorted(perm_rows, key=display_sort)
1113 1113
1114 1114 _admin_perm = 'usergroup.admin'
1115 1115 owner_row = []
1116 1116 if with_owner:
1117 1117 usr = AttributeDict(self.user.get_dict())
1118 1118 usr.owner_row = True
1119 1119 usr.permission = _admin_perm
1120 1120 owner_row.append(usr)
1121 1121
1122 1122 super_admin_rows = []
1123 1123 if with_admins:
1124 1124 for usr in User.get_all_super_admins():
1125 1125 # if this admin is also owner, don't double the record
1126 1126 if usr.user_id == owner_row[0].user_id:
1127 1127 owner_row[0].admin_row = True
1128 1128 else:
1129 1129 usr = AttributeDict(usr.get_dict())
1130 1130 usr.admin_row = True
1131 1131 usr.permission = _admin_perm
1132 1132 super_admin_rows.append(usr)
1133 1133
1134 1134 return super_admin_rows + owner_row + perm_rows
1135 1135
1136 1136 def permission_user_groups(self):
1137 1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141 1141
1142 1142 perm_rows = []
1143 1143 for _user_group in q.all():
1144 1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 1145 usr.permission = _user_group.permission.permission_name
1146 1146 perm_rows.append(usr)
1147 1147
1148 1148 return perm_rows
1149 1149
1150 1150 def _get_default_perms(self, user_group, suffix=''):
1151 1151 from rhodecode.model.permission import PermissionModel
1152 1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153 1153
1154 1154 def get_default_perms(self, suffix=''):
1155 1155 return self._get_default_perms(self, suffix)
1156 1156
1157 1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 1158 """
1159 1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 1160 basically forwarded.
1161 1161
1162 1162 """
1163 1163 user_group = self
1164 1164
1165 1165 data = {
1166 1166 'users_group_id': user_group.users_group_id,
1167 1167 'group_name': user_group.users_group_name,
1168 1168 'group_description': user_group.user_group_description,
1169 1169 'active': user_group.users_group_active,
1170 1170 'owner': user_group.user.username,
1171 1171 }
1172 1172 if with_group_members:
1173 1173 users = []
1174 1174 for user in user_group.members:
1175 1175 user = user.user
1176 1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 1177 data['users'] = users
1178 1178
1179 1179 return data
1180 1180
1181 1181
1182 1182 class UserGroupMember(Base, BaseModel):
1183 1183 __tablename__ = 'users_groups_members'
1184 1184 __table_args__ = (
1185 1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 1187 )
1188 1188
1189 1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192 1192
1193 1193 user = relationship('User', lazy='joined')
1194 1194 users_group = relationship('UserGroup')
1195 1195
1196 1196 def __init__(self, gr_id='', u_id=''):
1197 1197 self.users_group_id = gr_id
1198 1198 self.user_id = u_id
1199 1199
1200 1200
1201 1201 class RepositoryField(Base, BaseModel):
1202 1202 __tablename__ = 'repositories_fields'
1203 1203 __table_args__ = (
1204 1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 1207 )
1208 1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209 1209
1210 1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 1212 field_key = Column("field_key", String(250))
1213 1213 field_label = Column("field_label", String(1024), nullable=False)
1214 1214 field_value = Column("field_value", String(10000), nullable=False)
1215 1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218 1218
1219 1219 repository = relationship('Repository')
1220 1220
1221 1221 @property
1222 1222 def field_key_prefixed(self):
1223 1223 return 'ex_%s' % self.field_key
1224 1224
1225 1225 @classmethod
1226 1226 def un_prefix_key(cls, key):
1227 1227 if key.startswith(cls.PREFIX):
1228 1228 return key[len(cls.PREFIX):]
1229 1229 return key
1230 1230
1231 1231 @classmethod
1232 1232 def get_by_key_name(cls, key, repo):
1233 1233 row = cls.query()\
1234 1234 .filter(cls.repository == repo)\
1235 1235 .filter(cls.field_key == key).scalar()
1236 1236 return row
1237 1237
1238 1238
1239 1239 class Repository(Base, BaseModel):
1240 1240 __tablename__ = 'repositories'
1241 1241 __table_args__ = (
1242 1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 1245 )
1246 1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248 1248
1249 1249 STATE_CREATED = 'repo_state_created'
1250 1250 STATE_PENDING = 'repo_state_pending'
1251 1251 STATE_ERROR = 'repo_state_error'
1252 1252
1253 1253 LOCK_AUTOMATIC = 'lock_auto'
1254 1254 LOCK_API = 'lock_api'
1255 1255 LOCK_WEB = 'lock_web'
1256 1256 LOCK_PULL = 'lock_pull'
1257 1257
1258 1258 NAME_SEP = URL_SEP
1259 1259
1260 1260 repo_id = Column(
1261 1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 1262 primary_key=True)
1263 1263 _repo_name = Column(
1264 1264 "repo_name", Text(), nullable=False, default=None)
1265 1265 _repo_name_hash = Column(
1266 1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 1267 repo_state = Column("repo_state", String(255), nullable=True)
1268 1268
1269 1269 clone_uri = Column(
1270 1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 1271 default=None)
1272 1272 repo_type = Column(
1273 1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 1274 user_id = Column(
1275 1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 1276 unique=False, default=None)
1277 1277 private = Column(
1278 1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 1279 enable_statistics = Column(
1280 1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 1281 enable_downloads = Column(
1282 1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 1283 description = Column(
1284 1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 1285 created_on = Column(
1286 1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 updated_on = Column(
1289 1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 1290 default=datetime.datetime.now)
1291 1291 _landing_revision = Column(
1292 1292 "landing_revision", String(255), nullable=False, unique=False,
1293 1293 default=None)
1294 1294 enable_locking = Column(
1295 1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 1296 default=False)
1297 1297 _locked = Column(
1298 1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 1299 _changeset_cache = Column(
1300 1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301 1301
1302 1302 fork_id = Column(
1303 1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 1304 nullable=True, unique=False, default=None)
1305 1305 group_id = Column(
1306 1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 1307 unique=False, default=None)
1308 1308
1309 1309 user = relationship('User', lazy='joined')
1310 1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 1311 group = relationship('RepoGroup', lazy='joined')
1312 1312 repo_to_perm = relationship(
1313 1313 'UserRepoToPerm', cascade='all',
1314 1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317 1317
1318 1318 followers = relationship(
1319 1319 'UserFollowing',
1320 1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 1321 cascade='all')
1322 1322 extra_fields = relationship(
1323 1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 1324 logs = relationship('UserLog')
1325 1325 comments = relationship(
1326 1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 1327 pull_requests_source = relationship(
1328 1328 'PullRequest',
1329 1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 1330 cascade="all, delete, delete-orphan")
1331 1331 pull_requests_target = relationship(
1332 1332 'PullRequest',
1333 1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 1334 cascade="all, delete, delete-orphan")
1335 1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 1337 integrations = relationship('Integration',
1338 1338 cascade="all, delete, delete-orphan")
1339 1339
1340 1340 def __unicode__(self):
1341 1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 1342 safe_unicode(self.repo_name))
1343 1343
1344 1344 @hybrid_property
1345 1345 def landing_rev(self):
1346 1346 # always should return [rev_type, rev]
1347 1347 if self._landing_revision:
1348 1348 _rev_info = self._landing_revision.split(':')
1349 1349 if len(_rev_info) < 2:
1350 1350 _rev_info.insert(0, 'rev')
1351 1351 return [_rev_info[0], _rev_info[1]]
1352 1352 return [None, None]
1353 1353
1354 1354 @landing_rev.setter
1355 1355 def landing_rev(self, val):
1356 1356 if ':' not in val:
1357 1357 raise ValueError('value must be delimited with `:` and consist '
1358 1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 1359 self._landing_revision = val
1360 1360
1361 1361 @hybrid_property
1362 1362 def locked(self):
1363 1363 if self._locked:
1364 1364 user_id, timelocked, reason = self._locked.split(':')
1365 1365 lock_values = int(user_id), timelocked, reason
1366 1366 else:
1367 1367 lock_values = [None, None, None]
1368 1368 return lock_values
1369 1369
1370 1370 @locked.setter
1371 1371 def locked(self, val):
1372 1372 if val and isinstance(val, (list, tuple)):
1373 1373 self._locked = ':'.join(map(str, val))
1374 1374 else:
1375 1375 self._locked = None
1376 1376
1377 1377 @hybrid_property
1378 1378 def changeset_cache(self):
1379 1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 1380 dummy = EmptyCommit().__json__()
1381 1381 if not self._changeset_cache:
1382 1382 return dummy
1383 1383 try:
1384 1384 return json.loads(self._changeset_cache)
1385 1385 except TypeError:
1386 1386 return dummy
1387 1387 except Exception:
1388 1388 log.error(traceback.format_exc())
1389 1389 return dummy
1390 1390
1391 1391 @changeset_cache.setter
1392 1392 def changeset_cache(self, val):
1393 1393 try:
1394 1394 self._changeset_cache = json.dumps(val)
1395 1395 except Exception:
1396 1396 log.error(traceback.format_exc())
1397 1397
1398 1398 @hybrid_property
1399 1399 def repo_name(self):
1400 1400 return self._repo_name
1401 1401
1402 1402 @repo_name.setter
1403 1403 def repo_name(self, value):
1404 1404 self._repo_name = value
1405 1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406 1406
1407 1407 @classmethod
1408 1408 def normalize_repo_name(cls, repo_name):
1409 1409 """
1410 1410 Normalizes os specific repo_name to the format internally stored inside
1411 1411 database using URL_SEP
1412 1412
1413 1413 :param cls:
1414 1414 :param repo_name:
1415 1415 """
1416 1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417 1417
1418 1418 @classmethod
1419 1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 1420 session = Session()
1421 1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422 1422
1423 1423 if cache:
1424 1424 if identity_cache:
1425 1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 1426 if val:
1427 1427 return val
1428 1428 else:
1429 1429 q = q.options(
1430 1430 FromCache("sql_cache_short",
1431 1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432 1432
1433 1433 return q.scalar()
1434 1434
1435 1435 @classmethod
1436 1436 def get_by_full_path(cls, repo_full_path):
1437 1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 1438 repo_name = cls.normalize_repo_name(repo_name)
1439 1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440 1440
1441 1441 @classmethod
1442 1442 def get_repo_forks(cls, repo_id):
1443 1443 return cls.query().filter(Repository.fork_id == repo_id)
1444 1444
1445 1445 @classmethod
1446 1446 def base_path(cls):
1447 1447 """
1448 1448 Returns base path when all repos are stored
1449 1449
1450 1450 :param cls:
1451 1451 """
1452 1452 q = Session().query(RhodeCodeUi)\
1453 1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 1455 return q.one().ui_value
1456 1456
1457 1457 @classmethod
1458 1458 def is_valid(cls, repo_name):
1459 1459 """
1460 1460 returns True if given repo name is a valid filesystem repository
1461 1461
1462 1462 :param cls:
1463 1463 :param repo_name:
1464 1464 """
1465 1465 from rhodecode.lib.utils import is_valid_repo
1466 1466
1467 1467 return is_valid_repo(repo_name, cls.base_path())
1468 1468
1469 1469 @classmethod
1470 1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 1471 case_insensitive=True):
1472 1472 q = Repository.query()
1473 1473
1474 1474 if not isinstance(user_id, Optional):
1475 1475 q = q.filter(Repository.user_id == user_id)
1476 1476
1477 1477 if not isinstance(group_id, Optional):
1478 1478 q = q.filter(Repository.group_id == group_id)
1479 1479
1480 1480 if case_insensitive:
1481 1481 q = q.order_by(func.lower(Repository.repo_name))
1482 1482 else:
1483 1483 q = q.order_by(Repository.repo_name)
1484 1484 return q.all()
1485 1485
1486 1486 @property
1487 1487 def forks(self):
1488 1488 """
1489 1489 Return forks of this repo
1490 1490 """
1491 1491 return Repository.get_repo_forks(self.repo_id)
1492 1492
1493 1493 @property
1494 1494 def parent(self):
1495 1495 """
1496 1496 Returns fork parent
1497 1497 """
1498 1498 return self.fork
1499 1499
1500 1500 @property
1501 1501 def just_name(self):
1502 1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503 1503
1504 1504 @property
1505 1505 def groups_with_parents(self):
1506 1506 groups = []
1507 1507 if self.group is None:
1508 1508 return groups
1509 1509
1510 1510 cur_gr = self.group
1511 1511 groups.insert(0, cur_gr)
1512 1512 while 1:
1513 1513 gr = getattr(cur_gr, 'parent_group', None)
1514 1514 cur_gr = cur_gr.parent_group
1515 1515 if gr is None:
1516 1516 break
1517 1517 groups.insert(0, gr)
1518 1518
1519 1519 return groups
1520 1520
1521 1521 @property
1522 1522 def groups_and_repo(self):
1523 1523 return self.groups_with_parents, self
1524 1524
1525 1525 @LazyProperty
1526 1526 def repo_path(self):
1527 1527 """
1528 1528 Returns base full path for that repository means where it actually
1529 1529 exists on a filesystem
1530 1530 """
1531 1531 q = Session().query(RhodeCodeUi).filter(
1532 1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 1534 return q.one().ui_value
1535 1535
1536 1536 @property
1537 1537 def repo_full_path(self):
1538 1538 p = [self.repo_path]
1539 1539 # we need to split the name by / since this is how we store the
1540 1540 # names in the database, but that eventually needs to be converted
1541 1541 # into a valid system path
1542 1542 p += self.repo_name.split(self.NAME_SEP)
1543 1543 return os.path.join(*map(safe_unicode, p))
1544 1544
1545 1545 @property
1546 1546 def cache_keys(self):
1547 1547 """
1548 1548 Returns associated cache keys for that repo
1549 1549 """
1550 1550 return CacheKey.query()\
1551 1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 1552 .order_by(CacheKey.cache_key)\
1553 1553 .all()
1554 1554
1555 1555 def get_new_name(self, repo_name):
1556 1556 """
1557 1557 returns new full repository name based on assigned group and new new
1558 1558
1559 1559 :param group_name:
1560 1560 """
1561 1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563 1563
1564 1564 @property
1565 1565 def _config(self):
1566 1566 """
1567 1567 Returns db based config object.
1568 1568 """
1569 1569 from rhodecode.lib.utils import make_db_config
1570 1570 return make_db_config(clear_session=False, repo=self)
1571 1571
1572 1572 def permissions(self, with_admins=True, with_owner=True):
1573 1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 1575 joinedload(UserRepoToPerm.user),
1576 1576 joinedload(UserRepoToPerm.permission),)
1577 1577
1578 1578 # get owners and admins and permissions. We do a trick of re-writing
1579 1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 1580 # has a global reference and changing one object propagates to all
1581 1581 # others. This means if admin is also an owner admin_row that change
1582 1582 # would propagate to both objects
1583 1583 perm_rows = []
1584 1584 for _usr in q.all():
1585 1585 usr = AttributeDict(_usr.user.get_dict())
1586 1586 usr.permission = _usr.permission.permission_name
1587 1587 perm_rows.append(usr)
1588 1588
1589 1589 # filter the perm rows by 'default' first and then sort them by
1590 1590 # admin,write,read,none permissions sorted again alphabetically in
1591 1591 # each group
1592 1592 perm_rows = sorted(perm_rows, key=display_sort)
1593 1593
1594 1594 _admin_perm = 'repository.admin'
1595 1595 owner_row = []
1596 1596 if with_owner:
1597 1597 usr = AttributeDict(self.user.get_dict())
1598 1598 usr.owner_row = True
1599 1599 usr.permission = _admin_perm
1600 1600 owner_row.append(usr)
1601 1601
1602 1602 super_admin_rows = []
1603 1603 if with_admins:
1604 1604 for usr in User.get_all_super_admins():
1605 1605 # if this admin is also owner, don't double the record
1606 1606 if usr.user_id == owner_row[0].user_id:
1607 1607 owner_row[0].admin_row = True
1608 1608 else:
1609 1609 usr = AttributeDict(usr.get_dict())
1610 1610 usr.admin_row = True
1611 1611 usr.permission = _admin_perm
1612 1612 super_admin_rows.append(usr)
1613 1613
1614 1614 return super_admin_rows + owner_row + perm_rows
1615 1615
1616 1616 def permission_user_groups(self):
1617 1617 q = UserGroupRepoToPerm.query().filter(
1618 1618 UserGroupRepoToPerm.repository == self)
1619 1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 1620 joinedload(UserGroupRepoToPerm.users_group),
1621 1621 joinedload(UserGroupRepoToPerm.permission),)
1622 1622
1623 1623 perm_rows = []
1624 1624 for _user_group in q.all():
1625 1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 1626 usr.permission = _user_group.permission.permission_name
1627 1627 perm_rows.append(usr)
1628 1628
1629 1629 return perm_rows
1630 1630
1631 1631 def get_api_data(self, include_secrets=False):
1632 1632 """
1633 1633 Common function for generating repo api data
1634 1634
1635 1635 :param include_secrets: See :meth:`User.get_api_data`.
1636 1636
1637 1637 """
1638 1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 1639 # move this methods on models level.
1640 1640 from rhodecode.model.settings import SettingsModel
1641 1641
1642 1642 repo = self
1643 1643 _user_id, _time, _reason = self.locked
1644 1644
1645 1645 data = {
1646 1646 'repo_id': repo.repo_id,
1647 1647 'repo_name': repo.repo_name,
1648 1648 'repo_type': repo.repo_type,
1649 1649 'clone_uri': repo.clone_uri or '',
1650 1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 1651 'private': repo.private,
1652 1652 'created_on': repo.created_on,
1653 1653 'description': repo.description,
1654 1654 'landing_rev': repo.landing_rev,
1655 1655 'owner': repo.user.username,
1656 1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 1657 'enable_statistics': repo.enable_statistics,
1658 1658 'enable_locking': repo.enable_locking,
1659 1659 'enable_downloads': repo.enable_downloads,
1660 1660 'last_changeset': repo.changeset_cache,
1661 1661 'locked_by': User.get(_user_id).get_api_data(
1662 1662 include_secrets=include_secrets) if _user_id else None,
1663 1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 1664 'lock_reason': _reason if _reason else None,
1665 1665 }
1666 1666
1667 1667 # TODO: mikhail: should be per-repo settings here
1668 1668 rc_config = SettingsModel().get_all_settings()
1669 1669 repository_fields = str2bool(
1670 1670 rc_config.get('rhodecode_repository_fields'))
1671 1671 if repository_fields:
1672 1672 for f in self.extra_fields:
1673 1673 data[f.field_key_prefixed] = f.field_value
1674 1674
1675 1675 return data
1676 1676
1677 1677 @classmethod
1678 1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 1679 if not lock_time:
1680 1680 lock_time = time.time()
1681 1681 if not lock_reason:
1682 1682 lock_reason = cls.LOCK_AUTOMATIC
1683 1683 repo.locked = [user_id, lock_time, lock_reason]
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def unlock(cls, repo):
1689 1689 repo.locked = None
1690 1690 Session().add(repo)
1691 1691 Session().commit()
1692 1692
1693 1693 @classmethod
1694 1694 def getlock(cls, repo):
1695 1695 return repo.locked
1696 1696
1697 1697 def is_user_lock(self, user_id):
1698 1698 if self.lock[0]:
1699 1699 lock_user_id = safe_int(self.lock[0])
1700 1700 user_id = safe_int(user_id)
1701 1701 # both are ints, and they are equal
1702 1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703 1703
1704 1704 return False
1705 1705
1706 1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 1707 """
1708 1708 Checks locking on this repository, if locking is enabled and lock is
1709 1709 present returns a tuple of make_lock, locked, locked_by.
1710 1710 make_lock can have 3 states None (do nothing) True, make lock
1711 1711 False release lock, This value is later propagated to hooks, which
1712 1712 do the locking. Think about this as signals passed to hooks what to do.
1713 1713
1714 1714 """
1715 1715 # TODO: johbo: This is part of the business logic and should be moved
1716 1716 # into the RepositoryModel.
1717 1717
1718 1718 if action not in ('push', 'pull'):
1719 1719 raise ValueError("Invalid action value: %s" % repr(action))
1720 1720
1721 1721 # defines if locked error should be thrown to user
1722 1722 currently_locked = False
1723 1723 # defines if new lock should be made, tri-state
1724 1724 make_lock = None
1725 1725 repo = self
1726 1726 user = User.get(user_id)
1727 1727
1728 1728 lock_info = repo.locked
1729 1729
1730 1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 1731 if action == 'push':
1732 1732 # check if it's already locked !, if it is compare users
1733 1733 locked_by_user_id = lock_info[0]
1734 1734 if user.user_id == locked_by_user_id:
1735 1735 log.debug(
1736 1736 'Got `push` action from user %s, now unlocking', user)
1737 1737 # unlock if we have push from user who locked
1738 1738 make_lock = False
1739 1739 else:
1740 1740 # we're not the same user who locked, ban with
1741 1741 # code defined in settings (default is 423 HTTP Locked) !
1742 1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 1743 currently_locked = True
1744 1744 elif action == 'pull':
1745 1745 # [0] user [1] date
1746 1746 if lock_info[0] and lock_info[1]:
1747 1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 1748 currently_locked = True
1749 1749 else:
1750 1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 1751 make_lock = True
1752 1752
1753 1753 else:
1754 1754 log.debug('Repository %s do not have locking enabled', repo)
1755 1755
1756 1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 1757 make_lock, currently_locked, lock_info)
1758 1758
1759 1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 1762 # if we don't have at least write permission we cannot make a lock
1763 1763 log.debug('lock state reset back to FALSE due to lack '
1764 1764 'of at least read permission')
1765 1765 make_lock = False
1766 1766
1767 1767 return make_lock, currently_locked, lock_info
1768 1768
1769 1769 @property
1770 1770 def last_db_change(self):
1771 1771 return self.updated_on
1772 1772
1773 1773 @property
1774 1774 def clone_uri_hidden(self):
1775 1775 clone_uri = self.clone_uri
1776 1776 if clone_uri:
1777 1777 import urlobject
1778 1778 url_obj = urlobject.URLObject(clone_uri)
1779 1779 if url_obj.password:
1780 1780 clone_uri = url_obj.with_password('*****')
1781 1781 return clone_uri
1782 1782
1783 1783 def clone_url(self, **override):
1784 1784 qualified_home_url = url('home', qualified=True)
1785 1785
1786 1786 uri_tmpl = None
1787 1787 if 'with_id' in override:
1788 1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 1789 del override['with_id']
1790 1790
1791 1791 if 'uri_tmpl' in override:
1792 1792 uri_tmpl = override['uri_tmpl']
1793 1793 del override['uri_tmpl']
1794 1794
1795 1795 # we didn't override our tmpl from **overrides
1796 1796 if not uri_tmpl:
1797 1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 1798 try:
1799 1799 from pylons import tmpl_context as c
1800 1800 uri_tmpl = c.clone_uri_tmpl
1801 1801 except Exception:
1802 1802 # in any case if we call this outside of request context,
1803 1803 # ie, not having tmpl_context set up
1804 1804 pass
1805 1805
1806 1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 1807 qualifed_home_url=qualified_home_url,
1808 1808 repo_name=self.repo_name,
1809 1809 repo_id=self.repo_id, **override)
1810 1810
1811 1811 def set_state(self, state):
1812 1812 self.repo_state = state
1813 1813 Session().add(self)
1814 1814 #==========================================================================
1815 1815 # SCM PROPERTIES
1816 1816 #==========================================================================
1817 1817
1818 1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 1819 return get_commit_safe(
1820 1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821 1821
1822 1822 def get_changeset(self, rev=None, pre_load=None):
1823 1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 1824 commit_id = None
1825 1825 commit_idx = None
1826 1826 if isinstance(rev, basestring):
1827 1827 commit_id = rev
1828 1828 else:
1829 1829 commit_idx = rev
1830 1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 1831 pre_load=pre_load)
1832 1832
1833 1833 def get_landing_commit(self):
1834 1834 """
1835 1835 Returns landing commit, or if that doesn't exist returns the tip
1836 1836 """
1837 1837 _rev_type, _rev = self.landing_rev
1838 1838 commit = self.get_commit(_rev)
1839 1839 if isinstance(commit, EmptyCommit):
1840 1840 return self.get_commit()
1841 1841 return commit
1842 1842
1843 1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 1844 """
1845 1845 Update cache of last changeset for repository, keys should be::
1846 1846
1847 1847 short_id
1848 1848 raw_id
1849 1849 revision
1850 1850 parents
1851 1851 message
1852 1852 date
1853 1853 author
1854 1854
1855 1855 :param cs_cache:
1856 1856 """
1857 1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 1858 if cs_cache is None:
1859 1859 # use no-cache version here
1860 1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 1861 if scm_repo:
1862 1862 cs_cache = scm_repo.get_commit(
1863 1863 pre_load=["author", "date", "message", "parents"])
1864 1864 else:
1865 1865 cs_cache = EmptyCommit()
1866 1866
1867 1867 if isinstance(cs_cache, BaseChangeset):
1868 1868 cs_cache = cs_cache.__json__()
1869 1869
1870 1870 def is_outdated(new_cs_cache):
1871 1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 1873 return True
1874 1874 return False
1875 1875
1876 1876 # check if we have maybe already latest cached revision
1877 1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 1878 _default = datetime.datetime.fromtimestamp(0)
1879 1879 last_change = cs_cache.get('date') or _default
1880 1880 log.debug('updated repo %s with new cs cache %s',
1881 1881 self.repo_name, cs_cache)
1882 1882 self.updated_on = last_change
1883 1883 self.changeset_cache = cs_cache
1884 1884 Session().add(self)
1885 1885 Session().commit()
1886 1886 else:
1887 1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 1888 'commit already with latest changes', self.repo_name)
1889 1889
1890 1890 @property
1891 1891 def tip(self):
1892 1892 return self.get_commit('tip')
1893 1893
1894 1894 @property
1895 1895 def author(self):
1896 1896 return self.tip.author
1897 1897
1898 1898 @property
1899 1899 def last_change(self):
1900 1900 return self.scm_instance().last_change
1901 1901
1902 1902 def get_comments(self, revisions=None):
1903 1903 """
1904 1904 Returns comments for this repository grouped by revisions
1905 1905
1906 1906 :param revisions: filter query by revisions only
1907 1907 """
1908 1908 cmts = ChangesetComment.query()\
1909 1909 .filter(ChangesetComment.repo == self)
1910 1910 if revisions:
1911 1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 1912 grouped = collections.defaultdict(list)
1913 1913 for cmt in cmts.all():
1914 1914 grouped[cmt.revision].append(cmt)
1915 1915 return grouped
1916 1916
1917 1917 def statuses(self, revisions=None):
1918 1918 """
1919 1919 Returns statuses for this repository
1920 1920
1921 1921 :param revisions: list of revisions to get statuses for
1922 1922 """
1923 1923 statuses = ChangesetStatus.query()\
1924 1924 .filter(ChangesetStatus.repo == self)\
1925 1925 .filter(ChangesetStatus.version == 0)
1926 1926
1927 1927 if revisions:
1928 1928 # Try doing the filtering in chunks to avoid hitting limits
1929 1929 size = 500
1930 1930 status_results = []
1931 1931 for chunk in xrange(0, len(revisions), size):
1932 1932 status_results += statuses.filter(
1933 1933 ChangesetStatus.revision.in_(
1934 1934 revisions[chunk: chunk+size])
1935 1935 ).all()
1936 1936 else:
1937 1937 status_results = statuses.all()
1938 1938
1939 1939 grouped = {}
1940 1940
1941 1941 # maybe we have open new pullrequest without a status?
1942 1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 1945 for rev in pr.revisions:
1946 1946 pr_id = pr.pull_request_id
1947 1947 pr_repo = pr.target_repo.repo_name
1948 1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949 1949
1950 1950 for stat in status_results:
1951 1951 pr_id = pr_repo = None
1952 1952 if stat.pull_request:
1953 1953 pr_id = stat.pull_request.pull_request_id
1954 1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 1956 pr_id, pr_repo]
1957 1957 return grouped
1958 1958
1959 1959 # ==========================================================================
1960 1960 # SCM CACHE INSTANCE
1961 1961 # ==========================================================================
1962 1962
1963 1963 def scm_instance(self, **kwargs):
1964 1964 import rhodecode
1965 1965
1966 1966 # Passing a config will not hit the cache currently only used
1967 1967 # for repo2dbmapper
1968 1968 config = kwargs.pop('config', None)
1969 1969 cache = kwargs.pop('cache', None)
1970 1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 1971 # if cache is NOT defined use default global, else we have a full
1972 1972 # control over cache behaviour
1973 1973 if cache is None and full_cache and not config:
1974 1974 return self._get_instance_cached()
1975 1975 return self._get_instance(cache=bool(cache), config=config)
1976 1976
1977 1977 def _get_instance_cached(self):
1978 1978 @cache_region('long_term')
1979 1979 def _get_repo(cache_key):
1980 1980 return self._get_instance()
1981 1981
1982 1982 invalidator_context = CacheKey.repo_context_cache(
1983 1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984 1984
1985 1985 with invalidator_context as context:
1986 1986 context.invalidate()
1987 1987 repo = context.compute()
1988 1988
1989 1989 return repo
1990 1990
1991 1991 def _get_instance(self, cache=True, config=None):
1992 1992 config = config or self._config
1993 1993 custom_wire = {
1994 1994 'cache': cache # controls the vcs.remote cache
1995 1995 }
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False,
2001 2001 _vcs_alias=self.repo_type)
2002 2002
2003 2003 return repo
2004 2004
2005 2005 def __json__(self):
2006 2006 return {'landing_rev': self.landing_rev}
2007 2007
2008 2008 def get_dict(self):
2009 2009
2010 2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 2011 # keep compatibility with the code which uses `repo_name` field.
2012 2012
2013 2013 result = super(Repository, self).get_dict()
2014 2014 result['repo_name'] = result.pop('_repo_name', None)
2015 2015 return result
2016 2016
2017 2017
2018 2018 class RepoGroup(Base, BaseModel):
2019 2019 __tablename__ = 'groups'
2020 2020 __table_args__ = (
2021 2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 2022 CheckConstraint('group_id != group_parent_id'),
2023 2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2025 )
2026 2026 __mapper_args__ = {'order_by': 'group_name'}
2027 2027
2028 2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029 2029
2030 2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038 2038
2039 2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 2042 user = relationship('User')
2043 2043 integrations = relationship('Integration',
2044 2044 cascade="all, delete, delete-orphan")
2045 2045
2046 2046 def __init__(self, group_name='', parent_group=None):
2047 2047 self.group_name = group_name
2048 2048 self.parent_group = parent_group
2049 2049
2050 2050 def __unicode__(self):
2051 2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 2052 self.group_name)
2053 2053
2054 2054 @classmethod
2055 2055 def _generate_choice(cls, repo_group):
2056 2056 from webhelpers.html import literal as _literal
2057 2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059 2059
2060 2060 @classmethod
2061 2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 2062 if not groups:
2063 2063 groups = cls.query().all()
2064 2064
2065 2065 repo_groups = []
2066 2066 if show_empty_group:
2067 2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068 2068
2069 2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070 2070
2071 2071 repo_groups = sorted(
2072 2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 2073 return repo_groups
2074 2074
2075 2075 @classmethod
2076 2076 def url_sep(cls):
2077 2077 return URL_SEP
2078 2078
2079 2079 @classmethod
2080 2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 2081 if case_insensitive:
2082 2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 2083 == func.lower(group_name))
2084 2084 else:
2085 2085 gr = cls.query().filter(cls.group_name == group_name)
2086 2086 if cache:
2087 2087 gr = gr.options(FromCache(
2088 2088 "sql_cache_short",
2089 2089 "get_group_%s" % _hash_key(group_name)))
2090 2090 return gr.scalar()
2091 2091
2092 2092 @classmethod
2093 2093 def get_user_personal_repo_group(cls, user_id):
2094 2094 user = User.get(user_id)
2095 2095 return cls.query()\
2096 2096 .filter(cls.personal == true())\
2097 2097 .filter(cls.user == user).scalar()
2098 2098
2099 2099 @classmethod
2100 2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 2101 case_insensitive=True):
2102 2102 q = RepoGroup.query()
2103 2103
2104 2104 if not isinstance(user_id, Optional):
2105 2105 q = q.filter(RepoGroup.user_id == user_id)
2106 2106
2107 2107 if not isinstance(group_id, Optional):
2108 2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109 2109
2110 2110 if case_insensitive:
2111 2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 2112 else:
2113 2113 q = q.order_by(RepoGroup.group_name)
2114 2114 return q.all()
2115 2115
2116 2116 @property
2117 2117 def parents(self):
2118 2118 parents_recursion_limit = 10
2119 2119 groups = []
2120 2120 if self.parent_group is None:
2121 2121 return groups
2122 2122 cur_gr = self.parent_group
2123 2123 groups.insert(0, cur_gr)
2124 2124 cnt = 0
2125 2125 while 1:
2126 2126 cnt += 1
2127 2127 gr = getattr(cur_gr, 'parent_group', None)
2128 2128 cur_gr = cur_gr.parent_group
2129 2129 if gr is None:
2130 2130 break
2131 2131 if cnt == parents_recursion_limit:
2132 2132 # this will prevent accidental infinit loops
2133 2133 log.error(('more than %s parents found for group %s, stopping '
2134 2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 2135 break
2136 2136
2137 2137 groups.insert(0, gr)
2138 2138 return groups
2139 2139
2140 2140 @property
2141 2141 def children(self):
2142 2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143 2143
2144 2144 @property
2145 2145 def name(self):
2146 2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147 2147
2148 2148 @property
2149 2149 def full_path(self):
2150 2150 return self.group_name
2151 2151
2152 2152 @property
2153 2153 def full_path_splitted(self):
2154 2154 return self.group_name.split(RepoGroup.url_sep())
2155 2155
2156 2156 @property
2157 2157 def repositories(self):
2158 2158 return Repository.query()\
2159 2159 .filter(Repository.group == self)\
2160 2160 .order_by(Repository.repo_name)
2161 2161
2162 2162 @property
2163 2163 def repositories_recursive_count(self):
2164 2164 cnt = self.repositories.count()
2165 2165
2166 2166 def children_count(group):
2167 2167 cnt = 0
2168 2168 for child in group.children:
2169 2169 cnt += child.repositories.count()
2170 2170 cnt += children_count(child)
2171 2171 return cnt
2172 2172
2173 2173 return cnt + children_count(self)
2174 2174
2175 2175 def _recursive_objects(self, include_repos=True):
2176 2176 all_ = []
2177 2177
2178 2178 def _get_members(root_gr):
2179 2179 if include_repos:
2180 2180 for r in root_gr.repositories:
2181 2181 all_.append(r)
2182 2182 childs = root_gr.children.all()
2183 2183 if childs:
2184 2184 for gr in childs:
2185 2185 all_.append(gr)
2186 2186 _get_members(gr)
2187 2187
2188 2188 _get_members(self)
2189 2189 return [self] + all_
2190 2190
2191 2191 def recursive_groups_and_repos(self):
2192 2192 """
2193 2193 Recursive return all groups, with repositories in those groups
2194 2194 """
2195 2195 return self._recursive_objects()
2196 2196
2197 2197 def recursive_groups(self):
2198 2198 """
2199 2199 Returns all children groups for this group including children of children
2200 2200 """
2201 2201 return self._recursive_objects(include_repos=False)
2202 2202
2203 2203 def get_new_name(self, group_name):
2204 2204 """
2205 2205 returns new full group name based on parent and new name
2206 2206
2207 2207 :param group_name:
2208 2208 """
2209 2209 path_prefix = (self.parent_group.full_path_splitted if
2210 2210 self.parent_group else [])
2211 2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212 2212
2213 2213 def permissions(self, with_admins=True, with_owner=True):
2214 2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 2216 joinedload(UserRepoGroupToPerm.user),
2217 2217 joinedload(UserRepoGroupToPerm.permission),)
2218 2218
2219 2219 # get owners and admins and permissions. We do a trick of re-writing
2220 2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 2221 # has a global reference and changing one object propagates to all
2222 2222 # others. This means if admin is also an owner admin_row that change
2223 2223 # would propagate to both objects
2224 2224 perm_rows = []
2225 2225 for _usr in q.all():
2226 2226 usr = AttributeDict(_usr.user.get_dict())
2227 2227 usr.permission = _usr.permission.permission_name
2228 2228 perm_rows.append(usr)
2229 2229
2230 2230 # filter the perm rows by 'default' first and then sort them by
2231 2231 # admin,write,read,none permissions sorted again alphabetically in
2232 2232 # each group
2233 2233 perm_rows = sorted(perm_rows, key=display_sort)
2234 2234
2235 2235 _admin_perm = 'group.admin'
2236 2236 owner_row = []
2237 2237 if with_owner:
2238 2238 usr = AttributeDict(self.user.get_dict())
2239 2239 usr.owner_row = True
2240 2240 usr.permission = _admin_perm
2241 2241 owner_row.append(usr)
2242 2242
2243 2243 super_admin_rows = []
2244 2244 if with_admins:
2245 2245 for usr in User.get_all_super_admins():
2246 2246 # if this admin is also owner, don't double the record
2247 2247 if usr.user_id == owner_row[0].user_id:
2248 2248 owner_row[0].admin_row = True
2249 2249 else:
2250 2250 usr = AttributeDict(usr.get_dict())
2251 2251 usr.admin_row = True
2252 2252 usr.permission = _admin_perm
2253 2253 super_admin_rows.append(usr)
2254 2254
2255 2255 return super_admin_rows + owner_row + perm_rows
2256 2256
2257 2257 def permission_user_groups(self):
2258 2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262 2262
2263 2263 perm_rows = []
2264 2264 for _user_group in q.all():
2265 2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 2266 usr.permission = _user_group.permission.permission_name
2267 2267 perm_rows.append(usr)
2268 2268
2269 2269 return perm_rows
2270 2270
2271 2271 def get_api_data(self):
2272 2272 """
2273 2273 Common function for generating api data
2274 2274
2275 2275 """
2276 2276 group = self
2277 2277 data = {
2278 2278 'group_id': group.group_id,
2279 2279 'group_name': group.group_name,
2280 2280 'group_description': group.group_description,
2281 2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 2282 'repositories': [x.repo_name for x in group.repositories],
2283 2283 'owner': group.user.username,
2284 2284 }
2285 2285 return data
2286 2286
2287 2287
2288 2288 class Permission(Base, BaseModel):
2289 2289 __tablename__ = 'permissions'
2290 2290 __table_args__ = (
2291 2291 Index('p_perm_name_idx', 'permission_name'),
2292 2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 2294 )
2295 2295 PERMS = [
2296 2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297 2297
2298 2298 ('repository.none', _('Repository no access')),
2299 2299 ('repository.read', _('Repository read access')),
2300 2300 ('repository.write', _('Repository write access')),
2301 2301 ('repository.admin', _('Repository admin access')),
2302 2302
2303 2303 ('group.none', _('Repository group no access')),
2304 2304 ('group.read', _('Repository group read access')),
2305 2305 ('group.write', _('Repository group write access')),
2306 2306 ('group.admin', _('Repository group admin access')),
2307 2307
2308 2308 ('usergroup.none', _('User group no access')),
2309 2309 ('usergroup.read', _('User group read access')),
2310 2310 ('usergroup.write', _('User group write access')),
2311 2311 ('usergroup.admin', _('User group admin access')),
2312 2312
2313 2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315 2315
2316 2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318 2318
2319 2319 ('hg.create.none', _('Repository creation disabled')),
2320 2320 ('hg.create.repository', _('Repository creation enabled')),
2321 2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323 2323
2324 2324 ('hg.fork.none', _('Repository forking disabled')),
2325 2325 ('hg.fork.repository', _('Repository forking enabled')),
2326 2326
2327 2327 ('hg.register.none', _('Registration disabled')),
2328 2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330 2330
2331 2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334 2334
2335 2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337 2337
2338 2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 2340 ]
2341 2341
2342 2342 # definition of system default permissions for DEFAULT user
2343 2343 DEFAULT_USER_PERMISSIONS = [
2344 2344 'repository.read',
2345 2345 'group.read',
2346 2346 'usergroup.read',
2347 2347 'hg.create.repository',
2348 2348 'hg.repogroup.create.false',
2349 2349 'hg.usergroup.create.false',
2350 2350 'hg.create.write_on_repogroup.true',
2351 2351 'hg.fork.repository',
2352 2352 'hg.register.manual_activate',
2353 2353 'hg.password_reset.enabled',
2354 2354 'hg.extern_activate.auto',
2355 2355 'hg.inherit_default_perms.true',
2356 2356 ]
2357 2357
2358 2358 # defines which permissions are more important higher the more important
2359 2359 # Weight defines which permissions are more important.
2360 2360 # The higher number the more important.
2361 2361 PERM_WEIGHTS = {
2362 2362 'repository.none': 0,
2363 2363 'repository.read': 1,
2364 2364 'repository.write': 3,
2365 2365 'repository.admin': 4,
2366 2366
2367 2367 'group.none': 0,
2368 2368 'group.read': 1,
2369 2369 'group.write': 3,
2370 2370 'group.admin': 4,
2371 2371
2372 2372 'usergroup.none': 0,
2373 2373 'usergroup.read': 1,
2374 2374 'usergroup.write': 3,
2375 2375 'usergroup.admin': 4,
2376 2376
2377 2377 'hg.repogroup.create.false': 0,
2378 2378 'hg.repogroup.create.true': 1,
2379 2379
2380 2380 'hg.usergroup.create.false': 0,
2381 2381 'hg.usergroup.create.true': 1,
2382 2382
2383 2383 'hg.fork.none': 0,
2384 2384 'hg.fork.repository': 1,
2385 2385 'hg.create.none': 0,
2386 2386 'hg.create.repository': 1
2387 2387 }
2388 2388
2389 2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392 2392
2393 2393 def __unicode__(self):
2394 2394 return u"<%s('%s:%s')>" % (
2395 2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 2396 )
2397 2397
2398 2398 @classmethod
2399 2399 def get_by_key(cls, key):
2400 2400 return cls.query().filter(cls.permission_name == key).scalar()
2401 2401
2402 2402 @classmethod
2403 2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 2407 .filter(UserRepoToPerm.user_id == user_id)
2408 2408 if repo_id:
2409 2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 2410 return q.all()
2411 2411
2412 2412 @classmethod
2413 2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 2415 .join(
2416 2416 Permission,
2417 2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 2418 .join(
2419 2419 Repository,
2420 2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 2421 .join(
2422 2422 UserGroup,
2423 2423 UserGroupRepoToPerm.users_group_id ==
2424 2424 UserGroup.users_group_id)\
2425 2425 .join(
2426 2426 UserGroupMember,
2427 2427 UserGroupRepoToPerm.users_group_id ==
2428 2428 UserGroupMember.users_group_id)\
2429 2429 .filter(
2430 2430 UserGroupMember.user_id == user_id,
2431 2431 UserGroup.users_group_active == true())
2432 2432 if repo_id:
2433 2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 2434 return q.all()
2435 2435
2436 2436 @classmethod
2437 2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 2442 if repo_group_id:
2443 2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 2444 return q.all()
2445 2445
2446 2446 @classmethod
2447 2447 def get_default_group_perms_from_user_group(
2448 2448 cls, user_id, repo_group_id=None):
2449 2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 2450 .join(
2451 2451 Permission,
2452 2452 UserGroupRepoGroupToPerm.permission_id ==
2453 2453 Permission.permission_id)\
2454 2454 .join(
2455 2455 RepoGroup,
2456 2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 2457 .join(
2458 2458 UserGroup,
2459 2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 2460 UserGroup.users_group_id)\
2461 2461 .join(
2462 2462 UserGroupMember,
2463 2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 2464 UserGroupMember.users_group_id)\
2465 2465 .filter(
2466 2466 UserGroupMember.user_id == user_id,
2467 2467 UserGroup.users_group_active == true())
2468 2468 if repo_group_id:
2469 2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 2470 return q.all()
2471 2471
2472 2472 @classmethod
2473 2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 2478 if user_group_id:
2479 2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 2480 return q.all()
2481 2481
2482 2482 @classmethod
2483 2483 def get_default_user_group_perms_from_user_group(
2484 2484 cls, user_id, user_group_id=None):
2485 2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 2487 .join(
2488 2488 Permission,
2489 2489 UserGroupUserGroupToPerm.permission_id ==
2490 2490 Permission.permission_id)\
2491 2491 .join(
2492 2492 TargetUserGroup,
2493 2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 2494 TargetUserGroup.users_group_id)\
2495 2495 .join(
2496 2496 UserGroup,
2497 2497 UserGroupUserGroupToPerm.user_group_id ==
2498 2498 UserGroup.users_group_id)\
2499 2499 .join(
2500 2500 UserGroupMember,
2501 2501 UserGroupUserGroupToPerm.user_group_id ==
2502 2502 UserGroupMember.users_group_id)\
2503 2503 .filter(
2504 2504 UserGroupMember.user_id == user_id,
2505 2505 UserGroup.users_group_active == true())
2506 2506 if user_group_id:
2507 2507 q = q.filter(
2508 2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509 2509
2510 2510 return q.all()
2511 2511
2512 2512
2513 2513 class UserRepoToPerm(Base, BaseModel):
2514 2514 __tablename__ = 'repo_to_perm'
2515 2515 __table_args__ = (
2516 2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 2519 )
2520 2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524 2524
2525 2525 user = relationship('User')
2526 2526 repository = relationship('Repository')
2527 2527 permission = relationship('Permission')
2528 2528
2529 2529 @classmethod
2530 2530 def create(cls, user, repository, permission):
2531 2531 n = cls()
2532 2532 n.user = user
2533 2533 n.repository = repository
2534 2534 n.permission = permission
2535 2535 Session().add(n)
2536 2536 return n
2537 2537
2538 2538 def __unicode__(self):
2539 2539 return u'<%s => %s >' % (self.user, self.repository)
2540 2540
2541 2541
2542 2542 class UserUserGroupToPerm(Base, BaseModel):
2543 2543 __tablename__ = 'user_user_group_to_perm'
2544 2544 __table_args__ = (
2545 2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 2548 )
2549 2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553 2553
2554 2554 user = relationship('User')
2555 2555 user_group = relationship('UserGroup')
2556 2556 permission = relationship('Permission')
2557 2557
2558 2558 @classmethod
2559 2559 def create(cls, user, user_group, permission):
2560 2560 n = cls()
2561 2561 n.user = user
2562 2562 n.user_group = user_group
2563 2563 n.permission = permission
2564 2564 Session().add(n)
2565 2565 return n
2566 2566
2567 2567 def __unicode__(self):
2568 2568 return u'<%s => %s >' % (self.user, self.user_group)
2569 2569
2570 2570
2571 2571 class UserToPerm(Base, BaseModel):
2572 2572 __tablename__ = 'user_to_perm'
2573 2573 __table_args__ = (
2574 2574 UniqueConstraint('user_id', 'permission_id'),
2575 2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2577 )
2578 2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2581
2582 2582 user = relationship('User')
2583 2583 permission = relationship('Permission', lazy='joined')
2584 2584
2585 2585 def __unicode__(self):
2586 2586 return u'<%s => %s >' % (self.user, self.permission)
2587 2587
2588 2588
2589 2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 2590 __tablename__ = 'users_group_repo_to_perm'
2591 2591 __table_args__ = (
2592 2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 2595 )
2596 2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600 2600
2601 2601 users_group = relationship('UserGroup')
2602 2602 permission = relationship('Permission')
2603 2603 repository = relationship('Repository')
2604 2604
2605 2605 @classmethod
2606 2606 def create(cls, users_group, repository, permission):
2607 2607 n = cls()
2608 2608 n.users_group = users_group
2609 2609 n.repository = repository
2610 2610 n.permission = permission
2611 2611 Session().add(n)
2612 2612 return n
2613 2613
2614 2614 def __unicode__(self):
2615 2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616 2616
2617 2617
2618 2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 2619 __tablename__ = 'user_group_user_group_to_perm'
2620 2620 __table_args__ = (
2621 2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2625 )
2626 2626 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2627 2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630 2630
2631 2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 2633 permission = relationship('Permission')
2634 2634
2635 2635 @classmethod
2636 2636 def create(cls, target_user_group, user_group, permission):
2637 2637 n = cls()
2638 2638 n.target_user_group = target_user_group
2639 2639 n.user_group = user_group
2640 2640 n.permission = permission
2641 2641 Session().add(n)
2642 2642 return n
2643 2643
2644 2644 def __unicode__(self):
2645 2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646 2646
2647 2647
2648 2648 class UserGroupToPerm(Base, BaseModel):
2649 2649 __tablename__ = 'users_group_to_perm'
2650 2650 __table_args__ = (
2651 2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 2654 )
2655 2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 2658
2659 2659 users_group = relationship('UserGroup')
2660 2660 permission = relationship('Permission')
2661 2661
2662 2662
2663 2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 2664 __tablename__ = 'user_repo_group_to_perm'
2665 2665 __table_args__ = (
2666 2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 2669 )
2670 2670
2671 2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675 2675
2676 2676 user = relationship('User')
2677 2677 group = relationship('RepoGroup')
2678 2678 permission = relationship('Permission')
2679 2679
2680 2680 @classmethod
2681 2681 def create(cls, user, repository_group, permission):
2682 2682 n = cls()
2683 2683 n.user = user
2684 2684 n.group = repository_group
2685 2685 n.permission = permission
2686 2686 Session().add(n)
2687 2687 return n
2688 2688
2689 2689
2690 2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 2692 __table_args__ = (
2693 2693 UniqueConstraint('users_group_id', 'group_id'),
2694 2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 2696 )
2697 2697
2698 2698 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2699 2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 2702
2703 2703 users_group = relationship('UserGroup')
2704 2704 permission = relationship('Permission')
2705 2705 group = relationship('RepoGroup')
2706 2706
2707 2707 @classmethod
2708 2708 def create(cls, user_group, repository_group, permission):
2709 2709 n = cls()
2710 2710 n.users_group = user_group
2711 2711 n.group = repository_group
2712 2712 n.permission = permission
2713 2713 Session().add(n)
2714 2714 return n
2715 2715
2716 2716 def __unicode__(self):
2717 2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718 2718
2719 2719
2720 2720 class Statistics(Base, BaseModel):
2721 2721 __tablename__ = 'statistics'
2722 2722 __table_args__ = (
2723 2723 UniqueConstraint('repository_id'),
2724 2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2726 )
2727 2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733 2733
2734 2734 repository = relationship('Repository', single_parent=True)
2735 2735
2736 2736
2737 2737 class UserFollowing(Base, BaseModel):
2738 2738 __tablename__ = 'user_followings'
2739 2739 __table_args__ = (
2740 2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2744 )
2745 2745
2746 2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751 2751
2752 2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753 2753
2754 2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756 2756
2757 2757 @classmethod
2758 2758 def get_repo_followers(cls, repo_id):
2759 2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760 2760
2761 2761
2762 2762 class CacheKey(Base, BaseModel):
2763 2763 __tablename__ = 'cache_invalidation'
2764 2764 __table_args__ = (
2765 2765 UniqueConstraint('cache_key'),
2766 2766 Index('key_idx', 'cache_key'),
2767 2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 2769 )
2770 2770 CACHE_TYPE_ATOM = 'ATOM'
2771 2771 CACHE_TYPE_RSS = 'RSS'
2772 2772 CACHE_TYPE_README = 'README'
2773 2773
2774 2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778 2778
2779 2779 def __init__(self, cache_key, cache_args=''):
2780 2780 self.cache_key = cache_key
2781 2781 self.cache_args = cache_args
2782 2782 self.cache_active = False
2783 2783
2784 2784 def __unicode__(self):
2785 2785 return u"<%s('%s:%s[%s]')>" % (
2786 2786 self.__class__.__name__,
2787 2787 self.cache_id, self.cache_key, self.cache_active)
2788 2788
2789 2789 def _cache_key_partition(self):
2790 2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 2791 return prefix, repo_name, suffix
2792 2792
2793 2793 def get_prefix(self):
2794 2794 """
2795 2795 Try to extract prefix from existing cache key. The key could consist
2796 2796 of prefix, repo_name, suffix
2797 2797 """
2798 2798 # this returns prefix, repo_name, suffix
2799 2799 return self._cache_key_partition()[0]
2800 2800
2801 2801 def get_suffix(self):
2802 2802 """
2803 2803 get suffix that might have been used in _get_cache_key to
2804 2804 generate self.cache_key. Only used for informational purposes
2805 2805 in repo_edit.mako.
2806 2806 """
2807 2807 # prefix, repo_name, suffix
2808 2808 return self._cache_key_partition()[2]
2809 2809
2810 2810 @classmethod
2811 2811 def delete_all_cache(cls):
2812 2812 """
2813 2813 Delete all cache keys from database.
2814 2814 Should only be run when all instances are down and all entries
2815 2815 thus stale.
2816 2816 """
2817 2817 cls.query().delete()
2818 2818 Session().commit()
2819 2819
2820 2820 @classmethod
2821 2821 def get_cache_key(cls, repo_name, cache_type):
2822 2822 """
2823 2823
2824 2824 Generate a cache key for this process of RhodeCode instance.
2825 2825 Prefix most likely will be process id or maybe explicitly set
2826 2826 instance_id from .ini file.
2827 2827 """
2828 2828 import rhodecode
2829 2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830 2830
2831 2831 repo_as_unicode = safe_unicode(repo_name)
2832 2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 2833 if cache_type else repo_as_unicode
2834 2834
2835 2835 return u'{}{}'.format(prefix, key)
2836 2836
2837 2837 @classmethod
2838 2838 def set_invalidate(cls, repo_name, delete=False):
2839 2839 """
2840 2840 Mark all caches of a repo as invalid in the database.
2841 2841 """
2842 2842
2843 2843 try:
2844 2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 2845 if delete:
2846 2846 log.debug('cache objects deleted for repo %s',
2847 2847 safe_str(repo_name))
2848 2848 qry.delete()
2849 2849 else:
2850 2850 log.debug('cache objects marked as invalid for repo %s',
2851 2851 safe_str(repo_name))
2852 2852 qry.update({"cache_active": False})
2853 2853
2854 2854 Session().commit()
2855 2855 except Exception:
2856 2856 log.exception(
2857 2857 'Cache key invalidation failed for repository %s',
2858 2858 safe_str(repo_name))
2859 2859 Session().rollback()
2860 2860
2861 2861 @classmethod
2862 2862 def get_active_cache(cls, cache_key):
2863 2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 2864 if inv_obj:
2865 2865 return inv_obj
2866 2866 return None
2867 2867
2868 2868 @classmethod
2869 2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 2870 thread_scoped=False):
2871 2871 """
2872 2872 @cache_region('long_term')
2873 2873 def _heavy_calculation(cache_key):
2874 2874 return 'result'
2875 2875
2876 2876 cache_context = CacheKey.repo_context_cache(
2877 2877 _heavy_calculation, repo_name, cache_type)
2878 2878
2879 2879 with cache_context as context:
2880 2880 context.invalidate()
2881 2881 computed = context.compute()
2882 2882
2883 2883 assert computed == 'result'
2884 2884 """
2885 2885 from rhodecode.lib import caches
2886 2886 return caches.InvalidationContext(
2887 2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888 2888
2889 2889
2890 2890 class ChangesetComment(Base, BaseModel):
2891 2891 __tablename__ = 'changeset_comments'
2892 2892 __table_args__ = (
2893 2893 Index('cc_revision_idx', 'revision'),
2894 2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 2896 )
2897 2897
2898 2898 COMMENT_OUTDATED = u'comment_outdated'
2899 2899 COMMENT_TYPE_NOTE = u'note'
2900 2900 COMMENT_TYPE_TODO = u'todo'
2901 2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2902 2902
2903 2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2904 2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2905 2905 revision = Column('revision', String(40), nullable=True)
2906 2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2907 2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2908 2908 line_no = Column('line_no', Unicode(10), nullable=True)
2909 2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2910 2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2911 2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2912 2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2913 2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2915 2915 renderer = Column('renderer', Unicode(64), nullable=True)
2916 2916 display_state = Column('display_state', Unicode(128), nullable=True)
2917 2917
2918 2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2921 2921 author = relationship('User', lazy='joined')
2922 2922 repo = relationship('Repository')
2923 2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2924 2924 pull_request = relationship('PullRequest', lazy='joined')
2925 2925 pull_request_version = relationship('PullRequestVersion')
2926 2926
2927 2927 @classmethod
2928 2928 def get_users(cls, revision=None, pull_request_id=None):
2929 2929 """
2930 2930 Returns user associated with this ChangesetComment. ie those
2931 2931 who actually commented
2932 2932
2933 2933 :param cls:
2934 2934 :param revision:
2935 2935 """
2936 2936 q = Session().query(User)\
2937 2937 .join(ChangesetComment.author)
2938 2938 if revision:
2939 2939 q = q.filter(cls.revision == revision)
2940 2940 elif pull_request_id:
2941 2941 q = q.filter(cls.pull_request_id == pull_request_id)
2942 2942 return q.all()
2943 2943
2944 2944 @classmethod
2945 2945 def get_index_from_version(cls, pr_version, versions):
2946 2946 num_versions = [x.pull_request_version_id for x in versions]
2947 2947 try:
2948 2948 return num_versions.index(pr_version) +1
2949 2949 except (IndexError, ValueError):
2950 2950 return
2951 2951
2952 2952 @property
2953 2953 def outdated(self):
2954 2954 return self.display_state == self.COMMENT_OUTDATED
2955 2955
2956 2956 def outdated_at_version(self, version):
2957 2957 """
2958 2958 Checks if comment is outdated for given pull request version
2959 2959 """
2960 2960 return self.outdated and self.pull_request_version_id != version
2961 2961
2962 2962 def older_than_version(self, version):
2963 2963 """
2964 2964 Checks if comment is made from previous version than given
2965 2965 """
2966 2966 if version is None:
2967 2967 return self.pull_request_version_id is not None
2968 2968
2969 2969 return self.pull_request_version_id < version
2970 2970
2971 2971 @property
2972 2972 def resolved(self):
2973 2973 return self.resolved_by[0] if self.resolved_by else None
2974 2974
2975 @property
2976 def is_todo(self):
2977 return self.comment_type == self.COMMENT_TYPE_TODO
2978
2975 2979 def get_index_version(self, versions):
2976 2980 return self.get_index_from_version(
2977 2981 self.pull_request_version_id, versions)
2978 2982
2979 2983 def render(self, mentions=False):
2980 2984 from rhodecode.lib import helpers as h
2981 2985 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2982 2986
2983 2987 def __repr__(self):
2984 2988 if self.comment_id:
2985 2989 return '<DB:Comment #%s>' % self.comment_id
2986 2990 else:
2987 2991 return '<DB:Comment at %#x>' % id(self)
2988 2992
2989 2993
2990 2994 class ChangesetStatus(Base, BaseModel):
2991 2995 __tablename__ = 'changeset_statuses'
2992 2996 __table_args__ = (
2993 2997 Index('cs_revision_idx', 'revision'),
2994 2998 Index('cs_version_idx', 'version'),
2995 2999 UniqueConstraint('repo_id', 'revision', 'version'),
2996 3000 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2997 3001 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2998 3002 )
2999 3003 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3000 3004 STATUS_APPROVED = 'approved'
3001 3005 STATUS_REJECTED = 'rejected'
3002 3006 STATUS_UNDER_REVIEW = 'under_review'
3003 3007
3004 3008 STATUSES = [
3005 3009 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3006 3010 (STATUS_APPROVED, _("Approved")),
3007 3011 (STATUS_REJECTED, _("Rejected")),
3008 3012 (STATUS_UNDER_REVIEW, _("Under Review")),
3009 3013 ]
3010 3014
3011 3015 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3012 3016 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3013 3017 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3014 3018 revision = Column('revision', String(40), nullable=False)
3015 3019 status = Column('status', String(128), nullable=False, default=DEFAULT)
3016 3020 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3017 3021 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3018 3022 version = Column('version', Integer(), nullable=False, default=0)
3019 3023 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3020 3024
3021 3025 author = relationship('User', lazy='joined')
3022 3026 repo = relationship('Repository')
3023 3027 comment = relationship('ChangesetComment', lazy='joined')
3024 3028 pull_request = relationship('PullRequest', lazy='joined')
3025 3029
3026 3030 def __unicode__(self):
3027 3031 return u"<%s('%s[%s]:%s')>" % (
3028 3032 self.__class__.__name__,
3029 3033 self.status, self.version, self.author
3030 3034 )
3031 3035
3032 3036 @classmethod
3033 3037 def get_status_lbl(cls, value):
3034 3038 return dict(cls.STATUSES).get(value)
3035 3039
3036 3040 @property
3037 3041 def status_lbl(self):
3038 3042 return ChangesetStatus.get_status_lbl(self.status)
3039 3043
3040 3044
3041 3045 class _PullRequestBase(BaseModel):
3042 3046 """
3043 3047 Common attributes of pull request and version entries.
3044 3048 """
3045 3049
3046 3050 # .status values
3047 3051 STATUS_NEW = u'new'
3048 3052 STATUS_OPEN = u'open'
3049 3053 STATUS_CLOSED = u'closed'
3050 3054
3051 3055 title = Column('title', Unicode(255), nullable=True)
3052 3056 description = Column(
3053 3057 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3054 3058 nullable=True)
3055 3059 # new/open/closed status of pull request (not approve/reject/etc)
3056 3060 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3057 3061 created_on = Column(
3058 3062 'created_on', DateTime(timezone=False), nullable=False,
3059 3063 default=datetime.datetime.now)
3060 3064 updated_on = Column(
3061 3065 'updated_on', DateTime(timezone=False), nullable=False,
3062 3066 default=datetime.datetime.now)
3063 3067
3064 3068 @declared_attr
3065 3069 def user_id(cls):
3066 3070 return Column(
3067 3071 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3068 3072 unique=None)
3069 3073
3070 3074 # 500 revisions max
3071 3075 _revisions = Column(
3072 3076 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3073 3077
3074 3078 @declared_attr
3075 3079 def source_repo_id(cls):
3076 3080 # TODO: dan: rename column to source_repo_id
3077 3081 return Column(
3078 3082 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3079 3083 nullable=False)
3080 3084
3081 3085 source_ref = Column('org_ref', Unicode(255), nullable=False)
3082 3086
3083 3087 @declared_attr
3084 3088 def target_repo_id(cls):
3085 3089 # TODO: dan: rename column to target_repo_id
3086 3090 return Column(
3087 3091 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3088 3092 nullable=False)
3089 3093
3090 3094 target_ref = Column('other_ref', Unicode(255), nullable=False)
3091 3095 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3092 3096
3093 3097 # TODO: dan: rename column to last_merge_source_rev
3094 3098 _last_merge_source_rev = Column(
3095 3099 'last_merge_org_rev', String(40), nullable=True)
3096 3100 # TODO: dan: rename column to last_merge_target_rev
3097 3101 _last_merge_target_rev = Column(
3098 3102 'last_merge_other_rev', String(40), nullable=True)
3099 3103 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3100 3104 merge_rev = Column('merge_rev', String(40), nullable=True)
3101 3105
3102 3106 @hybrid_property
3103 3107 def revisions(self):
3104 3108 return self._revisions.split(':') if self._revisions else []
3105 3109
3106 3110 @revisions.setter
3107 3111 def revisions(self, val):
3108 3112 self._revisions = ':'.join(val)
3109 3113
3110 3114 @declared_attr
3111 3115 def author(cls):
3112 3116 return relationship('User', lazy='joined')
3113 3117
3114 3118 @declared_attr
3115 3119 def source_repo(cls):
3116 3120 return relationship(
3117 3121 'Repository',
3118 3122 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3119 3123
3120 3124 @property
3121 3125 def source_ref_parts(self):
3122 3126 return self.unicode_to_reference(self.source_ref)
3123 3127
3124 3128 @declared_attr
3125 3129 def target_repo(cls):
3126 3130 return relationship(
3127 3131 'Repository',
3128 3132 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3129 3133
3130 3134 @property
3131 3135 def target_ref_parts(self):
3132 3136 return self.unicode_to_reference(self.target_ref)
3133 3137
3134 3138 @property
3135 3139 def shadow_merge_ref(self):
3136 3140 return self.unicode_to_reference(self._shadow_merge_ref)
3137 3141
3138 3142 @shadow_merge_ref.setter
3139 3143 def shadow_merge_ref(self, ref):
3140 3144 self._shadow_merge_ref = self.reference_to_unicode(ref)
3141 3145
3142 3146 def unicode_to_reference(self, raw):
3143 3147 """
3144 3148 Convert a unicode (or string) to a reference object.
3145 3149 If unicode evaluates to False it returns None.
3146 3150 """
3147 3151 if raw:
3148 3152 refs = raw.split(':')
3149 3153 return Reference(*refs)
3150 3154 else:
3151 3155 return None
3152 3156
3153 3157 def reference_to_unicode(self, ref):
3154 3158 """
3155 3159 Convert a reference object to unicode.
3156 3160 If reference is None it returns None.
3157 3161 """
3158 3162 if ref:
3159 3163 return u':'.join(ref)
3160 3164 else:
3161 3165 return None
3162 3166
3163 3167 def get_api_data(self):
3164 3168 from rhodecode.model.pull_request import PullRequestModel
3165 3169 pull_request = self
3166 3170 merge_status = PullRequestModel().merge_status(pull_request)
3167 3171
3168 3172 pull_request_url = url(
3169 3173 'pullrequest_show', repo_name=self.target_repo.repo_name,
3170 3174 pull_request_id=self.pull_request_id, qualified=True)
3171 3175
3172 3176 merge_data = {
3173 3177 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3174 3178 'reference': (
3175 3179 pull_request.shadow_merge_ref._asdict()
3176 3180 if pull_request.shadow_merge_ref else None),
3177 3181 }
3178 3182
3179 3183 data = {
3180 3184 'pull_request_id': pull_request.pull_request_id,
3181 3185 'url': pull_request_url,
3182 3186 'title': pull_request.title,
3183 3187 'description': pull_request.description,
3184 3188 'status': pull_request.status,
3185 3189 'created_on': pull_request.created_on,
3186 3190 'updated_on': pull_request.updated_on,
3187 3191 'commit_ids': pull_request.revisions,
3188 3192 'review_status': pull_request.calculated_review_status(),
3189 3193 'mergeable': {
3190 3194 'status': merge_status[0],
3191 3195 'message': unicode(merge_status[1]),
3192 3196 },
3193 3197 'source': {
3194 3198 'clone_url': pull_request.source_repo.clone_url(),
3195 3199 'repository': pull_request.source_repo.repo_name,
3196 3200 'reference': {
3197 3201 'name': pull_request.source_ref_parts.name,
3198 3202 'type': pull_request.source_ref_parts.type,
3199 3203 'commit_id': pull_request.source_ref_parts.commit_id,
3200 3204 },
3201 3205 },
3202 3206 'target': {
3203 3207 'clone_url': pull_request.target_repo.clone_url(),
3204 3208 'repository': pull_request.target_repo.repo_name,
3205 3209 'reference': {
3206 3210 'name': pull_request.target_ref_parts.name,
3207 3211 'type': pull_request.target_ref_parts.type,
3208 3212 'commit_id': pull_request.target_ref_parts.commit_id,
3209 3213 },
3210 3214 },
3211 3215 'merge': merge_data,
3212 3216 'author': pull_request.author.get_api_data(include_secrets=False,
3213 3217 details='basic'),
3214 3218 'reviewers': [
3215 3219 {
3216 3220 'user': reviewer.get_api_data(include_secrets=False,
3217 3221 details='basic'),
3218 3222 'reasons': reasons,
3219 3223 'review_status': st[0][1].status if st else 'not_reviewed',
3220 3224 }
3221 3225 for reviewer, reasons, st in pull_request.reviewers_statuses()
3222 3226 ]
3223 3227 }
3224 3228
3225 3229 return data
3226 3230
3227 3231
3228 3232 class PullRequest(Base, _PullRequestBase):
3229 3233 __tablename__ = 'pull_requests'
3230 3234 __table_args__ = (
3231 3235 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3232 3236 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3233 3237 )
3234 3238
3235 3239 pull_request_id = Column(
3236 3240 'pull_request_id', Integer(), nullable=False, primary_key=True)
3237 3241
3238 3242 def __repr__(self):
3239 3243 if self.pull_request_id:
3240 3244 return '<DB:PullRequest #%s>' % self.pull_request_id
3241 3245 else:
3242 3246 return '<DB:PullRequest at %#x>' % id(self)
3243 3247
3244 3248 reviewers = relationship('PullRequestReviewers',
3245 3249 cascade="all, delete, delete-orphan")
3246 3250 statuses = relationship('ChangesetStatus')
3247 3251 comments = relationship('ChangesetComment',
3248 3252 cascade="all, delete, delete-orphan")
3249 3253 versions = relationship('PullRequestVersion',
3250 3254 cascade="all, delete, delete-orphan",
3251 3255 lazy='dynamic')
3252 3256
3253 3257
3254 3258 @classmethod
3255 3259 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3256 3260 internal_methods=None):
3257 3261
3258 3262 class PullRequestDisplay(object):
3259 3263 """
3260 3264 Special object wrapper for showing PullRequest data via Versions
3261 3265 It mimics PR object as close as possible. This is read only object
3262 3266 just for display
3263 3267 """
3264 3268
3265 3269 def __init__(self, attrs, internal=None):
3266 3270 self.attrs = attrs
3267 3271 # internal have priority over the given ones via attrs
3268 3272 self.internal = internal or ['versions']
3269 3273
3270 3274 def __getattr__(self, item):
3271 3275 if item in self.internal:
3272 3276 return getattr(self, item)
3273 3277 try:
3274 3278 return self.attrs[item]
3275 3279 except KeyError:
3276 3280 raise AttributeError(
3277 3281 '%s object has no attribute %s' % (self, item))
3278 3282
3279 3283 def __repr__(self):
3280 3284 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3281 3285
3282 3286 def versions(self):
3283 3287 return pull_request_obj.versions.order_by(
3284 3288 PullRequestVersion.pull_request_version_id).all()
3285 3289
3286 3290 def is_closed(self):
3287 3291 return pull_request_obj.is_closed()
3288 3292
3289 3293 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3290 3294
3291 3295 attrs.author = StrictAttributeDict(
3292 3296 pull_request_obj.author.get_api_data())
3293 3297 if pull_request_obj.target_repo:
3294 3298 attrs.target_repo = StrictAttributeDict(
3295 3299 pull_request_obj.target_repo.get_api_data())
3296 3300 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3297 3301
3298 3302 if pull_request_obj.source_repo:
3299 3303 attrs.source_repo = StrictAttributeDict(
3300 3304 pull_request_obj.source_repo.get_api_data())
3301 3305 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3302 3306
3303 3307 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3304 3308 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3305 3309 attrs.revisions = pull_request_obj.revisions
3306 3310
3307 3311 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3308 3312
3309 3313 return PullRequestDisplay(attrs, internal=internal_methods)
3310 3314
3311 3315 def is_closed(self):
3312 3316 return self.status == self.STATUS_CLOSED
3313 3317
3314 3318 def __json__(self):
3315 3319 return {
3316 3320 'revisions': self.revisions,
3317 3321 }
3318 3322
3319 3323 def calculated_review_status(self):
3320 3324 from rhodecode.model.changeset_status import ChangesetStatusModel
3321 3325 return ChangesetStatusModel().calculated_review_status(self)
3322 3326
3323 3327 def reviewers_statuses(self):
3324 3328 from rhodecode.model.changeset_status import ChangesetStatusModel
3325 3329 return ChangesetStatusModel().reviewers_statuses(self)
3326 3330
3327 3331
3328 3332 class PullRequestVersion(Base, _PullRequestBase):
3329 3333 __tablename__ = 'pull_request_versions'
3330 3334 __table_args__ = (
3331 3335 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3332 3336 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3333 3337 )
3334 3338
3335 3339 pull_request_version_id = Column(
3336 3340 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3337 3341 pull_request_id = Column(
3338 3342 'pull_request_id', Integer(),
3339 3343 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3340 3344 pull_request = relationship('PullRequest')
3341 3345
3342 3346 def __repr__(self):
3343 3347 if self.pull_request_version_id:
3344 3348 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3345 3349 else:
3346 3350 return '<DB:PullRequestVersion at %#x>' % id(self)
3347 3351
3348 3352 @property
3349 3353 def reviewers(self):
3350 3354 return self.pull_request.reviewers
3351 3355
3352 3356 @property
3353 3357 def versions(self):
3354 3358 return self.pull_request.versions
3355 3359
3356 3360 def is_closed(self):
3357 3361 # calculate from original
3358 3362 return self.pull_request.status == self.STATUS_CLOSED
3359 3363
3360 3364 def calculated_review_status(self):
3361 3365 return self.pull_request.calculated_review_status()
3362 3366
3363 3367 def reviewers_statuses(self):
3364 3368 return self.pull_request.reviewers_statuses()
3365 3369
3366 3370
3367 3371 class PullRequestReviewers(Base, BaseModel):
3368 3372 __tablename__ = 'pull_request_reviewers'
3369 3373 __table_args__ = (
3370 3374 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3371 3375 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3372 3376 )
3373 3377
3374 3378 def __init__(self, user=None, pull_request=None, reasons=None):
3375 3379 self.user = user
3376 3380 self.pull_request = pull_request
3377 3381 self.reasons = reasons or []
3378 3382
3379 3383 @hybrid_property
3380 3384 def reasons(self):
3381 3385 if not self._reasons:
3382 3386 return []
3383 3387 return self._reasons
3384 3388
3385 3389 @reasons.setter
3386 3390 def reasons(self, val):
3387 3391 val = val or []
3388 3392 if any(not isinstance(x, basestring) for x in val):
3389 3393 raise Exception('invalid reasons type, must be list of strings')
3390 3394 self._reasons = val
3391 3395
3392 3396 pull_requests_reviewers_id = Column(
3393 3397 'pull_requests_reviewers_id', Integer(), nullable=False,
3394 3398 primary_key=True)
3395 3399 pull_request_id = Column(
3396 3400 "pull_request_id", Integer(),
3397 3401 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3398 3402 user_id = Column(
3399 3403 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3400 3404 _reasons = Column(
3401 3405 'reason', MutationList.as_mutable(
3402 3406 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3403 3407
3404 3408 user = relationship('User')
3405 3409 pull_request = relationship('PullRequest')
3406 3410
3407 3411
3408 3412 class Notification(Base, BaseModel):
3409 3413 __tablename__ = 'notifications'
3410 3414 __table_args__ = (
3411 3415 Index('notification_type_idx', 'type'),
3412 3416 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3413 3417 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3414 3418 )
3415 3419
3416 3420 TYPE_CHANGESET_COMMENT = u'cs_comment'
3417 3421 TYPE_MESSAGE = u'message'
3418 3422 TYPE_MENTION = u'mention'
3419 3423 TYPE_REGISTRATION = u'registration'
3420 3424 TYPE_PULL_REQUEST = u'pull_request'
3421 3425 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3422 3426
3423 3427 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3424 3428 subject = Column('subject', Unicode(512), nullable=True)
3425 3429 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3426 3430 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3427 3431 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3428 3432 type_ = Column('type', Unicode(255))
3429 3433
3430 3434 created_by_user = relationship('User')
3431 3435 notifications_to_users = relationship('UserNotification', lazy='joined',
3432 3436 cascade="all, delete, delete-orphan")
3433 3437
3434 3438 @property
3435 3439 def recipients(self):
3436 3440 return [x.user for x in UserNotification.query()\
3437 3441 .filter(UserNotification.notification == self)\
3438 3442 .order_by(UserNotification.user_id.asc()).all()]
3439 3443
3440 3444 @classmethod
3441 3445 def create(cls, created_by, subject, body, recipients, type_=None):
3442 3446 if type_ is None:
3443 3447 type_ = Notification.TYPE_MESSAGE
3444 3448
3445 3449 notification = cls()
3446 3450 notification.created_by_user = created_by
3447 3451 notification.subject = subject
3448 3452 notification.body = body
3449 3453 notification.type_ = type_
3450 3454 notification.created_on = datetime.datetime.now()
3451 3455
3452 3456 for u in recipients:
3453 3457 assoc = UserNotification()
3454 3458 assoc.notification = notification
3455 3459
3456 3460 # if created_by is inside recipients mark his notification
3457 3461 # as read
3458 3462 if u.user_id == created_by.user_id:
3459 3463 assoc.read = True
3460 3464
3461 3465 u.notifications.append(assoc)
3462 3466 Session().add(notification)
3463 3467
3464 3468 return notification
3465 3469
3466 3470 @property
3467 3471 def description(self):
3468 3472 from rhodecode.model.notification import NotificationModel
3469 3473 return NotificationModel().make_description(self)
3470 3474
3471 3475
3472 3476 class UserNotification(Base, BaseModel):
3473 3477 __tablename__ = 'user_to_notification'
3474 3478 __table_args__ = (
3475 3479 UniqueConstraint('user_id', 'notification_id'),
3476 3480 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 3481 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3478 3482 )
3479 3483 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3480 3484 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3481 3485 read = Column('read', Boolean, default=False)
3482 3486 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3483 3487
3484 3488 user = relationship('User', lazy="joined")
3485 3489 notification = relationship('Notification', lazy="joined",
3486 3490 order_by=lambda: Notification.created_on.desc(),)
3487 3491
3488 3492 def mark_as_read(self):
3489 3493 self.read = True
3490 3494 Session().add(self)
3491 3495
3492 3496
3493 3497 class Gist(Base, BaseModel):
3494 3498 __tablename__ = 'gists'
3495 3499 __table_args__ = (
3496 3500 Index('g_gist_access_id_idx', 'gist_access_id'),
3497 3501 Index('g_created_on_idx', 'created_on'),
3498 3502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3499 3503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3500 3504 )
3501 3505 GIST_PUBLIC = u'public'
3502 3506 GIST_PRIVATE = u'private'
3503 3507 DEFAULT_FILENAME = u'gistfile1.txt'
3504 3508
3505 3509 ACL_LEVEL_PUBLIC = u'acl_public'
3506 3510 ACL_LEVEL_PRIVATE = u'acl_private'
3507 3511
3508 3512 gist_id = Column('gist_id', Integer(), primary_key=True)
3509 3513 gist_access_id = Column('gist_access_id', Unicode(250))
3510 3514 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3511 3515 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3512 3516 gist_expires = Column('gist_expires', Float(53), nullable=False)
3513 3517 gist_type = Column('gist_type', Unicode(128), nullable=False)
3514 3518 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3515 3519 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3516 3520 acl_level = Column('acl_level', Unicode(128), nullable=True)
3517 3521
3518 3522 owner = relationship('User')
3519 3523
3520 3524 def __repr__(self):
3521 3525 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3522 3526
3523 3527 @classmethod
3524 3528 def get_or_404(cls, id_):
3525 3529 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3526 3530 if not res:
3527 3531 raise HTTPNotFound
3528 3532 return res
3529 3533
3530 3534 @classmethod
3531 3535 def get_by_access_id(cls, gist_access_id):
3532 3536 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3533 3537
3534 3538 def gist_url(self):
3535 3539 import rhodecode
3536 3540 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3537 3541 if alias_url:
3538 3542 return alias_url.replace('{gistid}', self.gist_access_id)
3539 3543
3540 3544 return url('gist', gist_id=self.gist_access_id, qualified=True)
3541 3545
3542 3546 @classmethod
3543 3547 def base_path(cls):
3544 3548 """
3545 3549 Returns base path when all gists are stored
3546 3550
3547 3551 :param cls:
3548 3552 """
3549 3553 from rhodecode.model.gist import GIST_STORE_LOC
3550 3554 q = Session().query(RhodeCodeUi)\
3551 3555 .filter(RhodeCodeUi.ui_key == URL_SEP)
3552 3556 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3553 3557 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3554 3558
3555 3559 def get_api_data(self):
3556 3560 """
3557 3561 Common function for generating gist related data for API
3558 3562 """
3559 3563 gist = self
3560 3564 data = {
3561 3565 'gist_id': gist.gist_id,
3562 3566 'type': gist.gist_type,
3563 3567 'access_id': gist.gist_access_id,
3564 3568 'description': gist.gist_description,
3565 3569 'url': gist.gist_url(),
3566 3570 'expires': gist.gist_expires,
3567 3571 'created_on': gist.created_on,
3568 3572 'modified_at': gist.modified_at,
3569 3573 'content': None,
3570 3574 'acl_level': gist.acl_level,
3571 3575 }
3572 3576 return data
3573 3577
3574 3578 def __json__(self):
3575 3579 data = dict(
3576 3580 )
3577 3581 data.update(self.get_api_data())
3578 3582 return data
3579 3583 # SCM functions
3580 3584
3581 3585 def scm_instance(self, **kwargs):
3582 3586 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3583 3587 return get_vcs_instance(
3584 3588 repo_path=safe_str(full_repo_path), create=False)
3585 3589
3586 3590
3587 3591 class ExternalIdentity(Base, BaseModel):
3588 3592 __tablename__ = 'external_identities'
3589 3593 __table_args__ = (
3590 3594 Index('local_user_id_idx', 'local_user_id'),
3591 3595 Index('external_id_idx', 'external_id'),
3592 3596 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3593 3597 'mysql_charset': 'utf8'})
3594 3598
3595 3599 external_id = Column('external_id', Unicode(255), default=u'',
3596 3600 primary_key=True)
3597 3601 external_username = Column('external_username', Unicode(1024), default=u'')
3598 3602 local_user_id = Column('local_user_id', Integer(),
3599 3603 ForeignKey('users.user_id'), primary_key=True)
3600 3604 provider_name = Column('provider_name', Unicode(255), default=u'',
3601 3605 primary_key=True)
3602 3606 access_token = Column('access_token', String(1024), default=u'')
3603 3607 alt_token = Column('alt_token', String(1024), default=u'')
3604 3608 token_secret = Column('token_secret', String(1024), default=u'')
3605 3609
3606 3610 @classmethod
3607 3611 def by_external_id_and_provider(cls, external_id, provider_name,
3608 3612 local_user_id=None):
3609 3613 """
3610 3614 Returns ExternalIdentity instance based on search params
3611 3615
3612 3616 :param external_id:
3613 3617 :param provider_name:
3614 3618 :return: ExternalIdentity
3615 3619 """
3616 3620 query = cls.query()
3617 3621 query = query.filter(cls.external_id == external_id)
3618 3622 query = query.filter(cls.provider_name == provider_name)
3619 3623 if local_user_id:
3620 3624 query = query.filter(cls.local_user_id == local_user_id)
3621 3625 return query.first()
3622 3626
3623 3627 @classmethod
3624 3628 def user_by_external_id_and_provider(cls, external_id, provider_name):
3625 3629 """
3626 3630 Returns User instance based on search params
3627 3631
3628 3632 :param external_id:
3629 3633 :param provider_name:
3630 3634 :return: User
3631 3635 """
3632 3636 query = User.query()
3633 3637 query = query.filter(cls.external_id == external_id)
3634 3638 query = query.filter(cls.provider_name == provider_name)
3635 3639 query = query.filter(User.user_id == cls.local_user_id)
3636 3640 return query.first()
3637 3641
3638 3642 @classmethod
3639 3643 def by_local_user_id(cls, local_user_id):
3640 3644 """
3641 3645 Returns all tokens for user
3642 3646
3643 3647 :param local_user_id:
3644 3648 :return: ExternalIdentity
3645 3649 """
3646 3650 query = cls.query()
3647 3651 query = query.filter(cls.local_user_id == local_user_id)
3648 3652 return query
3649 3653
3650 3654
3651 3655 class Integration(Base, BaseModel):
3652 3656 __tablename__ = 'integrations'
3653 3657 __table_args__ = (
3654 3658 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3655 3659 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3656 3660 )
3657 3661
3658 3662 integration_id = Column('integration_id', Integer(), primary_key=True)
3659 3663 integration_type = Column('integration_type', String(255))
3660 3664 enabled = Column('enabled', Boolean(), nullable=False)
3661 3665 name = Column('name', String(255), nullable=False)
3662 3666 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3663 3667 default=False)
3664 3668
3665 3669 settings = Column(
3666 3670 'settings_json', MutationObj.as_mutable(
3667 3671 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3668 3672 repo_id = Column(
3669 3673 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3670 3674 nullable=True, unique=None, default=None)
3671 3675 repo = relationship('Repository', lazy='joined')
3672 3676
3673 3677 repo_group_id = Column(
3674 3678 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3675 3679 nullable=True, unique=None, default=None)
3676 3680 repo_group = relationship('RepoGroup', lazy='joined')
3677 3681
3678 3682 @property
3679 3683 def scope(self):
3680 3684 if self.repo:
3681 3685 return repr(self.repo)
3682 3686 if self.repo_group:
3683 3687 if self.child_repos_only:
3684 3688 return repr(self.repo_group) + ' (child repos only)'
3685 3689 else:
3686 3690 return repr(self.repo_group) + ' (recursive)'
3687 3691 if self.child_repos_only:
3688 3692 return 'root_repos'
3689 3693 return 'global'
3690 3694
3691 3695 def __repr__(self):
3692 3696 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3693 3697
3694 3698
3695 3699 class RepoReviewRuleUser(Base, BaseModel):
3696 3700 __tablename__ = 'repo_review_rules_users'
3697 3701 __table_args__ = (
3698 3702 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3699 3703 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3700 3704 )
3701 3705 repo_review_rule_user_id = Column(
3702 3706 'repo_review_rule_user_id', Integer(), primary_key=True)
3703 3707 repo_review_rule_id = Column("repo_review_rule_id",
3704 3708 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3705 3709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3706 3710 nullable=False)
3707 3711 user = relationship('User')
3708 3712
3709 3713
3710 3714 class RepoReviewRuleUserGroup(Base, BaseModel):
3711 3715 __tablename__ = 'repo_review_rules_users_groups'
3712 3716 __table_args__ = (
3713 3717 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3714 3718 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3715 3719 )
3716 3720 repo_review_rule_users_group_id = Column(
3717 3721 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3718 3722 repo_review_rule_id = Column("repo_review_rule_id",
3719 3723 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3720 3724 users_group_id = Column("users_group_id", Integer(),
3721 3725 ForeignKey('users_groups.users_group_id'), nullable=False)
3722 3726 users_group = relationship('UserGroup')
3723 3727
3724 3728
3725 3729 class RepoReviewRule(Base, BaseModel):
3726 3730 __tablename__ = 'repo_review_rules'
3727 3731 __table_args__ = (
3728 3732 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3729 3733 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3730 3734 )
3731 3735
3732 3736 repo_review_rule_id = Column(
3733 3737 'repo_review_rule_id', Integer(), primary_key=True)
3734 3738 repo_id = Column(
3735 3739 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3736 3740 repo = relationship('Repository', backref='review_rules')
3737 3741
3738 3742 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3739 3743 default=u'*') # glob
3740 3744 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3741 3745 default=u'*') # glob
3742 3746
3743 3747 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3744 3748 nullable=False, default=False)
3745 3749 rule_users = relationship('RepoReviewRuleUser')
3746 3750 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3747 3751
3748 3752 @hybrid_property
3749 3753 def branch_pattern(self):
3750 3754 return self._branch_pattern or '*'
3751 3755
3752 3756 def _validate_glob(self, value):
3753 3757 re.compile('^' + glob2re(value) + '$')
3754 3758
3755 3759 @branch_pattern.setter
3756 3760 def branch_pattern(self, value):
3757 3761 self._validate_glob(value)
3758 3762 self._branch_pattern = value or '*'
3759 3763
3760 3764 @hybrid_property
3761 3765 def file_pattern(self):
3762 3766 return self._file_pattern or '*'
3763 3767
3764 3768 @file_pattern.setter
3765 3769 def file_pattern(self, value):
3766 3770 self._validate_glob(value)
3767 3771 self._file_pattern = value or '*'
3768 3772
3769 3773 def matches(self, branch, files_changed):
3770 3774 """
3771 3775 Check if this review rule matches a branch/files in a pull request
3772 3776
3773 3777 :param branch: branch name for the commit
3774 3778 :param files_changed: list of file paths changed in the pull request
3775 3779 """
3776 3780
3777 3781 branch = branch or ''
3778 3782 files_changed = files_changed or []
3779 3783
3780 3784 branch_matches = True
3781 3785 if branch:
3782 3786 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3783 3787 branch_matches = bool(branch_regex.search(branch))
3784 3788
3785 3789 files_matches = True
3786 3790 if self.file_pattern != '*':
3787 3791 files_matches = False
3788 3792 file_regex = re.compile(glob2re(self.file_pattern))
3789 3793 for filename in files_changed:
3790 3794 if file_regex.search(filename):
3791 3795 files_matches = True
3792 3796 break
3793 3797
3794 3798 return branch_matches and files_matches
3795 3799
3796 3800 @property
3797 3801 def review_users(self):
3798 3802 """ Returns the users which this rule applies to """
3799 3803
3800 3804 users = set()
3801 3805 users |= set([
3802 3806 rule_user.user for rule_user in self.rule_users
3803 3807 if rule_user.user.active])
3804 3808 users |= set(
3805 3809 member.user
3806 3810 for rule_user_group in self.rule_user_groups
3807 3811 for member in rule_user_group.users_group.members
3808 3812 if member.user.active
3809 3813 )
3810 3814 return users
3811 3815
3812 3816 def __repr__(self):
3813 3817 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3814 3818 self.repo_review_rule_id, self.repo)
3815 3819
3816 3820
3817 3821 class DbMigrateVersion(Base, BaseModel):
3818 3822 __tablename__ = 'db_migrate_version'
3819 3823 __table_args__ = (
3820 3824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3821 3825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3822 3826 )
3823 3827 repository_id = Column('repository_id', String(250), primary_key=True)
3824 3828 repository_path = Column('repository_path', Text)
3825 3829 version = Column('version', Integer)
3826 3830
3827 3831
3828 3832 class DbSession(Base, BaseModel):
3829 3833 __tablename__ = 'db_session'
3830 3834 __table_args__ = (
3831 3835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3832 3836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3833 3837 )
3834 3838
3835 3839 def __repr__(self):
3836 3840 return '<DB:DbSession({})>'.format(self.id)
3837 3841
3838 3842 id = Column('id', Integer())
3839 3843 namespace = Column('namespace', String(255), primary_key=True)
3840 3844 accessed = Column('accessed', DateTime, nullable=False)
3841 3845 created = Column('created', DateTime, nullable=False)
3842 3846 data = Column('data', PickleType, nullable=False)
@@ -1,2223 +1,2257 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wide-mode-wrapper {
113 113 max-width:4000px !important;
114 114 }
115 115
116 116 .wrapper {
117 117 position: relative;
118 118 max-width: @wrapper-maxwidth;
119 119 margin: 0 auto;
120 120 }
121 121
122 122 #content {
123 123 clear: both;
124 124 padding: 0 @contentpadding;
125 125 }
126 126
127 127 .advanced-settings-fields{
128 128 input{
129 129 margin-left: @textmargin;
130 130 margin-right: @padding/2;
131 131 }
132 132 }
133 133
134 134 .cs_files_title {
135 135 margin: @pagepadding 0 0;
136 136 }
137 137
138 138 input.inline[type="file"] {
139 139 display: inline;
140 140 }
141 141
142 142 .error_page {
143 143 margin: 10% auto;
144 144
145 145 h1 {
146 146 color: @grey2;
147 147 }
148 148
149 149 .alert {
150 150 margin: @padding 0;
151 151 }
152 152
153 153 .error-branding {
154 154 font-family: @text-semibold;
155 155 color: @grey4;
156 156 }
157 157
158 158 .error_message {
159 159 font-family: @text-regular;
160 160 }
161 161
162 162 .sidebar {
163 163 min-height: 275px;
164 164 margin: 0;
165 165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 166 border: none;
167 167 }
168 168
169 169 .main-content {
170 170 position: relative;
171 171 margin: 0 @sidebarpadding @sidebarpadding;
172 172 padding: 0 0 0 @sidebarpadding;
173 173 border-left: @border-thickness solid @grey5;
174 174
175 175 @media (max-width:767px) {
176 176 clear: both;
177 177 width: 100%;
178 178 margin: 0;
179 179 border: none;
180 180 }
181 181 }
182 182
183 183 .inner-column {
184 184 float: left;
185 185 width: 29.75%;
186 186 min-height: 150px;
187 187 margin: @sidebarpadding 2% 0 0;
188 188 padding: 0 2% 0 0;
189 189 border-right: @border-thickness solid @grey5;
190 190
191 191 @media (max-width:767px) {
192 192 clear: both;
193 193 width: 100%;
194 194 border: none;
195 195 }
196 196
197 197 ul {
198 198 padding-left: 1.25em;
199 199 }
200 200
201 201 &:last-child {
202 202 margin: @sidebarpadding 0 0;
203 203 border: none;
204 204 }
205 205
206 206 h4 {
207 207 margin: 0 0 @padding;
208 208 font-family: @text-semibold;
209 209 }
210 210 }
211 211 }
212 212 .error-page-logo {
213 213 width: 130px;
214 214 height: 160px;
215 215 }
216 216
217 217 // HEADER
218 218 .header {
219 219
220 220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 221 // for the header and then remove the min-height. I chose a smaller value
222 222 // intentionally here to avoid rendering issues in the main navigation.
223 223 min-height: 49px;
224 224
225 225 position: relative;
226 226 vertical-align: bottom;
227 227 padding: 0 @header-padding;
228 228 background-color: @grey2;
229 229 color: @grey5;
230 230
231 231 .title {
232 232 overflow: visible;
233 233 }
234 234
235 235 &:before,
236 236 &:after {
237 237 content: "";
238 238 clear: both;
239 239 width: 100%;
240 240 }
241 241
242 242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 243 .select2-container .select2-choice .select2-arrow {
244 244 display: none;
245 245 }
246 246 }
247 247
248 248 #header-inner {
249 249 &.title {
250 250 margin: 0;
251 251 }
252 252 &:before,
253 253 &:after {
254 254 content: "";
255 255 clear: both;
256 256 }
257 257 }
258 258
259 259 // Gists
260 260 #files_data {
261 261 clear: both; //for firefox
262 262 }
263 263 #gistid {
264 264 margin-right: @padding;
265 265 }
266 266
267 267 // Global Settings Editor
268 268 .textarea.editor {
269 269 float: left;
270 270 position: relative;
271 271 max-width: @texteditor-width;
272 272
273 273 select {
274 274 position: absolute;
275 275 top:10px;
276 276 right:0;
277 277 }
278 278
279 279 .CodeMirror {
280 280 margin: 0;
281 281 }
282 282
283 283 .help-block {
284 284 margin: 0 0 @padding;
285 285 padding:.5em;
286 286 background-color: @grey6;
287 287 }
288 288 }
289 289
290 290 ul.auth_plugins {
291 291 margin: @padding 0 @padding @legend-width;
292 292 padding: 0;
293 293
294 294 li {
295 295 margin-bottom: @padding;
296 296 line-height: 1em;
297 297 list-style-type: none;
298 298
299 299 .auth_buttons .btn {
300 300 margin-right: @padding;
301 301 }
302 302
303 303 &:before { content: none; }
304 304 }
305 305 }
306 306
307 307
308 308 // My Account PR list
309 309
310 310 #show_closed {
311 311 margin: 0 1em 0 0;
312 312 }
313 313
314 314 .pullrequestlist {
315 315 .closed {
316 316 background-color: @grey6;
317 317 }
318 318 .td-status {
319 319 padding-left: .5em;
320 320 }
321 321 .log-container .truncate {
322 322 height: 2.75em;
323 323 white-space: pre-line;
324 324 }
325 325 table.rctable .user {
326 326 padding-left: 0;
327 327 }
328 328 table.rctable {
329 329 td.td-description,
330 330 .rc-user {
331 331 min-width: auto;
332 332 }
333 333 }
334 334 }
335 335
336 336 // Pull Requests
337 337
338 338 .pullrequests_section_head {
339 339 display: block;
340 340 clear: both;
341 341 margin: @padding 0;
342 342 font-family: @text-bold;
343 343 }
344 344
345 345 .pr-origininfo, .pr-targetinfo {
346 346 position: relative;
347 347
348 348 .tag {
349 349 display: inline-block;
350 350 margin: 0 1em .5em 0;
351 351 }
352 352
353 353 .clone-url {
354 354 display: inline-block;
355 355 margin: 0 0 .5em 0;
356 356 padding: 0;
357 357 line-height: 1.2em;
358 358 }
359 359 }
360 360
361 361 .pr-pullinfo {
362 362 clear: both;
363 363 margin: .5em 0;
364 364 }
365 365
366 366 #pr-title-input {
367 367 width: 72%;
368 368 font-size: 1em;
369 369 font-family: @text-bold;
370 370 margin: 0;
371 371 padding: 0 0 0 @padding/4;
372 372 line-height: 1.7em;
373 373 color: @text-color;
374 374 letter-spacing: .02em;
375 375 }
376 376
377 377 #pullrequest_title {
378 378 width: 100%;
379 379 box-sizing: border-box;
380 380 }
381 381
382 382 #pr_open_message {
383 383 border: @border-thickness solid #fff;
384 384 border-radius: @border-radius;
385 385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 386 text-align: right;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .pr-submit-button {
391 391 float: right;
392 392 margin: 0 0 0 5px;
393 393 }
394 394
395 395 .pr-spacing-container {
396 396 padding: 20px;
397 397 clear: both
398 398 }
399 399
400 400 #pr-description-input {
401 401 margin-bottom: 0;
402 402 }
403 403
404 404 .pr-description-label {
405 405 vertical-align: top;
406 406 }
407 407
408 408 .perms_section_head {
409 409 min-width: 625px;
410 410
411 411 h2 {
412 412 margin-bottom: 0;
413 413 }
414 414
415 415 .label-checkbox {
416 416 float: left;
417 417 }
418 418
419 419 &.field {
420 420 margin: @space 0 @padding;
421 421 }
422 422
423 423 &:first-child.field {
424 424 margin-top: 0;
425 425
426 426 .label {
427 427 margin-top: 0;
428 428 padding-top: 0;
429 429 }
430 430
431 431 .radios {
432 432 padding-top: 0;
433 433 }
434 434 }
435 435
436 436 .radios {
437 437 float: right;
438 438 position: relative;
439 439 width: 405px;
440 440 }
441 441 }
442 442
443 443 //--- MODULES ------------------//
444 444
445 445
446 446 // Server Announcement
447 447 #server-announcement {
448 448 width: 95%;
449 449 margin: @padding auto;
450 450 padding: @padding;
451 451 border-width: 2px;
452 452 border-style: solid;
453 453 .border-radius(2px);
454 454 font-family: @text-bold;
455 455
456 456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 461 }
462 462
463 463 // Fixed Sidebar Column
464 464 .sidebar-col-wrapper {
465 465 padding-left: @sidebar-all-width;
466 466
467 467 .sidebar {
468 468 width: @sidebar-width;
469 469 margin-left: -@sidebar-all-width;
470 470 }
471 471 }
472 472
473 473 .sidebar-col-wrapper.scw-small {
474 474 padding-left: @sidebar-small-all-width;
475 475
476 476 .sidebar {
477 477 width: @sidebar-small-width;
478 478 margin-left: -@sidebar-small-all-width;
479 479 }
480 480 }
481 481
482 482
483 483 // FOOTER
484 484 #footer {
485 485 padding: 0;
486 486 text-align: center;
487 487 vertical-align: middle;
488 488 color: @grey2;
489 489 background-color: @grey6;
490 490
491 491 p {
492 492 margin: 0;
493 493 padding: 1em;
494 494 line-height: 1em;
495 495 }
496 496
497 497 .server-instance { //server instance
498 498 display: none;
499 499 }
500 500
501 501 .title {
502 502 float: none;
503 503 margin: 0 auto;
504 504 }
505 505 }
506 506
507 507 button.close {
508 508 padding: 0;
509 509 cursor: pointer;
510 510 background: transparent;
511 511 border: 0;
512 512 .box-shadow(none);
513 513 -webkit-appearance: none;
514 514 }
515 515
516 516 .close {
517 517 float: right;
518 518 font-size: 21px;
519 519 font-family: @text-bootstrap;
520 520 line-height: 1em;
521 521 font-weight: bold;
522 522 color: @grey2;
523 523
524 524 &:hover,
525 525 &:focus {
526 526 color: @grey1;
527 527 text-decoration: none;
528 528 cursor: pointer;
529 529 }
530 530 }
531 531
532 532 // GRID
533 533 .sorting,
534 534 .sorting_desc,
535 535 .sorting_asc {
536 536 cursor: pointer;
537 537 }
538 538 .sorting_desc:after {
539 539 content: "\00A0\25B2";
540 540 font-size: .75em;
541 541 }
542 542 .sorting_asc:after {
543 543 content: "\00A0\25BC";
544 544 font-size: .68em;
545 545 }
546 546
547 547
548 548 .user_auth_tokens {
549 549
550 550 &.truncate {
551 551 white-space: nowrap;
552 552 overflow: hidden;
553 553 text-overflow: ellipsis;
554 554 }
555 555
556 556 .fields .field .input {
557 557 margin: 0;
558 558 }
559 559
560 560 input#description {
561 561 width: 100px;
562 562 margin: 0;
563 563 }
564 564
565 565 .drop-menu {
566 566 // TODO: johbo: Remove this, should work out of the box when
567 567 // having multiple inputs inline
568 568 margin: 0 0 0 5px;
569 569 }
570 570 }
571 571 #user_list_table {
572 572 .closed {
573 573 background-color: @grey6;
574 574 }
575 575 }
576 576
577 577
578 578 input {
579 579 &.disabled {
580 580 opacity: .5;
581 581 }
582 582 }
583 583
584 584 // remove extra padding in firefox
585 585 input::-moz-focus-inner { border:0; padding:0 }
586 586
587 587 .adjacent input {
588 588 margin-bottom: @padding;
589 589 }
590 590
591 591 .permissions_boxes {
592 592 display: block;
593 593 }
594 594
595 595 //TODO: lisa: this should be in tables
596 596 .show_more_col {
597 597 width: 20px;
598 598 }
599 599
600 600 //FORMS
601 601
602 602 .medium-inline,
603 603 input#description.medium-inline {
604 604 display: inline;
605 605 width: @medium-inline-input-width;
606 606 min-width: 100px;
607 607 }
608 608
609 609 select {
610 610 //reset
611 611 -webkit-appearance: none;
612 612 -moz-appearance: none;
613 613
614 614 display: inline-block;
615 615 height: 28px;
616 616 width: auto;
617 617 margin: 0 @padding @padding 0;
618 618 padding: 0 18px 0 8px;
619 619 line-height:1em;
620 620 font-size: @basefontsize;
621 621 border: @border-thickness solid @rcblue;
622 622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 623 color: @rcblue;
624 624
625 625 &:after {
626 626 content: "\00A0\25BE";
627 627 }
628 628
629 629 &:focus {
630 630 outline: none;
631 631 }
632 632 }
633 633
634 634 option {
635 635 &:focus {
636 636 outline: none;
637 637 }
638 638 }
639 639
640 640 input,
641 641 textarea {
642 642 padding: @input-padding;
643 643 border: @input-border-thickness solid @border-highlight-color;
644 644 .border-radius (@border-radius);
645 645 font-family: @text-light;
646 646 font-size: @basefontsize;
647 647
648 648 &.input-sm {
649 649 padding: 5px;
650 650 }
651 651
652 652 &#description {
653 653 min-width: @input-description-minwidth;
654 654 min-height: 1em;
655 655 padding: 10px;
656 656 }
657 657 }
658 658
659 659 .field-sm {
660 660 input,
661 661 textarea {
662 662 padding: 5px;
663 663 }
664 664 }
665 665
666 666 textarea {
667 667 display: block;
668 668 clear: both;
669 669 width: 100%;
670 670 min-height: 100px;
671 671 margin-bottom: @padding;
672 672 .box-sizing(border-box);
673 673 overflow: auto;
674 674 }
675 675
676 676 label {
677 677 font-family: @text-light;
678 678 }
679 679
680 680 // GRAVATARS
681 681 // centers gravatar on username to the right
682 682
683 683 .gravatar {
684 684 display: inline;
685 685 min-width: 16px;
686 686 min-height: 16px;
687 687 margin: -5px 0;
688 688 padding: 0;
689 689 line-height: 1em;
690 690 border: 1px solid @grey4;
691 691 box-sizing: content-box;
692 692
693 693 &.gravatar-large {
694 694 margin: -0.5em .25em -0.5em 0;
695 695 }
696 696
697 697 & + .user {
698 698 display: inline;
699 699 margin: 0;
700 700 padding: 0 0 0 .17em;
701 701 line-height: 1em;
702 702 }
703 703 }
704 704
705 705 .user-inline-data {
706 706 display: inline-block;
707 707 float: left;
708 708 padding-left: .5em;
709 709 line-height: 1.3em;
710 710 }
711 711
712 712 .rc-user { // gravatar + user wrapper
713 713 float: left;
714 714 position: relative;
715 715 min-width: 100px;
716 716 max-width: 200px;
717 717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
718 718 display: block;
719 719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
720 720
721 721
722 722 .gravatar {
723 723 display: block;
724 724 position: absolute;
725 725 top: 0;
726 726 left: 0;
727 727 min-width: @gravatar-size;
728 728 min-height: @gravatar-size;
729 729 margin: 0;
730 730 }
731 731
732 732 .user {
733 733 display: block;
734 734 max-width: 175px;
735 735 padding-top: 2px;
736 736 overflow: hidden;
737 737 text-overflow: ellipsis;
738 738 }
739 739 }
740 740
741 741 .gist-gravatar,
742 742 .journal_container {
743 743 .gravatar-large {
744 744 margin: 0 .5em -10px 0;
745 745 }
746 746 }
747 747
748 748
749 749 // ADMIN SETTINGS
750 750
751 751 // Tag Patterns
752 752 .tag_patterns {
753 753 .tag_input {
754 754 margin-bottom: @padding;
755 755 }
756 756 }
757 757
758 758 .locked_input {
759 759 position: relative;
760 760
761 761 input {
762 762 display: inline;
763 763 margin-top: 3px;
764 764 }
765 765
766 766 br {
767 767 display: none;
768 768 }
769 769
770 770 .error-message {
771 771 float: left;
772 772 width: 100%;
773 773 }
774 774
775 775 .lock_input_button {
776 776 display: inline;
777 777 }
778 778
779 779 .help-block {
780 780 clear: both;
781 781 }
782 782 }
783 783
784 784 // Notifications
785 785
786 786 .notifications_buttons {
787 787 margin: 0 0 @space 0;
788 788 padding: 0;
789 789
790 790 .btn {
791 791 display: inline-block;
792 792 }
793 793 }
794 794
795 795 .notification-list {
796 796
797 797 div {
798 798 display: inline-block;
799 799 vertical-align: middle;
800 800 }
801 801
802 802 .container {
803 803 display: block;
804 804 margin: 0 0 @padding 0;
805 805 }
806 806
807 807 .delete-notifications {
808 808 margin-left: @padding;
809 809 text-align: right;
810 810 cursor: pointer;
811 811 }
812 812
813 813 .read-notifications {
814 814 margin-left: @padding/2;
815 815 text-align: right;
816 816 width: 35px;
817 817 cursor: pointer;
818 818 }
819 819
820 820 .icon-minus-sign {
821 821 color: @alert2;
822 822 }
823 823
824 824 .icon-ok-sign {
825 825 color: @alert1;
826 826 }
827 827 }
828 828
829 829 .user_settings {
830 830 float: left;
831 831 clear: both;
832 832 display: block;
833 833 width: 100%;
834 834
835 835 .gravatar_box {
836 836 margin-bottom: @padding;
837 837
838 838 &:after {
839 839 content: " ";
840 840 clear: both;
841 841 width: 100%;
842 842 }
843 843 }
844 844
845 845 .fields .field {
846 846 clear: both;
847 847 }
848 848 }
849 849
850 850 .advanced_settings {
851 851 margin-bottom: @space;
852 852
853 853 .help-block {
854 854 margin-left: 0;
855 855 }
856 856
857 857 button + .help-block {
858 858 margin-top: @padding;
859 859 }
860 860 }
861 861
862 862 // admin settings radio buttons and labels
863 863 .label-2 {
864 864 float: left;
865 865 width: @label2-width;
866 866
867 867 label {
868 868 color: @grey1;
869 869 }
870 870 }
871 871 .checkboxes {
872 872 float: left;
873 873 width: @checkboxes-width;
874 874 margin-bottom: @padding;
875 875
876 876 .checkbox {
877 877 width: 100%;
878 878
879 879 label {
880 880 margin: 0;
881 881 padding: 0;
882 882 }
883 883 }
884 884
885 885 .checkbox + .checkbox {
886 886 display: inline-block;
887 887 }
888 888
889 889 label {
890 890 margin-right: 1em;
891 891 }
892 892 }
893 893
894 894 // CHANGELOG
895 895 .container_header {
896 896 float: left;
897 897 display: block;
898 898 width: 100%;
899 899 margin: @padding 0 @padding;
900 900
901 901 #filter_changelog {
902 902 float: left;
903 903 margin-right: @padding;
904 904 }
905 905
906 906 .breadcrumbs_light {
907 907 display: inline-block;
908 908 }
909 909 }
910 910
911 911 .info_box {
912 912 float: right;
913 913 }
914 914
915 915
916 916 #graph_nodes {
917 917 padding-top: 43px;
918 918 }
919 919
920 920 #graph_content{
921 921
922 922 // adjust for table headers so that graph renders properly
923 923 // #graph_nodes padding - table cell padding
924 924 padding-top: (@space - (@basefontsize * 2.4));
925 925
926 926 &.graph_full_width {
927 927 width: 100%;
928 928 max-width: 100%;
929 929 }
930 930 }
931 931
932 932 #graph {
933 933 .flag_status {
934 934 margin: 0;
935 935 }
936 936
937 937 .pagination-left {
938 938 float: left;
939 939 clear: both;
940 940 }
941 941
942 942 .log-container {
943 943 max-width: 345px;
944 944
945 945 .message{
946 946 max-width: 340px;
947 947 }
948 948 }
949 949
950 950 .graph-col-wrapper {
951 951 padding-left: 110px;
952 952
953 953 #graph_nodes {
954 954 width: 100px;
955 955 margin-left: -110px;
956 956 float: left;
957 957 clear: left;
958 958 }
959 959 }
960 960 }
961 961
962 962 #filter_changelog {
963 963 float: left;
964 964 }
965 965
966 966
967 967 //--- THEME ------------------//
968 968
969 969 #logo {
970 970 float: left;
971 971 margin: 9px 0 0 0;
972 972
973 973 .header {
974 974 background-color: transparent;
975 975 }
976 976
977 977 a {
978 978 display: inline-block;
979 979 }
980 980
981 981 img {
982 982 height:30px;
983 983 }
984 984 }
985 985
986 986 .logo-wrapper {
987 987 float:left;
988 988 }
989 989
990 990 .branding{
991 991 float: left;
992 992 padding: 9px 2px;
993 993 line-height: 1em;
994 994 font-size: @navigation-fontsize;
995 995 }
996 996
997 997 img {
998 998 border: none;
999 999 outline: none;
1000 1000 }
1001 1001 user-profile-header
1002 1002 label {
1003 1003
1004 1004 input[type="checkbox"] {
1005 1005 margin-right: 1em;
1006 1006 }
1007 1007 input[type="radio"] {
1008 1008 margin-right: 1em;
1009 1009 }
1010 1010 }
1011 1011
1012 1012 .flag_status {
1013 1013 margin: 2px 8px 6px 2px;
1014 1014 &.under_review {
1015 1015 .circle(5px, @alert3);
1016 1016 }
1017 1017 &.approved {
1018 1018 .circle(5px, @alert1);
1019 1019 }
1020 1020 &.rejected,
1021 1021 &.forced_closed{
1022 1022 .circle(5px, @alert2);
1023 1023 }
1024 1024 &.not_reviewed {
1025 1025 .circle(5px, @grey5);
1026 1026 }
1027 1027 }
1028 1028
1029 1029 .flag_status_comment_box {
1030 1030 margin: 5px 6px 0px 2px;
1031 1031 }
1032 1032 .test_pattern_preview {
1033 1033 margin: @space 0;
1034 1034
1035 1035 p {
1036 1036 margin-bottom: 0;
1037 1037 border-bottom: @border-thickness solid @border-default-color;
1038 1038 color: @grey3;
1039 1039 }
1040 1040
1041 1041 .btn {
1042 1042 margin-bottom: @padding;
1043 1043 }
1044 1044 }
1045 1045 #test_pattern_result {
1046 1046 display: none;
1047 1047 &:extend(pre);
1048 1048 padding: .9em;
1049 1049 color: @grey3;
1050 1050 background-color: @grey7;
1051 1051 border-right: @border-thickness solid @border-default-color;
1052 1052 border-bottom: @border-thickness solid @border-default-color;
1053 1053 border-left: @border-thickness solid @border-default-color;
1054 1054 }
1055 1055
1056 1056 #repo_vcs_settings {
1057 1057 #inherit_overlay_vcs_default {
1058 1058 display: none;
1059 1059 }
1060 1060 #inherit_overlay_vcs_custom {
1061 1061 display: custom;
1062 1062 }
1063 1063 &.inherited {
1064 1064 #inherit_overlay_vcs_default {
1065 1065 display: block;
1066 1066 }
1067 1067 #inherit_overlay_vcs_custom {
1068 1068 display: none;
1069 1069 }
1070 1070 }
1071 1071 }
1072 1072
1073 1073 .issue-tracker-link {
1074 1074 color: @rcblue;
1075 1075 }
1076 1076
1077 1077 // Issue Tracker Table Show/Hide
1078 1078 #repo_issue_tracker {
1079 1079 #inherit_overlay {
1080 1080 display: none;
1081 1081 }
1082 1082 #custom_overlay {
1083 1083 display: custom;
1084 1084 }
1085 1085 &.inherited {
1086 1086 #inherit_overlay {
1087 1087 display: block;
1088 1088 }
1089 1089 #custom_overlay {
1090 1090 display: none;
1091 1091 }
1092 1092 }
1093 1093 }
1094 1094 table.issuetracker {
1095 1095 &.readonly {
1096 1096 tr, td {
1097 1097 color: @grey3;
1098 1098 }
1099 1099 }
1100 1100 .edit {
1101 1101 display: none;
1102 1102 }
1103 1103 .editopen {
1104 1104 .edit {
1105 1105 display: inline;
1106 1106 }
1107 1107 .entry {
1108 1108 display: none;
1109 1109 }
1110 1110 }
1111 1111 tr td.td-action {
1112 1112 min-width: 117px;
1113 1113 }
1114 1114 td input {
1115 1115 max-width: none;
1116 1116 min-width: 30px;
1117 1117 width: 80%;
1118 1118 }
1119 1119 .issuetracker_pref input {
1120 1120 width: 40%;
1121 1121 }
1122 1122 input.edit_issuetracker_update {
1123 1123 margin-right: 0;
1124 1124 width: auto;
1125 1125 }
1126 1126 }
1127 1127
1128 1128 table.integrations {
1129 1129 .td-icon {
1130 1130 width: 20px;
1131 1131 .integration-icon {
1132 1132 height: 20px;
1133 1133 width: 20px;
1134 1134 }
1135 1135 }
1136 1136 }
1137 1137
1138 1138 .integrations {
1139 1139 a.integration-box {
1140 1140 color: @text-color;
1141 1141 &:hover {
1142 1142 .panel {
1143 1143 background: #fbfbfb;
1144 1144 }
1145 1145 }
1146 1146 .integration-icon {
1147 1147 width: 30px;
1148 1148 height: 30px;
1149 1149 margin-right: 20px;
1150 1150 float: left;
1151 1151 }
1152 1152
1153 1153 .panel-body {
1154 1154 padding: 10px;
1155 1155 }
1156 1156 .panel {
1157 1157 margin-bottom: 10px;
1158 1158 }
1159 1159 h2 {
1160 1160 display: inline-block;
1161 1161 margin: 0;
1162 1162 min-width: 140px;
1163 1163 }
1164 1164 }
1165 1165 }
1166 1166
1167 1167 //Permissions Settings
1168 1168 #add_perm {
1169 1169 margin: 0 0 @padding;
1170 1170 cursor: pointer;
1171 1171 }
1172 1172
1173 1173 .perm_ac {
1174 1174 input {
1175 1175 width: 95%;
1176 1176 }
1177 1177 }
1178 1178
1179 1179 .autocomplete-suggestions {
1180 1180 width: auto !important; // overrides autocomplete.js
1181 1181 margin: 0;
1182 1182 border: @border-thickness solid @rcblue;
1183 1183 border-radius: @border-radius;
1184 1184 color: @rcblue;
1185 1185 background-color: white;
1186 1186 }
1187 1187 .autocomplete-selected {
1188 1188 background: #F0F0F0;
1189 1189 }
1190 1190 .ac-container-wrap {
1191 1191 margin: 0;
1192 1192 padding: 8px;
1193 1193 border-bottom: @border-thickness solid @rclightblue;
1194 1194 list-style-type: none;
1195 1195 cursor: pointer;
1196 1196
1197 1197 &:hover {
1198 1198 background-color: @rclightblue;
1199 1199 }
1200 1200
1201 1201 img {
1202 1202 height: @gravatar-size;
1203 1203 width: @gravatar-size;
1204 1204 margin-right: 1em;
1205 1205 }
1206 1206
1207 1207 strong {
1208 1208 font-weight: normal;
1209 1209 }
1210 1210 }
1211 1211
1212 1212 // Settings Dropdown
1213 1213 .user-menu .container {
1214 1214 padding: 0 4px;
1215 1215 margin: 0;
1216 1216 }
1217 1217
1218 1218 .user-menu .gravatar {
1219 1219 cursor: pointer;
1220 1220 }
1221 1221
1222 1222 .codeblock {
1223 1223 margin-bottom: @padding;
1224 1224 clear: both;
1225 1225
1226 1226 .stats{
1227 1227 overflow: hidden;
1228 1228 }
1229 1229
1230 1230 .message{
1231 1231 textarea{
1232 1232 margin: 0;
1233 1233 }
1234 1234 }
1235 1235
1236 1236 .code-header {
1237 1237 .stats {
1238 1238 line-height: 2em;
1239 1239
1240 1240 .revision_id {
1241 1241 margin-left: 0;
1242 1242 }
1243 1243 .buttons {
1244 1244 padding-right: 0;
1245 1245 }
1246 1246 }
1247 1247
1248 1248 .item{
1249 1249 margin-right: 0.5em;
1250 1250 }
1251 1251 }
1252 1252
1253 1253 #editor_container{
1254 1254 position: relative;
1255 1255 margin: @padding;
1256 1256 }
1257 1257 }
1258 1258
1259 1259 #file_history_container {
1260 1260 display: none;
1261 1261 }
1262 1262
1263 1263 .file-history-inner {
1264 1264 margin-bottom: 10px;
1265 1265 }
1266 1266
1267 1267 // Pull Requests
1268 1268 .summary-details {
1269 1269 width: 72%;
1270 1270 }
1271 1271 .pr-summary {
1272 1272 border-bottom: @border-thickness solid @grey5;
1273 1273 margin-bottom: @space;
1274 1274 }
1275 1275 .reviewers-title {
1276 1276 width: 25%;
1277 1277 min-width: 200px;
1278 1278 }
1279 1279 .reviewers {
1280 1280 width: 25%;
1281 1281 min-width: 200px;
1282 1282 }
1283 1283 .reviewers ul li {
1284 1284 position: relative;
1285 1285 width: 100%;
1286 1286 margin-bottom: 8px;
1287 1287 }
1288 1288 .reviewers_member {
1289 1289 width: 100%;
1290 1290 overflow: auto;
1291 1291 }
1292 1292 .reviewer_reason {
1293 1293 padding-left: 20px;
1294 1294 }
1295 1295 .reviewer_status {
1296 1296 display: inline-block;
1297 1297 vertical-align: top;
1298 1298 width: 7%;
1299 1299 min-width: 20px;
1300 1300 height: 1.2em;
1301 1301 margin-top: 3px;
1302 1302 line-height: 1em;
1303 1303 }
1304 1304
1305 1305 .reviewer_name {
1306 1306 display: inline-block;
1307 1307 max-width: 83%;
1308 1308 padding-right: 20px;
1309 1309 vertical-align: middle;
1310 1310 line-height: 1;
1311 1311
1312 1312 .rc-user {
1313 1313 min-width: 0;
1314 1314 margin: -2px 1em 0 0;
1315 1315 }
1316 1316
1317 1317 .reviewer {
1318 1318 float: left;
1319 1319 }
1320 1320
1321 1321 &.to-delete {
1322 1322 .user,
1323 1323 .reviewer {
1324 1324 text-decoration: line-through;
1325 1325 }
1326 1326 }
1327 1327 }
1328 1328
1329 1329 .reviewer_member_remove {
1330 1330 position: absolute;
1331 1331 right: 0;
1332 1332 top: 0;
1333 1333 width: 16px;
1334 1334 margin-bottom: 10px;
1335 1335 padding: 0;
1336 1336 color: black;
1337 1337 }
1338 1338 .reviewer_member_status {
1339 1339 margin-top: 5px;
1340 1340 }
1341 1341 .pr-summary #summary{
1342 1342 width: 100%;
1343 1343 }
1344 1344 .pr-summary .action_button:hover {
1345 1345 border: 0;
1346 1346 cursor: pointer;
1347 1347 }
1348 1348 .pr-details-title {
1349 1349 padding-bottom: 8px;
1350 1350 border-bottom: @border-thickness solid @grey5;
1351 1351
1352 1352 .action_button.disabled {
1353 1353 color: @grey4;
1354 1354 cursor: inherit;
1355 1355 }
1356 1356 .action_button {
1357 1357 color: @rcblue;
1358 1358 }
1359 1359 }
1360 1360 .pr-details-content {
1361 1361 margin-top: @textmargin;
1362 1362 margin-bottom: @textmargin;
1363 1363 }
1364 1364 .pr-description {
1365 1365 white-space:pre-wrap;
1366 1366 }
1367 1367 .group_members {
1368 1368 margin-top: 0;
1369 1369 padding: 0;
1370 1370 list-style: outside none none;
1371 1371
1372 1372 img {
1373 1373 height: @gravatar-size;
1374 1374 width: @gravatar-size;
1375 1375 margin-right: .5em;
1376 1376 margin-left: 3px;
1377 1377 }
1378 1378
1379 1379 .to-delete {
1380 1380 .user {
1381 1381 text-decoration: line-through;
1382 1382 }
1383 1383 }
1384 1384 }
1385 1385
1386 1386 .compare_view_commits_title {
1387 1387 .disabled {
1388 1388 cursor: inherit;
1389 1389 &:hover{
1390 1390 background-color: inherit;
1391 1391 color: inherit;
1392 1392 }
1393 1393 }
1394 1394 }
1395 1395
1396 1396 // new entry in group_members
1397 1397 .td-author-new-entry {
1398 1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1399 1399 }
1400 1400
1401 1401 .usergroup_member_remove {
1402 1402 width: 16px;
1403 1403 margin-bottom: 10px;
1404 1404 padding: 0;
1405 1405 color: black !important;
1406 1406 cursor: pointer;
1407 1407 }
1408 1408
1409 1409 .reviewer_ac .ac-input {
1410 1410 width: 92%;
1411 1411 margin-bottom: 1em;
1412 1412 }
1413 1413
1414 1414 .compare_view_commits tr{
1415 1415 height: 20px;
1416 1416 }
1417 1417 .compare_view_commits td {
1418 1418 vertical-align: top;
1419 1419 padding-top: 10px;
1420 1420 }
1421 1421 .compare_view_commits .author {
1422 1422 margin-left: 5px;
1423 1423 }
1424 1424
1425 1425 .compare_view_files {
1426 1426 width: 100%;
1427 1427
1428 1428 td {
1429 1429 vertical-align: middle;
1430 1430 }
1431 1431 }
1432 1432
1433 1433 .compare_view_filepath {
1434 1434 color: @grey1;
1435 1435 }
1436 1436
1437 1437 .show_more {
1438 1438 display: inline-block;
1439 1439 position: relative;
1440 1440 vertical-align: middle;
1441 1441 width: 4px;
1442 1442 height: @basefontsize;
1443 1443
1444 1444 &:after {
1445 1445 content: "\00A0\25BE";
1446 1446 display: inline-block;
1447 1447 width:10px;
1448 1448 line-height: 5px;
1449 1449 font-size: 12px;
1450 1450 cursor: pointer;
1451 1451 }
1452 1452 }
1453 1453
1454 1454 .journal_more .show_more {
1455 1455 display: inline;
1456 1456
1457 1457 &:after {
1458 1458 content: none;
1459 1459 }
1460 1460 }
1461 1461
1462 1462 .open .show_more:after,
1463 1463 .select2-dropdown-open .show_more:after {
1464 1464 .rotate(180deg);
1465 1465 margin-left: 4px;
1466 1466 }
1467 1467
1468 1468
1469 1469 .compare_view_commits .collapse_commit:after {
1470 1470 cursor: pointer;
1471 1471 content: "\00A0\25B4";
1472 1472 margin-left: -3px;
1473 1473 font-size: 17px;
1474 1474 color: @grey4;
1475 1475 }
1476 1476
1477 1477 .diff_links {
1478 1478 margin-left: 8px;
1479 1479 }
1480 1480
1481 1481 div.ancestor {
1482 1482 margin: -30px 0px;
1483 1483 }
1484 1484
1485 1485 .cs_icon_td input[type="checkbox"] {
1486 1486 display: none;
1487 1487 }
1488 1488
1489 1489 .cs_icon_td .expand_file_icon:after {
1490 1490 cursor: pointer;
1491 1491 content: "\00A0\25B6";
1492 1492 font-size: 12px;
1493 1493 color: @grey4;
1494 1494 }
1495 1495
1496 1496 .cs_icon_td .collapse_file_icon:after {
1497 1497 cursor: pointer;
1498 1498 content: "\00A0\25BC";
1499 1499 font-size: 12px;
1500 1500 color: @grey4;
1501 1501 }
1502 1502
1503 1503 /*new binary
1504 1504 NEW_FILENODE = 1
1505 1505 DEL_FILENODE = 2
1506 1506 MOD_FILENODE = 3
1507 1507 RENAMED_FILENODE = 4
1508 1508 COPIED_FILENODE = 5
1509 1509 CHMOD_FILENODE = 6
1510 1510 BIN_FILENODE = 7
1511 1511 */
1512 1512 .cs_files_expand {
1513 1513 font-size: @basefontsize + 5px;
1514 1514 line-height: 1.8em;
1515 1515 float: right;
1516 1516 }
1517 1517
1518 1518 .cs_files_expand span{
1519 1519 color: @rcblue;
1520 1520 cursor: pointer;
1521 1521 }
1522 1522 .cs_files {
1523 1523 clear: both;
1524 1524 padding-bottom: @padding;
1525 1525
1526 1526 .cur_cs {
1527 1527 margin: 10px 2px;
1528 1528 font-weight: bold;
1529 1529 }
1530 1530
1531 1531 .node {
1532 1532 float: left;
1533 1533 }
1534 1534
1535 1535 .changes {
1536 1536 float: right;
1537 1537 color: white;
1538 1538 font-size: @basefontsize - 4px;
1539 1539 margin-top: 4px;
1540 1540 opacity: 0.6;
1541 1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1542 1542
1543 1543 .added {
1544 1544 background-color: @alert1;
1545 1545 float: left;
1546 1546 text-align: center;
1547 1547 }
1548 1548
1549 1549 .deleted {
1550 1550 background-color: @alert2;
1551 1551 float: left;
1552 1552 text-align: center;
1553 1553 }
1554 1554
1555 1555 .bin {
1556 1556 background-color: @alert1;
1557 1557 text-align: center;
1558 1558 }
1559 1559
1560 1560 /*new binary*/
1561 1561 .bin.bin1 {
1562 1562 background-color: @alert1;
1563 1563 text-align: center;
1564 1564 }
1565 1565
1566 1566 /*deleted binary*/
1567 1567 .bin.bin2 {
1568 1568 background-color: @alert2;
1569 1569 text-align: center;
1570 1570 }
1571 1571
1572 1572 /*mod binary*/
1573 1573 .bin.bin3 {
1574 1574 background-color: @grey2;
1575 1575 text-align: center;
1576 1576 }
1577 1577
1578 1578 /*rename file*/
1579 1579 .bin.bin4 {
1580 1580 background-color: @alert4;
1581 1581 text-align: center;
1582 1582 }
1583 1583
1584 1584 /*copied file*/
1585 1585 .bin.bin5 {
1586 1586 background-color: @alert4;
1587 1587 text-align: center;
1588 1588 }
1589 1589
1590 1590 /*chmod file*/
1591 1591 .bin.bin6 {
1592 1592 background-color: @grey2;
1593 1593 text-align: center;
1594 1594 }
1595 1595 }
1596 1596 }
1597 1597
1598 1598 .cs_files .cs_added, .cs_files .cs_A,
1599 1599 .cs_files .cs_added, .cs_files .cs_M,
1600 1600 .cs_files .cs_added, .cs_files .cs_D {
1601 1601 height: 16px;
1602 1602 padding-right: 10px;
1603 1603 margin-top: 7px;
1604 1604 text-align: left;
1605 1605 }
1606 1606
1607 1607 .cs_icon_td {
1608 1608 min-width: 16px;
1609 1609 width: 16px;
1610 1610 }
1611 1611
1612 1612 .pull-request-merge {
1613 padding: 10px 0;
1613 border: 1px solid @grey5;
1614 padding: 10px 0px 20px;
1614 1615 margin-top: 10px;
1615 1616 margin-bottom: 20px;
1616 1617 }
1617 1618
1619 .pull-request-merge ul {
1620 padding: 0px 0px;
1621 }
1622
1623 .pull-request-merge li:before{
1624 content:none;
1625 }
1626
1618 1627 .pull-request-merge .pull-request-wrap {
1619 height: 25px;
1620 padding: 5px 0;
1628 height: auto;
1629 padding: 0px 0px;
1630 text-align: right;
1621 1631 }
1622 1632
1623 1633 .pull-request-merge span {
1624 margin-right: 10px;
1634 margin-right: 5px;
1635 }
1636
1637 .pull-request-merge-actions {
1638 height: 30px;
1639 padding: 0px 0px;
1640 }
1641
1642 .merge-message {
1643 font-size: 1.2em
1625 1644 }
1645 .merge-message li{
1646 text-decoration: none;
1647 }
1648
1649 .merge-message.success i {
1650 color:@alert1;
1651 }
1652 .merge-message.warning i {
1653 color: @alert3;
1654 }
1655 .merge-message.error i {
1656 color:@alert2;
1657 }
1658
1659
1626 1660
1627 1661 .pr-versions {
1628 1662 position: relative;
1629 1663 top: 6px;
1630 1664 }
1631 1665
1632 1666 #close_pull_request {
1633 1667 margin-right: 0px;
1634 1668 }
1635 1669
1636 1670 .empty_data {
1637 1671 color: @grey4;
1638 1672 }
1639 1673
1640 1674 #changeset_compare_view_content {
1641 1675 margin-bottom: @space;
1642 1676 clear: both;
1643 1677 width: 100%;
1644 1678 box-sizing: border-box;
1645 1679 .border-radius(@border-radius);
1646 1680
1647 1681 .help-block {
1648 1682 margin: @padding 0;
1649 1683 color: @text-color;
1650 1684 }
1651 1685
1652 1686 .empty_data {
1653 1687 margin: @padding 0;
1654 1688 }
1655 1689
1656 1690 .alert {
1657 1691 margin-bottom: @space;
1658 1692 }
1659 1693 }
1660 1694
1661 1695 .table_disp {
1662 1696 .status {
1663 1697 width: auto;
1664 1698
1665 1699 .flag_status {
1666 1700 float: left;
1667 1701 }
1668 1702 }
1669 1703 }
1670 1704
1671 1705 .status_box_menu {
1672 1706 margin: 0;
1673 1707 }
1674 1708
1675 1709 .notification-table{
1676 1710 margin-bottom: @space;
1677 1711 display: table;
1678 1712 width: 100%;
1679 1713
1680 1714 .container{
1681 1715 display: table-row;
1682 1716
1683 1717 .notification-header{
1684 1718 border-bottom: @border-thickness solid @border-default-color;
1685 1719 }
1686 1720
1687 1721 .notification-subject{
1688 1722 display: table-cell;
1689 1723 }
1690 1724 }
1691 1725 }
1692 1726
1693 1727 // Notifications
1694 1728 .notification-header{
1695 1729 display: table;
1696 1730 width: 100%;
1697 1731 padding: floor(@basefontsize/2) 0;
1698 1732 line-height: 1em;
1699 1733
1700 1734 .desc, .delete-notifications, .read-notifications{
1701 1735 display: table-cell;
1702 1736 text-align: left;
1703 1737 }
1704 1738
1705 1739 .desc{
1706 1740 width: 1163px;
1707 1741 }
1708 1742
1709 1743 .delete-notifications, .read-notifications{
1710 1744 width: 35px;
1711 1745 min-width: 35px; //fixes when only one button is displayed
1712 1746 }
1713 1747 }
1714 1748
1715 1749 .notification-body {
1716 1750 .markdown-block,
1717 1751 .rst-block {
1718 1752 padding: @padding 0;
1719 1753 }
1720 1754
1721 1755 .notification-subject {
1722 1756 padding: @textmargin 0;
1723 1757 border-bottom: @border-thickness solid @border-default-color;
1724 1758 }
1725 1759 }
1726 1760
1727 1761
1728 1762 .notifications_buttons{
1729 1763 float: right;
1730 1764 }
1731 1765
1732 1766 #notification-status{
1733 1767 display: inline;
1734 1768 }
1735 1769
1736 1770 // Repositories
1737 1771
1738 1772 #summary.fields{
1739 1773 display: table;
1740 1774
1741 1775 .field{
1742 1776 display: table-row;
1743 1777
1744 1778 .label-summary{
1745 1779 display: table-cell;
1746 1780 min-width: @label-summary-minwidth;
1747 1781 padding-top: @padding/2;
1748 1782 padding-bottom: @padding/2;
1749 1783 padding-right: @padding/2;
1750 1784 }
1751 1785
1752 1786 .input{
1753 1787 display: table-cell;
1754 1788 padding: @padding/2;
1755 1789
1756 1790 input{
1757 1791 min-width: 29em;
1758 1792 padding: @padding/4;
1759 1793 }
1760 1794 }
1761 1795 .statistics, .downloads{
1762 1796 .disabled{
1763 1797 color: @grey4;
1764 1798 }
1765 1799 }
1766 1800 }
1767 1801 }
1768 1802
1769 1803 #summary{
1770 1804 width: 70%;
1771 1805 }
1772 1806
1773 1807
1774 1808 // Journal
1775 1809 .journal.title {
1776 1810 h5 {
1777 1811 float: left;
1778 1812 margin: 0;
1779 1813 width: 70%;
1780 1814 }
1781 1815
1782 1816 ul {
1783 1817 float: right;
1784 1818 display: inline-block;
1785 1819 margin: 0;
1786 1820 width: 30%;
1787 1821 text-align: right;
1788 1822
1789 1823 li {
1790 1824 display: inline;
1791 1825 font-size: @journal-fontsize;
1792 1826 line-height: 1em;
1793 1827
1794 1828 &:before { content: none; }
1795 1829 }
1796 1830 }
1797 1831 }
1798 1832
1799 1833 .filterexample {
1800 1834 position: absolute;
1801 1835 top: 95px;
1802 1836 left: @contentpadding;
1803 1837 color: @rcblue;
1804 1838 font-size: 11px;
1805 1839 font-family: @text-regular;
1806 1840 cursor: help;
1807 1841
1808 1842 &:hover {
1809 1843 color: @rcdarkblue;
1810 1844 }
1811 1845
1812 1846 @media (max-width:768px) {
1813 1847 position: relative;
1814 1848 top: auto;
1815 1849 left: auto;
1816 1850 display: block;
1817 1851 }
1818 1852 }
1819 1853
1820 1854
1821 1855 #journal{
1822 1856 margin-bottom: @space;
1823 1857
1824 1858 .journal_day{
1825 1859 margin-bottom: @textmargin/2;
1826 1860 padding-bottom: @textmargin/2;
1827 1861 font-size: @journal-fontsize;
1828 1862 border-bottom: @border-thickness solid @border-default-color;
1829 1863 }
1830 1864
1831 1865 .journal_container{
1832 1866 margin-bottom: @space;
1833 1867
1834 1868 .journal_user{
1835 1869 display: inline-block;
1836 1870 }
1837 1871 .journal_action_container{
1838 1872 display: block;
1839 1873 margin-top: @textmargin;
1840 1874
1841 1875 div{
1842 1876 display: inline;
1843 1877 }
1844 1878
1845 1879 div.journal_action_params{
1846 1880 display: block;
1847 1881 }
1848 1882
1849 1883 div.journal_repo:after{
1850 1884 content: "\A";
1851 1885 white-space: pre;
1852 1886 }
1853 1887
1854 1888 div.date{
1855 1889 display: block;
1856 1890 margin-bottom: @textmargin;
1857 1891 }
1858 1892 }
1859 1893 }
1860 1894 }
1861 1895
1862 1896 // Files
1863 1897 .edit-file-title {
1864 1898 border-bottom: @border-thickness solid @border-default-color;
1865 1899
1866 1900 .breadcrumbs {
1867 1901 margin-bottom: 0;
1868 1902 }
1869 1903 }
1870 1904
1871 1905 .edit-file-fieldset {
1872 1906 margin-top: @sidebarpadding;
1873 1907
1874 1908 .fieldset {
1875 1909 .left-label {
1876 1910 width: 13%;
1877 1911 }
1878 1912 .right-content {
1879 1913 width: 87%;
1880 1914 max-width: 100%;
1881 1915 }
1882 1916 .filename-label {
1883 1917 margin-top: 13px;
1884 1918 }
1885 1919 .commit-message-label {
1886 1920 margin-top: 4px;
1887 1921 }
1888 1922 .file-upload-input {
1889 1923 input {
1890 1924 display: none;
1891 1925 }
1892 1926 }
1893 1927 p {
1894 1928 margin-top: 5px;
1895 1929 }
1896 1930
1897 1931 }
1898 1932 .custom-path-link {
1899 1933 margin-left: 5px;
1900 1934 }
1901 1935 #commit {
1902 1936 resize: vertical;
1903 1937 }
1904 1938 }
1905 1939
1906 1940 .delete-file-preview {
1907 1941 max-height: 250px;
1908 1942 }
1909 1943
1910 1944 .new-file,
1911 1945 #filter_activate,
1912 1946 #filter_deactivate {
1913 1947 float: left;
1914 1948 margin: 0 0 0 15px;
1915 1949 }
1916 1950
1917 1951 h3.files_location{
1918 1952 line-height: 2.4em;
1919 1953 }
1920 1954
1921 1955 .browser-nav {
1922 1956 display: table;
1923 1957 margin-bottom: @space;
1924 1958
1925 1959
1926 1960 .info_box {
1927 1961 display: inline-table;
1928 1962 height: 2.5em;
1929 1963
1930 1964 .browser-cur-rev, .info_box_elem {
1931 1965 display: table-cell;
1932 1966 vertical-align: middle;
1933 1967 }
1934 1968
1935 1969 .info_box_elem {
1936 1970 border-top: @border-thickness solid @rcblue;
1937 1971 border-bottom: @border-thickness solid @rcblue;
1938 1972
1939 1973 #at_rev, a {
1940 1974 padding: 0.6em 0.9em;
1941 1975 margin: 0;
1942 1976 .box-shadow(none);
1943 1977 border: 0;
1944 1978 height: 12px;
1945 1979 }
1946 1980
1947 1981 input#at_rev {
1948 1982 max-width: 50px;
1949 1983 text-align: right;
1950 1984 }
1951 1985
1952 1986 &.previous {
1953 1987 border: @border-thickness solid @rcblue;
1954 1988 .disabled {
1955 1989 color: @grey4;
1956 1990 cursor: not-allowed;
1957 1991 }
1958 1992 }
1959 1993
1960 1994 &.next {
1961 1995 border: @border-thickness solid @rcblue;
1962 1996 .disabled {
1963 1997 color: @grey4;
1964 1998 cursor: not-allowed;
1965 1999 }
1966 2000 }
1967 2001 }
1968 2002
1969 2003 .browser-cur-rev {
1970 2004
1971 2005 span{
1972 2006 margin: 0;
1973 2007 color: @rcblue;
1974 2008 height: 12px;
1975 2009 display: inline-block;
1976 2010 padding: 0.7em 1em ;
1977 2011 border: @border-thickness solid @rcblue;
1978 2012 margin-right: @padding;
1979 2013 }
1980 2014 }
1981 2015 }
1982 2016
1983 2017 .search_activate {
1984 2018 display: table-cell;
1985 2019 vertical-align: middle;
1986 2020
1987 2021 input, label{
1988 2022 margin: 0;
1989 2023 padding: 0;
1990 2024 }
1991 2025
1992 2026 input{
1993 2027 margin-left: @textmargin;
1994 2028 }
1995 2029
1996 2030 }
1997 2031 }
1998 2032
1999 2033 .browser-cur-rev{
2000 2034 margin-bottom: @textmargin;
2001 2035 }
2002 2036
2003 2037 #node_filter_box_loading{
2004 2038 .info_text;
2005 2039 }
2006 2040
2007 2041 .browser-search {
2008 2042 margin: -25px 0px 5px 0px;
2009 2043 }
2010 2044
2011 2045 .node-filter {
2012 2046 font-size: @repo-title-fontsize;
2013 2047 padding: 4px 0px 0px 0px;
2014 2048
2015 2049 .node-filter-path {
2016 2050 float: left;
2017 2051 color: @grey4;
2018 2052 }
2019 2053 .node-filter-input {
2020 2054 float: left;
2021 2055 margin: -2px 0px 0px 2px;
2022 2056 input {
2023 2057 padding: 2px;
2024 2058 border: none;
2025 2059 font-size: @repo-title-fontsize;
2026 2060 }
2027 2061 }
2028 2062 }
2029 2063
2030 2064
2031 2065 .browser-result{
2032 2066 td a{
2033 2067 margin-left: 0.5em;
2034 2068 display: inline-block;
2035 2069
2036 2070 em{
2037 2071 font-family: @text-bold;
2038 2072 }
2039 2073 }
2040 2074 }
2041 2075
2042 2076 .browser-highlight{
2043 2077 background-color: @grey5-alpha;
2044 2078 }
2045 2079
2046 2080
2047 2081 // Search
2048 2082
2049 2083 .search-form{
2050 2084 #q {
2051 2085 width: @search-form-width;
2052 2086 }
2053 2087 .fields{
2054 2088 margin: 0 0 @space;
2055 2089 }
2056 2090
2057 2091 label{
2058 2092 display: inline-block;
2059 2093 margin-right: @textmargin;
2060 2094 padding-top: 0.25em;
2061 2095 }
2062 2096
2063 2097
2064 2098 .results{
2065 2099 clear: both;
2066 2100 margin: 0 0 @padding;
2067 2101 }
2068 2102 }
2069 2103
2070 2104 div.search-feedback-items {
2071 2105 display: inline-block;
2072 2106 padding:0px 0px 0px 96px;
2073 2107 }
2074 2108
2075 2109 div.search-code-body {
2076 2110 background-color: #ffffff; padding: 5px 0 5px 10px;
2077 2111 pre {
2078 2112 .match { background-color: #faffa6;}
2079 2113 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2080 2114 }
2081 2115 }
2082 2116
2083 2117 .expand_commit.search {
2084 2118 .show_more.open {
2085 2119 height: auto;
2086 2120 max-height: none;
2087 2121 }
2088 2122 }
2089 2123
2090 2124 .search-results {
2091 2125
2092 2126 h2 {
2093 2127 margin-bottom: 0;
2094 2128 }
2095 2129 .codeblock {
2096 2130 border: none;
2097 2131 background: transparent;
2098 2132 }
2099 2133
2100 2134 .codeblock-header {
2101 2135 border: none;
2102 2136 background: transparent;
2103 2137 }
2104 2138
2105 2139 .code-body {
2106 2140 border: @border-thickness solid @border-default-color;
2107 2141 .border-radius(@border-radius);
2108 2142 }
2109 2143
2110 2144 .td-commit {
2111 2145 &:extend(pre);
2112 2146 border-bottom: @border-thickness solid @border-default-color;
2113 2147 }
2114 2148
2115 2149 .message {
2116 2150 height: auto;
2117 2151 max-width: 350px;
2118 2152 white-space: normal;
2119 2153 text-overflow: initial;
2120 2154 overflow: visible;
2121 2155
2122 2156 .match { background-color: #faffa6;}
2123 2157 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2124 2158 }
2125 2159
2126 2160 }
2127 2161
2128 2162 table.rctable td.td-search-results div {
2129 2163 max-width: 100%;
2130 2164 }
2131 2165
2132 2166 #tip-box, .tip-box{
2133 2167 padding: @menupadding/2;
2134 2168 display: block;
2135 2169 border: @border-thickness solid @border-highlight-color;
2136 2170 .border-radius(@border-radius);
2137 2171 background-color: white;
2138 2172 z-index: 99;
2139 2173 white-space: pre-wrap;
2140 2174 }
2141 2175
2142 2176 #linktt {
2143 2177 width: 79px;
2144 2178 }
2145 2179
2146 2180 #help_kb .modal-content{
2147 2181 max-width: 750px;
2148 2182 margin: 10% auto;
2149 2183
2150 2184 table{
2151 2185 td,th{
2152 2186 border-bottom: none;
2153 2187 line-height: 2.5em;
2154 2188 }
2155 2189 th{
2156 2190 padding-bottom: @textmargin/2;
2157 2191 }
2158 2192 td.keys{
2159 2193 text-align: center;
2160 2194 }
2161 2195 }
2162 2196
2163 2197 .block-left{
2164 2198 width: 45%;
2165 2199 margin-right: 5%;
2166 2200 }
2167 2201 .modal-footer{
2168 2202 clear: both;
2169 2203 }
2170 2204 .key.tag{
2171 2205 padding: 0.5em;
2172 2206 background-color: @rcblue;
2173 2207 color: white;
2174 2208 border-color: @rcblue;
2175 2209 .box-shadow(none);
2176 2210 }
2177 2211 }
2178 2212
2179 2213
2180 2214
2181 2215 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2182 2216
2183 2217 @import 'statistics-graph';
2184 2218 @import 'tables';
2185 2219 @import 'forms';
2186 2220 @import 'diff';
2187 2221 @import 'summary';
2188 2222 @import 'navigation';
2189 2223
2190 2224 //--- SHOW/HIDE SECTIONS --//
2191 2225
2192 2226 .btn-collapse {
2193 2227 float: right;
2194 2228 text-align: right;
2195 2229 font-family: @text-light;
2196 2230 font-size: @basefontsize;
2197 2231 cursor: pointer;
2198 2232 border: none;
2199 2233 color: @rcblue;
2200 2234 }
2201 2235
2202 2236 table.rctable,
2203 2237 table.dataTable {
2204 2238 .btn-collapse {
2205 2239 float: right;
2206 2240 text-align: right;
2207 2241 }
2208 2242 }
2209 2243
2210 2244
2211 2245 // TODO: johbo: Fix for IE10, this avoids that we see a border
2212 2246 // and padding around checkboxes and radio boxes. Move to the right place,
2213 2247 // or better: Remove this once we did the form refactoring.
2214 2248 input[type=checkbox],
2215 2249 input[type=radio] {
2216 2250 padding: 0;
2217 2251 border: none;
2218 2252 }
2219 2253
2220 2254 .toggle-ajax-spinner{
2221 2255 height: 16px;
2222 2256 width: 16px;
2223 2257 }
@@ -1,796 +1,802 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.cmBox = this.withLineNo('#text');
96 96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
97 97
98 98 this.statusChange = this.withLineNo('#change_status');
99 99
100 100 this.submitForm = formElement;
101 101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 102 this.submitButtonText = this.submitButton.val();
103 103
104 104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 105 {'repo_name': templateContext.repo_name});
106 106
107 107 if (resolvesCommentId){
108 108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 110 $(this.commentType).prop('disabled', true);
111 111 $(this.commentType).addClass('disabled');
112 112
113 113 // disable select
114 114 setTimeout(function() {
115 115 $(self.statusChange).select2('readonly', true);
116 116 }, 10);
117 117
118 118 var resolvedInfo = (
119 119 '<li class="">' +
120 120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 122 '</li>'
123 123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 125 }
126 126
127 127 // based on commitId, or pullRequestId decide where do we submit
128 128 // out data
129 129 if (this.commitId){
130 130 this.submitUrl = pyroutes.url('changeset_comment',
131 131 {'repo_name': templateContext.repo_name,
132 132 'revision': this.commitId});
133 133 this.selfUrl = pyroutes.url('changeset_home',
134 134 {'repo_name': templateContext.repo_name,
135 135 'revision': this.commitId});
136 136
137 137 } else if (this.pullRequestId) {
138 138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 139 {'repo_name': templateContext.repo_name,
140 140 'pull_request_id': this.pullRequestId});
141 141 this.selfUrl = pyroutes.url('pullrequest_show',
142 142 {'repo_name': templateContext.repo_name,
143 143 'pull_request_id': this.pullRequestId});
144 144
145 145 } else {
146 146 throw new Error(
147 147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 148 }
149 149
150 150 // FUNCTIONS and helpers
151 151 var self = this;
152 152
153 153 this.isInline = function(){
154 154 return this.lineNo && this.lineNo != 'general';
155 155 };
156 156
157 157 this.getCmInstance = function(){
158 158 return this.cm
159 159 };
160 160
161 161 this.setPlaceholder = function(placeholder) {
162 162 var cm = this.getCmInstance();
163 163 if (cm){
164 164 cm.setOption('placeholder', placeholder);
165 165 }
166 166 };
167 167
168 168 this.getCommentStatus = function() {
169 169 return $(this.submitForm).find(this.statusChange).val();
170 170 };
171 171 this.getCommentType = function() {
172 172 return $(this.submitForm).find(this.commentType).val();
173 173 };
174 174
175 175 this.getResolvesId = function() {
176 176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 177 };
178 178 this.markCommentResolved = function(resolvedCommentId){
179 179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 181 };
182 182
183 183 this.isAllowedToSubmit = function() {
184 184 return !$(this.submitButton).prop('disabled');
185 185 };
186 186
187 187 this.initStatusChangeSelector = function(){
188 188 var formatChangeStatus = function(state, escapeMarkup) {
189 189 var originalOption = state.element;
190 190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 191 '<span>' + escapeMarkup(state.text) + '</span>';
192 192 };
193 193 var formatResult = function(result, container, query, escapeMarkup) {
194 194 return formatChangeStatus(result, escapeMarkup);
195 195 };
196 196
197 197 var formatSelection = function(data, container, escapeMarkup) {
198 198 return formatChangeStatus(data, escapeMarkup);
199 199 };
200 200
201 201 $(this.submitForm).find(this.statusChange).select2({
202 202 placeholder: _gettext('Status Review'),
203 203 formatResult: formatResult,
204 204 formatSelection: formatSelection,
205 205 containerCssClass: "drop-menu status_box_menu",
206 206 dropdownCssClass: "drop-menu-dropdown",
207 207 dropdownAutoWidth: true,
208 208 minimumResultsForSearch: -1
209 209 });
210 210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 211 var status = self.getCommentStatus();
212 212 if (status && !self.isInline()) {
213 213 $(self.submitButton).prop('disabled', false);
214 214 }
215 215
216 216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 217 self.setPlaceholder(placeholderText)
218 218 })
219 219 };
220 220
221 221 // reset the comment form into it's original state
222 222 this.resetCommentFormState = function(content) {
223 223 content = content || '';
224 224
225 225 $(this.editContainer).show();
226 226 $(this.editButton).parent().addClass('active');
227 227
228 228 $(this.previewContainer).hide();
229 229 $(this.previewButton).parent().removeClass('active');
230 230
231 231 this.setActionButtonsDisabled(true);
232 232 self.cm.setValue(content);
233 233 self.cm.setOption("readOnly", false);
234 234
235 235 if (this.resolvesId) {
236 236 // destroy the resolve action
237 237 $(this.resolvesId).parent().remove();
238 238 }
239 239
240 240 $(this.statusChange).select2('readonly', false);
241 241 };
242 242
243 this.globalSubmitSuccessCallback = function(){};
243 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
247 }
248 };
244 249
245 250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
246 251 failHandler = failHandler || function() {};
247 252 var postData = toQueryString(postData);
248 253 var request = $.ajax({
249 254 url: url,
250 255 type: 'POST',
251 256 data: postData,
252 257 headers: {'X-PARTIAL-XHR': true}
253 258 })
254 259 .done(function(data) {
255 260 successHandler(data);
256 261 })
257 262 .fail(function(data, textStatus, errorThrown){
258 263 alert(
259 264 "Error while submitting comment.\n" +
260 265 "Error code {0} ({1}).".format(data.status, data.statusText));
261 266 failHandler()
262 267 });
263 268 return request;
264 269 };
265 270
266 271 // overwrite a submitHandler, we need to do it for inline comments
267 272 this.setHandleFormSubmit = function(callback) {
268 273 this.handleFormSubmit = callback;
269 274 };
270 275
271 276 // overwrite a submitSuccessHandler
272 277 this.setGlobalSubmitSuccessCallback = function(callback) {
273 278 this.globalSubmitSuccessCallback = callback;
274 279 };
275 280
276 281 // default handler for for submit for main comments
277 282 this.handleFormSubmit = function() {
278 283 var text = self.cm.getValue();
279 284 var status = self.getCommentStatus();
280 285 var commentType = self.getCommentType();
281 286 var resolvesCommentId = self.getResolvesId();
282 287
283 288 if (text === "" && !status) {
284 289 return;
285 290 }
286 291
287 292 var excludeCancelBtn = false;
288 293 var submitEvent = true;
289 294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
290 295 self.cm.setOption("readOnly", true);
291 296
292 297 var postData = {
293 298 'text': text,
294 299 'changeset_status': status,
295 300 'comment_type': commentType,
296 301 'csrf_token': CSRF_TOKEN
297 302 };
298 303 if (resolvesCommentId){
299 304 postData['resolves_comment_id'] = resolvesCommentId;
300 305 }
301 306
302 307 var submitSuccessCallback = function(o) {
303 if (status) {
308 // reload page if we change status for single commit.
309 if (status && self.commitId) {
304 310 location.reload(true);
305 311 } else {
306 312 $('#injected_page_comments').append(o.rendered_text);
307 313 self.resetCommentFormState();
308 314 timeagoActivate();
309 315
310 316 // mark visually which comment was resolved
311 317 if (resolvesCommentId) {
312 318 self.markCommentResolved(resolvesCommentId);
313 319 }
314 320 }
315 321
316 322 // run global callback on submit
317 323 self.globalSubmitSuccessCallback();
318 324
319 325 };
320 326 var submitFailCallback = function(){
321 327 self.resetCommentFormState(text);
322 328 };
323 329 self.submitAjaxPOST(
324 330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
325 331 };
326 332
327 333 this.previewSuccessCallback = function(o) {
328 334 $(self.previewBoxSelector).html(o);
329 335 $(self.previewBoxSelector).removeClass('unloaded');
330 336
331 337 // swap buttons, making preview active
332 338 $(self.previewButton).parent().addClass('active');
333 339 $(self.editButton).parent().removeClass('active');
334 340
335 341 // unlock buttons
336 342 self.setActionButtonsDisabled(false);
337 343 };
338 344
339 345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
340 346 excludeCancelBtn = excludeCancelBtn || false;
341 347 submitEvent = submitEvent || false;
342 348
343 349 $(this.editButton).prop('disabled', state);
344 350 $(this.previewButton).prop('disabled', state);
345 351
346 352 if (!excludeCancelBtn) {
347 353 $(this.cancelButton).prop('disabled', state);
348 354 }
349 355
350 356 var submitState = state;
351 357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
352 358 // if the value of commit review status is set, we allow
353 359 // submit button, but only on Main form, lineNo means inline
354 360 submitState = false
355 361 }
356 362 $(this.submitButton).prop('disabled', submitState);
357 363 if (submitEvent) {
358 364 $(this.submitButton).val(_gettext('Submitting...'));
359 365 } else {
360 366 $(this.submitButton).val(this.submitButtonText);
361 367 }
362 368
363 369 };
364 370
365 371 // lock preview/edit/submit buttons on load, but exclude cancel button
366 372 var excludeCancelBtn = true;
367 373 this.setActionButtonsDisabled(true, excludeCancelBtn);
368 374
369 375 // anonymous users don't have access to initialized CM instance
370 376 if (this.cm !== undefined){
371 377 this.cm.on('change', function(cMirror) {
372 378 if (cMirror.getValue() === "") {
373 379 self.setActionButtonsDisabled(true, excludeCancelBtn)
374 380 } else {
375 381 self.setActionButtonsDisabled(false, excludeCancelBtn)
376 382 }
377 383 });
378 384 }
379 385
380 386 $(this.editButton).on('click', function(e) {
381 387 e.preventDefault();
382 388
383 389 $(self.previewButton).parent().removeClass('active');
384 390 $(self.previewContainer).hide();
385 391
386 392 $(self.editButton).parent().addClass('active');
387 393 $(self.editContainer).show();
388 394
389 395 });
390 396
391 397 $(this.previewButton).on('click', function(e) {
392 398 e.preventDefault();
393 399 var text = self.cm.getValue();
394 400
395 401 if (text === "") {
396 402 return;
397 403 }
398 404
399 405 var postData = {
400 406 'text': text,
401 407 'renderer': templateContext.visual.default_renderer,
402 408 'csrf_token': CSRF_TOKEN
403 409 };
404 410
405 411 // lock ALL buttons on preview
406 412 self.setActionButtonsDisabled(true);
407 413
408 414 $(self.previewBoxSelector).addClass('unloaded');
409 415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
410 416
411 417 $(self.editContainer).hide();
412 418 $(self.previewContainer).show();
413 419
414 420 // by default we reset state of comment preserving the text
415 421 var previewFailCallback = function(){
416 422 self.resetCommentFormState(text)
417 423 };
418 424 self.submitAjaxPOST(
419 425 self.previewUrl, postData, self.previewSuccessCallback,
420 426 previewFailCallback);
421 427
422 428 $(self.previewButton).parent().addClass('active');
423 429 $(self.editButton).parent().removeClass('active');
424 430 });
425 431
426 432 $(this.submitForm).submit(function(e) {
427 433 e.preventDefault();
428 434 var allowedToSubmit = self.isAllowedToSubmit();
429 435 if (!allowedToSubmit){
430 436 return false;
431 437 }
432 438 self.handleFormSubmit();
433 439 });
434 440
435 441 }
436 442
437 443 return CommentForm;
438 444 });
439 445
440 446 /* comments controller */
441 447 var CommentsController = function() {
442 448 var mainComment = '#text';
443 449 var self = this;
444 450
445 451 this.cancelComment = function(node) {
446 452 var $node = $(node);
447 453 var $td = $node.closest('td');
448 454 $node.closest('.comment-inline-form').remove();
449 455 return false;
450 456 };
451 457
452 458 this.getLineNumber = function(node) {
453 459 var $node = $(node);
454 460 return $node.closest('td').attr('data-line-number');
455 461 };
456 462
457 463 this.scrollToComment = function(node, offset, outdated) {
458 464 var outdated = outdated || false;
459 465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
460 466
461 467 if (!node) {
462 468 node = $('.comment-selected');
463 469 if (!node.length) {
464 470 node = $('comment-current')
465 471 }
466 472 }
467 473 $comment = $(node).closest(klass);
468 474 $comments = $(klass);
469 475
470 476 $('.comment-selected').removeClass('comment-selected');
471 477
472 478 var nextIdx = $(klass).index($comment) + offset;
473 479 if (nextIdx >= $comments.length) {
474 480 nextIdx = 0;
475 481 }
476 482 var $next = $(klass).eq(nextIdx);
477 483 var $cb = $next.closest('.cb');
478 484 $cb.removeClass('cb-collapsed');
479 485
480 486 var $filediffCollapseState = $cb.closest('.filediff').prev();
481 487 $filediffCollapseState.prop('checked', false);
482 488 $next.addClass('comment-selected');
483 489 scrollToElement($next);
484 490 return false;
485 491 };
486 492
487 493 this.nextComment = function(node) {
488 494 return self.scrollToComment(node, 1);
489 495 };
490 496
491 497 this.prevComment = function(node) {
492 498 return self.scrollToComment(node, -1);
493 499 };
494 500
495 501 this.nextOutdatedComment = function(node) {
496 502 return self.scrollToComment(node, 1, true);
497 503 };
498 504
499 505 this.prevOutdatedComment = function(node) {
500 506 return self.scrollToComment(node, -1, true);
501 507 };
502 508
503 509 this.deleteComment = function(node) {
504 510 if (!confirm(_gettext('Delete this comment?'))) {
505 511 return false;
506 512 }
507 513 var $node = $(node);
508 514 var $td = $node.closest('td');
509 515 var $comment = $node.closest('.comment');
510 516 var comment_id = $comment.attr('data-comment-id');
511 517 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
512 518 var postData = {
513 519 '_method': 'delete',
514 520 'csrf_token': CSRF_TOKEN
515 521 };
516 522
517 523 $comment.addClass('comment-deleting');
518 524 $comment.hide('fast');
519 525
520 526 var success = function(response) {
521 527 $comment.remove();
522 528 return false;
523 529 };
524 530 var failure = function(data, textStatus, xhr) {
525 531 alert("error processing request: " + textStatus);
526 532 $comment.show('fast');
527 533 $comment.removeClass('comment-deleting');
528 534 return false;
529 535 };
530 536 ajaxPOST(url, postData, success, failure);
531 537 };
532 538
533 539 this.toggleWideMode = function (node) {
534 540 if ($('#content').hasClass('wrapper')) {
535 541 $('#content').removeClass("wrapper");
536 542 $('#content').addClass("wide-mode-wrapper");
537 543 $(node).addClass('btn-success');
538 544 } else {
539 545 $('#content').removeClass("wide-mode-wrapper");
540 546 $('#content').addClass("wrapper");
541 547 $(node).removeClass('btn-success');
542 548 }
543 549 return false;
544 550 };
545 551
546 552 this.toggleComments = function(node, show) {
547 553 var $filediff = $(node).closest('.filediff');
548 554 if (show === true) {
549 555 $filediff.removeClass('hide-comments');
550 556 } else if (show === false) {
551 557 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
552 558 $filediff.addClass('hide-comments');
553 559 } else {
554 560 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
555 561 $filediff.toggleClass('hide-comments');
556 562 }
557 563 return false;
558 564 };
559 565
560 566 this.toggleLineComments = function(node) {
561 567 self.toggleComments(node, true);
562 568 var $node = $(node);
563 569 $node.closest('tr').toggleClass('hide-line-comments');
564 570 };
565 571
566 572 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
567 573 var pullRequestId = templateContext.pull_request_data.pull_request_id;
568 574 var commitId = templateContext.commit_data.commit_id;
569 575
570 576 var commentForm = new CommentForm(
571 577 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
572 578 var cm = commentForm.getCmInstance();
573 579
574 580 if (resolvesCommentId){
575 581 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
576 582 }
577 583
578 584 setTimeout(function() {
579 585 // callbacks
580 586 if (cm !== undefined) {
581 587 commentForm.setPlaceholder(placeholderText);
582 588 if (commentForm.isInline()) {
583 589 cm.focus();
584 590 cm.refresh();
585 591 }
586 592 }
587 593 }, 10);
588 594
589 595 // trigger scrolldown to the resolve comment, since it might be away
590 596 // from the clicked
591 597 if (resolvesCommentId){
592 598 var actionNode = $(commentForm.resolvesActionId).offset();
593 599
594 600 setTimeout(function() {
595 601 if (actionNode) {
596 602 $('body, html').animate({scrollTop: actionNode.top}, 10);
597 603 }
598 604 }, 100);
599 605 }
600 606
601 607 return commentForm;
602 608 };
603 609
604 610 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
605 611
606 612 var tmpl = $('#cb-comment-general-form-template').html();
607 613 tmpl = tmpl.format(null, 'general');
608 614 var $form = $(tmpl);
609 615
610 616 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
611 617 var curForm = $formPlaceholder.find('form');
612 618 if (curForm){
613 619 curForm.remove();
614 620 }
615 621 $formPlaceholder.append($form);
616 622
617 623 var _form = $($form[0]);
618 624 var commentForm = this.createCommentForm(
619 625 _form, lineNo, placeholderText, true, resolvesCommentId);
620 626 commentForm.initStatusChangeSelector();
621 627
622 628 return commentForm;
623 629 };
624 630
625 631 this.createComment = function(node, resolutionComment) {
626 632 var resolvesCommentId = resolutionComment || null;
627 633 var $node = $(node);
628 634 var $td = $node.closest('td');
629 635 var $form = $td.find('.comment-inline-form');
630 636
631 637 if (!$form.length) {
632 638
633 639 var $filediff = $node.closest('.filediff');
634 640 $filediff.removeClass('hide-comments');
635 641 var f_path = $filediff.attr('data-f-path');
636 642 var lineno = self.getLineNumber(node);
637 643 // create a new HTML from template
638 644 var tmpl = $('#cb-comment-inline-form-template').html();
639 645 tmpl = tmpl.format(f_path, lineno);
640 646 $form = $(tmpl);
641 647
642 648 var $comments = $td.find('.inline-comments');
643 649 if (!$comments.length) {
644 650 $comments = $(
645 651 $('#cb-comments-inline-container-template').html());
646 652 $td.append($comments);
647 653 }
648 654
649 655 $td.find('.cb-comment-add-button').before($form);
650 656
651 657 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
652 658 var _form = $($form[0]).find('form');
653 659
654 660 var commentForm = this.createCommentForm(
655 661 _form, lineno, placeholderText, false, resolvesCommentId);
656 662
657 663 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
658 664 form: _form,
659 665 parent: $td[0],
660 666 lineno: lineno,
661 667 f_path: f_path}
662 668 );
663 669
664 670 // set a CUSTOM submit handler for inline comments.
665 671 commentForm.setHandleFormSubmit(function(o) {
666 672 var text = commentForm.cm.getValue();
667 673 var commentType = commentForm.getCommentType();
668 674 var resolvesCommentId = commentForm.getResolvesId();
669 675
670 676 if (text === "") {
671 677 return;
672 678 }
673 679
674 680 if (lineno === undefined) {
675 681 alert('missing line !');
676 682 return;
677 683 }
678 684 if (f_path === undefined) {
679 685 alert('missing file path !');
680 686 return;
681 687 }
682 688
683 689 var excludeCancelBtn = false;
684 690 var submitEvent = true;
685 691 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
686 692 commentForm.cm.setOption("readOnly", true);
687 693 var postData = {
688 694 'text': text,
689 695 'f_path': f_path,
690 696 'line': lineno,
691 697 'comment_type': commentType,
692 698 'csrf_token': CSRF_TOKEN
693 699 };
694 700 if (resolvesCommentId){
695 701 postData['resolves_comment_id'] = resolvesCommentId;
696 702 }
697 703
698 704 var submitSuccessCallback = function(json_data) {
699 705 $form.remove();
700 706 try {
701 707 var html = json_data.rendered_text;
702 708 var lineno = json_data.line_no;
703 709 var target_id = json_data.target_id;
704 710
705 711 $comments.find('.cb-comment-add-button').before(html);
706 712
707 713 //mark visually which comment was resolved
708 714 if (resolvesCommentId) {
709 715 commentForm.markCommentResolved(resolvesCommentId);
710 716 }
711 717
712 718 // run global callback on submit
713 719 commentForm.globalSubmitSuccessCallback();
714 720
715 721 } catch (e) {
716 722 console.error(e);
717 723 }
718 724
719 725 // re trigger the linkification of next/prev navigation
720 726 linkifyComments($('.inline-comment-injected'));
721 727 timeagoActivate();
722 728 commentForm.setActionButtonsDisabled(false);
723 729
724 730 };
725 731 var submitFailCallback = function(){
726 732 commentForm.resetCommentFormState(text)
727 733 };
728 734 commentForm.submitAjaxPOST(
729 735 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
730 736 });
731 737 }
732 738
733 739 $form.addClass('comment-inline-form-open');
734 740 };
735 741
736 742 this.createResolutionComment = function(commentId){
737 743 // hide the trigger text
738 744 $('#resolve-comment-{0}'.format(commentId)).hide();
739 745
740 746 var comment = $('#comment-'+commentId);
741 747 var commentData = comment.data();
742 748 if (commentData.commentInline) {
743 749 this.createComment(comment, commentId)
744 750 } else {
745 751 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
746 752 }
747 753
748 754 return false;
749 755 };
750 756
751 757 this.submitResolution = function(commentId){
752 758 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
753 759 var commentForm = form.get(0).CommentForm;
754 760
755 761 var cm = commentForm.getCmInstance();
756 762 var renderer = templateContext.visual.default_renderer;
757 763 if (renderer == 'rst'){
758 764 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
759 765 } else if (renderer == 'markdown') {
760 766 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
761 767 } else {
762 768 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
763 769 }
764 770
765 771 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
766 772 form.submit();
767 773 return false;
768 774 };
769 775
770 776 this.renderInlineComments = function(file_comments) {
771 777 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
772 778
773 779 for (var i = 0; i < file_comments.length; i++) {
774 780 var box = file_comments[i];
775 781
776 782 var target_id = $(box).attr('target_id');
777 783
778 784 // actually comments with line numbers
779 785 var comments = box.children;
780 786
781 787 for (var j = 0; j < comments.length; j++) {
782 788 var data = {
783 789 'rendered_text': comments[j].outerHTML,
784 790 'line_no': $(comments[j]).attr('line'),
785 791 'target_id': target_id
786 792 };
787 793 }
788 794 }
789 795
790 796 // since order of injection is random, we're now re-iterating
791 797 // from correct order and filling in links
792 798 linkifyComments($('.inline-comment-injected'));
793 799 firefoxAnchorFix();
794 800 };
795 801
796 802 };
@@ -1,408 +1,385 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 10 % if inline:
11 11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 12 % else:
13 13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 14 % endif
15 15
16 16
17 17 <div class="comment
18 18 ${'comment-inline' if inline else 'comment-general'}
19 19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 20 id="comment-${comment.comment_id}"
21 21 line="${comment.line_no}"
22 22 data-comment-id="${comment.comment_id}"
23 23 data-comment-type="${comment.comment_type}"
24 24 data-comment-inline=${h.json.dumps(inline)}
25 25 style="${'display: none;' if outdated_at_ver else ''}">
26 26
27 27 <div class="meta">
28 28 <div class="comment-type-label">
29 29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 30 % if comment.comment_type == 'todo':
31 31 % if comment.resolved:
32 32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 34 </div>
35 35 % else:
36 36 <div class="resolved tooltip" style="display: none">
37 37 <span>${comment.comment_type}</span>
38 38 </div>
39 39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 40 ${comment.comment_type}
41 41 </div>
42 42 % endif
43 43 % else:
44 44 % if comment.resolved_comment:
45 45 fix
46 46 % else:
47 47 ${comment.comment_type or 'note'}
48 48 % endif
49 49 % endif
50 50 </div>
51 51 </div>
52 52
53 53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 54 ${base.gravatar_with_user(comment.author.email, 16)}
55 55 </div>
56 56 <div class="date">
57 57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 58 </div>
59 59 % if inline:
60 60 <span></span>
61 61 % else:
62 62 <div class="status-change">
63 63 % if comment.pull_request:
64 64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 65 % if comment.status_change:
66 66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 67 % else:
68 68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 69 % endif
70 70 </a>
71 71 % else:
72 72 % if comment.status_change:
73 73 ${_('Status change on commit')}:
74 74 % endif
75 75 % endif
76 76 </div>
77 77 % endif
78 78
79 79 % if comment.status_change:
80 80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 82 ${comment.status_change[0].status_lbl}
83 83 </div>
84 84 % endif
85 85
86 86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
87 87
88 88 <div class="comment-links-block">
89 89
90 90 % if inline:
91 91 <div class="pr-version-inline">
92 92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
93 93 % if outdated_at_ver:
94 94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
95 95 outdated ${'v{}'.format(pr_index_ver)} |
96 96 </code>
97 97 % elif pr_index_ver:
98 98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
99 99 ${'v{}'.format(pr_index_ver)} |
100 100 </code>
101 101 % endif
102 102 </a>
103 103 </div>
104 104 % else:
105 105 % if comment.pull_request_version_id and pr_index_ver:
106 106 |
107 107 <div class="pr-version">
108 108 % if comment.outdated:
109 109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
110 110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
111 111 </a>
112 112 % else:
113 113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
114 114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
115 115 <code class="pr-version-num">
116 116 ${'v{}'.format(pr_index_ver)}
117 117 </code>
118 118 </a>
119 119 </div>
120 120 % endif
121 121 </div>
122 122 % endif
123 123 % endif
124 124
125 125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
126 126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
127 127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
128 128 ## permissions to delete
129 129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
130 130 ## TODO: dan: add edit comment here
131 131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
132 132 %else:
133 133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
134 134 %endif
135 135 %else:
136 136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
137 137 %endif
138 138
139 139 %if not outdated_at_ver:
140 140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
141 141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
142 142 %endif
143 143
144 144 </div>
145 145 </div>
146 146 <div class="text">
147 147 ${comment.render(mentions=True)|n}
148 148 </div>
149 149
150 150 </div>
151 151 </%def>
152 152
153 153 ## generate main comments
154 154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
155 155 <div id="comments">
156 156 %for comment in comments:
157 157 <div id="comment-tr-${comment.comment_id}">
158 158 ## only render comments that are not from pull request, or from
159 159 ## pull request and a status change
160 160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
161 161 ${comment_block(comment)}
162 162 %endif
163 163 </div>
164 164 %endfor
165 165 ## to anchor ajax comments
166 166 <div id="injected_page_comments"></div>
167 167 </div>
168 168 </%def>
169 169
170 170
171 171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
172 172
173 ## merge status, and merge action
174 %if is_pull_request:
175 <div class="pull-request-merge">
176 %if c.allowed_to_merge:
177 <div class="pull-request-wrap">
178 <div class="pull-right">
179 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
180 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
182 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
183 ${h.end_form()}
184 </div>
185 </div>
186 %else:
187 <div class="pull-request-wrap">
188 <div class="pull-right">
189 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
190 </div>
191 </div>
192 %endif
193 </div>
194 %endif
195
196 173 <div class="comments">
197 174 <%
198 175 if is_pull_request:
199 176 placeholder = _('Leave a comment on this Pull Request.')
200 177 elif is_compare:
201 178 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
202 179 else:
203 180 placeholder = _('Leave a comment on this Commit.')
204 181 %>
205 182
206 183 % if c.rhodecode_user.username != h.DEFAULT_USER:
207 184 <div class="js-template" id="cb-comment-general-form-template">
208 185 ## template generated for injection
209 186 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
210 187 </div>
211 188
212 189 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
213 190 ## inject form here
214 191 </div>
215 192 <script type="text/javascript">
216 193 var lineNo = 'general';
217 194 var resolvesCommentId = null;
218 195 var generalCommentForm = Rhodecode.comments.createGeneralComment(
219 196 lineNo, "${placeholder}", resolvesCommentId);
220 197
221 198 // set custom success callback on rangeCommit
222 199 % if is_compare:
223 200 generalCommentForm.setHandleFormSubmit(function(o) {
224 201 var self = generalCommentForm;
225 202
226 203 var text = self.cm.getValue();
227 204 var status = self.getCommentStatus();
228 205 var commentType = self.getCommentType();
229 206
230 207 if (text === "" && !status) {
231 208 return;
232 209 }
233 210
234 211 // we can pick which commits we want to make the comment by
235 212 // selecting them via click on preview pane, this will alter the hidden inputs
236 213 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
237 214
238 215 var commitIds = [];
239 216 $('#changeset_compare_view_content .compare_select').each(function(el) {
240 217 var commitId = this.id.replace('row-', '');
241 218 if ($(this).hasClass('hl') || !cherryPicked) {
242 219 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
243 220 commitIds.push(commitId);
244 221 } else {
245 222 $("input[data-commit-id='{0}']".format(commitId)).val('')
246 223 }
247 224 });
248 225
249 226 self.setActionButtonsDisabled(true);
250 227 self.cm.setOption("readOnly", true);
251 228 var postData = {
252 229 'text': text,
253 230 'changeset_status': status,
254 231 'comment_type': commentType,
255 232 'commit_ids': commitIds,
256 233 'csrf_token': CSRF_TOKEN
257 234 };
258 235
259 236 var submitSuccessCallback = function(o) {
260 237 location.reload(true);
261 238 };
262 239 var submitFailCallback = function(){
263 240 self.resetCommentFormState(text)
264 241 };
265 242 self.submitAjaxPOST(
266 243 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
267 244 });
268 245 % endif
269 246
270 247
271 248 </script>
272 249 % else:
273 250 ## form state when not logged in
274 251 <div class="comment-form ac">
275 252
276 253 <div class="comment-area">
277 254 <div class="comment-area-header">
278 255 <ul class="nav-links clearfix">
279 256 <li class="active">
280 257 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
281 258 </li>
282 259 <li class="">
283 260 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
284 261 </li>
285 262 </ul>
286 263 </div>
287 264
288 265 <div class="comment-area-write" style="display: block;">
289 266 <div id="edit-container">
290 267 <div style="padding: 40px 0">
291 268 ${_('You need to be logged in to leave comments.')}
292 269 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
293 270 </div>
294 271 </div>
295 272 <div id="preview-container" class="clearfix" style="display: none;">
296 273 <div id="preview-box" class="preview-box"></div>
297 274 </div>
298 275 </div>
299 276
300 277 <div class="comment-area-footer">
301 278 <div class="toolbar">
302 279 <div class="toolbar-text">
303 280 </div>
304 281 </div>
305 282 </div>
306 283 </div>
307 284
308 285 <div class="comment-footer">
309 286 </div>
310 287
311 288 </div>
312 289 % endif
313 290
314 291 <script type="text/javascript">
315 292 bindToggleButtons();
316 293 </script>
317 294 </div>
318 295 </%def>
319 296
320 297
321 298 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
322 299 ## comment injected based on assumption that user is logged in
323 300
324 301 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
325 302
326 303 <div class="comment-area">
327 304 <div class="comment-area-header">
328 305 <ul class="nav-links clearfix">
329 306 <li class="active">
330 307 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
331 308 </li>
332 309 <li class="">
333 310 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
334 311 </li>
335 312 <li class="pull-right">
336 313 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
337 314 % for val in c.visual.comment_types:
338 315 <option value="${val}">${val.upper()}</option>
339 316 % endfor
340 317 </select>
341 318 </li>
342 319 </ul>
343 320 </div>
344 321
345 322 <div class="comment-area-write" style="display: block;">
346 323 <div id="edit-container_${lineno_id}">
347 324 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
348 325 </div>
349 326 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
350 327 <div id="preview-box_${lineno_id}" class="preview-box"></div>
351 328 </div>
352 329 </div>
353 330
354 331 <div class="comment-area-footer">
355 332 <div class="toolbar">
356 333 <div class="toolbar-text">
357 334 ${(_('Comments parsed using %s syntax with %s support.') % (
358 335 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
359 336 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
360 337 )
361 338 )|n}
362 339 </div>
363 340 </div>
364 341 </div>
365 342 </div>
366 343
367 344 <div class="comment-footer">
368 345
369 346 % if review_statuses:
370 347 <div class="status_box">
371 348 <select id="change_status_${lineno_id}" name="changeset_status">
372 349 <option></option> ## Placeholder
373 350 % for status, lbl in review_statuses:
374 351 <option value="${status}" data-status="${status}">${lbl}</option>
375 352 %if is_pull_request and change_status and status in ('approved', 'rejected'):
376 353 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
377 354 %endif
378 355 % endfor
379 356 </select>
380 357 </div>
381 358 % endif
382 359
383 360 ## inject extra inputs into the form
384 361 % if form_extras and isinstance(form_extras, (list, tuple)):
385 362 <div id="comment_form_extras">
386 363 % for form_ex_el in form_extras:
387 364 ${form_ex_el|n}
388 365 % endfor
389 366 </div>
390 367 % endif
391 368
392 369 <div class="action-buttons">
393 370 ## inline for has a file, and line-number together with cancel hide button.
394 371 % if form_type == 'inline':
395 372 <input type="hidden" name="f_path" value="{0}">
396 373 <input type="hidden" name="line" value="${lineno_id}">
397 374 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
398 375 ${_('Cancel')}
399 376 </button>
400 377 % endif
401 378 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
402 379
403 380 </div>
404 381 </div>
405 382
406 383 </form>
407 384
408 385 </%def> No newline at end of file
@@ -1,692 +1,713 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 <span id="pr-title">
12 12 ${c.pull_request.title}
13 13 %if c.pull_request.is_closed():
14 14 (${_('Closed')})
15 15 %endif
16 16 </span>
17 17 <div id="pr-title-edit" class="input" style="display: none;">
18 18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 19 </div>
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='showpullrequest')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31
32 32 <script type="text/javascript">
33 33 // TODO: marcink switch this to pyroutes
34 34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 </script>
37 37 <div class="box">
38 38
39 39 <div class="title">
40 40 ${self.repo_page_title(c.rhodecode_db_repo)}
41 41 </div>
42 42
43 43 ${self.breadcrumbs()}
44 44
45 45 <div class="box pr-summary">
46 46
47 47 <div class="summary-details block-left">
48 48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 49 <div class="pr-details-title">
50 50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 51 %if c.allowed_to_update:
52 52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 53 % if c.allowed_to_delete:
54 54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 57 ${h.end_form()}
58 58 % else:
59 59 ${_('Delete')}
60 60 % endif
61 61 </div>
62 62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 64 %endif
65 65 </div>
66 66
67 67 <div id="summary" class="fields pr-details-content">
68 68 <div class="field">
69 69 <div class="label-summary">
70 70 <label>${_('Origin')}:</label>
71 71 </div>
72 72 <div class="input">
73 73 <div class="pr-origininfo">
74 74 ## branch link is only valid if it is a branch
75 75 <span class="tag">
76 76 %if c.pull_request.source_ref_parts.type == 'branch':
77 77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 78 %else:
79 79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 80 %endif
81 81 </span>
82 82 <span class="clone-url">
83 83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 84 </span>
85 85 </div>
86 86 <div class="pr-pullinfo">
87 87 %if h.is_hg(c.pull_request.source_repo):
88 88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
89 89 %elif h.is_git(c.pull_request.source_repo):
90 90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
91 91 %endif
92 92 </div>
93 93 </div>
94 94 </div>
95 95 <div class="field">
96 96 <div class="label-summary">
97 97 <label>${_('Target')}:</label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-targetinfo">
101 101 ## branch link is only valid if it is a branch
102 102 <span class="tag">
103 103 %if c.pull_request.target_ref_parts.type == 'branch':
104 104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
105 105 %else:
106 106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
107 107 %endif
108 108 </span>
109 109 <span class="clone-url">
110 110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
111 111 </span>
112 112 </div>
113 113 </div>
114 114 </div>
115 115
116 116 ## Link to the shadow repository.
117 117 <div class="field">
118 118 <div class="label-summary">
119 119 <label>${_('Merge')}:</label>
120 120 </div>
121 121 <div class="input">
122 122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
123 123 <div class="pr-mergeinfo">
124 124 %if h.is_hg(c.pull_request.target_repo):
125 125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 126 %elif h.is_git(c.pull_request.target_repo):
127 127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
128 128 %endif
129 129 </div>
130 130 % else:
131 131 <div class="">
132 132 ${_('Shadow repository data not available')}.
133 133 </div>
134 134 % endif
135 135 </div>
136 136 </div>
137 137
138 138 <div class="field">
139 139 <div class="label-summary">
140 140 <label>${_('Review')}:</label>
141 141 </div>
142 142 <div class="input">
143 143 %if c.pull_request_review_status:
144 144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
145 145 <span class="changeset-status-lbl tooltip">
146 146 %if c.pull_request.is_closed():
147 147 ${_('Closed')},
148 148 %endif
149 149 ${h.commit_status_lbl(c.pull_request_review_status)}
150 150 </span>
151 151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
152 152 %endif
153 153 </div>
154 154 </div>
155 155 <div class="field">
156 156 <div class="pr-description-label label-summary">
157 157 <label>${_('Description')}:</label>
158 158 </div>
159 159 <div id="pr-desc" class="input">
160 160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
161 161 </div>
162 162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
163 163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
164 164 </div>
165 165 </div>
166 166
167 167 <div class="field">
168 168 <div class="label-summary">
169 169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
170 170 </div>
171 171
172 172 <div class="pr-versions">
173 173 % if c.show_version_changes:
174 174 <table>
175 175 ## CURRENTLY SELECT PR VERSION
176 176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
177 177 <td>
178 178 % if c.at_version_num is None:
179 179 <i class="icon-ok link"></i>
180 180 % else:
181 181 <i class="icon-comment"></i>
182 182 <code>
183 183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
184 184 </code>
185 185 % endif
186 186 </td>
187 187 <td>
188 188 <code>
189 189 % if c.versions:
190 190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
191 191 % else:
192 192 ${_('initial')}
193 193 % endif
194 194 </code>
195 195 </td>
196 196 <td>
197 197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
198 198 </td>
199 199 <td>
200 200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
201 201 </td>
202 202 <td align="right">
203 203 % if c.versions and c.at_version_num in [None, 'latest']:
204 204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
205 205 % endif
206 206 </td>
207 207 </tr>
208 208
209 209 ## SHOW ALL VERSIONS OF PR
210 210 <% ver_pr = None %>
211 211
212 212 % for data in reversed(list(enumerate(c.versions, 1))):
213 213 <% ver_pos = data[0] %>
214 214 <% ver = data[1] %>
215 215 <% ver_pr = ver.pull_request_version_id %>
216 216
217 217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
218 218 <td>
219 219 % if c.at_version_num == ver_pr:
220 220 <i class="icon-ok link"></i>
221 221 % else:
222 222 <i class="icon-comment"></i>
223 223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
225 225 </code>
226 226 % endif
227 227 </td>
228 228 <td>
229 229 <code>
230 230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
231 231 </code>
232 232 </td>
233 233 <td>
234 234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
235 235 </td>
236 236 <td>
237 237 ${_('created')} ${h.age_component(ver.updated_on)}
238 238 </td>
239 239 <td align="right">
240 240 % if c.at_version_num == ver_pr:
241 241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
242 242 % endif
243 243 </td>
244 244 </tr>
245 245 % endfor
246 246
247 247 ## show comment/inline comments summary
248 248 <tr>
249 249 <td>
250 250 </td>
251 251
252 252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
253 253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
254 254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
255 255
256 256
257 257 % if c.at_version:
258 258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
259 259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
260 260 ${_('Comments at this version')}:
261 261 % else:
262 262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
263 263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
264 264 ${_('Comments for this pull request')}:
265 265 % endif
266 266
267 267 %if general_comm_count_ver:
268 268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
269 269 %else:
270 270 ${_("%d General ") % general_comm_count_ver}
271 271 %endif
272 272
273 273 %if inline_comm_count_ver:
274 274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
275 275 %else:
276 276 , ${_("%d Inline") % inline_comm_count_ver}
277 277 %endif
278 278
279 279 %if outdated_comm_count_ver:
280 280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
281 281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
282 282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
283 283 %else:
284 284 , ${_("%d Outdated") % outdated_comm_count_ver}
285 285 %endif
286 286 </td>
287 287 </tr>
288 288
289 289 <tr>
290 290 <td></td>
291 291 <td colspan="4">
292 292 % if c.at_version:
293 293 <pre>
294 294 Changed commits:
295 295 * added: ${len(c.changes.added)}
296 296 * removed: ${len(c.changes.removed)}
297 297
298 298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
299 299 No file changes found
300 300 % else:
301 301 Changed files:
302 302 %for file_name in c.file_changes.added:
303 303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
304 304 %endfor
305 305 %for file_name in c.file_changes.modified:
306 306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
307 307 %endfor
308 308 %for file_name in c.file_changes.removed:
309 309 * R ${file_name}
310 310 %endfor
311 311 % endif
312 312 </pre>
313 313 % endif
314 314 </td>
315 315 </tr>
316 316 </table>
317 317 % else:
318 318 ${_('Pull request versions not available')}.
319 319 % endif
320 320 </div>
321 321 </div>
322 322
323 323 <div id="pr-save" class="field" style="display: none;">
324 324 <div class="label-summary"></div>
325 325 <div class="input">
326 326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
327 327 </div>
328 328 </div>
329 329 </div>
330 330 </div>
331 331 <div>
332 332 ## AUTHOR
333 333 <div class="reviewers-title block-right">
334 334 <div class="pr-details-title">
335 335 ${_('Author')}
336 336 </div>
337 337 </div>
338 338 <div class="block-right pr-details-content reviewers">
339 339 <ul class="group_members">
340 340 <li>
341 341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
342 342 </li>
343 343 </ul>
344 344 </div>
345 345 ## REVIEWERS
346 346 <div class="reviewers-title block-right">
347 347 <div class="pr-details-title">
348 348 ${_('Pull request reviewers')}
349 349 %if c.allowed_to_update:
350 350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
351 351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
352 352 %endif
353 353 </div>
354 354 </div>
355 355 <div id="reviewers" class="block-right pr-details-content reviewers">
356 356 ## members goes here !
357 357 <input type="hidden" name="__start__" value="review_members:sequence">
358 358 <ul id="review_members" class="group_members">
359 359 %for member,reasons,status in c.pull_request_reviewers:
360 360 <li id="reviewer_${member.user_id}">
361 361 <div class="reviewers_member">
362 362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
363 363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
364 364 </div>
365 365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
366 366 ${self.gravatar_with_user(member.email, 16)}
367 367 </div>
368 368 <input type="hidden" name="__start__" value="reviewer:mapping">
369 369 <input type="hidden" name="__start__" value="reasons:sequence">
370 370 %for reason in reasons:
371 371 <div class="reviewer_reason">- ${reason}</div>
372 372 <input type="hidden" name="reason" value="${reason}">
373 373
374 374 %endfor
375 375 <input type="hidden" name="__end__" value="reasons:sequence">
376 376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
377 377 <input type="hidden" name="__end__" value="reviewer:mapping">
378 378 %if c.allowed_to_update:
379 379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
380 380 <i class="icon-remove-sign" ></i>
381 381 </div>
382 382 %endif
383 383 </div>
384 384 </li>
385 385 %endfor
386 386 </ul>
387 387 <input type="hidden" name="__end__" value="review_members:sequence">
388 388 %if not c.pull_request.is_closed():
389 389 <div id="add_reviewer_input" class='ac' style="display: none;">
390 390 %if c.allowed_to_update:
391 391 <div class="reviewer_ac">
392 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
393 393 <div id="reviewers_container"></div>
394 394 </div>
395 395 <div>
396 396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
397 397 </div>
398 398 %endif
399 399 </div>
400 400 %endif
401 401 </div>
402 402 </div>
403 403 </div>
404 404 <div class="box">
405 405 ##DIFF
406 406 <div class="table" >
407 407 <div id="changeset_compare_view_content">
408 408 ##CS
409 409 % if c.missing_requirements:
410 410 <div class="box">
411 411 <div class="alert alert-warning">
412 412 <div>
413 413 <strong>${_('Missing requirements:')}</strong>
414 414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 415 </div>
416 416 </div>
417 417 </div>
418 418 % elif c.missing_commits:
419 419 <div class="box">
420 420 <div class="alert alert-warning">
421 421 <div>
422 422 <strong>${_('Missing commits')}:</strong>
423 423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 425 </div>
426 426 </div>
427 427 </div>
428 428 % endif
429 429 <div class="compare_view_commits_title">
430 430
431 431 <div class="pull-left">
432 432 <div class="btn-group">
433 433 <a
434 434 class="btn"
435 435 href="#"
436 436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
437 437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
438 438 </a>
439 439 <a
440 440 class="btn"
441 441 href="#"
442 442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
443 443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
444 444 </a>
445 445 </div>
446 446 </div>
447 447
448 448 <div class="pull-right">
449 449 % if c.allowed_to_update and not c.pull_request.is_closed():
450 450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
451 451 % else:
452 452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
453 453 % endif
454 454
455 455 </div>
456 456
457 457 </div>
458 458
459 459 % if not c.missing_commits:
460 460 <%include file="/compare/compare_commits.mako" />
461 461 <div class="cs_files">
462 462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
463 463 ${cbdiffs.render_diffset_menu()}
464 464 ${cbdiffs.render_diffset(
465 465 c.diffset, use_comments=True,
466 466 collapse_when_files_over=30,
467 467 disable_new_comments=not c.allowed_to_comment,
468 468 deleted_files_comments=c.deleted_files_comments)}
469 469 </div>
470 470 % else:
471 471 ## skipping commits we need to clear the view for missing commits
472 472 <div style="clear:both;"></div>
473 473 % endif
474 474
475 475 </div>
476 476 </div>
477 477
478 478 ## template for inline comment form
479 479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
480 480
481 481 ## render general comments
482 482
483 483 <div id="comment-tr-show">
484 484 <div class="comment">
485 % if general_outdated_comm_count_ver:
485 486 <div class="meta">
486 % if general_outdated_comm_count_ver:
487 % if general_outdated_comm_count_ver == 1:
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 % else:
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
493 % endif
487 % if general_outdated_comm_count_ver == 1:
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 % else:
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
494 493 % endif
495 494 </div>
495 % endif
496 496 </div>
497 497 </div>
498 498
499 499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
500 500
501 501 % if not c.pull_request.is_closed():
502 ## merge status, and merge action
503 <div class="pull-request-merge">
504 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
505 </div>
506
502 507 ## main comment form and it status
503 508 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
504 509 pull_request_id=c.pull_request.pull_request_id),
505 510 c.pull_request_review_status,
506 511 is_pull_request=True, change_status=c.allowed_to_change_status)}
507 512 %endif
508 513
509 514 <script type="text/javascript">
510 515 if (location.hash) {
511 516 var result = splitDelimitedHash(location.hash);
512 517 var line = $('html').find(result.loc);
513 518 // show hidden comments if we use location.hash
514 519 if (line.hasClass('comment-general')) {
515 520 $(line).show();
516 521 } else if (line.hasClass('comment-inline')) {
517 522 $(line).show();
518 523 var $cb = $(line).closest('.cb');
519 524 $cb.removeClass('cb-collapsed')
520 525 }
521 526 if (line.length > 0){
522 527 offsetScroll(line, 70);
523 528 }
524 529 }
525 530
526 531 $(function(){
527 532 ReviewerAutoComplete('user');
528 533 // custom code mirror
529 534 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
530 535
531 536 var PRDetails = {
532 537 editButton: $('#open_edit_pullrequest'),
533 538 closeButton: $('#close_edit_pullrequest'),
534 539 deleteButton: $('#delete_pullrequest'),
535 540 viewFields: $('#pr-desc, #pr-title'),
536 541 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
537 542
538 543 init: function() {
539 544 var that = this;
540 545 this.editButton.on('click', function(e) { that.edit(); });
541 546 this.closeButton.on('click', function(e) { that.view(); });
542 547 },
543 548
544 549 edit: function(event) {
545 550 this.viewFields.hide();
546 551 this.editButton.hide();
547 552 this.deleteButton.hide();
548 553 this.closeButton.show();
549 554 this.editFields.show();
550 555 codeMirrorInstance.refresh();
551 556 },
552 557
553 558 view: function(event) {
554 559 this.editButton.show();
555 560 this.deleteButton.show();
556 561 this.editFields.hide();
557 562 this.closeButton.hide();
558 563 this.viewFields.show();
559 564 }
560 565 };
561 566
562 567 var ReviewersPanel = {
563 568 editButton: $('#open_edit_reviewers'),
564 569 closeButton: $('#close_edit_reviewers'),
565 570 addButton: $('#add_reviewer_input'),
566 571 removeButtons: $('.reviewer_member_remove'),
567 572
568 573 init: function() {
569 574 var that = this;
570 575 this.editButton.on('click', function(e) { that.edit(); });
571 576 this.closeButton.on('click', function(e) { that.close(); });
572 577 },
573 578
574 579 edit: function(event) {
575 580 this.editButton.hide();
576 581 this.closeButton.show();
577 582 this.addButton.show();
578 583 this.removeButtons.css('visibility', 'visible');
579 584 },
580 585
581 586 close: function(event) {
582 587 this.editButton.show();
583 588 this.closeButton.hide();
584 589 this.addButton.hide();
585 590 this.removeButtons.css('visibility', 'hidden');
586 591 }
587 592 };
588 593
589 594 PRDetails.init();
590 595 ReviewersPanel.init();
591 596
592 597 showOutdated = function(self){
593 598 $('.comment-inline.comment-outdated').show();
594 599 $('.filediff-outdated').show();
595 600 $('.showOutdatedComments').hide();
596 601 $('.hideOutdatedComments').show();
597 602 };
598 603
599 604 hideOutdated = function(self){
600 605 $('.comment-inline.comment-outdated').hide();
601 606 $('.filediff-outdated').hide();
602 607 $('.hideOutdatedComments').hide();
603 608 $('.showOutdatedComments').show();
604 609 };
605 610
611 refreshMergeChecks = function(){
612 var loadUrl = "${h.url.current(merge_checks=1)}";
613 $('.pull-request-merge').css('opacity', 0.3);
614 $('.pull-request-merge').load(
615 loadUrl,function() {
616 $('.pull-request-merge').css('opacity', 1);
617 }
618 );
619 };
620
606 621 $('#show-outdated-comments').on('click', function(e){
607 622 var button = $(this);
608 623 var outdated = $('.comment-outdated');
609 624
610 625 if (button.html() === "(Show)") {
611 626 button.html("(Hide)");
612 627 outdated.show();
613 628 } else {
614 629 button.html("(Show)");
615 630 outdated.hide();
616 631 }
617 632 });
618 633
619 634 $('.show-inline-comments').on('change', function(e){
620 635 var show = 'none';
621 636 var target = e.currentTarget;
622 637 if(target.checked){
623 638 show = ''
624 639 }
625 640 var boxid = $(target).attr('id_for');
626 641 var comments = $('#{0} .inline-comments'.format(boxid));
627 642 var fn_display = function(idx){
628 643 $(this).css('display', show);
629 644 };
630 645 $(comments).each(fn_display);
631 646 var btns = $('#{0} .inline-comments-button'.format(boxid));
632 647 $(btns).each(fn_display);
633 648 });
634 649
635 650 $('#merge_pull_request_form').submit(function() {
636 651 if (!$('#merge_pull_request').attr('disabled')) {
637 652 $('#merge_pull_request').attr('disabled', 'disabled');
638 653 }
639 654 return true;
640 655 });
641 656
642 657 $('#edit_pull_request').on('click', function(e){
643 658 var title = $('#pr-title-input').val();
644 659 var description = codeMirrorInstance.getValue();
645 660 editPullRequest(
646 661 "${c.repo_name}", "${c.pull_request.pull_request_id}",
647 662 title, description);
648 663 });
649 664
650 665 $('#update_pull_request').on('click', function(e){
651 666 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
652 667 });
653 668
654 669 $('#update_commits').on('click', function(e){
655 670 var isDisabled = !$(e.currentTarget).attr('disabled');
656 671 $(e.currentTarget).text(_gettext('Updating...'));
657 672 $(e.currentTarget).attr('disabled', 'disabled');
658 673 if(isDisabled){
659 674 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
660 675 }
661 676
662 677 });
663 678 // fixing issue with caches on firefox
664 679 $('#update_commits').removeAttr("disabled");
665 680
666 681 $('#close_pull_request').on('click', function(e){
667 682 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
668 683 });
669 684
670 685 $('.show-inline-comments').on('click', function(e){
671 686 var boxid = $(this).attr('data-comment-id');
672 687 var button = $(this);
673 688
674 689 if(button.hasClass("comments-visible")) {
675 690 $('#{0} .inline-comments'.format(boxid)).each(function(index){
676 691 $(this).hide();
677 692 });
678 693 button.removeClass("comments-visible");
679 694 } else {
680 695 $('#{0} .inline-comments'.format(boxid)).each(function(index){
681 696 $(this).show();
682 697 });
683 698 button.addClass("comments-visible");
684 699 }
685 700 });
701
702 // register submit callback on commentForm form to track TODOs
703 window.commentFormGlobalSubmitSuccessCallback = function(){
704 refreshMergeChecks();
705 };
706
686 707 })
687 708 </script>
688 709
689 710 </div>
690 711 </div>
691 712
692 713 </%def>
General Comments 0
You need to be logged in to leave comments. Login now