##// END OF EJS Templates
meta-tags: cleanup support for metatags....
marcink -
r2091:90bf6103 default
parent child Browse files
Show More
@@ -1,2047 +1,2062 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 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
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from webhelpers.html import literal, HTML, escape
55 55 from webhelpers.html.tools import *
56 56 from webhelpers.html.builder import make_tag
57 57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 63 from webhelpers.pylonslib import Flash as _Flash
64 64 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
65 65 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
66 66 replace_whitespace, urlify, truncate, wrap_paragraphs
67 67 from webhelpers.date import time_ago_in_words
68 68 from webhelpers.paginate import Page as _Page
69 69 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
70 70 convert_boolean_attrs, NotGiven, _make_safe_id_component
71 71 from webhelpers2.number import format_byte_size
72 72
73 73 from rhodecode.lib.action_parser import action_parser
74 74 from rhodecode.lib.ext_json import json
75 75 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
76 76 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
77 77 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
78 78 AttributeDict, safe_int, md5, md5_safe
79 79 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
80 80 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
81 81 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
82 82 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
83 83 from rhodecode.model.changeset_status import ChangesetStatusModel
84 84 from rhodecode.model.db import Permission, User, Repository
85 85 from rhodecode.model.repo_group import RepoGroupModel
86 86 from rhodecode.model.settings import IssueTrackerSettingsModel
87 87
88 88 log = logging.getLogger(__name__)
89 89
90 90
91 91 DEFAULT_USER = User.DEFAULT_USER
92 92 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
93 93
94 94
95 95 def url(*args, **kw):
96 96 from pylons import url as pylons_url
97 97 return pylons_url(*args, **kw)
98 98
99 99
100 100 def pylons_url_current(*args, **kw):
101 101 """
102 102 This function overrides pylons.url.current() which returns the current
103 103 path so that it will also work from a pyramid only context. This
104 104 should be removed once port to pyramid is complete.
105 105 """
106 106 from pylons import url as pylons_url
107 107 if not args and not kw:
108 108 request = get_current_request()
109 109 return request.path
110 110 return pylons_url.current(*args, **kw)
111 111
112 112 url.current = pylons_url_current
113 113
114 114
115 115 def url_replace(**qargs):
116 116 """ Returns the current request url while replacing query string args """
117 117
118 118 request = get_current_request()
119 119 new_args = request.GET.mixed()
120 120 new_args.update(qargs)
121 121 return url('', **new_args)
122 122
123 123
124 124 def asset(path, ver=None, **kwargs):
125 125 """
126 126 Helper to generate a static asset file path for rhodecode assets
127 127
128 128 eg. h.asset('images/image.png', ver='3923')
129 129
130 130 :param path: path of asset
131 131 :param ver: optional version query param to append as ?ver=
132 132 """
133 133 request = get_current_request()
134 134 query = {}
135 135 query.update(kwargs)
136 136 if ver:
137 137 query = {'ver': ver}
138 138 return request.static_path(
139 139 'rhodecode:public/{}'.format(path), _query=query)
140 140
141 141
142 142 default_html_escape_table = {
143 143 ord('&'): u'&amp;',
144 144 ord('<'): u'&lt;',
145 145 ord('>'): u'&gt;',
146 146 ord('"'): u'&quot;',
147 147 ord("'"): u'&#39;',
148 148 }
149 149
150 150
151 151 def html_escape(text, html_escape_table=default_html_escape_table):
152 152 """Produce entities within text."""
153 153 return text.translate(html_escape_table)
154 154
155 155
156 156 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
157 157 """
158 158 Truncate string ``s`` at the first occurrence of ``sub``.
159 159
160 160 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
161 161 """
162 162 suffix_if_chopped = suffix_if_chopped or ''
163 163 pos = s.find(sub)
164 164 if pos == -1:
165 165 return s
166 166
167 167 if inclusive:
168 168 pos += len(sub)
169 169
170 170 chopped = s[:pos]
171 171 left = s[pos:].strip()
172 172
173 173 if left and suffix_if_chopped:
174 174 chopped += suffix_if_chopped
175 175
176 176 return chopped
177 177
178 178
179 179 def shorter(text, size=20):
180 180 postfix = '...'
181 181 if len(text) > size:
182 182 return text[:size - len(postfix)] + postfix
183 183 return text
184 184
185 185
186 186 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
187 187 """
188 188 Reset button
189 189 """
190 190 _set_input_attrs(attrs, type, name, value)
191 191 _set_id_attr(attrs, id, name)
192 192 convert_boolean_attrs(attrs, ["disabled"])
193 193 return HTML.input(**attrs)
194 194
195 195 reset = _reset
196 196 safeid = _make_safe_id_component
197 197
198 198
199 199 def branding(name, length=40):
200 200 return truncate(name, length, indicator="")
201 201
202 202
203 203 def FID(raw_id, path):
204 204 """
205 205 Creates a unique ID for filenode based on it's hash of path and commit
206 206 it's safe to use in urls
207 207
208 208 :param raw_id:
209 209 :param path:
210 210 """
211 211
212 212 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
213 213
214 214
215 215 class _GetError(object):
216 216 """Get error from form_errors, and represent it as span wrapped error
217 217 message
218 218
219 219 :param field_name: field to fetch errors for
220 220 :param form_errors: form errors dict
221 221 """
222 222
223 223 def __call__(self, field_name, form_errors):
224 224 tmpl = """<span class="error_msg">%s</span>"""
225 225 if form_errors and field_name in form_errors:
226 226 return literal(tmpl % form_errors.get(field_name))
227 227
228 228 get_error = _GetError()
229 229
230 230
231 231 class _ToolTip(object):
232 232
233 233 def __call__(self, tooltip_title, trim_at=50):
234 234 """
235 235 Special function just to wrap our text into nice formatted
236 236 autowrapped text
237 237
238 238 :param tooltip_title:
239 239 """
240 240 tooltip_title = escape(tooltip_title)
241 241 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
242 242 return tooltip_title
243 243 tooltip = _ToolTip()
244 244
245 245
246 246 def files_breadcrumbs(repo_name, commit_id, file_path):
247 247 if isinstance(file_path, str):
248 248 file_path = safe_unicode(file_path)
249 249
250 250 # TODO: johbo: Is this always a url like path, or is this operating
251 251 # system dependent?
252 252 path_segments = file_path.split('/')
253 253
254 254 repo_name_html = escape(repo_name)
255 255 if len(path_segments) == 1 and path_segments[0] == '':
256 256 url_segments = [repo_name_html]
257 257 else:
258 258 url_segments = [
259 259 link_to(
260 260 repo_name_html,
261 261 route_path(
262 262 'repo_files',
263 263 repo_name=repo_name,
264 264 commit_id=commit_id,
265 265 f_path=''),
266 266 class_='pjax-link')]
267 267
268 268 last_cnt = len(path_segments) - 1
269 269 for cnt, segment in enumerate(path_segments):
270 270 if not segment:
271 271 continue
272 272 segment_html = escape(segment)
273 273
274 274 if cnt != last_cnt:
275 275 url_segments.append(
276 276 link_to(
277 277 segment_html,
278 278 route_path(
279 279 'repo_files',
280 280 repo_name=repo_name,
281 281 commit_id=commit_id,
282 282 f_path='/'.join(path_segments[:cnt + 1])),
283 283 class_='pjax-link'))
284 284 else:
285 285 url_segments.append(segment_html)
286 286
287 287 return literal('/'.join(url_segments))
288 288
289 289
290 290 class CodeHtmlFormatter(HtmlFormatter):
291 291 """
292 292 My code Html Formatter for source codes
293 293 """
294 294
295 295 def wrap(self, source, outfile):
296 296 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
297 297
298 298 def _wrap_code(self, source):
299 299 for cnt, it in enumerate(source):
300 300 i, t = it
301 301 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
302 302 yield i, t
303 303
304 304 def _wrap_tablelinenos(self, inner):
305 305 dummyoutfile = StringIO.StringIO()
306 306 lncount = 0
307 307 for t, line in inner:
308 308 if t:
309 309 lncount += 1
310 310 dummyoutfile.write(line)
311 311
312 312 fl = self.linenostart
313 313 mw = len(str(lncount + fl - 1))
314 314 sp = self.linenospecial
315 315 st = self.linenostep
316 316 la = self.lineanchors
317 317 aln = self.anchorlinenos
318 318 nocls = self.noclasses
319 319 if sp:
320 320 lines = []
321 321
322 322 for i in range(fl, fl + lncount):
323 323 if i % st == 0:
324 324 if i % sp == 0:
325 325 if aln:
326 326 lines.append('<a href="#%s%d" class="special">%*d</a>' %
327 327 (la, i, mw, i))
328 328 else:
329 329 lines.append('<span class="special">%*d</span>' % (mw, i))
330 330 else:
331 331 if aln:
332 332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 333 else:
334 334 lines.append('%*d' % (mw, i))
335 335 else:
336 336 lines.append('')
337 337 ls = '\n'.join(lines)
338 338 else:
339 339 lines = []
340 340 for i in range(fl, fl + lncount):
341 341 if i % st == 0:
342 342 if aln:
343 343 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
344 344 else:
345 345 lines.append('%*d' % (mw, i))
346 346 else:
347 347 lines.append('')
348 348 ls = '\n'.join(lines)
349 349
350 350 # in case you wonder about the seemingly redundant <div> here: since the
351 351 # content in the other cell also is wrapped in a div, some browsers in
352 352 # some configurations seem to mess up the formatting...
353 353 if nocls:
354 354 yield 0, ('<table class="%stable">' % self.cssclass +
355 355 '<tr><td><div class="linenodiv" '
356 356 'style="background-color: #f0f0f0; padding-right: 10px">'
357 357 '<pre style="line-height: 125%">' +
358 358 ls + '</pre></div></td><td id="hlcode" class="code">')
359 359 else:
360 360 yield 0, ('<table class="%stable">' % self.cssclass +
361 361 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
362 362 ls + '</pre></div></td><td id="hlcode" class="code">')
363 363 yield 0, dummyoutfile.getvalue()
364 364 yield 0, '</td></tr></table>'
365 365
366 366
367 367 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
368 368 def __init__(self, **kw):
369 369 # only show these line numbers if set
370 370 self.only_lines = kw.pop('only_line_numbers', [])
371 371 self.query_terms = kw.pop('query_terms', [])
372 372 self.max_lines = kw.pop('max_lines', 5)
373 373 self.line_context = kw.pop('line_context', 3)
374 374 self.url = kw.pop('url', None)
375 375
376 376 super(CodeHtmlFormatter, self).__init__(**kw)
377 377
378 378 def _wrap_code(self, source):
379 379 for cnt, it in enumerate(source):
380 380 i, t = it
381 381 t = '<pre>%s</pre>' % t
382 382 yield i, t
383 383
384 384 def _wrap_tablelinenos(self, inner):
385 385 yield 0, '<table class="code-highlight %stable">' % self.cssclass
386 386
387 387 last_shown_line_number = 0
388 388 current_line_number = 1
389 389
390 390 for t, line in inner:
391 391 if not t:
392 392 yield t, line
393 393 continue
394 394
395 395 if current_line_number in self.only_lines:
396 396 if last_shown_line_number + 1 != current_line_number:
397 397 yield 0, '<tr>'
398 398 yield 0, '<td class="line">...</td>'
399 399 yield 0, '<td id="hlcode" class="code"></td>'
400 400 yield 0, '</tr>'
401 401
402 402 yield 0, '<tr>'
403 403 if self.url:
404 404 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
405 405 self.url, current_line_number, current_line_number)
406 406 else:
407 407 yield 0, '<td class="line"><a href="">%i</a></td>' % (
408 408 current_line_number)
409 409 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
410 410 yield 0, '</tr>'
411 411
412 412 last_shown_line_number = current_line_number
413 413
414 414 current_line_number += 1
415 415
416 416
417 417 yield 0, '</table>'
418 418
419 419
420 420 def extract_phrases(text_query):
421 421 """
422 422 Extracts phrases from search term string making sure phrases
423 423 contained in double quotes are kept together - and discarding empty values
424 424 or fully whitespace values eg.
425 425
426 426 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
427 427
428 428 """
429 429
430 430 in_phrase = False
431 431 buf = ''
432 432 phrases = []
433 433 for char in text_query:
434 434 if in_phrase:
435 435 if char == '"': # end phrase
436 436 phrases.append(buf)
437 437 buf = ''
438 438 in_phrase = False
439 439 continue
440 440 else:
441 441 buf += char
442 442 continue
443 443 else:
444 444 if char == '"': # start phrase
445 445 in_phrase = True
446 446 phrases.append(buf)
447 447 buf = ''
448 448 continue
449 449 elif char == ' ':
450 450 phrases.append(buf)
451 451 buf = ''
452 452 continue
453 453 else:
454 454 buf += char
455 455
456 456 phrases.append(buf)
457 457 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
458 458 return phrases
459 459
460 460
461 461 def get_matching_offsets(text, phrases):
462 462 """
463 463 Returns a list of string offsets in `text` that the list of `terms` match
464 464
465 465 >>> get_matching_offsets('some text here', ['some', 'here'])
466 466 [(0, 4), (10, 14)]
467 467
468 468 """
469 469 offsets = []
470 470 for phrase in phrases:
471 471 for match in re.finditer(phrase, text):
472 472 offsets.append((match.start(), match.end()))
473 473
474 474 return offsets
475 475
476 476
477 477 def normalize_text_for_matching(x):
478 478 """
479 479 Replaces all non alnum characters to spaces and lower cases the string,
480 480 useful for comparing two text strings without punctuation
481 481 """
482 482 return re.sub(r'[^\w]', ' ', x.lower())
483 483
484 484
485 485 def get_matching_line_offsets(lines, terms):
486 486 """ Return a set of `lines` indices (starting from 1) matching a
487 487 text search query, along with `context` lines above/below matching lines
488 488
489 489 :param lines: list of strings representing lines
490 490 :param terms: search term string to match in lines eg. 'some text'
491 491 :param context: number of lines above/below a matching line to add to result
492 492 :param max_lines: cut off for lines of interest
493 493 eg.
494 494
495 495 text = '''
496 496 words words words
497 497 words words words
498 498 some text some
499 499 words words words
500 500 words words words
501 501 text here what
502 502 '''
503 503 get_matching_line_offsets(text, 'text', context=1)
504 504 {3: [(5, 9)], 6: [(0, 4)]]
505 505
506 506 """
507 507 matching_lines = {}
508 508 phrases = [normalize_text_for_matching(phrase)
509 509 for phrase in extract_phrases(terms)]
510 510
511 511 for line_index, line in enumerate(lines, start=1):
512 512 match_offsets = get_matching_offsets(
513 513 normalize_text_for_matching(line), phrases)
514 514 if match_offsets:
515 515 matching_lines[line_index] = match_offsets
516 516
517 517 return matching_lines
518 518
519 519
520 520 def hsv_to_rgb(h, s, v):
521 521 """ Convert hsv color values to rgb """
522 522
523 523 if s == 0.0:
524 524 return v, v, v
525 525 i = int(h * 6.0) # XXX assume int() truncates!
526 526 f = (h * 6.0) - i
527 527 p = v * (1.0 - s)
528 528 q = v * (1.0 - s * f)
529 529 t = v * (1.0 - s * (1.0 - f))
530 530 i = i % 6
531 531 if i == 0:
532 532 return v, t, p
533 533 if i == 1:
534 534 return q, v, p
535 535 if i == 2:
536 536 return p, v, t
537 537 if i == 3:
538 538 return p, q, v
539 539 if i == 4:
540 540 return t, p, v
541 541 if i == 5:
542 542 return v, p, q
543 543
544 544
545 545 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
546 546 """
547 547 Generator for getting n of evenly distributed colors using
548 548 hsv color and golden ratio. It always return same order of colors
549 549
550 550 :param n: number of colors to generate
551 551 :param saturation: saturation of returned colors
552 552 :param lightness: lightness of returned colors
553 553 :returns: RGB tuple
554 554 """
555 555
556 556 golden_ratio = 0.618033988749895
557 557 h = 0.22717784590367374
558 558
559 559 for _ in xrange(n):
560 560 h += golden_ratio
561 561 h %= 1
562 562 HSV_tuple = [h, saturation, lightness]
563 563 RGB_tuple = hsv_to_rgb(*HSV_tuple)
564 564 yield map(lambda x: str(int(x * 256)), RGB_tuple)
565 565
566 566
567 567 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
568 568 """
569 569 Returns a function which when called with an argument returns a unique
570 570 color for that argument, eg.
571 571
572 572 :param n: number of colors to generate
573 573 :param saturation: saturation of returned colors
574 574 :param lightness: lightness of returned colors
575 575 :returns: css RGB string
576 576
577 577 >>> color_hash = color_hasher()
578 578 >>> color_hash('hello')
579 579 'rgb(34, 12, 59)'
580 580 >>> color_hash('hello')
581 581 'rgb(34, 12, 59)'
582 582 >>> color_hash('other')
583 583 'rgb(90, 224, 159)'
584 584 """
585 585
586 586 color_dict = {}
587 587 cgenerator = unique_color_generator(
588 588 saturation=saturation, lightness=lightness)
589 589
590 590 def get_color_string(thing):
591 591 if thing in color_dict:
592 592 col = color_dict[thing]
593 593 else:
594 594 col = color_dict[thing] = cgenerator.next()
595 595 return "rgb(%s)" % (', '.join(col))
596 596
597 597 return get_color_string
598 598
599 599
600 600 def get_lexer_safe(mimetype=None, filepath=None):
601 601 """
602 602 Tries to return a relevant pygments lexer using mimetype/filepath name,
603 603 defaulting to plain text if none could be found
604 604 """
605 605 lexer = None
606 606 try:
607 607 if mimetype:
608 608 lexer = get_lexer_for_mimetype(mimetype)
609 609 if not lexer:
610 610 lexer = get_lexer_for_filename(filepath)
611 611 except pygments.util.ClassNotFound:
612 612 pass
613 613
614 614 if not lexer:
615 615 lexer = get_lexer_by_name('text')
616 616
617 617 return lexer
618 618
619 619
620 620 def get_lexer_for_filenode(filenode):
621 621 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
622 622 return lexer
623 623
624 624
625 625 def pygmentize(filenode, **kwargs):
626 626 """
627 627 pygmentize function using pygments
628 628
629 629 :param filenode:
630 630 """
631 631 lexer = get_lexer_for_filenode(filenode)
632 632 return literal(code_highlight(filenode.content, lexer,
633 633 CodeHtmlFormatter(**kwargs)))
634 634
635 635
636 636 def is_following_repo(repo_name, user_id):
637 637 from rhodecode.model.scm import ScmModel
638 638 return ScmModel().is_following_repo(repo_name, user_id)
639 639
640 640
641 641 class _Message(object):
642 642 """A message returned by ``Flash.pop_messages()``.
643 643
644 644 Converting the message to a string returns the message text. Instances
645 645 also have the following attributes:
646 646
647 647 * ``message``: the message text.
648 648 * ``category``: the category specified when the message was created.
649 649 """
650 650
651 651 def __init__(self, category, message):
652 652 self.category = category
653 653 self.message = message
654 654
655 655 def __str__(self):
656 656 return self.message
657 657
658 658 __unicode__ = __str__
659 659
660 660 def __html__(self):
661 661 return escape(safe_unicode(self.message))
662 662
663 663
664 664 class Flash(_Flash):
665 665
666 666 def pop_messages(self, request=None):
667 667 """Return all accumulated messages and delete them from the session.
668 668
669 669 The return value is a list of ``Message`` objects.
670 670 """
671 671 messages = []
672 672
673 673 if request:
674 674 session = request.session
675 675 else:
676 676 from pylons import session
677 677
678 678 # Pop the 'old' pylons flash messages. They are tuples of the form
679 679 # (category, message)
680 680 for cat, msg in session.pop(self.session_key, []):
681 681 messages.append(_Message(cat, msg))
682 682
683 683 # Pop the 'new' pyramid flash messages for each category as list
684 684 # of strings.
685 685 for cat in self.categories:
686 686 for msg in session.pop_flash(queue=cat):
687 687 messages.append(_Message(cat, msg))
688 688 # Map messages from the default queue to the 'notice' category.
689 689 for msg in session.pop_flash():
690 690 messages.append(_Message('notice', msg))
691 691
692 692 session.save()
693 693 return messages
694 694
695 695 def json_alerts(self, request=None):
696 696 payloads = []
697 697 messages = flash.pop_messages(request=request)
698 698 if messages:
699 699 for message in messages:
700 700 subdata = {}
701 701 if hasattr(message.message, 'rsplit'):
702 702 flash_data = message.message.rsplit('|DELIM|', 1)
703 703 org_message = flash_data[0]
704 704 if len(flash_data) > 1:
705 705 subdata = json.loads(flash_data[1])
706 706 else:
707 707 org_message = message.message
708 708 payloads.append({
709 709 'message': {
710 710 'message': u'{}'.format(org_message),
711 711 'level': message.category,
712 712 'force': True,
713 713 'subdata': subdata
714 714 }
715 715 })
716 716 return json.dumps(payloads)
717 717
718 718 flash = Flash()
719 719
720 720 #==============================================================================
721 721 # SCM FILTERS available via h.
722 722 #==============================================================================
723 723 from rhodecode.lib.vcs.utils import author_name, author_email
724 724 from rhodecode.lib.utils2 import credentials_filter, age as _age
725 725 from rhodecode.model.db import User, ChangesetStatus
726 726
727 727 age = _age
728 728 capitalize = lambda x: x.capitalize()
729 729 email = author_email
730 730 short_id = lambda x: x[:12]
731 731 hide_credentials = lambda x: ''.join(credentials_filter(x))
732 732
733 733
734 734 def age_component(datetime_iso, value=None, time_is_local=False):
735 735 title = value or format_date(datetime_iso)
736 736 tzinfo = '+00:00'
737 737
738 738 # detect if we have a timezone info, otherwise, add it
739 739 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
740 740 if time_is_local:
741 741 tzinfo = time.strftime("+%H:%M",
742 742 time.gmtime(
743 743 (datetime.now() - datetime.utcnow()).seconds + 1
744 744 )
745 745 )
746 746
747 747 return literal(
748 748 '<time class="timeago tooltip" '
749 749 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
750 750 datetime_iso, title, tzinfo))
751 751
752 752
753 753 def _shorten_commit_id(commit_id):
754 754 from rhodecode import CONFIG
755 755 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
756 756 return commit_id[:def_len]
757 757
758 758
759 759 def show_id(commit):
760 760 """
761 761 Configurable function that shows ID
762 762 by default it's r123:fffeeefffeee
763 763
764 764 :param commit: commit instance
765 765 """
766 766 from rhodecode import CONFIG
767 767 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
768 768
769 769 raw_id = _shorten_commit_id(commit.raw_id)
770 770 if show_idx:
771 771 return 'r%s:%s' % (commit.idx, raw_id)
772 772 else:
773 773 return '%s' % (raw_id, )
774 774
775 775
776 776 def format_date(date):
777 777 """
778 778 use a standardized formatting for dates used in RhodeCode
779 779
780 780 :param date: date/datetime object
781 781 :return: formatted date
782 782 """
783 783
784 784 if date:
785 785 _fmt = "%a, %d %b %Y %H:%M:%S"
786 786 return safe_unicode(date.strftime(_fmt))
787 787
788 788 return u""
789 789
790 790
791 791 class _RepoChecker(object):
792 792
793 793 def __init__(self, backend_alias):
794 794 self._backend_alias = backend_alias
795 795
796 796 def __call__(self, repository):
797 797 if hasattr(repository, 'alias'):
798 798 _type = repository.alias
799 799 elif hasattr(repository, 'repo_type'):
800 800 _type = repository.repo_type
801 801 else:
802 802 _type = repository
803 803 return _type == self._backend_alias
804 804
805 805 is_git = _RepoChecker('git')
806 806 is_hg = _RepoChecker('hg')
807 807 is_svn = _RepoChecker('svn')
808 808
809 809
810 810 def get_repo_type_by_name(repo_name):
811 811 repo = Repository.get_by_repo_name(repo_name)
812 812 return repo.repo_type
813 813
814 814
815 815 def is_svn_without_proxy(repository):
816 816 if is_svn(repository):
817 817 from rhodecode.model.settings import VcsSettingsModel
818 818 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
819 819 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
820 820 return False
821 821
822 822
823 823 def discover_user(author):
824 824 """
825 825 Tries to discover RhodeCode User based on the autho string. Author string
826 826 is typically `FirstName LastName <email@address.com>`
827 827 """
828 828
829 829 # if author is already an instance use it for extraction
830 830 if isinstance(author, User):
831 831 return author
832 832
833 833 # Valid email in the attribute passed, see if they're in the system
834 834 _email = author_email(author)
835 835 if _email != '':
836 836 user = User.get_by_email(_email, case_insensitive=True, cache=True)
837 837 if user is not None:
838 838 return user
839 839
840 840 # Maybe it's a username, we try to extract it and fetch by username ?
841 841 _author = author_name(author)
842 842 user = User.get_by_username(_author, case_insensitive=True, cache=True)
843 843 if user is not None:
844 844 return user
845 845
846 846 return None
847 847
848 848
849 849 def email_or_none(author):
850 850 # extract email from the commit string
851 851 _email = author_email(author)
852 852
853 853 # If we have an email, use it, otherwise
854 854 # see if it contains a username we can get an email from
855 855 if _email != '':
856 856 return _email
857 857 else:
858 858 user = User.get_by_username(
859 859 author_name(author), case_insensitive=True, cache=True)
860 860
861 861 if user is not None:
862 862 return user.email
863 863
864 864 # No valid email, not a valid user in the system, none!
865 865 return None
866 866
867 867
868 868 def link_to_user(author, length=0, **kwargs):
869 869 user = discover_user(author)
870 870 # user can be None, but if we have it already it means we can re-use it
871 871 # in the person() function, so we save 1 intensive-query
872 872 if user:
873 873 author = user
874 874
875 875 display_person = person(author, 'username_or_name_or_email')
876 876 if length:
877 877 display_person = shorter(display_person, length)
878 878
879 879 if user:
880 880 return link_to(
881 881 escape(display_person),
882 882 route_path('user_profile', username=user.username),
883 883 **kwargs)
884 884 else:
885 885 return escape(display_person)
886 886
887 887
888 888 def person(author, show_attr="username_and_name"):
889 889 user = discover_user(author)
890 890 if user:
891 891 return getattr(user, show_attr)
892 892 else:
893 893 _author = author_name(author)
894 894 _email = email(author)
895 895 return _author or _email
896 896
897 897
898 898 def author_string(email):
899 899 if email:
900 900 user = User.get_by_email(email, case_insensitive=True, cache=True)
901 901 if user:
902 902 if user.first_name or user.last_name:
903 903 return '%s %s &lt;%s&gt;' % (
904 904 user.first_name, user.last_name, email)
905 905 else:
906 906 return email
907 907 else:
908 908 return email
909 909 else:
910 910 return None
911 911
912 912
913 913 def person_by_id(id_, show_attr="username_and_name"):
914 914 # attr to return from fetched user
915 915 person_getter = lambda usr: getattr(usr, show_attr)
916 916
917 917 #maybe it's an ID ?
918 918 if str(id_).isdigit() or isinstance(id_, int):
919 919 id_ = int(id_)
920 920 user = User.get(id_)
921 921 if user is not None:
922 922 return person_getter(user)
923 923 return id_
924 924
925 925
926 926 def gravatar_with_user(request, author, show_disabled=False):
927 927 _render = request.get_partial_renderer('base/base.mako')
928 928 return _render('gravatar_with_user', author, show_disabled=show_disabled)
929 929
930 930
931 def desc_stylize(value):
931 tags_paterns = OrderedDict((
932 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
933 '<div class="metatag" tag="lang">\\2</div>')),
934
935 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
936 '<div class="metatag" tag="see">see =&gt; \\1 </div>')),
937
938 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
939 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
940
941 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
942 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
943
944 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
945 '<div class="metatag" tag="ref \\1">\\1 =&gt; <a href="/\\2">\\2</a></div>')),
946
947 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev)\]'),
948 '<div class="metatag" tag="state \\1">\\1</div>')),
949
950 # label in grey
951 ('label', (re.compile(r'\[([a-z]+)\]'),
952 '<div class="metatag" tag="label">\\1</div>')),
953
954 # generic catch all in grey
955 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
956 '<div class="metatag" tag="generic">\\1</div>')),
957 ))
958
959
960 def extract_metatags(value):
932 961 """
933 converts tags from value into html equivalent
934
935 :param value:
962 Extract supported meta-tags from given text value
936 963 """
937 964 if not value:
938 965 return ''
939 966
940 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
941 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
942 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
943 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
944 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
945 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
946 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
947 '<div class="metatag" tag="lang">\\2</div>', value)
948 value = re.sub(r'\[([a-z]+)\]',
949 '<div class="metatag" tag="\\1">\\1</div>', value)
967 tags = []
968 for key, val in tags_paterns.items():
969 pat, replace_html = val
970 tags.extend([(key, x.group()) for x in pat.finditer(value)])
971 value = pat.sub('', value)
950 972
951 return value
973 return tags, value
952 974
953 975
954 def escaped_stylize(value):
976 def style_metatag(tag_type, value):
955 977 """
956 converts tags from value into html equivalent, but escaping its value first
978 converts tags from value into html equivalent
957 979 """
958 980 if not value:
959 981 return ''
960 982
961 # Using default webhelper escape method, but has to force it as a
962 # plain unicode instead of a markup tag to be used in regex expressions
963 value = unicode(escape(safe_unicode(value)))
983 html_value = value
984 tag_data = tags_paterns.get(tag_type)
985 if tag_data:
986 pat, replace_html = tag_data
987 # convert to plain `unicode` instead of a markup tag to be used in
988 # regex expressions. safe_unicode doesn't work here
989 html_value = pat.sub(replace_html, unicode(value))
964 990
965 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
966 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
967 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
968 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
969 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
970 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
971 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
972 '<div class="metatag" tag="lang">\\2</div>', value)
973 value = re.sub(r'\[([a-z]+)\]',
974 '<div class="metatag" tag="\\1">\\1</div>', value)
975
976 return value
991 return html_value
977 992
978 993
979 994 def bool2icon(value):
980 995 """
981 996 Returns boolean value of a given value, represented as html element with
982 997 classes that will represent icons
983 998
984 999 :param value: given value to convert to html node
985 1000 """
986 1001
987 1002 if value: # does bool conversion
988 1003 return HTML.tag('i', class_="icon-true")
989 1004 else: # not true as bool
990 1005 return HTML.tag('i', class_="icon-false")
991 1006
992 1007
993 1008 #==============================================================================
994 1009 # PERMS
995 1010 #==============================================================================
996 1011 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
997 1012 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
998 1013 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
999 1014 csrf_token_key
1000 1015
1001 1016
1002 1017 #==============================================================================
1003 1018 # GRAVATAR URL
1004 1019 #==============================================================================
1005 1020 class InitialsGravatar(object):
1006 1021 def __init__(self, email_address, first_name, last_name, size=30,
1007 1022 background=None, text_color='#fff'):
1008 1023 self.size = size
1009 1024 self.first_name = first_name
1010 1025 self.last_name = last_name
1011 1026 self.email_address = email_address
1012 1027 self.background = background or self.str2color(email_address)
1013 1028 self.text_color = text_color
1014 1029
1015 1030 def get_color_bank(self):
1016 1031 """
1017 1032 returns a predefined list of colors that gravatars can use.
1018 1033 Those are randomized distinct colors that guarantee readability and
1019 1034 uniqueness.
1020 1035
1021 1036 generated with: http://phrogz.net/css/distinct-colors.html
1022 1037 """
1023 1038 return [
1024 1039 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1025 1040 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1026 1041 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1027 1042 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1028 1043 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1029 1044 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1030 1045 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1031 1046 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1032 1047 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1033 1048 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1034 1049 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1035 1050 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1036 1051 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1037 1052 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1038 1053 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1039 1054 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1040 1055 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1041 1056 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1042 1057 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1043 1058 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1044 1059 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1045 1060 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1046 1061 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1047 1062 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1048 1063 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1049 1064 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1050 1065 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1051 1066 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1052 1067 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1053 1068 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1054 1069 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1055 1070 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1056 1071 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1057 1072 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1058 1073 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1059 1074 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1060 1075 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1061 1076 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1062 1077 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1063 1078 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1064 1079 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1065 1080 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1066 1081 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1067 1082 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1068 1083 '#4f8c46', '#368dd9', '#5c0073'
1069 1084 ]
1070 1085
1071 1086 def rgb_to_hex_color(self, rgb_tuple):
1072 1087 """
1073 1088 Converts an rgb_tuple passed to an hex color.
1074 1089
1075 1090 :param rgb_tuple: tuple with 3 ints represents rgb color space
1076 1091 """
1077 1092 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1078 1093
1079 1094 def email_to_int_list(self, email_str):
1080 1095 """
1081 1096 Get every byte of the hex digest value of email and turn it to integer.
1082 1097 It's going to be always between 0-255
1083 1098 """
1084 1099 digest = md5_safe(email_str.lower())
1085 1100 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1086 1101
1087 1102 def pick_color_bank_index(self, email_str, color_bank):
1088 1103 return self.email_to_int_list(email_str)[0] % len(color_bank)
1089 1104
1090 1105 def str2color(self, email_str):
1091 1106 """
1092 1107 Tries to map in a stable algorithm an email to color
1093 1108
1094 1109 :param email_str:
1095 1110 """
1096 1111 color_bank = self.get_color_bank()
1097 1112 # pick position (module it's length so we always find it in the
1098 1113 # bank even if it's smaller than 256 values
1099 1114 pos = self.pick_color_bank_index(email_str, color_bank)
1100 1115 return color_bank[pos]
1101 1116
1102 1117 def normalize_email(self, email_address):
1103 1118 import unicodedata
1104 1119 # default host used to fill in the fake/missing email
1105 1120 default_host = u'localhost'
1106 1121
1107 1122 if not email_address:
1108 1123 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1109 1124
1110 1125 email_address = safe_unicode(email_address)
1111 1126
1112 1127 if u'@' not in email_address:
1113 1128 email_address = u'%s@%s' % (email_address, default_host)
1114 1129
1115 1130 if email_address.endswith(u'@'):
1116 1131 email_address = u'%s%s' % (email_address, default_host)
1117 1132
1118 1133 email_address = unicodedata.normalize('NFKD', email_address)\
1119 1134 .encode('ascii', 'ignore')
1120 1135 return email_address
1121 1136
1122 1137 def get_initials(self):
1123 1138 """
1124 1139 Returns 2 letter initials calculated based on the input.
1125 1140 The algorithm picks first given email address, and takes first letter
1126 1141 of part before @, and then the first letter of server name. In case
1127 1142 the part before @ is in a format of `somestring.somestring2` it replaces
1128 1143 the server letter with first letter of somestring2
1129 1144
1130 1145 In case function was initialized with both first and lastname, this
1131 1146 overrides the extraction from email by first letter of the first and
1132 1147 last name. We add special logic to that functionality, In case Full name
1133 1148 is compound, like Guido Von Rossum, we use last part of the last name
1134 1149 (Von Rossum) picking `R`.
1135 1150
1136 1151 Function also normalizes the non-ascii characters to they ascii
1137 1152 representation, eg Δ„ => A
1138 1153 """
1139 1154 import unicodedata
1140 1155 # replace non-ascii to ascii
1141 1156 first_name = unicodedata.normalize(
1142 1157 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1143 1158 last_name = unicodedata.normalize(
1144 1159 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1145 1160
1146 1161 # do NFKD encoding, and also make sure email has proper format
1147 1162 email_address = self.normalize_email(self.email_address)
1148 1163
1149 1164 # first push the email initials
1150 1165 prefix, server = email_address.split('@', 1)
1151 1166
1152 1167 # check if prefix is maybe a 'first_name.last_name' syntax
1153 1168 _dot_split = prefix.rsplit('.', 1)
1154 1169 if len(_dot_split) == 2 and _dot_split[1]:
1155 1170 initials = [_dot_split[0][0], _dot_split[1][0]]
1156 1171 else:
1157 1172 initials = [prefix[0], server[0]]
1158 1173
1159 1174 # then try to replace either first_name or last_name
1160 1175 fn_letter = (first_name or " ")[0].strip()
1161 1176 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1162 1177
1163 1178 if fn_letter:
1164 1179 initials[0] = fn_letter
1165 1180
1166 1181 if ln_letter:
1167 1182 initials[1] = ln_letter
1168 1183
1169 1184 return ''.join(initials).upper()
1170 1185
1171 1186 def get_img_data_by_type(self, font_family, img_type):
1172 1187 default_user = """
1173 1188 <svg xmlns="http://www.w3.org/2000/svg"
1174 1189 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1175 1190 viewBox="-15 -10 439.165 429.164"
1176 1191
1177 1192 xml:space="preserve"
1178 1193 style="background:{background};" >
1179 1194
1180 1195 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1181 1196 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1182 1197 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1183 1198 168.596,153.916,216.671,
1184 1199 204.583,216.671z" fill="{text_color}"/>
1185 1200 <path d="M407.164,374.717L360.88,
1186 1201 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1187 1202 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1188 1203 15.366-44.203,23.488-69.076,23.488c-24.877,
1189 1204 0-48.762-8.122-69.078-23.488
1190 1205 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1191 1206 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1192 1207 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1193 1208 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1194 1209 19.402-10.527 C409.699,390.129,
1195 1210 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1196 1211 </svg>""".format(
1197 1212 size=self.size,
1198 1213 background='#979797', # @grey4
1199 1214 text_color=self.text_color,
1200 1215 font_family=font_family)
1201 1216
1202 1217 return {
1203 1218 "default_user": default_user
1204 1219 }[img_type]
1205 1220
1206 1221 def get_img_data(self, svg_type=None):
1207 1222 """
1208 1223 generates the svg metadata for image
1209 1224 """
1210 1225
1211 1226 font_family = ','.join([
1212 1227 'proximanovaregular',
1213 1228 'Proxima Nova Regular',
1214 1229 'Proxima Nova',
1215 1230 'Arial',
1216 1231 'Lucida Grande',
1217 1232 'sans-serif'
1218 1233 ])
1219 1234 if svg_type:
1220 1235 return self.get_img_data_by_type(font_family, svg_type)
1221 1236
1222 1237 initials = self.get_initials()
1223 1238 img_data = """
1224 1239 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1225 1240 width="{size}" height="{size}"
1226 1241 style="width: 100%; height: 100%; background-color: {background}"
1227 1242 viewBox="0 0 {size} {size}">
1228 1243 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1229 1244 pointer-events="auto" fill="{text_color}"
1230 1245 font-family="{font_family}"
1231 1246 style="font-weight: 400; font-size: {f_size}px;">{text}
1232 1247 </text>
1233 1248 </svg>""".format(
1234 1249 size=self.size,
1235 1250 f_size=self.size/1.85, # scale the text inside the box nicely
1236 1251 background=self.background,
1237 1252 text_color=self.text_color,
1238 1253 text=initials.upper(),
1239 1254 font_family=font_family)
1240 1255
1241 1256 return img_data
1242 1257
1243 1258 def generate_svg(self, svg_type=None):
1244 1259 img_data = self.get_img_data(svg_type)
1245 1260 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1246 1261
1247 1262
1248 1263 def initials_gravatar(email_address, first_name, last_name, size=30):
1249 1264 svg_type = None
1250 1265 if email_address == User.DEFAULT_USER_EMAIL:
1251 1266 svg_type = 'default_user'
1252 1267 klass = InitialsGravatar(email_address, first_name, last_name, size)
1253 1268 return klass.generate_svg(svg_type=svg_type)
1254 1269
1255 1270
1256 1271 def gravatar_url(email_address, size=30, request=None):
1257 1272 request = get_current_request()
1258 1273 if request and hasattr(request, 'call_context'):
1259 1274 _use_gravatar = request.call_context.visual.use_gravatar
1260 1275 _gravatar_url = request.call_context.visual.gravatar_url
1261 1276 else:
1262 1277 # doh, we need to re-import those to mock it later
1263 1278 from pylons import tmpl_context as c
1264 1279
1265 1280 _use_gravatar = c.visual.use_gravatar
1266 1281 _gravatar_url = c.visual.gravatar_url
1267 1282
1268 1283 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1269 1284
1270 1285 email_address = email_address or User.DEFAULT_USER_EMAIL
1271 1286 if isinstance(email_address, unicode):
1272 1287 # hashlib crashes on unicode items
1273 1288 email_address = safe_str(email_address)
1274 1289
1275 1290 # empty email or default user
1276 1291 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1277 1292 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1278 1293
1279 1294 if _use_gravatar:
1280 1295 # TODO: Disuse pyramid thread locals. Think about another solution to
1281 1296 # get the host and schema here.
1282 1297 request = get_current_request()
1283 1298 tmpl = safe_str(_gravatar_url)
1284 1299 tmpl = tmpl.replace('{email}', email_address)\
1285 1300 .replace('{md5email}', md5_safe(email_address.lower())) \
1286 1301 .replace('{netloc}', request.host)\
1287 1302 .replace('{scheme}', request.scheme)\
1288 1303 .replace('{size}', safe_str(size))
1289 1304 return tmpl
1290 1305 else:
1291 1306 return initials_gravatar(email_address, '', '', size=size)
1292 1307
1293 1308
1294 1309 class Page(_Page):
1295 1310 """
1296 1311 Custom pager to match rendering style with paginator
1297 1312 """
1298 1313
1299 1314 def _get_pos(self, cur_page, max_page, items):
1300 1315 edge = (items / 2) + 1
1301 1316 if (cur_page <= edge):
1302 1317 radius = max(items / 2, items - cur_page)
1303 1318 elif (max_page - cur_page) < edge:
1304 1319 radius = (items - 1) - (max_page - cur_page)
1305 1320 else:
1306 1321 radius = items / 2
1307 1322
1308 1323 left = max(1, (cur_page - (radius)))
1309 1324 right = min(max_page, cur_page + (radius))
1310 1325 return left, cur_page, right
1311 1326
1312 1327 def _range(self, regexp_match):
1313 1328 """
1314 1329 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1315 1330
1316 1331 Arguments:
1317 1332
1318 1333 regexp_match
1319 1334 A "re" (regular expressions) match object containing the
1320 1335 radius of linked pages around the current page in
1321 1336 regexp_match.group(1) as a string
1322 1337
1323 1338 This function is supposed to be called as a callable in
1324 1339 re.sub.
1325 1340
1326 1341 """
1327 1342 radius = int(regexp_match.group(1))
1328 1343
1329 1344 # Compute the first and last page number within the radius
1330 1345 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1331 1346 # -> leftmost_page = 5
1332 1347 # -> rightmost_page = 9
1333 1348 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1334 1349 self.last_page,
1335 1350 (radius * 2) + 1)
1336 1351 nav_items = []
1337 1352
1338 1353 # Create a link to the first page (unless we are on the first page
1339 1354 # or there would be no need to insert '..' spacers)
1340 1355 if self.page != self.first_page and self.first_page < leftmost_page:
1341 1356 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1342 1357
1343 1358 # Insert dots if there are pages between the first page
1344 1359 # and the currently displayed page range
1345 1360 if leftmost_page - self.first_page > 1:
1346 1361 # Wrap in a SPAN tag if nolink_attr is set
1347 1362 text = '..'
1348 1363 if self.dotdot_attr:
1349 1364 text = HTML.span(c=text, **self.dotdot_attr)
1350 1365 nav_items.append(text)
1351 1366
1352 1367 for thispage in xrange(leftmost_page, rightmost_page + 1):
1353 1368 # Hilight the current page number and do not use a link
1354 1369 if thispage == self.page:
1355 1370 text = '%s' % (thispage,)
1356 1371 # Wrap in a SPAN tag if nolink_attr is set
1357 1372 if self.curpage_attr:
1358 1373 text = HTML.span(c=text, **self.curpage_attr)
1359 1374 nav_items.append(text)
1360 1375 # Otherwise create just a link to that page
1361 1376 else:
1362 1377 text = '%s' % (thispage,)
1363 1378 nav_items.append(self._pagerlink(thispage, text))
1364 1379
1365 1380 # Insert dots if there are pages between the displayed
1366 1381 # page numbers and the end of the page range
1367 1382 if self.last_page - rightmost_page > 1:
1368 1383 text = '..'
1369 1384 # Wrap in a SPAN tag if nolink_attr is set
1370 1385 if self.dotdot_attr:
1371 1386 text = HTML.span(c=text, **self.dotdot_attr)
1372 1387 nav_items.append(text)
1373 1388
1374 1389 # Create a link to the very last page (unless we are on the last
1375 1390 # page or there would be no need to insert '..' spacers)
1376 1391 if self.page != self.last_page and rightmost_page < self.last_page:
1377 1392 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1378 1393
1379 1394 ## prerender links
1380 1395 #_page_link = url.current()
1381 1396 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1382 1397 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1383 1398 return self.separator.join(nav_items)
1384 1399
1385 1400 def pager(self, format='~2~', page_param='page', partial_param='partial',
1386 1401 show_if_single_page=False, separator=' ', onclick=None,
1387 1402 symbol_first='<<', symbol_last='>>',
1388 1403 symbol_previous='<', symbol_next='>',
1389 1404 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1390 1405 curpage_attr={'class': 'pager_curpage'},
1391 1406 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1392 1407
1393 1408 self.curpage_attr = curpage_attr
1394 1409 self.separator = separator
1395 1410 self.pager_kwargs = kwargs
1396 1411 self.page_param = page_param
1397 1412 self.partial_param = partial_param
1398 1413 self.onclick = onclick
1399 1414 self.link_attr = link_attr
1400 1415 self.dotdot_attr = dotdot_attr
1401 1416
1402 1417 # Don't show navigator if there is no more than one page
1403 1418 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1404 1419 return ''
1405 1420
1406 1421 from string import Template
1407 1422 # Replace ~...~ in token format by range of pages
1408 1423 result = re.sub(r'~(\d+)~', self._range, format)
1409 1424
1410 1425 # Interpolate '%' variables
1411 1426 result = Template(result).safe_substitute({
1412 1427 'first_page': self.first_page,
1413 1428 'last_page': self.last_page,
1414 1429 'page': self.page,
1415 1430 'page_count': self.page_count,
1416 1431 'items_per_page': self.items_per_page,
1417 1432 'first_item': self.first_item,
1418 1433 'last_item': self.last_item,
1419 1434 'item_count': self.item_count,
1420 1435 'link_first': self.page > self.first_page and \
1421 1436 self._pagerlink(self.first_page, symbol_first) or '',
1422 1437 'link_last': self.page < self.last_page and \
1423 1438 self._pagerlink(self.last_page, symbol_last) or '',
1424 1439 'link_previous': self.previous_page and \
1425 1440 self._pagerlink(self.previous_page, symbol_previous) \
1426 1441 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1427 1442 'link_next': self.next_page and \
1428 1443 self._pagerlink(self.next_page, symbol_next) \
1429 1444 or HTML.span(symbol_next, class_="pg-next disabled")
1430 1445 })
1431 1446
1432 1447 return literal(result)
1433 1448
1434 1449
1435 1450 #==============================================================================
1436 1451 # REPO PAGER, PAGER FOR REPOSITORY
1437 1452 #==============================================================================
1438 1453 class RepoPage(Page):
1439 1454
1440 1455 def __init__(self, collection, page=1, items_per_page=20,
1441 1456 item_count=None, url=None, **kwargs):
1442 1457
1443 1458 """Create a "RepoPage" instance. special pager for paging
1444 1459 repository
1445 1460 """
1446 1461 self._url_generator = url
1447 1462
1448 1463 # Safe the kwargs class-wide so they can be used in the pager() method
1449 1464 self.kwargs = kwargs
1450 1465
1451 1466 # Save a reference to the collection
1452 1467 self.original_collection = collection
1453 1468
1454 1469 self.collection = collection
1455 1470
1456 1471 # The self.page is the number of the current page.
1457 1472 # The first page has the number 1!
1458 1473 try:
1459 1474 self.page = int(page) # make it int() if we get it as a string
1460 1475 except (ValueError, TypeError):
1461 1476 self.page = 1
1462 1477
1463 1478 self.items_per_page = items_per_page
1464 1479
1465 1480 # Unless the user tells us how many items the collections has
1466 1481 # we calculate that ourselves.
1467 1482 if item_count is not None:
1468 1483 self.item_count = item_count
1469 1484 else:
1470 1485 self.item_count = len(self.collection)
1471 1486
1472 1487 # Compute the number of the first and last available page
1473 1488 if self.item_count > 0:
1474 1489 self.first_page = 1
1475 1490 self.page_count = int(math.ceil(float(self.item_count) /
1476 1491 self.items_per_page))
1477 1492 self.last_page = self.first_page + self.page_count - 1
1478 1493
1479 1494 # Make sure that the requested page number is the range of
1480 1495 # valid pages
1481 1496 if self.page > self.last_page:
1482 1497 self.page = self.last_page
1483 1498 elif self.page < self.first_page:
1484 1499 self.page = self.first_page
1485 1500
1486 1501 # Note: the number of items on this page can be less than
1487 1502 # items_per_page if the last page is not full
1488 1503 self.first_item = max(0, (self.item_count) - (self.page *
1489 1504 items_per_page))
1490 1505 self.last_item = ((self.item_count - 1) - items_per_page *
1491 1506 (self.page - 1))
1492 1507
1493 1508 self.items = list(self.collection[self.first_item:self.last_item + 1])
1494 1509
1495 1510 # Links to previous and next page
1496 1511 if self.page > self.first_page:
1497 1512 self.previous_page = self.page - 1
1498 1513 else:
1499 1514 self.previous_page = None
1500 1515
1501 1516 if self.page < self.last_page:
1502 1517 self.next_page = self.page + 1
1503 1518 else:
1504 1519 self.next_page = None
1505 1520
1506 1521 # No items available
1507 1522 else:
1508 1523 self.first_page = None
1509 1524 self.page_count = 0
1510 1525 self.last_page = None
1511 1526 self.first_item = None
1512 1527 self.last_item = None
1513 1528 self.previous_page = None
1514 1529 self.next_page = None
1515 1530 self.items = []
1516 1531
1517 1532 # This is a subclass of the 'list' type. Initialise the list now.
1518 1533 list.__init__(self, reversed(self.items))
1519 1534
1520 1535
1521 1536 def breadcrumb_repo_link(repo):
1522 1537 """
1523 1538 Makes a breadcrumbs path link to repo
1524 1539
1525 1540 ex::
1526 1541 group >> subgroup >> repo
1527 1542
1528 1543 :param repo: a Repository instance
1529 1544 """
1530 1545
1531 1546 path = [
1532 1547 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1533 1548 for group in repo.groups_with_parents
1534 1549 ] + [
1535 1550 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1536 1551 ]
1537 1552
1538 1553 return literal(' &raquo; '.join(path))
1539 1554
1540 1555
1541 1556 def format_byte_size_binary(file_size):
1542 1557 """
1543 1558 Formats file/folder sizes to standard.
1544 1559 """
1545 1560 if file_size is None:
1546 1561 file_size = 0
1547 1562
1548 1563 formatted_size = format_byte_size(file_size, binary=True)
1549 1564 return formatted_size
1550 1565
1551 1566
1552 1567 def urlify_text(text_, safe=True):
1553 1568 """
1554 1569 Extrac urls from text and make html links out of them
1555 1570
1556 1571 :param text_:
1557 1572 """
1558 1573
1559 1574 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1560 1575 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1561 1576
1562 1577 def url_func(match_obj):
1563 1578 url_full = match_obj.groups()[0]
1564 1579 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1565 1580 _newtext = url_pat.sub(url_func, text_)
1566 1581 if safe:
1567 1582 return literal(_newtext)
1568 1583 return _newtext
1569 1584
1570 1585
1571 1586 def urlify_commits(text_, repository):
1572 1587 """
1573 1588 Extract commit ids from text and make link from them
1574 1589
1575 1590 :param text_:
1576 1591 :param repository: repo name to build the URL with
1577 1592 """
1578 1593
1579 1594 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1580 1595
1581 1596 def url_func(match_obj):
1582 1597 commit_id = match_obj.groups()[1]
1583 1598 pref = match_obj.groups()[0]
1584 1599 suf = match_obj.groups()[2]
1585 1600
1586 1601 tmpl = (
1587 1602 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1588 1603 '%(commit_id)s</a>%(suf)s'
1589 1604 )
1590 1605 return tmpl % {
1591 1606 'pref': pref,
1592 1607 'cls': 'revision-link',
1593 1608 'url': route_url('repo_commit', repo_name=repository,
1594 1609 commit_id=commit_id),
1595 1610 'commit_id': commit_id,
1596 1611 'suf': suf
1597 1612 }
1598 1613
1599 1614 newtext = URL_PAT.sub(url_func, text_)
1600 1615
1601 1616 return newtext
1602 1617
1603 1618
1604 1619 def _process_url_func(match_obj, repo_name, uid, entry,
1605 1620 return_raw_data=False, link_format='html'):
1606 1621 pref = ''
1607 1622 if match_obj.group().startswith(' '):
1608 1623 pref = ' '
1609 1624
1610 1625 issue_id = ''.join(match_obj.groups())
1611 1626
1612 1627 if link_format == 'html':
1613 1628 tmpl = (
1614 1629 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1615 1630 '%(issue-prefix)s%(id-repr)s'
1616 1631 '</a>')
1617 1632 elif link_format == 'rst':
1618 1633 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1619 1634 elif link_format == 'markdown':
1620 1635 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1621 1636 else:
1622 1637 raise ValueError('Bad link_format:{}'.format(link_format))
1623 1638
1624 1639 (repo_name_cleaned,
1625 1640 parent_group_name) = RepoGroupModel().\
1626 1641 _get_group_name_and_parent(repo_name)
1627 1642
1628 1643 # variables replacement
1629 1644 named_vars = {
1630 1645 'id': issue_id,
1631 1646 'repo': repo_name,
1632 1647 'repo_name': repo_name_cleaned,
1633 1648 'group_name': parent_group_name
1634 1649 }
1635 1650 # named regex variables
1636 1651 named_vars.update(match_obj.groupdict())
1637 1652 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1638 1653
1639 1654 data = {
1640 1655 'pref': pref,
1641 1656 'cls': 'issue-tracker-link',
1642 1657 'url': _url,
1643 1658 'id-repr': issue_id,
1644 1659 'issue-prefix': entry['pref'],
1645 1660 'serv': entry['url'],
1646 1661 }
1647 1662 if return_raw_data:
1648 1663 return {
1649 1664 'id': issue_id,
1650 1665 'url': _url
1651 1666 }
1652 1667 return tmpl % data
1653 1668
1654 1669
1655 1670 def process_patterns(text_string, repo_name, link_format='html'):
1656 1671 allowed_formats = ['html', 'rst', 'markdown']
1657 1672 if link_format not in allowed_formats:
1658 1673 raise ValueError('Link format can be only one of:{} got {}'.format(
1659 1674 allowed_formats, link_format))
1660 1675
1661 1676 repo = None
1662 1677 if repo_name:
1663 1678 # Retrieving repo_name to avoid invalid repo_name to explode on
1664 1679 # IssueTrackerSettingsModel but still passing invalid name further down
1665 1680 repo = Repository.get_by_repo_name(repo_name, cache=True)
1666 1681
1667 1682 settings_model = IssueTrackerSettingsModel(repo=repo)
1668 1683 active_entries = settings_model.get_settings(cache=True)
1669 1684
1670 1685 issues_data = []
1671 1686 newtext = text_string
1672 1687
1673 1688 for uid, entry in active_entries.items():
1674 1689 log.debug('found issue tracker entry with uid %s' % (uid,))
1675 1690
1676 1691 if not (entry['pat'] and entry['url']):
1677 1692 log.debug('skipping due to missing data')
1678 1693 continue
1679 1694
1680 1695 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1681 1696 % (uid, entry['pat'], entry['url'], entry['pref']))
1682 1697
1683 1698 try:
1684 1699 pattern = re.compile(r'%s' % entry['pat'])
1685 1700 except re.error:
1686 1701 log.exception(
1687 1702 'issue tracker pattern: `%s` failed to compile',
1688 1703 entry['pat'])
1689 1704 continue
1690 1705
1691 1706 data_func = partial(
1692 1707 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1693 1708 return_raw_data=True)
1694 1709
1695 1710 for match_obj in pattern.finditer(text_string):
1696 1711 issues_data.append(data_func(match_obj))
1697 1712
1698 1713 url_func = partial(
1699 1714 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1700 1715 link_format=link_format)
1701 1716
1702 1717 newtext = pattern.sub(url_func, newtext)
1703 1718 log.debug('processed prefix:uid `%s`' % (uid,))
1704 1719
1705 1720 return newtext, issues_data
1706 1721
1707 1722
1708 1723 def urlify_commit_message(commit_text, repository=None):
1709 1724 """
1710 1725 Parses given text message and makes proper links.
1711 1726 issues are linked to given issue-server, and rest is a commit link
1712 1727
1713 1728 :param commit_text:
1714 1729 :param repository:
1715 1730 """
1716 1731 from pylons import url # doh, we need to re-import url to mock it later
1717 1732
1718 1733 def escaper(string):
1719 1734 return string.replace('<', '&lt;').replace('>', '&gt;')
1720 1735
1721 1736 newtext = escaper(commit_text)
1722 1737
1723 1738 # extract http/https links and make them real urls
1724 1739 newtext = urlify_text(newtext, safe=False)
1725 1740
1726 1741 # urlify commits - extract commit ids and make link out of them, if we have
1727 1742 # the scope of repository present.
1728 1743 if repository:
1729 1744 newtext = urlify_commits(newtext, repository)
1730 1745
1731 1746 # process issue tracker patterns
1732 1747 newtext, issues = process_patterns(newtext, repository or '')
1733 1748
1734 1749 return literal(newtext)
1735 1750
1736 1751
1737 1752 def render_binary(repo_name, file_obj):
1738 1753 """
1739 1754 Choose how to render a binary file
1740 1755 """
1741 1756 filename = file_obj.name
1742 1757
1743 1758 # images
1744 1759 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1745 1760 if fnmatch.fnmatch(filename, pat=ext):
1746 1761 alt = filename
1747 1762 src = route_path(
1748 1763 'repo_file_raw', repo_name=repo_name,
1749 1764 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1750 1765 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1751 1766
1752 1767
1753 1768 def renderer_from_filename(filename, exclude=None):
1754 1769 """
1755 1770 choose a renderer based on filename, this works only for text based files
1756 1771 """
1757 1772
1758 1773 # ipython
1759 1774 for ext in ['*.ipynb']:
1760 1775 if fnmatch.fnmatch(filename, pat=ext):
1761 1776 return 'jupyter'
1762 1777
1763 1778 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1764 1779 if is_markup:
1765 1780 return is_markup
1766 1781 return None
1767 1782
1768 1783
1769 1784 def render(source, renderer='rst', mentions=False, relative_urls=None,
1770 1785 repo_name=None):
1771 1786
1772 1787 def maybe_convert_relative_links(html_source):
1773 1788 if relative_urls:
1774 1789 return relative_links(html_source, relative_urls)
1775 1790 return html_source
1776 1791
1777 1792 if renderer == 'rst':
1778 1793 if repo_name:
1779 1794 # process patterns on comments if we pass in repo name
1780 1795 source, issues = process_patterns(
1781 1796 source, repo_name, link_format='rst')
1782 1797
1783 1798 return literal(
1784 1799 '<div class="rst-block">%s</div>' %
1785 1800 maybe_convert_relative_links(
1786 1801 MarkupRenderer.rst(source, mentions=mentions)))
1787 1802 elif renderer == 'markdown':
1788 1803 if repo_name:
1789 1804 # process patterns on comments if we pass in repo name
1790 1805 source, issues = process_patterns(
1791 1806 source, repo_name, link_format='markdown')
1792 1807
1793 1808 return literal(
1794 1809 '<div class="markdown-block">%s</div>' %
1795 1810 maybe_convert_relative_links(
1796 1811 MarkupRenderer.markdown(source, flavored=True,
1797 1812 mentions=mentions)))
1798 1813 elif renderer == 'jupyter':
1799 1814 return literal(
1800 1815 '<div class="ipynb">%s</div>' %
1801 1816 maybe_convert_relative_links(
1802 1817 MarkupRenderer.jupyter(source)))
1803 1818
1804 1819 # None means just show the file-source
1805 1820 return None
1806 1821
1807 1822
1808 1823 def commit_status(repo, commit_id):
1809 1824 return ChangesetStatusModel().get_status(repo, commit_id)
1810 1825
1811 1826
1812 1827 def commit_status_lbl(commit_status):
1813 1828 return dict(ChangesetStatus.STATUSES).get(commit_status)
1814 1829
1815 1830
1816 1831 def commit_time(repo_name, commit_id):
1817 1832 repo = Repository.get_by_repo_name(repo_name)
1818 1833 commit = repo.get_commit(commit_id=commit_id)
1819 1834 return commit.date
1820 1835
1821 1836
1822 1837 def get_permission_name(key):
1823 1838 return dict(Permission.PERMS).get(key)
1824 1839
1825 1840
1826 1841 def journal_filter_help(request):
1827 1842 _ = request.translate
1828 1843
1829 1844 return _(
1830 1845 'Example filter terms:\n' +
1831 1846 ' repository:vcs\n' +
1832 1847 ' username:marcin\n' +
1833 1848 ' username:(NOT marcin)\n' +
1834 1849 ' action:*push*\n' +
1835 1850 ' ip:127.0.0.1\n' +
1836 1851 ' date:20120101\n' +
1837 1852 ' date:[20120101100000 TO 20120102]\n' +
1838 1853 '\n' +
1839 1854 'Generate wildcards using \'*\' character:\n' +
1840 1855 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1841 1856 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1842 1857 '\n' +
1843 1858 'Optional AND / OR operators in queries\n' +
1844 1859 ' "repository:vcs OR repository:test"\n' +
1845 1860 ' "username:test AND repository:test*"\n'
1846 1861 )
1847 1862
1848 1863
1849 1864 def search_filter_help(searcher, request):
1850 1865 _ = request.translate
1851 1866
1852 1867 terms = ''
1853 1868 return _(
1854 1869 'Example filter terms for `{searcher}` search:\n' +
1855 1870 '{terms}\n' +
1856 1871 'Generate wildcards using \'*\' character:\n' +
1857 1872 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1858 1873 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1859 1874 '\n' +
1860 1875 'Optional AND / OR operators in queries\n' +
1861 1876 ' "repo_name:vcs OR repo_name:test"\n' +
1862 1877 ' "owner:test AND repo_name:test*"\n' +
1863 1878 'More: {search_doc}'
1864 1879 ).format(searcher=searcher.name,
1865 1880 terms=terms, search_doc=searcher.query_lang_doc)
1866 1881
1867 1882
1868 1883 def not_mapped_error(repo_name):
1869 1884 from rhodecode.translation import _
1870 1885 flash(_('%s repository is not mapped to db perhaps'
1871 1886 ' it was created or renamed from the filesystem'
1872 1887 ' please run the application again'
1873 1888 ' in order to rescan repositories') % repo_name, category='error')
1874 1889
1875 1890
1876 1891 def ip_range(ip_addr):
1877 1892 from rhodecode.model.db import UserIpMap
1878 1893 s, e = UserIpMap._get_ip_range(ip_addr)
1879 1894 return '%s - %s' % (s, e)
1880 1895
1881 1896
1882 1897 def form(url, method='post', needs_csrf_token=True, **attrs):
1883 1898 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1884 1899 if method.lower() != 'get' and needs_csrf_token:
1885 1900 raise Exception(
1886 1901 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1887 1902 'CSRF token. If the endpoint does not require such token you can ' +
1888 1903 'explicitly set the parameter needs_csrf_token to false.')
1889 1904
1890 1905 return wh_form(url, method=method, **attrs)
1891 1906
1892 1907
1893 1908 def secure_form(form_url, method="POST", multipart=False, **attrs):
1894 1909 """Start a form tag that points the action to an url. This
1895 1910 form tag will also include the hidden field containing
1896 1911 the auth token.
1897 1912
1898 1913 The url options should be given either as a string, or as a
1899 1914 ``url()`` function. The method for the form defaults to POST.
1900 1915
1901 1916 Options:
1902 1917
1903 1918 ``multipart``
1904 1919 If set to True, the enctype is set to "multipart/form-data".
1905 1920 ``method``
1906 1921 The method to use when submitting the form, usually either
1907 1922 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1908 1923 hidden input with name _method is added to simulate the verb
1909 1924 over POST.
1910 1925
1911 1926 """
1912 1927 from webhelpers.pylonslib.secure_form import insecure_form
1913 1928
1914 1929 session = None
1915 1930
1916 1931 # TODO(marcink): after pyramid migration require request variable ALWAYS
1917 1932 if 'request' in attrs:
1918 1933 session = attrs['request'].session
1919 1934 del attrs['request']
1920 1935
1921 1936 form = insecure_form(form_url, method, multipart, **attrs)
1922 1937 token = literal(
1923 1938 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1924 1939 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1925 1940
1926 1941 return literal("%s\n%s" % (form, token))
1927 1942
1928 1943
1929 1944 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1930 1945 select_html = select(name, selected, options, **attrs)
1931 1946 select2 = """
1932 1947 <script>
1933 1948 $(document).ready(function() {
1934 1949 $('#%s').select2({
1935 1950 containerCssClass: 'drop-menu',
1936 1951 dropdownCssClass: 'drop-menu-dropdown',
1937 1952 dropdownAutoWidth: true%s
1938 1953 });
1939 1954 });
1940 1955 </script>
1941 1956 """
1942 1957 filter_option = """,
1943 1958 minimumResultsForSearch: -1
1944 1959 """
1945 1960 input_id = attrs.get('id') or name
1946 1961 filter_enabled = "" if enable_filter else filter_option
1947 1962 select_script = literal(select2 % (input_id, filter_enabled))
1948 1963
1949 1964 return literal(select_html+select_script)
1950 1965
1951 1966
1952 1967 def get_visual_attr(tmpl_context_var, attr_name):
1953 1968 """
1954 1969 A safe way to get a variable from visual variable of template context
1955 1970
1956 1971 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1957 1972 :param attr_name: name of the attribute we fetch from the c.visual
1958 1973 """
1959 1974 visual = getattr(tmpl_context_var, 'visual', None)
1960 1975 if not visual:
1961 1976 return
1962 1977 else:
1963 1978 return getattr(visual, attr_name, None)
1964 1979
1965 1980
1966 1981 def get_last_path_part(file_node):
1967 1982 if not file_node.path:
1968 1983 return u''
1969 1984
1970 1985 path = safe_unicode(file_node.path.split('/')[-1])
1971 1986 return u'../' + path
1972 1987
1973 1988
1974 1989 def route_url(*args, **kwargs):
1975 1990 """
1976 1991 Wrapper around pyramids `route_url` (fully qualified url) function.
1977 1992 It is used to generate URLs from within pylons views or templates.
1978 1993 This will be removed when pyramid migration if finished.
1979 1994 """
1980 1995 req = get_current_request()
1981 1996 return req.route_url(*args, **kwargs)
1982 1997
1983 1998
1984 1999 def route_path(*args, **kwargs):
1985 2000 """
1986 2001 Wrapper around pyramids `route_path` function. It is used to generate
1987 2002 URLs from within pylons views or templates. This will be removed when
1988 2003 pyramid migration if finished.
1989 2004 """
1990 2005 req = get_current_request()
1991 2006 return req.route_path(*args, **kwargs)
1992 2007
1993 2008
1994 2009 def route_path_or_none(*args, **kwargs):
1995 2010 try:
1996 2011 return route_path(*args, **kwargs)
1997 2012 except KeyError:
1998 2013 return None
1999 2014
2000 2015
2001 2016 def static_url(*args, **kwds):
2002 2017 """
2003 2018 Wrapper around pyramids `route_path` function. It is used to generate
2004 2019 URLs from within pylons views or templates. This will be removed when
2005 2020 pyramid migration if finished.
2006 2021 """
2007 2022 req = get_current_request()
2008 2023 return req.static_url(*args, **kwds)
2009 2024
2010 2025
2011 2026 def resource_path(*args, **kwds):
2012 2027 """
2013 2028 Wrapper around pyramids `route_path` function. It is used to generate
2014 2029 URLs from within pylons views or templates. This will be removed when
2015 2030 pyramid migration if finished.
2016 2031 """
2017 2032 req = get_current_request()
2018 2033 return req.resource_path(*args, **kwds)
2019 2034
2020 2035
2021 2036 def api_call_example(method, args):
2022 2037 """
2023 2038 Generates an API call example via CURL
2024 2039 """
2025 2040 args_json = json.dumps(OrderedDict([
2026 2041 ('id', 1),
2027 2042 ('auth_token', 'SECRET'),
2028 2043 ('method', method),
2029 2044 ('args', args)
2030 2045 ]))
2031 2046 return literal(
2032 2047 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2033 2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2034 2049 "and needs to be of `api calls` role."
2035 2050 .format(
2036 2051 api_url=route_url('apiv2'),
2037 2052 token_url=route_url('my_account_auth_tokens'),
2038 2053 data=args_json))
2039 2054
2040 2055
2041 2056 def notification_description(notification, request):
2042 2057 """
2043 2058 Generate notification human readable description based on notification type
2044 2059 """
2045 2060 from rhodecode.model.notification import NotificationModel
2046 2061 return NotificationModel().make_description(
2047 2062 notification, translate=request.translate)
@@ -1,1025 +1,1020 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import os
23 23 import re
24 24 import shutil
25 25 import time
26 26 import traceback
27 27 import datetime
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode import events
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.auth import HasUserGroupPermissionAny
35 35 from rhodecode.lib.caching_query import FromCache
36 36 from rhodecode.lib.exceptions import AttachedForksError
37 37 from rhodecode.lib.hooks_base import log_delete_repository
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
40 40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 41 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
42 42 from rhodecode.lib.vcs.backends import get_backend
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (_hash_key,
45 45 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
46 46 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
47 47 RepoGroup, RepositoryField)
48 48
49 49 from rhodecode.model.settings import VcsSettingsModel
50 50
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoModel(BaseModel):
56 56
57 57 cls = Repository
58 58
59 59 def _get_user_group(self, users_group):
60 60 return self._get_instance(UserGroup, users_group,
61 61 callback=UserGroup.get_by_group_name)
62 62
63 63 def _get_repo_group(self, repo_group):
64 64 return self._get_instance(RepoGroup, repo_group,
65 65 callback=RepoGroup.get_by_group_name)
66 66
67 67 def _create_default_perms(self, repository, private):
68 68 # create default permission
69 69 default = 'repository.read'
70 70 def_user = User.get_default_user()
71 71 for p in def_user.user_perms:
72 72 if p.permission.permission_name.startswith('repository.'):
73 73 default = p.permission.permission_name
74 74 break
75 75
76 76 default_perm = 'repository.none' if private else default
77 77
78 78 repo_to_perm = UserRepoToPerm()
79 79 repo_to_perm.permission = Permission.get_by_key(default_perm)
80 80
81 81 repo_to_perm.repository = repository
82 82 repo_to_perm.user_id = def_user.user_id
83 83
84 84 return repo_to_perm
85 85
86 86 @LazyProperty
87 87 def repos_path(self):
88 88 """
89 89 Gets the repositories root path from database
90 90 """
91 91 settings_model = VcsSettingsModel(sa=self.sa)
92 92 return settings_model.get_repos_location()
93 93
94 94 def get(self, repo_id, cache=False):
95 95 repo = self.sa.query(Repository) \
96 96 .filter(Repository.repo_id == repo_id)
97 97
98 98 if cache:
99 99 repo = repo.options(
100 100 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
101 101 return repo.scalar()
102 102
103 103 def get_repo(self, repository):
104 104 return self._get_repo(repository)
105 105
106 106 def get_by_repo_name(self, repo_name, cache=False):
107 107 repo = self.sa.query(Repository) \
108 108 .filter(Repository.repo_name == repo_name)
109 109
110 110 if cache:
111 111 name_key = _hash_key(repo_name)
112 112 repo = repo.options(
113 113 FromCache("sql_cache_short", "get_repo_%s" % name_key))
114 114 return repo.scalar()
115 115
116 116 def _extract_id_from_repo_name(self, repo_name):
117 117 if repo_name.startswith('/'):
118 118 repo_name = repo_name.lstrip('/')
119 119 by_id_match = re.match(r'^_(\d{1,})', repo_name)
120 120 if by_id_match:
121 121 return by_id_match.groups()[0]
122 122
123 123 def get_repo_by_id(self, repo_name):
124 124 """
125 125 Extracts repo_name by id from special urls.
126 126 Example url is _11/repo_name
127 127
128 128 :param repo_name:
129 129 :return: repo object if matched else None
130 130 """
131 131
132 132 try:
133 133 _repo_id = self._extract_id_from_repo_name(repo_name)
134 134 if _repo_id:
135 135 return self.get(_repo_id)
136 136 except Exception:
137 137 log.exception('Failed to extract repo_name from URL')
138 138
139 139 return None
140 140
141 141 def get_repos_for_root(self, root, traverse=False):
142 142 if traverse:
143 143 like_expression = u'{}%'.format(safe_unicode(root))
144 144 repos = Repository.query().filter(
145 145 Repository.repo_name.like(like_expression)).all()
146 146 else:
147 147 if root and not isinstance(root, RepoGroup):
148 148 raise ValueError(
149 149 'Root must be an instance '
150 150 'of RepoGroup, got:{} instead'.format(type(root)))
151 151 repos = Repository.query().filter(Repository.group == root).all()
152 152 return repos
153 153
154 154 def get_url(self, repo, request=None, permalink=False):
155 155 if not request:
156 156 request = get_current_request()
157 157
158 158 if not request:
159 159 return
160 160
161 161 if permalink:
162 162 return request.route_url(
163 163 'repo_summary', repo_name=safe_str(repo.repo_id))
164 164 else:
165 165 return request.route_url(
166 166 'repo_summary', repo_name=safe_str(repo.repo_name))
167 167
168 168 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
169 169 if not request:
170 170 request = get_current_request()
171 171
172 172 if not request:
173 173 return
174 174
175 175 if permalink:
176 176 return request.route_url(
177 177 'repo_commit', repo_name=safe_str(repo.repo_id),
178 178 commit_id=commit_id)
179 179
180 180 else:
181 181 return request.route_url(
182 182 'repo_commit', repo_name=safe_str(repo.repo_name),
183 183 commit_id=commit_id)
184 184
185 185 @classmethod
186 186 def update_repoinfo(cls, repositories=None):
187 187 if not repositories:
188 188 repositories = Repository.getAll()
189 189 for repo in repositories:
190 190 repo.update_commit_cache()
191 191
192 192 def get_repos_as_dict(self, repo_list=None, admin=False,
193 193 super_user_actions=False):
194 194 _render = get_current_request().get_partial_renderer(
195 195 'data_table/_dt_elements.mako')
196 196 c = _render.get_call_context()
197 197
198 198 def quick_menu(repo_name):
199 199 return _render('quick_menu', repo_name)
200 200
201 201 def repo_lnk(name, rtype, rstate, private, fork_of):
202 202 return _render('repo_name', name, rtype, rstate, private, fork_of,
203 203 short_name=not admin, admin=False)
204 204
205 205 def last_change(last_change):
206 206 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
207 207 last_change = last_change + datetime.timedelta(seconds=
208 208 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
209 209 return _render("last_change", last_change)
210 210
211 211 def rss_lnk(repo_name):
212 212 return _render("rss", repo_name)
213 213
214 214 def atom_lnk(repo_name):
215 215 return _render("atom", repo_name)
216 216
217 217 def last_rev(repo_name, cs_cache):
218 218 return _render('revision', repo_name, cs_cache.get('revision'),
219 219 cs_cache.get('raw_id'), cs_cache.get('author'),
220 220 cs_cache.get('message'))
221 221
222 222 def desc(desc):
223 if c.visual.stylify_metatags:
224 desc = h.urlify_text(h.escaped_stylize(desc))
225 else:
226 desc = h.urlify_text(h.html_escape(desc))
227
228 return _render('repo_desc', desc)
223 return _render('repo_desc', desc, c.visual.stylify_metatags)
229 224
230 225 def state(repo_state):
231 226 return _render("repo_state", repo_state)
232 227
233 228 def repo_actions(repo_name):
234 229 return _render('repo_actions', repo_name, super_user_actions)
235 230
236 231 def user_profile(username):
237 232 return _render('user_profile', username)
238 233
239 234 repos_data = []
240 235 for repo in repo_list:
241 236 cs_cache = repo.changeset_cache
242 237 row = {
243 238 "menu": quick_menu(repo.repo_name),
244 239
245 240 "name": repo_lnk(repo.repo_name, repo.repo_type,
246 241 repo.repo_state, repo.private, repo.fork),
247 242 "name_raw": repo.repo_name.lower(),
248 243
249 244 "last_change": last_change(repo.last_db_change),
250 245 "last_change_raw": datetime_to_time(repo.last_db_change),
251 246
252 247 "last_changeset": last_rev(repo.repo_name, cs_cache),
253 248 "last_changeset_raw": cs_cache.get('revision'),
254 249
255 250 "desc": desc(repo.description_safe),
256 251 "owner": user_profile(repo.user.username),
257 252
258 253 "state": state(repo.repo_state),
259 254 "rss": rss_lnk(repo.repo_name),
260 255
261 256 "atom": atom_lnk(repo.repo_name),
262 257 }
263 258 if admin:
264 259 row.update({
265 260 "action": repo_actions(repo.repo_name),
266 261 })
267 262 repos_data.append(row)
268 263
269 264 return repos_data
270 265
271 266 def _get_defaults(self, repo_name):
272 267 """
273 268 Gets information about repository, and returns a dict for
274 269 usage in forms
275 270
276 271 :param repo_name:
277 272 """
278 273
279 274 repo_info = Repository.get_by_repo_name(repo_name)
280 275
281 276 if repo_info is None:
282 277 return None
283 278
284 279 defaults = repo_info.get_dict()
285 280 defaults['repo_name'] = repo_info.just_name
286 281
287 282 groups = repo_info.groups_with_parents
288 283 parent_group = groups[-1] if groups else None
289 284
290 285 # we use -1 as this is how in HTML, we mark an empty group
291 286 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
292 287
293 288 keys_to_process = (
294 289 {'k': 'repo_type', 'strip': False},
295 290 {'k': 'repo_enable_downloads', 'strip': True},
296 291 {'k': 'repo_description', 'strip': True},
297 292 {'k': 'repo_enable_locking', 'strip': True},
298 293 {'k': 'repo_landing_rev', 'strip': True},
299 294 {'k': 'clone_uri', 'strip': False},
300 295 {'k': 'repo_private', 'strip': True},
301 296 {'k': 'repo_enable_statistics', 'strip': True}
302 297 )
303 298
304 299 for item in keys_to_process:
305 300 attr = item['k']
306 301 if item['strip']:
307 302 attr = remove_prefix(item['k'], 'repo_')
308 303
309 304 val = defaults[attr]
310 305 if item['k'] == 'repo_landing_rev':
311 306 val = ':'.join(defaults[attr])
312 307 defaults[item['k']] = val
313 308 if item['k'] == 'clone_uri':
314 309 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
315 310
316 311 # fill owner
317 312 if repo_info.user:
318 313 defaults.update({'user': repo_info.user.username})
319 314 else:
320 315 replacement_user = User.get_first_super_admin().username
321 316 defaults.update({'user': replacement_user})
322 317
323 318 return defaults
324 319
325 320 def update(self, repo, **kwargs):
326 321 try:
327 322 cur_repo = self._get_repo(repo)
328 323 source_repo_name = cur_repo.repo_name
329 324 if 'user' in kwargs:
330 325 cur_repo.user = User.get_by_username(kwargs['user'])
331 326
332 327 if 'repo_group' in kwargs:
333 328 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
334 329 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
335 330
336 331 update_keys = [
337 332 (1, 'repo_description'),
338 333 (1, 'repo_landing_rev'),
339 334 (1, 'repo_private'),
340 335 (1, 'repo_enable_downloads'),
341 336 (1, 'repo_enable_locking'),
342 337 (1, 'repo_enable_statistics'),
343 338 (0, 'clone_uri'),
344 339 (0, 'fork_id')
345 340 ]
346 341 for strip, k in update_keys:
347 342 if k in kwargs:
348 343 val = kwargs[k]
349 344 if strip:
350 345 k = remove_prefix(k, 'repo_')
351 346
352 347 setattr(cur_repo, k, val)
353 348
354 349 new_name = cur_repo.get_new_name(kwargs['repo_name'])
355 350 cur_repo.repo_name = new_name
356 351
357 352 # if private flag is set, reset default permission to NONE
358 353 if kwargs.get('repo_private'):
359 354 EMPTY_PERM = 'repository.none'
360 355 RepoModel().grant_user_permission(
361 356 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
362 357 )
363 358
364 359 # handle extra fields
365 360 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
366 361 kwargs):
367 362 k = RepositoryField.un_prefix_key(field)
368 363 ex_field = RepositoryField.get_by_key_name(
369 364 key=k, repo=cur_repo)
370 365 if ex_field:
371 366 ex_field.field_value = kwargs[field]
372 367 self.sa.add(ex_field)
373 368 cur_repo.updated_on = datetime.datetime.now()
374 369 self.sa.add(cur_repo)
375 370
376 371 if source_repo_name != new_name:
377 372 # rename repository
378 373 self._rename_filesystem_repo(
379 374 old=source_repo_name, new=new_name)
380 375
381 376 return cur_repo
382 377 except Exception:
383 378 log.error(traceback.format_exc())
384 379 raise
385 380
386 381 def _create_repo(self, repo_name, repo_type, description, owner,
387 382 private=False, clone_uri=None, repo_group=None,
388 383 landing_rev='rev:tip', fork_of=None,
389 384 copy_fork_permissions=False, enable_statistics=False,
390 385 enable_locking=False, enable_downloads=False,
391 386 copy_group_permissions=False,
392 387 state=Repository.STATE_PENDING):
393 388 """
394 389 Create repository inside database with PENDING state, this should be
395 390 only executed by create() repo. With exception of importing existing
396 391 repos
397 392 """
398 393 from rhodecode.model.scm import ScmModel
399 394
400 395 owner = self._get_user(owner)
401 396 fork_of = self._get_repo(fork_of)
402 397 repo_group = self._get_repo_group(safe_int(repo_group))
403 398
404 399 try:
405 400 repo_name = safe_unicode(repo_name)
406 401 description = safe_unicode(description)
407 402 # repo name is just a name of repository
408 403 # while repo_name_full is a full qualified name that is combined
409 404 # with name and path of group
410 405 repo_name_full = repo_name
411 406 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
412 407
413 408 new_repo = Repository()
414 409 new_repo.repo_state = state
415 410 new_repo.enable_statistics = False
416 411 new_repo.repo_name = repo_name_full
417 412 new_repo.repo_type = repo_type
418 413 new_repo.user = owner
419 414 new_repo.group = repo_group
420 415 new_repo.description = description or repo_name
421 416 new_repo.private = private
422 417 new_repo.clone_uri = clone_uri
423 418 new_repo.landing_rev = landing_rev
424 419
425 420 new_repo.enable_statistics = enable_statistics
426 421 new_repo.enable_locking = enable_locking
427 422 new_repo.enable_downloads = enable_downloads
428 423
429 424 if repo_group:
430 425 new_repo.enable_locking = repo_group.enable_locking
431 426
432 427 if fork_of:
433 428 parent_repo = fork_of
434 429 new_repo.fork = parent_repo
435 430
436 431 events.trigger(events.RepoPreCreateEvent(new_repo))
437 432
438 433 self.sa.add(new_repo)
439 434
440 435 EMPTY_PERM = 'repository.none'
441 436 if fork_of and copy_fork_permissions:
442 437 repo = fork_of
443 438 user_perms = UserRepoToPerm.query() \
444 439 .filter(UserRepoToPerm.repository == repo).all()
445 440 group_perms = UserGroupRepoToPerm.query() \
446 441 .filter(UserGroupRepoToPerm.repository == repo).all()
447 442
448 443 for perm in user_perms:
449 444 UserRepoToPerm.create(
450 445 perm.user, new_repo, perm.permission)
451 446
452 447 for perm in group_perms:
453 448 UserGroupRepoToPerm.create(
454 449 perm.users_group, new_repo, perm.permission)
455 450 # in case we copy permissions and also set this repo to private
456 451 # override the default user permission to make it a private
457 452 # repo
458 453 if private:
459 454 RepoModel(self.sa).grant_user_permission(
460 455 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
461 456
462 457 elif repo_group and copy_group_permissions:
463 458 user_perms = UserRepoGroupToPerm.query() \
464 459 .filter(UserRepoGroupToPerm.group == repo_group).all()
465 460
466 461 group_perms = UserGroupRepoGroupToPerm.query() \
467 462 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
468 463
469 464 for perm in user_perms:
470 465 perm_name = perm.permission.permission_name.replace(
471 466 'group.', 'repository.')
472 467 perm_obj = Permission.get_by_key(perm_name)
473 468 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
474 469
475 470 for perm in group_perms:
476 471 perm_name = perm.permission.permission_name.replace(
477 472 'group.', 'repository.')
478 473 perm_obj = Permission.get_by_key(perm_name)
479 474 UserGroupRepoToPerm.create(
480 475 perm.users_group, new_repo, perm_obj)
481 476
482 477 if private:
483 478 RepoModel(self.sa).grant_user_permission(
484 479 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
485 480
486 481 else:
487 482 perm_obj = self._create_default_perms(new_repo, private)
488 483 self.sa.add(perm_obj)
489 484
490 485 # now automatically start following this repository as owner
491 486 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
492 487 owner.user_id)
493 488
494 489 # we need to flush here, in order to check if database won't
495 490 # throw any exceptions, create filesystem dirs at the very end
496 491 self.sa.flush()
497 492 events.trigger(events.RepoCreateEvent(new_repo))
498 493 return new_repo
499 494
500 495 except Exception:
501 496 log.error(traceback.format_exc())
502 497 raise
503 498
504 499 def create(self, form_data, cur_user):
505 500 """
506 501 Create repository using celery tasks
507 502
508 503 :param form_data:
509 504 :param cur_user:
510 505 """
511 506 from rhodecode.lib.celerylib import tasks, run_task
512 507 return run_task(tasks.create_repo, form_data, cur_user)
513 508
514 509 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
515 510 perm_deletions=None, check_perms=True,
516 511 cur_user=None):
517 512 if not perm_additions:
518 513 perm_additions = []
519 514 if not perm_updates:
520 515 perm_updates = []
521 516 if not perm_deletions:
522 517 perm_deletions = []
523 518
524 519 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
525 520
526 521 changes = {
527 522 'added': [],
528 523 'updated': [],
529 524 'deleted': []
530 525 }
531 526 # update permissions
532 527 for member_id, perm, member_type in perm_updates:
533 528 member_id = int(member_id)
534 529 if member_type == 'user':
535 530 member_name = User.get(member_id).username
536 531 # this updates also current one if found
537 532 self.grant_user_permission(
538 533 repo=repo, user=member_id, perm=perm)
539 534 else: # set for user group
540 535 # check if we have permissions to alter this usergroup
541 536 member_name = UserGroup.get(member_id).users_group_name
542 537 if not check_perms or HasUserGroupPermissionAny(
543 538 *req_perms)(member_name, user=cur_user):
544 539 self.grant_user_group_permission(
545 540 repo=repo, group_name=member_id, perm=perm)
546 541
547 542 changes['updated'].append({'type': member_type, 'id': member_id,
548 543 'name': member_name, 'new_perm': perm})
549 544
550 545 # set new permissions
551 546 for member_id, perm, member_type in perm_additions:
552 547 member_id = int(member_id)
553 548 if member_type == 'user':
554 549 member_name = User.get(member_id).username
555 550 self.grant_user_permission(
556 551 repo=repo, user=member_id, perm=perm)
557 552 else: # set for user group
558 553 # check if we have permissions to alter this usergroup
559 554 member_name = UserGroup.get(member_id).users_group_name
560 555 if not check_perms or HasUserGroupPermissionAny(
561 556 *req_perms)(member_name, user=cur_user):
562 557 self.grant_user_group_permission(
563 558 repo=repo, group_name=member_id, perm=perm)
564 559 changes['added'].append({'type': member_type, 'id': member_id,
565 560 'name': member_name, 'new_perm': perm})
566 561 # delete permissions
567 562 for member_id, perm, member_type in perm_deletions:
568 563 member_id = int(member_id)
569 564 if member_type == 'user':
570 565 member_name = User.get(member_id).username
571 566 self.revoke_user_permission(repo=repo, user=member_id)
572 567 else: # set for user group
573 568 # check if we have permissions to alter this usergroup
574 569 member_name = UserGroup.get(member_id).users_group_name
575 570 if not check_perms or HasUserGroupPermissionAny(
576 571 *req_perms)(member_name, user=cur_user):
577 572 self.revoke_user_group_permission(
578 573 repo=repo, group_name=member_id)
579 574
580 575 changes['deleted'].append({'type': member_type, 'id': member_id,
581 576 'name': member_name, 'new_perm': perm})
582 577 return changes
583 578
584 579 def create_fork(self, form_data, cur_user):
585 580 """
586 581 Simple wrapper into executing celery task for fork creation
587 582
588 583 :param form_data:
589 584 :param cur_user:
590 585 """
591 586 from rhodecode.lib.celerylib import tasks, run_task
592 587 return run_task(tasks.create_repo_fork, form_data, cur_user)
593 588
594 589 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
595 590 """
596 591 Delete given repository, forks parameter defines what do do with
597 592 attached forks. Throws AttachedForksError if deleted repo has attached
598 593 forks
599 594
600 595 :param repo:
601 596 :param forks: str 'delete' or 'detach'
602 597 :param fs_remove: remove(archive) repo from filesystem
603 598 """
604 599 if not cur_user:
605 600 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
606 601 repo = self._get_repo(repo)
607 602 if repo:
608 603 if forks == 'detach':
609 604 for r in repo.forks:
610 605 r.fork = None
611 606 self.sa.add(r)
612 607 elif forks == 'delete':
613 608 for r in repo.forks:
614 609 self.delete(r, forks='delete')
615 610 elif [f for f in repo.forks]:
616 611 raise AttachedForksError()
617 612
618 613 old_repo_dict = repo.get_dict()
619 614 events.trigger(events.RepoPreDeleteEvent(repo))
620 615 try:
621 616 self.sa.delete(repo)
622 617 if fs_remove:
623 618 self._delete_filesystem_repo(repo)
624 619 else:
625 620 log.debug('skipping removal from filesystem')
626 621 old_repo_dict.update({
627 622 'deleted_by': cur_user,
628 623 'deleted_on': time.time(),
629 624 })
630 625 log_delete_repository(**old_repo_dict)
631 626 events.trigger(events.RepoDeleteEvent(repo))
632 627 except Exception:
633 628 log.error(traceback.format_exc())
634 629 raise
635 630
636 631 def grant_user_permission(self, repo, user, perm):
637 632 """
638 633 Grant permission for user on given repository, or update existing one
639 634 if found
640 635
641 636 :param repo: Instance of Repository, repository_id, or repository name
642 637 :param user: Instance of User, user_id or username
643 638 :param perm: Instance of Permission, or permission_name
644 639 """
645 640 user = self._get_user(user)
646 641 repo = self._get_repo(repo)
647 642 permission = self._get_perm(perm)
648 643
649 644 # check if we have that permission already
650 645 obj = self.sa.query(UserRepoToPerm) \
651 646 .filter(UserRepoToPerm.user == user) \
652 647 .filter(UserRepoToPerm.repository == repo) \
653 648 .scalar()
654 649 if obj is None:
655 650 # create new !
656 651 obj = UserRepoToPerm()
657 652 obj.repository = repo
658 653 obj.user = user
659 654 obj.permission = permission
660 655 self.sa.add(obj)
661 656 log.debug('Granted perm %s to %s on %s', perm, user, repo)
662 657 action_logger_generic(
663 658 'granted permission: {} to user: {} on repo: {}'.format(
664 659 perm, user, repo), namespace='security.repo')
665 660 return obj
666 661
667 662 def revoke_user_permission(self, repo, user):
668 663 """
669 664 Revoke permission for user on given repository
670 665
671 666 :param repo: Instance of Repository, repository_id, or repository name
672 667 :param user: Instance of User, user_id or username
673 668 """
674 669
675 670 user = self._get_user(user)
676 671 repo = self._get_repo(repo)
677 672
678 673 obj = self.sa.query(UserRepoToPerm) \
679 674 .filter(UserRepoToPerm.repository == repo) \
680 675 .filter(UserRepoToPerm.user == user) \
681 676 .scalar()
682 677 if obj:
683 678 self.sa.delete(obj)
684 679 log.debug('Revoked perm on %s on %s', repo, user)
685 680 action_logger_generic(
686 681 'revoked permission from user: {} on repo: {}'.format(
687 682 user, repo), namespace='security.repo')
688 683
689 684 def grant_user_group_permission(self, repo, group_name, perm):
690 685 """
691 686 Grant permission for user group on given repository, or update
692 687 existing one if found
693 688
694 689 :param repo: Instance of Repository, repository_id, or repository name
695 690 :param group_name: Instance of UserGroup, users_group_id,
696 691 or user group name
697 692 :param perm: Instance of Permission, or permission_name
698 693 """
699 694 repo = self._get_repo(repo)
700 695 group_name = self._get_user_group(group_name)
701 696 permission = self._get_perm(perm)
702 697
703 698 # check if we have that permission already
704 699 obj = self.sa.query(UserGroupRepoToPerm) \
705 700 .filter(UserGroupRepoToPerm.users_group == group_name) \
706 701 .filter(UserGroupRepoToPerm.repository == repo) \
707 702 .scalar()
708 703
709 704 if obj is None:
710 705 # create new
711 706 obj = UserGroupRepoToPerm()
712 707
713 708 obj.repository = repo
714 709 obj.users_group = group_name
715 710 obj.permission = permission
716 711 self.sa.add(obj)
717 712 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
718 713 action_logger_generic(
719 714 'granted permission: {} to usergroup: {} on repo: {}'.format(
720 715 perm, group_name, repo), namespace='security.repo')
721 716
722 717 return obj
723 718
724 719 def revoke_user_group_permission(self, repo, group_name):
725 720 """
726 721 Revoke permission for user group on given repository
727 722
728 723 :param repo: Instance of Repository, repository_id, or repository name
729 724 :param group_name: Instance of UserGroup, users_group_id,
730 725 or user group name
731 726 """
732 727 repo = self._get_repo(repo)
733 728 group_name = self._get_user_group(group_name)
734 729
735 730 obj = self.sa.query(UserGroupRepoToPerm) \
736 731 .filter(UserGroupRepoToPerm.repository == repo) \
737 732 .filter(UserGroupRepoToPerm.users_group == group_name) \
738 733 .scalar()
739 734 if obj:
740 735 self.sa.delete(obj)
741 736 log.debug('Revoked perm to %s on %s', repo, group_name)
742 737 action_logger_generic(
743 738 'revoked permission from usergroup: {} on repo: {}'.format(
744 739 group_name, repo), namespace='security.repo')
745 740
746 741 def delete_stats(self, repo_name):
747 742 """
748 743 removes stats for given repo
749 744
750 745 :param repo_name:
751 746 """
752 747 repo = self._get_repo(repo_name)
753 748 try:
754 749 obj = self.sa.query(Statistics) \
755 750 .filter(Statistics.repository == repo).scalar()
756 751 if obj:
757 752 self.sa.delete(obj)
758 753 except Exception:
759 754 log.error(traceback.format_exc())
760 755 raise
761 756
762 757 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
763 758 field_type='str', field_desc=''):
764 759
765 760 repo = self._get_repo(repo_name)
766 761
767 762 new_field = RepositoryField()
768 763 new_field.repository = repo
769 764 new_field.field_key = field_key
770 765 new_field.field_type = field_type # python type
771 766 new_field.field_value = field_value
772 767 new_field.field_desc = field_desc
773 768 new_field.field_label = field_label
774 769 self.sa.add(new_field)
775 770 return new_field
776 771
777 772 def delete_repo_field(self, repo_name, field_key):
778 773 repo = self._get_repo(repo_name)
779 774 field = RepositoryField.get_by_key_name(field_key, repo)
780 775 if field:
781 776 self.sa.delete(field)
782 777
783 778 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
784 779 clone_uri=None, repo_store_location=None,
785 780 use_global_config=False):
786 781 """
787 782 makes repository on filesystem. It's group aware means it'll create
788 783 a repository within a group, and alter the paths accordingly of
789 784 group location
790 785
791 786 :param repo_name:
792 787 :param alias:
793 788 :param parent:
794 789 :param clone_uri:
795 790 :param repo_store_location:
796 791 """
797 792 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
798 793 from rhodecode.model.scm import ScmModel
799 794
800 795 if Repository.NAME_SEP in repo_name:
801 796 raise ValueError(
802 797 'repo_name must not contain groups got `%s`' % repo_name)
803 798
804 799 if isinstance(repo_group, RepoGroup):
805 800 new_parent_path = os.sep.join(repo_group.full_path_splitted)
806 801 else:
807 802 new_parent_path = repo_group or ''
808 803
809 804 if repo_store_location:
810 805 _paths = [repo_store_location]
811 806 else:
812 807 _paths = [self.repos_path, new_parent_path, repo_name]
813 808 # we need to make it str for mercurial
814 809 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
815 810
816 811 # check if this path is not a repository
817 812 if is_valid_repo(repo_path, self.repos_path):
818 813 raise Exception('This path %s is a valid repository' % repo_path)
819 814
820 815 # check if this path is a group
821 816 if is_valid_repo_group(repo_path, self.repos_path):
822 817 raise Exception('This path %s is a valid group' % repo_path)
823 818
824 819 log.info('creating repo %s in %s from url: `%s`',
825 820 repo_name, safe_unicode(repo_path),
826 821 obfuscate_url_pw(clone_uri))
827 822
828 823 backend = get_backend(repo_type)
829 824
830 825 config_repo = None if use_global_config else repo_name
831 826 if config_repo and new_parent_path:
832 827 config_repo = Repository.NAME_SEP.join(
833 828 (new_parent_path, config_repo))
834 829 config = make_db_config(clear_session=False, repo=config_repo)
835 830 config.set('extensions', 'largefiles', '')
836 831
837 832 # patch and reset hooks section of UI config to not run any
838 833 # hooks on creating remote repo
839 834 config.clear_section('hooks')
840 835
841 836 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
842 837 if repo_type == 'git':
843 838 repo = backend(
844 839 repo_path, config=config, create=True, src_url=clone_uri,
845 840 bare=True)
846 841 else:
847 842 repo = backend(
848 843 repo_path, config=config, create=True, src_url=clone_uri)
849 844
850 845 ScmModel().install_hooks(repo, repo_type=repo_type)
851 846
852 847 log.debug('Created repo %s with %s backend',
853 848 safe_unicode(repo_name), safe_unicode(repo_type))
854 849 return repo
855 850
856 851 def _rename_filesystem_repo(self, old, new):
857 852 """
858 853 renames repository on filesystem
859 854
860 855 :param old: old name
861 856 :param new: new name
862 857 """
863 858 log.info('renaming repo from %s to %s', old, new)
864 859
865 860 old_path = os.path.join(self.repos_path, old)
866 861 new_path = os.path.join(self.repos_path, new)
867 862 if os.path.isdir(new_path):
868 863 raise Exception(
869 864 'Was trying to rename to already existing dir %s' % new_path
870 865 )
871 866 shutil.move(old_path, new_path)
872 867
873 868 def _delete_filesystem_repo(self, repo):
874 869 """
875 870 removes repo from filesystem, the removal is acctually made by
876 871 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
877 872 repository is no longer valid for rhodecode, can be undeleted later on
878 873 by reverting the renames on this repository
879 874
880 875 :param repo: repo object
881 876 """
882 877 rm_path = os.path.join(self.repos_path, repo.repo_name)
883 878 repo_group = repo.group
884 879 log.info("Removing repository %s", rm_path)
885 880 # disable hg/git internal that it doesn't get detected as repo
886 881 alias = repo.repo_type
887 882
888 883 config = make_db_config(clear_session=False)
889 884 config.set('extensions', 'largefiles', '')
890 885 bare = getattr(repo.scm_instance(config=config), 'bare', False)
891 886
892 887 # skip this for bare git repos
893 888 if not bare:
894 889 # disable VCS repo
895 890 vcs_path = os.path.join(rm_path, '.%s' % alias)
896 891 if os.path.exists(vcs_path):
897 892 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
898 893
899 894 _now = datetime.datetime.now()
900 895 _ms = str(_now.microsecond).rjust(6, '0')
901 896 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
902 897 repo.just_name)
903 898 if repo_group:
904 899 # if repository is in group, prefix the removal path with the group
905 900 args = repo_group.full_path_splitted + [_d]
906 901 _d = os.path.join(*args)
907 902
908 903 if os.path.isdir(rm_path):
909 904 shutil.move(rm_path, os.path.join(self.repos_path, _d))
910 905
911 906
912 907 class ReadmeFinder:
913 908 """
914 909 Utility which knows how to find a readme for a specific commit.
915 910
916 911 The main idea is that this is a configurable algorithm. When creating an
917 912 instance you can define parameters, currently only the `default_renderer`.
918 913 Based on this configuration the method :meth:`search` behaves slightly
919 914 different.
920 915 """
921 916
922 917 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
923 918 path_re = re.compile(r'^docs?', re.IGNORECASE)
924 919
925 920 default_priorities = {
926 921 None: 0,
927 922 '.text': 2,
928 923 '.txt': 3,
929 924 '.rst': 1,
930 925 '.rest': 2,
931 926 '.md': 1,
932 927 '.mkdn': 2,
933 928 '.mdown': 3,
934 929 '.markdown': 4,
935 930 }
936 931
937 932 path_priority = {
938 933 'doc': 0,
939 934 'docs': 1,
940 935 }
941 936
942 937 FALLBACK_PRIORITY = 99
943 938
944 939 RENDERER_TO_EXTENSION = {
945 940 'rst': ['.rst', '.rest'],
946 941 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
947 942 }
948 943
949 944 def __init__(self, default_renderer=None):
950 945 self._default_renderer = default_renderer
951 946 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
952 947 default_renderer, [])
953 948
954 949 def search(self, commit, path='/'):
955 950 """
956 951 Find a readme in the given `commit`.
957 952 """
958 953 nodes = commit.get_nodes(path)
959 954 matches = self._match_readmes(nodes)
960 955 matches = self._sort_according_to_priority(matches)
961 956 if matches:
962 957 return matches[0].node
963 958
964 959 paths = self._match_paths(nodes)
965 960 paths = self._sort_paths_according_to_priority(paths)
966 961 for path in paths:
967 962 match = self.search(commit, path=path)
968 963 if match:
969 964 return match
970 965
971 966 return None
972 967
973 968 def _match_readmes(self, nodes):
974 969 for node in nodes:
975 970 if not node.is_file():
976 971 continue
977 972 path = node.path.rsplit('/', 1)[-1]
978 973 match = self.readme_re.match(path)
979 974 if match:
980 975 extension = match.group(1)
981 976 yield ReadmeMatch(node, match, self._priority(extension))
982 977
983 978 def _match_paths(self, nodes):
984 979 for node in nodes:
985 980 if not node.is_dir():
986 981 continue
987 982 match = self.path_re.match(node.path)
988 983 if match:
989 984 yield node.path
990 985
991 986 def _priority(self, extension):
992 987 renderer_priority = (
993 988 0 if extension in self._renderer_extensions else 1)
994 989 extension_priority = self.default_priorities.get(
995 990 extension, self.FALLBACK_PRIORITY)
996 991 return (renderer_priority, extension_priority)
997 992
998 993 def _sort_according_to_priority(self, matches):
999 994
1000 995 def priority_and_path(match):
1001 996 return (match.priority, match.path)
1002 997
1003 998 return sorted(matches, key=priority_and_path)
1004 999
1005 1000 def _sort_paths_according_to_priority(self, paths):
1006 1001
1007 1002 def priority_and_path(path):
1008 1003 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1009 1004
1010 1005 return sorted(paths, key=priority_and_path)
1011 1006
1012 1007
1013 1008 class ReadmeMatch:
1014 1009
1015 1010 def __init__(self, node, match, priority):
1016 1011 self.node = node
1017 1012 self._match = match
1018 1013 self.priority = priority
1019 1014
1020 1015 @property
1021 1016 def path(self):
1022 1017 return self.node.path
1023 1018
1024 1019 def __repr__(self):
1025 1020 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,744 +1,738 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 repo group model for RhodeCode
24 24 """
25 25
26 26 import os
27 27 import datetime
28 28 import itertools
29 29 import logging
30 30 import shutil
31 31 import traceback
32 32 import string
33 33
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import (_hash_key,
39 39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 40 UserGroup, Repository)
41 41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.lib.utils2 import action_logger_generic, datetime_to_time
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoGroupModel(BaseModel):
49 49
50 50 cls = RepoGroup
51 51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
52 52 PERSONAL_GROUP_PATTERN = '${username}' # default
53 53
54 54 def _get_user_group(self, users_group):
55 55 return self._get_instance(UserGroup, users_group,
56 56 callback=UserGroup.get_by_group_name)
57 57
58 58 def _get_repo_group(self, repo_group):
59 59 return self._get_instance(RepoGroup, repo_group,
60 60 callback=RepoGroup.get_by_group_name)
61 61
62 62 @LazyProperty
63 63 def repos_path(self):
64 64 """
65 65 Gets the repositories root path from database
66 66 """
67 67
68 68 settings_model = VcsSettingsModel(sa=self.sa)
69 69 return settings_model.get_repos_location()
70 70
71 71 def get_by_group_name(self, repo_group_name, cache=None):
72 72 repo = self.sa.query(RepoGroup) \
73 73 .filter(RepoGroup.group_name == repo_group_name)
74 74
75 75 if cache:
76 76 name_key = _hash_key(repo_group_name)
77 77 repo = repo.options(
78 78 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
79 79 return repo.scalar()
80 80
81 81 def get_default_create_personal_repo_group(self):
82 82 value = SettingsModel().get_setting_by_name(
83 83 'create_personal_repo_group')
84 84 return value.app_settings_value if value else None or False
85 85
86 86 def get_personal_group_name_pattern(self):
87 87 value = SettingsModel().get_setting_by_name(
88 88 'personal_repo_group_pattern')
89 89 val = value.app_settings_value if value else None
90 90 group_template = val or self.PERSONAL_GROUP_PATTERN
91 91
92 92 group_template = group_template.lstrip('/')
93 93 return group_template
94 94
95 95 def get_personal_group_name(self, user):
96 96 template = self.get_personal_group_name_pattern()
97 97 return string.Template(template).safe_substitute(
98 98 username=user.username,
99 99 user_id=user.user_id,
100 100 )
101 101
102 102 def create_personal_repo_group(self, user, commit_early=True):
103 103 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
104 104 personal_repo_group_name = self.get_personal_group_name(user)
105 105
106 106 # create a new one
107 107 RepoGroupModel().create(
108 108 group_name=personal_repo_group_name,
109 109 group_description=desc,
110 110 owner=user.username,
111 111 personal=True,
112 112 commit_early=commit_early)
113 113
114 114 def _create_default_perms(self, new_group):
115 115 # create default permission
116 116 default_perm = 'group.read'
117 117 def_user = User.get_default_user()
118 118 for p in def_user.user_perms:
119 119 if p.permission.permission_name.startswith('group.'):
120 120 default_perm = p.permission.permission_name
121 121 break
122 122
123 123 repo_group_to_perm = UserRepoGroupToPerm()
124 124 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
125 125
126 126 repo_group_to_perm.group = new_group
127 127 repo_group_to_perm.user_id = def_user.user_id
128 128 return repo_group_to_perm
129 129
130 130 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
131 131 get_object=False):
132 132 """
133 133 Get's the group name and a parent group name from given group name.
134 134 If repo_in_path is set to truth, we asume the full path also includes
135 135 repo name, in such case we clean the last element.
136 136
137 137 :param group_name_full:
138 138 """
139 139 split_paths = 1
140 140 if repo_in_path:
141 141 split_paths = 2
142 142 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
143 143
144 144 if repo_in_path and len(_parts) > 1:
145 145 # such case last element is the repo_name
146 146 _parts.pop(-1)
147 147 group_name_cleaned = _parts[-1] # just the group name
148 148 parent_repo_group_name = None
149 149
150 150 if len(_parts) > 1:
151 151 parent_repo_group_name = _parts[0]
152 152
153 153 parent_group = None
154 154 if parent_repo_group_name:
155 155 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
156 156
157 157 if get_object:
158 158 return group_name_cleaned, parent_repo_group_name, parent_group
159 159
160 160 return group_name_cleaned, parent_repo_group_name
161 161
162 162 def check_exist_filesystem(self, group_name, exc_on_failure=True):
163 163 create_path = os.path.join(self.repos_path, group_name)
164 164 log.debug('creating new group in %s', create_path)
165 165
166 166 if os.path.isdir(create_path):
167 167 if exc_on_failure:
168 168 abs_create_path = os.path.abspath(create_path)
169 169 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
170 170 return False
171 171 return True
172 172
173 173 def _create_group(self, group_name):
174 174 """
175 175 makes repository group on filesystem
176 176
177 177 :param repo_name:
178 178 :param parent_id:
179 179 """
180 180
181 181 self.check_exist_filesystem(group_name)
182 182 create_path = os.path.join(self.repos_path, group_name)
183 183 log.debug('creating new group in %s', create_path)
184 184 os.makedirs(create_path, mode=0755)
185 185 log.debug('created group in %s', create_path)
186 186
187 187 def _rename_group(self, old, new):
188 188 """
189 189 Renames a group on filesystem
190 190
191 191 :param group_name:
192 192 """
193 193
194 194 if old == new:
195 195 log.debug('skipping group rename')
196 196 return
197 197
198 198 log.debug('renaming repository group from %s to %s', old, new)
199 199
200 200 old_path = os.path.join(self.repos_path, old)
201 201 new_path = os.path.join(self.repos_path, new)
202 202
203 203 log.debug('renaming repos paths from %s to %s', old_path, new_path)
204 204
205 205 if os.path.isdir(new_path):
206 206 raise Exception('Was trying to rename to already '
207 207 'existing dir %s' % new_path)
208 208 shutil.move(old_path, new_path)
209 209
210 210 def _delete_filesystem_group(self, group, force_delete=False):
211 211 """
212 212 Deletes a group from a filesystem
213 213
214 214 :param group: instance of group from database
215 215 :param force_delete: use shutil rmtree to remove all objects
216 216 """
217 217 paths = group.full_path.split(RepoGroup.url_sep())
218 218 paths = os.sep.join(paths)
219 219
220 220 rm_path = os.path.join(self.repos_path, paths)
221 221 log.info("Removing group %s", rm_path)
222 222 # delete only if that path really exists
223 223 if os.path.isdir(rm_path):
224 224 if force_delete:
225 225 shutil.rmtree(rm_path)
226 226 else:
227 227 # archive that group`
228 228 _now = datetime.datetime.now()
229 229 _ms = str(_now.microsecond).rjust(6, '0')
230 230 _d = 'rm__%s_GROUP_%s' % (
231 231 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
232 232 shutil.move(rm_path, os.path.join(self.repos_path, _d))
233 233
234 234 def create(self, group_name, group_description, owner, just_db=False,
235 235 copy_permissions=False, personal=None, commit_early=True):
236 236
237 237 (group_name_cleaned,
238 238 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
239 239
240 240 parent_group = None
241 241 if parent_group_name:
242 242 parent_group = self._get_repo_group(parent_group_name)
243 243 if not parent_group:
244 244 # we tried to create a nested group, but the parent is not
245 245 # existing
246 246 raise ValueError(
247 247 'Parent group `%s` given in `%s` group name '
248 248 'is not yet existing.' % (parent_group_name, group_name))
249 249
250 250 # because we are doing a cleanup, we need to check if such directory
251 251 # already exists. If we don't do that we can accidentally delete
252 252 # existing directory via cleanup that can cause data issues, since
253 253 # delete does a folder rename to special syntax later cleanup
254 254 # functions can delete this
255 255 cleanup_group = self.check_exist_filesystem(group_name,
256 256 exc_on_failure=False)
257 257 try:
258 258 user = self._get_user(owner)
259 259 new_repo_group = RepoGroup()
260 260 new_repo_group.user = user
261 261 new_repo_group.group_description = group_description or group_name
262 262 new_repo_group.parent_group = parent_group
263 263 new_repo_group.group_name = group_name
264 264 new_repo_group.personal = personal
265 265
266 266 self.sa.add(new_repo_group)
267 267
268 268 # create an ADMIN permission for owner except if we're super admin,
269 269 # later owner should go into the owner field of groups
270 270 if not user.is_admin:
271 271 self.grant_user_permission(repo_group=new_repo_group,
272 272 user=owner, perm='group.admin')
273 273
274 274 if parent_group and copy_permissions:
275 275 # copy permissions from parent
276 276 user_perms = UserRepoGroupToPerm.query() \
277 277 .filter(UserRepoGroupToPerm.group == parent_group).all()
278 278
279 279 group_perms = UserGroupRepoGroupToPerm.query() \
280 280 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
281 281
282 282 for perm in user_perms:
283 283 # don't copy over the permission for user who is creating
284 284 # this group, if he is not super admin he get's admin
285 285 # permission set above
286 286 if perm.user != user or user.is_admin:
287 287 UserRepoGroupToPerm.create(
288 288 perm.user, new_repo_group, perm.permission)
289 289
290 290 for perm in group_perms:
291 291 UserGroupRepoGroupToPerm.create(
292 292 perm.users_group, new_repo_group, perm.permission)
293 293 else:
294 294 perm_obj = self._create_default_perms(new_repo_group)
295 295 self.sa.add(perm_obj)
296 296
297 297 # now commit the changes, earlier so we are sure everything is in
298 298 # the database.
299 299 if commit_early:
300 300 self.sa.commit()
301 301 if not just_db:
302 302 self._create_group(new_repo_group.group_name)
303 303
304 304 # trigger the post hook
305 305 from rhodecode.lib.hooks_base import log_create_repository_group
306 306 repo_group = RepoGroup.get_by_group_name(group_name)
307 307 log_create_repository_group(
308 308 created_by=user.username, **repo_group.get_dict())
309 309
310 310 # Trigger create event.
311 311 events.trigger(events.RepoGroupCreateEvent(repo_group))
312 312
313 313 return new_repo_group
314 314 except Exception:
315 315 self.sa.rollback()
316 316 log.exception('Exception occurred when creating repository group, '
317 317 'doing cleanup...')
318 318 # rollback things manually !
319 319 repo_group = RepoGroup.get_by_group_name(group_name)
320 320 if repo_group:
321 321 RepoGroup.delete(repo_group.group_id)
322 322 self.sa.commit()
323 323 if cleanup_group:
324 324 RepoGroupModel()._delete_filesystem_group(repo_group)
325 325 raise
326 326
327 327 def update_permissions(
328 328 self, repo_group, perm_additions=None, perm_updates=None,
329 329 perm_deletions=None, recursive=None, check_perms=True,
330 330 cur_user=None):
331 331 from rhodecode.model.repo import RepoModel
332 332 from rhodecode.lib.auth import HasUserGroupPermissionAny
333 333
334 334 if not perm_additions:
335 335 perm_additions = []
336 336 if not perm_updates:
337 337 perm_updates = []
338 338 if not perm_deletions:
339 339 perm_deletions = []
340 340
341 341 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
342 342
343 343 changes = {
344 344 'added': [],
345 345 'updated': [],
346 346 'deleted': []
347 347 }
348 348
349 349 def _set_perm_user(obj, user, perm):
350 350 if isinstance(obj, RepoGroup):
351 351 self.grant_user_permission(
352 352 repo_group=obj, user=user, perm=perm)
353 353 elif isinstance(obj, Repository):
354 354 # private repos will not allow to change the default
355 355 # permissions using recursive mode
356 356 if obj.private and user == User.DEFAULT_USER:
357 357 return
358 358
359 359 # we set group permission but we have to switch to repo
360 360 # permission
361 361 perm = perm.replace('group.', 'repository.')
362 362 RepoModel().grant_user_permission(
363 363 repo=obj, user=user, perm=perm)
364 364
365 365 def _set_perm_group(obj, users_group, perm):
366 366 if isinstance(obj, RepoGroup):
367 367 self.grant_user_group_permission(
368 368 repo_group=obj, group_name=users_group, perm=perm)
369 369 elif isinstance(obj, Repository):
370 370 # we set group permission but we have to switch to repo
371 371 # permission
372 372 perm = perm.replace('group.', 'repository.')
373 373 RepoModel().grant_user_group_permission(
374 374 repo=obj, group_name=users_group, perm=perm)
375 375
376 376 def _revoke_perm_user(obj, user):
377 377 if isinstance(obj, RepoGroup):
378 378 self.revoke_user_permission(repo_group=obj, user=user)
379 379 elif isinstance(obj, Repository):
380 380 RepoModel().revoke_user_permission(repo=obj, user=user)
381 381
382 382 def _revoke_perm_group(obj, user_group):
383 383 if isinstance(obj, RepoGroup):
384 384 self.revoke_user_group_permission(
385 385 repo_group=obj, group_name=user_group)
386 386 elif isinstance(obj, Repository):
387 387 RepoModel().revoke_user_group_permission(
388 388 repo=obj, group_name=user_group)
389 389
390 390 # start updates
391 391 log.debug('Now updating permissions for %s in recursive mode:%s',
392 392 repo_group, recursive)
393 393
394 394 # initialize check function, we'll call that multiple times
395 395 has_group_perm = HasUserGroupPermissionAny(*req_perms)
396 396
397 397 for obj in repo_group.recursive_groups_and_repos():
398 398 # iterated obj is an instance of a repos group or repository in
399 399 # that group, recursive option can be: none, repos, groups, all
400 400 if recursive == 'all':
401 401 obj = obj
402 402 elif recursive == 'repos':
403 403 # skip groups, other than this one
404 404 if isinstance(obj, RepoGroup) and not obj == repo_group:
405 405 continue
406 406 elif recursive == 'groups':
407 407 # skip repos
408 408 if isinstance(obj, Repository):
409 409 continue
410 410 else: # recursive == 'none':
411 411 # DEFAULT option - don't apply to iterated objects
412 412 # also we do a break at the end of this loop. if we are not
413 413 # in recursive mode
414 414 obj = repo_group
415 415
416 416 change_obj = obj.get_api_data()
417 417
418 418 # update permissions
419 419 for member_id, perm, member_type in perm_updates:
420 420 member_id = int(member_id)
421 421 if member_type == 'user':
422 422 member_name = User.get(member_id).username
423 423 # this updates also current one if found
424 424 _set_perm_user(obj, user=member_id, perm=perm)
425 425 else: # set for user group
426 426 member_name = UserGroup.get(member_id).users_group_name
427 427 if not check_perms or has_group_perm(member_name,
428 428 user=cur_user):
429 429 _set_perm_group(obj, users_group=member_id, perm=perm)
430 430
431 431 changes['updated'].append(
432 432 {'change_obj': change_obj, 'type': member_type,
433 433 'id': member_id, 'name': member_name, 'new_perm': perm})
434 434
435 435 # set new permissions
436 436 for member_id, perm, member_type in perm_additions:
437 437 member_id = int(member_id)
438 438 if member_type == 'user':
439 439 member_name = User.get(member_id).username
440 440 _set_perm_user(obj, user=member_id, perm=perm)
441 441 else: # set for user group
442 442 # check if we have permissions to alter this usergroup
443 443 member_name = UserGroup.get(member_id).users_group_name
444 444 if not check_perms or has_group_perm(member_name,
445 445 user=cur_user):
446 446 _set_perm_group(obj, users_group=member_id, perm=perm)
447 447
448 448 changes['added'].append(
449 449 {'change_obj': change_obj, 'type': member_type,
450 450 'id': member_id, 'name': member_name, 'new_perm': perm})
451 451
452 452 # delete permissions
453 453 for member_id, perm, member_type in perm_deletions:
454 454 member_id = int(member_id)
455 455 if member_type == 'user':
456 456 member_name = User.get(member_id).username
457 457 _revoke_perm_user(obj, user=member_id)
458 458 else: # set for user group
459 459 # check if we have permissions to alter this usergroup
460 460 member_name = UserGroup.get(member_id).users_group_name
461 461 if not check_perms or has_group_perm(member_name,
462 462 user=cur_user):
463 463 _revoke_perm_group(obj, user_group=member_id)
464 464
465 465 changes['deleted'].append(
466 466 {'change_obj': change_obj, 'type': member_type,
467 467 'id': member_id, 'name': member_name, 'new_perm': perm})
468 468
469 469 # if it's not recursive call for all,repos,groups
470 470 # break the loop and don't proceed with other changes
471 471 if recursive not in ['all', 'repos', 'groups']:
472 472 break
473 473
474 474 return changes
475 475
476 476 def update(self, repo_group, form_data):
477 477 try:
478 478 repo_group = self._get_repo_group(repo_group)
479 479 old_path = repo_group.full_path
480 480
481 481 # change properties
482 482 if 'group_description' in form_data:
483 483 repo_group.group_description = form_data['group_description']
484 484
485 485 if 'enable_locking' in form_data:
486 486 repo_group.enable_locking = form_data['enable_locking']
487 487
488 488 if 'group_parent_id' in form_data:
489 489 parent_group = (
490 490 self._get_repo_group(form_data['group_parent_id']))
491 491 repo_group.group_parent_id = (
492 492 parent_group.group_id if parent_group else None)
493 493 repo_group.parent_group = parent_group
494 494
495 495 # mikhail: to update the full_path, we have to explicitly
496 496 # update group_name
497 497 group_name = form_data.get('group_name', repo_group.name)
498 498 repo_group.group_name = repo_group.get_new_name(group_name)
499 499
500 500 new_path = repo_group.full_path
501 501
502 502 if 'user' in form_data:
503 503 repo_group.user = User.get_by_username(form_data['user'])
504 504 repo_group.updated_on = datetime.datetime.now()
505 505 self.sa.add(repo_group)
506 506
507 507 # iterate over all members of this groups and do fixes
508 508 # set locking if given
509 509 # if obj is a repoGroup also fix the name of the group according
510 510 # to the parent
511 511 # if obj is a Repo fix it's name
512 512 # this can be potentially heavy operation
513 513 for obj in repo_group.recursive_groups_and_repos():
514 514 # set the value from it's parent
515 515 obj.enable_locking = repo_group.enable_locking
516 516 if isinstance(obj, RepoGroup):
517 517 new_name = obj.get_new_name(obj.name)
518 518 log.debug('Fixing group %s to new name %s',
519 519 obj.group_name, new_name)
520 520 obj.group_name = new_name
521 521 obj.updated_on = datetime.datetime.now()
522 522 elif isinstance(obj, Repository):
523 523 # we need to get all repositories from this new group and
524 524 # rename them accordingly to new group path
525 525 new_name = obj.get_new_name(obj.just_name)
526 526 log.debug('Fixing repo %s to new name %s',
527 527 obj.repo_name, new_name)
528 528 obj.repo_name = new_name
529 529 obj.updated_on = datetime.datetime.now()
530 530 self.sa.add(obj)
531 531
532 532 self._rename_group(old_path, new_path)
533 533
534 534 # Trigger update event.
535 535 events.trigger(events.RepoGroupUpdateEvent(repo_group))
536 536
537 537 return repo_group
538 538 except Exception:
539 539 log.error(traceback.format_exc())
540 540 raise
541 541
542 542 def delete(self, repo_group, force_delete=False, fs_remove=True):
543 543 repo_group = self._get_repo_group(repo_group)
544 544 if not repo_group:
545 545 return False
546 546 try:
547 547 self.sa.delete(repo_group)
548 548 if fs_remove:
549 549 self._delete_filesystem_group(repo_group, force_delete)
550 550 else:
551 551 log.debug('skipping removal from filesystem')
552 552
553 553 # Trigger delete event.
554 554 events.trigger(events.RepoGroupDeleteEvent(repo_group))
555 555 return True
556 556
557 557 except Exception:
558 558 log.error('Error removing repo_group %s', repo_group)
559 559 raise
560 560
561 561 def grant_user_permission(self, repo_group, user, perm):
562 562 """
563 563 Grant permission for user on given repository group, or update
564 564 existing one if found
565 565
566 566 :param repo_group: Instance of RepoGroup, repositories_group_id,
567 567 or repositories_group name
568 568 :param user: Instance of User, user_id or username
569 569 :param perm: Instance of Permission, or permission_name
570 570 """
571 571
572 572 repo_group = self._get_repo_group(repo_group)
573 573 user = self._get_user(user)
574 574 permission = self._get_perm(perm)
575 575
576 576 # check if we have that permission already
577 577 obj = self.sa.query(UserRepoGroupToPerm)\
578 578 .filter(UserRepoGroupToPerm.user == user)\
579 579 .filter(UserRepoGroupToPerm.group == repo_group)\
580 580 .scalar()
581 581 if obj is None:
582 582 # create new !
583 583 obj = UserRepoGroupToPerm()
584 584 obj.group = repo_group
585 585 obj.user = user
586 586 obj.permission = permission
587 587 self.sa.add(obj)
588 588 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
589 589 action_logger_generic(
590 590 'granted permission: {} to user: {} on repogroup: {}'.format(
591 591 perm, user, repo_group), namespace='security.repogroup')
592 592 return obj
593 593
594 594 def revoke_user_permission(self, repo_group, user):
595 595 """
596 596 Revoke permission for user on given repository group
597 597
598 598 :param repo_group: Instance of RepoGroup, repositories_group_id,
599 599 or repositories_group name
600 600 :param user: Instance of User, user_id or username
601 601 """
602 602
603 603 repo_group = self._get_repo_group(repo_group)
604 604 user = self._get_user(user)
605 605
606 606 obj = self.sa.query(UserRepoGroupToPerm)\
607 607 .filter(UserRepoGroupToPerm.user == user)\
608 608 .filter(UserRepoGroupToPerm.group == repo_group)\
609 609 .scalar()
610 610 if obj:
611 611 self.sa.delete(obj)
612 612 log.debug('Revoked perm on %s on %s', repo_group, user)
613 613 action_logger_generic(
614 614 'revoked permission from user: {} on repogroup: {}'.format(
615 615 user, repo_group), namespace='security.repogroup')
616 616
617 617 def grant_user_group_permission(self, repo_group, group_name, perm):
618 618 """
619 619 Grant permission for user group on given repository group, or update
620 620 existing one if found
621 621
622 622 :param repo_group: Instance of RepoGroup, repositories_group_id,
623 623 or repositories_group name
624 624 :param group_name: Instance of UserGroup, users_group_id,
625 625 or user group name
626 626 :param perm: Instance of Permission, or permission_name
627 627 """
628 628 repo_group = self._get_repo_group(repo_group)
629 629 group_name = self._get_user_group(group_name)
630 630 permission = self._get_perm(perm)
631 631
632 632 # check if we have that permission already
633 633 obj = self.sa.query(UserGroupRepoGroupToPerm)\
634 634 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
635 635 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
636 636 .scalar()
637 637
638 638 if obj is None:
639 639 # create new
640 640 obj = UserGroupRepoGroupToPerm()
641 641
642 642 obj.group = repo_group
643 643 obj.users_group = group_name
644 644 obj.permission = permission
645 645 self.sa.add(obj)
646 646 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
647 647 action_logger_generic(
648 648 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
649 649 perm, group_name, repo_group), namespace='security.repogroup')
650 650 return obj
651 651
652 652 def revoke_user_group_permission(self, repo_group, group_name):
653 653 """
654 654 Revoke permission for user group on given repository group
655 655
656 656 :param repo_group: Instance of RepoGroup, repositories_group_id,
657 657 or repositories_group name
658 658 :param group_name: Instance of UserGroup, users_group_id,
659 659 or user group name
660 660 """
661 661 repo_group = self._get_repo_group(repo_group)
662 662 group_name = self._get_user_group(group_name)
663 663
664 664 obj = self.sa.query(UserGroupRepoGroupToPerm)\
665 665 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
666 666 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
667 667 .scalar()
668 668 if obj:
669 669 self.sa.delete(obj)
670 670 log.debug('Revoked perm to %s on %s', repo_group, group_name)
671 671 action_logger_generic(
672 672 'revoked permission from usergroup: {} on repogroup: {}'.format(
673 673 group_name, repo_group), namespace='security.repogroup')
674 674
675 675 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
676 676 super_user_actions=False):
677 677
678 678 from pyramid.threadlocal import get_current_request
679 679 _render = get_current_request().get_partial_renderer(
680 680 'data_table/_dt_elements.mako')
681 681 c = _render.get_call_context()
682 682 h = _render.get_helpers()
683 683
684 684 def quick_menu(repo_group_name):
685 685 return _render('quick_repo_group_menu', repo_group_name)
686 686
687 687 def repo_group_lnk(repo_group_name):
688 688 return _render('repo_group_name', repo_group_name)
689 689
690 690 def last_change(last_change):
691 691 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
692 692 last_change = last_change + datetime.timedelta(seconds=
693 693 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
694 694 return _render("last_change", last_change)
695 695
696 696 def desc(desc, personal):
697 prefix = h.escaped_stylize(u'[personal] ') if personal else ''
698
699 if c.visual.stylify_metatags:
700 desc = h.urlify_text(prefix + h.escaped_stylize(desc))
701 else:
702 desc = h.urlify_text(prefix + h.html_escape(desc))
703
704 return _render('repo_group_desc', desc)
697 return _render(
698 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
705 699
706 700 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
707 701 return _render(
708 702 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
709 703
710 704 def repo_group_name(repo_group_name, children_groups):
711 705 return _render("repo_group_name", repo_group_name, children_groups)
712 706
713 707 def user_profile(username):
714 708 return _render('user_profile', username)
715 709
716 710 repo_group_data = []
717 711 for group in repo_group_list:
718 712
719 713 row = {
720 714 "menu": quick_menu(group.group_name),
721 715 "name": repo_group_lnk(group.group_name),
722 716 "name_raw": group.group_name,
723 717 "last_change": last_change(group.last_db_change),
724 718 "last_change_raw": datetime_to_time(group.last_db_change),
725 719 "desc": desc(group.description_safe, group.personal),
726 720 "top_level_repos": 0,
727 721 "owner": user_profile(group.user.username)
728 722 }
729 723 if admin:
730 724 repo_count = group.repositories.count()
731 725 children_groups = map(
732 726 h.safe_unicode,
733 727 itertools.chain((g.name for g in group.parents),
734 728 (x.name for x in [group])))
735 729 row.update({
736 730 "action": repo_group_actions(
737 731 group.group_id, group.group_name, repo_count),
738 732 "top_level_repos": repo_count,
739 733 "name": repo_group_name(group.group_name, children_groups),
740 734
741 735 })
742 736 repo_group_data.append(row)
743 737
744 738 return repo_group_data
@@ -1,109 +1,126 b''
1 1 // tags.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // TAGS
6 6 .tag,
7 7 .tagtag {
8 8 display: inline-block;
9 9 min-height: 0;
10 10 margin: 0 auto;
11 11 padding: .25em;
12 12 text-align: center;
13 13 font-size: (-1 + @basefontsize); //fit in tables
14 14 line-height: .9em;
15 15 border: none;
16 16 .border-radius(@border-radius);
17 17 font-family: @text-regular;
18 18 background-image: none;
19 19 color: @grey4;
20 20 .border ( @border-thickness-tags, @grey4 );
21 21 white-space: nowrap;
22 22 a {
23 23 color: inherit;
24 24 text-decoration: underline;
25 25
26 26 i,
27 27 [class^="icon-"]:before,
28 28 [class*=" icon-"]:before {
29 29 text-decoration: none;
30 30 }
31 31 }
32 32 }
33 33
34 34 .tag0 { .border ( @border-thickness-tags, @grey4 ); color:@grey4; }
35 35 .tag1 { .border ( @border-thickness-tags, @color1 ); color:@color1; }
36 36 .tag2 { .border ( @border-thickness-tags, @color2 ); color:@color2; }
37 37 .tag3 { .border ( @border-thickness-tags, @color3 ); color:@color3; }
38 38 .tag4 { .border ( @border-thickness-tags, @color4 ); color:@color4; }
39 39 .tag5 { .border ( @border-thickness-tags, @color5 ); color:@color5; }
40 40 .tag6 { .border ( @border-thickness-tags, @color6 ); color:@color6; }
41 41 .tag7 { .border ( @border-thickness-tags, @color7 ); color:@color7; }
42 42 .tag8 { .border ( @border-thickness-tags, @color8 ); color:@color8; }
43 43
44 44 .metatag-list {
45 45 margin: 0;
46 46 padding: 0;
47 47
48 48 li {
49 49 margin: 0 0 @padding;
50 50 line-height: 1em;
51 51 list-style-type: none;
52 52
53 53 &:before { content: none; }
54 54 }
55 55 }
56 56
57 57 .branchtag, .booktag {
58 58 &:extend(.tag);
59 59
60 60
61 61 a {
62 62 color:inherit;
63 63 }
64 64 }
65 65
66 66 .metatag {
67 67 &:extend(.tag);
68 68 a {
69 69 color:inherit;
70 70 text-decoration: underline;
71 71 }
72 72 }
73 73
74 [tag="featured"] { &:extend(.tag1); }
75 [tag="stale"] { &:extend(.tag2); }
76 [tag="dead"] { &:extend(.tag3); }
77 [tag="lang"] { &:extend(.tag4); }
78 [tag="license"] { &:extend(.tag5); }
79 [tag="requires"] { &:extend(.tag6); }
80 [tag="recommends"] { &:extend(.tag7); }
74 [tag="generic"] { &:extend(.tag0); }
75 [tag="label"] { &:extend(.tag0); }
76
77 [tag="state featured"] { &:extend(.tag1); }
78 [tag="state dev"] { &:extend(.tag1); }
79 [tag="ref base"] { &:extend(.tag1); }
80
81 [tag="state stable"] { &:extend(.tag2); }
82 [tag="state stale"] { &:extend(.tag2); }
83
84 [tag="ref requires"] { &:extend(.tag3); }
85
86 [tag="state dead"] { &:extend(.tag4); }
87
88 [tag="ref conflicts"] { &:extend(.tag4); }
89
90 [tag="license"] { &:extend(.tag6); }
91
92 [tag="lang"] { &:extend(.tag7); }
93 [tag="language"] { &:extend(.tag7); }
94 [tag="ref recommends"] { &:extend(.tag7); }
95
81 96 [tag="see"] { &:extend(.tag8); }
97 [tag="url"] { &:extend(.tag8); }
98
82 99
83 100 .perm_overriden {
84 101 text-decoration: line-through;
85 102 opacity: 0.6;
86 103 }
87 104
88 105 .perm_tag {
89 106 &:extend(.tag);
90 107
91 108 &.read {
92 109 &:extend(.tag1);
93 110 }
94 111
95 112 &.write {
96 113 &:extend(.tag4);
97 114 }
98 115 &.admin {
99 116 &:extend(.tag5);
100 117 }
101 118 }
102 119
103 120 .phase-draft {
104 121 color: @color3
105 122 }
106 123
107 124 .phase-secret {
108 125 color:@grey3
109 126 }
@@ -1,100 +1,106 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Add repository group')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
13 13 &raquo;
14 14 ${h.link_to(_('Repository groups'),h.url('repo_groups'))}
15 15 &raquo;
16 16 ${_('Add Repository Group')}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_nav()">
20 20 ${self.menu_items(active='admin')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <div class="box">
25 25 <!-- box / title -->
26 26 <div class="title">
27 27 ${self.breadcrumbs()}
28 28 </div>
29 29 <!-- end box / title -->
30 30 ${h.secure_form(h.url('repo_groups'), method='post')}
31 31 <div class="form">
32 32 <!-- fields -->
33 33 <div class="fields">
34 34 <div class="field">
35 35 <div class="label">
36 36 <label for="group_name">${_('Group Name')}:</label>
37 37 </div>
38 38 <div class="input">
39 39 ${h.text('group_name', class_="medium")}
40 40 </div>
41 41 </div>
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="group_description">${_('Description')}:</label>
46 46 </div>
47 47 <div class="textarea editor">
48 48 ${h.textarea('group_description',cols=23,rows=5,class_="medium")}
49 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
50 <span class="help-block">${_('Plain text format with support of {metatags}').format(metatags=metatags_url)|n}</span>
51 <span id="meta-tags-desc" style="display: none">
52 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
53 ${dt.metatags_help()}
54 </span>
49 55 </div>
50 56 </div>
51 57
52 58 <div class="field">
53 59 <div class="label">
54 60 <label for="group_parent_id">${_('Group Parent')}:</label>
55 61 </div>
56 62 <div class="select">
57 63 ${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
58 64 </div>
59 65 </div>
60 66
61 67 <div id="copy_perms" class="field">
62 68 <div class="label label-checkbox">
63 69 <label for="group_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
64 70 </div>
65 71 <div class="checkboxes">
66 72 ${h.checkbox('group_copy_permissions', value="True", checked="checked")}
67 73 <span class="help-block">${_('Copy permission settings from parent repository group.')}</span>
68 74 </div>
69 75 </div>
70 76
71 77 <div class="buttons">
72 78 ${h.submit('save',_('Save'),class_="btn")}
73 79 </div>
74 80 </div>
75 81 </div>
76 82 ${h.end_form()}
77 83 </div>
78 84 <script>
79 85 $(document).ready(function(){
80 86 var setCopyPermsOption = function(group_val){
81 87 if(group_val != "-1"){
82 88 $('#copy_perms').show()
83 89 }
84 90 else{
85 91 $('#copy_perms').hide();
86 92 }
87 93 }
88 94 $("#group_parent_id").select2({
89 95 'containerCssClass': "drop-menu",
90 96 'dropdownCssClass': "drop-menu-dropdown",
91 97 'dropdownAutoWidth': true
92 98 });
93 99 setCopyPermsOption($('#group_parent_id').val())
94 100 $("#group_parent_id").on("change", function(e) {
95 101 setCopyPermsOption(e.val)
96 102 })
97 103 $('#group_name').focus();
98 104 })
99 105 </script>
100 106 </%def>
@@ -1,84 +1,90 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <div class="panel panel-default">
5 5 <div class="panel-heading">
6 6 <h3 class="panel-title">${_('Settings for Repository Group: %s') % c.repo_group.name}</h3>
7 7 </div>
8 8 <div class="panel-body">
9 9 ${h.secure_form(h.url('update_repo_group',group_name=c.repo_group.group_name),method='put')}
10 10 <div class="form">
11 11 <!-- fields -->
12 12 <div class="fields">
13 13 <div class="field">
14 14 <div class="label">
15 15 <label for="group_name">${_('Group Name')}:</label>
16 16 </div>
17 17 <div class="input">
18 18 ${h.text('group_name',class_='medium')}
19 19 </div>
20 20 </div>
21 21
22 22 <div class="field badged-field">
23 23 <div class="label">
24 24 <label for="user">${_('Owner')}:</label>
25 25 </div>
26 26 <div class="input">
27 27 <div class="badge-input-container">
28 28 <div class="user-badge">
29 29 ${base.gravatar_with_user(c.repo_group.user.email, show_disabled=not c.repo_group.user.active)}
30 30 </div>
31 31 <div class="badge-input-wrap">
32 32 ${h.text('user', class_="medium", autocomplete="off")}
33 33 </div>
34 34 </div>
35 35 <form:error name="user"/>
36 36 <p class="help-block">${_('Change owner of this repository group.')}</p>
37 37 </div>
38 38 </div>
39 39
40 40 <div class="field">
41 41 <div class="label label-textarea">
42 42 <label for="group_description">${_('Description')}:</label>
43 43 </div>
44 44 <div class="textarea text-area editor">
45 45 ${h.textarea('group_description',cols=23,rows=5,class_="medium")}
46 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
47 <span class="help-block">${_('Plain text format with support of {metatags}').format(metatags=metatags_url)|n}</span>
48 <span id="meta-tags-desc" style="display: none">
49 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
50 ${dt.metatags_help()}
51 </span>
46 52 </div>
47 53 </div>
48 54
49 55 <div class="field">
50 56 <div class="label">
51 57 <label for="group_parent_id">${_('Group parent')}:</label>
52 58 </div>
53 59 <div class="select">
54 60 ${h.select('group_parent_id','',c.repo_groups,class_="medium")}
55 61 </div>
56 62 </div>
57 63 <div class="field">
58 64 <div class="label label-checkbox">
59 65 <label for="enable_locking">${_('Enable Repository Locking')}:</label>
60 66 </div>
61 67 <div class="checkboxes">
62 68 ${h.checkbox('enable_locking',value="True")}
63 69 <span class="help-block">${_('Repository locking will be enabled on all subgroups and repositories inside this repository group. Pulling from a repository locks it, and it is unlocked by pushing back by the same user.')}</span>
64 70 </div>
65 71 </div>
66 72 <div class="buttons">
67 73 ${h.submit('save',_('Save'),class_="btn")}
68 74 ${h.reset('reset',_('Reset'),class_="btn")}
69 75 </div>
70 76 </div>
71 77 </div>
72 78 ${h.end_form()}
73 79 </div>
74 80 </div>
75 81 <script>
76 82 $(document).ready(function(){
77 83 $("#group_parent_id").select2({
78 84 'containerCssClass': "drop-menu",
79 85 'dropdownCssClass': "drop-menu-dropdown",
80 86 'dropdownAutoWidth': true
81 87 });
82 88 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
83 89 })
84 90 </script>
@@ -1,159 +1,164 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ${h.secure_form(h.route_path('repo_create'), method='POST', request=request)}
4 4 <div class="form">
5 5 <!-- fields -->
6 6 <div class="fields">
7 7 <div class="field">
8 8 <div class="label">
9 9 <label for="repo_name">${_('Name')}:</label>
10 10 </div>
11 11 <div class="input">
12 12 ${h.text('repo_name', class_="medium")}
13 13 <div class="info-block">
14 14 <a id="remote_clone_toggle" href="#"><i class="icon-download-alt"></i> ${_('Import Existing Repository ?')}</a>
15 15 </div>
16 16 %if not c.rhodecode_user.is_admin:
17 17 ${h.hidden('user_created',True)}
18 18 %endif
19 19 </div>
20 20 </div>
21 21 <div id="remote_clone" class="field" style="display: none;">
22 22 <div class="label">
23 23 <label for="clone_uri">${_('Clone from')}:</label>
24 24 </div>
25 25 <div class="input">
26 26 ${h.text('clone_uri', class_="medium")}
27 27 <span class="help-block">
28 28 <pre>
29 29 - The repository must be accessible over http:// or https://
30 30 - For Git projects it's recommended appending .git to the end of clone url.
31 31 - Make sure to select proper repository type from the below selector before importing it.
32 32 - If your HTTP[S] repository is not publicly accessible,
33 33 add authentication information to the URL: https://username:password@server.company.com/repo-name.
34 34 - The Git LFS/Mercurial Largefiles objects will not be imported.
35 35 - For very large repositories, it's recommended to manually copy them into the
36 36 RhodeCode <a href="${h.url('admin_settings_vcs', anchor='vcs-storage-options')}">storage location</a> and run <a href="${h.url('admin_settings_mapping')}">Remap and Rescan</a>.
37 37 </pre>
38 38 </span>
39 39 </div>
40 40 </div>
41 41 <div class="field">
42 42 <div class="label">
43 43 <label for="repo_description">${_('Description')}:</label>
44 44 </div>
45 45 <div class="textarea editor">
46 46 ${h.textarea('repo_description')}
47 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
47 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
48 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
49 <span id="meta-tags-desc" style="display: none">
50 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
51 ${dt.metatags_help()}
52 </span>
48 53 </div>
49 54 </div>
50 55 <div class="field">
51 56 <div class="label">
52 57 <label for="repo_group">${_('Repository Group')}:</label>
53 58 </div>
54 59 <div class="select">
55 60 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
56 61 % if c.personal_repo_group:
57 62 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
58 63 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
59 64 </a>
60 65 % endif
61 66 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
62 67 </div>
63 68 </div>
64 69 <div id="copy_perms" class="field">
65 70 <div class="label label-checkbox">
66 71 <label for="repo_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
67 72 </div>
68 73 <div class="checkboxes">
69 74 ${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
70 75 <span class="help-block">${_('Copy permission set from the parent repository group.')}</span>
71 76 </div>
72 77 </div>
73 78 <div class="field">
74 79 <div class="label">
75 80 <label for="repo_type">${_('Type')}:</label>
76 81 </div>
77 82 <div class="select">
78 83 ${h.select('repo_type','hg',c.backends)}
79 84 <span class="help-block">${_('Set the type of repository to create.')}</span>
80 85 </div>
81 86 </div>
82 87 <div class="field">
83 88 <div class="label">
84 89 <label for="repo_landing_rev">${_('Landing commit')}:</label>
85 90 </div>
86 91 <div class="select">
87 92 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
88 93 <span class="help-block">${_('The default commit for file pages, downloads, full text search index, and README generation.')}</span>
89 94 </div>
90 95 </div>
91 96 <div class="field">
92 97 <div class="label label-checkbox">
93 98 <label for="repo_private">${_('Private Repository')}:</label>
94 99 </div>
95 100 <div class="checkboxes">
96 101 ${h.checkbox('repo_private',value="True")}
97 102 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
98 103 </div>
99 104 </div>
100 105 <div class="buttons">
101 106 ${h.submit('save',_('Save'),class_="btn")}
102 107 </div>
103 108 </div>
104 109 </div>
105 110 <script>
106 111 $(document).ready(function(){
107 112 var setCopyPermsOption = function(group_val){
108 113 if(group_val != "-1"){
109 114 $('#copy_perms').show()
110 115 }
111 116 else{
112 117 $('#copy_perms').hide();
113 118 }
114 119 };
115 120
116 121 $('#remote_clone_toggle').on('click', function(e){
117 122 $('#remote_clone').show();
118 123 e.preventDefault();
119 124 });
120 125
121 126 if($('#remote_clone input').hasClass('error')){
122 127 $('#remote_clone').show();
123 128 }
124 129 if($('#remote_clone input').val()){
125 130 $('#remote_clone').show();
126 131 }
127 132
128 133 $("#repo_group").select2({
129 134 'containerCssClass': "drop-menu",
130 135 'dropdownCssClass': "drop-menu-dropdown",
131 136 'dropdownAutoWidth': true,
132 137 'width': "resolve"
133 138 });
134 139
135 140 setCopyPermsOption($('#repo_group').val());
136 141 $("#repo_group").on("change", function(e) {
137 142 setCopyPermsOption(e.val)
138 143 });
139 144
140 145 $("#repo_type").select2({
141 146 'containerCssClass': "drop-menu",
142 147 'dropdownCssClass': "drop-menu-dropdown",
143 148 'minimumResultsForSearch': -1,
144 149 });
145 150 $("#repo_landing_rev").select2({
146 151 'containerCssClass': "drop-menu",
147 152 'dropdownCssClass': "drop-menu-dropdown",
148 153 'minimumResultsForSearch': -1,
149 154 });
150 155 $('#repo_name').focus();
151 156
152 157 $('#select_my_group').on('click', function(e){
153 158 e.preventDefault();
154 159 $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
155 160 })
156 161
157 162 })
158 163 </script>
159 164 ${h.end_form()}
@@ -1,260 +1,266 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <div class="panel panel-default">
5 5 <div class="panel-heading">
6 6 <h3 class="panel-title">${_('Settings for Repository: %s') % c.rhodecode_db_repo.repo_name}</h3>
7 7 </div>
8 8 <div class="panel-body">
9 9 ${h.secure_form(h.route_path('edit_repo', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)}
10 10 <div class="form">
11 11 <!-- fields -->
12 12 <div class="fields">
13 13
14 14 <div class="field">
15 15 <div class="label">
16 16 <label for="repo_name">${_('Name')}:</label>
17 17 </div>
18 18 <div class="input">
19 19 ${c.form['repo_name'].render(css_class='medium', oid='repo_name')|n}
20 20 ${c.form.render_error(request, c.form['repo_name'])|n}
21 21
22 22 <p class="help-block">${_('Non-changeable id')}: `_${c.rhodecode_db_repo.repo_id}` <span><a href="#" onclick="$('#clone_id').toggle();return false">${_('what is that ?')}</a></span></p>
23 23 <p id="clone_id" style="display:none;">
24 24 ${_('URL by id')}: `${c.rhodecode_db_repo.clone_url(with_id=True)}` <br/>
25 25 ${_('''In case this repository is renamed or moved into another group the repository url changes.
26 26 Using above url guarantees that this repository will always be accessible under such url.
27 27 Useful for CI systems, or any other cases that you need to hardcode the url into 3rd party service.''')}</p>
28 28 </div>
29 29 </div>
30 30
31 31 <div class="field">
32 32 <div class="label">
33 33 <label for="repo_group">${_('Repository group')}:</label>
34 34 </div>
35 35 <div class="select">
36 36 ${c.form['repo_group'].render(css_class='medium', oid='repo_group')|n}
37 37 ${c.form.render_error(request, c.form['repo_group'])|n}
38 38
39 39 % if c.personal_repo_group:
40 40 <a class="btn" href="#" data-personal-group-name="${c.personal_repo_group.group_name}" data-personal-group-id="${c.personal_repo_group.group_id}" onclick="selectMyGroup(this); return false">
41 41 ${_('Select my personal group (`%(repo_group_name)s`)') % {'repo_group_name': c.personal_repo_group.group_name}}
42 42 </a>
43 43 % endif
44 44 <p class="help-block">${_('Optional select a group to put this repository into.')}</p>
45 45 </div>
46 46 </div>
47 47
48 48 % if c.rhodecode_db_repo.repo_type != 'svn':
49 49 <div class="field">
50 50 <div class="label">
51 51 <label for="clone_uri">${_('Remote uri')}:</label>
52 52 </div>
53 53 <div class="input">
54 54 %if c.rhodecode_db_repo.clone_uri:
55 55 ## display, if we don't have any errors
56 56 % if not c.form['repo_clone_uri'].error:
57 57 <div id="clone_uri_hidden" class='text-as-placeholder'>
58 58 <span id="clone_uri_hidden_value">${c.rhodecode_db_repo.clone_uri_hidden}</span>
59 59 <span class="link" id="edit_clone_uri"><i class="icon-edit"></i>${_('edit')}</span>
60 60 </div>
61 61 % endif
62 62
63 63 ## alter field
64 64 <div id="alter_clone_uri" style="${'' if c.form['repo_clone_uri'].error else 'display: none'}">
65 65 ${c.form['repo_clone_uri'].render(css_class='medium', oid='clone_uri', placeholder=_('enter new value, or leave empty to remove'))|n}
66 66 ${c.form.render_error(request, c.form['repo_clone_uri'])|n}
67 67 % if c.form['repo_clone_uri'].error:
68 68 ## we got error from form subit, means we modify the url
69 69 ${h.hidden('repo_clone_uri_change', 'MOD')}
70 70 % else:
71 71 ${h.hidden('repo_clone_uri_change', 'OLD')}
72 72 % endif
73 73
74 74 % if not c.form['repo_clone_uri'].error:
75 75 <span class="link" id="cancel_edit_clone_uri">${_('cancel')}</span>
76 76 % endif
77 77
78 78 </div>
79 79 %else:
80 80 ## not set yet, display form to set it
81 81 ${c.form['repo_clone_uri'].render(css_class='medium', oid='clone_uri')|n}
82 82 ${c.form.render_error(request, c.form['repo_clone_uri'])|n}
83 83 ${h.hidden('repo_clone_uri_change', 'NEW')}
84 84 %endif
85 85 <p id="alter_clone_uri_help_block" class="help-block">
86 86 <% pull_link = h.literal(h.link_to('remote sync', h.route_path('edit_repo_remote', repo_name=c.repo_name))) %>
87 87 ${_('http[s] url where from repository was imported, this field can used for doing {pull_link}.').format(pull_link=pull_link)|n} <br/>
88 88 ${_('This field is stored encrypted inside Database, a format of http://user:password@server.com/repo_name can be used and will be hidden from display.')}
89 89 </p>
90 90 </div>
91 91 </div>
92 92 % else:
93 93 ${h.hidden('repo_clone_uri', '')}
94 94 % endif
95 95
96 96 <div class="field">
97 97 <div class="label">
98 98 <label for="repo_landing_commit_ref">${_('Landing commit')}:</label>
99 99 </div>
100 100 <div class="select">
101 101 ${c.form['repo_landing_commit_ref'].render(css_class='medium', oid='repo_landing_commit_ref')|n}
102 102 ${c.form.render_error(request, c.form['repo_landing_commit_ref'])|n}
103 103 <p class="help-block">${_('Default commit for files page, downloads, full text search index and readme')}</p>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="field badged-field">
108 108 <div class="label">
109 109 <label for="repo_owner">${_('Owner')}:</label>
110 110 </div>
111 111 <div class="input">
112 112 <div class="badge-input-container">
113 113 <div class="user-badge">
114 114 ${base.gravatar_with_user(c.rhodecode_db_repo.user.email or c.rhodecode_db_repo.user.username, show_disabled=not c.rhodecode_db_repo.user.active)}
115 115 </div>
116 116 <div class="badge-input-wrap">
117 117 ${c.form['repo_owner'].render(css_class='medium', oid='repo_owner')|n}
118 118 </div>
119 119 </div>
120 120 ${c.form.render_error(request, c.form['repo_owner'])|n}
121 121 <p class="help-block">${_('Change owner of this repository.')}</p>
122 122 </div>
123 123 </div>
124 124
125 125 <div class="field">
126 126 <div class="label label-textarea">
127 127 <label for="repo_description">${_('Description')}:</label>
128 128 </div>
129 129 <div class="textarea text-area editor">
130 130 ${c.form['repo_description'].render(css_class='medium', oid='repo_description')|n}
131 131 ${c.form.render_error(request, c.form['repo_description'])|n}
132 <p class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</p>
132
133 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
134 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
135 <span id="meta-tags-desc" style="display: none">
136 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
137 ${dt.metatags_help()}
138 </span>
133 139 </div>
134 140 </div>
135 141
136 142 <div class="field">
137 143 <div class="label label-checkbox">
138 144 <label for="${c.form['repo_private'].oid}">${_('Private repository')}:</label>
139 145 </div>
140 146 <div class="checkboxes">
141 147 ${c.form['repo_private'].render(css_class='medium')|n}
142 148 ${c.form.render_error(request, c.form['repo_private'])|n}
143 149 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
144 150 </div>
145 151 </div>
146 152 <div class="field">
147 153 <div class="label label-checkbox">
148 154 <label for="${c.form['repo_enable_statistics'].oid}">${_('Enable statistics')}:</label>
149 155 </div>
150 156 <div class="checkboxes">
151 157 ${c.form['repo_enable_statistics'].render(css_class='medium')|n}
152 158 ${c.form.render_error(request, c.form['repo_enable_statistics'])|n}
153 159 <span class="help-block">${_('Enable statistics window on summary page.')}</span>
154 160 </div>
155 161 </div>
156 162 <div class="field">
157 163 <div class="label label-checkbox">
158 164 <label for="${c.form['repo_enable_downloads'].oid}">${_('Enable downloads')}:</label>
159 165 </div>
160 166 <div class="checkboxes">
161 167 ${c.form['repo_enable_downloads'].render(css_class='medium')|n}
162 168 ${c.form.render_error(request, c.form['repo_enable_downloads'])|n}
163 169 <span class="help-block">${_('Enable download menu on summary page.')}</span>
164 170 </div>
165 171 </div>
166 172 <div class="field">
167 173 <div class="label label-checkbox">
168 174 <label for="${c.form['repo_enable_locking'].oid}">${_('Enable automatic locking')}:</label>
169 175 </div>
170 176 <div class="checkboxes">
171 177 ${c.form['repo_enable_locking'].render(css_class='medium')|n}
172 178 ${c.form.render_error(request, c.form['repo_enable_locking'])|n}
173 179 <span class="help-block">${_('Enable automatic locking on repository. Pulling from this repository creates a lock that can be released by pushing back by the same user')}</span>
174 180 </div>
175 181 </div>
176 182
177 183 %if c.visual.repository_fields:
178 184 ## EXTRA FIELDS
179 185 %for field in c.repo_fields:
180 186 <div class="field">
181 187 <div class="label">
182 188 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
183 189 </div>
184 190 <div class="input input-medium">
185 191 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
186 192 %if field.field_desc:
187 193 <span class="help-block">${field.field_desc}</span>
188 194 %endif
189 195 </div>
190 196 </div>
191 197 %endfor
192 198 %endif
193 199 <div class="buttons">
194 200 ${h.submit('save',_('Save'),class_="btn")}
195 201 ${h.reset('reset',_('Reset'),class_="btn")}
196 202 </div>
197 203 </div>
198 204 </div>
199 205 ${h.end_form()}
200 206 </div>
201 207 </div>
202 208
203 209 <script>
204 210 $(document).ready(function(){
205 211 var cloneUrl = function() {
206 212 var alterButton = $('#alter_clone_uri');
207 213 var editButton = $('#edit_clone_uri');
208 214 var cancelEditButton = $('#cancel_edit_clone_uri');
209 215 var hiddenUrl = $('#clone_uri_hidden');
210 216 var hiddenUrlValue = $('#clone_uri_hidden_value');
211 217 var input = $('#clone_uri');
212 218 var helpBlock = $('#alter_clone_uri_help_block');
213 219 var changedFlag = $('#repo_clone_uri_change');
214 220 var originalText = helpBlock.html();
215 221 var obfuscatedUrl = hiddenUrlValue.html();
216 222
217 223 var edit = function(e) {
218 224 alterButton.show();
219 225 editButton.hide();
220 226 hiddenUrl.hide();
221 227
222 228 //add the old value next to input for verification
223 229 helpBlock.html("(" + obfuscatedUrl + ")" + "<br\>" + originalText);
224 230 changedFlag.val('MOD');
225 231 };
226 232
227 233 var cancelEdit = function(e) {
228 234 alterButton.hide();
229 235 editButton.show();
230 236 hiddenUrl.show();
231 237
232 238 helpBlock.html(originalText);
233 239 changedFlag.val('OLD');
234 240 input.val('');
235 241 };
236 242
237 243 var initEvents = function() {
238 244 editButton.on('click', edit);
239 245 cancelEditButton.on('click', cancelEdit);
240 246 };
241 247
242 248 var setInitialState = function() {
243 249 if (input.hasClass('error')) {
244 250 alterButton.show();
245 251 editButton.hide();
246 252 hiddenUrl.hide();
247 253 }
248 254 };
249 255
250 256 setInitialState();
251 257 initEvents();
252 258 }();
253 259
254 260 selectMyGroup = function(element) {
255 261 $("#repo_group").val($(element).data('personalGroupId')).trigger("change");
256 262 };
257 263
258 264 UsersAutoComplete('repo_owner', '${c.rhodecode_user.user_id}');
259 265 });
260 266 </script>
@@ -1,237 +1,226 b''
1 1 ${h.secure_form(h.url('admin_settings_visual'), method='post')}
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading" id="general">
5 5 <h3 class="panel-title">${_('General')}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 <div class="checkbox">
9 9 ${h.checkbox('rhodecode_repository_fields','True')}
10 10 <label for="rhodecode_repository_fields">${_('Use repository extra fields')}</label>
11 11 </div>
12 12 <span class="help-block">${_('Allows storing additional customized fields per repository.')}</span>
13 13
14 14 <div></div>
15 15 <div class="checkbox">
16 16 ${h.checkbox('rhodecode_show_version','True')}
17 17 <label for="rhodecode_show_version">${_('Show RhodeCode version')}</label>
18 18 </div>
19 19 <span class="help-block">${_('Shows or hides a version number of RhodeCode displayed in the footer.')}</span>
20 20 </div>
21 21 </div>
22 22
23 23
24 24 <div class="panel panel-default">
25 25 <div class="panel-heading" id="gravatars">
26 26 <h3 class="panel-title">${_('Gravatars')}</h3>
27 27 </div>
28 28 <div class="panel-body">
29 29 <div class="checkbox">
30 30 ${h.checkbox('rhodecode_use_gravatar','True')}
31 31 <label for="rhodecode_use_gravatar">${_('Use Gravatars based avatars')}</label>
32 32 </div>
33 33 <span class="help-block">${_('Use gravatar.com as avatar system for RhodeCode accounts. If this is disabled avatars are generated based on initials and email.')}</span>
34 34
35 35 <div class="label">
36 36 <label for="rhodecode_gravatar_url">${_('Gravatar URL')}</label>
37 37 </div>
38 38 <div class="input">
39 39 <div class="field">
40 40 ${h.text('rhodecode_gravatar_url', size='100%')}
41 41 </div>
42 42
43 43 <div class="field">
44 44 <span class="help-block">${_('''Gravatar url allows you to use other avatar server application.
45 45 Following variables of the URL will be replaced accordingly.
46 46 {scheme} 'http' or 'https' sent from running RhodeCode server,
47 47 {email} user email,
48 48 {md5email} md5 hash of the user email (like at gravatar.com),
49 49 {size} size of the image that is expected from the server application,
50 50 {netloc} network location/server host of running RhodeCode server''')}</span>
51 51 </div>
52 52 </div>
53 53 </div>
54 54 </div>
55 55
56 56
57 57 <div class="panel panel-default">
58 58 <div class="panel-heading" id="meta-tagging">
59 59 <h3 class="panel-title">${_('Meta-Tagging')}</h3>
60 60 </div>
61 61 <div class="panel-body">
62 62 <div class="checkbox">
63 63 ${h.checkbox('rhodecode_stylify_metatags','True')}
64 64 <label for="rhodecode_stylify_metatags">${_('Stylify recognised meta tags')}</label>
65 65 </div>
66 <span class="help-block">${_('Parses meta tags from repository description field and turns them into colored tags.')}</span>
66 <span class="help-block">${_('Parses meta tags from repository or repository group description fields and turns them into colored tags.')}</span>
67 67 <div>
68 <table>
69 <tr><td>[featured] </td><td><span class="metatag" tag="featured">featured</span></td></tr>
70 <tr><td>[stale] </td><td><span class="metatag" tag="stale">stale</span></td></tr>
71 <tr><td>[dead] </td><td><span class="metatag" tag="dead">dead</span></td></tr>
72 <tr><td>[personal] </td><td><span class="metatag" tag="personal">personal</span></td></tr>
73
74 <tr><td>[lang =&gt; lang] </td><td><span class="metatag" tag="lang" >lang</span></td></tr>
75
76 <tr><td>[license =&gt; License] </td><td><span class="metatag" tag="license"><a href="http://www.opensource.org/licenses/License" >License</a></span></td></tr>
77 <tr><td>[requires =&gt; Repo] </td><td><span class="metatag" tag="requires" >requires =&gt; <a href="#" >Repo</a></span></td></tr>
78 <tr><td>[recommends =&gt; Repo] </td><td><span class="metatag" tag="recommends" >recommends =&gt; <a href="#" >Repo</a></span></td></tr>
79 <tr><td>[see =&gt; URI] </td><td><span class="metatag" tag="see">see =&gt; <a href="#">URI</a> </span></td></tr>
80 </table>
68 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
69 ${dt.metatags_help()}
81 70 </div>
82 71 </div>
83 72 </div>
84 73
85 74
86 75 <div class="panel panel-default">
87 76 <div class="panel-heading">
88 77 <h3 class="panel-title">${_('Dashboard Items')}</h3>
89 78 </div>
90 79 <div class="panel-body">
91 80 <div class="label">
92 81 <label for="rhodecode_dashboard_items">${_('Main page dashboard items')}</label>
93 82 </div>
94 83 <div class="field input">
95 84 ${h.text('rhodecode_dashboard_items',size=5)}
96 85 </div>
97 86 <div class="field">
98 87 <span class="help-block">${_('Number of items displayed in the main page dashboard before pagination is shown.')}</span>
99 88 </div>
100 89
101 90 <div class="label">
102 91 <label for="rhodecode_admin_grid_items">${_('Admin pages items')}</label>
103 92 </div>
104 93 <div class="field input">
105 94 ${h.text('rhodecode_admin_grid_items',size=5)}
106 95 </div>
107 96 <div class="field">
108 97 <span class="help-block">${_('Number of items displayed in the admin pages grids before pagination is shown.')}</span>
109 98 </div>
110 99 </div>
111 100 </div>
112 101
113 102
114 103
115 104 <div class="panel panel-default">
116 105 <div class="panel-heading" id="commit-id">
117 106 <h3 class="panel-title">${_('Commit ID Style')}</h3>
118 107 </div>
119 108 <div class="panel-body">
120 109 <div class="label">
121 110 <label for="rhodecode_show_sha_length">${_('Commit sha length')}</label>
122 111 </div>
123 112 <div class="input">
124 113 <div class="field">
125 114 ${h.text('rhodecode_show_sha_length',size=5)}
126 115 </div>
127 116 <div class="field">
128 117 <span class="help-block">${_('''Number of chars to show in commit sha displayed in web interface.
129 118 By default it's shown as r123:9043a6a4c226 this value defines the
130 119 length of the sha after the `r123:` part.''')}</span>
131 120 </div>
132 121 </div>
133 122
134 123 <div class="checkbox">
135 124 ${h.checkbox('rhodecode_show_revision_number','True')}
136 125 <label for="rhodecode_show_revision_number">${_('Show commit ID numeric reference')} / ${_('Commit show revision number')}</label>
137 126 </div>
138 127 <span class="help-block">${_('''Show revision number in commit sha displayed in web interface.
139 128 By default it's shown as r123:9043a6a4c226 this value defines the
140 129 if the `r123:` part is shown.''')}</span>
141 130 </div>
142 131 </div>
143 132
144 133
145 134 <div class="panel panel-default">
146 135 <div class="panel-heading" id="icons">
147 136 <h3 class="panel-title">${_('Icons')}</h3>
148 137 </div>
149 138 <div class="panel-body">
150 139 <div class="checkbox">
151 140 ${h.checkbox('rhodecode_show_public_icon','True')}
152 141 <label for="rhodecode_show_public_icon">${_('Show public repo icon on repositories')}</label>
153 142 </div>
154 143 <div></div>
155 144
156 145 <div class="checkbox">
157 146 ${h.checkbox('rhodecode_show_private_icon','True')}
158 147 <label for="rhodecode_show_private_icon">${_('Show private repo icon on repositories')}</label>
159 148 </div>
160 149 <span class="help-block">${_('Show public/private icons next to repositories names.')}</span>
161 150 </div>
162 151 </div>
163 152
164 153
165 154 <div class="panel panel-default">
166 155 <div class="panel-heading">
167 156 <h3 class="panel-title">${_('Markup Renderer')}</h3>
168 157 </div>
169 158 <div class="panel-body">
170 159 <div class="field select">
171 160 ${h.select('rhodecode_markup_renderer', '', ['rst', 'markdown'])}
172 161 </div>
173 162 <div class="field">
174 163 <span class="help-block">${_('Default renderer used to render comments, pull request descriptions and other description elements. After change old entries will still work correctly.')}</span>
175 164 </div>
176 165 </div>
177 166 </div>
178 167
179 168 <div class="panel panel-default">
180 169 <div class="panel-heading">
181 170 <h3 class="panel-title">${_('Clone URL')}</h3>
182 171 </div>
183 172 <div class="panel-body">
184 173 <div class="field">
185 174 ${h.text('rhodecode_clone_uri_tmpl', size=60)}
186 175 </div>
187 176
188 177 <div class="field">
189 178 <span class="help-block">
190 179 ${_('''Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:
191 180 {scheme} 'http' or 'https' sent from running RhodeCode server,
192 181 {user} current user username,
193 182 {netloc} network location/server host of running RhodeCode server,
194 183 {repo} full repository name,
195 184 {repoid} ID of repository, can be used to contruct clone-by-id''')}
196 185 </span>
197 186 </div>
198 187 </div>
199 188 </div>
200 189
201 190 <div class="panel panel-default">
202 191 <div class="panel-heading">
203 192 <h3 class="panel-title">${_('Custom Support Link')}</h3>
204 193 </div>
205 194 <div class="panel-body">
206 195 <div class="field">
207 196 ${h.text('rhodecode_support_url', size=60)}
208 197 </div>
209 198 <div class="field">
210 199 <span class="help-block">
211 200 ${_('''Custom url for the support link located at the bottom.
212 201 The default is set to %(default_url)s. In case there's a need
213 202 to change the support link to internal issue tracker, it should be done here.
214 203 ''') % {'default_url': h.url('rhodecode_support')}}
215 204 </span>
216 205 </div>
217 206 </div>
218 207 </div>
219 208
220 209 <div class="buttons">
221 210 ${h.submit('save',_('Save settings'),class_="btn")}
222 211 ${h.reset('reset',_('Reset'),class_="btn")}
223 212 </div>
224 213
225 214
226 215 ${h.end_form()}
227 216
228 217 <script>
229 218 $(document).ready(function() {
230 219 $('#rhodecode_markup_renderer').select2({
231 220 containerCssClass: 'drop-menu',
232 221 dropdownCssClass: 'drop-menu-dropdown',
233 222 dropdownAutoWidth: true,
234 223 minimumResultsForSearch: -1
235 224 });
236 225 });
237 226 </script>
@@ -1,314 +1,376 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 <%def name="metatags_help()">
7 <table>
8 <%
9 example_tags = [
10 ('state','[stable]'),
11 ('state','[stale]'),
12 ('state','[featured]'),
13 ('state','[dev]'),
14 ('state','[dead]'),
15
16 ('label','[personal]'),
17 ('generic','[v2.0.0]'),
18
19 ('lang','[lang =&gt; JavaScript]'),
20 ('license','[license =&gt; LicenseName]'),
21
22 ('ref','[requires =&gt; RepoName]'),
23 ('ref','[recommends =&gt; GroupName]'),
24 ('ref','[conflicts =&gt; SomeName]'),
25 ('ref','[base =&gt; SomeName]'),
26 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
27 ('see','[see =&gt; http://rhodecode.com]'),
28 ]
29 %>
30 % for tag_type, tag in example_tags:
31 <tr>
32 <td>${tag|n}</td>
33 <td>${h.style_metatag(tag_type, tag)|n}</td>
34 </tr>
35 % endfor
36 </table>
37 </%def>
38
6 39 ## REPOSITORY RENDERERS
7 40 <%def name="quick_menu(repo_name)">
8 41 <i class="icon-more"></i>
9 42 <div class="menu_items_container hidden">
10 43 <ul class="menu_items">
11 44 <li>
12 45 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
13 46 <span>${_('Summary')}</span>
14 47 </a>
15 48 </li>
16 49 <li>
17 50 <a title="${_('Changelog')}" href="${h.route_path('repo_changelog',repo_name=repo_name)}">
18 51 <span>${_('Changelog')}</span>
19 52 </a>
20 53 </li>
21 54 <li>
22 55 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
23 56 <span>${_('Files')}</span>
24 57 </a>
25 58 </li>
26 59 <li>
27 60 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
28 61 <span>${_('Fork')}</span>
29 62 </a>
30 63 </li>
31 64 </ul>
32 65 </div>
33 66 </%def>
34 67
35 68 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 69 <%
37 70 def get_name(name,short_name=short_name):
38 71 if short_name:
39 72 return name.split('/')[-1]
40 73 else:
41 74 return name
42 75 %>
43 76 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 77 ##NAME
45 78 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
46 79
47 80 ##TYPE OF REPO
48 81 %if h.is_hg(rtype):
49 82 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 83 %elif h.is_git(rtype):
51 84 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 85 %elif h.is_svn(rtype):
53 86 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 87 %endif
55 88
56 89 ##PRIVATE/PUBLIC
57 90 %if private and c.visual.show_private_icon:
58 91 <i class="icon-lock" title="${_('Private repository')}"></i>
59 92 %elif not private and c.visual.show_public_icon:
60 93 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 94 %else:
62 95 <span></span>
63 96 %endif
64 97 ${get_name(name)}
65 98 </a>
66 99 %if fork_of:
67 100 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 101 %endif
69 102 %if rstate == 'repo_state_pending':
70 103 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
71 104 (${_('creating...')})
72 105 </span>
73 106 %endif
74 107 </div>
75 108 </%def>
76 109
77 <%def name="repo_desc(description)">
78 <div class="truncate-wrap">${description}</div>
110 <%def name="repo_desc(description, stylify_metatags)">
111 <%
112 tags, description = h.extract_metatags(description)
113 %>
114
115 <div class="truncate-wrap">
116 % if stylify_metatags:
117 % for tag_type, tag in tags:
118 ${h.style_metatag(tag_type, tag)|n}
119 % endfor
120 % endif
121 ${description}
122 </div>
123
79 124 </%def>
80 125
81 126 <%def name="last_change(last_change)">
82 127 ${h.age_component(last_change)}
83 128 </%def>
84 129
85 130 <%def name="revision(name,rev,tip,author,last_msg)">
86 131 <div>
87 132 %if rev >= 0:
88 133 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.route_path('repo_commit',repo_name=name,commit_id=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
89 134 %else:
90 135 ${_('No commits yet')}
91 136 %endif
92 137 </div>
93 138 </%def>
94 139
95 140 <%def name="rss(name)">
96 141 %if c.rhodecode_user.username != h.DEFAULT_USER:
97 142 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
98 143 %else:
99 144 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
100 145 %endif
101 146 </%def>
102 147
103 148 <%def name="atom(name)">
104 149 %if c.rhodecode_user.username != h.DEFAULT_USER:
105 150 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
106 151 %else:
107 152 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
108 153 %endif
109 154 </%def>
110 155
111 156 <%def name="user_gravatar(email, size=16)">
112 157 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
113 158 ${base.gravatar(email, 16)}
114 159 </div>
115 160 </%def>
116 161
117 162 <%def name="repo_actions(repo_name, super_user=True)">
118 163 <div>
119 164 <div class="grid_edit">
120 165 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
121 166 <i class="icon-pencil"></i>Edit</a>
122 167 </div>
123 168 <div class="grid_delete">
124 169 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST', request=request)}
125 170 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
126 171 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
127 172 ${h.end_form()}
128 173 </div>
129 174 </div>
130 175 </%def>
131 176
132 177 <%def name="repo_state(repo_state)">
133 178 <div>
134 179 %if repo_state == 'repo_state_pending':
135 180 <div class="tag tag4">${_('Creating')}</div>
136 181 %elif repo_state == 'repo_state_created':
137 182 <div class="tag tag1">${_('Created')}</div>
138 183 %else:
139 184 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
140 185 %endif
141 186 </div>
142 187 </%def>
143 188
144 189
145 190 ## REPO GROUP RENDERERS
146 191 <%def name="quick_repo_group_menu(repo_group_name)">
147 192 <i class="icon-more"></i>
148 193 <div class="menu_items_container hidden">
149 194 <ul class="menu_items">
150 195 <li>
151 196 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
152 197 </li>
153 198
154 199 </ul>
155 200 </div>
156 201 </%def>
157 202
158 203 <%def name="repo_group_name(repo_group_name, children_groups=None)">
159 204 <div>
160 205 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
161 206 <i class="icon-folder-close" title="${_('Repository group')}"></i>
162 207 %if children_groups:
163 208 ${h.literal(' &raquo; '.join(children_groups))}
164 209 %else:
165 210 ${repo_group_name}
166 211 %endif
167 212 </a>
168 213 </div>
169 214 </%def>
170 215
171 <%def name="repo_group_desc(description)">
172 <div class="truncate-wrap">${description}</div>
216 <%def name="repo_group_desc(description, personal, stylify_metatags)">
217
218 <%
219 tags, description = h.extract_metatags(description)
220 %>
221
222 <div class="truncate-wrap">
223 % if personal:
224 <div class="metatag" tag="personal">${_('personal')}</div>
225 % endif
226
227 % if stylify_metatags:
228 % for tag_type, tag in tags:
229 ${h.style_metatag(tag_type, tag)|n}
230 % endfor
231 % endif
232 ${description}
233 </div>
234
173 235 </%def>
174 236
175 237 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
176 238 <div class="grid_edit">
177 239 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
178 240 </div>
179 241 <div class="grid_delete">
180 242 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
181 243 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
182 244 onclick="return confirm('"+_ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
183 245 ${h.end_form()}
184 246 </div>
185 247 </%def>
186 248
187 249
188 250 <%def name="user_actions(user_id, username)">
189 251 <div class="grid_edit">
190 252 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
191 253 <i class="icon-pencil"></i>Edit</a>
192 254 </div>
193 255 <div class="grid_delete">
194 256 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
195 257 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
196 258 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
197 259 ${h.end_form()}
198 260 </div>
199 261 </%def>
200 262
201 263 <%def name="user_group_actions(user_group_id, user_group_name)">
202 264 <div class="grid_edit">
203 265 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
204 266 </div>
205 267 <div class="grid_delete">
206 268 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), method='POST', request=request)}
207 269 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
208 270 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
209 271 ${h.end_form()}
210 272 </div>
211 273 </%def>
212 274
213 275
214 276 <%def name="user_name(user_id, username)">
215 277 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
216 278 </%def>
217 279
218 280 <%def name="user_profile(username)">
219 281 ${base.gravatar_with_user(username, 16)}
220 282 </%def>
221 283
222 284 <%def name="user_group_name(user_group_id, user_group_name)">
223 285 <div>
224 286 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}">
225 287 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
226 288 </div>
227 289 </%def>
228 290
229 291
230 292 ## GISTS
231 293
232 294 <%def name="gist_gravatar(full_contact)">
233 295 <div class="gist_gravatar">
234 296 ${base.gravatar(full_contact, 30)}
235 297 </div>
236 298 </%def>
237 299
238 300 <%def name="gist_access_id(gist_access_id, full_contact)">
239 301 <div>
240 302 <b>
241 303 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
242 304 </b>
243 305 </div>
244 306 </%def>
245 307
246 308 <%def name="gist_author(full_contact, created_on, expires)">
247 309 ${base.gravatar_with_user(full_contact, 16)}
248 310 </%def>
249 311
250 312
251 313 <%def name="gist_created(created_on)">
252 314 <div class="created">
253 315 ${h.age_component(created_on, time_is_local=True)}
254 316 </div>
255 317 </%def>
256 318
257 319 <%def name="gist_expires(expires)">
258 320 <div class="created">
259 321 %if expires == -1:
260 322 ${_('never')}
261 323 %else:
262 324 ${h.age_component(h.time_to_utcdatetime(expires))}
263 325 %endif
264 326 </div>
265 327 </%def>
266 328
267 329 <%def name="gist_type(gist_type)">
268 330 %if gist_type != 'public':
269 331 <div class="tag">${_('Private')}</div>
270 332 %endif
271 333 </%def>
272 334
273 335 <%def name="gist_description(gist_description)">
274 336 ${gist_description}
275 337 </%def>
276 338
277 339
278 340 ## PULL REQUESTS GRID RENDERERS
279 341
280 342 <%def name="pullrequest_target_repo(repo_name)">
281 343 <div class="truncate">
282 344 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
283 345 </div>
284 346 </%def>
285 347 <%def name="pullrequest_status(status)">
286 348 <div class="${'flag_status %s' % status} pull-left"></div>
287 349 </%def>
288 350
289 351 <%def name="pullrequest_title(title, description)">
290 352 ${title} <br/>
291 353 ${h.shorter(description, 40)}
292 354 </%def>
293 355
294 356 <%def name="pullrequest_comments(comments_nr)">
295 357 <i class="icon-comment"></i> ${comments_nr}
296 358 </%def>
297 359
298 360 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
299 361 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
300 362 % if short:
301 363 #${pull_request_id}
302 364 % else:
303 365 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
304 366 % endif
305 367 </a>
306 368 </%def>
307 369
308 370 <%def name="pullrequest_updated_on(updated_on)">
309 371 ${h.age_component(h.time_to_utcdatetime(updated_on))}
310 372 </%def>
311 373
312 374 <%def name="pullrequest_author(full_contact)">
313 375 ${base.gravatar_with_user(full_contact, 16)}
314 376 </%def>
@@ -1,129 +1,134 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Fork repository %s') % c.repo_name}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('New Fork')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='repositories')}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_subnav()">
20 20 ${self.repo_menu(active='options')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <div class="box">
25 25 <div class="title">
26 26 ${self.repo_page_title(c.rhodecode_db_repo)}
27 27 ${self.breadcrumbs()}
28 28 </div>
29 29
30 30 ${h.secure_form(h.route_path('repo_fork_create',repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)}
31 31 <div class="form">
32 32 <!-- fields -->
33 33 <div class="fields">
34 34
35 35 <div class="field">
36 36 <div class="label">
37 37 <label for="repo_name">${_('Fork name')}:</label>
38 38 </div>
39 39 <div class="input">
40 40 ${h.text('repo_name', class_="medium")}
41 41 ${h.hidden('repo_type',c.rhodecode_db_repo.repo_type)}
42 42 ${h.hidden('fork_parent_id',c.rhodecode_db_repo.repo_id)}
43 43 </div>
44 44 </div>
45 45
46 46 <div class="field">
47 47 <div class="label label-textarea">
48 48 <label for="description">${_('Description')}:</label>
49 49 </div>
50 50 <div class="textarea-repo textarea text-area editor">
51 51 ${h.textarea('description')}
52 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
52 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
53 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
54 <span id="meta-tags-desc" style="display: none">
55 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
56 ${dt.metatags_help()}
57 </span>
53 58 </div>
54 59 </div>
55 60
56 61 <div class="field">
57 62 <div class="label">
58 63 <label for="repo_group">${_('Repository group')}:</label>
59 64 </div>
60 65 <div class="select">
61 66 ${h.select('repo_group','',c.repo_groups,class_="medium")}
62 67 % if c.personal_repo_group:
63 68 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
64 69 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
65 70 </a>
66 71 % endif
67 72 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
68 73 </div>
69 74 </div>
70 75
71 76 <div class="field">
72 77 <div class="label">
73 78 <label for="landing_rev">${_('Landing commit')}:</label>
74 79 </div>
75 80 <div class="select">
76 81 ${h.select('landing_rev','',c.landing_revs,class_="medium")}
77 82 <span class="help-block">${_('Default commit for files page, downloads, whoosh and readme')}</span>
78 83 </div>
79 84 </div>
80 85
81 86 <div class="field">
82 87 <div class="label label-checkbox">
83 88 <label for="private">${_('Private')}:</label>
84 89 </div>
85 90 <div class="checkboxes">
86 91 ${h.checkbox('private',value="True")}
87 92 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
88 93 </div>
89 94 </div>
90 95
91 96 <div class="field">
92 97 <div class="label label-checkbox">
93 98 <label for="private">${_('Copy permissions')}:</label>
94 99 </div>
95 100 <div class="checkboxes">
96 101 ${h.checkbox('copy_permissions',value="True", checked="checked")}
97 102 <span class="help-block">${_('Copy permissions from forked repository')}</span>
98 103 </div>
99 104 </div>
100 105
101 106 <div class="buttons">
102 107 ${h.submit('',_('Fork this Repository'),class_="btn")}
103 108 </div>
104 109 </div>
105 110 </div>
106 111 ${h.end_form()}
107 112 </div>
108 113 <script>
109 114 $(document).ready(function(){
110 115 $("#repo_group").select2({
111 116 'dropdownAutoWidth': true,
112 117 'containerCssClass': "drop-menu",
113 118 'dropdownCssClass': "drop-menu-dropdown",
114 119 'width': "resolve"
115 120 });
116 121 $("#landing_rev").select2({
117 122 'containerCssClass': "drop-menu",
118 123 'dropdownCssClass': "drop-menu-dropdown",
119 124 'minimumResultsForSearch': -1
120 125 });
121 126 $('#repo_name').focus();
122 127
123 128 $('#select_my_group').on('click', function(e){
124 129 e.preventDefault();
125 130 $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
126 131 })
127 132 })
128 133 </script>
129 134 </%def>
@@ -1,211 +1,210 b''
1 1 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
2 2 <span class="branchtag tag">
3 3 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
4 4 <i class="icon-branch"></i>${_ungettext(
5 5 '%(num)s Branch','%(num)s Branches', len(branches)) % {'num': len(branches)}}</a>
6 6 </span>
7 7
8 8 %if closed_branches:
9 9 <span class="branchtag tag">
10 10 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
11 11 <i class="icon-branch"></i>${_ungettext(
12 12 '%(num)s Closed Branch', '%(num)s Closed Branches', len(closed_branches)) % {'num': len(closed_branches)}}</a>
13 13 </span>
14 14 %endif
15 15
16 16 <span class="tagtag tag">
17 17 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
18 18 <i class="icon-tag"></i>${_ungettext(
19 19 '%(num)s Tag', '%(num)s Tags', len(tags)) % {'num': len(tags)}}</a>
20 20 </span>
21 21
22 22 %if bookmarks:
23 23 <span class="booktag tag">
24 24 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
25 25 <i class="icon-bookmark"></i>${_ungettext(
26 26 '%(num)s Bookmark', '%(num)s Bookmarks', len(bookmarks)) % {'num': len(bookmarks)}}</a>
27 27 </span>
28 28 %endif
29 29 </%def>
30 30
31 31 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
32 32 <% summary = lambda n:{False:'summary-short'}.get(n) %>
33 33
34 34 <div id="summary-menu-stats" class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <div class="breadcrumbs files_location">
37 37 <h4>
38 38 ${breadcrumbs_links}
39 39 </h4>
40 40 </div>
41 41 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
42 42 ${_('Show More')}
43 43 </div>
44 44 </div>
45 45
46 46 <div class="fieldset">
47 47 %if h.is_svn_without_proxy(c.rhodecode_db_repo):
48 48 <div class="left-label disabled">
49 49 ${_('Read-only url')}:
50 50 </div>
51 51 <div class="right-content disabled">
52 52 <input type="text" class="input-monospace" id="clone_url" disabled value="${c.clone_repo_url}"/>
53 53 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
54 54
55 55 <input type="text" class="input-monospace" id="clone_url_id" disabled value="${c.clone_repo_url_id}" style="display: none;"/>
56 56 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
57 57
58 58 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
59 59 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
60 60
61 61 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
62 62 </div>
63 63 %else:
64 64 <div class="left-label">
65 65 ${_('Clone url')}:
66 66 </div>
67 67 <div class="right-content">
68 68 <input type="text" class="input-monospace" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
69 69 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
70 70
71 71 <input type="text" class="input-monospace" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}" style="display: none;"/>
72 72 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
73 73
74 74 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
75 75 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
76 76 </div>
77 77 %endif
78 78 </div>
79 79
80 80 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
81 81 <div class="left-label">
82 82 ${_('Description')}:
83 83 </div>
84 84 <div class="right-content">
85 %if c.visual.stylify_metatags:
86 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.escaped_stylize(c.rhodecode_db_repo.description))}</div>
87 %else:
88 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.html_escape(c.rhodecode_db_repo.description))}</div>
89 %endif
85 <div class="input ${summary(c.show_stats)}">
86 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
87 ${dt.repo_desc(c.rhodecode_db_repo.description_safe, c.visual.stylify_metatags)}
88 </div>
90 89 </div>
91 90 </div>
92 91
93 92 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
94 93 <div class="left-label">
95 94 ${_('Information')}:
96 95 </div>
97 96 <div class="right-content">
98 97
99 98 <div class="repo-size">
100 99 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
101 100
102 101 ## commits
103 102 % if commit_rev == -1:
104 103 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}},
105 104 % else:
106 105 <a href="${h.route_path('repo_changelog', repo_name=c.repo_name)}">
107 106 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>,
108 107 % endif
109 108
110 109 ## forks
111 110 <a title="${_('Number of Repository Forks')}" href="${h.route_path('repo_forks_show_all', repo_name=c.repo_name)}">
112 111 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>,
113 112
114 113 ## repo size
115 114 % if commit_rev == -1:
116 115 <span class="stats-bullet">0 B</span>
117 116 % else:
118 117 <span class="stats-bullet" id="repo_size_container">
119 118 ${_('Calculating Repository Size...')}
120 119 </span>
121 120 % endif
122 121 </div>
123 122
124 123 <div class="commit-info">
125 124 <div class="tags">
126 125 % if c.rhodecode_repo:
127 126 ${refs_counters(
128 127 c.rhodecode_repo.branches,
129 128 c.rhodecode_repo.branches_closed,
130 129 c.rhodecode_repo.tags,
131 130 c.rhodecode_repo.bookmarks)}
132 131 % else:
133 132 ## missing requirements can make c.rhodecode_repo None
134 133 ${refs_counters([], [], [], [])}
135 134 % endif
136 135 </div>
137 136 </div>
138 137
139 138 </div>
140 139 </div>
141 140
142 141 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
143 142 <div class="left-label">
144 143 ${_('Statistics')}:
145 144 </div>
146 145 <div class="right-content">
147 146 <div class="input ${summary(c.show_stats)} statistics">
148 147 % if c.show_stats:
149 148 <div id="lang_stats" class="enabled">
150 149 ${_('Calculating Code Statistics...')}
151 150 </div>
152 151 % else:
153 152 <span class="disabled">
154 153 ${_('Statistics are disabled for this repository')}
155 154 </span>
156 155 % if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
157 156 , ${h.link_to(_('enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_statistics'))}
158 157 % endif
159 158 % endif
160 159 </div>
161 160
162 161 </div>
163 162 </div>
164 163
165 164 % if show_downloads:
166 165 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
167 166 <div class="left-label">
168 167 ${_('Downloads')}:
169 168 </div>
170 169 <div class="right-content">
171 170 <div class="input ${summary(c.show_stats)} downloads">
172 171 % if c.rhodecode_repo and len(c.rhodecode_repo.revisions) == 0:
173 172 <span class="disabled">
174 173 ${_('There are no downloads yet')}
175 174 </span>
176 175 % elif not c.enable_downloads:
177 176 <span class="disabled">
178 177 ${_('Downloads are disabled for this repository')}
179 178 </span>
180 179 % if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
181 180 , ${h.link_to(_('enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
182 181 % endif
183 182 % else:
184 183 <span class="enabled">
185 184 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
186 185 <i class="icon-archive"></i> tip.zip
187 186 ## replaced by some JS on select
188 187 </a>
189 188 </span>
190 189 ${h.hidden('download_options')}
191 190 % endif
192 191 </div>
193 192 </div>
194 193 </div>
195 194 % endif
196 195
197 196 </div><!--end summary-detail-->
198 197 </%def>
199 198
200 199 <%def name="summary_stats(gravatar_function)">
201 200 <div class="sidebar-right">
202 201 <div class="summary-detail-header">
203 202 <h4 class="item">
204 203 ${_('Owner')}
205 204 </h4>
206 205 </div>
207 206 <div class="sidebar-right-content">
208 207 ${gravatar_function(c.rhodecode_db_repo.user.email, 16)}
209 208 </div>
210 209 </div><!--end sidebar-right-->
211 210 </%def>
@@ -1,540 +1,624 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Package for testing various lib/helper functions in rhodecode
24 24 """
25 25
26 26 import datetime
27 27 import string
28 28 import mock
29 29 import pytest
30 30
31 31 from rhodecode.tests import no_newline_id_generator
32 32 from rhodecode.tests.utils import run_test_concurrently
33 33 from rhodecode.lib.helpers import InitialsGravatar
34 34
35 35 from rhodecode.lib.utils2 import AttributeDict
36 36 from rhodecode.model.db import Repository
37 37
38 38
39 39 def _urls_for_proto(proto):
40 40 return [
41 41 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
42 42 '%s://127.0.0.1' % proto),
43 43 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
44 44 '%s://127.0.0.1' % proto),
45 45 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
46 46 '%s://127.0.0.1' % proto),
47 47 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
48 48 '%s://127.0.0.1:8080' % proto),
49 49 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
50 50 '%s://domain.org' % proto),
51 51 ('%s://user:pass@domain.org:8080' % proto,
52 52 ['%s://' % proto, 'domain.org', '8080'],
53 53 '%s://domain.org:8080' % proto),
54 54 ]
55 55
56 56 TEST_URLS = _urls_for_proto('http') + _urls_for_proto('https')
57 57
58 58
59 59 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
60 60 def test_uri_filter(test_url, expected, expected_creds):
61 61 from rhodecode.lib.utils2 import uri_filter
62 62 assert uri_filter(test_url) == expected
63 63
64 64
65 65 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
66 66 def test_credentials_filter(test_url, expected, expected_creds):
67 67 from rhodecode.lib.utils2 import credentials_filter
68 68 assert credentials_filter(test_url) == expected_creds
69 69
70 70
71 71 @pytest.mark.parametrize("str_bool, expected", [
72 72 ('t', True),
73 73 ('true', True),
74 74 ('y', True),
75 75 ('yes', True),
76 76 ('on', True),
77 77 ('1', True),
78 78 ('Y', True),
79 79 ('yeS', True),
80 80 ('Y', True),
81 81 ('TRUE', True),
82 82 ('T', True),
83 83 ('False', False),
84 84 ('F', False),
85 85 ('FALSE', False),
86 86 ('0', False),
87 87 ('-1', False),
88 88 ('', False)
89 89 ])
90 90 def test_str2bool(str_bool, expected):
91 91 from rhodecode.lib.utils2 import str2bool
92 92 assert str2bool(str_bool) == expected
93 93
94 94
95 95 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
96 96 [
97 97 (pref+"", []),
98 98 (pref+"Hi there @marcink", ['marcink']),
99 99 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
100 100 (pref+"Hi there @marcink\n", ['marcink']),
101 101 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
102 102 (pref+"Hi there marcin@rhodecode.com", []),
103 103 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
104 104 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
105 105 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
106 106 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
107 107 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
108 108 (pref+"@john @mary, please review", ["john", "mary"]),
109 109 (pref+"@john,@mary, please review", ["john", "mary"]),
110 110 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
111 111 (pref+"@first hi there @marcink here's my email marcin@email.com "
112 112 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
113 113 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
114 114 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
115 115 (pref+"user.dot hej ! not-needed maril@domain.org", []),
116 116 (pref+"\n@marcin", ['marcin']),
117 117 ]
118 118 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
119 119 def test_mention_extractor(text, expected):
120 120 from rhodecode.lib.utils2 import extract_mentioned_users
121 121 got = extract_mentioned_users(text)
122 122 assert sorted(got, key=lambda x: x.lower()) == got
123 123 assert set(expected) == set(got)
124 124
125 125 @pytest.mark.parametrize("age_args, expected, kw", [
126 126 ({}, u'just now', {}),
127 127 ({'seconds': -1}, u'1 second ago', {}),
128 128 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
129 129 ({'hours': -1}, u'1 hour ago', {}),
130 130 ({'hours': -24}, u'1 day ago', {}),
131 131 ({'hours': -24 * 5}, u'5 days ago', {}),
132 132 ({'months': -1}, u'1 month ago', {}),
133 133 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
134 134 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
135 135 ({}, u'just now', {'short_format': True}),
136 136 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
137 137 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
138 138 ({'hours': -1}, u'1h ago', {'short_format': True}),
139 139 ({'hours': -24}, u'1d ago', {'short_format': True}),
140 140 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
141 141 ({'months': -1}, u'1m ago', {'short_format': True}),
142 142 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
143 143 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
144 144 ])
145 145 def test_age(age_args, expected, kw, pylonsapp):
146 146 from rhodecode.lib.utils2 import age
147 147 from dateutil import relativedelta
148 148 n = datetime.datetime(year=2012, month=5, day=17)
149 149 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
150 150
151 151 def translate(elem):
152 152 return elem.interpolate()
153 153
154 154 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
155 155
156 156
157 157 @pytest.mark.parametrize("age_args, expected, kw", [
158 158 ({}, u'just now', {}),
159 159 ({'seconds': 1}, u'in 1 second', {}),
160 160 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
161 161 ({'hours': 1}, u'in 1 hour', {}),
162 162 ({'hours': 24}, u'in 1 day', {}),
163 163 ({'hours': 24 * 5}, u'in 5 days', {}),
164 164 ({'months': 1}, u'in 1 month', {}),
165 165 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
166 166 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
167 167 ({}, u'just now', {'short_format': True}),
168 168 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
169 169 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
170 170 ({'hours': 1}, u'in 1h', {'short_format': True}),
171 171 ({'hours': 24}, u'in 1d', {'short_format': True}),
172 172 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
173 173 ({'months': 1}, u'in 1m', {'short_format': True}),
174 174 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
175 175 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
176 176 ])
177 177 def test_age_in_future(age_args, expected, kw, pylonsapp):
178 178 from rhodecode.lib.utils2 import age
179 179 from dateutil import relativedelta
180 180 n = datetime.datetime(year=2012, month=5, day=17)
181 181 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
182 182
183 183 def translate(elem):
184 184 return elem.interpolate()
185 185
186 186 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
187 187
188 188
189 def test_tag_exctrator():
190 sample = (
191 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =>>< sa]"
192 "[requires] [stale] [see<>=>] [see => http://url.com]"
193 "[requires => url] [lang => python] [just a tag] <html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
194 "[,d] [ => ULR ] [obsolete] [desc]]"
195 )
196 from rhodecode.lib.helpers import desc_stylize, escaped_stylize
197 res = desc_stylize(sample)
198 assert '<div class="metatag" tag="tag">tag</div>' in res
199 assert '<div class="metatag" tag="obsolete">obsolete</div>' in res
200 assert '<div class="metatag" tag="stale">stale</div>' in res
201 assert '<div class="metatag" tag="lang">python</div>' in res
202 assert '<div class="metatag" tag="requires">requires =&gt; <a href="/url">url</a></div>' in res
203 assert '<div class="metatag" tag="tag">tag</div>' in res
204 assert '<html_tag first=\'abc\' attr=\"my.url?attr=&another=\"></html_tag>' in res
189 @pytest.mark.parametrize("sample, expected_tags", [
190 ((
191 "hello world [stale]"
192 ),
193 [
194 ('state', '[stale]'),
195 ]),
196 # entry
197 ((
198 "hello world [v2.0.0] [v1.0.0]"
199 ),
200 [
201 ('generic', '[v2.0.0]'),
202 ('generic', '[v1.0.0]'),
203 ]),
204 # entry
205 ((
206 "he[ll]o wo[rl]d"
207 ),
208 [
209 ('label', '[ll]'),
210 ('label', '[rl]'),
211 ]),
212 # entry
213 ((
214 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
215 ),
216 [
217 ('state', '[stale]'),
218 ('state', '[featured]'),
219 ('state', '[stale]'),
220 ('state', '[dead]'),
221 ('state', '[dev]'),
222 ]),
223 # entry
224 ((
225 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
226 ),
227 [
228 ('state', '[stale]'),
229 ('url', '[url =&gt; [name](http://rc.com)]'),
230 ]),
231 # entry
232 ((
233 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
234 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
235 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
236 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
237 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
238 ),
239 [
240 ('label', '[desc]'),
241 ('label', '[obsolete]'),
242 ('label', '[or]'),
243 ('label', '[requires]'),
244 ('label', '[tag]'),
245 ('state', '[stale]'),
246 ('lang', '[lang =&gt; python]'),
247 ('ref', '[requires =&gt; url]'),
248 ('see', '[see =&gt; http://url.com]'),
205 249
206 res_encoded = escaped_stylize(sample)
207 assert '<div class="metatag" tag="tag">tag</div>' in res_encoded
208 assert '<div class="metatag" tag="obsolete">obsolete</div>' in res_encoded
209 assert '<div class="metatag" tag="stale">stale</div>' in res_encoded
210 assert '<div class="metatag" tag="lang">python</div>' in res_encoded
211 assert '<div class="metatag" tag="requires">requires =&gt; <a href="/url">url</a></div>' in res_encoded
212 assert '<div class="metatag" tag="tag">tag</div>' in res_encoded
213 assert '&lt;html_tag first=&#39;abc&#39; attr=&#34;my.url?attr=&amp;another=&#34;&gt;&lt;/html_tag&gt;' in res_encoded
250 ]),
251
252 ], ids=no_newline_id_generator)
253 def test_metatag_extraction(sample, expected_tags):
254 from rhodecode.lib.helpers import extract_metatags
255 tags, value = extract_metatags(sample)
256 assert sorted(tags) == sorted(expected_tags)
257
258
259 @pytest.mark.parametrize("tag_data, expected_html", [
260
261 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
262 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
263 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
264 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
265 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
266
267 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
268 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
269
270 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
271 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
272 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
273 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
274 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
275 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
276
277 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
278 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
279 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
280 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
281
282 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires =&gt; <a href="/RepoName">RepoName</a></div>'),
283 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends =&gt; <a href="/GroupName">GroupName</a></div>'),
284 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts =&gt; <a href="/SomeName">SomeName</a></div>'),
285 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base =&gt; <a href="/SomeName">SomeName</a></div>'),
286
287 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see =&gt; http://rhodecode.com </div>'),
288
289 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
290 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
291 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
292
293 ])
294 def test_metatags_stylize(tag_data, expected_html):
295 from rhodecode.lib.helpers import style_metatag
296 tag_type,value = tag_data
297 assert style_metatag(tag_type, value) == expected_html
214 298
215 299
216 300 @pytest.mark.parametrize("tmpl_url, email, expected", [
217 301 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
218 302
219 303 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
220 304 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
221 305
222 306 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
223 307 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
224 308
225 309 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
226 310 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
227 311
228 312 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
229 313 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
230 314 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
231 315 ])
232 316 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
233 317 from rhodecode.lib.helpers import gravatar_url
234 318
235 319 # mock pyramid.threadlocals
236 320 def fake_get_current_request():
237 321 request_stub.scheme = 'https'
238 322 request_stub.host = 'server.com'
239 323 return request_stub
240 324
241 325 # mock pylons.tmpl_context
242 326 def fake_tmpl_context(_url):
243 327 _c = AttributeDict()
244 328 _c.visual = AttributeDict()
245 329 _c.visual.use_gravatar = True
246 330 _c.visual.gravatar_url = _url
247 331
248 332 return _c
249 333
250 334 with mock.patch('rhodecode.lib.helpers.get_current_request',
251 335 fake_get_current_request):
252 336 fake = fake_tmpl_context(_url=tmpl_url)
253 337 with mock.patch('pylons.tmpl_context', fake):
254 338 grav = gravatar_url(email_address=email, size=24)
255 339 assert grav == expected
256 340
257 341
258 342 @pytest.mark.parametrize(
259 343 "email, first_name, last_name, expected_initials, expected_color", [
260 344
261 345 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
262 346 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
263 347 # special cases of email
264 348 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
265 349 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
266 350 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
267 351
268 352 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
269 353 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
270 354
271 355 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
272 356 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
273 357 # partials
274 358 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
275 359 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
276 360 # non-ascii
277 361 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
278 362 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
279 363
280 364 # special cases, LDAP can provide those...
281 365 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
282 366 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
283 367 ('null', '', '', 'NL', '#8c4646'),
284 368 ('some.@abc.com', 'some', '', 'SA', '#664e33')
285 369 ])
286 370 def test_initials_gravatar_pick_of_initials_and_color_algo(
287 371 email, first_name, last_name, expected_initials, expected_color):
288 372 instance = InitialsGravatar(email, first_name, last_name)
289 373 assert instance.get_initials() == expected_initials
290 374 assert instance.str2color(email) == expected_color
291 375
292 376
293 377 def test_initials_gravatar_mapping_algo():
294 378 pos = set()
295 379 instance = InitialsGravatar('', '', '')
296 380 iterations = 0
297 381
298 382 variations = []
299 383 for letter1 in string.ascii_letters:
300 384 for letter2 in string.ascii_letters[::-1][:10]:
301 385 for letter3 in string.ascii_letters[:10]:
302 386 variations.append(
303 387 '%s@rhodecode.com' % (letter1+letter2+letter3))
304 388
305 389 max_variations = 4096
306 390 for email in variations[:max_variations]:
307 391 iterations += 1
308 392 pos.add(
309 393 instance.pick_color_bank_index(email,
310 394 instance.get_color_bank()))
311 395
312 396 # we assume that we have match all 256 possible positions,
313 397 # in reasonable amount of different email addresses
314 398 assert len(pos) == 256
315 399 assert iterations == max_variations
316 400
317 401
318 402 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
319 403 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
320 404 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
321 405 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
322 406 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
323 407 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
324 408 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
325 409 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
326 410 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
327 411 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
328 412 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
329 413 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
330 414 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
331 415 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
332 416 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
333 417 ])
334 418 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
335 419 from rhodecode.lib.utils2 import get_clone_url
336 420
337 421 class RequestStub(object):
338 422 def request_url(self, name):
339 423 return 'http://vps1:8000' + prefix
340 424
341 425 def route_url(self, name):
342 426 return self.request_url(name)
343 427
344 428 clone_url = get_clone_url(
345 429 request=RequestStub(),
346 430 uri_tmpl=tmpl,
347 431 repo_name=repo_name, repo_id=23, **overrides)
348 432 assert clone_url == expected
349 433
350 434
351 435 def _quick_url(text, tmpl="""<a class="revision-link" href="%s">%s</a>""", url_=None):
352 436 """
353 437 Changes `some text url[foo]` => `some text <a href="/">foo</a>
354 438
355 439 :param text:
356 440 """
357 441 import re
358 442 # quickly change expected url[] into a link
359 443 URL_PAT = re.compile(r'(?:url\[)(.+?)(?:\])')
360 444
361 445 def url_func(match_obj):
362 446 _url = match_obj.groups()[0]
363 447 return tmpl % (url_ or '/some-url', _url)
364 448 return URL_PAT.sub(url_func, text)
365 449
366 450
367 451 @pytest.mark.parametrize("sample, expected", [
368 452 ("",
369 453 ""),
370 454 ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
371 455 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68"),
372 456 ("from rev 000000000000",
373 457 "from rev url[000000000000]"),
374 458 ("from rev 000000000000123123 also rev 000000000000",
375 459 "from rev url[000000000000123123] also rev url[000000000000]"),
376 460 ("this should-000 00",
377 461 "this should-000 00"),
378 462 ("longtextffffffffff rev 123123123123",
379 463 "longtextffffffffff rev url[123123123123]"),
380 464 ("rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
381 465 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff"),
382 466 ("ffffffffffff some text traalaa",
383 467 "url[ffffffffffff] some text traalaa"),
384 468 ("""Multi line
385 469 123123123123
386 470 some text 123123123123
387 471 sometimes !
388 472 """,
389 473 """Multi line
390 474 url[123123123123]
391 475 some text url[123123123123]
392 476 sometimes !
393 477 """)
394 478 ], ids=no_newline_id_generator)
395 479 def test_urlify_commits(sample, expected):
396 480 def fake_url(self, *args, **kwargs):
397 481 return '/some-url'
398 482
399 483 expected = _quick_url(expected)
400 484
401 485 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
402 486 from rhodecode.lib.helpers import urlify_commits
403 487 assert urlify_commits(sample, 'repo_name') == expected
404 488
405 489
406 490 @pytest.mark.parametrize("sample, expected, url_", [
407 491 ("",
408 492 "",
409 493 ""),
410 494 ("https://svn.apache.org/repos",
411 495 "url[https://svn.apache.org/repos]",
412 496 "https://svn.apache.org/repos"),
413 497 ("http://svn.apache.org/repos",
414 498 "url[http://svn.apache.org/repos]",
415 499 "http://svn.apache.org/repos"),
416 500 ("from rev a also rev http://google.com",
417 501 "from rev a also rev url[http://google.com]",
418 502 "http://google.com"),
419 503 ("""Multi line
420 504 https://foo.bar.com
421 505 some text lalala""",
422 506 """Multi line
423 507 url[https://foo.bar.com]
424 508 some text lalala""",
425 509 "https://foo.bar.com")
426 510 ], ids=no_newline_id_generator)
427 511 def test_urlify_test(sample, expected, url_):
428 512 from rhodecode.lib.helpers import urlify_text
429 513 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
430 514 assert urlify_text(sample) == expected
431 515
432 516
433 517 @pytest.mark.parametrize("test, expected", [
434 518 ("", None),
435 519 ("/_2", '2'),
436 520 ("_2", '2'),
437 521 ("/_2/", '2'),
438 522 ("_2/", '2'),
439 523
440 524 ("/_21", '21'),
441 525 ("_21", '21'),
442 526 ("/_21/", '21'),
443 527 ("_21/", '21'),
444 528
445 529 ("/_21/foobar", '21'),
446 530 ("_21/121", '21'),
447 531 ("/_21/_12", '21'),
448 532 ("_21/rc/foo", '21'),
449 533
450 534 ])
451 535 def test_get_repo_by_id(test, expected):
452 536 from rhodecode.model.repo import RepoModel
453 537 _test = RepoModel()._extract_id_from_repo_name(test)
454 538 assert _test == expected
455 539
456 540
457 541 @pytest.mark.parametrize("test_repo_name, repo_type", [
458 542 ("test_repo_1", None),
459 543 ("repo_group/foobar", None),
460 544 ("test_non_asci_Δ…Δ‡Δ™", None),
461 545 (u"test_non_asci_unicode_Δ…Δ‡Δ™", None),
462 546 ])
463 547 def test_invalidation_context(pylonsapp, test_repo_name, repo_type):
464 548 from beaker.cache import cache_region
465 549 from rhodecode.lib import caches
466 550 from rhodecode.model.db import CacheKey
467 551
468 552 @cache_region('long_term')
469 553 def _dummy_func(cache_key):
470 554 return 'result'
471 555
472 556 invalidator_context = CacheKey.repo_context_cache(
473 557 _dummy_func, test_repo_name, 'repo')
474 558
475 559 with invalidator_context as context:
476 560 invalidated = context.invalidate()
477 561 result = context.compute()
478 562
479 563 assert invalidated == True
480 564 assert 'result' == result
481 565 assert isinstance(context, caches.FreshRegionCache)
482 566
483 567 assert 'InvalidationContext' in repr(invalidator_context)
484 568
485 569 with invalidator_context as context:
486 570 context.invalidate()
487 571 result = context.compute()
488 572
489 573 assert 'result' == result
490 574 assert isinstance(context, caches.ActiveRegionCache)
491 575
492 576
493 577 def test_invalidation_context_exception_in_compute(pylonsapp):
494 578 from rhodecode.model.db import CacheKey
495 579 from beaker.cache import cache_region
496 580
497 581 @cache_region('long_term')
498 582 def _dummy_func(cache_key):
499 583 # this causes error since it doesn't get any params
500 584 raise Exception('ups')
501 585
502 586 invalidator_context = CacheKey.repo_context_cache(
503 587 _dummy_func, 'test_repo_2', 'repo')
504 588
505 589 with pytest.raises(Exception):
506 590 with invalidator_context as context:
507 591 context.invalidate()
508 592 context.compute()
509 593
510 594
511 595 @pytest.mark.parametrize('execution_number', range(5))
512 596 def test_cache_invalidation_race_condition(execution_number, pylonsapp):
513 597 import time
514 598 from beaker.cache import cache_region
515 599 from rhodecode.model.db import CacheKey
516 600
517 601 if CacheKey.metadata.bind.url.get_backend_name() == "mysql":
518 602 reason = (
519 603 'Fails on MariaDB due to some locking issues. Investigation'
520 604 ' needed')
521 605 pytest.xfail(reason=reason)
522 606
523 607 @run_test_concurrently(25)
524 608 def test_create_and_delete_cache_keys():
525 609 time.sleep(0.2)
526 610
527 611 @cache_region('long_term')
528 612 def _dummy_func(cache_key):
529 613 return 'result'
530 614
531 615 invalidator_context = CacheKey.repo_context_cache(
532 616 _dummy_func, 'test_repo_1', 'repo')
533 617
534 618 with invalidator_context as context:
535 619 context.invalidate()
536 620 context.compute()
537 621
538 622 CacheKey.set_invalidate('test_repo_1', delete=True)
539 623
540 624 test_create_and_delete_cache_keys()
General Comments 0
You need to be logged in to leave comments. Login now