##// END OF EJS Templates
auth-tokens: expose all roles with explanation to help users understand it better.
marcink -
r4430:d880ce51 default
parent child Browse files
Show More
@@ -1,2041 +1,2041 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27 import base64
28 28
29 29 import os
30 30 import random
31 31 import hashlib
32 32 import StringIO
33 33 import textwrap
34 34 import urllib
35 35 import math
36 36 import logging
37 37 import re
38 38 import time
39 39 import string
40 40 import hashlib
41 41 from collections import OrderedDict
42 42
43 43 import pygments
44 44 import itertools
45 45 import fnmatch
46 46 import bleach
47 47
48 48 from pyramid import compat
49 49 from datetime import datetime
50 50 from functools import partial
51 51 from pygments.formatters.html import HtmlFormatter
52 52 from pygments.lexers import (
53 53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 54
55 55 from pyramid.threadlocal import get_current_request
56 56
57 57 from webhelpers2.html import literal, HTML, escape
58 58 from webhelpers2.html._autolink import _auto_link_urls
59 59 from webhelpers2.html.tools import (
60 60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61 61
62 62 from webhelpers2.text import (
63 63 chop_at, collapse, convert_accented_entities,
64 64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 65 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 66 from webhelpers2.date import time_ago_in_words
67 67
68 68 from webhelpers2.html.tags import (
69 69 _input, NotGiven, _make_safe_id_component as safeid,
70 70 form as insecure_form,
71 71 auto_discovery_link, checkbox, end_form, file,
72 72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 73 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 74 ul, radio, Options)
75 75
76 76 from webhelpers2.number import format_byte_size
77 77
78 78 from rhodecode.lib.action_parser import action_parser
79 79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 80 from rhodecode.lib.ext_json import json
81 81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 82 from rhodecode.lib.utils2 import (
83 83 str2bool, safe_unicode, safe_str,
84 84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 89 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 90 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 91 from rhodecode.model.changeset_status import ChangesetStatusModel
92 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.db import Permission, User, Repository, UserApiKeys
93 93 from rhodecode.model.repo_group import RepoGroupModel
94 94 from rhodecode.model.settings import IssueTrackerSettingsModel
95 95
96 96
97 97 log = logging.getLogger(__name__)
98 98
99 99
100 100 DEFAULT_USER = User.DEFAULT_USER
101 101 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
102 102
103 103
104 104 def asset(path, ver=None, **kwargs):
105 105 """
106 106 Helper to generate a static asset file path for rhodecode assets
107 107
108 108 eg. h.asset('images/image.png', ver='3923')
109 109
110 110 :param path: path of asset
111 111 :param ver: optional version query param to append as ?ver=
112 112 """
113 113 request = get_current_request()
114 114 query = {}
115 115 query.update(kwargs)
116 116 if ver:
117 117 query = {'ver': ver}
118 118 return request.static_path(
119 119 'rhodecode:public/{}'.format(path), _query=query)
120 120
121 121
122 122 default_html_escape_table = {
123 123 ord('&'): u'&amp;',
124 124 ord('<'): u'&lt;',
125 125 ord('>'): u'&gt;',
126 126 ord('"'): u'&quot;',
127 127 ord("'"): u'&#39;',
128 128 }
129 129
130 130
131 131 def html_escape(text, html_escape_table=default_html_escape_table):
132 132 """Produce entities within text."""
133 133 return text.translate(html_escape_table)
134 134
135 135
136 136 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
137 137 """
138 138 Truncate string ``s`` at the first occurrence of ``sub``.
139 139
140 140 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
141 141 """
142 142 suffix_if_chopped = suffix_if_chopped or ''
143 143 pos = s.find(sub)
144 144 if pos == -1:
145 145 return s
146 146
147 147 if inclusive:
148 148 pos += len(sub)
149 149
150 150 chopped = s[:pos]
151 151 left = s[pos:].strip()
152 152
153 153 if left and suffix_if_chopped:
154 154 chopped += suffix_if_chopped
155 155
156 156 return chopped
157 157
158 158
159 159 def shorter(text, size=20, prefix=False):
160 160 postfix = '...'
161 161 if len(text) > size:
162 162 if prefix:
163 163 # shorten in front
164 164 return postfix + text[-(size - len(postfix)):]
165 165 else:
166 166 return text[:size - len(postfix)] + postfix
167 167 return text
168 168
169 169
170 170 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 171 """
172 172 Reset button
173 173 """
174 174 return _input(type, name, value, id, attrs)
175 175
176 176
177 177 def select(name, selected_values, options, id=NotGiven, **attrs):
178 178
179 179 if isinstance(options, (list, tuple)):
180 180 options_iter = options
181 181 # Handle old value,label lists ... where value also can be value,label lists
182 182 options = Options()
183 183 for opt in options_iter:
184 184 if isinstance(opt, tuple) and len(opt) == 2:
185 185 value, label = opt
186 186 elif isinstance(opt, basestring):
187 187 value = label = opt
188 188 else:
189 189 raise ValueError('invalid select option type %r' % type(opt))
190 190
191 191 if isinstance(value, (list, tuple)):
192 192 option_group = options.add_optgroup(label)
193 193 for opt2 in value:
194 194 if isinstance(opt2, tuple) and len(opt2) == 2:
195 195 group_value, group_label = opt2
196 196 elif isinstance(opt2, basestring):
197 197 group_value = group_label = opt2
198 198 else:
199 199 raise ValueError('invalid select option type %r' % type(opt2))
200 200
201 201 option_group.add_option(group_label, group_value)
202 202 else:
203 203 options.add_option(label, value)
204 204
205 205 return raw_select(name, selected_values, options, id=id, **attrs)
206 206
207 207
208 208 def branding(name, length=40):
209 209 return truncate(name, length, indicator="")
210 210
211 211
212 212 def FID(raw_id, path):
213 213 """
214 214 Creates a unique ID for filenode based on it's hash of path and commit
215 215 it's safe to use in urls
216 216
217 217 :param raw_id:
218 218 :param path:
219 219 """
220 220
221 221 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
222 222
223 223
224 224 class _GetError(object):
225 225 """Get error from form_errors, and represent it as span wrapped error
226 226 message
227 227
228 228 :param field_name: field to fetch errors for
229 229 :param form_errors: form errors dict
230 230 """
231 231
232 232 def __call__(self, field_name, form_errors):
233 233 tmpl = """<span class="error_msg">%s</span>"""
234 234 if form_errors and field_name in form_errors:
235 235 return literal(tmpl % form_errors.get(field_name))
236 236
237 237
238 238 get_error = _GetError()
239 239
240 240
241 241 class _ToolTip(object):
242 242
243 243 def __call__(self, tooltip_title, trim_at=50):
244 244 """
245 245 Special function just to wrap our text into nice formatted
246 246 autowrapped text
247 247
248 248 :param tooltip_title:
249 249 """
250 250 tooltip_title = escape(tooltip_title)
251 251 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
252 252 return tooltip_title
253 253
254 254
255 255 tooltip = _ToolTip()
256 256
257 257 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
258 258
259 259
260 260 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
261 261 limit_items=False, linkify_last_item=False, hide_last_item=False,
262 262 copy_path_icon=True):
263 263 if isinstance(file_path, str):
264 264 file_path = safe_unicode(file_path)
265 265
266 266 if at_ref:
267 267 route_qry = {'at': at_ref}
268 268 default_landing_ref = at_ref or landing_ref_name or commit_id
269 269 else:
270 270 route_qry = None
271 271 default_landing_ref = commit_id
272 272
273 273 # first segment is a `HOME` link to repo files root location
274 274 root_name = literal(u'<i class="icon-home"></i>')
275 275
276 276 url_segments = [
277 277 link_to(
278 278 root_name,
279 279 repo_files_by_ref_url(
280 280 repo_name,
281 281 repo_type,
282 282 f_path=None, # None here is a special case for SVN repos,
283 283 # that won't prefix with a ref
284 284 ref_name=default_landing_ref,
285 285 commit_id=commit_id,
286 286 query=route_qry
287 287 )
288 288 )]
289 289
290 290 path_segments = file_path.split('/')
291 291 last_cnt = len(path_segments) - 1
292 292 for cnt, segment in enumerate(path_segments):
293 293 if not segment:
294 294 continue
295 295 segment_html = escape(segment)
296 296
297 297 last_item = cnt == last_cnt
298 298
299 299 if last_item and hide_last_item:
300 300 # iterate over and hide last element
301 301 continue
302 302
303 303 if last_item and linkify_last_item is False:
304 304 # plain version
305 305 url_segments.append(segment_html)
306 306 else:
307 307 url_segments.append(
308 308 link_to(
309 309 segment_html,
310 310 repo_files_by_ref_url(
311 311 repo_name,
312 312 repo_type,
313 313 f_path='/'.join(path_segments[:cnt + 1]),
314 314 ref_name=default_landing_ref,
315 315 commit_id=commit_id,
316 316 query=route_qry
317 317 ),
318 318 ))
319 319
320 320 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
321 321 if limit_items and len(limited_url_segments) < len(url_segments):
322 322 url_segments = limited_url_segments
323 323
324 324 full_path = file_path
325 325 if copy_path_icon:
326 326 icon = files_icon.format(escape(full_path))
327 327 else:
328 328 icon = ''
329 329
330 330 if file_path == '':
331 331 return root_name
332 332 else:
333 333 return literal(' / '.join(url_segments) + icon)
334 334
335 335
336 336 def files_url_data(request):
337 337 matchdict = request.matchdict
338 338
339 339 if 'f_path' not in matchdict:
340 340 matchdict['f_path'] = ''
341 341
342 342 if 'commit_id' not in matchdict:
343 343 matchdict['commit_id'] = 'tip'
344 344
345 345 return json.dumps(matchdict)
346 346
347 347
348 348 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
349 349 _is_svn = is_svn(db_repo_type)
350 350 final_f_path = f_path
351 351
352 352 if _is_svn:
353 353 """
354 354 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
355 355 actually commit_id followed by the ref_name. This should be done only in case
356 356 This is a initial landing url, without additional paths.
357 357
358 358 like: /1000/tags/1.0.0/?at=tags/1.0.0
359 359 """
360 360
361 361 if ref_name and ref_name != 'tip':
362 362 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
363 363 # for SVN we only do this magic prefix if it's root, .eg landing revision
364 364 # of files link. If we are in the tree we don't need this since we traverse the url
365 365 # that has everything stored
366 366 if f_path in ['', '/']:
367 367 final_f_path = '/'.join([ref_name, f_path])
368 368
369 369 # SVN always needs a commit_id explicitly, without a named REF
370 370 default_commit_id = commit_id
371 371 else:
372 372 """
373 373 For git and mercurial we construct a new URL using the names instead of commit_id
374 374 like: /master/some_path?at=master
375 375 """
376 376 # We currently do not support branches with slashes
377 377 if '/' in ref_name:
378 378 default_commit_id = commit_id
379 379 else:
380 380 default_commit_id = ref_name
381 381
382 382 # sometimes we pass f_path as None, to indicate explicit no prefix,
383 383 # we translate it to string to not have None
384 384 final_f_path = final_f_path or ''
385 385
386 386 files_url = route_path(
387 387 'repo_files',
388 388 repo_name=db_repo_name,
389 389 commit_id=default_commit_id,
390 390 f_path=final_f_path,
391 391 _query=query
392 392 )
393 393 return files_url
394 394
395 395
396 396 def code_highlight(code, lexer, formatter, use_hl_filter=False):
397 397 """
398 398 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
399 399
400 400 If ``outfile`` is given and a valid file object (an object
401 401 with a ``write`` method), the result will be written to it, otherwise
402 402 it is returned as a string.
403 403 """
404 404 if use_hl_filter:
405 405 # add HL filter
406 406 from rhodecode.lib.index import search_utils
407 407 lexer.add_filter(search_utils.ElasticSearchHLFilter())
408 408 return pygments.format(pygments.lex(code, lexer), formatter)
409 409
410 410
411 411 class CodeHtmlFormatter(HtmlFormatter):
412 412 """
413 413 My code Html Formatter for source codes
414 414 """
415 415
416 416 def wrap(self, source, outfile):
417 417 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
418 418
419 419 def _wrap_code(self, source):
420 420 for cnt, it in enumerate(source):
421 421 i, t = it
422 422 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
423 423 yield i, t
424 424
425 425 def _wrap_tablelinenos(self, inner):
426 426 dummyoutfile = StringIO.StringIO()
427 427 lncount = 0
428 428 for t, line in inner:
429 429 if t:
430 430 lncount += 1
431 431 dummyoutfile.write(line)
432 432
433 433 fl = self.linenostart
434 434 mw = len(str(lncount + fl - 1))
435 435 sp = self.linenospecial
436 436 st = self.linenostep
437 437 la = self.lineanchors
438 438 aln = self.anchorlinenos
439 439 nocls = self.noclasses
440 440 if sp:
441 441 lines = []
442 442
443 443 for i in range(fl, fl + lncount):
444 444 if i % st == 0:
445 445 if i % sp == 0:
446 446 if aln:
447 447 lines.append('<a href="#%s%d" class="special">%*d</a>' %
448 448 (la, i, mw, i))
449 449 else:
450 450 lines.append('<span class="special">%*d</span>' % (mw, i))
451 451 else:
452 452 if aln:
453 453 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
454 454 else:
455 455 lines.append('%*d' % (mw, i))
456 456 else:
457 457 lines.append('')
458 458 ls = '\n'.join(lines)
459 459 else:
460 460 lines = []
461 461 for i in range(fl, fl + lncount):
462 462 if i % st == 0:
463 463 if aln:
464 464 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
465 465 else:
466 466 lines.append('%*d' % (mw, i))
467 467 else:
468 468 lines.append('')
469 469 ls = '\n'.join(lines)
470 470
471 471 # in case you wonder about the seemingly redundant <div> here: since the
472 472 # content in the other cell also is wrapped in a div, some browsers in
473 473 # some configurations seem to mess up the formatting...
474 474 if nocls:
475 475 yield 0, ('<table class="%stable">' % self.cssclass +
476 476 '<tr><td><div class="linenodiv" '
477 477 'style="background-color: #f0f0f0; padding-right: 10px">'
478 478 '<pre style="line-height: 125%">' +
479 479 ls + '</pre></div></td><td id="hlcode" class="code">')
480 480 else:
481 481 yield 0, ('<table class="%stable">' % self.cssclass +
482 482 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
483 483 ls + '</pre></div></td><td id="hlcode" class="code">')
484 484 yield 0, dummyoutfile.getvalue()
485 485 yield 0, '</td></tr></table>'
486 486
487 487
488 488 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
489 489 def __init__(self, **kw):
490 490 # only show these line numbers if set
491 491 self.only_lines = kw.pop('only_line_numbers', [])
492 492 self.query_terms = kw.pop('query_terms', [])
493 493 self.max_lines = kw.pop('max_lines', 5)
494 494 self.line_context = kw.pop('line_context', 3)
495 495 self.url = kw.pop('url', None)
496 496
497 497 super(CodeHtmlFormatter, self).__init__(**kw)
498 498
499 499 def _wrap_code(self, source):
500 500 for cnt, it in enumerate(source):
501 501 i, t = it
502 502 t = '<pre>%s</pre>' % t
503 503 yield i, t
504 504
505 505 def _wrap_tablelinenos(self, inner):
506 506 yield 0, '<table class="code-highlight %stable">' % self.cssclass
507 507
508 508 last_shown_line_number = 0
509 509 current_line_number = 1
510 510
511 511 for t, line in inner:
512 512 if not t:
513 513 yield t, line
514 514 continue
515 515
516 516 if current_line_number in self.only_lines:
517 517 if last_shown_line_number + 1 != current_line_number:
518 518 yield 0, '<tr>'
519 519 yield 0, '<td class="line">...</td>'
520 520 yield 0, '<td id="hlcode" class="code"></td>'
521 521 yield 0, '</tr>'
522 522
523 523 yield 0, '<tr>'
524 524 if self.url:
525 525 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
526 526 self.url, current_line_number, current_line_number)
527 527 else:
528 528 yield 0, '<td class="line"><a href="">%i</a></td>' % (
529 529 current_line_number)
530 530 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
531 531 yield 0, '</tr>'
532 532
533 533 last_shown_line_number = current_line_number
534 534
535 535 current_line_number += 1
536 536
537 537 yield 0, '</table>'
538 538
539 539
540 540 def hsv_to_rgb(h, s, v):
541 541 """ Convert hsv color values to rgb """
542 542
543 543 if s == 0.0:
544 544 return v, v, v
545 545 i = int(h * 6.0) # XXX assume int() truncates!
546 546 f = (h * 6.0) - i
547 547 p = v * (1.0 - s)
548 548 q = v * (1.0 - s * f)
549 549 t = v * (1.0 - s * (1.0 - f))
550 550 i = i % 6
551 551 if i == 0:
552 552 return v, t, p
553 553 if i == 1:
554 554 return q, v, p
555 555 if i == 2:
556 556 return p, v, t
557 557 if i == 3:
558 558 return p, q, v
559 559 if i == 4:
560 560 return t, p, v
561 561 if i == 5:
562 562 return v, p, q
563 563
564 564
565 565 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
566 566 """
567 567 Generator for getting n of evenly distributed colors using
568 568 hsv color and golden ratio. It always return same order of colors
569 569
570 570 :param n: number of colors to generate
571 571 :param saturation: saturation of returned colors
572 572 :param lightness: lightness of returned colors
573 573 :returns: RGB tuple
574 574 """
575 575
576 576 golden_ratio = 0.618033988749895
577 577 h = 0.22717784590367374
578 578
579 579 for _ in xrange(n):
580 580 h += golden_ratio
581 581 h %= 1
582 582 HSV_tuple = [h, saturation, lightness]
583 583 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 584 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 585
586 586
587 587 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
588 588 """
589 589 Returns a function which when called with an argument returns a unique
590 590 color for that argument, eg.
591 591
592 592 :param n: number of colors to generate
593 593 :param saturation: saturation of returned colors
594 594 :param lightness: lightness of returned colors
595 595 :returns: css RGB string
596 596
597 597 >>> color_hash = color_hasher()
598 598 >>> color_hash('hello')
599 599 'rgb(34, 12, 59)'
600 600 >>> color_hash('hello')
601 601 'rgb(34, 12, 59)'
602 602 >>> color_hash('other')
603 603 'rgb(90, 224, 159)'
604 604 """
605 605
606 606 color_dict = {}
607 607 cgenerator = unique_color_generator(
608 608 saturation=saturation, lightness=lightness)
609 609
610 610 def get_color_string(thing):
611 611 if thing in color_dict:
612 612 col = color_dict[thing]
613 613 else:
614 614 col = color_dict[thing] = cgenerator.next()
615 615 return "rgb(%s)" % (', '.join(col))
616 616
617 617 return get_color_string
618 618
619 619
620 620 def get_lexer_safe(mimetype=None, filepath=None):
621 621 """
622 622 Tries to return a relevant pygments lexer using mimetype/filepath name,
623 623 defaulting to plain text if none could be found
624 624 """
625 625 lexer = None
626 626 try:
627 627 if mimetype:
628 628 lexer = get_lexer_for_mimetype(mimetype)
629 629 if not lexer:
630 630 lexer = get_lexer_for_filename(filepath)
631 631 except pygments.util.ClassNotFound:
632 632 pass
633 633
634 634 if not lexer:
635 635 lexer = get_lexer_by_name('text')
636 636
637 637 return lexer
638 638
639 639
640 640 def get_lexer_for_filenode(filenode):
641 641 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
642 642 return lexer
643 643
644 644
645 645 def pygmentize(filenode, **kwargs):
646 646 """
647 647 pygmentize function using pygments
648 648
649 649 :param filenode:
650 650 """
651 651 lexer = get_lexer_for_filenode(filenode)
652 652 return literal(code_highlight(filenode.content, lexer,
653 653 CodeHtmlFormatter(**kwargs)))
654 654
655 655
656 656 def is_following_repo(repo_name, user_id):
657 657 from rhodecode.model.scm import ScmModel
658 658 return ScmModel().is_following_repo(repo_name, user_id)
659 659
660 660
661 661 class _Message(object):
662 662 """A message returned by ``Flash.pop_messages()``.
663 663
664 664 Converting the message to a string returns the message text. Instances
665 665 also have the following attributes:
666 666
667 667 * ``message``: the message text.
668 668 * ``category``: the category specified when the message was created.
669 669 """
670 670
671 671 def __init__(self, category, message, sub_data=None):
672 672 self.category = category
673 673 self.message = message
674 674 self.sub_data = sub_data or {}
675 675
676 676 def __str__(self):
677 677 return self.message
678 678
679 679 __unicode__ = __str__
680 680
681 681 def __html__(self):
682 682 return escape(safe_unicode(self.message))
683 683
684 684
685 685 class Flash(object):
686 686 # List of allowed categories. If None, allow any category.
687 687 categories = ["warning", "notice", "error", "success"]
688 688
689 689 # Default category if none is specified.
690 690 default_category = "notice"
691 691
692 692 def __init__(self, session_key="flash", categories=None,
693 693 default_category=None):
694 694 """
695 695 Instantiate a ``Flash`` object.
696 696
697 697 ``session_key`` is the key to save the messages under in the user's
698 698 session.
699 699
700 700 ``categories`` is an optional list which overrides the default list
701 701 of categories.
702 702
703 703 ``default_category`` overrides the default category used for messages
704 704 when none is specified.
705 705 """
706 706 self.session_key = session_key
707 707 if categories is not None:
708 708 self.categories = categories
709 709 if default_category is not None:
710 710 self.default_category = default_category
711 711 if self.categories and self.default_category not in self.categories:
712 712 raise ValueError(
713 713 "unrecognized default category %r" % (self.default_category,))
714 714
715 715 def pop_messages(self, session=None, request=None):
716 716 """
717 717 Return all accumulated messages and delete them from the session.
718 718
719 719 The return value is a list of ``Message`` objects.
720 720 """
721 721 messages = []
722 722
723 723 if not session:
724 724 if not request:
725 725 request = get_current_request()
726 726 session = request.session
727 727
728 728 # Pop the 'old' pylons flash messages. They are tuples of the form
729 729 # (category, message)
730 730 for cat, msg in session.pop(self.session_key, []):
731 731 messages.append(_Message(cat, msg))
732 732
733 733 # Pop the 'new' pyramid flash messages for each category as list
734 734 # of strings.
735 735 for cat in self.categories:
736 736 for msg in session.pop_flash(queue=cat):
737 737 sub_data = {}
738 738 if hasattr(msg, 'rsplit'):
739 739 flash_data = msg.rsplit('|DELIM|', 1)
740 740 org_message = flash_data[0]
741 741 if len(flash_data) > 1:
742 742 sub_data = json.loads(flash_data[1])
743 743 else:
744 744 org_message = msg
745 745
746 746 messages.append(_Message(cat, org_message, sub_data=sub_data))
747 747
748 748 # Map messages from the default queue to the 'notice' category.
749 749 for msg in session.pop_flash():
750 750 messages.append(_Message('notice', msg))
751 751
752 752 session.save()
753 753 return messages
754 754
755 755 def json_alerts(self, session=None, request=None):
756 756 payloads = []
757 757 messages = flash.pop_messages(session=session, request=request) or []
758 758 for message in messages:
759 759 payloads.append({
760 760 'message': {
761 761 'message': u'{}'.format(message.message),
762 762 'level': message.category,
763 763 'force': True,
764 764 'subdata': message.sub_data
765 765 }
766 766 })
767 767 return json.dumps(payloads)
768 768
769 769 def __call__(self, message, category=None, ignore_duplicate=True,
770 770 session=None, request=None):
771 771
772 772 if not session:
773 773 if not request:
774 774 request = get_current_request()
775 775 session = request.session
776 776
777 777 session.flash(
778 778 message, queue=category, allow_duplicate=not ignore_duplicate)
779 779
780 780
781 781 flash = Flash()
782 782
783 783 #==============================================================================
784 784 # SCM FILTERS available via h.
785 785 #==============================================================================
786 786 from rhodecode.lib.vcs.utils import author_name, author_email
787 787 from rhodecode.lib.utils2 import age, age_from_seconds
788 788 from rhodecode.model.db import User, ChangesetStatus
789 789
790 790
791 791 email = author_email
792 792
793 793
794 794 def capitalize(raw_text):
795 795 return raw_text.capitalize()
796 796
797 797
798 798 def short_id(long_id):
799 799 return long_id[:12]
800 800
801 801
802 802 def hide_credentials(url):
803 803 from rhodecode.lib.utils2 import credentials_filter
804 804 return credentials_filter(url)
805 805
806 806
807 807 import pytz
808 808 import tzlocal
809 809 local_timezone = tzlocal.get_localzone()
810 810
811 811
812 812 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
813 813 title = value or format_date(datetime_iso)
814 814 tzinfo = '+00:00'
815 815
816 816 # detect if we have a timezone info, otherwise, add it
817 817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
818 818 force_timezone = os.environ.get('RC_TIMEZONE', '')
819 819 if force_timezone:
820 820 force_timezone = pytz.timezone(force_timezone)
821 821 timezone = force_timezone or local_timezone
822 822 offset = timezone.localize(datetime_iso).strftime('%z')
823 823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 824
825 825 return literal(
826 826 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
827 827 cls='tooltip' if tooltip else '',
828 828 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
829 829 title=title, dt=datetime_iso, tzinfo=tzinfo
830 830 ))
831 831
832 832
833 833 def _shorten_commit_id(commit_id, commit_len=None):
834 834 if commit_len is None:
835 835 request = get_current_request()
836 836 commit_len = request.call_context.visual.show_sha_length
837 837 return commit_id[:commit_len]
838 838
839 839
840 840 def show_id(commit, show_idx=None, commit_len=None):
841 841 """
842 842 Configurable function that shows ID
843 843 by default it's r123:fffeeefffeee
844 844
845 845 :param commit: commit instance
846 846 """
847 847 if show_idx is None:
848 848 request = get_current_request()
849 849 show_idx = request.call_context.visual.show_revision_number
850 850
851 851 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
852 852 if show_idx:
853 853 return 'r%s:%s' % (commit.idx, raw_id)
854 854 else:
855 855 return '%s' % (raw_id, )
856 856
857 857
858 858 def format_date(date):
859 859 """
860 860 use a standardized formatting for dates used in RhodeCode
861 861
862 862 :param date: date/datetime object
863 863 :return: formatted date
864 864 """
865 865
866 866 if date:
867 867 _fmt = "%a, %d %b %Y %H:%M:%S"
868 868 return safe_unicode(date.strftime(_fmt))
869 869
870 870 return u""
871 871
872 872
873 873 class _RepoChecker(object):
874 874
875 875 def __init__(self, backend_alias):
876 876 self._backend_alias = backend_alias
877 877
878 878 def __call__(self, repository):
879 879 if hasattr(repository, 'alias'):
880 880 _type = repository.alias
881 881 elif hasattr(repository, 'repo_type'):
882 882 _type = repository.repo_type
883 883 else:
884 884 _type = repository
885 885 return _type == self._backend_alias
886 886
887 887
888 888 is_git = _RepoChecker('git')
889 889 is_hg = _RepoChecker('hg')
890 890 is_svn = _RepoChecker('svn')
891 891
892 892
893 893 def get_repo_type_by_name(repo_name):
894 894 repo = Repository.get_by_repo_name(repo_name)
895 895 if repo:
896 896 return repo.repo_type
897 897
898 898
899 899 def is_svn_without_proxy(repository):
900 900 if is_svn(repository):
901 901 from rhodecode.model.settings import VcsSettingsModel
902 902 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
903 903 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
904 904 return False
905 905
906 906
907 907 def discover_user(author):
908 908 """
909 909 Tries to discover RhodeCode User based on the author string. Author string
910 910 is typically `FirstName LastName <email@address.com>`
911 911 """
912 912
913 913 # if author is already an instance use it for extraction
914 914 if isinstance(author, User):
915 915 return author
916 916
917 917 # Valid email in the attribute passed, see if they're in the system
918 918 _email = author_email(author)
919 919 if _email != '':
920 920 user = User.get_by_email(_email, case_insensitive=True, cache=True)
921 921 if user is not None:
922 922 return user
923 923
924 924 # Maybe it's a username, we try to extract it and fetch by username ?
925 925 _author = author_name(author)
926 926 user = User.get_by_username(_author, case_insensitive=True, cache=True)
927 927 if user is not None:
928 928 return user
929 929
930 930 return None
931 931
932 932
933 933 def email_or_none(author):
934 934 # extract email from the commit string
935 935 _email = author_email(author)
936 936
937 937 # If we have an email, use it, otherwise
938 938 # see if it contains a username we can get an email from
939 939 if _email != '':
940 940 return _email
941 941 else:
942 942 user = User.get_by_username(
943 943 author_name(author), case_insensitive=True, cache=True)
944 944
945 945 if user is not None:
946 946 return user.email
947 947
948 948 # No valid email, not a valid user in the system, none!
949 949 return None
950 950
951 951
952 952 def link_to_user(author, length=0, **kwargs):
953 953 user = discover_user(author)
954 954 # user can be None, but if we have it already it means we can re-use it
955 955 # in the person() function, so we save 1 intensive-query
956 956 if user:
957 957 author = user
958 958
959 959 display_person = person(author, 'username_or_name_or_email')
960 960 if length:
961 961 display_person = shorter(display_person, length)
962 962
963 963 if user and user.username != user.DEFAULT_USER:
964 964 return link_to(
965 965 escape(display_person),
966 966 route_path('user_profile', username=user.username),
967 967 **kwargs)
968 968 else:
969 969 return escape(display_person)
970 970
971 971
972 972 def link_to_group(users_group_name, **kwargs):
973 973 return link_to(
974 974 escape(users_group_name),
975 975 route_path('user_group_profile', user_group_name=users_group_name),
976 976 **kwargs)
977 977
978 978
979 979 def person(author, show_attr="username_and_name"):
980 980 user = discover_user(author)
981 981 if user:
982 982 return getattr(user, show_attr)
983 983 else:
984 984 _author = author_name(author)
985 985 _email = email(author)
986 986 return _author or _email
987 987
988 988
989 989 def author_string(email):
990 990 if email:
991 991 user = User.get_by_email(email, case_insensitive=True, cache=True)
992 992 if user:
993 993 if user.first_name or user.last_name:
994 994 return '%s %s &lt;%s&gt;' % (
995 995 user.first_name, user.last_name, email)
996 996 else:
997 997 return email
998 998 else:
999 999 return email
1000 1000 else:
1001 1001 return None
1002 1002
1003 1003
1004 1004 def person_by_id(id_, show_attr="username_and_name"):
1005 1005 # attr to return from fetched user
1006 1006 person_getter = lambda usr: getattr(usr, show_attr)
1007 1007
1008 1008 #maybe it's an ID ?
1009 1009 if str(id_).isdigit() or isinstance(id_, int):
1010 1010 id_ = int(id_)
1011 1011 user = User.get(id_)
1012 1012 if user is not None:
1013 1013 return person_getter(user)
1014 1014 return id_
1015 1015
1016 1016
1017 1017 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1018 1018 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1019 1019 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1020 1020
1021 1021
1022 1022 tags_paterns = OrderedDict((
1023 1023 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1024 1024 '<div class="metatag" tag="lang">\\2</div>')),
1025 1025
1026 1026 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1027 1027 '<div class="metatag" tag="see">see: \\1 </div>')),
1028 1028
1029 1029 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1030 1030 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1031 1031
1032 1032 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1033 1033 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1034 1034
1035 1035 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1036 1036 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1037 1037
1038 1038 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1039 1039 '<div class="metatag" tag="state \\1">\\1</div>')),
1040 1040
1041 1041 # label in grey
1042 1042 ('label', (re.compile(r'\[([a-z]+)\]'),
1043 1043 '<div class="metatag" tag="label">\\1</div>')),
1044 1044
1045 1045 # generic catch all in grey
1046 1046 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1047 1047 '<div class="metatag" tag="generic">\\1</div>')),
1048 1048 ))
1049 1049
1050 1050
1051 1051 def extract_metatags(value):
1052 1052 """
1053 1053 Extract supported meta-tags from given text value
1054 1054 """
1055 1055 tags = []
1056 1056 if not value:
1057 1057 return tags, ''
1058 1058
1059 1059 for key, val in tags_paterns.items():
1060 1060 pat, replace_html = val
1061 1061 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1062 1062 value = pat.sub('', value)
1063 1063
1064 1064 return tags, value
1065 1065
1066 1066
1067 1067 def style_metatag(tag_type, value):
1068 1068 """
1069 1069 converts tags from value into html equivalent
1070 1070 """
1071 1071 if not value:
1072 1072 return ''
1073 1073
1074 1074 html_value = value
1075 1075 tag_data = tags_paterns.get(tag_type)
1076 1076 if tag_data:
1077 1077 pat, replace_html = tag_data
1078 1078 # convert to plain `unicode` instead of a markup tag to be used in
1079 1079 # regex expressions. safe_unicode doesn't work here
1080 1080 html_value = pat.sub(replace_html, unicode(value))
1081 1081
1082 1082 return html_value
1083 1083
1084 1084
1085 1085 def bool2icon(value, show_at_false=True):
1086 1086 """
1087 1087 Returns boolean value of a given value, represented as html element with
1088 1088 classes that will represent icons
1089 1089
1090 1090 :param value: given value to convert to html node
1091 1091 """
1092 1092
1093 1093 if value: # does bool conversion
1094 1094 return HTML.tag('i', class_="icon-true", title='True')
1095 1095 else: # not true as bool
1096 1096 if show_at_false:
1097 1097 return HTML.tag('i', class_="icon-false", title='False')
1098 1098 return HTML.tag('i')
1099 1099
1100 1100 #==============================================================================
1101 1101 # PERMS
1102 1102 #==============================================================================
1103 1103 from rhodecode.lib.auth import (
1104 1104 HasPermissionAny, HasPermissionAll,
1105 1105 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1106 1106 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1107 1107 csrf_token_key, AuthUser)
1108 1108
1109 1109
1110 1110 #==============================================================================
1111 1111 # GRAVATAR URL
1112 1112 #==============================================================================
1113 1113 class InitialsGravatar(object):
1114 1114 def __init__(self, email_address, first_name, last_name, size=30,
1115 1115 background=None, text_color='#fff'):
1116 1116 self.size = size
1117 1117 self.first_name = first_name
1118 1118 self.last_name = last_name
1119 1119 self.email_address = email_address
1120 1120 self.background = background or self.str2color(email_address)
1121 1121 self.text_color = text_color
1122 1122
1123 1123 def get_color_bank(self):
1124 1124 """
1125 1125 returns a predefined list of colors that gravatars can use.
1126 1126 Those are randomized distinct colors that guarantee readability and
1127 1127 uniqueness.
1128 1128
1129 1129 generated with: http://phrogz.net/css/distinct-colors.html
1130 1130 """
1131 1131 return [
1132 1132 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1133 1133 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1134 1134 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1135 1135 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1136 1136 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1137 1137 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1138 1138 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1139 1139 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1140 1140 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1141 1141 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1142 1142 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1143 1143 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1144 1144 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1145 1145 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1146 1146 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1147 1147 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1148 1148 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1149 1149 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1150 1150 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1151 1151 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1152 1152 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1153 1153 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1154 1154 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1155 1155 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1156 1156 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1157 1157 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1158 1158 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1159 1159 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1160 1160 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1161 1161 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1162 1162 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1163 1163 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1164 1164 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1165 1165 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1166 1166 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1167 1167 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1168 1168 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1169 1169 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1170 1170 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1171 1171 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1172 1172 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1173 1173 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1174 1174 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1175 1175 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1176 1176 '#4f8c46', '#368dd9', '#5c0073'
1177 1177 ]
1178 1178
1179 1179 def rgb_to_hex_color(self, rgb_tuple):
1180 1180 """
1181 1181 Converts an rgb_tuple passed to an hex color.
1182 1182
1183 1183 :param rgb_tuple: tuple with 3 ints represents rgb color space
1184 1184 """
1185 1185 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1186 1186
1187 1187 def email_to_int_list(self, email_str):
1188 1188 """
1189 1189 Get every byte of the hex digest value of email and turn it to integer.
1190 1190 It's going to be always between 0-255
1191 1191 """
1192 1192 digest = md5_safe(email_str.lower())
1193 1193 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1194 1194
1195 1195 def pick_color_bank_index(self, email_str, color_bank):
1196 1196 return self.email_to_int_list(email_str)[0] % len(color_bank)
1197 1197
1198 1198 def str2color(self, email_str):
1199 1199 """
1200 1200 Tries to map in a stable algorithm an email to color
1201 1201
1202 1202 :param email_str:
1203 1203 """
1204 1204 color_bank = self.get_color_bank()
1205 1205 # pick position (module it's length so we always find it in the
1206 1206 # bank even if it's smaller than 256 values
1207 1207 pos = self.pick_color_bank_index(email_str, color_bank)
1208 1208 return color_bank[pos]
1209 1209
1210 1210 def normalize_email(self, email_address):
1211 1211 import unicodedata
1212 1212 # default host used to fill in the fake/missing email
1213 1213 default_host = u'localhost'
1214 1214
1215 1215 if not email_address:
1216 1216 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1217 1217
1218 1218 email_address = safe_unicode(email_address)
1219 1219
1220 1220 if u'@' not in email_address:
1221 1221 email_address = u'%s@%s' % (email_address, default_host)
1222 1222
1223 1223 if email_address.endswith(u'@'):
1224 1224 email_address = u'%s%s' % (email_address, default_host)
1225 1225
1226 1226 email_address = unicodedata.normalize('NFKD', email_address)\
1227 1227 .encode('ascii', 'ignore')
1228 1228 return email_address
1229 1229
1230 1230 def get_initials(self):
1231 1231 """
1232 1232 Returns 2 letter initials calculated based on the input.
1233 1233 The algorithm picks first given email address, and takes first letter
1234 1234 of part before @, and then the first letter of server name. In case
1235 1235 the part before @ is in a format of `somestring.somestring2` it replaces
1236 1236 the server letter with first letter of somestring2
1237 1237
1238 1238 In case function was initialized with both first and lastname, this
1239 1239 overrides the extraction from email by first letter of the first and
1240 1240 last name. We add special logic to that functionality, In case Full name
1241 1241 is compound, like Guido Von Rossum, we use last part of the last name
1242 1242 (Von Rossum) picking `R`.
1243 1243
1244 1244 Function also normalizes the non-ascii characters to they ascii
1245 1245 representation, eg Δ„ => A
1246 1246 """
1247 1247 import unicodedata
1248 1248 # replace non-ascii to ascii
1249 1249 first_name = unicodedata.normalize(
1250 1250 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1251 1251 last_name = unicodedata.normalize(
1252 1252 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1253 1253
1254 1254 # do NFKD encoding, and also make sure email has proper format
1255 1255 email_address = self.normalize_email(self.email_address)
1256 1256
1257 1257 # first push the email initials
1258 1258 prefix, server = email_address.split('@', 1)
1259 1259
1260 1260 # check if prefix is maybe a 'first_name.last_name' syntax
1261 1261 _dot_split = prefix.rsplit('.', 1)
1262 1262 if len(_dot_split) == 2 and _dot_split[1]:
1263 1263 initials = [_dot_split[0][0], _dot_split[1][0]]
1264 1264 else:
1265 1265 initials = [prefix[0], server[0]]
1266 1266
1267 1267 # then try to replace either first_name or last_name
1268 1268 fn_letter = (first_name or " ")[0].strip()
1269 1269 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1270 1270
1271 1271 if fn_letter:
1272 1272 initials[0] = fn_letter
1273 1273
1274 1274 if ln_letter:
1275 1275 initials[1] = ln_letter
1276 1276
1277 1277 return ''.join(initials).upper()
1278 1278
1279 1279 def get_img_data_by_type(self, font_family, img_type):
1280 1280 default_user = """
1281 1281 <svg xmlns="http://www.w3.org/2000/svg"
1282 1282 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1283 1283 viewBox="-15 -10 439.165 429.164"
1284 1284
1285 1285 xml:space="preserve"
1286 1286 style="background:{background};" >
1287 1287
1288 1288 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1289 1289 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1290 1290 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1291 1291 168.596,153.916,216.671,
1292 1292 204.583,216.671z" fill="{text_color}"/>
1293 1293 <path d="M407.164,374.717L360.88,
1294 1294 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1295 1295 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1296 1296 15.366-44.203,23.488-69.076,23.488c-24.877,
1297 1297 0-48.762-8.122-69.078-23.488
1298 1298 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1299 1299 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1300 1300 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1301 1301 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1302 1302 19.402-10.527 C409.699,390.129,
1303 1303 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1304 1304 </svg>""".format(
1305 1305 size=self.size,
1306 1306 background='#979797', # @grey4
1307 1307 text_color=self.text_color,
1308 1308 font_family=font_family)
1309 1309
1310 1310 return {
1311 1311 "default_user": default_user
1312 1312 }[img_type]
1313 1313
1314 1314 def get_img_data(self, svg_type=None):
1315 1315 """
1316 1316 generates the svg metadata for image
1317 1317 """
1318 1318 fonts = [
1319 1319 '-apple-system',
1320 1320 'BlinkMacSystemFont',
1321 1321 'Segoe UI',
1322 1322 'Roboto',
1323 1323 'Oxygen-Sans',
1324 1324 'Ubuntu',
1325 1325 'Cantarell',
1326 1326 'Helvetica Neue',
1327 1327 'sans-serif'
1328 1328 ]
1329 1329 font_family = ','.join(fonts)
1330 1330 if svg_type:
1331 1331 return self.get_img_data_by_type(font_family, svg_type)
1332 1332
1333 1333 initials = self.get_initials()
1334 1334 img_data = """
1335 1335 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1336 1336 width="{size}" height="{size}"
1337 1337 style="width: 100%; height: 100%; background-color: {background}"
1338 1338 viewBox="0 0 {size} {size}">
1339 1339 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1340 1340 pointer-events="auto" fill="{text_color}"
1341 1341 font-family="{font_family}"
1342 1342 style="font-weight: 400; font-size: {f_size}px;">{text}
1343 1343 </text>
1344 1344 </svg>""".format(
1345 1345 size=self.size,
1346 1346 f_size=self.size/2.05, # scale the text inside the box nicely
1347 1347 background=self.background,
1348 1348 text_color=self.text_color,
1349 1349 text=initials.upper(),
1350 1350 font_family=font_family)
1351 1351
1352 1352 return img_data
1353 1353
1354 1354 def generate_svg(self, svg_type=None):
1355 1355 img_data = self.get_img_data(svg_type)
1356 1356 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1357 1357
1358 1358
1359 1359 def initials_gravatar(email_address, first_name, last_name, size=30):
1360 1360 svg_type = None
1361 1361 if email_address == User.DEFAULT_USER_EMAIL:
1362 1362 svg_type = 'default_user'
1363 1363 klass = InitialsGravatar(email_address, first_name, last_name, size)
1364 1364 return klass.generate_svg(svg_type=svg_type)
1365 1365
1366 1366
1367 1367 def gravatar_url(email_address, size=30, request=None):
1368 1368 request = get_current_request()
1369 1369 _use_gravatar = request.call_context.visual.use_gravatar
1370 1370 _gravatar_url = request.call_context.visual.gravatar_url
1371 1371
1372 1372 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1373 1373
1374 1374 email_address = email_address or User.DEFAULT_USER_EMAIL
1375 1375 if isinstance(email_address, unicode):
1376 1376 # hashlib crashes on unicode items
1377 1377 email_address = safe_str(email_address)
1378 1378
1379 1379 # empty email or default user
1380 1380 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1381 1381 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1382 1382
1383 1383 if _use_gravatar:
1384 1384 # TODO: Disuse pyramid thread locals. Think about another solution to
1385 1385 # get the host and schema here.
1386 1386 request = get_current_request()
1387 1387 tmpl = safe_str(_gravatar_url)
1388 1388 tmpl = tmpl.replace('{email}', email_address)\
1389 1389 .replace('{md5email}', md5_safe(email_address.lower())) \
1390 1390 .replace('{netloc}', request.host)\
1391 1391 .replace('{scheme}', request.scheme)\
1392 1392 .replace('{size}', safe_str(size))
1393 1393 return tmpl
1394 1394 else:
1395 1395 return initials_gravatar(email_address, '', '', size=size)
1396 1396
1397 1397
1398 1398 def breadcrumb_repo_link(repo):
1399 1399 """
1400 1400 Makes a breadcrumbs path link to repo
1401 1401
1402 1402 ex::
1403 1403 group >> subgroup >> repo
1404 1404
1405 1405 :param repo: a Repository instance
1406 1406 """
1407 1407
1408 1408 path = [
1409 1409 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1410 1410 title='last change:{}'.format(format_date(group.last_commit_change)))
1411 1411 for group in repo.groups_with_parents
1412 1412 ] + [
1413 1413 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1414 1414 title='last change:{}'.format(format_date(repo.last_commit_change)))
1415 1415 ]
1416 1416
1417 1417 return literal(' &raquo; '.join(path))
1418 1418
1419 1419
1420 1420 def breadcrumb_repo_group_link(repo_group):
1421 1421 """
1422 1422 Makes a breadcrumbs path link to repo
1423 1423
1424 1424 ex::
1425 1425 group >> subgroup
1426 1426
1427 1427 :param repo_group: a Repository Group instance
1428 1428 """
1429 1429
1430 1430 path = [
1431 1431 link_to(group.name,
1432 1432 route_path('repo_group_home', repo_group_name=group.group_name),
1433 1433 title='last change:{}'.format(format_date(group.last_commit_change)))
1434 1434 for group in repo_group.parents
1435 1435 ] + [
1436 1436 link_to(repo_group.name,
1437 1437 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1438 1438 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1439 1439 ]
1440 1440
1441 1441 return literal(' &raquo; '.join(path))
1442 1442
1443 1443
1444 1444 def format_byte_size_binary(file_size):
1445 1445 """
1446 1446 Formats file/folder sizes to standard.
1447 1447 """
1448 1448 if file_size is None:
1449 1449 file_size = 0
1450 1450
1451 1451 formatted_size = format_byte_size(file_size, binary=True)
1452 1452 return formatted_size
1453 1453
1454 1454
1455 1455 def urlify_text(text_, safe=True, **href_attrs):
1456 1456 """
1457 1457 Extract urls from text and make html links out of them
1458 1458 """
1459 1459
1460 1460 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1461 1461 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1462 1462
1463 1463 def url_func(match_obj):
1464 1464 url_full = match_obj.groups()[0]
1465 1465 a_options = dict(href_attrs)
1466 1466 a_options['href'] = url_full
1467 1467 a_text = url_full
1468 1468 return HTML.tag("a", a_text, **a_options)
1469 1469
1470 1470 _new_text = url_pat.sub(url_func, text_)
1471 1471
1472 1472 if safe:
1473 1473 return literal(_new_text)
1474 1474 return _new_text
1475 1475
1476 1476
1477 1477 def urlify_commits(text_, repo_name):
1478 1478 """
1479 1479 Extract commit ids from text and make link from them
1480 1480
1481 1481 :param text_:
1482 1482 :param repo_name: repo name to build the URL with
1483 1483 """
1484 1484
1485 1485 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1486 1486
1487 1487 def url_func(match_obj):
1488 1488 commit_id = match_obj.groups()[1]
1489 1489 pref = match_obj.groups()[0]
1490 1490 suf = match_obj.groups()[2]
1491 1491
1492 1492 tmpl = (
1493 1493 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1494 1494 '%(commit_id)s</a>%(suf)s'
1495 1495 )
1496 1496 return tmpl % {
1497 1497 'pref': pref,
1498 1498 'cls': 'revision-link',
1499 1499 'url': route_url(
1500 1500 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1501 1501 'commit_id': commit_id,
1502 1502 'suf': suf,
1503 1503 'hovercard_alt': 'Commit: {}'.format(commit_id),
1504 1504 'hovercard_url': route_url(
1505 1505 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1506 1506 }
1507 1507
1508 1508 new_text = url_pat.sub(url_func, text_)
1509 1509
1510 1510 return new_text
1511 1511
1512 1512
1513 1513 def _process_url_func(match_obj, repo_name, uid, entry,
1514 1514 return_raw_data=False, link_format='html'):
1515 1515 pref = ''
1516 1516 if match_obj.group().startswith(' '):
1517 1517 pref = ' '
1518 1518
1519 1519 issue_id = ''.join(match_obj.groups())
1520 1520
1521 1521 if link_format == 'html':
1522 1522 tmpl = (
1523 1523 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1524 1524 '%(issue-prefix)s%(id-repr)s'
1525 1525 '</a>')
1526 1526 elif link_format == 'html+hovercard':
1527 1527 tmpl = (
1528 1528 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1529 1529 '%(issue-prefix)s%(id-repr)s'
1530 1530 '</a>')
1531 1531 elif link_format in ['rst', 'rst+hovercard']:
1532 1532 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1533 1533 elif link_format in ['markdown', 'markdown+hovercard']:
1534 1534 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1535 1535 else:
1536 1536 raise ValueError('Bad link_format:{}'.format(link_format))
1537 1537
1538 1538 (repo_name_cleaned,
1539 1539 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1540 1540
1541 1541 # variables replacement
1542 1542 named_vars = {
1543 1543 'id': issue_id,
1544 1544 'repo': repo_name,
1545 1545 'repo_name': repo_name_cleaned,
1546 1546 'group_name': parent_group_name,
1547 1547 # set dummy keys so we always have them
1548 1548 'hostname': '',
1549 1549 'netloc': '',
1550 1550 'scheme': ''
1551 1551 }
1552 1552
1553 1553 request = get_current_request()
1554 1554 if request:
1555 1555 # exposes, hostname, netloc, scheme
1556 1556 host_data = get_host_info(request)
1557 1557 named_vars.update(host_data)
1558 1558
1559 1559 # named regex variables
1560 1560 named_vars.update(match_obj.groupdict())
1561 1561 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1562 1562 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1563 1563 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1564 1564
1565 1565 def quote_cleaner(input_str):
1566 1566 """Remove quotes as it's HTML"""
1567 1567 return input_str.replace('"', '')
1568 1568
1569 1569 data = {
1570 1570 'pref': pref,
1571 1571 'cls': quote_cleaner('issue-tracker-link'),
1572 1572 'url': quote_cleaner(_url),
1573 1573 'id-repr': issue_id,
1574 1574 'issue-prefix': entry['pref'],
1575 1575 'serv': entry['url'],
1576 1576 'title': bleach.clean(desc, strip=True),
1577 1577 'hovercard_url': hovercard_url
1578 1578 }
1579 1579
1580 1580 if return_raw_data:
1581 1581 return {
1582 1582 'id': issue_id,
1583 1583 'url': _url
1584 1584 }
1585 1585 return tmpl % data
1586 1586
1587 1587
1588 1588 def get_active_pattern_entries(repo_name):
1589 1589 repo = None
1590 1590 if repo_name:
1591 1591 # Retrieving repo_name to avoid invalid repo_name to explode on
1592 1592 # IssueTrackerSettingsModel but still passing invalid name further down
1593 1593 repo = Repository.get_by_repo_name(repo_name, cache=True)
1594 1594
1595 1595 settings_model = IssueTrackerSettingsModel(repo=repo)
1596 1596 active_entries = settings_model.get_settings(cache=True)
1597 1597 return active_entries
1598 1598
1599 1599
1600 1600 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1601 1601
1602 1602
1603 1603 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1604 1604
1605 1605 allowed_formats = ['html', 'rst', 'markdown',
1606 1606 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1607 1607 if link_format not in allowed_formats:
1608 1608 raise ValueError('Link format can be only one of:{} got {}'.format(
1609 1609 allowed_formats, link_format))
1610 1610
1611 1611 if active_entries is None:
1612 1612 log.debug('Fetch active patterns for repo: %s', repo_name)
1613 1613 active_entries = get_active_pattern_entries(repo_name)
1614 1614
1615 1615 issues_data = []
1616 1616 new_text = text_string
1617 1617
1618 1618 log.debug('Got %s entries to process', len(active_entries))
1619 1619 for uid, entry in active_entries.items():
1620 1620 log.debug('found issue tracker entry with uid %s', uid)
1621 1621
1622 1622 if not (entry['pat'] and entry['url']):
1623 1623 log.debug('skipping due to missing data')
1624 1624 continue
1625 1625
1626 1626 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1627 1627 uid, entry['pat'], entry['url'], entry['pref'])
1628 1628
1629 1629 if entry.get('pat_compiled'):
1630 1630 pattern = entry['pat_compiled']
1631 1631 else:
1632 1632 try:
1633 1633 pattern = re.compile(r'%s' % entry['pat'])
1634 1634 except re.error:
1635 1635 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1636 1636 continue
1637 1637
1638 1638 data_func = partial(
1639 1639 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1640 1640 return_raw_data=True)
1641 1641
1642 1642 for match_obj in pattern.finditer(text_string):
1643 1643 issues_data.append(data_func(match_obj))
1644 1644
1645 1645 url_func = partial(
1646 1646 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1647 1647 link_format=link_format)
1648 1648
1649 1649 new_text = pattern.sub(url_func, new_text)
1650 1650 log.debug('processed prefix:uid `%s`', uid)
1651 1651
1652 1652 # finally use global replace, eg !123 -> pr-link, those will not catch
1653 1653 # if already similar pattern exists
1654 1654 server_url = '${scheme}://${netloc}'
1655 1655 pr_entry = {
1656 1656 'pref': '!',
1657 1657 'url': server_url + '/_admin/pull-requests/${id}',
1658 1658 'desc': 'Pull Request !${id}',
1659 1659 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1660 1660 }
1661 1661 pr_url_func = partial(
1662 1662 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1663 1663 link_format=link_format+'+hovercard')
1664 1664 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1665 1665 log.debug('processed !pr pattern')
1666 1666
1667 1667 return new_text, issues_data
1668 1668
1669 1669
1670 1670 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1671 1671 """
1672 1672 Parses given text message and makes proper links.
1673 1673 issues are linked to given issue-server, and rest is a commit link
1674 1674 """
1675 1675
1676 1676 def escaper(_text):
1677 1677 return _text.replace('<', '&lt;').replace('>', '&gt;')
1678 1678
1679 1679 new_text = escaper(commit_text)
1680 1680
1681 1681 # extract http/https links and make them real urls
1682 1682 new_text = urlify_text(new_text, safe=False)
1683 1683
1684 1684 # urlify commits - extract commit ids and make link out of them, if we have
1685 1685 # the scope of repository present.
1686 1686 if repository:
1687 1687 new_text = urlify_commits(new_text, repository)
1688 1688
1689 1689 # process issue tracker patterns
1690 1690 new_text, issues = process_patterns(new_text, repository or '',
1691 1691 active_entries=active_pattern_entries)
1692 1692
1693 1693 return literal(new_text)
1694 1694
1695 1695
1696 1696 def render_binary(repo_name, file_obj):
1697 1697 """
1698 1698 Choose how to render a binary file
1699 1699 """
1700 1700
1701 1701 # unicode
1702 1702 filename = file_obj.name
1703 1703
1704 1704 # images
1705 1705 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1706 1706 if fnmatch.fnmatch(filename, pat=ext):
1707 1707 src = route_path(
1708 1708 'repo_file_raw', repo_name=repo_name,
1709 1709 commit_id=file_obj.commit.raw_id,
1710 1710 f_path=file_obj.path)
1711 1711
1712 1712 return literal(
1713 1713 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1714 1714
1715 1715
1716 1716 def renderer_from_filename(filename, exclude=None):
1717 1717 """
1718 1718 choose a renderer based on filename, this works only for text based files
1719 1719 """
1720 1720
1721 1721 # ipython
1722 1722 for ext in ['*.ipynb']:
1723 1723 if fnmatch.fnmatch(filename, pat=ext):
1724 1724 return 'jupyter'
1725 1725
1726 1726 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1727 1727 if is_markup:
1728 1728 return is_markup
1729 1729 return None
1730 1730
1731 1731
1732 1732 def render(source, renderer='rst', mentions=False, relative_urls=None,
1733 1733 repo_name=None, active_pattern_entries=None):
1734 1734
1735 1735 def maybe_convert_relative_links(html_source):
1736 1736 if relative_urls:
1737 1737 return relative_links(html_source, relative_urls)
1738 1738 return html_source
1739 1739
1740 1740 if renderer == 'plain':
1741 1741 return literal(
1742 1742 MarkupRenderer.plain(source, leading_newline=False))
1743 1743
1744 1744 elif renderer == 'rst':
1745 1745 if repo_name:
1746 1746 # process patterns on comments if we pass in repo name
1747 1747 source, issues = process_patterns(
1748 1748 source, repo_name, link_format='rst',
1749 1749 active_entries=active_pattern_entries)
1750 1750
1751 1751 return literal(
1752 1752 '<div class="rst-block">%s</div>' %
1753 1753 maybe_convert_relative_links(
1754 1754 MarkupRenderer.rst(source, mentions=mentions)))
1755 1755
1756 1756 elif renderer == 'markdown':
1757 1757 if repo_name:
1758 1758 # process patterns on comments if we pass in repo name
1759 1759 source, issues = process_patterns(
1760 1760 source, repo_name, link_format='markdown',
1761 1761 active_entries=active_pattern_entries)
1762 1762
1763 1763 return literal(
1764 1764 '<div class="markdown-block">%s</div>' %
1765 1765 maybe_convert_relative_links(
1766 1766 MarkupRenderer.markdown(source, flavored=True,
1767 1767 mentions=mentions)))
1768 1768
1769 1769 elif renderer == 'jupyter':
1770 1770 return literal(
1771 1771 '<div class="ipynb">%s</div>' %
1772 1772 maybe_convert_relative_links(
1773 1773 MarkupRenderer.jupyter(source)))
1774 1774
1775 1775 # None means just show the file-source
1776 1776 return None
1777 1777
1778 1778
1779 1779 def commit_status(repo, commit_id):
1780 1780 return ChangesetStatusModel().get_status(repo, commit_id)
1781 1781
1782 1782
1783 1783 def commit_status_lbl(commit_status):
1784 1784 return dict(ChangesetStatus.STATUSES).get(commit_status)
1785 1785
1786 1786
1787 1787 def commit_time(repo_name, commit_id):
1788 1788 repo = Repository.get_by_repo_name(repo_name)
1789 1789 commit = repo.get_commit(commit_id=commit_id)
1790 1790 return commit.date
1791 1791
1792 1792
1793 1793 def get_permission_name(key):
1794 1794 return dict(Permission.PERMS).get(key)
1795 1795
1796 1796
1797 1797 def journal_filter_help(request):
1798 1798 _ = request.translate
1799 1799 from rhodecode.lib.audit_logger import ACTIONS
1800 1800 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1801 1801
1802 1802 return _(
1803 1803 'Example filter terms:\n' +
1804 1804 ' repository:vcs\n' +
1805 1805 ' username:marcin\n' +
1806 1806 ' username:(NOT marcin)\n' +
1807 1807 ' action:*push*\n' +
1808 1808 ' ip:127.0.0.1\n' +
1809 1809 ' date:20120101\n' +
1810 1810 ' date:[20120101100000 TO 20120102]\n' +
1811 1811 '\n' +
1812 1812 'Actions: {actions}\n' +
1813 1813 '\n' +
1814 1814 'Generate wildcards using \'*\' character:\n' +
1815 1815 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1816 1816 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1817 1817 '\n' +
1818 1818 'Optional AND / OR operators in queries\n' +
1819 1819 ' "repository:vcs OR repository:test"\n' +
1820 1820 ' "username:test AND repository:test*"\n'
1821 1821 ).format(actions=actions)
1822 1822
1823 1823
1824 1824 def not_mapped_error(repo_name):
1825 1825 from rhodecode.translation import _
1826 1826 flash(_('%s repository is not mapped to db perhaps'
1827 1827 ' it was created or renamed from the filesystem'
1828 1828 ' please run the application again'
1829 1829 ' in order to rescan repositories') % repo_name, category='error')
1830 1830
1831 1831
1832 1832 def ip_range(ip_addr):
1833 1833 from rhodecode.model.db import UserIpMap
1834 1834 s, e = UserIpMap._get_ip_range(ip_addr)
1835 1835 return '%s - %s' % (s, e)
1836 1836
1837 1837
1838 1838 def form(url, method='post', needs_csrf_token=True, **attrs):
1839 1839 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1840 1840 if method.lower() != 'get' and needs_csrf_token:
1841 1841 raise Exception(
1842 1842 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1843 1843 'CSRF token. If the endpoint does not require such token you can ' +
1844 1844 'explicitly set the parameter needs_csrf_token to false.')
1845 1845
1846 1846 return insecure_form(url, method=method, **attrs)
1847 1847
1848 1848
1849 1849 def secure_form(form_url, method="POST", multipart=False, **attrs):
1850 1850 """Start a form tag that points the action to an url. This
1851 1851 form tag will also include the hidden field containing
1852 1852 the auth token.
1853 1853
1854 1854 The url options should be given either as a string, or as a
1855 1855 ``url()`` function. The method for the form defaults to POST.
1856 1856
1857 1857 Options:
1858 1858
1859 1859 ``multipart``
1860 1860 If set to True, the enctype is set to "multipart/form-data".
1861 1861 ``method``
1862 1862 The method to use when submitting the form, usually either
1863 1863 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1864 1864 hidden input with name _method is added to simulate the verb
1865 1865 over POST.
1866 1866
1867 1867 """
1868 1868
1869 1869 if 'request' in attrs:
1870 1870 session = attrs['request'].session
1871 1871 del attrs['request']
1872 1872 else:
1873 1873 raise ValueError(
1874 1874 'Calling this form requires request= to be passed as argument')
1875 1875
1876 1876 _form = insecure_form(form_url, method, multipart, **attrs)
1877 1877 token = literal(
1878 1878 '<input type="hidden" name="{}" value="{}">'.format(
1879 1879 csrf_token_key, get_csrf_token(session)))
1880 1880
1881 1881 return literal("%s\n%s" % (_form, token))
1882 1882
1883 1883
1884 1884 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1885 1885 select_html = select(name, selected, options, **attrs)
1886 1886
1887 1887 select2 = """
1888 1888 <script>
1889 1889 $(document).ready(function() {
1890 1890 $('#%s').select2({
1891 1891 containerCssClass: 'drop-menu %s',
1892 1892 dropdownCssClass: 'drop-menu-dropdown',
1893 1893 dropdownAutoWidth: true%s
1894 1894 });
1895 1895 });
1896 1896 </script>
1897 1897 """
1898 1898
1899 1899 filter_option = """,
1900 1900 minimumResultsForSearch: -1
1901 1901 """
1902 1902 input_id = attrs.get('id') or name
1903 1903 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1904 1904 filter_enabled = "" if enable_filter else filter_option
1905 1905 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1906 1906
1907 1907 return literal(select_html+select_script)
1908 1908
1909 1909
1910 1910 def get_visual_attr(tmpl_context_var, attr_name):
1911 1911 """
1912 1912 A safe way to get a variable from visual variable of template context
1913 1913
1914 1914 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1915 1915 :param attr_name: name of the attribute we fetch from the c.visual
1916 1916 """
1917 1917 visual = getattr(tmpl_context_var, 'visual', None)
1918 1918 if not visual:
1919 1919 return
1920 1920 else:
1921 1921 return getattr(visual, attr_name, None)
1922 1922
1923 1923
1924 1924 def get_last_path_part(file_node):
1925 1925 if not file_node.path:
1926 1926 return u'/'
1927 1927
1928 1928 path = safe_unicode(file_node.path.split('/')[-1])
1929 1929 return u'../' + path
1930 1930
1931 1931
1932 1932 def route_url(*args, **kwargs):
1933 1933 """
1934 1934 Wrapper around pyramids `route_url` (fully qualified url) function.
1935 1935 """
1936 1936 req = get_current_request()
1937 1937 return req.route_url(*args, **kwargs)
1938 1938
1939 1939
1940 1940 def route_path(*args, **kwargs):
1941 1941 """
1942 1942 Wrapper around pyramids `route_path` function.
1943 1943 """
1944 1944 req = get_current_request()
1945 1945 return req.route_path(*args, **kwargs)
1946 1946
1947 1947
1948 1948 def route_path_or_none(*args, **kwargs):
1949 1949 try:
1950 1950 return route_path(*args, **kwargs)
1951 1951 except KeyError:
1952 1952 return None
1953 1953
1954 1954
1955 1955 def current_route_path(request, **kw):
1956 1956 new_args = request.GET.mixed()
1957 1957 new_args.update(kw)
1958 1958 return request.current_route_path(_query=new_args)
1959 1959
1960 1960
1961 1961 def curl_api_example(method, args):
1962 1962 args_json = json.dumps(OrderedDict([
1963 1963 ('id', 1),
1964 1964 ('auth_token', 'SECRET'),
1965 1965 ('method', method),
1966 1966 ('args', args)
1967 1967 ]))
1968 1968
1969 1969 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1970 1970 api_url=route_url('apiv2'),
1971 1971 args_json=args_json
1972 1972 )
1973 1973
1974 1974
1975 1975 def api_call_example(method, args):
1976 1976 """
1977 1977 Generates an API call example via CURL
1978 1978 """
1979 1979 curl_call = curl_api_example(method, args)
1980 1980
1981 1981 return literal(
1982 1982 curl_call +
1983 1983 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1984 1984 "and needs to be of `api calls` role."
1985 1985 .format(token_url=route_url('my_account_auth_tokens')))
1986 1986
1987 1987
1988 1988 def notification_description(notification, request):
1989 1989 """
1990 1990 Generate notification human readable description based on notification type
1991 1991 """
1992 1992 from rhodecode.model.notification import NotificationModel
1993 1993 return NotificationModel().make_description(
1994 1994 notification, translate=request.translate)
1995 1995
1996 1996
1997 1997 def go_import_header(request, db_repo=None):
1998 1998 """
1999 1999 Creates a header for go-import functionality in Go Lang
2000 2000 """
2001 2001
2002 2002 if not db_repo:
2003 2003 return
2004 2004 if 'go-get' not in request.GET:
2005 2005 return
2006 2006
2007 2007 clone_url = db_repo.clone_url()
2008 2008 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2009 2009 # we have a repo and go-get flag,
2010 2010 return literal('<meta name="go-import" content="{} {} {}">'.format(
2011 2011 prefix, db_repo.repo_type, clone_url))
2012 2012
2013 2013
2014 2014 def reviewer_as_json(*args, **kwargs):
2015 2015 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2016 2016 return _reviewer_as_json(*args, **kwargs)
2017 2017
2018 2018
2019 2019 def get_repo_view_type(request):
2020 2020 route_name = request.matched_route.name
2021 2021 route_to_view_type = {
2022 2022 'repo_changelog': 'commits',
2023 2023 'repo_commits': 'commits',
2024 2024 'repo_files': 'files',
2025 2025 'repo_summary': 'summary',
2026 2026 'repo_commit': 'commit'
2027 2027 }
2028 2028
2029 2029 return route_to_view_type.get(route_name)
2030 2030
2031 2031
2032 2032 def is_active(menu_entry, selected):
2033 2033 """
2034 2034 Returns active class for selecting menus in templates
2035 2035 <li class=${h.is_active('settings', current_active)}></li>
2036 2036 """
2037 2037 if not isinstance(menu_entry, list):
2038 2038 menu_entry = [menu_entry]
2039 2039
2040 2040 if selected in menu_entry:
2041 2041 return "active"
@@ -1,5645 +1,5663 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 extra_sort_num = '1' # default
107 107
108 108 # NOTE(dan): inactive duplicates goes last
109 109 if getattr(obj, 'duplicate_perm', None):
110 110 extra_sort_num = '9'
111 111 return prefix + extra_sort_num + obj.username
112 112
113 113
114 114 def display_user_group_sort(obj):
115 115 """
116 116 Sort function used to sort permissions in .permissions() function of
117 117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 118 of all other resources
119 119 """
120 120
121 121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 122 return prefix + obj.users_group_name
123 123
124 124
125 125 def _hash_key(k):
126 126 return sha1_safe(k)
127 127
128 128
129 129 def in_filter_generator(qry, items, limit=500):
130 130 """
131 131 Splits IN() into multiple with OR
132 132 e.g.::
133 133 cnt = Repository.query().filter(
134 134 or_(
135 135 *in_filter_generator(Repository.repo_id, range(100000))
136 136 )).count()
137 137 """
138 138 if not items:
139 139 # empty list will cause empty query which might cause security issues
140 140 # this can lead to hidden unpleasant results
141 141 items = [-1]
142 142
143 143 parts = []
144 144 for chunk in xrange(0, len(items), limit):
145 145 parts.append(
146 146 qry.in_(items[chunk: chunk + limit])
147 147 )
148 148
149 149 return parts
150 150
151 151
152 152 base_table_args = {
153 153 'extend_existing': True,
154 154 'mysql_engine': 'InnoDB',
155 155 'mysql_charset': 'utf8',
156 156 'sqlite_autoincrement': True
157 157 }
158 158
159 159
160 160 class EncryptedTextValue(TypeDecorator):
161 161 """
162 162 Special column for encrypted long text data, use like::
163 163
164 164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 165
166 166 This column is intelligent so if value is in unencrypted form it return
167 167 unencrypted form, but on save it always encrypts
168 168 """
169 169 impl = Text
170 170
171 171 def process_bind_param(self, value, dialect):
172 172 """
173 173 Setter for storing value
174 174 """
175 175 import rhodecode
176 176 if not value:
177 177 return value
178 178
179 179 # protect against double encrypting if values is already encrypted
180 180 if value.startswith('enc$aes$') \
181 181 or value.startswith('enc$aes_hmac$') \
182 182 or value.startswith('enc2$'):
183 183 raise ValueError('value needs to be in unencrypted format, '
184 184 'ie. not starting with enc$ or enc2$')
185 185
186 186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 187 if algo == 'aes':
188 188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 189 elif algo == 'fernet':
190 190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 191 else:
192 192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193 193
194 194 def process_result_value(self, value, dialect):
195 195 """
196 196 Getter for retrieving value
197 197 """
198 198
199 199 import rhodecode
200 200 if not value:
201 201 return value
202 202
203 203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 205 if algo == 'aes':
206 206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 207 elif algo == 'fernet':
208 208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 209 else:
210 210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 211 return decrypted_data
212 212
213 213
214 214 class BaseModel(object):
215 215 """
216 216 Base Model for all classes
217 217 """
218 218
219 219 @classmethod
220 220 def _get_keys(cls):
221 221 """return column names for this model """
222 222 return class_mapper(cls).c.keys()
223 223
224 224 def get_dict(self):
225 225 """
226 226 return dict with keys and values corresponding
227 227 to this model data """
228 228
229 229 d = {}
230 230 for k in self._get_keys():
231 231 d[k] = getattr(self, k)
232 232
233 233 # also use __json__() if present to get additional fields
234 234 _json_attr = getattr(self, '__json__', None)
235 235 if _json_attr:
236 236 # update with attributes from __json__
237 237 if callable(_json_attr):
238 238 _json_attr = _json_attr()
239 239 for k, val in _json_attr.iteritems():
240 240 d[k] = val
241 241 return d
242 242
243 243 def get_appstruct(self):
244 244 """return list with keys and values tuples corresponding
245 245 to this model data """
246 246
247 247 lst = []
248 248 for k in self._get_keys():
249 249 lst.append((k, getattr(self, k),))
250 250 return lst
251 251
252 252 def populate_obj(self, populate_dict):
253 253 """populate model with data from given populate_dict"""
254 254
255 255 for k in self._get_keys():
256 256 if k in populate_dict:
257 257 setattr(self, k, populate_dict[k])
258 258
259 259 @classmethod
260 260 def query(cls):
261 261 return Session().query(cls)
262 262
263 263 @classmethod
264 264 def get(cls, id_):
265 265 if id_:
266 266 return cls.query().get(id_)
267 267
268 268 @classmethod
269 269 def get_or_404(cls, id_):
270 270 from pyramid.httpexceptions import HTTPNotFound
271 271
272 272 try:
273 273 id_ = int(id_)
274 274 except (TypeError, ValueError):
275 275 raise HTTPNotFound()
276 276
277 277 res = cls.query().get(id_)
278 278 if not res:
279 279 raise HTTPNotFound()
280 280 return res
281 281
282 282 @classmethod
283 283 def getAll(cls):
284 284 # deprecated and left for backward compatibility
285 285 return cls.get_all()
286 286
287 287 @classmethod
288 288 def get_all(cls):
289 289 return cls.query().all()
290 290
291 291 @classmethod
292 292 def delete(cls, id_):
293 293 obj = cls.query().get(id_)
294 294 Session().delete(obj)
295 295
296 296 @classmethod
297 297 def identity_cache(cls, session, attr_name, value):
298 298 exist_in_session = []
299 299 for (item_cls, pkey), instance in session.identity_map.items():
300 300 if cls == item_cls and getattr(instance, attr_name) == value:
301 301 exist_in_session.append(instance)
302 302 if exist_in_session:
303 303 if len(exist_in_session) == 1:
304 304 return exist_in_session[0]
305 305 log.exception(
306 306 'multiple objects with attr %s and '
307 307 'value %s found with same name: %r',
308 308 attr_name, value, exist_in_session)
309 309
310 310 def __repr__(self):
311 311 if hasattr(self, '__unicode__'):
312 312 # python repr needs to return str
313 313 try:
314 314 return safe_str(self.__unicode__())
315 315 except UnicodeDecodeError:
316 316 pass
317 317 return '<DB:%s>' % (self.__class__.__name__)
318 318
319 319
320 320 class RhodeCodeSetting(Base, BaseModel):
321 321 __tablename__ = 'rhodecode_settings'
322 322 __table_args__ = (
323 323 UniqueConstraint('app_settings_name'),
324 324 base_table_args
325 325 )
326 326
327 327 SETTINGS_TYPES = {
328 328 'str': safe_str,
329 329 'int': safe_int,
330 330 'unicode': safe_unicode,
331 331 'bool': str2bool,
332 332 'list': functools.partial(aslist, sep=',')
333 333 }
334 334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 335 GLOBAL_CONF_KEY = 'app_settings'
336 336
337 337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341 341
342 342 def __init__(self, key='', val='', type='unicode'):
343 343 self.app_settings_name = key
344 344 self.app_settings_type = type
345 345 self.app_settings_value = val
346 346
347 347 @validates('_app_settings_value')
348 348 def validate_settings_value(self, key, val):
349 349 assert type(val) == unicode
350 350 return val
351 351
352 352 @hybrid_property
353 353 def app_settings_value(self):
354 354 v = self._app_settings_value
355 355 _type = self.app_settings_type
356 356 if _type:
357 357 _type = self.app_settings_type.split('.')[0]
358 358 # decode the encrypted value
359 359 if 'encrypted' in self.app_settings_type:
360 360 cipher = EncryptedTextValue()
361 361 v = safe_unicode(cipher.process_result_value(v, None))
362 362
363 363 converter = self.SETTINGS_TYPES.get(_type) or \
364 364 self.SETTINGS_TYPES['unicode']
365 365 return converter(v)
366 366
367 367 @app_settings_value.setter
368 368 def app_settings_value(self, val):
369 369 """
370 370 Setter that will always make sure we use unicode in app_settings_value
371 371
372 372 :param val:
373 373 """
374 374 val = safe_unicode(val)
375 375 # encode the encrypted value
376 376 if 'encrypted' in self.app_settings_type:
377 377 cipher = EncryptedTextValue()
378 378 val = safe_unicode(cipher.process_bind_param(val, None))
379 379 self._app_settings_value = val
380 380
381 381 @hybrid_property
382 382 def app_settings_type(self):
383 383 return self._app_settings_type
384 384
385 385 @app_settings_type.setter
386 386 def app_settings_type(self, val):
387 387 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 388 raise Exception('type must be one of %s got %s'
389 389 % (self.SETTINGS_TYPES.keys(), val))
390 390 self._app_settings_type = val
391 391
392 392 @classmethod
393 393 def get_by_prefix(cls, prefix):
394 394 return RhodeCodeSetting.query()\
395 395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 396 .all()
397 397
398 398 def __unicode__(self):
399 399 return u"<%s('%s:%s[%s]')>" % (
400 400 self.__class__.__name__,
401 401 self.app_settings_name, self.app_settings_value,
402 402 self.app_settings_type
403 403 )
404 404
405 405
406 406 class RhodeCodeUi(Base, BaseModel):
407 407 __tablename__ = 'rhodecode_ui'
408 408 __table_args__ = (
409 409 UniqueConstraint('ui_key'),
410 410 base_table_args
411 411 )
412 412
413 413 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 414 # HG
415 415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 416 HOOK_PULL = 'outgoing.pull_logger'
417 417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 419 HOOK_PUSH = 'changegroup.push_logger'
420 420 HOOK_PUSH_KEY = 'pushkey.key_push'
421 421
422 422 HOOKS_BUILTIN = [
423 423 HOOK_PRE_PULL,
424 424 HOOK_PULL,
425 425 HOOK_PRE_PUSH,
426 426 HOOK_PRETX_PUSH,
427 427 HOOK_PUSH,
428 428 HOOK_PUSH_KEY,
429 429 ]
430 430
431 431 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 432 # git part is currently hardcoded.
433 433
434 434 # SVN PATTERNS
435 435 SVN_BRANCH_ID = 'vcs_svn_branch'
436 436 SVN_TAG_ID = 'vcs_svn_tag'
437 437
438 438 ui_id = Column(
439 439 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 440 primary_key=True)
441 441 ui_section = Column(
442 442 "ui_section", String(255), nullable=True, unique=None, default=None)
443 443 ui_key = Column(
444 444 "ui_key", String(255), nullable=True, unique=None, default=None)
445 445 ui_value = Column(
446 446 "ui_value", String(255), nullable=True, unique=None, default=None)
447 447 ui_active = Column(
448 448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449 449
450 450 def __repr__(self):
451 451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 452 self.ui_key, self.ui_value)
453 453
454 454
455 455 class RepoRhodeCodeSetting(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_settings'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'app_settings_name', 'repository_id',
460 460 name='uq_repo_rhodecode_setting_name_repo_id'),
461 461 base_table_args
462 462 )
463 463
464 464 repository_id = Column(
465 465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 466 nullable=False)
467 467 app_settings_id = Column(
468 468 "app_settings_id", Integer(), nullable=False, unique=True,
469 469 default=None, primary_key=True)
470 470 app_settings_name = Column(
471 471 "app_settings_name", String(255), nullable=True, unique=None,
472 472 default=None)
473 473 _app_settings_value = Column(
474 474 "app_settings_value", String(4096), nullable=True, unique=None,
475 475 default=None)
476 476 _app_settings_type = Column(
477 477 "app_settings_type", String(255), nullable=True, unique=None,
478 478 default=None)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __init__(self, repository_id, key='', val='', type='unicode'):
483 483 self.repository_id = repository_id
484 484 self.app_settings_name = key
485 485 self.app_settings_type = type
486 486 self.app_settings_value = val
487 487
488 488 @validates('_app_settings_value')
489 489 def validate_settings_value(self, key, val):
490 490 assert type(val) == unicode
491 491 return val
492 492
493 493 @hybrid_property
494 494 def app_settings_value(self):
495 495 v = self._app_settings_value
496 496 type_ = self.app_settings_type
497 497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 499 return converter(v)
500 500
501 501 @app_settings_value.setter
502 502 def app_settings_value(self, val):
503 503 """
504 504 Setter that will always make sure we use unicode in app_settings_value
505 505
506 506 :param val:
507 507 """
508 508 self._app_settings_value = safe_unicode(val)
509 509
510 510 @hybrid_property
511 511 def app_settings_type(self):
512 512 return self._app_settings_type
513 513
514 514 @app_settings_type.setter
515 515 def app_settings_type(self, val):
516 516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 517 if val not in SETTINGS_TYPES:
518 518 raise Exception('type must be one of %s got %s'
519 519 % (SETTINGS_TYPES.keys(), val))
520 520 self._app_settings_type = val
521 521
522 522 def __unicode__(self):
523 523 return u"<%s('%s:%s:%s[%s]')>" % (
524 524 self.__class__.__name__, self.repository.repo_name,
525 525 self.app_settings_name, self.app_settings_value,
526 526 self.app_settings_type
527 527 )
528 528
529 529
530 530 class RepoRhodeCodeUi(Base, BaseModel):
531 531 __tablename__ = 'repo_rhodecode_ui'
532 532 __table_args__ = (
533 533 UniqueConstraint(
534 534 'repository_id', 'ui_section', 'ui_key',
535 535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 536 base_table_args
537 537 )
538 538
539 539 repository_id = Column(
540 540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 541 nullable=False)
542 542 ui_id = Column(
543 543 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 544 primary_key=True)
545 545 ui_section = Column(
546 546 "ui_section", String(255), nullable=True, unique=None, default=None)
547 547 ui_key = Column(
548 548 "ui_key", String(255), nullable=True, unique=None, default=None)
549 549 ui_value = Column(
550 550 "ui_value", String(255), nullable=True, unique=None, default=None)
551 551 ui_active = Column(
552 552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553 553
554 554 repository = relationship('Repository')
555 555
556 556 def __repr__(self):
557 557 return '<%s[%s:%s]%s=>%s]>' % (
558 558 self.__class__.__name__, self.repository.repo_name,
559 559 self.ui_section, self.ui_key, self.ui_value)
560 560
561 561
562 562 class User(Base, BaseModel):
563 563 __tablename__ = 'users'
564 564 __table_args__ = (
565 565 UniqueConstraint('username'), UniqueConstraint('email'),
566 566 Index('u_username_idx', 'username'),
567 567 Index('u_email_idx', 'email'),
568 568 base_table_args
569 569 )
570 570
571 571 DEFAULT_USER = 'default'
572 572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574 574
575 575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 576 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 577 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586 586
587 587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593 593
594 594 user_log = relationship('UserLog')
595 595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596 596
597 597 repositories = relationship('Repository')
598 598 repository_groups = relationship('RepoGroup')
599 599 user_groups = relationship('UserGroup')
600 600
601 601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603 603
604 604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 607
608 608 group_member = relationship('UserGroupMember', cascade='all')
609 609
610 610 notifications = relationship('UserNotification', cascade='all')
611 611 # notifications assigned to this user
612 612 user_created_notifications = relationship('Notification', cascade='all')
613 613 # comments created by this user
614 614 user_comments = relationship('ChangesetComment', cascade='all')
615 615 # user profile extra info
616 616 user_emails = relationship('UserEmailMap', cascade='all')
617 617 user_ip_map = relationship('UserIpMap', cascade='all')
618 618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620 620
621 621 # gists
622 622 user_gists = relationship('Gist', cascade='all')
623 623 # user pull requests
624 624 user_pull_requests = relationship('PullRequest', cascade='all')
625 625
626 626 # external identities
627 627 external_identities = relationship(
628 628 'ExternalIdentity',
629 629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 630 cascade='all')
631 631 # review rules
632 632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633 633
634 634 # artifacts owned
635 635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636 636
637 637 # no cascade, set NULL
638 638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639 639
640 640 def __unicode__(self):
641 641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 642 self.user_id, self.username)
643 643
644 644 @hybrid_property
645 645 def email(self):
646 646 return self._email
647 647
648 648 @email.setter
649 649 def email(self, val):
650 650 self._email = val.lower() if val else None
651 651
652 652 @hybrid_property
653 653 def first_name(self):
654 654 from rhodecode.lib import helpers as h
655 655 if self.name:
656 656 return h.escape(self.name)
657 657 return self.name
658 658
659 659 @hybrid_property
660 660 def last_name(self):
661 661 from rhodecode.lib import helpers as h
662 662 if self.lastname:
663 663 return h.escape(self.lastname)
664 664 return self.lastname
665 665
666 666 @hybrid_property
667 667 def api_key(self):
668 668 """
669 669 Fetch if exist an auth-token with role ALL connected to this user
670 670 """
671 671 user_auth_token = UserApiKeys.query()\
672 672 .filter(UserApiKeys.user_id == self.user_id)\
673 673 .filter(or_(UserApiKeys.expires == -1,
674 674 UserApiKeys.expires >= time.time()))\
675 675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 676 if user_auth_token:
677 677 user_auth_token = user_auth_token.api_key
678 678
679 679 return user_auth_token
680 680
681 681 @api_key.setter
682 682 def api_key(self, val):
683 683 # don't allow to set API key this is deprecated for now
684 684 self._api_key = None
685 685
686 686 @property
687 687 def reviewer_pull_requests(self):
688 688 return PullRequestReviewers.query() \
689 689 .options(joinedload(PullRequestReviewers.pull_request)) \
690 690 .filter(PullRequestReviewers.user_id == self.user_id) \
691 691 .all()
692 692
693 693 @property
694 694 def firstname(self):
695 695 # alias for future
696 696 return self.name
697 697
698 698 @property
699 699 def emails(self):
700 700 other = UserEmailMap.query()\
701 701 .filter(UserEmailMap.user == self) \
702 702 .order_by(UserEmailMap.email_id.asc()) \
703 703 .all()
704 704 return [self.email] + [x.email for x in other]
705 705
706 706 def emails_cached(self):
707 707 emails = UserEmailMap.query()\
708 708 .filter(UserEmailMap.user == self) \
709 709 .order_by(UserEmailMap.email_id.asc())
710 710
711 711 emails = emails.options(
712 712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 713 )
714 714
715 715 return [self.email] + [x.email for x in emails]
716 716
717 717 @property
718 718 def auth_tokens(self):
719 719 auth_tokens = self.get_auth_tokens()
720 720 return [x.api_key for x in auth_tokens]
721 721
722 722 def get_auth_tokens(self):
723 723 return UserApiKeys.query()\
724 724 .filter(UserApiKeys.user == self)\
725 725 .order_by(UserApiKeys.user_api_key_id.asc())\
726 726 .all()
727 727
728 728 @LazyProperty
729 729 def feed_token(self):
730 730 return self.get_feed_token()
731 731
732 732 def get_feed_token(self, cache=True):
733 733 feed_tokens = UserApiKeys.query()\
734 734 .filter(UserApiKeys.user == self)\
735 735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 736 if cache:
737 737 feed_tokens = feed_tokens.options(
738 738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739 739
740 740 feed_tokens = feed_tokens.all()
741 741 if feed_tokens:
742 742 return feed_tokens[0].api_key
743 743 return 'NO_FEED_TOKEN_AVAILABLE'
744 744
745 745 @LazyProperty
746 746 def artifact_token(self):
747 747 return self.get_artifact_token()
748 748
749 749 def get_artifact_token(self, cache=True):
750 750 artifacts_tokens = UserApiKeys.query()\
751 751 .filter(UserApiKeys.user == self)\
752 752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
753 753 if cache:
754 754 artifacts_tokens = artifacts_tokens.options(
755 755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
756 756
757 757 artifacts_tokens = artifacts_tokens.all()
758 758 if artifacts_tokens:
759 759 return artifacts_tokens[0].api_key
760 760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
761 761
762 762 @classmethod
763 763 def get(cls, user_id, cache=False):
764 764 if not user_id:
765 765 return
766 766
767 767 user = cls.query()
768 768 if cache:
769 769 user = user.options(
770 770 FromCache("sql_cache_short", "get_users_%s" % user_id))
771 771 return user.get(user_id)
772 772
773 773 @classmethod
774 774 def extra_valid_auth_tokens(cls, user, role=None):
775 775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
776 776 .filter(or_(UserApiKeys.expires == -1,
777 777 UserApiKeys.expires >= time.time()))
778 778 if role:
779 779 tokens = tokens.filter(or_(UserApiKeys.role == role,
780 780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
781 781 return tokens.all()
782 782
783 783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
784 784 from rhodecode.lib import auth
785 785
786 786 log.debug('Trying to authenticate user: %s via auth-token, '
787 787 'and roles: %s', self, roles)
788 788
789 789 if not auth_token:
790 790 return False
791 791
792 792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
793 793 tokens_q = UserApiKeys.query()\
794 794 .filter(UserApiKeys.user_id == self.user_id)\
795 795 .filter(or_(UserApiKeys.expires == -1,
796 796 UserApiKeys.expires >= time.time()))
797 797
798 798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
799 799
800 800 crypto_backend = auth.crypto_backend()
801 801 enc_token_map = {}
802 802 plain_token_map = {}
803 803 for token in tokens_q:
804 804 if token.api_key.startswith(crypto_backend.ENC_PREF):
805 805 enc_token_map[token.api_key] = token
806 806 else:
807 807 plain_token_map[token.api_key] = token
808 808 log.debug(
809 809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
810 810 len(plain_token_map), len(enc_token_map))
811 811
812 812 # plain token match comes first
813 813 match = plain_token_map.get(auth_token)
814 814
815 815 # check encrypted tokens now
816 816 if not match:
817 817 for token_hash, token in enc_token_map.items():
818 818 # NOTE(marcink): this is expensive to calculate, but most secure
819 819 if crypto_backend.hash_check(auth_token, token_hash):
820 820 match = token
821 821 break
822 822
823 823 if match:
824 824 log.debug('Found matching token %s', match)
825 825 if match.repo_id:
826 826 log.debug('Found scope, checking for scope match of token %s', match)
827 827 if match.repo_id == scope_repo_id:
828 828 return True
829 829 else:
830 830 log.debug(
831 831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
832 832 'and calling scope is:%s, skipping further checks',
833 833 match.repo, scope_repo_id)
834 834 return False
835 835 else:
836 836 return True
837 837
838 838 return False
839 839
840 840 @property
841 841 def ip_addresses(self):
842 842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
843 843 return [x.ip_addr for x in ret]
844 844
845 845 @property
846 846 def username_and_name(self):
847 847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
848 848
849 849 @property
850 850 def username_or_name_or_email(self):
851 851 full_name = self.full_name if self.full_name is not ' ' else None
852 852 return self.username or full_name or self.email
853 853
854 854 @property
855 855 def full_name(self):
856 856 return '%s %s' % (self.first_name, self.last_name)
857 857
858 858 @property
859 859 def full_name_or_username(self):
860 860 return ('%s %s' % (self.first_name, self.last_name)
861 861 if (self.first_name and self.last_name) else self.username)
862 862
863 863 @property
864 864 def full_contact(self):
865 865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
866 866
867 867 @property
868 868 def short_contact(self):
869 869 return '%s %s' % (self.first_name, self.last_name)
870 870
871 871 @property
872 872 def is_admin(self):
873 873 return self.admin
874 874
875 875 @property
876 876 def language(self):
877 877 return self.user_data.get('language')
878 878
879 879 def AuthUser(self, **kwargs):
880 880 """
881 881 Returns instance of AuthUser for this user
882 882 """
883 883 from rhodecode.lib.auth import AuthUser
884 884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
885 885
886 886 @hybrid_property
887 887 def user_data(self):
888 888 if not self._user_data:
889 889 return {}
890 890
891 891 try:
892 892 return json.loads(self._user_data)
893 893 except TypeError:
894 894 return {}
895 895
896 896 @user_data.setter
897 897 def user_data(self, val):
898 898 if not isinstance(val, dict):
899 899 raise Exception('user_data must be dict, got %s' % type(val))
900 900 try:
901 901 self._user_data = json.dumps(val)
902 902 except Exception:
903 903 log.error(traceback.format_exc())
904 904
905 905 @classmethod
906 906 def get_by_username(cls, username, case_insensitive=False,
907 907 cache=False, identity_cache=False):
908 908 session = Session()
909 909
910 910 if case_insensitive:
911 911 q = cls.query().filter(
912 912 func.lower(cls.username) == func.lower(username))
913 913 else:
914 914 q = cls.query().filter(cls.username == username)
915 915
916 916 if cache:
917 917 if identity_cache:
918 918 val = cls.identity_cache(session, 'username', username)
919 919 if val:
920 920 return val
921 921 else:
922 922 cache_key = "get_user_by_name_%s" % _hash_key(username)
923 923 q = q.options(
924 924 FromCache("sql_cache_short", cache_key))
925 925
926 926 return q.scalar()
927 927
928 928 @classmethod
929 929 def get_by_auth_token(cls, auth_token, cache=False):
930 930 q = UserApiKeys.query()\
931 931 .filter(UserApiKeys.api_key == auth_token)\
932 932 .filter(or_(UserApiKeys.expires == -1,
933 933 UserApiKeys.expires >= time.time()))
934 934 if cache:
935 935 q = q.options(
936 936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
937 937
938 938 match = q.first()
939 939 if match:
940 940 return match.user
941 941
942 942 @classmethod
943 943 def get_by_email(cls, email, case_insensitive=False, cache=False):
944 944
945 945 if case_insensitive:
946 946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
947 947
948 948 else:
949 949 q = cls.query().filter(cls.email == email)
950 950
951 951 email_key = _hash_key(email)
952 952 if cache:
953 953 q = q.options(
954 954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
955 955
956 956 ret = q.scalar()
957 957 if ret is None:
958 958 q = UserEmailMap.query()
959 959 # try fetching in alternate email map
960 960 if case_insensitive:
961 961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
962 962 else:
963 963 q = q.filter(UserEmailMap.email == email)
964 964 q = q.options(joinedload(UserEmailMap.user))
965 965 if cache:
966 966 q = q.options(
967 967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
968 968 ret = getattr(q.scalar(), 'user', None)
969 969
970 970 return ret
971 971
972 972 @classmethod
973 973 def get_from_cs_author(cls, author):
974 974 """
975 975 Tries to get User objects out of commit author string
976 976
977 977 :param author:
978 978 """
979 979 from rhodecode.lib.helpers import email, author_name
980 980 # Valid email in the attribute passed, see if they're in the system
981 981 _email = email(author)
982 982 if _email:
983 983 user = cls.get_by_email(_email, case_insensitive=True)
984 984 if user:
985 985 return user
986 986 # Maybe we can match by username?
987 987 _author = author_name(author)
988 988 user = cls.get_by_username(_author, case_insensitive=True)
989 989 if user:
990 990 return user
991 991
992 992 def update_userdata(self, **kwargs):
993 993 usr = self
994 994 old = usr.user_data
995 995 old.update(**kwargs)
996 996 usr.user_data = old
997 997 Session().add(usr)
998 998 log.debug('updated userdata with %s', kwargs)
999 999
1000 1000 def update_lastlogin(self):
1001 1001 """Update user lastlogin"""
1002 1002 self.last_login = datetime.datetime.now()
1003 1003 Session().add(self)
1004 1004 log.debug('updated user %s lastlogin', self.username)
1005 1005
1006 1006 def update_password(self, new_password):
1007 1007 from rhodecode.lib.auth import get_crypt_password
1008 1008
1009 1009 self.password = get_crypt_password(new_password)
1010 1010 Session().add(self)
1011 1011
1012 1012 @classmethod
1013 1013 def get_first_super_admin(cls):
1014 1014 user = User.query()\
1015 1015 .filter(User.admin == true()) \
1016 1016 .order_by(User.user_id.asc()) \
1017 1017 .first()
1018 1018
1019 1019 if user is None:
1020 1020 raise Exception('FATAL: Missing administrative account!')
1021 1021 return user
1022 1022
1023 1023 @classmethod
1024 1024 def get_all_super_admins(cls, only_active=False):
1025 1025 """
1026 1026 Returns all admin accounts sorted by username
1027 1027 """
1028 1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1029 1029 if only_active:
1030 1030 qry = qry.filter(User.active == true())
1031 1031 return qry.all()
1032 1032
1033 1033 @classmethod
1034 1034 def get_all_user_ids(cls, only_active=True):
1035 1035 """
1036 1036 Returns all users IDs
1037 1037 """
1038 1038 qry = Session().query(User.user_id)
1039 1039
1040 1040 if only_active:
1041 1041 qry = qry.filter(User.active == true())
1042 1042 return [x.user_id for x in qry]
1043 1043
1044 1044 @classmethod
1045 1045 def get_default_user(cls, cache=False, refresh=False):
1046 1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1047 1047 if user is None:
1048 1048 raise Exception('FATAL: Missing default account!')
1049 1049 if refresh:
1050 1050 # The default user might be based on outdated state which
1051 1051 # has been loaded from the cache.
1052 1052 # A call to refresh() ensures that the
1053 1053 # latest state from the database is used.
1054 1054 Session().refresh(user)
1055 1055 return user
1056 1056
1057 1057 @classmethod
1058 1058 def get_default_user_id(cls):
1059 1059 import rhodecode
1060 1060 return rhodecode.CONFIG['default_user_id']
1061 1061
1062 1062 def _get_default_perms(self, user, suffix=''):
1063 1063 from rhodecode.model.permission import PermissionModel
1064 1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1065 1065
1066 1066 def get_default_perms(self, suffix=''):
1067 1067 return self._get_default_perms(self, suffix)
1068 1068
1069 1069 def get_api_data(self, include_secrets=False, details='full'):
1070 1070 """
1071 1071 Common function for generating user related data for API
1072 1072
1073 1073 :param include_secrets: By default secrets in the API data will be replaced
1074 1074 by a placeholder value to prevent exposing this data by accident. In case
1075 1075 this data shall be exposed, set this flag to ``True``.
1076 1076
1077 1077 :param details: details can be 'basic|full' basic gives only a subset of
1078 1078 the available user information that includes user_id, name and emails.
1079 1079 """
1080 1080 user = self
1081 1081 user_data = self.user_data
1082 1082 data = {
1083 1083 'user_id': user.user_id,
1084 1084 'username': user.username,
1085 1085 'firstname': user.name,
1086 1086 'lastname': user.lastname,
1087 1087 'description': user.description,
1088 1088 'email': user.email,
1089 1089 'emails': user.emails,
1090 1090 }
1091 1091 if details == 'basic':
1092 1092 return data
1093 1093
1094 1094 auth_token_length = 40
1095 1095 auth_token_replacement = '*' * auth_token_length
1096 1096
1097 1097 extras = {
1098 1098 'auth_tokens': [auth_token_replacement],
1099 1099 'active': user.active,
1100 1100 'admin': user.admin,
1101 1101 'extern_type': user.extern_type,
1102 1102 'extern_name': user.extern_name,
1103 1103 'last_login': user.last_login,
1104 1104 'last_activity': user.last_activity,
1105 1105 'ip_addresses': user.ip_addresses,
1106 1106 'language': user_data.get('language')
1107 1107 }
1108 1108 data.update(extras)
1109 1109
1110 1110 if include_secrets:
1111 1111 data['auth_tokens'] = user.auth_tokens
1112 1112 return data
1113 1113
1114 1114 def __json__(self):
1115 1115 data = {
1116 1116 'full_name': self.full_name,
1117 1117 'full_name_or_username': self.full_name_or_username,
1118 1118 'short_contact': self.short_contact,
1119 1119 'full_contact': self.full_contact,
1120 1120 }
1121 1121 data.update(self.get_api_data())
1122 1122 return data
1123 1123
1124 1124
1125 1125 class UserApiKeys(Base, BaseModel):
1126 1126 __tablename__ = 'user_api_keys'
1127 1127 __table_args__ = (
1128 1128 Index('uak_api_key_idx', 'api_key'),
1129 1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1130 1130 base_table_args
1131 1131 )
1132 1132 __mapper_args__ = {}
1133 1133
1134 1134 # ApiKey role
1135 1135 ROLE_ALL = 'token_role_all'
1136 ROLE_HTTP = 'token_role_http'
1137 1136 ROLE_VCS = 'token_role_vcs'
1138 1137 ROLE_API = 'token_role_api'
1138 ROLE_HTTP = 'token_role_http'
1139 1139 ROLE_FEED = 'token_role_feed'
1140 1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 # The last one is ignored in the list as we only
1142 # use it for one action, and cannot be created by users
1141 1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1142 1144
1143 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1144 1146
1145 1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1146 1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1147 1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1148 1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1149 1151 expires = Column('expires', Float(53), nullable=False)
1150 1152 role = Column('role', String(255), nullable=True)
1151 1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1152 1154
1153 1155 # scope columns
1154 1156 repo_id = Column(
1155 1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1156 1158 nullable=True, unique=None, default=None)
1157 1159 repo = relationship('Repository', lazy='joined')
1158 1160
1159 1161 repo_group_id = Column(
1160 1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1161 1163 nullable=True, unique=None, default=None)
1162 1164 repo_group = relationship('RepoGroup', lazy='joined')
1163 1165
1164 1166 user = relationship('User', lazy='joined')
1165 1167
1166 1168 def __unicode__(self):
1167 1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1168 1170
1169 1171 def __json__(self):
1170 1172 data = {
1171 1173 'auth_token': self.api_key,
1172 1174 'role': self.role,
1173 1175 'scope': self.scope_humanized,
1174 1176 'expired': self.expired
1175 1177 }
1176 1178 return data
1177 1179
1178 1180 def get_api_data(self, include_secrets=False):
1179 1181 data = self.__json__()
1180 1182 if include_secrets:
1181 1183 return data
1182 1184 else:
1183 1185 data['auth_token'] = self.token_obfuscated
1184 1186 return data
1185 1187
1186 1188 @hybrid_property
1187 1189 def description_safe(self):
1188 1190 from rhodecode.lib import helpers as h
1189 1191 return h.escape(self.description)
1190 1192
1191 1193 @property
1192 1194 def expired(self):
1193 1195 if self.expires == -1:
1194 1196 return False
1195 1197 return time.time() > self.expires
1196 1198
1197 1199 @classmethod
1198 1200 def _get_role_name(cls, role):
1199 1201 return {
1200 1202 cls.ROLE_ALL: _('all'),
1201 1203 cls.ROLE_HTTP: _('http/web interface'),
1202 1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1203 1205 cls.ROLE_API: _('api calls'),
1204 1206 cls.ROLE_FEED: _('feed access'),
1205 1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1206 1208 }.get(role, role)
1207 1209
1210 @classmethod
1211 def _get_role_description(cls, role):
1212 return {
1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 'Such Token should be used then instead of a password to '
1219 'interact with a repository, and additionally can be '
1220 'limited to single repository using repo scope.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 }.get(role, role)
1225
1208 1226 @property
1209 1227 def role_humanized(self):
1210 1228 return self._get_role_name(self.role)
1211 1229
1212 1230 def _get_scope(self):
1213 1231 if self.repo:
1214 1232 return 'Repository: {}'.format(self.repo.repo_name)
1215 1233 if self.repo_group:
1216 1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1217 1235 return 'Global'
1218 1236
1219 1237 @property
1220 1238 def scope_humanized(self):
1221 1239 return self._get_scope()
1222 1240
1223 1241 @property
1224 1242 def token_obfuscated(self):
1225 1243 if self.api_key:
1226 1244 return self.api_key[:4] + "****"
1227 1245
1228 1246
1229 1247 class UserEmailMap(Base, BaseModel):
1230 1248 __tablename__ = 'user_email_map'
1231 1249 __table_args__ = (
1232 1250 Index('uem_email_idx', 'email'),
1233 1251 UniqueConstraint('email'),
1234 1252 base_table_args
1235 1253 )
1236 1254 __mapper_args__ = {}
1237 1255
1238 1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1239 1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1240 1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1241 1259 user = relationship('User', lazy='joined')
1242 1260
1243 1261 @validates('_email')
1244 1262 def validate_email(self, key, email):
1245 1263 # check if this email is not main one
1246 1264 main_email = Session().query(User).filter(User.email == email).scalar()
1247 1265 if main_email is not None:
1248 1266 raise AttributeError('email %s is present is user table' % email)
1249 1267 return email
1250 1268
1251 1269 @hybrid_property
1252 1270 def email(self):
1253 1271 return self._email
1254 1272
1255 1273 @email.setter
1256 1274 def email(self, val):
1257 1275 self._email = val.lower() if val else None
1258 1276
1259 1277
1260 1278 class UserIpMap(Base, BaseModel):
1261 1279 __tablename__ = 'user_ip_map'
1262 1280 __table_args__ = (
1263 1281 UniqueConstraint('user_id', 'ip_addr'),
1264 1282 base_table_args
1265 1283 )
1266 1284 __mapper_args__ = {}
1267 1285
1268 1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1269 1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1270 1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1271 1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1272 1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1273 1291 user = relationship('User', lazy='joined')
1274 1292
1275 1293 @hybrid_property
1276 1294 def description_safe(self):
1277 1295 from rhodecode.lib import helpers as h
1278 1296 return h.escape(self.description)
1279 1297
1280 1298 @classmethod
1281 1299 def _get_ip_range(cls, ip_addr):
1282 1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1283 1301 return [str(net.network_address), str(net.broadcast_address)]
1284 1302
1285 1303 def __json__(self):
1286 1304 return {
1287 1305 'ip_addr': self.ip_addr,
1288 1306 'ip_range': self._get_ip_range(self.ip_addr),
1289 1307 }
1290 1308
1291 1309 def __unicode__(self):
1292 1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1293 1311 self.user_id, self.ip_addr)
1294 1312
1295 1313
1296 1314 class UserSshKeys(Base, BaseModel):
1297 1315 __tablename__ = 'user_ssh_keys'
1298 1316 __table_args__ = (
1299 1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1300 1318
1301 1319 UniqueConstraint('ssh_key_fingerprint'),
1302 1320
1303 1321 base_table_args
1304 1322 )
1305 1323 __mapper_args__ = {}
1306 1324
1307 1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1309 1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1310 1328
1311 1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1312 1330
1313 1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1314 1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1315 1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1316 1334
1317 1335 user = relationship('User', lazy='joined')
1318 1336
1319 1337 def __json__(self):
1320 1338 data = {
1321 1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1322 1340 'description': self.description,
1323 1341 'created_on': self.created_on
1324 1342 }
1325 1343 return data
1326 1344
1327 1345 def get_api_data(self):
1328 1346 data = self.__json__()
1329 1347 return data
1330 1348
1331 1349
1332 1350 class UserLog(Base, BaseModel):
1333 1351 __tablename__ = 'user_logs'
1334 1352 __table_args__ = (
1335 1353 base_table_args,
1336 1354 )
1337 1355
1338 1356 VERSION_1 = 'v1'
1339 1357 VERSION_2 = 'v2'
1340 1358 VERSIONS = [VERSION_1, VERSION_2]
1341 1359
1342 1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1343 1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1344 1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1345 1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1346 1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1347 1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1348 1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1349 1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1350 1368
1351 1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1352 1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1353 1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1354 1372
1355 1373 def __unicode__(self):
1356 1374 return u"<%s('id:%s:%s')>" % (
1357 1375 self.__class__.__name__, self.repository_name, self.action)
1358 1376
1359 1377 def __json__(self):
1360 1378 return {
1361 1379 'user_id': self.user_id,
1362 1380 'username': self.username,
1363 1381 'repository_id': self.repository_id,
1364 1382 'repository_name': self.repository_name,
1365 1383 'user_ip': self.user_ip,
1366 1384 'action_date': self.action_date,
1367 1385 'action': self.action,
1368 1386 }
1369 1387
1370 1388 @hybrid_property
1371 1389 def entry_id(self):
1372 1390 return self.user_log_id
1373 1391
1374 1392 @property
1375 1393 def action_as_day(self):
1376 1394 return datetime.date(*self.action_date.timetuple()[:3])
1377 1395
1378 1396 user = relationship('User')
1379 1397 repository = relationship('Repository', cascade='')
1380 1398
1381 1399
1382 1400 class UserGroup(Base, BaseModel):
1383 1401 __tablename__ = 'users_groups'
1384 1402 __table_args__ = (
1385 1403 base_table_args,
1386 1404 )
1387 1405
1388 1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1389 1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1390 1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1391 1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1392 1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1393 1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1394 1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1395 1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1396 1414
1397 1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1398 1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1399 1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1400 1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1401 1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1402 1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1403 1421
1404 1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1405 1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1406 1424
1407 1425 @classmethod
1408 1426 def _load_group_data(cls, column):
1409 1427 if not column:
1410 1428 return {}
1411 1429
1412 1430 try:
1413 1431 return json.loads(column) or {}
1414 1432 except TypeError:
1415 1433 return {}
1416 1434
1417 1435 @hybrid_property
1418 1436 def description_safe(self):
1419 1437 from rhodecode.lib import helpers as h
1420 1438 return h.escape(self.user_group_description)
1421 1439
1422 1440 @hybrid_property
1423 1441 def group_data(self):
1424 1442 return self._load_group_data(self._group_data)
1425 1443
1426 1444 @group_data.expression
1427 1445 def group_data(self, **kwargs):
1428 1446 return self._group_data
1429 1447
1430 1448 @group_data.setter
1431 1449 def group_data(self, val):
1432 1450 try:
1433 1451 self._group_data = json.dumps(val)
1434 1452 except Exception:
1435 1453 log.error(traceback.format_exc())
1436 1454
1437 1455 @classmethod
1438 1456 def _load_sync(cls, group_data):
1439 1457 if group_data:
1440 1458 return group_data.get('extern_type')
1441 1459
1442 1460 @property
1443 1461 def sync(self):
1444 1462 return self._load_sync(self.group_data)
1445 1463
1446 1464 def __unicode__(self):
1447 1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1448 1466 self.users_group_id,
1449 1467 self.users_group_name)
1450 1468
1451 1469 @classmethod
1452 1470 def get_by_group_name(cls, group_name, cache=False,
1453 1471 case_insensitive=False):
1454 1472 if case_insensitive:
1455 1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1456 1474 func.lower(group_name))
1457 1475
1458 1476 else:
1459 1477 q = cls.query().filter(cls.users_group_name == group_name)
1460 1478 if cache:
1461 1479 q = q.options(
1462 1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1463 1481 return q.scalar()
1464 1482
1465 1483 @classmethod
1466 1484 def get(cls, user_group_id, cache=False):
1467 1485 if not user_group_id:
1468 1486 return
1469 1487
1470 1488 user_group = cls.query()
1471 1489 if cache:
1472 1490 user_group = user_group.options(
1473 1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1474 1492 return user_group.get(user_group_id)
1475 1493
1476 1494 def permissions(self, with_admins=True, with_owner=True,
1477 1495 expand_from_user_groups=False):
1478 1496 """
1479 1497 Permissions for user groups
1480 1498 """
1481 1499 _admin_perm = 'usergroup.admin'
1482 1500
1483 1501 owner_row = []
1484 1502 if with_owner:
1485 1503 usr = AttributeDict(self.user.get_dict())
1486 1504 usr.owner_row = True
1487 1505 usr.permission = _admin_perm
1488 1506 owner_row.append(usr)
1489 1507
1490 1508 super_admin_ids = []
1491 1509 super_admin_rows = []
1492 1510 if with_admins:
1493 1511 for usr in User.get_all_super_admins():
1494 1512 super_admin_ids.append(usr.user_id)
1495 1513 # if this admin is also owner, don't double the record
1496 1514 if usr.user_id == owner_row[0].user_id:
1497 1515 owner_row[0].admin_row = True
1498 1516 else:
1499 1517 usr = AttributeDict(usr.get_dict())
1500 1518 usr.admin_row = True
1501 1519 usr.permission = _admin_perm
1502 1520 super_admin_rows.append(usr)
1503 1521
1504 1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1505 1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1506 1524 joinedload(UserUserGroupToPerm.user),
1507 1525 joinedload(UserUserGroupToPerm.permission),)
1508 1526
1509 1527 # get owners and admins and permissions. We do a trick of re-writing
1510 1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1511 1529 # has a global reference and changing one object propagates to all
1512 1530 # others. This means if admin is also an owner admin_row that change
1513 1531 # would propagate to both objects
1514 1532 perm_rows = []
1515 1533 for _usr in q.all():
1516 1534 usr = AttributeDict(_usr.user.get_dict())
1517 1535 # if this user is also owner/admin, mark as duplicate record
1518 1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1519 1537 usr.duplicate_perm = True
1520 1538 usr.permission = _usr.permission.permission_name
1521 1539 perm_rows.append(usr)
1522 1540
1523 1541 # filter the perm rows by 'default' first and then sort them by
1524 1542 # admin,write,read,none permissions sorted again alphabetically in
1525 1543 # each group
1526 1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1527 1545
1528 1546 user_groups_rows = []
1529 1547 if expand_from_user_groups:
1530 1548 for ug in self.permission_user_groups(with_members=True):
1531 1549 for user_data in ug.members:
1532 1550 user_groups_rows.append(user_data)
1533 1551
1534 1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1535 1553
1536 1554 def permission_user_groups(self, with_members=False):
1537 1555 q = UserGroupUserGroupToPerm.query()\
1538 1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1539 1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1540 1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1541 1559 joinedload(UserGroupUserGroupToPerm.permission),)
1542 1560
1543 1561 perm_rows = []
1544 1562 for _user_group in q.all():
1545 1563 entry = AttributeDict(_user_group.user_group.get_dict())
1546 1564 entry.permission = _user_group.permission.permission_name
1547 1565 if with_members:
1548 1566 entry.members = [x.user.get_dict()
1549 1567 for x in _user_group.user_group.members]
1550 1568 perm_rows.append(entry)
1551 1569
1552 1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1553 1571 return perm_rows
1554 1572
1555 1573 def _get_default_perms(self, user_group, suffix=''):
1556 1574 from rhodecode.model.permission import PermissionModel
1557 1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1558 1576
1559 1577 def get_default_perms(self, suffix=''):
1560 1578 return self._get_default_perms(self, suffix)
1561 1579
1562 1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1563 1581 """
1564 1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1565 1583 basically forwarded.
1566 1584
1567 1585 """
1568 1586 user_group = self
1569 1587 data = {
1570 1588 'users_group_id': user_group.users_group_id,
1571 1589 'group_name': user_group.users_group_name,
1572 1590 'group_description': user_group.user_group_description,
1573 1591 'active': user_group.users_group_active,
1574 1592 'owner': user_group.user.username,
1575 1593 'sync': user_group.sync,
1576 1594 'owner_email': user_group.user.email,
1577 1595 }
1578 1596
1579 1597 if with_group_members:
1580 1598 users = []
1581 1599 for user in user_group.members:
1582 1600 user = user.user
1583 1601 users.append(user.get_api_data(include_secrets=include_secrets))
1584 1602 data['users'] = users
1585 1603
1586 1604 return data
1587 1605
1588 1606
1589 1607 class UserGroupMember(Base, BaseModel):
1590 1608 __tablename__ = 'users_groups_members'
1591 1609 __table_args__ = (
1592 1610 base_table_args,
1593 1611 )
1594 1612
1595 1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1596 1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1597 1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1598 1616
1599 1617 user = relationship('User', lazy='joined')
1600 1618 users_group = relationship('UserGroup')
1601 1619
1602 1620 def __init__(self, gr_id='', u_id=''):
1603 1621 self.users_group_id = gr_id
1604 1622 self.user_id = u_id
1605 1623
1606 1624
1607 1625 class RepositoryField(Base, BaseModel):
1608 1626 __tablename__ = 'repositories_fields'
1609 1627 __table_args__ = (
1610 1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1611 1629 base_table_args,
1612 1630 )
1613 1631
1614 1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1615 1633
1616 1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1617 1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1618 1636 field_key = Column("field_key", String(250))
1619 1637 field_label = Column("field_label", String(1024), nullable=False)
1620 1638 field_value = Column("field_value", String(10000), nullable=False)
1621 1639 field_desc = Column("field_desc", String(1024), nullable=False)
1622 1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1623 1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1624 1642
1625 1643 repository = relationship('Repository')
1626 1644
1627 1645 @property
1628 1646 def field_key_prefixed(self):
1629 1647 return 'ex_%s' % self.field_key
1630 1648
1631 1649 @classmethod
1632 1650 def un_prefix_key(cls, key):
1633 1651 if key.startswith(cls.PREFIX):
1634 1652 return key[len(cls.PREFIX):]
1635 1653 return key
1636 1654
1637 1655 @classmethod
1638 1656 def get_by_key_name(cls, key, repo):
1639 1657 row = cls.query()\
1640 1658 .filter(cls.repository == repo)\
1641 1659 .filter(cls.field_key == key).scalar()
1642 1660 return row
1643 1661
1644 1662
1645 1663 class Repository(Base, BaseModel):
1646 1664 __tablename__ = 'repositories'
1647 1665 __table_args__ = (
1648 1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1649 1667 base_table_args,
1650 1668 )
1651 1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1652 1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1653 1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1654 1672
1655 1673 STATE_CREATED = 'repo_state_created'
1656 1674 STATE_PENDING = 'repo_state_pending'
1657 1675 STATE_ERROR = 'repo_state_error'
1658 1676
1659 1677 LOCK_AUTOMATIC = 'lock_auto'
1660 1678 LOCK_API = 'lock_api'
1661 1679 LOCK_WEB = 'lock_web'
1662 1680 LOCK_PULL = 'lock_pull'
1663 1681
1664 1682 NAME_SEP = URL_SEP
1665 1683
1666 1684 repo_id = Column(
1667 1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1668 1686 primary_key=True)
1669 1687 _repo_name = Column(
1670 1688 "repo_name", Text(), nullable=False, default=None)
1671 1689 repo_name_hash = Column(
1672 1690 "repo_name_hash", String(255), nullable=False, unique=True)
1673 1691 repo_state = Column("repo_state", String(255), nullable=True)
1674 1692
1675 1693 clone_uri = Column(
1676 1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1677 1695 default=None)
1678 1696 push_uri = Column(
1679 1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1680 1698 default=None)
1681 1699 repo_type = Column(
1682 1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1683 1701 user_id = Column(
1684 1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1685 1703 unique=False, default=None)
1686 1704 private = Column(
1687 1705 "private", Boolean(), nullable=True, unique=None, default=None)
1688 1706 archived = Column(
1689 1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1690 1708 enable_statistics = Column(
1691 1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1692 1710 enable_downloads = Column(
1693 1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1694 1712 description = Column(
1695 1713 "description", String(10000), nullable=True, unique=None, default=None)
1696 1714 created_on = Column(
1697 1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1698 1716 default=datetime.datetime.now)
1699 1717 updated_on = Column(
1700 1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1701 1719 default=datetime.datetime.now)
1702 1720 _landing_revision = Column(
1703 1721 "landing_revision", String(255), nullable=False, unique=False,
1704 1722 default=None)
1705 1723 enable_locking = Column(
1706 1724 "enable_locking", Boolean(), nullable=False, unique=None,
1707 1725 default=False)
1708 1726 _locked = Column(
1709 1727 "locked", String(255), nullable=True, unique=False, default=None)
1710 1728 _changeset_cache = Column(
1711 1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1712 1730
1713 1731 fork_id = Column(
1714 1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1715 1733 nullable=True, unique=False, default=None)
1716 1734 group_id = Column(
1717 1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1718 1736 unique=False, default=None)
1719 1737
1720 1738 user = relationship('User', lazy='joined')
1721 1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1722 1740 group = relationship('RepoGroup', lazy='joined')
1723 1741 repo_to_perm = relationship(
1724 1742 'UserRepoToPerm', cascade='all',
1725 1743 order_by='UserRepoToPerm.repo_to_perm_id')
1726 1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1727 1745 stats = relationship('Statistics', cascade='all', uselist=False)
1728 1746
1729 1747 followers = relationship(
1730 1748 'UserFollowing',
1731 1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1732 1750 cascade='all')
1733 1751 extra_fields = relationship(
1734 1752 'RepositoryField', cascade="all, delete-orphan")
1735 1753 logs = relationship('UserLog')
1736 1754 comments = relationship(
1737 1755 'ChangesetComment', cascade="all, delete-orphan")
1738 1756 pull_requests_source = relationship(
1739 1757 'PullRequest',
1740 1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1741 1759 cascade="all, delete-orphan")
1742 1760 pull_requests_target = relationship(
1743 1761 'PullRequest',
1744 1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1745 1763 cascade="all, delete-orphan")
1746 1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1747 1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1748 1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1749 1767
1750 1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1751 1769
1752 1770 # no cascade, set NULL
1753 1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1754 1772
1755 1773 def __unicode__(self):
1756 1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1757 1775 safe_unicode(self.repo_name))
1758 1776
1759 1777 @hybrid_property
1760 1778 def description_safe(self):
1761 1779 from rhodecode.lib import helpers as h
1762 1780 return h.escape(self.description)
1763 1781
1764 1782 @hybrid_property
1765 1783 def landing_rev(self):
1766 1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1767 1785 if self._landing_revision:
1768 1786 _rev_info = self._landing_revision.split(':')
1769 1787 if len(_rev_info) < 2:
1770 1788 _rev_info.insert(0, 'rev')
1771 1789 return [_rev_info[0], _rev_info[1]]
1772 1790 return [None, None]
1773 1791
1774 1792 @property
1775 1793 def landing_ref_type(self):
1776 1794 return self.landing_rev[0]
1777 1795
1778 1796 @property
1779 1797 def landing_ref_name(self):
1780 1798 return self.landing_rev[1]
1781 1799
1782 1800 @landing_rev.setter
1783 1801 def landing_rev(self, val):
1784 1802 if ':' not in val:
1785 1803 raise ValueError('value must be delimited with `:` and consist '
1786 1804 'of <rev_type>:<rev>, got %s instead' % val)
1787 1805 self._landing_revision = val
1788 1806
1789 1807 @hybrid_property
1790 1808 def locked(self):
1791 1809 if self._locked:
1792 1810 user_id, timelocked, reason = self._locked.split(':')
1793 1811 lock_values = int(user_id), timelocked, reason
1794 1812 else:
1795 1813 lock_values = [None, None, None]
1796 1814 return lock_values
1797 1815
1798 1816 @locked.setter
1799 1817 def locked(self, val):
1800 1818 if val and isinstance(val, (list, tuple)):
1801 1819 self._locked = ':'.join(map(str, val))
1802 1820 else:
1803 1821 self._locked = None
1804 1822
1805 1823 @classmethod
1806 1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1807 1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1808 1826 dummy = EmptyCommit().__json__()
1809 1827 if not changeset_cache_raw:
1810 1828 dummy['source_repo_id'] = repo_id
1811 1829 return json.loads(json.dumps(dummy))
1812 1830
1813 1831 try:
1814 1832 return json.loads(changeset_cache_raw)
1815 1833 except TypeError:
1816 1834 return dummy
1817 1835 except Exception:
1818 1836 log.error(traceback.format_exc())
1819 1837 return dummy
1820 1838
1821 1839 @hybrid_property
1822 1840 def changeset_cache(self):
1823 1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1824 1842
1825 1843 @changeset_cache.setter
1826 1844 def changeset_cache(self, val):
1827 1845 try:
1828 1846 self._changeset_cache = json.dumps(val)
1829 1847 except Exception:
1830 1848 log.error(traceback.format_exc())
1831 1849
1832 1850 @hybrid_property
1833 1851 def repo_name(self):
1834 1852 return self._repo_name
1835 1853
1836 1854 @repo_name.setter
1837 1855 def repo_name(self, value):
1838 1856 self._repo_name = value
1839 1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1840 1858
1841 1859 @classmethod
1842 1860 def normalize_repo_name(cls, repo_name):
1843 1861 """
1844 1862 Normalizes os specific repo_name to the format internally stored inside
1845 1863 database using URL_SEP
1846 1864
1847 1865 :param cls:
1848 1866 :param repo_name:
1849 1867 """
1850 1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1851 1869
1852 1870 @classmethod
1853 1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1854 1872 session = Session()
1855 1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1856 1874
1857 1875 if cache:
1858 1876 if identity_cache:
1859 1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1860 1878 if val:
1861 1879 return val
1862 1880 else:
1863 1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1864 1882 q = q.options(
1865 1883 FromCache("sql_cache_short", cache_key))
1866 1884
1867 1885 return q.scalar()
1868 1886
1869 1887 @classmethod
1870 1888 def get_by_id_or_repo_name(cls, repoid):
1871 1889 if isinstance(repoid, (int, long)):
1872 1890 try:
1873 1891 repo = cls.get(repoid)
1874 1892 except ValueError:
1875 1893 repo = None
1876 1894 else:
1877 1895 repo = cls.get_by_repo_name(repoid)
1878 1896 return repo
1879 1897
1880 1898 @classmethod
1881 1899 def get_by_full_path(cls, repo_full_path):
1882 1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1883 1901 repo_name = cls.normalize_repo_name(repo_name)
1884 1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1885 1903
1886 1904 @classmethod
1887 1905 def get_repo_forks(cls, repo_id):
1888 1906 return cls.query().filter(Repository.fork_id == repo_id)
1889 1907
1890 1908 @classmethod
1891 1909 def base_path(cls):
1892 1910 """
1893 1911 Returns base path when all repos are stored
1894 1912
1895 1913 :param cls:
1896 1914 """
1897 1915 q = Session().query(RhodeCodeUi)\
1898 1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1899 1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1900 1918 return q.one().ui_value
1901 1919
1902 1920 @classmethod
1903 1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1904 1922 case_insensitive=True, archived=False):
1905 1923 q = Repository.query()
1906 1924
1907 1925 if not archived:
1908 1926 q = q.filter(Repository.archived.isnot(true()))
1909 1927
1910 1928 if not isinstance(user_id, Optional):
1911 1929 q = q.filter(Repository.user_id == user_id)
1912 1930
1913 1931 if not isinstance(group_id, Optional):
1914 1932 q = q.filter(Repository.group_id == group_id)
1915 1933
1916 1934 if case_insensitive:
1917 1935 q = q.order_by(func.lower(Repository.repo_name))
1918 1936 else:
1919 1937 q = q.order_by(Repository.repo_name)
1920 1938
1921 1939 return q.all()
1922 1940
1923 1941 @property
1924 1942 def repo_uid(self):
1925 1943 return '_{}'.format(self.repo_id)
1926 1944
1927 1945 @property
1928 1946 def forks(self):
1929 1947 """
1930 1948 Return forks of this repo
1931 1949 """
1932 1950 return Repository.get_repo_forks(self.repo_id)
1933 1951
1934 1952 @property
1935 1953 def parent(self):
1936 1954 """
1937 1955 Returns fork parent
1938 1956 """
1939 1957 return self.fork
1940 1958
1941 1959 @property
1942 1960 def just_name(self):
1943 1961 return self.repo_name.split(self.NAME_SEP)[-1]
1944 1962
1945 1963 @property
1946 1964 def groups_with_parents(self):
1947 1965 groups = []
1948 1966 if self.group is None:
1949 1967 return groups
1950 1968
1951 1969 cur_gr = self.group
1952 1970 groups.insert(0, cur_gr)
1953 1971 while 1:
1954 1972 gr = getattr(cur_gr, 'parent_group', None)
1955 1973 cur_gr = cur_gr.parent_group
1956 1974 if gr is None:
1957 1975 break
1958 1976 groups.insert(0, gr)
1959 1977
1960 1978 return groups
1961 1979
1962 1980 @property
1963 1981 def groups_and_repo(self):
1964 1982 return self.groups_with_parents, self
1965 1983
1966 1984 @LazyProperty
1967 1985 def repo_path(self):
1968 1986 """
1969 1987 Returns base full path for that repository means where it actually
1970 1988 exists on a filesystem
1971 1989 """
1972 1990 q = Session().query(RhodeCodeUi).filter(
1973 1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1974 1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1975 1993 return q.one().ui_value
1976 1994
1977 1995 @property
1978 1996 def repo_full_path(self):
1979 1997 p = [self.repo_path]
1980 1998 # we need to split the name by / since this is how we store the
1981 1999 # names in the database, but that eventually needs to be converted
1982 2000 # into a valid system path
1983 2001 p += self.repo_name.split(self.NAME_SEP)
1984 2002 return os.path.join(*map(safe_unicode, p))
1985 2003
1986 2004 @property
1987 2005 def cache_keys(self):
1988 2006 """
1989 2007 Returns associated cache keys for that repo
1990 2008 """
1991 2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1992 2010 repo_id=self.repo_id)
1993 2011 return CacheKey.query()\
1994 2012 .filter(CacheKey.cache_args == invalidation_namespace)\
1995 2013 .order_by(CacheKey.cache_key)\
1996 2014 .all()
1997 2015
1998 2016 @property
1999 2017 def cached_diffs_relative_dir(self):
2000 2018 """
2001 2019 Return a relative to the repository store path of cached diffs
2002 2020 used for safe display for users, who shouldn't know the absolute store
2003 2021 path
2004 2022 """
2005 2023 return os.path.join(
2006 2024 os.path.dirname(self.repo_name),
2007 2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2008 2026
2009 2027 @property
2010 2028 def cached_diffs_dir(self):
2011 2029 path = self.repo_full_path
2012 2030 return os.path.join(
2013 2031 os.path.dirname(path),
2014 2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2015 2033
2016 2034 def cached_diffs(self):
2017 2035 diff_cache_dir = self.cached_diffs_dir
2018 2036 if os.path.isdir(diff_cache_dir):
2019 2037 return os.listdir(diff_cache_dir)
2020 2038 return []
2021 2039
2022 2040 def shadow_repos(self):
2023 2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2024 2042 return [
2025 2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2026 2044 if x.startswith(shadow_repos_pattern)]
2027 2045
2028 2046 def get_new_name(self, repo_name):
2029 2047 """
2030 2048 returns new full repository name based on assigned group and new new
2031 2049
2032 2050 :param group_name:
2033 2051 """
2034 2052 path_prefix = self.group.full_path_splitted if self.group else []
2035 2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2036 2054
2037 2055 @property
2038 2056 def _config(self):
2039 2057 """
2040 2058 Returns db based config object.
2041 2059 """
2042 2060 from rhodecode.lib.utils import make_db_config
2043 2061 return make_db_config(clear_session=False, repo=self)
2044 2062
2045 2063 def permissions(self, with_admins=True, with_owner=True,
2046 2064 expand_from_user_groups=False):
2047 2065 """
2048 2066 Permissions for repositories
2049 2067 """
2050 2068 _admin_perm = 'repository.admin'
2051 2069
2052 2070 owner_row = []
2053 2071 if with_owner:
2054 2072 usr = AttributeDict(self.user.get_dict())
2055 2073 usr.owner_row = True
2056 2074 usr.permission = _admin_perm
2057 2075 usr.permission_id = None
2058 2076 owner_row.append(usr)
2059 2077
2060 2078 super_admin_ids = []
2061 2079 super_admin_rows = []
2062 2080 if with_admins:
2063 2081 for usr in User.get_all_super_admins():
2064 2082 super_admin_ids.append(usr.user_id)
2065 2083 # if this admin is also owner, don't double the record
2066 2084 if usr.user_id == owner_row[0].user_id:
2067 2085 owner_row[0].admin_row = True
2068 2086 else:
2069 2087 usr = AttributeDict(usr.get_dict())
2070 2088 usr.admin_row = True
2071 2089 usr.permission = _admin_perm
2072 2090 usr.permission_id = None
2073 2091 super_admin_rows.append(usr)
2074 2092
2075 2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2076 2094 q = q.options(joinedload(UserRepoToPerm.repository),
2077 2095 joinedload(UserRepoToPerm.user),
2078 2096 joinedload(UserRepoToPerm.permission),)
2079 2097
2080 2098 # get owners and admins and permissions. We do a trick of re-writing
2081 2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2082 2100 # has a global reference and changing one object propagates to all
2083 2101 # others. This means if admin is also an owner admin_row that change
2084 2102 # would propagate to both objects
2085 2103 perm_rows = []
2086 2104 for _usr in q.all():
2087 2105 usr = AttributeDict(_usr.user.get_dict())
2088 2106 # if this user is also owner/admin, mark as duplicate record
2089 2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2090 2108 usr.duplicate_perm = True
2091 2109 # also check if this permission is maybe used by branch_permissions
2092 2110 if _usr.branch_perm_entry:
2093 2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2094 2112
2095 2113 usr.permission = _usr.permission.permission_name
2096 2114 usr.permission_id = _usr.repo_to_perm_id
2097 2115 perm_rows.append(usr)
2098 2116
2099 2117 # filter the perm rows by 'default' first and then sort them by
2100 2118 # admin,write,read,none permissions sorted again alphabetically in
2101 2119 # each group
2102 2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2103 2121
2104 2122 user_groups_rows = []
2105 2123 if expand_from_user_groups:
2106 2124 for ug in self.permission_user_groups(with_members=True):
2107 2125 for user_data in ug.members:
2108 2126 user_groups_rows.append(user_data)
2109 2127
2110 2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2111 2129
2112 2130 def permission_user_groups(self, with_members=True):
2113 2131 q = UserGroupRepoToPerm.query()\
2114 2132 .filter(UserGroupRepoToPerm.repository == self)
2115 2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2116 2134 joinedload(UserGroupRepoToPerm.users_group),
2117 2135 joinedload(UserGroupRepoToPerm.permission),)
2118 2136
2119 2137 perm_rows = []
2120 2138 for _user_group in q.all():
2121 2139 entry = AttributeDict(_user_group.users_group.get_dict())
2122 2140 entry.permission = _user_group.permission.permission_name
2123 2141 if with_members:
2124 2142 entry.members = [x.user.get_dict()
2125 2143 for x in _user_group.users_group.members]
2126 2144 perm_rows.append(entry)
2127 2145
2128 2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2129 2147 return perm_rows
2130 2148
2131 2149 def get_api_data(self, include_secrets=False):
2132 2150 """
2133 2151 Common function for generating repo api data
2134 2152
2135 2153 :param include_secrets: See :meth:`User.get_api_data`.
2136 2154
2137 2155 """
2138 2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2139 2157 # move this methods on models level.
2140 2158 from rhodecode.model.settings import SettingsModel
2141 2159 from rhodecode.model.repo import RepoModel
2142 2160
2143 2161 repo = self
2144 2162 _user_id, _time, _reason = self.locked
2145 2163
2146 2164 data = {
2147 2165 'repo_id': repo.repo_id,
2148 2166 'repo_name': repo.repo_name,
2149 2167 'repo_type': repo.repo_type,
2150 2168 'clone_uri': repo.clone_uri or '',
2151 2169 'push_uri': repo.push_uri or '',
2152 2170 'url': RepoModel().get_url(self),
2153 2171 'private': repo.private,
2154 2172 'created_on': repo.created_on,
2155 2173 'description': repo.description_safe,
2156 2174 'landing_rev': repo.landing_rev,
2157 2175 'owner': repo.user.username,
2158 2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2159 2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2160 2178 'enable_statistics': repo.enable_statistics,
2161 2179 'enable_locking': repo.enable_locking,
2162 2180 'enable_downloads': repo.enable_downloads,
2163 2181 'last_changeset': repo.changeset_cache,
2164 2182 'locked_by': User.get(_user_id).get_api_data(
2165 2183 include_secrets=include_secrets) if _user_id else None,
2166 2184 'locked_date': time_to_datetime(_time) if _time else None,
2167 2185 'lock_reason': _reason if _reason else None,
2168 2186 }
2169 2187
2170 2188 # TODO: mikhail: should be per-repo settings here
2171 2189 rc_config = SettingsModel().get_all_settings()
2172 2190 repository_fields = str2bool(
2173 2191 rc_config.get('rhodecode_repository_fields'))
2174 2192 if repository_fields:
2175 2193 for f in self.extra_fields:
2176 2194 data[f.field_key_prefixed] = f.field_value
2177 2195
2178 2196 return data
2179 2197
2180 2198 @classmethod
2181 2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2182 2200 if not lock_time:
2183 2201 lock_time = time.time()
2184 2202 if not lock_reason:
2185 2203 lock_reason = cls.LOCK_AUTOMATIC
2186 2204 repo.locked = [user_id, lock_time, lock_reason]
2187 2205 Session().add(repo)
2188 2206 Session().commit()
2189 2207
2190 2208 @classmethod
2191 2209 def unlock(cls, repo):
2192 2210 repo.locked = None
2193 2211 Session().add(repo)
2194 2212 Session().commit()
2195 2213
2196 2214 @classmethod
2197 2215 def getlock(cls, repo):
2198 2216 return repo.locked
2199 2217
2200 2218 def is_user_lock(self, user_id):
2201 2219 if self.lock[0]:
2202 2220 lock_user_id = safe_int(self.lock[0])
2203 2221 user_id = safe_int(user_id)
2204 2222 # both are ints, and they are equal
2205 2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2206 2224
2207 2225 return False
2208 2226
2209 2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2210 2228 """
2211 2229 Checks locking on this repository, if locking is enabled and lock is
2212 2230 present returns a tuple of make_lock, locked, locked_by.
2213 2231 make_lock can have 3 states None (do nothing) True, make lock
2214 2232 False release lock, This value is later propagated to hooks, which
2215 2233 do the locking. Think about this as signals passed to hooks what to do.
2216 2234
2217 2235 """
2218 2236 # TODO: johbo: This is part of the business logic and should be moved
2219 2237 # into the RepositoryModel.
2220 2238
2221 2239 if action not in ('push', 'pull'):
2222 2240 raise ValueError("Invalid action value: %s" % repr(action))
2223 2241
2224 2242 # defines if locked error should be thrown to user
2225 2243 currently_locked = False
2226 2244 # defines if new lock should be made, tri-state
2227 2245 make_lock = None
2228 2246 repo = self
2229 2247 user = User.get(user_id)
2230 2248
2231 2249 lock_info = repo.locked
2232 2250
2233 2251 if repo and (repo.enable_locking or not only_when_enabled):
2234 2252 if action == 'push':
2235 2253 # check if it's already locked !, if it is compare users
2236 2254 locked_by_user_id = lock_info[0]
2237 2255 if user.user_id == locked_by_user_id:
2238 2256 log.debug(
2239 2257 'Got `push` action from user %s, now unlocking', user)
2240 2258 # unlock if we have push from user who locked
2241 2259 make_lock = False
2242 2260 else:
2243 2261 # we're not the same user who locked, ban with
2244 2262 # code defined in settings (default is 423 HTTP Locked) !
2245 2263 log.debug('Repo %s is currently locked by %s', repo, user)
2246 2264 currently_locked = True
2247 2265 elif action == 'pull':
2248 2266 # [0] user [1] date
2249 2267 if lock_info[0] and lock_info[1]:
2250 2268 log.debug('Repo %s is currently locked by %s', repo, user)
2251 2269 currently_locked = True
2252 2270 else:
2253 2271 log.debug('Setting lock on repo %s by %s', repo, user)
2254 2272 make_lock = True
2255 2273
2256 2274 else:
2257 2275 log.debug('Repository %s do not have locking enabled', repo)
2258 2276
2259 2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2260 2278 make_lock, currently_locked, lock_info)
2261 2279
2262 2280 from rhodecode.lib.auth import HasRepoPermissionAny
2263 2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2264 2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2265 2283 # if we don't have at least write permission we cannot make a lock
2266 2284 log.debug('lock state reset back to FALSE due to lack '
2267 2285 'of at least read permission')
2268 2286 make_lock = False
2269 2287
2270 2288 return make_lock, currently_locked, lock_info
2271 2289
2272 2290 @property
2273 2291 def last_commit_cache_update_diff(self):
2274 2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2275 2293
2276 2294 @classmethod
2277 2295 def _load_commit_change(cls, last_commit_cache):
2278 2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2279 2297 empty_date = datetime.datetime.fromtimestamp(0)
2280 2298 date_latest = last_commit_cache.get('date', empty_date)
2281 2299 try:
2282 2300 return parse_datetime(date_latest)
2283 2301 except Exception:
2284 2302 return empty_date
2285 2303
2286 2304 @property
2287 2305 def last_commit_change(self):
2288 2306 return self._load_commit_change(self.changeset_cache)
2289 2307
2290 2308 @property
2291 2309 def last_db_change(self):
2292 2310 return self.updated_on
2293 2311
2294 2312 @property
2295 2313 def clone_uri_hidden(self):
2296 2314 clone_uri = self.clone_uri
2297 2315 if clone_uri:
2298 2316 import urlobject
2299 2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2300 2318 if url_obj.password:
2301 2319 clone_uri = url_obj.with_password('*****')
2302 2320 return clone_uri
2303 2321
2304 2322 @property
2305 2323 def push_uri_hidden(self):
2306 2324 push_uri = self.push_uri
2307 2325 if push_uri:
2308 2326 import urlobject
2309 2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2310 2328 if url_obj.password:
2311 2329 push_uri = url_obj.with_password('*****')
2312 2330 return push_uri
2313 2331
2314 2332 def clone_url(self, **override):
2315 2333 from rhodecode.model.settings import SettingsModel
2316 2334
2317 2335 uri_tmpl = None
2318 2336 if 'with_id' in override:
2319 2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2320 2338 del override['with_id']
2321 2339
2322 2340 if 'uri_tmpl' in override:
2323 2341 uri_tmpl = override['uri_tmpl']
2324 2342 del override['uri_tmpl']
2325 2343
2326 2344 ssh = False
2327 2345 if 'ssh' in override:
2328 2346 ssh = True
2329 2347 del override['ssh']
2330 2348
2331 2349 # we didn't override our tmpl from **overrides
2332 2350 request = get_current_request()
2333 2351 if not uri_tmpl:
2334 2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2335 2353 rc_config = request.call_context.rc_config
2336 2354 else:
2337 2355 rc_config = SettingsModel().get_all_settings(cache=True)
2338 2356
2339 2357 if ssh:
2340 2358 uri_tmpl = rc_config.get(
2341 2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2342 2360
2343 2361 else:
2344 2362 uri_tmpl = rc_config.get(
2345 2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2346 2364
2347 2365 return get_clone_url(request=request,
2348 2366 uri_tmpl=uri_tmpl,
2349 2367 repo_name=self.repo_name,
2350 2368 repo_id=self.repo_id,
2351 2369 repo_type=self.repo_type,
2352 2370 **override)
2353 2371
2354 2372 def set_state(self, state):
2355 2373 self.repo_state = state
2356 2374 Session().add(self)
2357 2375 #==========================================================================
2358 2376 # SCM PROPERTIES
2359 2377 #==========================================================================
2360 2378
2361 2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2362 2380 return get_commit_safe(
2363 2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2364 2382 maybe_unreachable=maybe_unreachable)
2365 2383
2366 2384 def get_changeset(self, rev=None, pre_load=None):
2367 2385 warnings.warn("Use get_commit", DeprecationWarning)
2368 2386 commit_id = None
2369 2387 commit_idx = None
2370 2388 if isinstance(rev, compat.string_types):
2371 2389 commit_id = rev
2372 2390 else:
2373 2391 commit_idx = rev
2374 2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2375 2393 pre_load=pre_load)
2376 2394
2377 2395 def get_landing_commit(self):
2378 2396 """
2379 2397 Returns landing commit, or if that doesn't exist returns the tip
2380 2398 """
2381 2399 _rev_type, _rev = self.landing_rev
2382 2400 commit = self.get_commit(_rev)
2383 2401 if isinstance(commit, EmptyCommit):
2384 2402 return self.get_commit()
2385 2403 return commit
2386 2404
2387 2405 def flush_commit_cache(self):
2388 2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2389 2407 self.update_commit_cache()
2390 2408
2391 2409 def update_commit_cache(self, cs_cache=None, config=None):
2392 2410 """
2393 2411 Update cache of last commit for repository
2394 2412 cache_keys should be::
2395 2413
2396 2414 source_repo_id
2397 2415 short_id
2398 2416 raw_id
2399 2417 revision
2400 2418 parents
2401 2419 message
2402 2420 date
2403 2421 author
2404 2422 updated_on
2405 2423
2406 2424 """
2407 2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2408 2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2409 2427 empty_date = datetime.datetime.fromtimestamp(0)
2410 2428
2411 2429 if cs_cache is None:
2412 2430 # use no-cache version here
2413 2431 try:
2414 2432 scm_repo = self.scm_instance(cache=False, config=config)
2415 2433 except VCSError:
2416 2434 scm_repo = None
2417 2435 empty = scm_repo is None or scm_repo.is_empty()
2418 2436
2419 2437 if not empty:
2420 2438 cs_cache = scm_repo.get_commit(
2421 2439 pre_load=["author", "date", "message", "parents", "branch"])
2422 2440 else:
2423 2441 cs_cache = EmptyCommit()
2424 2442
2425 2443 if isinstance(cs_cache, BaseChangeset):
2426 2444 cs_cache = cs_cache.__json__()
2427 2445
2428 2446 def is_outdated(new_cs_cache):
2429 2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2430 2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2431 2449 return True
2432 2450 return False
2433 2451
2434 2452 # check if we have maybe already latest cached revision
2435 2453 if is_outdated(cs_cache) or not self.changeset_cache:
2436 2454 _current_datetime = datetime.datetime.utcnow()
2437 2455 last_change = cs_cache.get('date') or _current_datetime
2438 2456 # we check if last update is newer than the new value
2439 2457 # if yes, we use the current timestamp instead. Imagine you get
2440 2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2441 2459 last_change_timestamp = datetime_to_time(last_change)
2442 2460 current_timestamp = datetime_to_time(last_change)
2443 2461 if last_change_timestamp > current_timestamp and not empty:
2444 2462 cs_cache['date'] = _current_datetime
2445 2463
2446 2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2447 2465 cs_cache['updated_on'] = time.time()
2448 2466 self.changeset_cache = cs_cache
2449 2467 self.updated_on = last_change
2450 2468 Session().add(self)
2451 2469 Session().commit()
2452 2470
2453 2471 else:
2454 2472 if empty:
2455 2473 cs_cache = EmptyCommit().__json__()
2456 2474 else:
2457 2475 cs_cache = self.changeset_cache
2458 2476
2459 2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2460 2478
2461 2479 cs_cache['updated_on'] = time.time()
2462 2480 self.changeset_cache = cs_cache
2463 2481 self.updated_on = _date_latest
2464 2482 Session().add(self)
2465 2483 Session().commit()
2466 2484
2467 2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2468 2486 self.repo_name, cs_cache, _date_latest)
2469 2487
2470 2488 @property
2471 2489 def tip(self):
2472 2490 return self.get_commit('tip')
2473 2491
2474 2492 @property
2475 2493 def author(self):
2476 2494 return self.tip.author
2477 2495
2478 2496 @property
2479 2497 def last_change(self):
2480 2498 return self.scm_instance().last_change
2481 2499
2482 2500 def get_comments(self, revisions=None):
2483 2501 """
2484 2502 Returns comments for this repository grouped by revisions
2485 2503
2486 2504 :param revisions: filter query by revisions only
2487 2505 """
2488 2506 cmts = ChangesetComment.query()\
2489 2507 .filter(ChangesetComment.repo == self)
2490 2508 if revisions:
2491 2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2492 2510 grouped = collections.defaultdict(list)
2493 2511 for cmt in cmts.all():
2494 2512 grouped[cmt.revision].append(cmt)
2495 2513 return grouped
2496 2514
2497 2515 def statuses(self, revisions=None):
2498 2516 """
2499 2517 Returns statuses for this repository
2500 2518
2501 2519 :param revisions: list of revisions to get statuses for
2502 2520 """
2503 2521 statuses = ChangesetStatus.query()\
2504 2522 .filter(ChangesetStatus.repo == self)\
2505 2523 .filter(ChangesetStatus.version == 0)
2506 2524
2507 2525 if revisions:
2508 2526 # Try doing the filtering in chunks to avoid hitting limits
2509 2527 size = 500
2510 2528 status_results = []
2511 2529 for chunk in xrange(0, len(revisions), size):
2512 2530 status_results += statuses.filter(
2513 2531 ChangesetStatus.revision.in_(
2514 2532 revisions[chunk: chunk+size])
2515 2533 ).all()
2516 2534 else:
2517 2535 status_results = statuses.all()
2518 2536
2519 2537 grouped = {}
2520 2538
2521 2539 # maybe we have open new pullrequest without a status?
2522 2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2523 2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2524 2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2525 2543 for rev in pr.revisions:
2526 2544 pr_id = pr.pull_request_id
2527 2545 pr_repo = pr.target_repo.repo_name
2528 2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2529 2547
2530 2548 for stat in status_results:
2531 2549 pr_id = pr_repo = None
2532 2550 if stat.pull_request:
2533 2551 pr_id = stat.pull_request.pull_request_id
2534 2552 pr_repo = stat.pull_request.target_repo.repo_name
2535 2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2536 2554 pr_id, pr_repo]
2537 2555 return grouped
2538 2556
2539 2557 # ==========================================================================
2540 2558 # SCM CACHE INSTANCE
2541 2559 # ==========================================================================
2542 2560
2543 2561 def scm_instance(self, **kwargs):
2544 2562 import rhodecode
2545 2563
2546 2564 # Passing a config will not hit the cache currently only used
2547 2565 # for repo2dbmapper
2548 2566 config = kwargs.pop('config', None)
2549 2567 cache = kwargs.pop('cache', None)
2550 2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2551 2569 if vcs_full_cache is not None:
2552 2570 # allows override global config
2553 2571 full_cache = vcs_full_cache
2554 2572 else:
2555 2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2556 2574 # if cache is NOT defined use default global, else we have a full
2557 2575 # control over cache behaviour
2558 2576 if cache is None and full_cache and not config:
2559 2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2560 2578 return self._get_instance_cached()
2561 2579
2562 2580 # cache here is sent to the "vcs server"
2563 2581 return self._get_instance(cache=bool(cache), config=config)
2564 2582
2565 2583 def _get_instance_cached(self):
2566 2584 from rhodecode.lib import rc_cache
2567 2585
2568 2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2569 2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2570 2588 repo_id=self.repo_id)
2571 2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2572 2590
2573 2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2574 2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2575 2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2576 2594
2577 2595 # we must use thread scoped cache here,
2578 2596 # because each thread of gevent needs it's own not shared connection and cache
2579 2597 # we also alter `args` so the cache key is individual for every green thread.
2580 2598 inv_context_manager = rc_cache.InvalidationContext(
2581 2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2582 2600 thread_scoped=True)
2583 2601 with inv_context_manager as invalidation_context:
2584 2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2585 2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2586 2604
2587 2605 # re-compute and store cache if we get invalidate signal
2588 2606 if invalidation_context.should_invalidate():
2589 2607 instance = get_instance_cached.refresh(*args)
2590 2608 else:
2591 2609 instance = get_instance_cached(*args)
2592 2610
2593 2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2594 2612 return instance
2595 2613
2596 2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2597 2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2598 2616 self.repo_type, self.repo_path, cache)
2599 2617 config = config or self._config
2600 2618 custom_wire = {
2601 2619 'cache': cache, # controls the vcs.remote cache
2602 2620 'repo_state_uid': repo_state_uid
2603 2621 }
2604 2622 repo = get_vcs_instance(
2605 2623 repo_path=safe_str(self.repo_full_path),
2606 2624 config=config,
2607 2625 with_wire=custom_wire,
2608 2626 create=False,
2609 2627 _vcs_alias=self.repo_type)
2610 2628 if repo is not None:
2611 2629 repo.count() # cache rebuild
2612 2630 return repo
2613 2631
2614 2632 def get_shadow_repository_path(self, workspace_id):
2615 2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2616 2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2617 2635 self.repo_full_path, self.repo_id, workspace_id)
2618 2636 return shadow_repo_path
2619 2637
2620 2638 def __json__(self):
2621 2639 return {'landing_rev': self.landing_rev}
2622 2640
2623 2641 def get_dict(self):
2624 2642
2625 2643 # Since we transformed `repo_name` to a hybrid property, we need to
2626 2644 # keep compatibility with the code which uses `repo_name` field.
2627 2645
2628 2646 result = super(Repository, self).get_dict()
2629 2647 result['repo_name'] = result.pop('_repo_name', None)
2630 2648 return result
2631 2649
2632 2650
2633 2651 class RepoGroup(Base, BaseModel):
2634 2652 __tablename__ = 'groups'
2635 2653 __table_args__ = (
2636 2654 UniqueConstraint('group_name', 'group_parent_id'),
2637 2655 base_table_args,
2638 2656 )
2639 2657 __mapper_args__ = {'order_by': 'group_name'}
2640 2658
2641 2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2642 2660
2643 2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2644 2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2645 2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2646 2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2647 2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2648 2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2649 2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2650 2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2651 2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2652 2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2653 2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2654 2672
2655 2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2656 2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2657 2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2658 2676 user = relationship('User')
2659 2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2660 2678
2661 2679 # no cascade, set NULL
2662 2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2663 2681
2664 2682 def __init__(self, group_name='', parent_group=None):
2665 2683 self.group_name = group_name
2666 2684 self.parent_group = parent_group
2667 2685
2668 2686 def __unicode__(self):
2669 2687 return u"<%s('id:%s:%s')>" % (
2670 2688 self.__class__.__name__, self.group_id, self.group_name)
2671 2689
2672 2690 @hybrid_property
2673 2691 def group_name(self):
2674 2692 return self._group_name
2675 2693
2676 2694 @group_name.setter
2677 2695 def group_name(self, value):
2678 2696 self._group_name = value
2679 2697 self.group_name_hash = self.hash_repo_group_name(value)
2680 2698
2681 2699 @classmethod
2682 2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2683 2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2684 2702 dummy = EmptyCommit().__json__()
2685 2703 if not changeset_cache_raw:
2686 2704 dummy['source_repo_id'] = repo_id
2687 2705 return json.loads(json.dumps(dummy))
2688 2706
2689 2707 try:
2690 2708 return json.loads(changeset_cache_raw)
2691 2709 except TypeError:
2692 2710 return dummy
2693 2711 except Exception:
2694 2712 log.error(traceback.format_exc())
2695 2713 return dummy
2696 2714
2697 2715 @hybrid_property
2698 2716 def changeset_cache(self):
2699 2717 return self._load_changeset_cache('', self._changeset_cache)
2700 2718
2701 2719 @changeset_cache.setter
2702 2720 def changeset_cache(self, val):
2703 2721 try:
2704 2722 self._changeset_cache = json.dumps(val)
2705 2723 except Exception:
2706 2724 log.error(traceback.format_exc())
2707 2725
2708 2726 @validates('group_parent_id')
2709 2727 def validate_group_parent_id(self, key, val):
2710 2728 """
2711 2729 Check cycle references for a parent group to self
2712 2730 """
2713 2731 if self.group_id and val:
2714 2732 assert val != self.group_id
2715 2733
2716 2734 return val
2717 2735
2718 2736 @hybrid_property
2719 2737 def description_safe(self):
2720 2738 from rhodecode.lib import helpers as h
2721 2739 return h.escape(self.group_description)
2722 2740
2723 2741 @classmethod
2724 2742 def hash_repo_group_name(cls, repo_group_name):
2725 2743 val = remove_formatting(repo_group_name)
2726 2744 val = safe_str(val).lower()
2727 2745 chars = []
2728 2746 for c in val:
2729 2747 if c not in string.ascii_letters:
2730 2748 c = str(ord(c))
2731 2749 chars.append(c)
2732 2750
2733 2751 return ''.join(chars)
2734 2752
2735 2753 @classmethod
2736 2754 def _generate_choice(cls, repo_group):
2737 2755 from webhelpers2.html import literal as _literal
2738 2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2739 2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2740 2758
2741 2759 @classmethod
2742 2760 def groups_choices(cls, groups=None, show_empty_group=True):
2743 2761 if not groups:
2744 2762 groups = cls.query().all()
2745 2763
2746 2764 repo_groups = []
2747 2765 if show_empty_group:
2748 2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2749 2767
2750 2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2751 2769
2752 2770 repo_groups = sorted(
2753 2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2754 2772 return repo_groups
2755 2773
2756 2774 @classmethod
2757 2775 def url_sep(cls):
2758 2776 return URL_SEP
2759 2777
2760 2778 @classmethod
2761 2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2762 2780 if case_insensitive:
2763 2781 gr = cls.query().filter(func.lower(cls.group_name)
2764 2782 == func.lower(group_name))
2765 2783 else:
2766 2784 gr = cls.query().filter(cls.group_name == group_name)
2767 2785 if cache:
2768 2786 name_key = _hash_key(group_name)
2769 2787 gr = gr.options(
2770 2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2771 2789 return gr.scalar()
2772 2790
2773 2791 @classmethod
2774 2792 def get_user_personal_repo_group(cls, user_id):
2775 2793 user = User.get(user_id)
2776 2794 if user.username == User.DEFAULT_USER:
2777 2795 return None
2778 2796
2779 2797 return cls.query()\
2780 2798 .filter(cls.personal == true()) \
2781 2799 .filter(cls.user == user) \
2782 2800 .order_by(cls.group_id.asc()) \
2783 2801 .first()
2784 2802
2785 2803 @classmethod
2786 2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2787 2805 case_insensitive=True):
2788 2806 q = RepoGroup.query()
2789 2807
2790 2808 if not isinstance(user_id, Optional):
2791 2809 q = q.filter(RepoGroup.user_id == user_id)
2792 2810
2793 2811 if not isinstance(group_id, Optional):
2794 2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2795 2813
2796 2814 if case_insensitive:
2797 2815 q = q.order_by(func.lower(RepoGroup.group_name))
2798 2816 else:
2799 2817 q = q.order_by(RepoGroup.group_name)
2800 2818 return q.all()
2801 2819
2802 2820 @property
2803 2821 def parents(self, parents_recursion_limit=10):
2804 2822 groups = []
2805 2823 if self.parent_group is None:
2806 2824 return groups
2807 2825 cur_gr = self.parent_group
2808 2826 groups.insert(0, cur_gr)
2809 2827 cnt = 0
2810 2828 while 1:
2811 2829 cnt += 1
2812 2830 gr = getattr(cur_gr, 'parent_group', None)
2813 2831 cur_gr = cur_gr.parent_group
2814 2832 if gr is None:
2815 2833 break
2816 2834 if cnt == parents_recursion_limit:
2817 2835 # this will prevent accidental infinit loops
2818 2836 log.error('more than %s parents found for group %s, stopping '
2819 2837 'recursive parent fetching', parents_recursion_limit, self)
2820 2838 break
2821 2839
2822 2840 groups.insert(0, gr)
2823 2841 return groups
2824 2842
2825 2843 @property
2826 2844 def last_commit_cache_update_diff(self):
2827 2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2828 2846
2829 2847 @classmethod
2830 2848 def _load_commit_change(cls, last_commit_cache):
2831 2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2832 2850 empty_date = datetime.datetime.fromtimestamp(0)
2833 2851 date_latest = last_commit_cache.get('date', empty_date)
2834 2852 try:
2835 2853 return parse_datetime(date_latest)
2836 2854 except Exception:
2837 2855 return empty_date
2838 2856
2839 2857 @property
2840 2858 def last_commit_change(self):
2841 2859 return self._load_commit_change(self.changeset_cache)
2842 2860
2843 2861 @property
2844 2862 def last_db_change(self):
2845 2863 return self.updated_on
2846 2864
2847 2865 @property
2848 2866 def children(self):
2849 2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2850 2868
2851 2869 @property
2852 2870 def name(self):
2853 2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2854 2872
2855 2873 @property
2856 2874 def full_path(self):
2857 2875 return self.group_name
2858 2876
2859 2877 @property
2860 2878 def full_path_splitted(self):
2861 2879 return self.group_name.split(RepoGroup.url_sep())
2862 2880
2863 2881 @property
2864 2882 def repositories(self):
2865 2883 return Repository.query()\
2866 2884 .filter(Repository.group == self)\
2867 2885 .order_by(Repository.repo_name)
2868 2886
2869 2887 @property
2870 2888 def repositories_recursive_count(self):
2871 2889 cnt = self.repositories.count()
2872 2890
2873 2891 def children_count(group):
2874 2892 cnt = 0
2875 2893 for child in group.children:
2876 2894 cnt += child.repositories.count()
2877 2895 cnt += children_count(child)
2878 2896 return cnt
2879 2897
2880 2898 return cnt + children_count(self)
2881 2899
2882 2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2883 2901 all_ = []
2884 2902
2885 2903 def _get_members(root_gr):
2886 2904 if include_repos:
2887 2905 for r in root_gr.repositories:
2888 2906 all_.append(r)
2889 2907 childs = root_gr.children.all()
2890 2908 if childs:
2891 2909 for gr in childs:
2892 2910 if include_groups:
2893 2911 all_.append(gr)
2894 2912 _get_members(gr)
2895 2913
2896 2914 root_group = []
2897 2915 if include_groups:
2898 2916 root_group = [self]
2899 2917
2900 2918 _get_members(self)
2901 2919 return root_group + all_
2902 2920
2903 2921 def recursive_groups_and_repos(self):
2904 2922 """
2905 2923 Recursive return all groups, with repositories in those groups
2906 2924 """
2907 2925 return self._recursive_objects()
2908 2926
2909 2927 def recursive_groups(self):
2910 2928 """
2911 2929 Returns all children groups for this group including children of children
2912 2930 """
2913 2931 return self._recursive_objects(include_repos=False)
2914 2932
2915 2933 def recursive_repos(self):
2916 2934 """
2917 2935 Returns all children repositories for this group
2918 2936 """
2919 2937 return self._recursive_objects(include_groups=False)
2920 2938
2921 2939 def get_new_name(self, group_name):
2922 2940 """
2923 2941 returns new full group name based on parent and new name
2924 2942
2925 2943 :param group_name:
2926 2944 """
2927 2945 path_prefix = (self.parent_group.full_path_splitted if
2928 2946 self.parent_group else [])
2929 2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2930 2948
2931 2949 def update_commit_cache(self, config=None):
2932 2950 """
2933 2951 Update cache of last commit for newest repository inside this repository group.
2934 2952 cache_keys should be::
2935 2953
2936 2954 source_repo_id
2937 2955 short_id
2938 2956 raw_id
2939 2957 revision
2940 2958 parents
2941 2959 message
2942 2960 date
2943 2961 author
2944 2962
2945 2963 """
2946 2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2947 2965 empty_date = datetime.datetime.fromtimestamp(0)
2948 2966
2949 2967 def repo_groups_and_repos(root_gr):
2950 2968 for _repo in root_gr.repositories:
2951 2969 yield _repo
2952 2970 for child_group in root_gr.children.all():
2953 2971 yield child_group
2954 2972
2955 2973 latest_repo_cs_cache = {}
2956 2974 for obj in repo_groups_and_repos(self):
2957 2975 repo_cs_cache = obj.changeset_cache
2958 2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2959 2977 date_current = repo_cs_cache.get('date', empty_date)
2960 2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2961 2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2962 2980 latest_repo_cs_cache = repo_cs_cache
2963 2981 if hasattr(obj, 'repo_id'):
2964 2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2965 2983 else:
2966 2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2967 2985
2968 2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2969 2987
2970 2988 latest_repo_cs_cache['updated_on'] = time.time()
2971 2989 self.changeset_cache = latest_repo_cs_cache
2972 2990 self.updated_on = _date_latest
2973 2991 Session().add(self)
2974 2992 Session().commit()
2975 2993
2976 2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2977 2995 self.group_name, latest_repo_cs_cache, _date_latest)
2978 2996
2979 2997 def permissions(self, with_admins=True, with_owner=True,
2980 2998 expand_from_user_groups=False):
2981 2999 """
2982 3000 Permissions for repository groups
2983 3001 """
2984 3002 _admin_perm = 'group.admin'
2985 3003
2986 3004 owner_row = []
2987 3005 if with_owner:
2988 3006 usr = AttributeDict(self.user.get_dict())
2989 3007 usr.owner_row = True
2990 3008 usr.permission = _admin_perm
2991 3009 owner_row.append(usr)
2992 3010
2993 3011 super_admin_ids = []
2994 3012 super_admin_rows = []
2995 3013 if with_admins:
2996 3014 for usr in User.get_all_super_admins():
2997 3015 super_admin_ids.append(usr.user_id)
2998 3016 # if this admin is also owner, don't double the record
2999 3017 if usr.user_id == owner_row[0].user_id:
3000 3018 owner_row[0].admin_row = True
3001 3019 else:
3002 3020 usr = AttributeDict(usr.get_dict())
3003 3021 usr.admin_row = True
3004 3022 usr.permission = _admin_perm
3005 3023 super_admin_rows.append(usr)
3006 3024
3007 3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3008 3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3009 3027 joinedload(UserRepoGroupToPerm.user),
3010 3028 joinedload(UserRepoGroupToPerm.permission),)
3011 3029
3012 3030 # get owners and admins and permissions. We do a trick of re-writing
3013 3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3014 3032 # has a global reference and changing one object propagates to all
3015 3033 # others. This means if admin is also an owner admin_row that change
3016 3034 # would propagate to both objects
3017 3035 perm_rows = []
3018 3036 for _usr in q.all():
3019 3037 usr = AttributeDict(_usr.user.get_dict())
3020 3038 # if this user is also owner/admin, mark as duplicate record
3021 3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3022 3040 usr.duplicate_perm = True
3023 3041 usr.permission = _usr.permission.permission_name
3024 3042 perm_rows.append(usr)
3025 3043
3026 3044 # filter the perm rows by 'default' first and then sort them by
3027 3045 # admin,write,read,none permissions sorted again alphabetically in
3028 3046 # each group
3029 3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3030 3048
3031 3049 user_groups_rows = []
3032 3050 if expand_from_user_groups:
3033 3051 for ug in self.permission_user_groups(with_members=True):
3034 3052 for user_data in ug.members:
3035 3053 user_groups_rows.append(user_data)
3036 3054
3037 3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3038 3056
3039 3057 def permission_user_groups(self, with_members=False):
3040 3058 q = UserGroupRepoGroupToPerm.query()\
3041 3059 .filter(UserGroupRepoGroupToPerm.group == self)
3042 3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3043 3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3044 3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3045 3063
3046 3064 perm_rows = []
3047 3065 for _user_group in q.all():
3048 3066 entry = AttributeDict(_user_group.users_group.get_dict())
3049 3067 entry.permission = _user_group.permission.permission_name
3050 3068 if with_members:
3051 3069 entry.members = [x.user.get_dict()
3052 3070 for x in _user_group.users_group.members]
3053 3071 perm_rows.append(entry)
3054 3072
3055 3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3056 3074 return perm_rows
3057 3075
3058 3076 def get_api_data(self):
3059 3077 """
3060 3078 Common function for generating api data
3061 3079
3062 3080 """
3063 3081 group = self
3064 3082 data = {
3065 3083 'group_id': group.group_id,
3066 3084 'group_name': group.group_name,
3067 3085 'group_description': group.description_safe,
3068 3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3069 3087 'repositories': [x.repo_name for x in group.repositories],
3070 3088 'owner': group.user.username,
3071 3089 }
3072 3090 return data
3073 3091
3074 3092 def get_dict(self):
3075 3093 # Since we transformed `group_name` to a hybrid property, we need to
3076 3094 # keep compatibility with the code which uses `group_name` field.
3077 3095 result = super(RepoGroup, self).get_dict()
3078 3096 result['group_name'] = result.pop('_group_name', None)
3079 3097 return result
3080 3098
3081 3099
3082 3100 class Permission(Base, BaseModel):
3083 3101 __tablename__ = 'permissions'
3084 3102 __table_args__ = (
3085 3103 Index('p_perm_name_idx', 'permission_name'),
3086 3104 base_table_args,
3087 3105 )
3088 3106
3089 3107 PERMS = [
3090 3108 ('hg.admin', _('RhodeCode Super Administrator')),
3091 3109
3092 3110 ('repository.none', _('Repository no access')),
3093 3111 ('repository.read', _('Repository read access')),
3094 3112 ('repository.write', _('Repository write access')),
3095 3113 ('repository.admin', _('Repository admin access')),
3096 3114
3097 3115 ('group.none', _('Repository group no access')),
3098 3116 ('group.read', _('Repository group read access')),
3099 3117 ('group.write', _('Repository group write access')),
3100 3118 ('group.admin', _('Repository group admin access')),
3101 3119
3102 3120 ('usergroup.none', _('User group no access')),
3103 3121 ('usergroup.read', _('User group read access')),
3104 3122 ('usergroup.write', _('User group write access')),
3105 3123 ('usergroup.admin', _('User group admin access')),
3106 3124
3107 3125 ('branch.none', _('Branch no permissions')),
3108 3126 ('branch.merge', _('Branch access by web merge')),
3109 3127 ('branch.push', _('Branch access by push')),
3110 3128 ('branch.push_force', _('Branch access by push with force')),
3111 3129
3112 3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3113 3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3114 3132
3115 3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3116 3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3117 3135
3118 3136 ('hg.create.none', _('Repository creation disabled')),
3119 3137 ('hg.create.repository', _('Repository creation enabled')),
3120 3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3121 3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3122 3140
3123 3141 ('hg.fork.none', _('Repository forking disabled')),
3124 3142 ('hg.fork.repository', _('Repository forking enabled')),
3125 3143
3126 3144 ('hg.register.none', _('Registration disabled')),
3127 3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3128 3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3129 3147
3130 3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3131 3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3132 3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3133 3151
3134 3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3135 3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3136 3154
3137 3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3138 3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3139 3157 ]
3140 3158
3141 3159 # definition of system default permissions for DEFAULT user, created on
3142 3160 # system setup
3143 3161 DEFAULT_USER_PERMISSIONS = [
3144 3162 # object perms
3145 3163 'repository.read',
3146 3164 'group.read',
3147 3165 'usergroup.read',
3148 3166 # branch, for backward compat we need same value as before so forced pushed
3149 3167 'branch.push_force',
3150 3168 # global
3151 3169 'hg.create.repository',
3152 3170 'hg.repogroup.create.false',
3153 3171 'hg.usergroup.create.false',
3154 3172 'hg.create.write_on_repogroup.true',
3155 3173 'hg.fork.repository',
3156 3174 'hg.register.manual_activate',
3157 3175 'hg.password_reset.enabled',
3158 3176 'hg.extern_activate.auto',
3159 3177 'hg.inherit_default_perms.true',
3160 3178 ]
3161 3179
3162 3180 # defines which permissions are more important higher the more important
3163 3181 # Weight defines which permissions are more important.
3164 3182 # The higher number the more important.
3165 3183 PERM_WEIGHTS = {
3166 3184 'repository.none': 0,
3167 3185 'repository.read': 1,
3168 3186 'repository.write': 3,
3169 3187 'repository.admin': 4,
3170 3188
3171 3189 'group.none': 0,
3172 3190 'group.read': 1,
3173 3191 'group.write': 3,
3174 3192 'group.admin': 4,
3175 3193
3176 3194 'usergroup.none': 0,
3177 3195 'usergroup.read': 1,
3178 3196 'usergroup.write': 3,
3179 3197 'usergroup.admin': 4,
3180 3198
3181 3199 'branch.none': 0,
3182 3200 'branch.merge': 1,
3183 3201 'branch.push': 3,
3184 3202 'branch.push_force': 4,
3185 3203
3186 3204 'hg.repogroup.create.false': 0,
3187 3205 'hg.repogroup.create.true': 1,
3188 3206
3189 3207 'hg.usergroup.create.false': 0,
3190 3208 'hg.usergroup.create.true': 1,
3191 3209
3192 3210 'hg.fork.none': 0,
3193 3211 'hg.fork.repository': 1,
3194 3212 'hg.create.none': 0,
3195 3213 'hg.create.repository': 1
3196 3214 }
3197 3215
3198 3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3199 3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3200 3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3201 3219
3202 3220 def __unicode__(self):
3203 3221 return u"<%s('%s:%s')>" % (
3204 3222 self.__class__.__name__, self.permission_id, self.permission_name
3205 3223 )
3206 3224
3207 3225 @classmethod
3208 3226 def get_by_key(cls, key):
3209 3227 return cls.query().filter(cls.permission_name == key).scalar()
3210 3228
3211 3229 @classmethod
3212 3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3213 3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3214 3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3215 3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3216 3234 .filter(UserRepoToPerm.user_id == user_id)
3217 3235 if repo_id:
3218 3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3219 3237 return q.all()
3220 3238
3221 3239 @classmethod
3222 3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3223 3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3224 3242 .join(
3225 3243 Permission,
3226 3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3227 3245 .join(
3228 3246 UserRepoToPerm,
3229 3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3230 3248 .filter(UserRepoToPerm.user_id == user_id)
3231 3249
3232 3250 if repo_id:
3233 3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3234 3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3235 3253
3236 3254 @classmethod
3237 3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3238 3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3239 3257 .join(
3240 3258 Permission,
3241 3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3242 3260 .join(
3243 3261 Repository,
3244 3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3245 3263 .join(
3246 3264 UserGroup,
3247 3265 UserGroupRepoToPerm.users_group_id ==
3248 3266 UserGroup.users_group_id)\
3249 3267 .join(
3250 3268 UserGroupMember,
3251 3269 UserGroupRepoToPerm.users_group_id ==
3252 3270 UserGroupMember.users_group_id)\
3253 3271 .filter(
3254 3272 UserGroupMember.user_id == user_id,
3255 3273 UserGroup.users_group_active == true())
3256 3274 if repo_id:
3257 3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3258 3276 return q.all()
3259 3277
3260 3278 @classmethod
3261 3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3262 3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3263 3281 .join(
3264 3282 Permission,
3265 3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3266 3284 .join(
3267 3285 UserGroupRepoToPerm,
3268 3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3269 3287 .join(
3270 3288 UserGroup,
3271 3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3272 3290 .join(
3273 3291 UserGroupMember,
3274 3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3275 3293 .filter(
3276 3294 UserGroupMember.user_id == user_id,
3277 3295 UserGroup.users_group_active == true())
3278 3296
3279 3297 if repo_id:
3280 3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3281 3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3282 3300
3283 3301 @classmethod
3284 3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3285 3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3286 3304 .join(
3287 3305 Permission,
3288 3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3289 3307 .join(
3290 3308 RepoGroup,
3291 3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3292 3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3293 3311 if repo_group_id:
3294 3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3295 3313 return q.all()
3296 3314
3297 3315 @classmethod
3298 3316 def get_default_group_perms_from_user_group(
3299 3317 cls, user_id, repo_group_id=None):
3300 3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3301 3319 .join(
3302 3320 Permission,
3303 3321 UserGroupRepoGroupToPerm.permission_id ==
3304 3322 Permission.permission_id)\
3305 3323 .join(
3306 3324 RepoGroup,
3307 3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3308 3326 .join(
3309 3327 UserGroup,
3310 3328 UserGroupRepoGroupToPerm.users_group_id ==
3311 3329 UserGroup.users_group_id)\
3312 3330 .join(
3313 3331 UserGroupMember,
3314 3332 UserGroupRepoGroupToPerm.users_group_id ==
3315 3333 UserGroupMember.users_group_id)\
3316 3334 .filter(
3317 3335 UserGroupMember.user_id == user_id,
3318 3336 UserGroup.users_group_active == true())
3319 3337 if repo_group_id:
3320 3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3321 3339 return q.all()
3322 3340
3323 3341 @classmethod
3324 3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3325 3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3326 3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3327 3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3328 3346 .filter(UserUserGroupToPerm.user_id == user_id)
3329 3347 if user_group_id:
3330 3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3331 3349 return q.all()
3332 3350
3333 3351 @classmethod
3334 3352 def get_default_user_group_perms_from_user_group(
3335 3353 cls, user_id, user_group_id=None):
3336 3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3337 3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3338 3356 .join(
3339 3357 Permission,
3340 3358 UserGroupUserGroupToPerm.permission_id ==
3341 3359 Permission.permission_id)\
3342 3360 .join(
3343 3361 TargetUserGroup,
3344 3362 UserGroupUserGroupToPerm.target_user_group_id ==
3345 3363 TargetUserGroup.users_group_id)\
3346 3364 .join(
3347 3365 UserGroup,
3348 3366 UserGroupUserGroupToPerm.user_group_id ==
3349 3367 UserGroup.users_group_id)\
3350 3368 .join(
3351 3369 UserGroupMember,
3352 3370 UserGroupUserGroupToPerm.user_group_id ==
3353 3371 UserGroupMember.users_group_id)\
3354 3372 .filter(
3355 3373 UserGroupMember.user_id == user_id,
3356 3374 UserGroup.users_group_active == true())
3357 3375 if user_group_id:
3358 3376 q = q.filter(
3359 3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3360 3378
3361 3379 return q.all()
3362 3380
3363 3381
3364 3382 class UserRepoToPerm(Base, BaseModel):
3365 3383 __tablename__ = 'repo_to_perm'
3366 3384 __table_args__ = (
3367 3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3368 3386 base_table_args
3369 3387 )
3370 3388
3371 3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3372 3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3373 3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3374 3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3375 3393
3376 3394 user = relationship('User')
3377 3395 repository = relationship('Repository')
3378 3396 permission = relationship('Permission')
3379 3397
3380 3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3381 3399
3382 3400 @classmethod
3383 3401 def create(cls, user, repository, permission):
3384 3402 n = cls()
3385 3403 n.user = user
3386 3404 n.repository = repository
3387 3405 n.permission = permission
3388 3406 Session().add(n)
3389 3407 return n
3390 3408
3391 3409 def __unicode__(self):
3392 3410 return u'<%s => %s >' % (self.user, self.repository)
3393 3411
3394 3412
3395 3413 class UserUserGroupToPerm(Base, BaseModel):
3396 3414 __tablename__ = 'user_user_group_to_perm'
3397 3415 __table_args__ = (
3398 3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3399 3417 base_table_args
3400 3418 )
3401 3419
3402 3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3403 3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3404 3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3405 3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3406 3424
3407 3425 user = relationship('User')
3408 3426 user_group = relationship('UserGroup')
3409 3427 permission = relationship('Permission')
3410 3428
3411 3429 @classmethod
3412 3430 def create(cls, user, user_group, permission):
3413 3431 n = cls()
3414 3432 n.user = user
3415 3433 n.user_group = user_group
3416 3434 n.permission = permission
3417 3435 Session().add(n)
3418 3436 return n
3419 3437
3420 3438 def __unicode__(self):
3421 3439 return u'<%s => %s >' % (self.user, self.user_group)
3422 3440
3423 3441
3424 3442 class UserToPerm(Base, BaseModel):
3425 3443 __tablename__ = 'user_to_perm'
3426 3444 __table_args__ = (
3427 3445 UniqueConstraint('user_id', 'permission_id'),
3428 3446 base_table_args
3429 3447 )
3430 3448
3431 3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3432 3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3433 3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3434 3452
3435 3453 user = relationship('User')
3436 3454 permission = relationship('Permission', lazy='joined')
3437 3455
3438 3456 def __unicode__(self):
3439 3457 return u'<%s => %s >' % (self.user, self.permission)
3440 3458
3441 3459
3442 3460 class UserGroupRepoToPerm(Base, BaseModel):
3443 3461 __tablename__ = 'users_group_repo_to_perm'
3444 3462 __table_args__ = (
3445 3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3446 3464 base_table_args
3447 3465 )
3448 3466
3449 3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3450 3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3451 3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3452 3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3453 3471
3454 3472 users_group = relationship('UserGroup')
3455 3473 permission = relationship('Permission')
3456 3474 repository = relationship('Repository')
3457 3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3458 3476
3459 3477 @classmethod
3460 3478 def create(cls, users_group, repository, permission):
3461 3479 n = cls()
3462 3480 n.users_group = users_group
3463 3481 n.repository = repository
3464 3482 n.permission = permission
3465 3483 Session().add(n)
3466 3484 return n
3467 3485
3468 3486 def __unicode__(self):
3469 3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3470 3488
3471 3489
3472 3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3473 3491 __tablename__ = 'user_group_user_group_to_perm'
3474 3492 __table_args__ = (
3475 3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3476 3494 CheckConstraint('target_user_group_id != user_group_id'),
3477 3495 base_table_args
3478 3496 )
3479 3497
3480 3498 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)
3481 3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3482 3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3483 3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3484 3502
3485 3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3486 3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3487 3505 permission = relationship('Permission')
3488 3506
3489 3507 @classmethod
3490 3508 def create(cls, target_user_group, user_group, permission):
3491 3509 n = cls()
3492 3510 n.target_user_group = target_user_group
3493 3511 n.user_group = user_group
3494 3512 n.permission = permission
3495 3513 Session().add(n)
3496 3514 return n
3497 3515
3498 3516 def __unicode__(self):
3499 3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3500 3518
3501 3519
3502 3520 class UserGroupToPerm(Base, BaseModel):
3503 3521 __tablename__ = 'users_group_to_perm'
3504 3522 __table_args__ = (
3505 3523 UniqueConstraint('users_group_id', 'permission_id',),
3506 3524 base_table_args
3507 3525 )
3508 3526
3509 3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3510 3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3511 3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3512 3530
3513 3531 users_group = relationship('UserGroup')
3514 3532 permission = relationship('Permission')
3515 3533
3516 3534
3517 3535 class UserRepoGroupToPerm(Base, BaseModel):
3518 3536 __tablename__ = 'user_repo_group_to_perm'
3519 3537 __table_args__ = (
3520 3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3521 3539 base_table_args
3522 3540 )
3523 3541
3524 3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3525 3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3526 3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3527 3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3528 3546
3529 3547 user = relationship('User')
3530 3548 group = relationship('RepoGroup')
3531 3549 permission = relationship('Permission')
3532 3550
3533 3551 @classmethod
3534 3552 def create(cls, user, repository_group, permission):
3535 3553 n = cls()
3536 3554 n.user = user
3537 3555 n.group = repository_group
3538 3556 n.permission = permission
3539 3557 Session().add(n)
3540 3558 return n
3541 3559
3542 3560
3543 3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3544 3562 __tablename__ = 'users_group_repo_group_to_perm'
3545 3563 __table_args__ = (
3546 3564 UniqueConstraint('users_group_id', 'group_id'),
3547 3565 base_table_args
3548 3566 )
3549 3567
3550 3568 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)
3551 3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3552 3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3553 3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3554 3572
3555 3573 users_group = relationship('UserGroup')
3556 3574 permission = relationship('Permission')
3557 3575 group = relationship('RepoGroup')
3558 3576
3559 3577 @classmethod
3560 3578 def create(cls, user_group, repository_group, permission):
3561 3579 n = cls()
3562 3580 n.users_group = user_group
3563 3581 n.group = repository_group
3564 3582 n.permission = permission
3565 3583 Session().add(n)
3566 3584 return n
3567 3585
3568 3586 def __unicode__(self):
3569 3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3570 3588
3571 3589
3572 3590 class Statistics(Base, BaseModel):
3573 3591 __tablename__ = 'statistics'
3574 3592 __table_args__ = (
3575 3593 base_table_args
3576 3594 )
3577 3595
3578 3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3579 3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3580 3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3581 3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3582 3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3583 3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3584 3602
3585 3603 repository = relationship('Repository', single_parent=True)
3586 3604
3587 3605
3588 3606 class UserFollowing(Base, BaseModel):
3589 3607 __tablename__ = 'user_followings'
3590 3608 __table_args__ = (
3591 3609 UniqueConstraint('user_id', 'follows_repository_id'),
3592 3610 UniqueConstraint('user_id', 'follows_user_id'),
3593 3611 base_table_args
3594 3612 )
3595 3613
3596 3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3597 3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3598 3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3599 3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3600 3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3601 3619
3602 3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3603 3621
3604 3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3605 3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3606 3624
3607 3625 @classmethod
3608 3626 def get_repo_followers(cls, repo_id):
3609 3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3610 3628
3611 3629
3612 3630 class CacheKey(Base, BaseModel):
3613 3631 __tablename__ = 'cache_invalidation'
3614 3632 __table_args__ = (
3615 3633 UniqueConstraint('cache_key'),
3616 3634 Index('key_idx', 'cache_key'),
3617 3635 base_table_args,
3618 3636 )
3619 3637
3620 3638 CACHE_TYPE_FEED = 'FEED'
3621 3639
3622 3640 # namespaces used to register process/thread aware caches
3623 3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3624 3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3625 3643
3626 3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3627 3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3628 3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3629 3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3630 3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3631 3649
3632 3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3633 3651 self.cache_key = cache_key
3634 3652 self.cache_args = cache_args
3635 3653 self.cache_active = False
3636 3654 # first key should be same for all entries, since all workers should share it
3637 3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3638 3656
3639 3657 def __unicode__(self):
3640 3658 return u"<%s('%s:%s[%s]')>" % (
3641 3659 self.__class__.__name__,
3642 3660 self.cache_id, self.cache_key, self.cache_active)
3643 3661
3644 3662 def _cache_key_partition(self):
3645 3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3646 3664 return prefix, repo_name, suffix
3647 3665
3648 3666 def get_prefix(self):
3649 3667 """
3650 3668 Try to extract prefix from existing cache key. The key could consist
3651 3669 of prefix, repo_name, suffix
3652 3670 """
3653 3671 # this returns prefix, repo_name, suffix
3654 3672 return self._cache_key_partition()[0]
3655 3673
3656 3674 def get_suffix(self):
3657 3675 """
3658 3676 get suffix that might have been used in _get_cache_key to
3659 3677 generate self.cache_key. Only used for informational purposes
3660 3678 in repo_edit.mako.
3661 3679 """
3662 3680 # prefix, repo_name, suffix
3663 3681 return self._cache_key_partition()[2]
3664 3682
3665 3683 @classmethod
3666 3684 def generate_new_state_uid(cls, based_on=None):
3667 3685 if based_on:
3668 3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3669 3687 else:
3670 3688 return str(uuid.uuid4())
3671 3689
3672 3690 @classmethod
3673 3691 def delete_all_cache(cls):
3674 3692 """
3675 3693 Delete all cache keys from database.
3676 3694 Should only be run when all instances are down and all entries
3677 3695 thus stale.
3678 3696 """
3679 3697 cls.query().delete()
3680 3698 Session().commit()
3681 3699
3682 3700 @classmethod
3683 3701 def set_invalidate(cls, cache_uid, delete=False):
3684 3702 """
3685 3703 Mark all caches of a repo as invalid in the database.
3686 3704 """
3687 3705
3688 3706 try:
3689 3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3690 3708 if delete:
3691 3709 qry.delete()
3692 3710 log.debug('cache objects deleted for cache args %s',
3693 3711 safe_str(cache_uid))
3694 3712 else:
3695 3713 qry.update({"cache_active": False,
3696 3714 "cache_state_uid": cls.generate_new_state_uid()})
3697 3715 log.debug('cache objects marked as invalid for cache args %s',
3698 3716 safe_str(cache_uid))
3699 3717
3700 3718 Session().commit()
3701 3719 except Exception:
3702 3720 log.exception(
3703 3721 'Cache key invalidation failed for cache args %s',
3704 3722 safe_str(cache_uid))
3705 3723 Session().rollback()
3706 3724
3707 3725 @classmethod
3708 3726 def get_active_cache(cls, cache_key):
3709 3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3710 3728 if inv_obj:
3711 3729 return inv_obj
3712 3730 return None
3713 3731
3714 3732 @classmethod
3715 3733 def get_namespace_map(cls, namespace):
3716 3734 return {
3717 3735 x.cache_key: x
3718 3736 for x in cls.query().filter(cls.cache_args == namespace)}
3719 3737
3720 3738
3721 3739 class ChangesetComment(Base, BaseModel):
3722 3740 __tablename__ = 'changeset_comments'
3723 3741 __table_args__ = (
3724 3742 Index('cc_revision_idx', 'revision'),
3725 3743 base_table_args,
3726 3744 )
3727 3745
3728 3746 COMMENT_OUTDATED = u'comment_outdated'
3729 3747 COMMENT_TYPE_NOTE = u'note'
3730 3748 COMMENT_TYPE_TODO = u'todo'
3731 3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3732 3750
3733 3751 OP_IMMUTABLE = u'immutable'
3734 3752 OP_CHANGEABLE = u'changeable'
3735 3753
3736 3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3737 3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3738 3756 revision = Column('revision', String(40), nullable=True)
3739 3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3740 3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3741 3759 line_no = Column('line_no', Unicode(10), nullable=True)
3742 3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3743 3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3744 3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3745 3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3746 3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3747 3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3748 3766 renderer = Column('renderer', Unicode(64), nullable=True)
3749 3767 display_state = Column('display_state', Unicode(128), nullable=True)
3750 3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3751 3769
3752 3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3753 3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3754 3772
3755 3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3756 3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3757 3775
3758 3776 author = relationship('User', lazy='joined')
3759 3777 repo = relationship('Repository')
3760 3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3761 3779 pull_request = relationship('PullRequest', lazy='joined')
3762 3780 pull_request_version = relationship('PullRequestVersion')
3763 3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3764 3782
3765 3783 @classmethod
3766 3784 def get_users(cls, revision=None, pull_request_id=None):
3767 3785 """
3768 3786 Returns user associated with this ChangesetComment. ie those
3769 3787 who actually commented
3770 3788
3771 3789 :param cls:
3772 3790 :param revision:
3773 3791 """
3774 3792 q = Session().query(User)\
3775 3793 .join(ChangesetComment.author)
3776 3794 if revision:
3777 3795 q = q.filter(cls.revision == revision)
3778 3796 elif pull_request_id:
3779 3797 q = q.filter(cls.pull_request_id == pull_request_id)
3780 3798 return q.all()
3781 3799
3782 3800 @classmethod
3783 3801 def get_index_from_version(cls, pr_version, versions):
3784 3802 num_versions = [x.pull_request_version_id for x in versions]
3785 3803 try:
3786 return num_versions.index(pr_version) +1
3804 return num_versions.index(pr_version) + 1
3787 3805 except (IndexError, ValueError):
3788 3806 return
3789 3807
3790 3808 @property
3791 3809 def outdated(self):
3792 3810 return self.display_state == self.COMMENT_OUTDATED
3793 3811
3794 3812 @property
3795 3813 def immutable(self):
3796 3814 return self.immutable_state == self.OP_IMMUTABLE
3797 3815
3798 3816 def outdated_at_version(self, version):
3799 3817 """
3800 3818 Checks if comment is outdated for given pull request version
3801 3819 """
3802 3820 return self.outdated and self.pull_request_version_id != version
3803 3821
3804 3822 def older_than_version(self, version):
3805 3823 """
3806 3824 Checks if comment is made from previous version than given
3807 3825 """
3808 3826 if version is None:
3809 3827 return self.pull_request_version_id is not None
3810 3828
3811 3829 return self.pull_request_version_id < version
3812 3830
3813 3831 @property
3814 3832 def commit_id(self):
3815 3833 """New style naming to stop using .revision"""
3816 3834 return self.revision
3817 3835
3818 3836 @property
3819 3837 def resolved(self):
3820 3838 return self.resolved_by[0] if self.resolved_by else None
3821 3839
3822 3840 @property
3823 3841 def is_todo(self):
3824 3842 return self.comment_type == self.COMMENT_TYPE_TODO
3825 3843
3826 3844 @property
3827 3845 def is_inline(self):
3828 3846 return self.line_no and self.f_path
3829 3847
3830 3848 def get_index_version(self, versions):
3831 3849 return self.get_index_from_version(
3832 3850 self.pull_request_version_id, versions)
3833 3851
3834 3852 def __repr__(self):
3835 3853 if self.comment_id:
3836 3854 return '<DB:Comment #%s>' % self.comment_id
3837 3855 else:
3838 3856 return '<DB:Comment at %#x>' % id(self)
3839 3857
3840 3858 def get_api_data(self):
3841 3859 comment = self
3842 3860 data = {
3843 3861 'comment_id': comment.comment_id,
3844 3862 'comment_type': comment.comment_type,
3845 3863 'comment_text': comment.text,
3846 3864 'comment_status': comment.status_change,
3847 3865 'comment_f_path': comment.f_path,
3848 3866 'comment_lineno': comment.line_no,
3849 3867 'comment_author': comment.author,
3850 3868 'comment_created_on': comment.created_on,
3851 3869 'comment_resolved_by': self.resolved,
3852 3870 'comment_commit_id': comment.revision,
3853 3871 'comment_pull_request_id': comment.pull_request_id,
3854 3872 }
3855 3873 return data
3856 3874
3857 3875 def __json__(self):
3858 3876 data = dict()
3859 3877 data.update(self.get_api_data())
3860 3878 return data
3861 3879
3862 3880
3863 3881 class ChangesetCommentHistory(Base, BaseModel):
3864 3882 __tablename__ = 'changeset_comments_history'
3865 3883 __table_args__ = (
3866 3884 Index('cch_comment_id_idx', 'comment_id'),
3867 3885 base_table_args,
3868 3886 )
3869 3887
3870 3888 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3871 3889 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3872 3890 version = Column("version", Integer(), nullable=False, default=0)
3873 3891 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3874 3892 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3875 3893 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3876 3894 deleted = Column('deleted', Boolean(), default=False)
3877 3895
3878 3896 author = relationship('User', lazy='joined')
3879 3897 comment = relationship('ChangesetComment', cascade="all, delete")
3880 3898
3881 3899 @classmethod
3882 3900 def get_version(cls, comment_id):
3883 3901 q = Session().query(ChangesetCommentHistory).filter(
3884 3902 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3885 3903 if q.count() == 0:
3886 3904 return 1
3887 3905 elif q.count() >= q[0].version:
3888 3906 return q.count() + 1
3889 3907 else:
3890 3908 return q[0].version + 1
3891 3909
3892 3910
3893 3911 class ChangesetStatus(Base, BaseModel):
3894 3912 __tablename__ = 'changeset_statuses'
3895 3913 __table_args__ = (
3896 3914 Index('cs_revision_idx', 'revision'),
3897 3915 Index('cs_version_idx', 'version'),
3898 3916 UniqueConstraint('repo_id', 'revision', 'version'),
3899 3917 base_table_args
3900 3918 )
3901 3919
3902 3920 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3903 3921 STATUS_APPROVED = 'approved'
3904 3922 STATUS_REJECTED = 'rejected'
3905 3923 STATUS_UNDER_REVIEW = 'under_review'
3906 3924
3907 3925 STATUSES = [
3908 3926 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3909 3927 (STATUS_APPROVED, _("Approved")),
3910 3928 (STATUS_REJECTED, _("Rejected")),
3911 3929 (STATUS_UNDER_REVIEW, _("Under Review")),
3912 3930 ]
3913 3931
3914 3932 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3915 3933 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3916 3934 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3917 3935 revision = Column('revision', String(40), nullable=False)
3918 3936 status = Column('status', String(128), nullable=False, default=DEFAULT)
3919 3937 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3920 3938 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3921 3939 version = Column('version', Integer(), nullable=False, default=0)
3922 3940 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3923 3941
3924 3942 author = relationship('User', lazy='joined')
3925 3943 repo = relationship('Repository')
3926 3944 comment = relationship('ChangesetComment', lazy='joined')
3927 3945 pull_request = relationship('PullRequest', lazy='joined')
3928 3946
3929 3947 def __unicode__(self):
3930 3948 return u"<%s('%s[v%s]:%s')>" % (
3931 3949 self.__class__.__name__,
3932 3950 self.status, self.version, self.author
3933 3951 )
3934 3952
3935 3953 @classmethod
3936 3954 def get_status_lbl(cls, value):
3937 3955 return dict(cls.STATUSES).get(value)
3938 3956
3939 3957 @property
3940 3958 def status_lbl(self):
3941 3959 return ChangesetStatus.get_status_lbl(self.status)
3942 3960
3943 3961 def get_api_data(self):
3944 3962 status = self
3945 3963 data = {
3946 3964 'status_id': status.changeset_status_id,
3947 3965 'status': status.status,
3948 3966 }
3949 3967 return data
3950 3968
3951 3969 def __json__(self):
3952 3970 data = dict()
3953 3971 data.update(self.get_api_data())
3954 3972 return data
3955 3973
3956 3974
3957 3975 class _SetState(object):
3958 3976 """
3959 3977 Context processor allowing changing state for sensitive operation such as
3960 3978 pull request update or merge
3961 3979 """
3962 3980
3963 3981 def __init__(self, pull_request, pr_state, back_state=None):
3964 3982 self._pr = pull_request
3965 3983 self._org_state = back_state or pull_request.pull_request_state
3966 3984 self._pr_state = pr_state
3967 3985 self._current_state = None
3968 3986
3969 3987 def __enter__(self):
3970 3988 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3971 3989 self._pr, self._pr_state)
3972 3990 self.set_pr_state(self._pr_state)
3973 3991 return self
3974 3992
3975 3993 def __exit__(self, exc_type, exc_val, exc_tb):
3976 3994 if exc_val is not None:
3977 3995 log.error(traceback.format_exc(exc_tb))
3978 3996 return None
3979 3997
3980 3998 self.set_pr_state(self._org_state)
3981 3999 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3982 4000 self._pr, self._org_state)
3983 4001
3984 4002 @property
3985 4003 def state(self):
3986 4004 return self._current_state
3987 4005
3988 4006 def set_pr_state(self, pr_state):
3989 4007 try:
3990 4008 self._pr.pull_request_state = pr_state
3991 4009 Session().add(self._pr)
3992 4010 Session().commit()
3993 4011 self._current_state = pr_state
3994 4012 except Exception:
3995 4013 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3996 4014 raise
3997 4015
3998 4016
3999 4017 class _PullRequestBase(BaseModel):
4000 4018 """
4001 4019 Common attributes of pull request and version entries.
4002 4020 """
4003 4021
4004 4022 # .status values
4005 4023 STATUS_NEW = u'new'
4006 4024 STATUS_OPEN = u'open'
4007 4025 STATUS_CLOSED = u'closed'
4008 4026
4009 4027 # available states
4010 4028 STATE_CREATING = u'creating'
4011 4029 STATE_UPDATING = u'updating'
4012 4030 STATE_MERGING = u'merging'
4013 4031 STATE_CREATED = u'created'
4014 4032
4015 4033 title = Column('title', Unicode(255), nullable=True)
4016 4034 description = Column(
4017 4035 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4018 4036 nullable=True)
4019 4037 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4020 4038
4021 4039 # new/open/closed status of pull request (not approve/reject/etc)
4022 4040 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4023 4041 created_on = Column(
4024 4042 'created_on', DateTime(timezone=False), nullable=False,
4025 4043 default=datetime.datetime.now)
4026 4044 updated_on = Column(
4027 4045 'updated_on', DateTime(timezone=False), nullable=False,
4028 4046 default=datetime.datetime.now)
4029 4047
4030 4048 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4031 4049
4032 4050 @declared_attr
4033 4051 def user_id(cls):
4034 4052 return Column(
4035 4053 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4036 4054 unique=None)
4037 4055
4038 4056 # 500 revisions max
4039 4057 _revisions = Column(
4040 4058 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4041 4059
4042 4060 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4043 4061
4044 4062 @declared_attr
4045 4063 def source_repo_id(cls):
4046 4064 # TODO: dan: rename column to source_repo_id
4047 4065 return Column(
4048 4066 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4049 4067 nullable=False)
4050 4068
4051 4069 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4052 4070
4053 4071 @hybrid_property
4054 4072 def source_ref(self):
4055 4073 return self._source_ref
4056 4074
4057 4075 @source_ref.setter
4058 4076 def source_ref(self, val):
4059 4077 parts = (val or '').split(':')
4060 4078 if len(parts) != 3:
4061 4079 raise ValueError(
4062 4080 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4063 4081 self._source_ref = safe_unicode(val)
4064 4082
4065 4083 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4066 4084
4067 4085 @hybrid_property
4068 4086 def target_ref(self):
4069 4087 return self._target_ref
4070 4088
4071 4089 @target_ref.setter
4072 4090 def target_ref(self, val):
4073 4091 parts = (val or '').split(':')
4074 4092 if len(parts) != 3:
4075 4093 raise ValueError(
4076 4094 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4077 4095 self._target_ref = safe_unicode(val)
4078 4096
4079 4097 @declared_attr
4080 4098 def target_repo_id(cls):
4081 4099 # TODO: dan: rename column to target_repo_id
4082 4100 return Column(
4083 4101 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4084 4102 nullable=False)
4085 4103
4086 4104 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4087 4105
4088 4106 # TODO: dan: rename column to last_merge_source_rev
4089 4107 _last_merge_source_rev = Column(
4090 4108 'last_merge_org_rev', String(40), nullable=True)
4091 4109 # TODO: dan: rename column to last_merge_target_rev
4092 4110 _last_merge_target_rev = Column(
4093 4111 'last_merge_other_rev', String(40), nullable=True)
4094 4112 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4095 4113 last_merge_metadata = Column(
4096 4114 'last_merge_metadata', MutationObj.as_mutable(
4097 4115 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4098 4116
4099 4117 merge_rev = Column('merge_rev', String(40), nullable=True)
4100 4118
4101 4119 reviewer_data = Column(
4102 4120 'reviewer_data_json', MutationObj.as_mutable(
4103 4121 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4104 4122
4105 4123 @property
4106 4124 def reviewer_data_json(self):
4107 4125 return json.dumps(self.reviewer_data)
4108 4126
4109 4127 @property
4110 4128 def work_in_progress(self):
4111 4129 """checks if pull request is work in progress by checking the title"""
4112 4130 title = self.title.upper()
4113 4131 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4114 4132 return True
4115 4133 return False
4116 4134
4117 4135 @hybrid_property
4118 4136 def description_safe(self):
4119 4137 from rhodecode.lib import helpers as h
4120 4138 return h.escape(self.description)
4121 4139
4122 4140 @hybrid_property
4123 4141 def revisions(self):
4124 4142 return self._revisions.split(':') if self._revisions else []
4125 4143
4126 4144 @revisions.setter
4127 4145 def revisions(self, val):
4128 4146 self._revisions = u':'.join(val)
4129 4147
4130 4148 @hybrid_property
4131 4149 def last_merge_status(self):
4132 4150 return safe_int(self._last_merge_status)
4133 4151
4134 4152 @last_merge_status.setter
4135 4153 def last_merge_status(self, val):
4136 4154 self._last_merge_status = val
4137 4155
4138 4156 @declared_attr
4139 4157 def author(cls):
4140 4158 return relationship('User', lazy='joined')
4141 4159
4142 4160 @declared_attr
4143 4161 def source_repo(cls):
4144 4162 return relationship(
4145 4163 'Repository',
4146 4164 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4147 4165
4148 4166 @property
4149 4167 def source_ref_parts(self):
4150 4168 return self.unicode_to_reference(self.source_ref)
4151 4169
4152 4170 @declared_attr
4153 4171 def target_repo(cls):
4154 4172 return relationship(
4155 4173 'Repository',
4156 4174 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4157 4175
4158 4176 @property
4159 4177 def target_ref_parts(self):
4160 4178 return self.unicode_to_reference(self.target_ref)
4161 4179
4162 4180 @property
4163 4181 def shadow_merge_ref(self):
4164 4182 return self.unicode_to_reference(self._shadow_merge_ref)
4165 4183
4166 4184 @shadow_merge_ref.setter
4167 4185 def shadow_merge_ref(self, ref):
4168 4186 self._shadow_merge_ref = self.reference_to_unicode(ref)
4169 4187
4170 4188 @staticmethod
4171 4189 def unicode_to_reference(raw):
4172 4190 """
4173 4191 Convert a unicode (or string) to a reference object.
4174 4192 If unicode evaluates to False it returns None.
4175 4193 """
4176 4194 if raw:
4177 4195 refs = raw.split(':')
4178 4196 return Reference(*refs)
4179 4197 else:
4180 4198 return None
4181 4199
4182 4200 @staticmethod
4183 4201 def reference_to_unicode(ref):
4184 4202 """
4185 4203 Convert a reference object to unicode.
4186 4204 If reference is None it returns None.
4187 4205 """
4188 4206 if ref:
4189 4207 return u':'.join(ref)
4190 4208 else:
4191 4209 return None
4192 4210
4193 4211 def get_api_data(self, with_merge_state=True):
4194 4212 from rhodecode.model.pull_request import PullRequestModel
4195 4213
4196 4214 pull_request = self
4197 4215 if with_merge_state:
4198 4216 merge_response, merge_status, msg = \
4199 4217 PullRequestModel().merge_status(pull_request)
4200 4218 merge_state = {
4201 4219 'status': merge_status,
4202 4220 'message': safe_unicode(msg),
4203 4221 }
4204 4222 else:
4205 4223 merge_state = {'status': 'not_available',
4206 4224 'message': 'not_available'}
4207 4225
4208 4226 merge_data = {
4209 4227 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4210 4228 'reference': (
4211 4229 pull_request.shadow_merge_ref._asdict()
4212 4230 if pull_request.shadow_merge_ref else None),
4213 4231 }
4214 4232
4215 4233 data = {
4216 4234 'pull_request_id': pull_request.pull_request_id,
4217 4235 'url': PullRequestModel().get_url(pull_request),
4218 4236 'title': pull_request.title,
4219 4237 'description': pull_request.description,
4220 4238 'status': pull_request.status,
4221 4239 'state': pull_request.pull_request_state,
4222 4240 'created_on': pull_request.created_on,
4223 4241 'updated_on': pull_request.updated_on,
4224 4242 'commit_ids': pull_request.revisions,
4225 4243 'review_status': pull_request.calculated_review_status(),
4226 4244 'mergeable': merge_state,
4227 4245 'source': {
4228 4246 'clone_url': pull_request.source_repo.clone_url(),
4229 4247 'repository': pull_request.source_repo.repo_name,
4230 4248 'reference': {
4231 4249 'name': pull_request.source_ref_parts.name,
4232 4250 'type': pull_request.source_ref_parts.type,
4233 4251 'commit_id': pull_request.source_ref_parts.commit_id,
4234 4252 },
4235 4253 },
4236 4254 'target': {
4237 4255 'clone_url': pull_request.target_repo.clone_url(),
4238 4256 'repository': pull_request.target_repo.repo_name,
4239 4257 'reference': {
4240 4258 'name': pull_request.target_ref_parts.name,
4241 4259 'type': pull_request.target_ref_parts.type,
4242 4260 'commit_id': pull_request.target_ref_parts.commit_id,
4243 4261 },
4244 4262 },
4245 4263 'merge': merge_data,
4246 4264 'author': pull_request.author.get_api_data(include_secrets=False,
4247 4265 details='basic'),
4248 4266 'reviewers': [
4249 4267 {
4250 4268 'user': reviewer.get_api_data(include_secrets=False,
4251 4269 details='basic'),
4252 4270 'reasons': reasons,
4253 4271 'review_status': st[0][1].status if st else 'not_reviewed',
4254 4272 }
4255 4273 for obj, reviewer, reasons, mandatory, st in
4256 4274 pull_request.reviewers_statuses()
4257 4275 ]
4258 4276 }
4259 4277
4260 4278 return data
4261 4279
4262 4280 def set_state(self, pull_request_state, final_state=None):
4263 4281 """
4264 4282 # goes from initial state to updating to initial state.
4265 4283 # initial state can be changed by specifying back_state=
4266 4284 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4267 4285 pull_request.merge()
4268 4286
4269 4287 :param pull_request_state:
4270 4288 :param final_state:
4271 4289
4272 4290 """
4273 4291
4274 4292 return _SetState(self, pull_request_state, back_state=final_state)
4275 4293
4276 4294
4277 4295 class PullRequest(Base, _PullRequestBase):
4278 4296 __tablename__ = 'pull_requests'
4279 4297 __table_args__ = (
4280 4298 base_table_args,
4281 4299 )
4282 4300
4283 4301 pull_request_id = Column(
4284 4302 'pull_request_id', Integer(), nullable=False, primary_key=True)
4285 4303
4286 4304 def __repr__(self):
4287 4305 if self.pull_request_id:
4288 4306 return '<DB:PullRequest #%s>' % self.pull_request_id
4289 4307 else:
4290 4308 return '<DB:PullRequest at %#x>' % id(self)
4291 4309
4292 4310 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4293 4311 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4294 4312 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4295 4313 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4296 4314 lazy='dynamic')
4297 4315
4298 4316 @classmethod
4299 4317 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4300 4318 internal_methods=None):
4301 4319
4302 4320 class PullRequestDisplay(object):
4303 4321 """
4304 4322 Special object wrapper for showing PullRequest data via Versions
4305 4323 It mimics PR object as close as possible. This is read only object
4306 4324 just for display
4307 4325 """
4308 4326
4309 4327 def __init__(self, attrs, internal=None):
4310 4328 self.attrs = attrs
4311 4329 # internal have priority over the given ones via attrs
4312 4330 self.internal = internal or ['versions']
4313 4331
4314 4332 def __getattr__(self, item):
4315 4333 if item in self.internal:
4316 4334 return getattr(self, item)
4317 4335 try:
4318 4336 return self.attrs[item]
4319 4337 except KeyError:
4320 4338 raise AttributeError(
4321 4339 '%s object has no attribute %s' % (self, item))
4322 4340
4323 4341 def __repr__(self):
4324 4342 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4325 4343
4326 4344 def versions(self):
4327 4345 return pull_request_obj.versions.order_by(
4328 4346 PullRequestVersion.pull_request_version_id).all()
4329 4347
4330 4348 def is_closed(self):
4331 4349 return pull_request_obj.is_closed()
4332 4350
4333 4351 def is_state_changing(self):
4334 4352 return pull_request_obj.is_state_changing()
4335 4353
4336 4354 @property
4337 4355 def pull_request_version_id(self):
4338 4356 return getattr(pull_request_obj, 'pull_request_version_id', None)
4339 4357
4340 4358 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4341 4359
4342 4360 attrs.author = StrictAttributeDict(
4343 4361 pull_request_obj.author.get_api_data())
4344 4362 if pull_request_obj.target_repo:
4345 4363 attrs.target_repo = StrictAttributeDict(
4346 4364 pull_request_obj.target_repo.get_api_data())
4347 4365 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4348 4366
4349 4367 if pull_request_obj.source_repo:
4350 4368 attrs.source_repo = StrictAttributeDict(
4351 4369 pull_request_obj.source_repo.get_api_data())
4352 4370 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4353 4371
4354 4372 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4355 4373 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4356 4374 attrs.revisions = pull_request_obj.revisions
4357 4375 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4358 4376 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4359 4377 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4360 4378 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4361 4379
4362 4380 return PullRequestDisplay(attrs, internal=internal_methods)
4363 4381
4364 4382 def is_closed(self):
4365 4383 return self.status == self.STATUS_CLOSED
4366 4384
4367 4385 def is_state_changing(self):
4368 4386 return self.pull_request_state != PullRequest.STATE_CREATED
4369 4387
4370 4388 def __json__(self):
4371 4389 return {
4372 4390 'revisions': self.revisions,
4373 4391 'versions': self.versions_count
4374 4392 }
4375 4393
4376 4394 def calculated_review_status(self):
4377 4395 from rhodecode.model.changeset_status import ChangesetStatusModel
4378 4396 return ChangesetStatusModel().calculated_review_status(self)
4379 4397
4380 4398 def reviewers_statuses(self):
4381 4399 from rhodecode.model.changeset_status import ChangesetStatusModel
4382 4400 return ChangesetStatusModel().reviewers_statuses(self)
4383 4401
4384 4402 @property
4385 4403 def workspace_id(self):
4386 4404 from rhodecode.model.pull_request import PullRequestModel
4387 4405 return PullRequestModel()._workspace_id(self)
4388 4406
4389 4407 def get_shadow_repo(self):
4390 4408 workspace_id = self.workspace_id
4391 4409 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4392 4410 if os.path.isdir(shadow_repository_path):
4393 4411 vcs_obj = self.target_repo.scm_instance()
4394 4412 return vcs_obj.get_shadow_instance(shadow_repository_path)
4395 4413
4396 4414 @property
4397 4415 def versions_count(self):
4398 4416 """
4399 4417 return number of versions this PR have, e.g a PR that once been
4400 4418 updated will have 2 versions
4401 4419 """
4402 4420 return self.versions.count() + 1
4403 4421
4404 4422
4405 4423 class PullRequestVersion(Base, _PullRequestBase):
4406 4424 __tablename__ = 'pull_request_versions'
4407 4425 __table_args__ = (
4408 4426 base_table_args,
4409 4427 )
4410 4428
4411 4429 pull_request_version_id = Column(
4412 4430 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4413 4431 pull_request_id = Column(
4414 4432 'pull_request_id', Integer(),
4415 4433 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4416 4434 pull_request = relationship('PullRequest')
4417 4435
4418 4436 def __repr__(self):
4419 4437 if self.pull_request_version_id:
4420 4438 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4421 4439 else:
4422 4440 return '<DB:PullRequestVersion at %#x>' % id(self)
4423 4441
4424 4442 @property
4425 4443 def reviewers(self):
4426 4444 return self.pull_request.reviewers
4427 4445
4428 4446 @property
4429 4447 def versions(self):
4430 4448 return self.pull_request.versions
4431 4449
4432 4450 def is_closed(self):
4433 4451 # calculate from original
4434 4452 return self.pull_request.status == self.STATUS_CLOSED
4435 4453
4436 4454 def is_state_changing(self):
4437 4455 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4438 4456
4439 4457 def calculated_review_status(self):
4440 4458 return self.pull_request.calculated_review_status()
4441 4459
4442 4460 def reviewers_statuses(self):
4443 4461 return self.pull_request.reviewers_statuses()
4444 4462
4445 4463
4446 4464 class PullRequestReviewers(Base, BaseModel):
4447 4465 __tablename__ = 'pull_request_reviewers'
4448 4466 __table_args__ = (
4449 4467 base_table_args,
4450 4468 )
4451 4469
4452 4470 @hybrid_property
4453 4471 def reasons(self):
4454 4472 if not self._reasons:
4455 4473 return []
4456 4474 return self._reasons
4457 4475
4458 4476 @reasons.setter
4459 4477 def reasons(self, val):
4460 4478 val = val or []
4461 4479 if any(not isinstance(x, compat.string_types) for x in val):
4462 4480 raise Exception('invalid reasons type, must be list of strings')
4463 4481 self._reasons = val
4464 4482
4465 4483 pull_requests_reviewers_id = Column(
4466 4484 'pull_requests_reviewers_id', Integer(), nullable=False,
4467 4485 primary_key=True)
4468 4486 pull_request_id = Column(
4469 4487 "pull_request_id", Integer(),
4470 4488 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4471 4489 user_id = Column(
4472 4490 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4473 4491 _reasons = Column(
4474 4492 'reason', MutationList.as_mutable(
4475 4493 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4476 4494
4477 4495 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4478 4496 user = relationship('User')
4479 4497 pull_request = relationship('PullRequest')
4480 4498
4481 4499 rule_data = Column(
4482 4500 'rule_data_json',
4483 4501 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4484 4502
4485 4503 def rule_user_group_data(self):
4486 4504 """
4487 4505 Returns the voting user group rule data for this reviewer
4488 4506 """
4489 4507
4490 4508 if self.rule_data and 'vote_rule' in self.rule_data:
4491 4509 user_group_data = {}
4492 4510 if 'rule_user_group_entry_id' in self.rule_data:
4493 4511 # means a group with voting rules !
4494 4512 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4495 4513 user_group_data['name'] = self.rule_data['rule_name']
4496 4514 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4497 4515
4498 4516 return user_group_data
4499 4517
4500 4518 def __unicode__(self):
4501 4519 return u"<%s('id:%s')>" % (self.__class__.__name__,
4502 4520 self.pull_requests_reviewers_id)
4503 4521
4504 4522
4505 4523 class Notification(Base, BaseModel):
4506 4524 __tablename__ = 'notifications'
4507 4525 __table_args__ = (
4508 4526 Index('notification_type_idx', 'type'),
4509 4527 base_table_args,
4510 4528 )
4511 4529
4512 4530 TYPE_CHANGESET_COMMENT = u'cs_comment'
4513 4531 TYPE_MESSAGE = u'message'
4514 4532 TYPE_MENTION = u'mention'
4515 4533 TYPE_REGISTRATION = u'registration'
4516 4534 TYPE_PULL_REQUEST = u'pull_request'
4517 4535 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4518 4536 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4519 4537
4520 4538 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4521 4539 subject = Column('subject', Unicode(512), nullable=True)
4522 4540 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4523 4541 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4524 4542 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4525 4543 type_ = Column('type', Unicode(255))
4526 4544
4527 4545 created_by_user = relationship('User')
4528 4546 notifications_to_users = relationship('UserNotification', lazy='joined',
4529 4547 cascade="all, delete-orphan")
4530 4548
4531 4549 @property
4532 4550 def recipients(self):
4533 4551 return [x.user for x in UserNotification.query()\
4534 4552 .filter(UserNotification.notification == self)\
4535 4553 .order_by(UserNotification.user_id.asc()).all()]
4536 4554
4537 4555 @classmethod
4538 4556 def create(cls, created_by, subject, body, recipients, type_=None):
4539 4557 if type_ is None:
4540 4558 type_ = Notification.TYPE_MESSAGE
4541 4559
4542 4560 notification = cls()
4543 4561 notification.created_by_user = created_by
4544 4562 notification.subject = subject
4545 4563 notification.body = body
4546 4564 notification.type_ = type_
4547 4565 notification.created_on = datetime.datetime.now()
4548 4566
4549 4567 # For each recipient link the created notification to his account
4550 4568 for u in recipients:
4551 4569 assoc = UserNotification()
4552 4570 assoc.user_id = u.user_id
4553 4571 assoc.notification = notification
4554 4572
4555 4573 # if created_by is inside recipients mark his notification
4556 4574 # as read
4557 4575 if u.user_id == created_by.user_id:
4558 4576 assoc.read = True
4559 4577 Session().add(assoc)
4560 4578
4561 4579 Session().add(notification)
4562 4580
4563 4581 return notification
4564 4582
4565 4583
4566 4584 class UserNotification(Base, BaseModel):
4567 4585 __tablename__ = 'user_to_notification'
4568 4586 __table_args__ = (
4569 4587 UniqueConstraint('user_id', 'notification_id'),
4570 4588 base_table_args
4571 4589 )
4572 4590
4573 4591 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4574 4592 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4575 4593 read = Column('read', Boolean, default=False)
4576 4594 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4577 4595
4578 4596 user = relationship('User', lazy="joined")
4579 4597 notification = relationship('Notification', lazy="joined",
4580 4598 order_by=lambda: Notification.created_on.desc(),)
4581 4599
4582 4600 def mark_as_read(self):
4583 4601 self.read = True
4584 4602 Session().add(self)
4585 4603
4586 4604
4587 4605 class UserNotice(Base, BaseModel):
4588 4606 __tablename__ = 'user_notices'
4589 4607 __table_args__ = (
4590 4608 base_table_args
4591 4609 )
4592 4610
4593 4611 NOTIFICATION_TYPE_MESSAGE = 'message'
4594 4612 NOTIFICATION_TYPE_NOTICE = 'notice'
4595 4613
4596 4614 NOTIFICATION_LEVEL_INFO = 'info'
4597 4615 NOTIFICATION_LEVEL_WARNING = 'warning'
4598 4616 NOTIFICATION_LEVEL_ERROR = 'error'
4599 4617
4600 4618 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4601 4619
4602 4620 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4603 4621 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4604 4622
4605 4623 notice_read = Column('notice_read', Boolean, default=False)
4606 4624
4607 4625 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4608 4626 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4609 4627
4610 4628 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4611 4629 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4612 4630
4613 4631 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4614 4632 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4615 4633
4616 4634 @classmethod
4617 4635 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4618 4636
4619 4637 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4620 4638 cls.NOTIFICATION_LEVEL_WARNING,
4621 4639 cls.NOTIFICATION_LEVEL_INFO]:
4622 4640 return
4623 4641
4624 4642 from rhodecode.model.user import UserModel
4625 4643 user = UserModel().get_user(user)
4626 4644
4627 4645 new_notice = UserNotice()
4628 4646 if not allow_duplicate:
4629 4647 existing_msg = UserNotice().query() \
4630 4648 .filter(UserNotice.user == user) \
4631 4649 .filter(UserNotice.notice_body == body) \
4632 4650 .filter(UserNotice.notice_read == false()) \
4633 4651 .scalar()
4634 4652 if existing_msg:
4635 4653 log.warning('Ignoring duplicate notice for user %s', user)
4636 4654 return
4637 4655
4638 4656 new_notice.user = user
4639 4657 new_notice.notice_subject = subject
4640 4658 new_notice.notice_body = body
4641 4659 new_notice.notification_level = notice_level
4642 4660 Session().add(new_notice)
4643 4661 Session().commit()
4644 4662
4645 4663
4646 4664 class Gist(Base, BaseModel):
4647 4665 __tablename__ = 'gists'
4648 4666 __table_args__ = (
4649 4667 Index('g_gist_access_id_idx', 'gist_access_id'),
4650 4668 Index('g_created_on_idx', 'created_on'),
4651 4669 base_table_args
4652 4670 )
4653 4671
4654 4672 GIST_PUBLIC = u'public'
4655 4673 GIST_PRIVATE = u'private'
4656 4674 DEFAULT_FILENAME = u'gistfile1.txt'
4657 4675
4658 4676 ACL_LEVEL_PUBLIC = u'acl_public'
4659 4677 ACL_LEVEL_PRIVATE = u'acl_private'
4660 4678
4661 4679 gist_id = Column('gist_id', Integer(), primary_key=True)
4662 4680 gist_access_id = Column('gist_access_id', Unicode(250))
4663 4681 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4664 4682 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4665 4683 gist_expires = Column('gist_expires', Float(53), nullable=False)
4666 4684 gist_type = Column('gist_type', Unicode(128), nullable=False)
4667 4685 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4668 4686 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4669 4687 acl_level = Column('acl_level', Unicode(128), nullable=True)
4670 4688
4671 4689 owner = relationship('User')
4672 4690
4673 4691 def __repr__(self):
4674 4692 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4675 4693
4676 4694 @hybrid_property
4677 4695 def description_safe(self):
4678 4696 from rhodecode.lib import helpers as h
4679 4697 return h.escape(self.gist_description)
4680 4698
4681 4699 @classmethod
4682 4700 def get_or_404(cls, id_):
4683 4701 from pyramid.httpexceptions import HTTPNotFound
4684 4702
4685 4703 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4686 4704 if not res:
4687 4705 raise HTTPNotFound()
4688 4706 return res
4689 4707
4690 4708 @classmethod
4691 4709 def get_by_access_id(cls, gist_access_id):
4692 4710 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4693 4711
4694 4712 def gist_url(self):
4695 4713 from rhodecode.model.gist import GistModel
4696 4714 return GistModel().get_url(self)
4697 4715
4698 4716 @classmethod
4699 4717 def base_path(cls):
4700 4718 """
4701 4719 Returns base path when all gists are stored
4702 4720
4703 4721 :param cls:
4704 4722 """
4705 4723 from rhodecode.model.gist import GIST_STORE_LOC
4706 4724 q = Session().query(RhodeCodeUi)\
4707 4725 .filter(RhodeCodeUi.ui_key == URL_SEP)
4708 4726 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4709 4727 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4710 4728
4711 4729 def get_api_data(self):
4712 4730 """
4713 4731 Common function for generating gist related data for API
4714 4732 """
4715 4733 gist = self
4716 4734 data = {
4717 4735 'gist_id': gist.gist_id,
4718 4736 'type': gist.gist_type,
4719 4737 'access_id': gist.gist_access_id,
4720 4738 'description': gist.gist_description,
4721 4739 'url': gist.gist_url(),
4722 4740 'expires': gist.gist_expires,
4723 4741 'created_on': gist.created_on,
4724 4742 'modified_at': gist.modified_at,
4725 4743 'content': None,
4726 4744 'acl_level': gist.acl_level,
4727 4745 }
4728 4746 return data
4729 4747
4730 4748 def __json__(self):
4731 4749 data = dict(
4732 4750 )
4733 4751 data.update(self.get_api_data())
4734 4752 return data
4735 4753 # SCM functions
4736 4754
4737 4755 def scm_instance(self, **kwargs):
4738 4756 """
4739 4757 Get an instance of VCS Repository
4740 4758
4741 4759 :param kwargs:
4742 4760 """
4743 4761 from rhodecode.model.gist import GistModel
4744 4762 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4745 4763 return get_vcs_instance(
4746 4764 repo_path=safe_str(full_repo_path), create=False,
4747 4765 _vcs_alias=GistModel.vcs_backend)
4748 4766
4749 4767
4750 4768 class ExternalIdentity(Base, BaseModel):
4751 4769 __tablename__ = 'external_identities'
4752 4770 __table_args__ = (
4753 4771 Index('local_user_id_idx', 'local_user_id'),
4754 4772 Index('external_id_idx', 'external_id'),
4755 4773 base_table_args
4756 4774 )
4757 4775
4758 4776 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4759 4777 external_username = Column('external_username', Unicode(1024), default=u'')
4760 4778 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4761 4779 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4762 4780 access_token = Column('access_token', String(1024), default=u'')
4763 4781 alt_token = Column('alt_token', String(1024), default=u'')
4764 4782 token_secret = Column('token_secret', String(1024), default=u'')
4765 4783
4766 4784 @classmethod
4767 4785 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4768 4786 """
4769 4787 Returns ExternalIdentity instance based on search params
4770 4788
4771 4789 :param external_id:
4772 4790 :param provider_name:
4773 4791 :return: ExternalIdentity
4774 4792 """
4775 4793 query = cls.query()
4776 4794 query = query.filter(cls.external_id == external_id)
4777 4795 query = query.filter(cls.provider_name == provider_name)
4778 4796 if local_user_id:
4779 4797 query = query.filter(cls.local_user_id == local_user_id)
4780 4798 return query.first()
4781 4799
4782 4800 @classmethod
4783 4801 def user_by_external_id_and_provider(cls, external_id, provider_name):
4784 4802 """
4785 4803 Returns User instance based on search params
4786 4804
4787 4805 :param external_id:
4788 4806 :param provider_name:
4789 4807 :return: User
4790 4808 """
4791 4809 query = User.query()
4792 4810 query = query.filter(cls.external_id == external_id)
4793 4811 query = query.filter(cls.provider_name == provider_name)
4794 4812 query = query.filter(User.user_id == cls.local_user_id)
4795 4813 return query.first()
4796 4814
4797 4815 @classmethod
4798 4816 def by_local_user_id(cls, local_user_id):
4799 4817 """
4800 4818 Returns all tokens for user
4801 4819
4802 4820 :param local_user_id:
4803 4821 :return: ExternalIdentity
4804 4822 """
4805 4823 query = cls.query()
4806 4824 query = query.filter(cls.local_user_id == local_user_id)
4807 4825 return query
4808 4826
4809 4827 @classmethod
4810 4828 def load_provider_plugin(cls, plugin_id):
4811 4829 from rhodecode.authentication.base import loadplugin
4812 4830 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4813 4831 auth_plugin = loadplugin(_plugin_id)
4814 4832 return auth_plugin
4815 4833
4816 4834
4817 4835 class Integration(Base, BaseModel):
4818 4836 __tablename__ = 'integrations'
4819 4837 __table_args__ = (
4820 4838 base_table_args
4821 4839 )
4822 4840
4823 4841 integration_id = Column('integration_id', Integer(), primary_key=True)
4824 4842 integration_type = Column('integration_type', String(255))
4825 4843 enabled = Column('enabled', Boolean(), nullable=False)
4826 4844 name = Column('name', String(255), nullable=False)
4827 4845 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4828 4846 default=False)
4829 4847
4830 4848 settings = Column(
4831 4849 'settings_json', MutationObj.as_mutable(
4832 4850 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4833 4851 repo_id = Column(
4834 4852 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4835 4853 nullable=True, unique=None, default=None)
4836 4854 repo = relationship('Repository', lazy='joined')
4837 4855
4838 4856 repo_group_id = Column(
4839 4857 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4840 4858 nullable=True, unique=None, default=None)
4841 4859 repo_group = relationship('RepoGroup', lazy='joined')
4842 4860
4843 4861 @property
4844 4862 def scope(self):
4845 4863 if self.repo:
4846 4864 return repr(self.repo)
4847 4865 if self.repo_group:
4848 4866 if self.child_repos_only:
4849 4867 return repr(self.repo_group) + ' (child repos only)'
4850 4868 else:
4851 4869 return repr(self.repo_group) + ' (recursive)'
4852 4870 if self.child_repos_only:
4853 4871 return 'root_repos'
4854 4872 return 'global'
4855 4873
4856 4874 def __repr__(self):
4857 4875 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4858 4876
4859 4877
4860 4878 class RepoReviewRuleUser(Base, BaseModel):
4861 4879 __tablename__ = 'repo_review_rules_users'
4862 4880 __table_args__ = (
4863 4881 base_table_args
4864 4882 )
4865 4883
4866 4884 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4867 4885 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4868 4886 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4869 4887 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4870 4888 user = relationship('User')
4871 4889
4872 4890 def rule_data(self):
4873 4891 return {
4874 4892 'mandatory': self.mandatory
4875 4893 }
4876 4894
4877 4895
4878 4896 class RepoReviewRuleUserGroup(Base, BaseModel):
4879 4897 __tablename__ = 'repo_review_rules_users_groups'
4880 4898 __table_args__ = (
4881 4899 base_table_args
4882 4900 )
4883 4901
4884 4902 VOTE_RULE_ALL = -1
4885 4903
4886 4904 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4887 4905 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4888 4906 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4889 4907 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4890 4908 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4891 4909 users_group = relationship('UserGroup')
4892 4910
4893 4911 def rule_data(self):
4894 4912 return {
4895 4913 'mandatory': self.mandatory,
4896 4914 'vote_rule': self.vote_rule
4897 4915 }
4898 4916
4899 4917 @property
4900 4918 def vote_rule_label(self):
4901 4919 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4902 4920 return 'all must vote'
4903 4921 else:
4904 4922 return 'min. vote {}'.format(self.vote_rule)
4905 4923
4906 4924
4907 4925 class RepoReviewRule(Base, BaseModel):
4908 4926 __tablename__ = 'repo_review_rules'
4909 4927 __table_args__ = (
4910 4928 base_table_args
4911 4929 )
4912 4930
4913 4931 repo_review_rule_id = Column(
4914 4932 'repo_review_rule_id', Integer(), primary_key=True)
4915 4933 repo_id = Column(
4916 4934 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4917 4935 repo = relationship('Repository', backref='review_rules')
4918 4936
4919 4937 review_rule_name = Column('review_rule_name', String(255))
4920 4938 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4921 4939 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4922 4940 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4923 4941
4924 4942 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4925 4943 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4926 4944 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4927 4945 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4928 4946
4929 4947 rule_users = relationship('RepoReviewRuleUser')
4930 4948 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4931 4949
4932 4950 def _validate_pattern(self, value):
4933 4951 re.compile('^' + glob2re(value) + '$')
4934 4952
4935 4953 @hybrid_property
4936 4954 def source_branch_pattern(self):
4937 4955 return self._branch_pattern or '*'
4938 4956
4939 4957 @source_branch_pattern.setter
4940 4958 def source_branch_pattern(self, value):
4941 4959 self._validate_pattern(value)
4942 4960 self._branch_pattern = value or '*'
4943 4961
4944 4962 @hybrid_property
4945 4963 def target_branch_pattern(self):
4946 4964 return self._target_branch_pattern or '*'
4947 4965
4948 4966 @target_branch_pattern.setter
4949 4967 def target_branch_pattern(self, value):
4950 4968 self._validate_pattern(value)
4951 4969 self._target_branch_pattern = value or '*'
4952 4970
4953 4971 @hybrid_property
4954 4972 def file_pattern(self):
4955 4973 return self._file_pattern or '*'
4956 4974
4957 4975 @file_pattern.setter
4958 4976 def file_pattern(self, value):
4959 4977 self._validate_pattern(value)
4960 4978 self._file_pattern = value or '*'
4961 4979
4962 4980 def matches(self, source_branch, target_branch, files_changed):
4963 4981 """
4964 4982 Check if this review rule matches a branch/files in a pull request
4965 4983
4966 4984 :param source_branch: source branch name for the commit
4967 4985 :param target_branch: target branch name for the commit
4968 4986 :param files_changed: list of file paths changed in the pull request
4969 4987 """
4970 4988
4971 4989 source_branch = source_branch or ''
4972 4990 target_branch = target_branch or ''
4973 4991 files_changed = files_changed or []
4974 4992
4975 4993 branch_matches = True
4976 4994 if source_branch or target_branch:
4977 4995 if self.source_branch_pattern == '*':
4978 4996 source_branch_match = True
4979 4997 else:
4980 4998 if self.source_branch_pattern.startswith('re:'):
4981 4999 source_pattern = self.source_branch_pattern[3:]
4982 5000 else:
4983 5001 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4984 5002 source_branch_regex = re.compile(source_pattern)
4985 5003 source_branch_match = bool(source_branch_regex.search(source_branch))
4986 5004 if self.target_branch_pattern == '*':
4987 5005 target_branch_match = True
4988 5006 else:
4989 5007 if self.target_branch_pattern.startswith('re:'):
4990 5008 target_pattern = self.target_branch_pattern[3:]
4991 5009 else:
4992 5010 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4993 5011 target_branch_regex = re.compile(target_pattern)
4994 5012 target_branch_match = bool(target_branch_regex.search(target_branch))
4995 5013
4996 5014 branch_matches = source_branch_match and target_branch_match
4997 5015
4998 5016 files_matches = True
4999 5017 if self.file_pattern != '*':
5000 5018 files_matches = False
5001 5019 if self.file_pattern.startswith('re:'):
5002 5020 file_pattern = self.file_pattern[3:]
5003 5021 else:
5004 5022 file_pattern = glob2re(self.file_pattern)
5005 5023 file_regex = re.compile(file_pattern)
5006 5024 for file_data in files_changed:
5007 5025 filename = file_data.get('filename')
5008 5026
5009 5027 if file_regex.search(filename):
5010 5028 files_matches = True
5011 5029 break
5012 5030
5013 5031 return branch_matches and files_matches
5014 5032
5015 5033 @property
5016 5034 def review_users(self):
5017 5035 """ Returns the users which this rule applies to """
5018 5036
5019 5037 users = collections.OrderedDict()
5020 5038
5021 5039 for rule_user in self.rule_users:
5022 5040 if rule_user.user.active:
5023 5041 if rule_user.user not in users:
5024 5042 users[rule_user.user.username] = {
5025 5043 'user': rule_user.user,
5026 5044 'source': 'user',
5027 5045 'source_data': {},
5028 5046 'data': rule_user.rule_data()
5029 5047 }
5030 5048
5031 5049 for rule_user_group in self.rule_user_groups:
5032 5050 source_data = {
5033 5051 'user_group_id': rule_user_group.users_group.users_group_id,
5034 5052 'name': rule_user_group.users_group.users_group_name,
5035 5053 'members': len(rule_user_group.users_group.members)
5036 5054 }
5037 5055 for member in rule_user_group.users_group.members:
5038 5056 if member.user.active:
5039 5057 key = member.user.username
5040 5058 if key in users:
5041 5059 # skip this member as we have him already
5042 5060 # this prevents from override the "first" matched
5043 5061 # users with duplicates in multiple groups
5044 5062 continue
5045 5063
5046 5064 users[key] = {
5047 5065 'user': member.user,
5048 5066 'source': 'user_group',
5049 5067 'source_data': source_data,
5050 5068 'data': rule_user_group.rule_data()
5051 5069 }
5052 5070
5053 5071 return users
5054 5072
5055 5073 def user_group_vote_rule(self, user_id):
5056 5074
5057 5075 rules = []
5058 5076 if not self.rule_user_groups:
5059 5077 return rules
5060 5078
5061 5079 for user_group in self.rule_user_groups:
5062 5080 user_group_members = [x.user_id for x in user_group.users_group.members]
5063 5081 if user_id in user_group_members:
5064 5082 rules.append(user_group)
5065 5083 return rules
5066 5084
5067 5085 def __repr__(self):
5068 5086 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5069 5087 self.repo_review_rule_id, self.repo)
5070 5088
5071 5089
5072 5090 class ScheduleEntry(Base, BaseModel):
5073 5091 __tablename__ = 'schedule_entries'
5074 5092 __table_args__ = (
5075 5093 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5076 5094 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5077 5095 base_table_args,
5078 5096 )
5079 5097
5080 5098 schedule_types = ['crontab', 'timedelta', 'integer']
5081 5099 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5082 5100
5083 5101 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5084 5102 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5085 5103 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5086 5104
5087 5105 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5088 5106 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5089 5107
5090 5108 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5091 5109 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5092 5110
5093 5111 # task
5094 5112 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5095 5113 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5096 5114 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5097 5115 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5098 5116
5099 5117 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5100 5118 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5101 5119
5102 5120 @hybrid_property
5103 5121 def schedule_type(self):
5104 5122 return self._schedule_type
5105 5123
5106 5124 @schedule_type.setter
5107 5125 def schedule_type(self, val):
5108 5126 if val not in self.schedule_types:
5109 5127 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5110 5128 val, self.schedule_type))
5111 5129
5112 5130 self._schedule_type = val
5113 5131
5114 5132 @classmethod
5115 5133 def get_uid(cls, obj):
5116 5134 args = obj.task_args
5117 5135 kwargs = obj.task_kwargs
5118 5136 if isinstance(args, JsonRaw):
5119 5137 try:
5120 5138 args = json.loads(args)
5121 5139 except ValueError:
5122 5140 args = tuple()
5123 5141
5124 5142 if isinstance(kwargs, JsonRaw):
5125 5143 try:
5126 5144 kwargs = json.loads(kwargs)
5127 5145 except ValueError:
5128 5146 kwargs = dict()
5129 5147
5130 5148 dot_notation = obj.task_dot_notation
5131 5149 val = '.'.join(map(safe_str, [
5132 5150 sorted(dot_notation), args, sorted(kwargs.items())]))
5133 5151 return hashlib.sha1(val).hexdigest()
5134 5152
5135 5153 @classmethod
5136 5154 def get_by_schedule_name(cls, schedule_name):
5137 5155 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5138 5156
5139 5157 @classmethod
5140 5158 def get_by_schedule_id(cls, schedule_id):
5141 5159 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5142 5160
5143 5161 @property
5144 5162 def task(self):
5145 5163 return self.task_dot_notation
5146 5164
5147 5165 @property
5148 5166 def schedule(self):
5149 5167 from rhodecode.lib.celerylib.utils import raw_2_schedule
5150 5168 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5151 5169 return schedule
5152 5170
5153 5171 @property
5154 5172 def args(self):
5155 5173 try:
5156 5174 return list(self.task_args or [])
5157 5175 except ValueError:
5158 5176 return list()
5159 5177
5160 5178 @property
5161 5179 def kwargs(self):
5162 5180 try:
5163 5181 return dict(self.task_kwargs or {})
5164 5182 except ValueError:
5165 5183 return dict()
5166 5184
5167 5185 def _as_raw(self, val):
5168 5186 if hasattr(val, 'de_coerce'):
5169 5187 val = val.de_coerce()
5170 5188 if val:
5171 5189 val = json.dumps(val)
5172 5190
5173 5191 return val
5174 5192
5175 5193 @property
5176 5194 def schedule_definition_raw(self):
5177 5195 return self._as_raw(self.schedule_definition)
5178 5196
5179 5197 @property
5180 5198 def args_raw(self):
5181 5199 return self._as_raw(self.task_args)
5182 5200
5183 5201 @property
5184 5202 def kwargs_raw(self):
5185 5203 return self._as_raw(self.task_kwargs)
5186 5204
5187 5205 def __repr__(self):
5188 5206 return '<DB:ScheduleEntry({}:{})>'.format(
5189 5207 self.schedule_entry_id, self.schedule_name)
5190 5208
5191 5209
5192 5210 @event.listens_for(ScheduleEntry, 'before_update')
5193 5211 def update_task_uid(mapper, connection, target):
5194 5212 target.task_uid = ScheduleEntry.get_uid(target)
5195 5213
5196 5214
5197 5215 @event.listens_for(ScheduleEntry, 'before_insert')
5198 5216 def set_task_uid(mapper, connection, target):
5199 5217 target.task_uid = ScheduleEntry.get_uid(target)
5200 5218
5201 5219
5202 5220 class _BaseBranchPerms(BaseModel):
5203 5221 @classmethod
5204 5222 def compute_hash(cls, value):
5205 5223 return sha1_safe(value)
5206 5224
5207 5225 @hybrid_property
5208 5226 def branch_pattern(self):
5209 5227 return self._branch_pattern or '*'
5210 5228
5211 5229 @hybrid_property
5212 5230 def branch_hash(self):
5213 5231 return self._branch_hash
5214 5232
5215 5233 def _validate_glob(self, value):
5216 5234 re.compile('^' + glob2re(value) + '$')
5217 5235
5218 5236 @branch_pattern.setter
5219 5237 def branch_pattern(self, value):
5220 5238 self._validate_glob(value)
5221 5239 self._branch_pattern = value or '*'
5222 5240 # set the Hash when setting the branch pattern
5223 5241 self._branch_hash = self.compute_hash(self._branch_pattern)
5224 5242
5225 5243 def matches(self, branch):
5226 5244 """
5227 5245 Check if this the branch matches entry
5228 5246
5229 5247 :param branch: branch name for the commit
5230 5248 """
5231 5249
5232 5250 branch = branch or ''
5233 5251
5234 5252 branch_matches = True
5235 5253 if branch:
5236 5254 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5237 5255 branch_matches = bool(branch_regex.search(branch))
5238 5256
5239 5257 return branch_matches
5240 5258
5241 5259
5242 5260 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5243 5261 __tablename__ = 'user_to_repo_branch_permissions'
5244 5262 __table_args__ = (
5245 5263 base_table_args
5246 5264 )
5247 5265
5248 5266 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5249 5267
5250 5268 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5251 5269 repo = relationship('Repository', backref='user_branch_perms')
5252 5270
5253 5271 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5254 5272 permission = relationship('Permission')
5255 5273
5256 5274 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5257 5275 user_repo_to_perm = relationship('UserRepoToPerm')
5258 5276
5259 5277 rule_order = Column('rule_order', Integer(), nullable=False)
5260 5278 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5261 5279 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5262 5280
5263 5281 def __unicode__(self):
5264 5282 return u'<UserBranchPermission(%s => %r)>' % (
5265 5283 self.user_repo_to_perm, self.branch_pattern)
5266 5284
5267 5285
5268 5286 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5269 5287 __tablename__ = 'user_group_to_repo_branch_permissions'
5270 5288 __table_args__ = (
5271 5289 base_table_args
5272 5290 )
5273 5291
5274 5292 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5275 5293
5276 5294 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5277 5295 repo = relationship('Repository', backref='user_group_branch_perms')
5278 5296
5279 5297 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5280 5298 permission = relationship('Permission')
5281 5299
5282 5300 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5283 5301 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5284 5302
5285 5303 rule_order = Column('rule_order', Integer(), nullable=False)
5286 5304 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5287 5305 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5288 5306
5289 5307 def __unicode__(self):
5290 5308 return u'<UserBranchPermission(%s => %r)>' % (
5291 5309 self.user_group_repo_to_perm, self.branch_pattern)
5292 5310
5293 5311
5294 5312 class UserBookmark(Base, BaseModel):
5295 5313 __tablename__ = 'user_bookmarks'
5296 5314 __table_args__ = (
5297 5315 UniqueConstraint('user_id', 'bookmark_repo_id'),
5298 5316 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5299 5317 UniqueConstraint('user_id', 'bookmark_position'),
5300 5318 base_table_args
5301 5319 )
5302 5320
5303 5321 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5304 5322 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5305 5323 position = Column("bookmark_position", Integer(), nullable=False)
5306 5324 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5307 5325 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5308 5326 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5309 5327
5310 5328 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5311 5329 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5312 5330
5313 5331 user = relationship("User")
5314 5332
5315 5333 repository = relationship("Repository")
5316 5334 repository_group = relationship("RepoGroup")
5317 5335
5318 5336 @classmethod
5319 5337 def get_by_position_for_user(cls, position, user_id):
5320 5338 return cls.query() \
5321 5339 .filter(UserBookmark.user_id == user_id) \
5322 5340 .filter(UserBookmark.position == position).scalar()
5323 5341
5324 5342 @classmethod
5325 5343 def get_bookmarks_for_user(cls, user_id, cache=True):
5326 5344 bookmarks = cls.query() \
5327 5345 .filter(UserBookmark.user_id == user_id) \
5328 5346 .options(joinedload(UserBookmark.repository)) \
5329 5347 .options(joinedload(UserBookmark.repository_group)) \
5330 5348 .order_by(UserBookmark.position.asc())
5331 5349
5332 5350 if cache:
5333 5351 bookmarks = bookmarks.options(
5334 5352 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5335 5353 )
5336 5354
5337 5355 return bookmarks.all()
5338 5356
5339 5357 def __unicode__(self):
5340 5358 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5341 5359
5342 5360
5343 5361 class FileStore(Base, BaseModel):
5344 5362 __tablename__ = 'file_store'
5345 5363 __table_args__ = (
5346 5364 base_table_args
5347 5365 )
5348 5366
5349 5367 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5350 5368 file_uid = Column('file_uid', String(1024), nullable=False)
5351 5369 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5352 5370 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5353 5371 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5354 5372
5355 5373 # sha256 hash
5356 5374 file_hash = Column('file_hash', String(512), nullable=False)
5357 5375 file_size = Column('file_size', BigInteger(), nullable=False)
5358 5376
5359 5377 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5360 5378 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5361 5379 accessed_count = Column('accessed_count', Integer(), default=0)
5362 5380
5363 5381 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5364 5382
5365 5383 # if repo/repo_group reference is set, check for permissions
5366 5384 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5367 5385
5368 5386 # hidden defines an attachment that should be hidden from showing in artifact listing
5369 5387 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5370 5388
5371 5389 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5372 5390 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5373 5391
5374 5392 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5375 5393
5376 5394 # scope limited to user, which requester have access to
5377 5395 scope_user_id = Column(
5378 5396 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5379 5397 nullable=True, unique=None, default=None)
5380 5398 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5381 5399
5382 5400 # scope limited to user group, which requester have access to
5383 5401 scope_user_group_id = Column(
5384 5402 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5385 5403 nullable=True, unique=None, default=None)
5386 5404 user_group = relationship('UserGroup', lazy='joined')
5387 5405
5388 5406 # scope limited to repo, which requester have access to
5389 5407 scope_repo_id = Column(
5390 5408 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5391 5409 nullable=True, unique=None, default=None)
5392 5410 repo = relationship('Repository', lazy='joined')
5393 5411
5394 5412 # scope limited to repo group, which requester have access to
5395 5413 scope_repo_group_id = Column(
5396 5414 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5397 5415 nullable=True, unique=None, default=None)
5398 5416 repo_group = relationship('RepoGroup', lazy='joined')
5399 5417
5400 5418 @classmethod
5401 5419 def get_by_store_uid(cls, file_store_uid):
5402 5420 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5403 5421
5404 5422 @classmethod
5405 5423 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5406 5424 file_description='', enabled=True, hidden=False, check_acl=True,
5407 5425 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5408 5426
5409 5427 store_entry = FileStore()
5410 5428 store_entry.file_uid = file_uid
5411 5429 store_entry.file_display_name = file_display_name
5412 5430 store_entry.file_org_name = filename
5413 5431 store_entry.file_size = file_size
5414 5432 store_entry.file_hash = file_hash
5415 5433 store_entry.file_description = file_description
5416 5434
5417 5435 store_entry.check_acl = check_acl
5418 5436 store_entry.enabled = enabled
5419 5437 store_entry.hidden = hidden
5420 5438
5421 5439 store_entry.user_id = user_id
5422 5440 store_entry.scope_user_id = scope_user_id
5423 5441 store_entry.scope_repo_id = scope_repo_id
5424 5442 store_entry.scope_repo_group_id = scope_repo_group_id
5425 5443
5426 5444 return store_entry
5427 5445
5428 5446 @classmethod
5429 5447 def store_metadata(cls, file_store_id, args, commit=True):
5430 5448 file_store = FileStore.get(file_store_id)
5431 5449 if file_store is None:
5432 5450 return
5433 5451
5434 5452 for section, key, value, value_type in args:
5435 5453 has_key = FileStoreMetadata().query() \
5436 5454 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5437 5455 .filter(FileStoreMetadata.file_store_meta_section == section) \
5438 5456 .filter(FileStoreMetadata.file_store_meta_key == key) \
5439 5457 .scalar()
5440 5458 if has_key:
5441 5459 msg = 'key `{}` already defined under section `{}` for this file.'\
5442 5460 .format(key, section)
5443 5461 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5444 5462
5445 5463 # NOTE(marcink): raises ArtifactMetadataBadValueType
5446 5464 FileStoreMetadata.valid_value_type(value_type)
5447 5465
5448 5466 meta_entry = FileStoreMetadata()
5449 5467 meta_entry.file_store = file_store
5450 5468 meta_entry.file_store_meta_section = section
5451 5469 meta_entry.file_store_meta_key = key
5452 5470 meta_entry.file_store_meta_value_type = value_type
5453 5471 meta_entry.file_store_meta_value = value
5454 5472
5455 5473 Session().add(meta_entry)
5456 5474
5457 5475 try:
5458 5476 if commit:
5459 5477 Session().commit()
5460 5478 except IntegrityError:
5461 5479 Session().rollback()
5462 5480 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5463 5481
5464 5482 @classmethod
5465 5483 def bump_access_counter(cls, file_uid, commit=True):
5466 5484 FileStore().query()\
5467 5485 .filter(FileStore.file_uid == file_uid)\
5468 5486 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5469 5487 FileStore.accessed_on: datetime.datetime.now()})
5470 5488 if commit:
5471 5489 Session().commit()
5472 5490
5473 5491 def __json__(self):
5474 5492 data = {
5475 5493 'filename': self.file_display_name,
5476 5494 'filename_org': self.file_org_name,
5477 5495 'file_uid': self.file_uid,
5478 5496 'description': self.file_description,
5479 5497 'hidden': self.hidden,
5480 5498 'size': self.file_size,
5481 5499 'created_on': self.created_on,
5482 5500 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5483 5501 'downloaded_times': self.accessed_count,
5484 5502 'sha256': self.file_hash,
5485 5503 'metadata': self.file_metadata,
5486 5504 }
5487 5505
5488 5506 return data
5489 5507
5490 5508 def __repr__(self):
5491 5509 return '<FileStore({})>'.format(self.file_store_id)
5492 5510
5493 5511
5494 5512 class FileStoreMetadata(Base, BaseModel):
5495 5513 __tablename__ = 'file_store_metadata'
5496 5514 __table_args__ = (
5497 5515 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5498 5516 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5499 5517 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5500 5518 base_table_args
5501 5519 )
5502 5520 SETTINGS_TYPES = {
5503 5521 'str': safe_str,
5504 5522 'int': safe_int,
5505 5523 'unicode': safe_unicode,
5506 5524 'bool': str2bool,
5507 5525 'list': functools.partial(aslist, sep=',')
5508 5526 }
5509 5527
5510 5528 file_store_meta_id = Column(
5511 5529 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5512 5530 primary_key=True)
5513 5531 _file_store_meta_section = Column(
5514 5532 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5515 5533 nullable=True, unique=None, default=None)
5516 5534 _file_store_meta_section_hash = Column(
5517 5535 "file_store_meta_section_hash", String(255),
5518 5536 nullable=True, unique=None, default=None)
5519 5537 _file_store_meta_key = Column(
5520 5538 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5521 5539 nullable=True, unique=None, default=None)
5522 5540 _file_store_meta_key_hash = Column(
5523 5541 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5524 5542 _file_store_meta_value = Column(
5525 5543 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5526 5544 nullable=True, unique=None, default=None)
5527 5545 _file_store_meta_value_type = Column(
5528 5546 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5529 5547 default='unicode')
5530 5548
5531 5549 file_store_id = Column(
5532 5550 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5533 5551 nullable=True, unique=None, default=None)
5534 5552
5535 5553 file_store = relationship('FileStore', lazy='joined')
5536 5554
5537 5555 @classmethod
5538 5556 def valid_value_type(cls, value):
5539 5557 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5540 5558 raise ArtifactMetadataBadValueType(
5541 5559 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5542 5560
5543 5561 @hybrid_property
5544 5562 def file_store_meta_section(self):
5545 5563 return self._file_store_meta_section
5546 5564
5547 5565 @file_store_meta_section.setter
5548 5566 def file_store_meta_section(self, value):
5549 5567 self._file_store_meta_section = value
5550 5568 self._file_store_meta_section_hash = _hash_key(value)
5551 5569
5552 5570 @hybrid_property
5553 5571 def file_store_meta_key(self):
5554 5572 return self._file_store_meta_key
5555 5573
5556 5574 @file_store_meta_key.setter
5557 5575 def file_store_meta_key(self, value):
5558 5576 self._file_store_meta_key = value
5559 5577 self._file_store_meta_key_hash = _hash_key(value)
5560 5578
5561 5579 @hybrid_property
5562 5580 def file_store_meta_value(self):
5563 5581 val = self._file_store_meta_value
5564 5582
5565 5583 if self._file_store_meta_value_type:
5566 5584 # e.g unicode.encrypted == unicode
5567 5585 _type = self._file_store_meta_value_type.split('.')[0]
5568 5586 # decode the encrypted value if it's encrypted field type
5569 5587 if '.encrypted' in self._file_store_meta_value_type:
5570 5588 cipher = EncryptedTextValue()
5571 5589 val = safe_unicode(cipher.process_result_value(val, None))
5572 5590 # do final type conversion
5573 5591 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5574 5592 val = converter(val)
5575 5593
5576 5594 return val
5577 5595
5578 5596 @file_store_meta_value.setter
5579 5597 def file_store_meta_value(self, val):
5580 5598 val = safe_unicode(val)
5581 5599 # encode the encrypted value
5582 5600 if '.encrypted' in self.file_store_meta_value_type:
5583 5601 cipher = EncryptedTextValue()
5584 5602 val = safe_unicode(cipher.process_bind_param(val, None))
5585 5603 self._file_store_meta_value = val
5586 5604
5587 5605 @hybrid_property
5588 5606 def file_store_meta_value_type(self):
5589 5607 return self._file_store_meta_value_type
5590 5608
5591 5609 @file_store_meta_value_type.setter
5592 5610 def file_store_meta_value_type(self, val):
5593 5611 # e.g unicode.encrypted
5594 5612 self.valid_value_type(val)
5595 5613 self._file_store_meta_value_type = val
5596 5614
5597 5615 def __json__(self):
5598 5616 data = {
5599 5617 'artifact': self.file_store.file_uid,
5600 5618 'section': self.file_store_meta_section,
5601 5619 'key': self.file_store_meta_key,
5602 5620 'value': self.file_store_meta_value,
5603 5621 }
5604 5622
5605 5623 return data
5606 5624
5607 5625 def __repr__(self):
5608 5626 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5609 5627 self.file_store_meta_key, self.file_store_meta_value)
5610 5628
5611 5629
5612 5630 class DbMigrateVersion(Base, BaseModel):
5613 5631 __tablename__ = 'db_migrate_version'
5614 5632 __table_args__ = (
5615 5633 base_table_args,
5616 5634 )
5617 5635
5618 5636 repository_id = Column('repository_id', String(250), primary_key=True)
5619 5637 repository_path = Column('repository_path', Text)
5620 5638 version = Column('version', Integer)
5621 5639
5622 5640 @classmethod
5623 5641 def set_version(cls, version):
5624 5642 """
5625 5643 Helper for forcing a different version, usually for debugging purposes via ishell.
5626 5644 """
5627 5645 ver = DbMigrateVersion.query().first()
5628 5646 ver.version = version
5629 5647 Session().commit()
5630 5648
5631 5649
5632 5650 class DbSession(Base, BaseModel):
5633 5651 __tablename__ = 'db_session'
5634 5652 __table_args__ = (
5635 5653 base_table_args,
5636 5654 )
5637 5655
5638 5656 def __repr__(self):
5639 5657 return '<DB:DbSession({})>'.format(self.id)
5640 5658
5641 5659 id = Column('id', Integer())
5642 5660 namespace = Column('namespace', String(255), primary_key=True)
5643 5661 accessed = Column('accessed', DateTime, nullable=False)
5644 5662 created = Column('created', DateTime, nullable=False)
5645 5663 data = Column('data', PickleType, nullable=False)
@@ -1,199 +1,205 b''
1 1 <div class="panel panel-default">
2 2 <script>
3 3 var showAuthToken = function(authTokenId) {
4 4 return _showAuthToken(authTokenId, pyroutes.url('my_account_auth_tokens_view'))
5 5 }
6 6 </script>
7 7
8 8 <div class="panel-heading">
9 9 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
10 10 </div>
11 11 <div class="panel-body">
12 12 <div class="apikeys_wrap">
13 13 <p>
14 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
15 'Each token can have a role. Token with a role can be used only in given context, '
16 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
14 ${_('Available roles')}:
15 <ul>
16 % for role in h.UserApiKeys.ROLES:
17 <li>
18 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
19 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
20 </li>
21 % endfor
22 </ul>
17 23 </p>
18 24 <table class="rctable auth_tokens">
19 25 <tr>
20 26 <th>${_('Token')}</th>
21 27 <th>${_('Description')}</th>
22 28 <th>${_('Role')}</th>
23 29 <th>${_('Repository Scope')}</th>
24 30 <th>${_('Expiration')}</th>
25 31 <th>${_('Action')}</th>
26 32 </tr>
27 33 %if c.user_auth_tokens:
28 34 %for auth_token in c.user_auth_tokens:
29 35 <tr class="${('expired' if auth_token.expired else '')}">
30 36 <td class="td-authtoken">
31 37 <div class="user_auth_tokens">
32 38 <code class="cursor-pointer" onclick="showAuthToken(${auth_token.user_api_key_id})">
33 39 ${auth_token.token_obfuscated}
34 40 </code>
35 41 </div>
36 42 </td>
37 43 <td class="td-wrap">${auth_token.description}</td>
38 44 <td class="td-tags">
39 <span class="tag disabled">${auth_token.role_humanized}</span>
45 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
40 46 </td>
41 47 <td class="td">${auth_token.scope_humanized}</td>
42 48 <td class="td-exp">
43 49 %if auth_token.expires == -1:
44 50 ${_('never')}
45 51 %else:
46 52 %if auth_token.expired:
47 53 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
48 54 %else:
49 55 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
50 56 %endif
51 57 %endif
52 58 </td>
53 59 <td class="td-action">
54 60 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), request=request)}
55 61 <input name="del_auth_token" type="hidden" value="${auth_token.user_api_key_id}">
56 62 <button class="btn btn-link btn-danger" type="submit"
57 63 onclick="submitConfirm(event, this, _gettext('Confirm to delete this auth token'), _gettext('Delete'), '${auth_token.token_obfuscated}')"
58 64 >
59 65 ${_('Delete')}
60 66 </button>
61 67 ${h.end_form()}
62 68 </td>
63 69 </tr>
64 70 %endfor
65 71 %else:
66 72 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
67 73 %endif
68 74 </table>
69 75 </div>
70 76
71 77 <div class="user_auth_tokens">
72 78 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), request=request)}
73 79 <div class="form form-vertical">
74 80 <!-- fields -->
75 81 <div class="fields">
76 82 <div class="field">
77 83 <div class="label">
78 84 <label for="new_email">${_('New authentication token')}:</label>
79 85 </div>
80 86 <div class="input">
81 87 ${h.text('description', class_='medium', placeholder=_('Description'))}
82 88 ${h.hidden('lifetime')}
83 89 ${h.select('role', request.GET.get('token_role', ''), c.role_options)}
84 90
85 91 % if c.allow_scoped_tokens:
86 92 ${h.hidden('scope_repo_id')}
87 93 % else:
88 94 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
89 95 % endif
90 96 </div>
91 97 <p class="help-block">
92 98 ${_('Repository scope works only with tokens with VCS type.')}
93 99 </p>
94 100 </div>
95 101 <div class="buttons">
96 102 ${h.submit('save',_('Add'),class_="btn")}
97 103 ${h.reset('reset',_('Reset'),class_="btn")}
98 104 </div>
99 105 </div>
100 106 </div>
101 107 ${h.end_form()}
102 108 </div>
103 109 </div>
104 110 </div>
105 111
106 112 <script>
107 113 $(document).ready(function(){
108 114
109 115 var select2Options = {
110 116 'containerCssClass': "drop-menu",
111 117 'dropdownCssClass': "drop-menu-dropdown",
112 118 'dropdownAutoWidth': true
113 119 };
114 120 $("#role").select2(select2Options);
115 121
116 122 var preloadData = {
117 123 results: [
118 124 % for entry in c.lifetime_values:
119 125 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
120 126 % endfor
121 127 ]
122 128 };
123 129
124 130 $("#lifetime").select2({
125 131 containerCssClass: "drop-menu",
126 132 dropdownCssClass: "drop-menu-dropdown",
127 133 dropdownAutoWidth: true,
128 134 data: preloadData,
129 135 placeholder: "${_('Select or enter expiration date')}",
130 136 query: function(query) {
131 137 feedLifetimeOptions(query, preloadData);
132 138 }
133 139 });
134 140
135 141
136 142 var repoFilter = function(data) {
137 143 var results = [];
138 144
139 145 if (!data.results[0]) {
140 146 return data
141 147 }
142 148
143 149 $.each(data.results[0].children, function() {
144 150 // replace name to ID for submision
145 151 this.id = this.repo_id;
146 152 results.push(this);
147 153 });
148 154
149 155 data.results[0].children = results;
150 156 return data;
151 157 };
152 158
153 159 $("#scope_repo_id_disabled").select2(select2Options);
154 160
155 161 var selectVcsScope = function() {
156 162 // select vcs scope and disable input
157 163 $("#role").select2("val", "${c.role_vcs}").trigger('change');
158 164 $("#role").select2("readonly", true)
159 165 };
160 166
161 167 $("#scope_repo_id").select2({
162 168 cachedDataSource: {},
163 169 minimumInputLength: 2,
164 170 placeholder: "${_('repository scope')}",
165 171 dropdownAutoWidth: true,
166 172 containerCssClass: "drop-menu",
167 173 dropdownCssClass: "drop-menu-dropdown",
168 174 formatResult: formatRepoResult,
169 175 query: $.debounce(250, function(query){
170 176 self = this;
171 177 var cacheKey = query.term;
172 178 var cachedData = self.cachedDataSource[cacheKey];
173 179
174 180 if (cachedData) {
175 181 query.callback({results: cachedData.results});
176 182 } else {
177 183 $.ajax({
178 184 url: pyroutes.url('repo_list_data'),
179 185 data: {'query': query.term},
180 186 dataType: 'json',
181 187 type: 'GET',
182 188 success: function(data) {
183 189 data = repoFilter(data);
184 190 self.cachedDataSource[cacheKey] = data;
185 191 query.callback({results: data.results});
186 192 },
187 193 error: function(data, textStatus, errorThrown) {
188 194 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
189 195 }
190 196 })
191 197 }
192 198 })
193 199 });
194 200 $("#scope_repo_id").on('select2-selecting', function(e){
195 201 selectVcsScope()
196 202 });
197 203
198 204 });
199 205 </script>
@@ -1,204 +1,210 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <script>
5 5 var showAuthToken = function(authTokenId) {
6 6 return _showAuthToken(authTokenId, pyroutes.url('edit_user_auth_tokens_view', {'user_id': '${c.user.user_id}'}))
7 7 }
8 8 </script>
9 9
10 10 <div class="panel-heading">
11 11 <h3 class="panel-title">
12 12 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
13 13 &nbsp;- ${_('Authentication Tokens')}
14 14 </h3>
15 15 </div>
16 16 <div class="panel-body">
17 17 <div class="apikeys_wrap">
18 18 <p>
19 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
20 'Each token can have a role. Token with a role can be used only in given context, '
21 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
19 ${_('Available roles')}:
20 <ul>
21 % for role in h.UserApiKeys.ROLES:
22 <li>
23 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
24 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
25 </li>
26 % endfor
27 </ul>
22 28 </p>
23 29 <table class="rctable auth_tokens">
24 30 <tr>
25 31 <th>${_('Token')}</th>
26 32 <th>${_('Description')}</th>
27 33 <th>${_('Role')}</th>
28 34 <th>${_('Repository Scope')}</th>
29 35 <th>${_('Expiration')}</th>
30 36 <th>${_('Action')}</th>
31 37 </tr>
32 38 %if c.user_auth_tokens:
33 39 %for auth_token in c.user_auth_tokens:
34 40 <tr class="${('expired' if auth_token.expired else '')}">
35 41 <td class="td-authtoken">
36 42 <div class="user_auth_tokens">
37 43 <code class="cursor-pointer" onclick="showAuthToken(${auth_token.user_api_key_id})">
38 44 ${auth_token.token_obfuscated}
39 45 </code>
40 46 </div>
41 47 </td>
42 48 <td class="td-wrap">${auth_token.description}</td>
43 49 <td class="td-tags">
44 <span class="tag disabled">${auth_token.role_humanized}</span>
50 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
45 51 </td>
46 52 <td class="td">${auth_token.scope_humanized}</td>
47 53 <td class="td-exp">
48 54 %if auth_token.expires == -1:
49 55 ${_('never')}
50 56 %else:
51 57 %if auth_token.expired:
52 58 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
53 59 %else:
54 60 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
55 61 %endif
56 62 %endif
57 63 </td>
58 64 <td class="td-action">
59 65 ${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), request=request)}
60 66 <input name="del_auth_token" type="hidden" value="${auth_token.user_api_key_id}">
61 67 <button class="btn btn-link btn-danger" type="submit"
62 68 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
63 69 ${_('Delete')}
64 70 </button>
65 71 ${h.end_form()}
66 72 </td>
67 73 </tr>
68 74 %endfor
69 75 %else:
70 76 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
71 77 %endif
72 78 </table>
73 79 </div>
74 80
75 81 <div class="user_auth_tokens">
76 82 ${h.secure_form(h.route_path('edit_user_auth_tokens_add', user_id=c.user.user_id), request=request)}
77 83 <div class="form form-vertical">
78 84 <!-- fields -->
79 85 <div class="fields">
80 86 <div class="field">
81 87 <div class="label">
82 88 <label for="new_email">${_('New authentication token')}:</label>
83 89 </div>
84 90 <div class="input">
85 91 ${h.text('description', class_='medium', placeholder=_('Description'))}
86 92 ${h.hidden('lifetime')}
87 93 ${h.select('role', '', c.role_options)}
88 94
89 95 % if c.allow_scoped_tokens:
90 96 ${h.hidden('scope_repo_id')}
91 97 % else:
92 98 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
93 99 % endif
94 100 </div>
95 101 <p class="help-block">
96 102 ${_('Repository scope works only with tokens with VCS type.')}
97 103 </p>
98 104 </div>
99 105 <div class="buttons">
100 106 ${h.submit('save',_('Add'),class_="btn")}
101 107 ${h.reset('reset',_('Reset'),class_="btn")}
102 108 </div>
103 109 </div>
104 110 </div>
105 111 ${h.end_form()}
106 112 </div>
107 113 </div>
108 114 </div>
109 115
110 116 <script>
111 117
112 118 $(document).ready(function(){
113 119
114 120 var select2Options = {
115 121 'containerCssClass': "drop-menu",
116 122 'dropdownCssClass': "drop-menu-dropdown",
117 123 'dropdownAutoWidth': true
118 124 };
119 125 $("#role").select2(select2Options);
120 126
121 127 var preloadData = {
122 128 results: [
123 129 % for entry in c.lifetime_values:
124 130 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
125 131 % endfor
126 132 ]
127 133 };
128 134
129 135 $("#lifetime").select2({
130 136 containerCssClass: "drop-menu",
131 137 dropdownCssClass: "drop-menu-dropdown",
132 138 dropdownAutoWidth: true,
133 139 data: preloadData,
134 140 placeholder: "${_('Select or enter expiration date')}",
135 141 query: function(query) {
136 142 feedLifetimeOptions(query, preloadData);
137 143 }
138 144 });
139 145
140 146
141 147 var repoFilter = function(data) {
142 148 var results = [];
143 149
144 150 if (!data.results[0]) {
145 151 return data
146 152 }
147 153
148 154 $.each(data.results[0].children, function() {
149 155 // replace name to ID for submision
150 156 this.id = this.repo_id;
151 157 results.push(this);
152 158 });
153 159
154 160 data.results[0].children = results;
155 161 return data;
156 162 };
157 163
158 164 $("#scope_repo_id_disabled").select2(select2Options);
159 165
160 166 var selectVcsScope = function() {
161 167 // select vcs scope and disable input
162 168 $("#role").select2("val", "${c.role_vcs}").trigger('change');
163 169 $("#role").select2("readonly", true)
164 170 };
165 171
166 172 $("#scope_repo_id").select2({
167 173 cachedDataSource: {},
168 174 minimumInputLength: 2,
169 175 placeholder: "${_('repository scope')}",
170 176 dropdownAutoWidth: true,
171 177 containerCssClass: "drop-menu",
172 178 dropdownCssClass: "drop-menu-dropdown",
173 179 formatResult: formatRepoResult,
174 180 query: $.debounce(250, function(query){
175 181 self = this;
176 182 var cacheKey = query.term;
177 183 var cachedData = self.cachedDataSource[cacheKey];
178 184
179 185 if (cachedData) {
180 186 query.callback({results: cachedData.results});
181 187 } else {
182 188 $.ajax({
183 189 url: pyroutes.url('repo_list_data'),
184 190 data: {'query': query.term},
185 191 dataType: 'json',
186 192 type: 'GET',
187 193 success: function(data) {
188 194 data = repoFilter(data);
189 195 self.cachedDataSource[cacheKey] = data;
190 196 query.callback({results: data.results});
191 197 },
192 198 error: function(data, textStatus, errorThrown) {
193 199 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
194 200 }
195 201 })
196 202 }
197 203 })
198 204 });
199 205 $("#scope_repo_id").on('select2-selecting', function(e){
200 206 selectVcsScope()
201 207 });
202 208
203 209 });
204 210 </script>
General Comments 0
You need to be logged in to leave comments. Login now