##// END OF EJS Templates
patterns: enabled global !NUM match for new style pull-request references.
dan -
r4041:db4eefe4 default
parent child Browse files
Show More
@@ -1,743 +1,742 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 mock
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.lib.utils2 import md5
27 27 from rhodecode.model.db import RhodeCodeUi
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 30 from rhodecode.tests import assert_session_flash
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42
43 43 'admin_settings':
44 44 ADMIN_PREFIX +'/settings',
45 45 'admin_settings_update':
46 46 ADMIN_PREFIX + '/settings/update',
47 47 'admin_settings_global':
48 48 ADMIN_PREFIX + '/settings/global',
49 49 'admin_settings_global_update':
50 50 ADMIN_PREFIX + '/settings/global/update',
51 51 'admin_settings_vcs':
52 52 ADMIN_PREFIX + '/settings/vcs',
53 53 'admin_settings_vcs_update':
54 54 ADMIN_PREFIX + '/settings/vcs/update',
55 55 'admin_settings_vcs_svn_pattern_delete':
56 56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 57 'admin_settings_mapping':
58 58 ADMIN_PREFIX + '/settings/mapping',
59 59 'admin_settings_mapping_update':
60 60 ADMIN_PREFIX + '/settings/mapping/update',
61 61 'admin_settings_visual':
62 62 ADMIN_PREFIX + '/settings/visual',
63 63 'admin_settings_visual_update':
64 64 ADMIN_PREFIX + '/settings/visual/update',
65 65 'admin_settings_issuetracker':
66 66 ADMIN_PREFIX + '/settings/issue-tracker',
67 67 'admin_settings_issuetracker_update':
68 68 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 69 'admin_settings_issuetracker_test':
70 70 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 71 'admin_settings_issuetracker_delete':
72 72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 73 'admin_settings_email':
74 74 ADMIN_PREFIX + '/settings/email',
75 75 'admin_settings_email_update':
76 76 ADMIN_PREFIX + '/settings/email/update',
77 77 'admin_settings_hooks':
78 78 ADMIN_PREFIX + '/settings/hooks',
79 79 'admin_settings_hooks_update':
80 80 ADMIN_PREFIX + '/settings/hooks/update',
81 81 'admin_settings_hooks_delete':
82 82 ADMIN_PREFIX + '/settings/hooks/delete',
83 83 'admin_settings_search':
84 84 ADMIN_PREFIX + '/settings/search',
85 85 'admin_settings_labs':
86 86 ADMIN_PREFIX + '/settings/labs',
87 87 'admin_settings_labs_update':
88 88 ADMIN_PREFIX + '/settings/labs/update',
89 89
90 90 'admin_settings_sessions':
91 91 ADMIN_PREFIX + '/settings/sessions',
92 92 'admin_settings_sessions_cleanup':
93 93 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 94 'admin_settings_system':
95 95 ADMIN_PREFIX + '/settings/system',
96 96 'admin_settings_system_update':
97 97 ADMIN_PREFIX + '/settings/system/updates',
98 98 'admin_settings_open_source':
99 99 ADMIN_PREFIX + '/settings/open_source',
100 100
101 101
102 102 }[name].format(**kwargs)
103 103
104 104 if params:
105 105 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
106 106 return base_url
107 107
108 108
109 109 @pytest.mark.usefixtures('autologin_user', 'app')
110 110 class TestAdminSettingsController(object):
111 111
112 112 @pytest.mark.parametrize('urlname', [
113 113 'admin_settings_vcs',
114 114 'admin_settings_mapping',
115 115 'admin_settings_global',
116 116 'admin_settings_visual',
117 117 'admin_settings_email',
118 118 'admin_settings_hooks',
119 119 'admin_settings_search',
120 120 ])
121 121 def test_simple_get(self, urlname):
122 122 self.app.get(route_path(urlname))
123 123
124 124 def test_create_custom_hook(self, csrf_token):
125 125 response = self.app.post(
126 126 route_path('admin_settings_hooks_update'),
127 127 params={
128 128 'new_hook_ui_key': 'test_hooks_1',
129 129 'new_hook_ui_value': 'cd /tmp',
130 130 'csrf_token': csrf_token})
131 131
132 132 response = response.follow()
133 133 response.mustcontain('test_hooks_1')
134 134 response.mustcontain('cd /tmp')
135 135
136 136 def test_create_custom_hook_delete(self, csrf_token):
137 137 response = self.app.post(
138 138 route_path('admin_settings_hooks_update'),
139 139 params={
140 140 'new_hook_ui_key': 'test_hooks_2',
141 141 'new_hook_ui_value': 'cd /tmp2',
142 142 'csrf_token': csrf_token})
143 143
144 144 response = response.follow()
145 145 response.mustcontain('test_hooks_2')
146 146 response.mustcontain('cd /tmp2')
147 147
148 148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149 149
150 150 # delete
151 151 self.app.post(
152 152 route_path('admin_settings_hooks_delete'),
153 153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 154 response = self.app.get(route_path('admin_settings_hooks'))
155 155 response.mustcontain(no=['test_hooks_2'])
156 156 response.mustcontain(no=['cd /tmp2'])
157 157
158 158
159 159 @pytest.mark.usefixtures('autologin_user', 'app')
160 160 class TestAdminSettingsGlobal(object):
161 161
162 162 def test_pre_post_code_code_active(self, csrf_token):
163 163 pre_code = 'rc-pre-code-187652122'
164 164 post_code = 'rc-postcode-98165231'
165 165
166 166 response = self.post_and_verify_settings({
167 167 'rhodecode_pre_code': pre_code,
168 168 'rhodecode_post_code': post_code,
169 169 'csrf_token': csrf_token,
170 170 })
171 171
172 172 response = response.follow()
173 173 response.mustcontain(pre_code, post_code)
174 174
175 175 def test_pre_post_code_code_inactive(self, csrf_token):
176 176 pre_code = 'rc-pre-code-187652122'
177 177 post_code = 'rc-postcode-98165231'
178 178 response = self.post_and_verify_settings({
179 179 'rhodecode_pre_code': '',
180 180 'rhodecode_post_code': '',
181 181 'csrf_token': csrf_token,
182 182 })
183 183
184 184 response = response.follow()
185 185 response.mustcontain(no=[pre_code, post_code])
186 186
187 187 def test_captcha_activate(self, csrf_token):
188 188 self.post_and_verify_settings({
189 189 'rhodecode_captcha_private_key': '1234567890',
190 190 'rhodecode_captcha_public_key': '1234567890',
191 191 'csrf_token': csrf_token,
192 192 })
193 193
194 194 response = self.app.get(ADMIN_PREFIX + '/register')
195 195 response.mustcontain('captcha')
196 196
197 197 def test_captcha_deactivate(self, csrf_token):
198 198 self.post_and_verify_settings({
199 199 'rhodecode_captcha_private_key': '',
200 200 'rhodecode_captcha_public_key': '1234567890',
201 201 'csrf_token': csrf_token,
202 202 })
203 203
204 204 response = self.app.get(ADMIN_PREFIX + '/register')
205 205 response.mustcontain(no=['captcha'])
206 206
207 207 def test_title_change(self, csrf_token):
208 208 old_title = 'RhodeCode'
209 209
210 210 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
211 211 response = self.post_and_verify_settings({
212 212 'rhodecode_title': new_title,
213 213 'csrf_token': csrf_token,
214 214 })
215 215
216 216 response = response.follow()
217 217 response.mustcontain(new_title)
218 218
219 219 def post_and_verify_settings(self, settings):
220 220 old_title = 'RhodeCode'
221 221 old_realm = 'RhodeCode authentication'
222 222 params = {
223 223 'rhodecode_title': old_title,
224 224 'rhodecode_realm': old_realm,
225 225 'rhodecode_pre_code': '',
226 226 'rhodecode_post_code': '',
227 227 'rhodecode_captcha_private_key': '',
228 228 'rhodecode_captcha_public_key': '',
229 229 'rhodecode_create_personal_repo_group': False,
230 230 'rhodecode_personal_repo_group_pattern': '${username}',
231 231 }
232 232 params.update(settings)
233 233 response = self.app.post(
234 234 route_path('admin_settings_global_update'), params=params)
235 235
236 236 assert_session_flash(response, 'Updated application settings')
237 237 app_settings = SettingsModel().get_all_settings()
238 238 del settings['csrf_token']
239 239 for key, value in settings.iteritems():
240 240 assert app_settings[key] == value.decode('utf-8')
241 241
242 242 return response
243 243
244 244
245 245 @pytest.mark.usefixtures('autologin_user', 'app')
246 246 class TestAdminSettingsVcs(object):
247 247
248 248 def test_contains_svn_default_patterns(self):
249 249 response = self.app.get(route_path('admin_settings_vcs'))
250 250 expected_patterns = [
251 251 '/trunk',
252 252 '/branches/*',
253 253 '/tags/*',
254 254 ]
255 255 for pattern in expected_patterns:
256 256 response.mustcontain(pattern)
257 257
258 258 def test_add_new_svn_branch_and_tag_pattern(
259 259 self, backend_svn, form_defaults, disable_sql_cache,
260 260 csrf_token):
261 261 form_defaults.update({
262 262 'new_svn_branch': '/exp/branches/*',
263 263 'new_svn_tag': '/important_tags/*',
264 264 'csrf_token': csrf_token,
265 265 })
266 266
267 267 response = self.app.post(
268 268 route_path('admin_settings_vcs_update'),
269 269 params=form_defaults, status=302)
270 270 response = response.follow()
271 271
272 272 # Expect to find the new values on the page
273 273 response.mustcontain('/exp/branches/*')
274 274 response.mustcontain('/important_tags/*')
275 275
276 276 # Expect that those patterns are used to match branches and tags now
277 277 repo = backend_svn['svn-simple-layout'].scm_instance()
278 278 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 279 assert 'important_tags/v0.5' in repo.tags
280 280
281 281 def test_add_same_svn_value_twice_shows_an_error_message(
282 282 self, form_defaults, csrf_token, settings_util):
283 283 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 284 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285 285
286 286 response = self.app.post(
287 287 route_path('admin_settings_vcs_update'),
288 288 params={
289 289 'paths_root_path': form_defaults['paths_root_path'],
290 290 'new_svn_branch': '/test',
291 291 'new_svn_tag': '/test',
292 292 'csrf_token': csrf_token,
293 293 },
294 294 status=200)
295 295
296 296 response.mustcontain("Pattern already exists")
297 297 response.mustcontain("Some form inputs contain invalid data.")
298 298
299 299 @pytest.mark.parametrize('section', [
300 300 'vcs_svn_branch',
301 301 'vcs_svn_tag',
302 302 ])
303 303 def test_delete_svn_patterns(
304 304 self, section, csrf_token, settings_util):
305 305 setting = settings_util.create_rhodecode_ui(
306 306 section, '/test_delete', cleanup=False)
307 307
308 308 self.app.post(
309 309 route_path('admin_settings_vcs_svn_pattern_delete'),
310 310 params={
311 311 'delete_svn_pattern': setting.ui_id,
312 312 'csrf_token': csrf_token},
313 313 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314 314
315 315 @pytest.mark.parametrize('section', [
316 316 'vcs_svn_branch',
317 317 'vcs_svn_tag',
318 318 ])
319 319 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 320 self, section, csrf_token, settings_util):
321 321 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322 322
323 323 self.app.post(
324 324 route_path('admin_settings_vcs_svn_pattern_delete'),
325 325 params={
326 326 'delete_svn_pattern': setting.ui_id,
327 327 'csrf_token': csrf_token},
328 328 status=404)
329 329
330 330 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 331 form_defaults.update({
332 332 'csrf_token': csrf_token,
333 333 'extensions_hgsubversion': 'True',
334 334 })
335 335 response = self.app.post(
336 336 route_path('admin_settings_vcs_update'),
337 337 params=form_defaults,
338 338 status=302)
339 339
340 340 response = response.follow()
341 341 extensions_input = (
342 342 '<input id="extensions_hgsubversion" '
343 343 'name="extensions_hgsubversion" type="checkbox" '
344 344 'value="True" checked="checked" />')
345 345 response.mustcontain(extensions_input)
346 346
347 347 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 348 form_defaults.update({
349 349 'csrf_token': csrf_token,
350 350 'extensions_evolve': 'True',
351 351 })
352 352 response = self.app.post(
353 353 route_path('admin_settings_vcs_update'),
354 354 params=form_defaults,
355 355 status=302)
356 356
357 357 response = response.follow()
358 358 extensions_input = (
359 359 '<input id="extensions_evolve" '
360 360 'name="extensions_evolve" type="checkbox" '
361 361 'value="True" checked="checked" />')
362 362 response.mustcontain(extensions_input)
363 363
364 364 def test_has_a_section_for_pull_request_settings(self):
365 365 response = self.app.get(route_path('admin_settings_vcs'))
366 366 response.mustcontain('Pull Request Settings')
367 367
368 368 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 369 response = self.app.get(route_path('admin_settings_vcs'))
370 370 assert_response = response.assert_response()
371 371 assert_response.one_element_exists(
372 372 '[name=rhodecode_use_outdated_comments]')
373 373
374 374 @pytest.mark.parametrize('new_value', [True, False])
375 375 def test_allows_to_change_invalidation_of_inline_comments(
376 376 self, form_defaults, csrf_token, new_value):
377 377 setting_key = 'use_outdated_comments'
378 378 setting = SettingsModel().create_or_update_setting(
379 379 setting_key, not new_value, 'bool')
380 380 Session().add(setting)
381 381 Session().commit()
382 382
383 383 form_defaults.update({
384 384 'csrf_token': csrf_token,
385 385 'rhodecode_use_outdated_comments': str(new_value),
386 386 })
387 387 response = self.app.post(
388 388 route_path('admin_settings_vcs_update'),
389 389 params=form_defaults,
390 390 status=302)
391 391 response = response.follow()
392 392 setting = SettingsModel().get_setting_by_name(setting_key)
393 393 assert setting.app_settings_value is new_value
394 394
395 395 @pytest.mark.parametrize('new_value', [True, False])
396 396 def test_allows_to_change_hg_rebase_merge_strategy(
397 397 self, form_defaults, csrf_token, new_value):
398 398 setting_key = 'hg_use_rebase_for_merging'
399 399
400 400 form_defaults.update({
401 401 'csrf_token': csrf_token,
402 402 'rhodecode_' + setting_key: str(new_value),
403 403 })
404 404
405 405 with mock.patch.dict(
406 406 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 407 self.app.post(
408 408 route_path('admin_settings_vcs_update'),
409 409 params=form_defaults,
410 410 status=302)
411 411
412 412 setting = SettingsModel().get_setting_by_name(setting_key)
413 413 assert setting.app_settings_value is new_value
414 414
415 415 @pytest.fixture()
416 416 def disable_sql_cache(self, request):
417 417 patcher = mock.patch(
418 418 'rhodecode.lib.caching_query.FromCache.process_query')
419 419 request.addfinalizer(patcher.stop)
420 420 patcher.start()
421 421
422 422 @pytest.fixture()
423 423 def form_defaults(self):
424 424 from rhodecode.apps.admin.views.settings import AdminSettingsView
425 425 return AdminSettingsView._form_defaults()
426 426
427 427 # TODO: johbo: What we really want is to checkpoint before a test run and
428 428 # reset the session afterwards.
429 429 @pytest.fixture(scope='class', autouse=True)
430 430 def cleanup_settings(self, request, baseapp):
431 431 ui_id = RhodeCodeUi.ui_id
432 432 original_ids = list(
433 433 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434 434
435 435 @request.addfinalizer
436 436 def cleanup():
437 437 RhodeCodeUi.query().filter(
438 438 ui_id.notin_(original_ids)).delete(False)
439 439
440 440
441 441 @pytest.mark.usefixtures('autologin_user', 'app')
442 442 class TestLabsSettings(object):
443 443 def test_get_settings_page_disabled(self):
444 444 with mock.patch.dict(
445 445 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446 446
447 447 response = self.app.get(
448 448 route_path('admin_settings_labs'), status=302)
449 449
450 450 assert response.location.endswith(route_path('admin_settings'))
451 451
452 452 def test_get_settings_page_enabled(self):
453 453 from rhodecode.apps.admin.views import settings
454 454 lab_settings = [
455 455 settings.LabSetting(
456 456 key='rhodecode_bool',
457 457 type='bool',
458 458 group='bool group',
459 459 label='bool label',
460 460 help='bool help'
461 461 ),
462 462 settings.LabSetting(
463 463 key='rhodecode_text',
464 464 type='unicode',
465 465 group='text group',
466 466 label='text label',
467 467 help='text help'
468 468 ),
469 469 ]
470 470 with mock.patch.dict(rhodecode.CONFIG,
471 471 {'labs_settings_active': 'true'}):
472 472 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 473 response = self.app.get(route_path('admin_settings_labs'))
474 474
475 475 assert '<label>bool group:</label>' in response
476 476 assert '<label for="rhodecode_bool">bool label</label>' in response
477 477 assert '<p class="help-block">bool help</p>' in response
478 478 assert 'name="rhodecode_bool" type="checkbox"' in response
479 479
480 480 assert '<label>text group:</label>' in response
481 481 assert '<label for="rhodecode_text">text label</label>' in response
482 482 assert '<p class="help-block">text help</p>' in response
483 483 assert 'name="rhodecode_text" size="60" type="text"' in response
484 484
485 485
486 486 @pytest.mark.usefixtures('app')
487 487 class TestOpenSourceLicenses(object):
488 488
489 489 def test_records_are_displayed(self, autologin_user):
490 490 sample_licenses = [
491 491 {
492 492 "license": [
493 493 {
494 494 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 495 "shortName": "bsdOriginal",
496 496 "spdxId": "BSD-4-Clause",
497 497 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 498 }
499 499 ],
500 500 "name": "python2.7-coverage-3.7.1"
501 501 },
502 502 {
503 503 "license": [
504 504 {
505 505 "fullName": "MIT License",
506 506 "shortName": "mit",
507 507 "spdxId": "MIT",
508 508 "url": "http://spdx.org/licenses/MIT.html"
509 509 }
510 510 ],
511 511 "name": "python2.7-bootstrapped-pip-9.0.1"
512 512 },
513 513 ]
514 514 read_licenses_patch = mock.patch(
515 515 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 516 return_value=sample_licenses)
517 517 with read_licenses_patch:
518 518 response = self.app.get(
519 519 route_path('admin_settings_open_source'), status=200)
520 520
521 521 assert_response = response.assert_response()
522 522 assert_response.element_contains(
523 523 '.panel-heading', 'Licenses of Third Party Packages')
524 524 for license_data in sample_licenses:
525 525 response.mustcontain(license_data["license"][0]["spdxId"])
526 526 assert_response.element_contains('.panel-body', license_data["name"])
527 527
528 528 def test_records_can_be_read(self, autologin_user):
529 529 response = self.app.get(
530 530 route_path('admin_settings_open_source'), status=200)
531 531 assert_response = response.assert_response()
532 532 assert_response.element_contains(
533 533 '.panel-heading', 'Licenses of Third Party Packages')
534 534
535 535 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 536 self.app.get(
537 537 route_path('admin_settings_open_source'), status=404)
538 538
539 539
540 540 @pytest.mark.usefixtures('app')
541 541 class TestUserSessions(object):
542 542
543 543 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 544 self.app.get(route_path('admin_settings_sessions'), status=404)
545 545
546 546 def test_show_sessions_page(self, autologin_user):
547 547 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 548 response.mustcontain('file')
549 549
550 550 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551 551
552 552 post_data = {
553 553 'csrf_token': csrf_token,
554 554 'expire_days': '60'
555 555 }
556 556 response = self.app.post(
557 557 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 558 status=302)
559 559 assert_session_flash(response, 'Cleaned up old sessions')
560 560
561 561
562 562 @pytest.mark.usefixtures('app')
563 563 class TestAdminSystemInfo(object):
564 564
565 565 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 566 self.app.get(route_path('admin_settings_system'), status=404)
567 567
568 568 def test_system_info_page(self, autologin_user):
569 569 response = self.app.get(route_path('admin_settings_system'))
570 570 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 571 rhodecode.__version__))
572 572
573 573 def test_system_update_new_version(self, autologin_user):
574 574 update_data = {
575 575 'versions': [
576 576 {
577 577 'version': '100.3.1415926535',
578 578 'general': 'The latest version we are ever going to ship'
579 579 },
580 580 {
581 581 'version': '0.0.0',
582 582 'general': 'The first version we ever shipped'
583 583 }
584 584 ]
585 585 }
586 586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 587 response = self.app.get(route_path('admin_settings_system_update'))
588 588 response.mustcontain('A <b>new version</b> is available')
589 589
590 590 def test_system_update_nothing_new(self, autologin_user):
591 591 update_data = {
592 592 'versions': [
593 593 {
594 594 'version': '0.0.0',
595 595 'general': 'The first version we ever shipped'
596 596 }
597 597 ]
598 598 }
599 599 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 600 response = self.app.get(route_path('admin_settings_system_update'))
601 601 response.mustcontain(
602 602 'This instance is already running the <b>latest</b> stable version')
603 603
604 604 def test_system_update_bad_response(self, autologin_user):
605 605 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 606 response = self.app.get(route_path('admin_settings_system_update'))
607 607 response.mustcontain(
608 608 'Bad data sent from update server')
609 609
610 610
611 611 @pytest.mark.usefixtures("app")
612 612 class TestAdminSettingsIssueTracker(object):
613 613 RC_PREFIX = 'rhodecode_'
614 614 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 615 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 616
617 617 def test_issuetracker_index(self, autologin_user):
618 618 response = self.app.get(route_path('admin_settings_issuetracker'))
619 619 assert response.status_code == 200
620 620
621 621 def test_add_empty_issuetracker_pattern(
622 622 self, request, autologin_user, csrf_token):
623 623 post_url = route_path('admin_settings_issuetracker_update')
624 624 post_data = {
625 625 'csrf_token': csrf_token
626 626 }
627 627 self.app.post(post_url, post_data, status=302)
628 628
629 629 def test_add_issuetracker_pattern(
630 630 self, request, autologin_user, csrf_token):
631 631 pattern = 'issuetracker_pat'
632 632 another_pattern = pattern+'1'
633 633 post_url = route_path('admin_settings_issuetracker_update')
634 634 post_data = {
635 635 'new_pattern_pattern_0': pattern,
636 636 'new_pattern_url_0': 'http://url',
637 637 'new_pattern_prefix_0': 'prefix',
638 638 'new_pattern_description_0': 'description',
639 639 'new_pattern_pattern_1': another_pattern,
640 640 'new_pattern_url_1': 'https://url1',
641 641 'new_pattern_prefix_1': 'prefix1',
642 642 'new_pattern_description_1': 'description1',
643 643 'csrf_token': csrf_token
644 644 }
645 645 self.app.post(post_url, post_data, status=302)
646 646 settings = SettingsModel().get_all_settings()
647 647 self.uid = md5(pattern)
648 648 assert settings[self.PATTERN_KEY+self.uid] == pattern
649 649 self.another_uid = md5(another_pattern)
650 650 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
651 651
652 652 @request.addfinalizer
653 653 def cleanup():
654 654 defaults = SettingsModel().get_all_settings()
655 655
656 656 entries = [name for name in defaults if (
657 657 (self.uid in name) or (self.another_uid) in name)]
658 658 start = len(self.RC_PREFIX)
659 659 for del_key in entries:
660 660 # TODO: anderson: get_by_name needs name without prefix
661 661 entry = SettingsModel().get_setting_by_name(del_key[start:])
662 662 Session().delete(entry)
663 663
664 664 Session().commit()
665 665
666 666 def test_edit_issuetracker_pattern(
667 667 self, autologin_user, backend, csrf_token, request):
668 668 old_pattern = 'issuetracker_pat'
669 669 old_uid = md5(old_pattern)
670 670 pattern = 'issuetracker_pat_new'
671 671 self.new_uid = md5(pattern)
672 672
673 673 SettingsModel().create_or_update_setting(
674 674 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
675 675
676 676 post_url = route_path('admin_settings_issuetracker_update')
677 677 post_data = {
678 678 'new_pattern_pattern_0': pattern,
679 679 'new_pattern_url_0': 'https://url',
680 680 'new_pattern_prefix_0': 'prefix',
681 681 'new_pattern_description_0': 'description',
682 682 'uid': old_uid,
683 683 'csrf_token': csrf_token
684 684 }
685 685 self.app.post(post_url, post_data, status=302)
686 686 settings = SettingsModel().get_all_settings()
687 687 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
688 688 assert self.PATTERN_KEY+old_uid not in settings
689 689
690 690 @request.addfinalizer
691 691 def cleanup():
692 692 IssueTrackerSettingsModel().delete_entries(self.new_uid)
693 693
694 694 def test_replace_issuetracker_pattern_description(
695 695 self, autologin_user, csrf_token, request, settings_util):
696 696 prefix = 'issuetracker'
697 697 pattern = 'issuetracker_pat'
698 698 self.uid = md5(pattern)
699 699 pattern_key = '_'.join([prefix, 'pat', self.uid])
700 700 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
701 701 desc_key = '_'.join([prefix, 'desc', self.uid])
702 702 rc_desc_key = '_'.join(['rhodecode', desc_key])
703 703 new_description = 'new_description'
704 704
705 705 settings_util.create_rhodecode_setting(
706 706 pattern_key, pattern, 'unicode', cleanup=False)
707 707 settings_util.create_rhodecode_setting(
708 708 desc_key, 'old description', 'unicode', cleanup=False)
709 709
710 710 post_url = route_path('admin_settings_issuetracker_update')
711 711 post_data = {
712 712 'new_pattern_pattern_0': pattern,
713 713 'new_pattern_url_0': 'https://url',
714 714 'new_pattern_prefix_0': 'prefix',
715 715 'new_pattern_description_0': new_description,
716 716 'uid': self.uid,
717 717 'csrf_token': csrf_token
718 718 }
719 719 self.app.post(post_url, post_data, status=302)
720 720 settings = SettingsModel().get_all_settings()
721 721 assert settings[rc_pattern_key] == pattern
722 722 assert settings[rc_desc_key] == new_description
723 723
724 724 @request.addfinalizer
725 725 def cleanup():
726 726 IssueTrackerSettingsModel().delete_entries(self.uid)
727 727
728 728 def test_delete_issuetracker_pattern(
729 729 self, autologin_user, backend, csrf_token, settings_util):
730 730 pattern = 'issuetracker_pat'
731 731 uid = md5(pattern)
732 732 settings_util.create_rhodecode_setting(
733 733 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
734 734
735 735 post_url = route_path('admin_settings_issuetracker_delete')
736 736 post_data = {
737 '_method': 'delete',
738 737 'uid': uid,
739 738 'csrf_token': csrf_token
740 739 }
741 740 self.app.post(post_url, post_data, status=302)
742 741 settings = SettingsModel().get_all_settings()
743 742 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
@@ -1,2088 +1,2107 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20, prefix=False):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 if prefix:
157 157 # shorten in front
158 158 return postfix + text[-(size - len(postfix)):]
159 159 else:
160 160 return text[:size - len(postfix)] + postfix
161 161 return text
162 162
163 163
164 164 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
165 165 """
166 166 Reset button
167 167 """
168 168 _set_input_attrs(attrs, type, name, value)
169 169 _set_id_attr(attrs, id, name)
170 170 convert_boolean_attrs(attrs, ["disabled"])
171 171 return HTML.input(**attrs)
172 172
173 173 reset = _reset
174 174 safeid = _make_safe_id_component
175 175
176 176
177 177 def branding(name, length=40):
178 178 return truncate(name, length, indicator="")
179 179
180 180
181 181 def FID(raw_id, path):
182 182 """
183 183 Creates a unique ID for filenode based on it's hash of path and commit
184 184 it's safe to use in urls
185 185
186 186 :param raw_id:
187 187 :param path:
188 188 """
189 189
190 190 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
191 191
192 192
193 193 class _GetError(object):
194 194 """Get error from form_errors, and represent it as span wrapped error
195 195 message
196 196
197 197 :param field_name: field to fetch errors for
198 198 :param form_errors: form errors dict
199 199 """
200 200
201 201 def __call__(self, field_name, form_errors):
202 202 tmpl = """<span class="error_msg">%s</span>"""
203 203 if form_errors and field_name in form_errors:
204 204 return literal(tmpl % form_errors.get(field_name))
205 205
206 206
207 207 get_error = _GetError()
208 208
209 209
210 210 class _ToolTip(object):
211 211
212 212 def __call__(self, tooltip_title, trim_at=50):
213 213 """
214 214 Special function just to wrap our text into nice formatted
215 215 autowrapped text
216 216
217 217 :param tooltip_title:
218 218 """
219 219 tooltip_title = escape(tooltip_title)
220 220 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
221 221 return tooltip_title
222 222
223 223
224 224 tooltip = _ToolTip()
225 225
226 226 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
227 227
228 228
229 229 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
230 230 if isinstance(file_path, str):
231 231 file_path = safe_unicode(file_path)
232 232
233 233 route_qry = {'at': at_ref} if at_ref else None
234 234
235 235 # first segment is a `..` link to repo files
236 236 root_name = literal(u'<i class="icon-home"></i>')
237 237 url_segments = [
238 238 link_to(
239 239 root_name,
240 240 route_path(
241 241 'repo_files',
242 242 repo_name=repo_name,
243 243 commit_id=commit_id,
244 244 f_path='',
245 245 _query=route_qry),
246 246 )]
247 247
248 248 path_segments = file_path.split('/')
249 249 last_cnt = len(path_segments) - 1
250 250 for cnt, segment in enumerate(path_segments):
251 251 if not segment:
252 252 continue
253 253 segment_html = escape(segment)
254 254
255 255 last_item = cnt == last_cnt
256 256
257 257 if last_item and linkify_last_item is False:
258 258 # plain version
259 259 url_segments.append(segment_html)
260 260 else:
261 261 url_segments.append(
262 262 link_to(
263 263 segment_html,
264 264 route_path(
265 265 'repo_files',
266 266 repo_name=repo_name,
267 267 commit_id=commit_id,
268 268 f_path='/'.join(path_segments[:cnt + 1]),
269 269 _query=route_qry),
270 270 ))
271 271
272 272 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
273 273 if limit_items and len(limited_url_segments) < len(url_segments):
274 274 url_segments = limited_url_segments
275 275
276 276 full_path = file_path
277 277 icon = files_icon.format(escape(full_path))
278 278 if file_path == '':
279 279 return root_name
280 280 else:
281 281 return literal(' / '.join(url_segments) + icon)
282 282
283 283
284 284 def files_url_data(request):
285 285 matchdict = request.matchdict
286 286
287 287 if 'f_path' not in matchdict:
288 288 matchdict['f_path'] = ''
289 289
290 290 if 'commit_id' not in matchdict:
291 291 matchdict['commit_id'] = 'tip'
292 292
293 293 return json.dumps(matchdict)
294 294
295 295
296 296 def code_highlight(code, lexer, formatter, use_hl_filter=False):
297 297 """
298 298 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
299 299
300 300 If ``outfile`` is given and a valid file object (an object
301 301 with a ``write`` method), the result will be written to it, otherwise
302 302 it is returned as a string.
303 303 """
304 304 if use_hl_filter:
305 305 # add HL filter
306 306 from rhodecode.lib.index import search_utils
307 307 lexer.add_filter(search_utils.ElasticSearchHLFilter())
308 308 return pygments.format(pygments.lex(code, lexer), formatter)
309 309
310 310
311 311 class CodeHtmlFormatter(HtmlFormatter):
312 312 """
313 313 My code Html Formatter for source codes
314 314 """
315 315
316 316 def wrap(self, source, outfile):
317 317 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
318 318
319 319 def _wrap_code(self, source):
320 320 for cnt, it in enumerate(source):
321 321 i, t = it
322 322 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
323 323 yield i, t
324 324
325 325 def _wrap_tablelinenos(self, inner):
326 326 dummyoutfile = StringIO.StringIO()
327 327 lncount = 0
328 328 for t, line in inner:
329 329 if t:
330 330 lncount += 1
331 331 dummyoutfile.write(line)
332 332
333 333 fl = self.linenostart
334 334 mw = len(str(lncount + fl - 1))
335 335 sp = self.linenospecial
336 336 st = self.linenostep
337 337 la = self.lineanchors
338 338 aln = self.anchorlinenos
339 339 nocls = self.noclasses
340 340 if sp:
341 341 lines = []
342 342
343 343 for i in range(fl, fl + lncount):
344 344 if i % st == 0:
345 345 if i % sp == 0:
346 346 if aln:
347 347 lines.append('<a href="#%s%d" class="special">%*d</a>' %
348 348 (la, i, mw, i))
349 349 else:
350 350 lines.append('<span class="special">%*d</span>' % (mw, i))
351 351 else:
352 352 if aln:
353 353 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
354 354 else:
355 355 lines.append('%*d' % (mw, i))
356 356 else:
357 357 lines.append('')
358 358 ls = '\n'.join(lines)
359 359 else:
360 360 lines = []
361 361 for i in range(fl, fl + lncount):
362 362 if i % st == 0:
363 363 if aln:
364 364 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
365 365 else:
366 366 lines.append('%*d' % (mw, i))
367 367 else:
368 368 lines.append('')
369 369 ls = '\n'.join(lines)
370 370
371 371 # in case you wonder about the seemingly redundant <div> here: since the
372 372 # content in the other cell also is wrapped in a div, some browsers in
373 373 # some configurations seem to mess up the formatting...
374 374 if nocls:
375 375 yield 0, ('<table class="%stable">' % self.cssclass +
376 376 '<tr><td><div class="linenodiv" '
377 377 'style="background-color: #f0f0f0; padding-right: 10px">'
378 378 '<pre style="line-height: 125%">' +
379 379 ls + '</pre></div></td><td id="hlcode" class="code">')
380 380 else:
381 381 yield 0, ('<table class="%stable">' % self.cssclass +
382 382 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
383 383 ls + '</pre></div></td><td id="hlcode" class="code">')
384 384 yield 0, dummyoutfile.getvalue()
385 385 yield 0, '</td></tr></table>'
386 386
387 387
388 388 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
389 389 def __init__(self, **kw):
390 390 # only show these line numbers if set
391 391 self.only_lines = kw.pop('only_line_numbers', [])
392 392 self.query_terms = kw.pop('query_terms', [])
393 393 self.max_lines = kw.pop('max_lines', 5)
394 394 self.line_context = kw.pop('line_context', 3)
395 395 self.url = kw.pop('url', None)
396 396
397 397 super(CodeHtmlFormatter, self).__init__(**kw)
398 398
399 399 def _wrap_code(self, source):
400 400 for cnt, it in enumerate(source):
401 401 i, t = it
402 402 t = '<pre>%s</pre>' % t
403 403 yield i, t
404 404
405 405 def _wrap_tablelinenos(self, inner):
406 406 yield 0, '<table class="code-highlight %stable">' % self.cssclass
407 407
408 408 last_shown_line_number = 0
409 409 current_line_number = 1
410 410
411 411 for t, line in inner:
412 412 if not t:
413 413 yield t, line
414 414 continue
415 415
416 416 if current_line_number in self.only_lines:
417 417 if last_shown_line_number + 1 != current_line_number:
418 418 yield 0, '<tr>'
419 419 yield 0, '<td class="line">...</td>'
420 420 yield 0, '<td id="hlcode" class="code"></td>'
421 421 yield 0, '</tr>'
422 422
423 423 yield 0, '<tr>'
424 424 if self.url:
425 425 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
426 426 self.url, current_line_number, current_line_number)
427 427 else:
428 428 yield 0, '<td class="line"><a href="">%i</a></td>' % (
429 429 current_line_number)
430 430 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
431 431 yield 0, '</tr>'
432 432
433 433 last_shown_line_number = current_line_number
434 434
435 435 current_line_number += 1
436 436
437 437 yield 0, '</table>'
438 438
439 439
440 440 def hsv_to_rgb(h, s, v):
441 441 """ Convert hsv color values to rgb """
442 442
443 443 if s == 0.0:
444 444 return v, v, v
445 445 i = int(h * 6.0) # XXX assume int() truncates!
446 446 f = (h * 6.0) - i
447 447 p = v * (1.0 - s)
448 448 q = v * (1.0 - s * f)
449 449 t = v * (1.0 - s * (1.0 - f))
450 450 i = i % 6
451 451 if i == 0:
452 452 return v, t, p
453 453 if i == 1:
454 454 return q, v, p
455 455 if i == 2:
456 456 return p, v, t
457 457 if i == 3:
458 458 return p, q, v
459 459 if i == 4:
460 460 return t, p, v
461 461 if i == 5:
462 462 return v, p, q
463 463
464 464
465 465 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
466 466 """
467 467 Generator for getting n of evenly distributed colors using
468 468 hsv color and golden ratio. It always return same order of colors
469 469
470 470 :param n: number of colors to generate
471 471 :param saturation: saturation of returned colors
472 472 :param lightness: lightness of returned colors
473 473 :returns: RGB tuple
474 474 """
475 475
476 476 golden_ratio = 0.618033988749895
477 477 h = 0.22717784590367374
478 478
479 479 for _ in xrange(n):
480 480 h += golden_ratio
481 481 h %= 1
482 482 HSV_tuple = [h, saturation, lightness]
483 483 RGB_tuple = hsv_to_rgb(*HSV_tuple)
484 484 yield map(lambda x: str(int(x * 256)), RGB_tuple)
485 485
486 486
487 487 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
488 488 """
489 489 Returns a function which when called with an argument returns a unique
490 490 color for that argument, eg.
491 491
492 492 :param n: number of colors to generate
493 493 :param saturation: saturation of returned colors
494 494 :param lightness: lightness of returned colors
495 495 :returns: css RGB string
496 496
497 497 >>> color_hash = color_hasher()
498 498 >>> color_hash('hello')
499 499 'rgb(34, 12, 59)'
500 500 >>> color_hash('hello')
501 501 'rgb(34, 12, 59)'
502 502 >>> color_hash('other')
503 503 'rgb(90, 224, 159)'
504 504 """
505 505
506 506 color_dict = {}
507 507 cgenerator = unique_color_generator(
508 508 saturation=saturation, lightness=lightness)
509 509
510 510 def get_color_string(thing):
511 511 if thing in color_dict:
512 512 col = color_dict[thing]
513 513 else:
514 514 col = color_dict[thing] = cgenerator.next()
515 515 return "rgb(%s)" % (', '.join(col))
516 516
517 517 return get_color_string
518 518
519 519
520 520 def get_lexer_safe(mimetype=None, filepath=None):
521 521 """
522 522 Tries to return a relevant pygments lexer using mimetype/filepath name,
523 523 defaulting to plain text if none could be found
524 524 """
525 525 lexer = None
526 526 try:
527 527 if mimetype:
528 528 lexer = get_lexer_for_mimetype(mimetype)
529 529 if not lexer:
530 530 lexer = get_lexer_for_filename(filepath)
531 531 except pygments.util.ClassNotFound:
532 532 pass
533 533
534 534 if not lexer:
535 535 lexer = get_lexer_by_name('text')
536 536
537 537 return lexer
538 538
539 539
540 540 def get_lexer_for_filenode(filenode):
541 541 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
542 542 return lexer
543 543
544 544
545 545 def pygmentize(filenode, **kwargs):
546 546 """
547 547 pygmentize function using pygments
548 548
549 549 :param filenode:
550 550 """
551 551 lexer = get_lexer_for_filenode(filenode)
552 552 return literal(code_highlight(filenode.content, lexer,
553 553 CodeHtmlFormatter(**kwargs)))
554 554
555 555
556 556 def is_following_repo(repo_name, user_id):
557 557 from rhodecode.model.scm import ScmModel
558 558 return ScmModel().is_following_repo(repo_name, user_id)
559 559
560 560
561 561 class _Message(object):
562 562 """A message returned by ``Flash.pop_messages()``.
563 563
564 564 Converting the message to a string returns the message text. Instances
565 565 also have the following attributes:
566 566
567 567 * ``message``: the message text.
568 568 * ``category``: the category specified when the message was created.
569 569 """
570 570
571 571 def __init__(self, category, message):
572 572 self.category = category
573 573 self.message = message
574 574
575 575 def __str__(self):
576 576 return self.message
577 577
578 578 __unicode__ = __str__
579 579
580 580 def __html__(self):
581 581 return escape(safe_unicode(self.message))
582 582
583 583
584 584 class Flash(object):
585 585 # List of allowed categories. If None, allow any category.
586 586 categories = ["warning", "notice", "error", "success"]
587 587
588 588 # Default category if none is specified.
589 589 default_category = "notice"
590 590
591 591 def __init__(self, session_key="flash", categories=None,
592 592 default_category=None):
593 593 """
594 594 Instantiate a ``Flash`` object.
595 595
596 596 ``session_key`` is the key to save the messages under in the user's
597 597 session.
598 598
599 599 ``categories`` is an optional list which overrides the default list
600 600 of categories.
601 601
602 602 ``default_category`` overrides the default category used for messages
603 603 when none is specified.
604 604 """
605 605 self.session_key = session_key
606 606 if categories is not None:
607 607 self.categories = categories
608 608 if default_category is not None:
609 609 self.default_category = default_category
610 610 if self.categories and self.default_category not in self.categories:
611 611 raise ValueError(
612 612 "unrecognized default category %r" % (self.default_category,))
613 613
614 614 def pop_messages(self, session=None, request=None):
615 615 """
616 616 Return all accumulated messages and delete them from the session.
617 617
618 618 The return value is a list of ``Message`` objects.
619 619 """
620 620 messages = []
621 621
622 622 if not session:
623 623 if not request:
624 624 request = get_current_request()
625 625 session = request.session
626 626
627 627 # Pop the 'old' pylons flash messages. They are tuples of the form
628 628 # (category, message)
629 629 for cat, msg in session.pop(self.session_key, []):
630 630 messages.append(_Message(cat, msg))
631 631
632 632 # Pop the 'new' pyramid flash messages for each category as list
633 633 # of strings.
634 634 for cat in self.categories:
635 635 for msg in session.pop_flash(queue=cat):
636 636 messages.append(_Message(cat, msg))
637 637 # Map messages from the default queue to the 'notice' category.
638 638 for msg in session.pop_flash():
639 639 messages.append(_Message('notice', msg))
640 640
641 641 session.save()
642 642 return messages
643 643
644 644 def json_alerts(self, session=None, request=None):
645 645 payloads = []
646 646 messages = flash.pop_messages(session=session, request=request)
647 647 if messages:
648 648 for message in messages:
649 649 subdata = {}
650 650 if hasattr(message.message, 'rsplit'):
651 651 flash_data = message.message.rsplit('|DELIM|', 1)
652 652 org_message = flash_data[0]
653 653 if len(flash_data) > 1:
654 654 subdata = json.loads(flash_data[1])
655 655 else:
656 656 org_message = message.message
657 657 payloads.append({
658 658 'message': {
659 659 'message': u'{}'.format(org_message),
660 660 'level': message.category,
661 661 'force': True,
662 662 'subdata': subdata
663 663 }
664 664 })
665 665 return json.dumps(payloads)
666 666
667 667 def __call__(self, message, category=None, ignore_duplicate=True,
668 668 session=None, request=None):
669 669
670 670 if not session:
671 671 if not request:
672 672 request = get_current_request()
673 673 session = request.session
674 674
675 675 session.flash(
676 676 message, queue=category, allow_duplicate=not ignore_duplicate)
677 677
678 678
679 679 flash = Flash()
680 680
681 681 #==============================================================================
682 682 # SCM FILTERS available via h.
683 683 #==============================================================================
684 684 from rhodecode.lib.vcs.utils import author_name, author_email
685 685 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
686 686 from rhodecode.model.db import User, ChangesetStatus
687 687
688 688 capitalize = lambda x: x.capitalize()
689 689 email = author_email
690 690 short_id = lambda x: x[:12]
691 691 hide_credentials = lambda x: ''.join(credentials_filter(x))
692 692
693 693
694 694 import pytz
695 695 import tzlocal
696 696 local_timezone = tzlocal.get_localzone()
697 697
698 698
699 699 def age_component(datetime_iso, value=None, time_is_local=False):
700 700 title = value or format_date(datetime_iso)
701 701 tzinfo = '+00:00'
702 702
703 703 # detect if we have a timezone info, otherwise, add it
704 704 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
705 705 force_timezone = os.environ.get('RC_TIMEZONE', '')
706 706 if force_timezone:
707 707 force_timezone = pytz.timezone(force_timezone)
708 708 timezone = force_timezone or local_timezone
709 709 offset = timezone.localize(datetime_iso).strftime('%z')
710 710 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
711 711
712 712 return literal(
713 713 '<time class="timeago tooltip" '
714 714 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
715 715 datetime_iso, title, tzinfo))
716 716
717 717
718 718 def _shorten_commit_id(commit_id, commit_len=None):
719 719 if commit_len is None:
720 720 request = get_current_request()
721 721 commit_len = request.call_context.visual.show_sha_length
722 722 return commit_id[:commit_len]
723 723
724 724
725 725 def show_id(commit, show_idx=None, commit_len=None):
726 726 """
727 727 Configurable function that shows ID
728 728 by default it's r123:fffeeefffeee
729 729
730 730 :param commit: commit instance
731 731 """
732 732 if show_idx is None:
733 733 request = get_current_request()
734 734 show_idx = request.call_context.visual.show_revision_number
735 735
736 736 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
737 737 if show_idx:
738 738 return 'r%s:%s' % (commit.idx, raw_id)
739 739 else:
740 740 return '%s' % (raw_id, )
741 741
742 742
743 743 def format_date(date):
744 744 """
745 745 use a standardized formatting for dates used in RhodeCode
746 746
747 747 :param date: date/datetime object
748 748 :return: formatted date
749 749 """
750 750
751 751 if date:
752 752 _fmt = "%a, %d %b %Y %H:%M:%S"
753 753 return safe_unicode(date.strftime(_fmt))
754 754
755 755 return u""
756 756
757 757
758 758 class _RepoChecker(object):
759 759
760 760 def __init__(self, backend_alias):
761 761 self._backend_alias = backend_alias
762 762
763 763 def __call__(self, repository):
764 764 if hasattr(repository, 'alias'):
765 765 _type = repository.alias
766 766 elif hasattr(repository, 'repo_type'):
767 767 _type = repository.repo_type
768 768 else:
769 769 _type = repository
770 770 return _type == self._backend_alias
771 771
772 772
773 773 is_git = _RepoChecker('git')
774 774 is_hg = _RepoChecker('hg')
775 775 is_svn = _RepoChecker('svn')
776 776
777 777
778 778 def get_repo_type_by_name(repo_name):
779 779 repo = Repository.get_by_repo_name(repo_name)
780 780 if repo:
781 781 return repo.repo_type
782 782
783 783
784 784 def is_svn_without_proxy(repository):
785 785 if is_svn(repository):
786 786 from rhodecode.model.settings import VcsSettingsModel
787 787 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
788 788 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
789 789 return False
790 790
791 791
792 792 def discover_user(author):
793 793 """
794 794 Tries to discover RhodeCode User based on the autho string. Author string
795 795 is typically `FirstName LastName <email@address.com>`
796 796 """
797 797
798 798 # if author is already an instance use it for extraction
799 799 if isinstance(author, User):
800 800 return author
801 801
802 802 # Valid email in the attribute passed, see if they're in the system
803 803 _email = author_email(author)
804 804 if _email != '':
805 805 user = User.get_by_email(_email, case_insensitive=True, cache=True)
806 806 if user is not None:
807 807 return user
808 808
809 809 # Maybe it's a username, we try to extract it and fetch by username ?
810 810 _author = author_name(author)
811 811 user = User.get_by_username(_author, case_insensitive=True, cache=True)
812 812 if user is not None:
813 813 return user
814 814
815 815 return None
816 816
817 817
818 818 def email_or_none(author):
819 819 # extract email from the commit string
820 820 _email = author_email(author)
821 821
822 822 # If we have an email, use it, otherwise
823 823 # see if it contains a username we can get an email from
824 824 if _email != '':
825 825 return _email
826 826 else:
827 827 user = User.get_by_username(
828 828 author_name(author), case_insensitive=True, cache=True)
829 829
830 830 if user is not None:
831 831 return user.email
832 832
833 833 # No valid email, not a valid user in the system, none!
834 834 return None
835 835
836 836
837 837 def link_to_user(author, length=0, **kwargs):
838 838 user = discover_user(author)
839 839 # user can be None, but if we have it already it means we can re-use it
840 840 # in the person() function, so we save 1 intensive-query
841 841 if user:
842 842 author = user
843 843
844 844 display_person = person(author, 'username_or_name_or_email')
845 845 if length:
846 846 display_person = shorter(display_person, length)
847 847
848 848 if user:
849 849 return link_to(
850 850 escape(display_person),
851 851 route_path('user_profile', username=user.username),
852 852 **kwargs)
853 853 else:
854 854 return escape(display_person)
855 855
856 856
857 857 def link_to_group(users_group_name, **kwargs):
858 858 return link_to(
859 859 escape(users_group_name),
860 860 route_path('user_group_profile', user_group_name=users_group_name),
861 861 **kwargs)
862 862
863 863
864 864 def person(author, show_attr="username_and_name"):
865 865 user = discover_user(author)
866 866 if user:
867 867 return getattr(user, show_attr)
868 868 else:
869 869 _author = author_name(author)
870 870 _email = email(author)
871 871 return _author or _email
872 872
873 873
874 874 def author_string(email):
875 875 if email:
876 876 user = User.get_by_email(email, case_insensitive=True, cache=True)
877 877 if user:
878 878 if user.first_name or user.last_name:
879 879 return '%s %s &lt;%s&gt;' % (
880 880 user.first_name, user.last_name, email)
881 881 else:
882 882 return email
883 883 else:
884 884 return email
885 885 else:
886 886 return None
887 887
888 888
889 889 def person_by_id(id_, show_attr="username_and_name"):
890 890 # attr to return from fetched user
891 891 person_getter = lambda usr: getattr(usr, show_attr)
892 892
893 893 #maybe it's an ID ?
894 894 if str(id_).isdigit() or isinstance(id_, int):
895 895 id_ = int(id_)
896 896 user = User.get(id_)
897 897 if user is not None:
898 898 return person_getter(user)
899 899 return id_
900 900
901 901
902 902 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 903 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 904 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
905 905
906 906
907 907 tags_paterns = OrderedDict((
908 908 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
909 909 '<div class="metatag" tag="lang">\\2</div>')),
910 910
911 911 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
912 912 '<div class="metatag" tag="see">see: \\1 </div>')),
913 913
914 914 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
915 915 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
916 916
917 917 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
918 918 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
919 919
920 920 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
921 921 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
922 922
923 923 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
924 924 '<div class="metatag" tag="state \\1">\\1</div>')),
925 925
926 926 # label in grey
927 927 ('label', (re.compile(r'\[([a-z]+)\]'),
928 928 '<div class="metatag" tag="label">\\1</div>')),
929 929
930 930 # generic catch all in grey
931 931 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
932 932 '<div class="metatag" tag="generic">\\1</div>')),
933 933 ))
934 934
935 935
936 936 def extract_metatags(value):
937 937 """
938 938 Extract supported meta-tags from given text value
939 939 """
940 940 tags = []
941 941 if not value:
942 942 return tags, ''
943 943
944 944 for key, val in tags_paterns.items():
945 945 pat, replace_html = val
946 946 tags.extend([(key, x.group()) for x in pat.finditer(value)])
947 947 value = pat.sub('', value)
948 948
949 949 return tags, value
950 950
951 951
952 952 def style_metatag(tag_type, value):
953 953 """
954 954 converts tags from value into html equivalent
955 955 """
956 956 if not value:
957 957 return ''
958 958
959 959 html_value = value
960 960 tag_data = tags_paterns.get(tag_type)
961 961 if tag_data:
962 962 pat, replace_html = tag_data
963 963 # convert to plain `unicode` instead of a markup tag to be used in
964 964 # regex expressions. safe_unicode doesn't work here
965 965 html_value = pat.sub(replace_html, unicode(value))
966 966
967 967 return html_value
968 968
969 969
970 970 def bool2icon(value, show_at_false=True):
971 971 """
972 972 Returns boolean value of a given value, represented as html element with
973 973 classes that will represent icons
974 974
975 975 :param value: given value to convert to html node
976 976 """
977 977
978 978 if value: # does bool conversion
979 979 return HTML.tag('i', class_="icon-true", title='True')
980 980 else: # not true as bool
981 981 if show_at_false:
982 982 return HTML.tag('i', class_="icon-false", title='False')
983 983 return HTML.tag('i')
984 984
985 985 #==============================================================================
986 986 # PERMS
987 987 #==============================================================================
988 988 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
989 989 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
990 990 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
991 991 csrf_token_key
992 992
993 993
994 994 #==============================================================================
995 995 # GRAVATAR URL
996 996 #==============================================================================
997 997 class InitialsGravatar(object):
998 998 def __init__(self, email_address, first_name, last_name, size=30,
999 999 background=None, text_color='#fff'):
1000 1000 self.size = size
1001 1001 self.first_name = first_name
1002 1002 self.last_name = last_name
1003 1003 self.email_address = email_address
1004 1004 self.background = background or self.str2color(email_address)
1005 1005 self.text_color = text_color
1006 1006
1007 1007 def get_color_bank(self):
1008 1008 """
1009 1009 returns a predefined list of colors that gravatars can use.
1010 1010 Those are randomized distinct colors that guarantee readability and
1011 1011 uniqueness.
1012 1012
1013 1013 generated with: http://phrogz.net/css/distinct-colors.html
1014 1014 """
1015 1015 return [
1016 1016 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1017 1017 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1018 1018 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1019 1019 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1020 1020 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1021 1021 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1022 1022 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1023 1023 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1024 1024 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1025 1025 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1026 1026 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1027 1027 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1028 1028 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1029 1029 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1030 1030 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1031 1031 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1032 1032 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1033 1033 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1034 1034 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1035 1035 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1036 1036 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1037 1037 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1038 1038 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1039 1039 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1040 1040 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1041 1041 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1042 1042 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1043 1043 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1044 1044 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1045 1045 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1046 1046 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1047 1047 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1048 1048 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1049 1049 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1050 1050 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1051 1051 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1052 1052 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1053 1053 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1054 1054 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1055 1055 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1056 1056 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1057 1057 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1058 1058 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1059 1059 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1060 1060 '#4f8c46', '#368dd9', '#5c0073'
1061 1061 ]
1062 1062
1063 1063 def rgb_to_hex_color(self, rgb_tuple):
1064 1064 """
1065 1065 Converts an rgb_tuple passed to an hex color.
1066 1066
1067 1067 :param rgb_tuple: tuple with 3 ints represents rgb color space
1068 1068 """
1069 1069 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1070 1070
1071 1071 def email_to_int_list(self, email_str):
1072 1072 """
1073 1073 Get every byte of the hex digest value of email and turn it to integer.
1074 1074 It's going to be always between 0-255
1075 1075 """
1076 1076 digest = md5_safe(email_str.lower())
1077 1077 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1078 1078
1079 1079 def pick_color_bank_index(self, email_str, color_bank):
1080 1080 return self.email_to_int_list(email_str)[0] % len(color_bank)
1081 1081
1082 1082 def str2color(self, email_str):
1083 1083 """
1084 1084 Tries to map in a stable algorithm an email to color
1085 1085
1086 1086 :param email_str:
1087 1087 """
1088 1088 color_bank = self.get_color_bank()
1089 1089 # pick position (module it's length so we always find it in the
1090 1090 # bank even if it's smaller than 256 values
1091 1091 pos = self.pick_color_bank_index(email_str, color_bank)
1092 1092 return color_bank[pos]
1093 1093
1094 1094 def normalize_email(self, email_address):
1095 1095 import unicodedata
1096 1096 # default host used to fill in the fake/missing email
1097 1097 default_host = u'localhost'
1098 1098
1099 1099 if not email_address:
1100 1100 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1101 1101
1102 1102 email_address = safe_unicode(email_address)
1103 1103
1104 1104 if u'@' not in email_address:
1105 1105 email_address = u'%s@%s' % (email_address, default_host)
1106 1106
1107 1107 if email_address.endswith(u'@'):
1108 1108 email_address = u'%s%s' % (email_address, default_host)
1109 1109
1110 1110 email_address = unicodedata.normalize('NFKD', email_address)\
1111 1111 .encode('ascii', 'ignore')
1112 1112 return email_address
1113 1113
1114 1114 def get_initials(self):
1115 1115 """
1116 1116 Returns 2 letter initials calculated based on the input.
1117 1117 The algorithm picks first given email address, and takes first letter
1118 1118 of part before @, and then the first letter of server name. In case
1119 1119 the part before @ is in a format of `somestring.somestring2` it replaces
1120 1120 the server letter with first letter of somestring2
1121 1121
1122 1122 In case function was initialized with both first and lastname, this
1123 1123 overrides the extraction from email by first letter of the first and
1124 1124 last name. We add special logic to that functionality, In case Full name
1125 1125 is compound, like Guido Von Rossum, we use last part of the last name
1126 1126 (Von Rossum) picking `R`.
1127 1127
1128 1128 Function also normalizes the non-ascii characters to they ascii
1129 1129 representation, eg Δ„ => A
1130 1130 """
1131 1131 import unicodedata
1132 1132 # replace non-ascii to ascii
1133 1133 first_name = unicodedata.normalize(
1134 1134 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1135 1135 last_name = unicodedata.normalize(
1136 1136 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1137 1137
1138 1138 # do NFKD encoding, and also make sure email has proper format
1139 1139 email_address = self.normalize_email(self.email_address)
1140 1140
1141 1141 # first push the email initials
1142 1142 prefix, server = email_address.split('@', 1)
1143 1143
1144 1144 # check if prefix is maybe a 'first_name.last_name' syntax
1145 1145 _dot_split = prefix.rsplit('.', 1)
1146 1146 if len(_dot_split) == 2 and _dot_split[1]:
1147 1147 initials = [_dot_split[0][0], _dot_split[1][0]]
1148 1148 else:
1149 1149 initials = [prefix[0], server[0]]
1150 1150
1151 1151 # then try to replace either first_name or last_name
1152 1152 fn_letter = (first_name or " ")[0].strip()
1153 1153 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1154 1154
1155 1155 if fn_letter:
1156 1156 initials[0] = fn_letter
1157 1157
1158 1158 if ln_letter:
1159 1159 initials[1] = ln_letter
1160 1160
1161 1161 return ''.join(initials).upper()
1162 1162
1163 1163 def get_img_data_by_type(self, font_family, img_type):
1164 1164 default_user = """
1165 1165 <svg xmlns="http://www.w3.org/2000/svg"
1166 1166 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1167 1167 viewBox="-15 -10 439.165 429.164"
1168 1168
1169 1169 xml:space="preserve"
1170 1170 style="background:{background};" >
1171 1171
1172 1172 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1173 1173 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1174 1174 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1175 1175 168.596,153.916,216.671,
1176 1176 204.583,216.671z" fill="{text_color}"/>
1177 1177 <path d="M407.164,374.717L360.88,
1178 1178 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1179 1179 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1180 1180 15.366-44.203,23.488-69.076,23.488c-24.877,
1181 1181 0-48.762-8.122-69.078-23.488
1182 1182 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1183 1183 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1184 1184 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1185 1185 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1186 1186 19.402-10.527 C409.699,390.129,
1187 1187 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1188 1188 </svg>""".format(
1189 1189 size=self.size,
1190 1190 background='#979797', # @grey4
1191 1191 text_color=self.text_color,
1192 1192 font_family=font_family)
1193 1193
1194 1194 return {
1195 1195 "default_user": default_user
1196 1196 }[img_type]
1197 1197
1198 1198 def get_img_data(self, svg_type=None):
1199 1199 """
1200 1200 generates the svg metadata for image
1201 1201 """
1202 1202 fonts = [
1203 1203 '-apple-system',
1204 1204 'BlinkMacSystemFont',
1205 1205 'Segoe UI',
1206 1206 'Roboto',
1207 1207 'Oxygen-Sans',
1208 1208 'Ubuntu',
1209 1209 'Cantarell',
1210 1210 'Helvetica Neue',
1211 1211 'sans-serif'
1212 1212 ]
1213 1213 font_family = ','.join(fonts)
1214 1214 if svg_type:
1215 1215 return self.get_img_data_by_type(font_family, svg_type)
1216 1216
1217 1217 initials = self.get_initials()
1218 1218 img_data = """
1219 1219 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1220 1220 width="{size}" height="{size}"
1221 1221 style="width: 100%; height: 100%; background-color: {background}"
1222 1222 viewBox="0 0 {size} {size}">
1223 1223 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1224 1224 pointer-events="auto" fill="{text_color}"
1225 1225 font-family="{font_family}"
1226 1226 style="font-weight: 400; font-size: {f_size}px;">{text}
1227 1227 </text>
1228 1228 </svg>""".format(
1229 1229 size=self.size,
1230 1230 f_size=self.size/2.05, # scale the text inside the box nicely
1231 1231 background=self.background,
1232 1232 text_color=self.text_color,
1233 1233 text=initials.upper(),
1234 1234 font_family=font_family)
1235 1235
1236 1236 return img_data
1237 1237
1238 1238 def generate_svg(self, svg_type=None):
1239 1239 img_data = self.get_img_data(svg_type)
1240 1240 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1241 1241
1242 1242
1243 1243 def initials_gravatar(email_address, first_name, last_name, size=30):
1244 1244 svg_type = None
1245 1245 if email_address == User.DEFAULT_USER_EMAIL:
1246 1246 svg_type = 'default_user'
1247 1247 klass = InitialsGravatar(email_address, first_name, last_name, size)
1248 1248 return klass.generate_svg(svg_type=svg_type)
1249 1249
1250 1250
1251 1251 def gravatar_url(email_address, size=30, request=None):
1252 1252 request = get_current_request()
1253 1253 _use_gravatar = request.call_context.visual.use_gravatar
1254 1254 _gravatar_url = request.call_context.visual.gravatar_url
1255 1255
1256 1256 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1257 1257
1258 1258 email_address = email_address or User.DEFAULT_USER_EMAIL
1259 1259 if isinstance(email_address, unicode):
1260 1260 # hashlib crashes on unicode items
1261 1261 email_address = safe_str(email_address)
1262 1262
1263 1263 # empty email or default user
1264 1264 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1265 1265 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1266 1266
1267 1267 if _use_gravatar:
1268 1268 # TODO: Disuse pyramid thread locals. Think about another solution to
1269 1269 # get the host and schema here.
1270 1270 request = get_current_request()
1271 1271 tmpl = safe_str(_gravatar_url)
1272 1272 tmpl = tmpl.replace('{email}', email_address)\
1273 1273 .replace('{md5email}', md5_safe(email_address.lower())) \
1274 1274 .replace('{netloc}', request.host)\
1275 1275 .replace('{scheme}', request.scheme)\
1276 1276 .replace('{size}', safe_str(size))
1277 1277 return tmpl
1278 1278 else:
1279 1279 return initials_gravatar(email_address, '', '', size=size)
1280 1280
1281 1281
1282 1282 class Page(_Page):
1283 1283 """
1284 1284 Custom pager to match rendering style with paginator
1285 1285 """
1286 1286
1287 1287 def _get_pos(self, cur_page, max_page, items):
1288 1288 edge = (items / 2) + 1
1289 1289 if (cur_page <= edge):
1290 1290 radius = max(items / 2, items - cur_page)
1291 1291 elif (max_page - cur_page) < edge:
1292 1292 radius = (items - 1) - (max_page - cur_page)
1293 1293 else:
1294 1294 radius = items / 2
1295 1295
1296 1296 left = max(1, (cur_page - (radius)))
1297 1297 right = min(max_page, cur_page + (radius))
1298 1298 return left, cur_page, right
1299 1299
1300 1300 def _range(self, regexp_match):
1301 1301 """
1302 1302 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1303 1303
1304 1304 Arguments:
1305 1305
1306 1306 regexp_match
1307 1307 A "re" (regular expressions) match object containing the
1308 1308 radius of linked pages around the current page in
1309 1309 regexp_match.group(1) as a string
1310 1310
1311 1311 This function is supposed to be called as a callable in
1312 1312 re.sub.
1313 1313
1314 1314 """
1315 1315 radius = int(regexp_match.group(1))
1316 1316
1317 1317 # Compute the first and last page number within the radius
1318 1318 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1319 1319 # -> leftmost_page = 5
1320 1320 # -> rightmost_page = 9
1321 1321 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1322 1322 self.last_page,
1323 1323 (radius * 2) + 1)
1324 1324 nav_items = []
1325 1325
1326 1326 # Create a link to the first page (unless we are on the first page
1327 1327 # or there would be no need to insert '..' spacers)
1328 1328 if self.page != self.first_page and self.first_page < leftmost_page:
1329 1329 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1330 1330
1331 1331 # Insert dots if there are pages between the first page
1332 1332 # and the currently displayed page range
1333 1333 if leftmost_page - self.first_page > 1:
1334 1334 # Wrap in a SPAN tag if nolink_attr is set
1335 1335 text = '..'
1336 1336 if self.dotdot_attr:
1337 1337 text = HTML.span(c=text, **self.dotdot_attr)
1338 1338 nav_items.append(text)
1339 1339
1340 1340 for thispage in xrange(leftmost_page, rightmost_page + 1):
1341 1341 # Hilight the current page number and do not use a link
1342 1342 if thispage == self.page:
1343 1343 text = '%s' % (thispage,)
1344 1344 # Wrap in a SPAN tag if nolink_attr is set
1345 1345 if self.curpage_attr:
1346 1346 text = HTML.span(c=text, **self.curpage_attr)
1347 1347 nav_items.append(text)
1348 1348 # Otherwise create just a link to that page
1349 1349 else:
1350 1350 text = '%s' % (thispage,)
1351 1351 nav_items.append(self._pagerlink(thispage, text))
1352 1352
1353 1353 # Insert dots if there are pages between the displayed
1354 1354 # page numbers and the end of the page range
1355 1355 if self.last_page - rightmost_page > 1:
1356 1356 text = '..'
1357 1357 # Wrap in a SPAN tag if nolink_attr is set
1358 1358 if self.dotdot_attr:
1359 1359 text = HTML.span(c=text, **self.dotdot_attr)
1360 1360 nav_items.append(text)
1361 1361
1362 1362 # Create a link to the very last page (unless we are on the last
1363 1363 # page or there would be no need to insert '..' spacers)
1364 1364 if self.page != self.last_page and rightmost_page < self.last_page:
1365 1365 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1366 1366
1367 1367 ## prerender links
1368 1368 #_page_link = url.current()
1369 1369 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 1370 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 1371 return self.separator.join(nav_items)
1372 1372
1373 1373 def pager(self, format='~2~', page_param='page', partial_param='partial',
1374 1374 show_if_single_page=False, separator=' ', onclick=None,
1375 1375 symbol_first='<<', symbol_last='>>',
1376 1376 symbol_previous='<', symbol_next='>',
1377 1377 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1378 1378 curpage_attr={'class': 'pager_curpage'},
1379 1379 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1380 1380
1381 1381 self.curpage_attr = curpage_attr
1382 1382 self.separator = separator
1383 1383 self.pager_kwargs = kwargs
1384 1384 self.page_param = page_param
1385 1385 self.partial_param = partial_param
1386 1386 self.onclick = onclick
1387 1387 self.link_attr = link_attr
1388 1388 self.dotdot_attr = dotdot_attr
1389 1389
1390 1390 # Don't show navigator if there is no more than one page
1391 1391 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1392 1392 return ''
1393 1393
1394 1394 from string import Template
1395 1395 # Replace ~...~ in token format by range of pages
1396 1396 result = re.sub(r'~(\d+)~', self._range, format)
1397 1397
1398 1398 # Interpolate '%' variables
1399 1399 result = Template(result).safe_substitute({
1400 1400 'first_page': self.first_page,
1401 1401 'last_page': self.last_page,
1402 1402 'page': self.page,
1403 1403 'page_count': self.page_count,
1404 1404 'items_per_page': self.items_per_page,
1405 1405 'first_item': self.first_item,
1406 1406 'last_item': self.last_item,
1407 1407 'item_count': self.item_count,
1408 1408 'link_first': self.page > self.first_page and \
1409 1409 self._pagerlink(self.first_page, symbol_first) or '',
1410 1410 'link_last': self.page < self.last_page and \
1411 1411 self._pagerlink(self.last_page, symbol_last) or '',
1412 1412 'link_previous': self.previous_page and \
1413 1413 self._pagerlink(self.previous_page, symbol_previous) \
1414 1414 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1415 1415 'link_next': self.next_page and \
1416 1416 self._pagerlink(self.next_page, symbol_next) \
1417 1417 or HTML.span(symbol_next, class_="pg-next disabled")
1418 1418 })
1419 1419
1420 1420 return literal(result)
1421 1421
1422 1422
1423 1423 #==============================================================================
1424 1424 # REPO PAGER, PAGER FOR REPOSITORY
1425 1425 #==============================================================================
1426 1426 class RepoPage(Page):
1427 1427
1428 1428 def __init__(self, collection, page=1, items_per_page=20,
1429 1429 item_count=None, url=None, **kwargs):
1430 1430
1431 1431 """Create a "RepoPage" instance. special pager for paging
1432 1432 repository
1433 1433 """
1434 1434 self._url_generator = url
1435 1435
1436 1436 # Safe the kwargs class-wide so they can be used in the pager() method
1437 1437 self.kwargs = kwargs
1438 1438
1439 1439 # Save a reference to the collection
1440 1440 self.original_collection = collection
1441 1441
1442 1442 self.collection = collection
1443 1443
1444 1444 # The self.page is the number of the current page.
1445 1445 # The first page has the number 1!
1446 1446 try:
1447 1447 self.page = int(page) # make it int() if we get it as a string
1448 1448 except (ValueError, TypeError):
1449 1449 self.page = 1
1450 1450
1451 1451 self.items_per_page = items_per_page
1452 1452
1453 1453 # Unless the user tells us how many items the collections has
1454 1454 # we calculate that ourselves.
1455 1455 if item_count is not None:
1456 1456 self.item_count = item_count
1457 1457 else:
1458 1458 self.item_count = len(self.collection)
1459 1459
1460 1460 # Compute the number of the first and last available page
1461 1461 if self.item_count > 0:
1462 1462 self.first_page = 1
1463 1463 self.page_count = int(math.ceil(float(self.item_count) /
1464 1464 self.items_per_page))
1465 1465 self.last_page = self.first_page + self.page_count - 1
1466 1466
1467 1467 # Make sure that the requested page number is the range of
1468 1468 # valid pages
1469 1469 if self.page > self.last_page:
1470 1470 self.page = self.last_page
1471 1471 elif self.page < self.first_page:
1472 1472 self.page = self.first_page
1473 1473
1474 1474 # Note: the number of items on this page can be less than
1475 1475 # items_per_page if the last page is not full
1476 1476 self.first_item = max(0, (self.item_count) - (self.page *
1477 1477 items_per_page))
1478 1478 self.last_item = ((self.item_count - 1) - items_per_page *
1479 1479 (self.page - 1))
1480 1480
1481 1481 self.items = list(self.collection[self.first_item:self.last_item + 1])
1482 1482
1483 1483 # Links to previous and next page
1484 1484 if self.page > self.first_page:
1485 1485 self.previous_page = self.page - 1
1486 1486 else:
1487 1487 self.previous_page = None
1488 1488
1489 1489 if self.page < self.last_page:
1490 1490 self.next_page = self.page + 1
1491 1491 else:
1492 1492 self.next_page = None
1493 1493
1494 1494 # No items available
1495 1495 else:
1496 1496 self.first_page = None
1497 1497 self.page_count = 0
1498 1498 self.last_page = None
1499 1499 self.first_item = None
1500 1500 self.last_item = None
1501 1501 self.previous_page = None
1502 1502 self.next_page = None
1503 1503 self.items = []
1504 1504
1505 1505 # This is a subclass of the 'list' type. Initialise the list now.
1506 1506 list.__init__(self, reversed(self.items))
1507 1507
1508 1508
1509 1509 def breadcrumb_repo_link(repo):
1510 1510 """
1511 1511 Makes a breadcrumbs path link to repo
1512 1512
1513 1513 ex::
1514 1514 group >> subgroup >> repo
1515 1515
1516 1516 :param repo: a Repository instance
1517 1517 """
1518 1518
1519 1519 path = [
1520 1520 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1521 1521 title='last change:{}'.format(format_date(group.last_commit_change)))
1522 1522 for group in repo.groups_with_parents
1523 1523 ] + [
1524 1524 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1525 1525 title='last change:{}'.format(format_date(repo.last_commit_change)))
1526 1526 ]
1527 1527
1528 1528 return literal(' &raquo; '.join(path))
1529 1529
1530 1530
1531 1531 def breadcrumb_repo_group_link(repo_group):
1532 1532 """
1533 1533 Makes a breadcrumbs path link to repo
1534 1534
1535 1535 ex::
1536 1536 group >> subgroup
1537 1537
1538 1538 :param repo_group: a Repository Group instance
1539 1539 """
1540 1540
1541 1541 path = [
1542 1542 link_to(group.name,
1543 1543 route_path('repo_group_home', repo_group_name=group.group_name),
1544 1544 title='last change:{}'.format(format_date(group.last_commit_change)))
1545 1545 for group in repo_group.parents
1546 1546 ] + [
1547 1547 link_to(repo_group.name,
1548 1548 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1549 1549 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1550 1550 ]
1551 1551
1552 1552 return literal(' &raquo; '.join(path))
1553 1553
1554 1554
1555 1555 def format_byte_size_binary(file_size):
1556 1556 """
1557 1557 Formats file/folder sizes to standard.
1558 1558 """
1559 1559 if file_size is None:
1560 1560 file_size = 0
1561 1561
1562 1562 formatted_size = format_byte_size(file_size, binary=True)
1563 1563 return formatted_size
1564 1564
1565 1565
1566 1566 def urlify_text(text_, safe=True):
1567 1567 """
1568 1568 Extrac urls from text and make html links out of them
1569 1569
1570 1570 :param text_:
1571 1571 """
1572 1572
1573 1573 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1574 1574 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1575 1575
1576 1576 def url_func(match_obj):
1577 1577 url_full = match_obj.groups()[0]
1578 1578 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1579 1579 _newtext = url_pat.sub(url_func, text_)
1580 1580 if safe:
1581 1581 return literal(_newtext)
1582 1582 return _newtext
1583 1583
1584 1584
1585 1585 def urlify_commits(text_, repository):
1586 1586 """
1587 1587 Extract commit ids from text and make link from them
1588 1588
1589 1589 :param text_:
1590 1590 :param repository: repo name to build the URL with
1591 1591 """
1592 1592
1593 1593 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1594 1594
1595 1595 def url_func(match_obj):
1596 1596 commit_id = match_obj.groups()[1]
1597 1597 pref = match_obj.groups()[0]
1598 1598 suf = match_obj.groups()[2]
1599 1599
1600 1600 tmpl = (
1601 1601 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1602 1602 '%(commit_id)s</a>%(suf)s'
1603 1603 )
1604 1604 return tmpl % {
1605 1605 'pref': pref,
1606 1606 'cls': 'revision-link',
1607 1607 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1608 1608 'commit_id': commit_id,
1609 1609 'suf': suf
1610 1610 }
1611 1611
1612 1612 newtext = URL_PAT.sub(url_func, text_)
1613 1613
1614 1614 return newtext
1615 1615
1616 1616
1617 1617 def _process_url_func(match_obj, repo_name, uid, entry,
1618 1618 return_raw_data=False, link_format='html'):
1619 1619 pref = ''
1620 1620 if match_obj.group().startswith(' '):
1621 1621 pref = ' '
1622 1622
1623 1623 issue_id = ''.join(match_obj.groups())
1624 1624
1625 1625 if link_format == 'html':
1626 1626 tmpl = (
1627 1627 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1628 1628 '%(issue-prefix)s%(id-repr)s'
1629 1629 '</a>')
1630 elif link_format == 'html+hovercard':
1631 tmpl = (
1632 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1633 '%(issue-prefix)s%(id-repr)s'
1634 '</a>')
1630 1635 elif link_format == 'rst':
1631 1636 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1632 1637 elif link_format == 'markdown':
1633 1638 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1634 1639 else:
1635 1640 raise ValueError('Bad link_format:{}'.format(link_format))
1636 1641
1637 1642 (repo_name_cleaned,
1638 1643 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1639 1644
1640 1645 # variables replacement
1641 1646 named_vars = {
1642 1647 'id': issue_id,
1643 1648 'repo': repo_name,
1644 1649 'repo_name': repo_name_cleaned,
1645 1650 'group_name': parent_group_name,
1646 1651 }
1647 1652 # named regex variables
1648 1653 named_vars.update(match_obj.groupdict())
1649 1654 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1655 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1650 1656
1651 1657 def quote_cleaner(input_str):
1652 1658 """Remove quotes as it's HTML"""
1653 1659 return input_str.replace('"', '')
1654 1660
1655 1661 data = {
1656 1662 'pref': pref,
1657 1663 'cls': quote_cleaner('issue-tracker-link'),
1658 1664 'url': quote_cleaner(_url),
1659 1665 'id-repr': issue_id,
1660 1666 'issue-prefix': entry['pref'],
1661 1667 'serv': entry['url'],
1662 'title': entry['desc']
1668 'title': desc,
1669 'hovercard_url': ''
1663 1670 }
1664 1671 if return_raw_data:
1665 1672 return {
1666 1673 'id': issue_id,
1667 1674 'url': _url
1668 1675 }
1669 1676 return tmpl % data
1670 1677
1671 1678
1672 1679 def get_active_pattern_entries(repo_name):
1673 1680 repo = None
1674 1681 if repo_name:
1675 1682 # Retrieving repo_name to avoid invalid repo_name to explode on
1676 1683 # IssueTrackerSettingsModel but still passing invalid name further down
1677 1684 repo = Repository.get_by_repo_name(repo_name, cache=True)
1678 1685
1679 1686 settings_model = IssueTrackerSettingsModel(repo=repo)
1680 1687 active_entries = settings_model.get_settings(cache=True)
1681 1688 return active_entries
1682 1689
1683 1690
1684 1691 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1685 1692
1686 1693 allowed_formats = ['html', 'rst', 'markdown']
1687 1694 if link_format not in allowed_formats:
1688 1695 raise ValueError('Link format can be only one of:{} got {}'.format(
1689 1696 allowed_formats, link_format))
1690 1697
1691 1698 active_entries = active_entries or get_active_pattern_entries(repo_name)
1692 1699 issues_data = []
1693 1700 new_text = text_string
1694 1701
1695 1702 log.debug('Got %s entries to process', len(active_entries))
1696 1703 for uid, entry in active_entries.items():
1697 1704 log.debug('found issue tracker entry with uid %s', uid)
1698 1705
1699 1706 if not (entry['pat'] and entry['url']):
1700 1707 log.debug('skipping due to missing data')
1701 1708 continue
1702 1709
1703 1710 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1704 1711 uid, entry['pat'], entry['url'], entry['pref'])
1705 1712
1706 1713 try:
1707 1714 pattern = re.compile(r'%s' % entry['pat'])
1708 1715 except re.error:
1709 1716 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1710 1717 continue
1711 1718
1712 1719 data_func = partial(
1713 1720 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1714 1721 return_raw_data=True)
1715 1722
1716 1723 for match_obj in pattern.finditer(text_string):
1717 1724 issues_data.append(data_func(match_obj))
1718 1725
1719 1726 url_func = partial(
1720 1727 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1721 1728 link_format=link_format)
1722 1729
1723 1730 new_text = pattern.sub(url_func, new_text)
1724 1731 log.debug('processed prefix:uid `%s`', uid)
1725 1732
1733 # finally use global replace, eg !123 -> pr-link, those will not catch
1734 # if already similar pattern exists
1735 pr_entry = {
1736 'pref': '!',
1737 'url': '/_admin/pull-requests/${id}',
1738 'desc': 'Pull Request !${id}'
1739 }
1740 pr_url_func = partial(
1741 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None)
1742 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1743 log.debug('processed !pr pattern')
1744
1726 1745 return new_text, issues_data
1727 1746
1728 1747
1729 1748 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1730 1749 """
1731 1750 Parses given text message and makes proper links.
1732 1751 issues are linked to given issue-server, and rest is a commit link
1733 1752
1734 1753 :param commit_text:
1735 1754 :param repository:
1736 1755 """
1737 def escaper(string):
1738 return string.replace('<', '&lt;').replace('>', '&gt;')
1756 def escaper(_text):
1757 return _text.replace('<', '&lt;').replace('>', '&gt;')
1739 1758
1740 newtext = escaper(commit_text)
1759 new_text = escaper(commit_text)
1741 1760
1742 1761 # extract http/https links and make them real urls
1743 newtext = urlify_text(newtext, safe=False)
1762 new_text = urlify_text(new_text, safe=False)
1744 1763
1745 1764 # urlify commits - extract commit ids and make link out of them, if we have
1746 1765 # the scope of repository present.
1747 1766 if repository:
1748 newtext = urlify_commits(newtext, repository)
1767 new_text = urlify_commits(new_text, repository)
1749 1768
1750 1769 # process issue tracker patterns
1751 newtext, issues = process_patterns(newtext, repository or '',
1752 active_entries=active_pattern_entries)
1770 new_text, issues = process_patterns(new_text, repository or '',
1771 active_entries=active_pattern_entries)
1753 1772
1754 return literal(newtext)
1773 return literal(new_text)
1755 1774
1756 1775
1757 1776 def render_binary(repo_name, file_obj):
1758 1777 """
1759 1778 Choose how to render a binary file
1760 1779 """
1761 1780
1762 1781 filename = file_obj.name
1763 1782
1764 1783 # images
1765 1784 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1766 1785 if fnmatch.fnmatch(filename, pat=ext):
1767 1786 alt = escape(filename)
1768 1787 src = route_path(
1769 1788 'repo_file_raw', repo_name=repo_name,
1770 1789 commit_id=file_obj.commit.raw_id,
1771 1790 f_path=file_obj.path)
1772 1791 return literal(
1773 1792 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1774 1793
1775 1794
1776 1795 def renderer_from_filename(filename, exclude=None):
1777 1796 """
1778 1797 choose a renderer based on filename, this works only for text based files
1779 1798 """
1780 1799
1781 1800 # ipython
1782 1801 for ext in ['*.ipynb']:
1783 1802 if fnmatch.fnmatch(filename, pat=ext):
1784 1803 return 'jupyter'
1785 1804
1786 1805 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1787 1806 if is_markup:
1788 1807 return is_markup
1789 1808 return None
1790 1809
1791 1810
1792 1811 def render(source, renderer='rst', mentions=False, relative_urls=None,
1793 1812 repo_name=None):
1794 1813
1795 1814 def maybe_convert_relative_links(html_source):
1796 1815 if relative_urls:
1797 1816 return relative_links(html_source, relative_urls)
1798 1817 return html_source
1799 1818
1800 1819 if renderer == 'plain':
1801 1820 return literal(
1802 1821 MarkupRenderer.plain(source, leading_newline=False))
1803 1822
1804 1823 elif renderer == 'rst':
1805 1824 if repo_name:
1806 1825 # process patterns on comments if we pass in repo name
1807 1826 source, issues = process_patterns(
1808 1827 source, repo_name, link_format='rst')
1809 1828
1810 1829 return literal(
1811 1830 '<div class="rst-block">%s</div>' %
1812 1831 maybe_convert_relative_links(
1813 1832 MarkupRenderer.rst(source, mentions=mentions)))
1814 1833
1815 1834 elif renderer == 'markdown':
1816 1835 if repo_name:
1817 1836 # process patterns on comments if we pass in repo name
1818 1837 source, issues = process_patterns(
1819 1838 source, repo_name, link_format='markdown')
1820 1839
1821 1840 return literal(
1822 1841 '<div class="markdown-block">%s</div>' %
1823 1842 maybe_convert_relative_links(
1824 1843 MarkupRenderer.markdown(source, flavored=True,
1825 1844 mentions=mentions)))
1826 1845
1827 1846 elif renderer == 'jupyter':
1828 1847 return literal(
1829 1848 '<div class="ipynb">%s</div>' %
1830 1849 maybe_convert_relative_links(
1831 1850 MarkupRenderer.jupyter(source)))
1832 1851
1833 1852 # None means just show the file-source
1834 1853 return None
1835 1854
1836 1855
1837 1856 def commit_status(repo, commit_id):
1838 1857 return ChangesetStatusModel().get_status(repo, commit_id)
1839 1858
1840 1859
1841 1860 def commit_status_lbl(commit_status):
1842 1861 return dict(ChangesetStatus.STATUSES).get(commit_status)
1843 1862
1844 1863
1845 1864 def commit_time(repo_name, commit_id):
1846 1865 repo = Repository.get_by_repo_name(repo_name)
1847 1866 commit = repo.get_commit(commit_id=commit_id)
1848 1867 return commit.date
1849 1868
1850 1869
1851 1870 def get_permission_name(key):
1852 1871 return dict(Permission.PERMS).get(key)
1853 1872
1854 1873
1855 1874 def journal_filter_help(request):
1856 1875 _ = request.translate
1857 1876 from rhodecode.lib.audit_logger import ACTIONS
1858 1877 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1859 1878
1860 1879 return _(
1861 1880 'Example filter terms:\n' +
1862 1881 ' repository:vcs\n' +
1863 1882 ' username:marcin\n' +
1864 1883 ' username:(NOT marcin)\n' +
1865 1884 ' action:*push*\n' +
1866 1885 ' ip:127.0.0.1\n' +
1867 1886 ' date:20120101\n' +
1868 1887 ' date:[20120101100000 TO 20120102]\n' +
1869 1888 '\n' +
1870 1889 'Actions: {actions}\n' +
1871 1890 '\n' +
1872 1891 'Generate wildcards using \'*\' character:\n' +
1873 1892 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1874 1893 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1875 1894 '\n' +
1876 1895 'Optional AND / OR operators in queries\n' +
1877 1896 ' "repository:vcs OR repository:test"\n' +
1878 1897 ' "username:test AND repository:test*"\n'
1879 1898 ).format(actions=actions)
1880 1899
1881 1900
1882 1901 def not_mapped_error(repo_name):
1883 1902 from rhodecode.translation import _
1884 1903 flash(_('%s repository is not mapped to db perhaps'
1885 1904 ' it was created or renamed from the filesystem'
1886 1905 ' please run the application again'
1887 1906 ' in order to rescan repositories') % repo_name, category='error')
1888 1907
1889 1908
1890 1909 def ip_range(ip_addr):
1891 1910 from rhodecode.model.db import UserIpMap
1892 1911 s, e = UserIpMap._get_ip_range(ip_addr)
1893 1912 return '%s - %s' % (s, e)
1894 1913
1895 1914
1896 1915 def form(url, method='post', needs_csrf_token=True, **attrs):
1897 1916 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1898 1917 if method.lower() != 'get' and needs_csrf_token:
1899 1918 raise Exception(
1900 1919 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1901 1920 'CSRF token. If the endpoint does not require such token you can ' +
1902 1921 'explicitly set the parameter needs_csrf_token to false.')
1903 1922
1904 1923 return wh_form(url, method=method, **attrs)
1905 1924
1906 1925
1907 1926 def secure_form(form_url, method="POST", multipart=False, **attrs):
1908 1927 """Start a form tag that points the action to an url. This
1909 1928 form tag will also include the hidden field containing
1910 1929 the auth token.
1911 1930
1912 1931 The url options should be given either as a string, or as a
1913 1932 ``url()`` function. The method for the form defaults to POST.
1914 1933
1915 1934 Options:
1916 1935
1917 1936 ``multipart``
1918 1937 If set to True, the enctype is set to "multipart/form-data".
1919 1938 ``method``
1920 1939 The method to use when submitting the form, usually either
1921 1940 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1922 1941 hidden input with name _method is added to simulate the verb
1923 1942 over POST.
1924 1943
1925 1944 """
1926 1945 from webhelpers.pylonslib.secure_form import insecure_form
1927 1946
1928 1947 if 'request' in attrs:
1929 1948 session = attrs['request'].session
1930 1949 del attrs['request']
1931 1950 else:
1932 1951 raise ValueError(
1933 1952 'Calling this form requires request= to be passed as argument')
1934 1953
1935 1954 form = insecure_form(form_url, method, multipart, **attrs)
1936 1955 token = literal(
1937 1956 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1938 1957 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1939 1958
1940 1959 return literal("%s\n%s" % (form, token))
1941 1960
1942 1961
1943 1962 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1944 1963 select_html = select(name, selected, options, **attrs)
1945 1964
1946 1965 select2 = """
1947 1966 <script>
1948 1967 $(document).ready(function() {
1949 1968 $('#%s').select2({
1950 1969 containerCssClass: 'drop-menu %s',
1951 1970 dropdownCssClass: 'drop-menu-dropdown',
1952 1971 dropdownAutoWidth: true%s
1953 1972 });
1954 1973 });
1955 1974 </script>
1956 1975 """
1957 1976
1958 1977 filter_option = """,
1959 1978 minimumResultsForSearch: -1
1960 1979 """
1961 1980 input_id = attrs.get('id') or name
1962 1981 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1963 1982 filter_enabled = "" if enable_filter else filter_option
1964 1983 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1965 1984
1966 1985 return literal(select_html+select_script)
1967 1986
1968 1987
1969 1988 def get_visual_attr(tmpl_context_var, attr_name):
1970 1989 """
1971 1990 A safe way to get a variable from visual variable of template context
1972 1991
1973 1992 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1974 1993 :param attr_name: name of the attribute we fetch from the c.visual
1975 1994 """
1976 1995 visual = getattr(tmpl_context_var, 'visual', None)
1977 1996 if not visual:
1978 1997 return
1979 1998 else:
1980 1999 return getattr(visual, attr_name, None)
1981 2000
1982 2001
1983 2002 def get_last_path_part(file_node):
1984 2003 if not file_node.path:
1985 2004 return u'/'
1986 2005
1987 2006 path = safe_unicode(file_node.path.split('/')[-1])
1988 2007 return u'../' + path
1989 2008
1990 2009
1991 2010 def route_url(*args, **kwargs):
1992 2011 """
1993 2012 Wrapper around pyramids `route_url` (fully qualified url) function.
1994 2013 """
1995 2014 req = get_current_request()
1996 2015 return req.route_url(*args, **kwargs)
1997 2016
1998 2017
1999 2018 def route_path(*args, **kwargs):
2000 2019 """
2001 2020 Wrapper around pyramids `route_path` function.
2002 2021 """
2003 2022 req = get_current_request()
2004 2023 return req.route_path(*args, **kwargs)
2005 2024
2006 2025
2007 2026 def route_path_or_none(*args, **kwargs):
2008 2027 try:
2009 2028 return route_path(*args, **kwargs)
2010 2029 except KeyError:
2011 2030 return None
2012 2031
2013 2032
2014 2033 def current_route_path(request, **kw):
2015 2034 new_args = request.GET.mixed()
2016 2035 new_args.update(kw)
2017 2036 return request.current_route_path(_query=new_args)
2018 2037
2019 2038
2020 2039 def curl_api_example(method, args):
2021 2040 args_json = json.dumps(OrderedDict([
2022 2041 ('id', 1),
2023 2042 ('auth_token', 'SECRET'),
2024 2043 ('method', method),
2025 2044 ('args', args)
2026 2045 ]))
2027 2046
2028 2047 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2029 2048 api_url=route_url('apiv2'),
2030 2049 args_json=args_json
2031 2050 )
2032 2051
2033 2052
2034 2053 def api_call_example(method, args):
2035 2054 """
2036 2055 Generates an API call example via CURL
2037 2056 """
2038 2057 curl_call = curl_api_example(method, args)
2039 2058
2040 2059 return literal(
2041 2060 curl_call +
2042 2061 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2043 2062 "and needs to be of `api calls` role."
2044 2063 .format(token_url=route_url('my_account_auth_tokens')))
2045 2064
2046 2065
2047 2066 def notification_description(notification, request):
2048 2067 """
2049 2068 Generate notification human readable description based on notification type
2050 2069 """
2051 2070 from rhodecode.model.notification import NotificationModel
2052 2071 return NotificationModel().make_description(
2053 2072 notification, translate=request.translate)
2054 2073
2055 2074
2056 2075 def go_import_header(request, db_repo=None):
2057 2076 """
2058 2077 Creates a header for go-import functionality in Go Lang
2059 2078 """
2060 2079
2061 2080 if not db_repo:
2062 2081 return
2063 2082 if 'go-get' not in request.GET:
2064 2083 return
2065 2084
2066 2085 clone_url = db_repo.clone_url()
2067 2086 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2068 2087 # we have a repo and go-get flag,
2069 2088 return literal('<meta name="go-import" content="{} {} {}">'.format(
2070 2089 prefix, db_repo.repo_type, clone_url))
2071 2090
2072 2091
2073 2092 def reviewer_as_json(*args, **kwargs):
2074 2093 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2075 2094 return _reviewer_as_json(*args, **kwargs)
2076 2095
2077 2096
2078 2097 def get_repo_view_type(request):
2079 2098 route_name = request.matched_route.name
2080 2099 route_to_view_type = {
2081 2100 'repo_changelog': 'commits',
2082 2101 'repo_commits': 'commits',
2083 2102 'repo_files': 'files',
2084 2103 'repo_summary': 'summary',
2085 2104 'repo_commit': 'commit'
2086 2105 }
2087 2106
2088 2107 return route_to_view_type.get(route_name)
@@ -1,219 +1,219 b''
1 1 ## snippet for displaying issue tracker settings
2 2 ## usage:
3 3 ## <%namespace name="its" file="/base/issue_tracker_settings.mako"/>
4 4 ## ${its.issue_tracker_settings_table(patterns, form_url, delete_url)}
5 5 ## ${its.issue_tracker_settings_test(test_url)}
6 6
7 7 <%def name="issue_tracker_settings_table(patterns, form_url, delete_url)">
8 8 <table class="rctable issuetracker">
9 9 <tr>
10 10 <th>${_('Description')}</th>
11 11 <th>${_('Pattern')}</th>
12 12 <th>${_('Url')}</th>
13 13 <th>${_('Prefix')}</th>
14 14 <th ></th>
15 15 </tr>
16 16 <tr>
17 17 <td class="td-description issue-tracker-example">Example</td>
18 18 <td class="td-regex issue-tracker-example">${'(?:#)(?P<issue_id>\d+)'}</td>
19 19 <td class="td-url issue-tracker-example">${'https://myissueserver.com/${repo}/issue/${issue_id}'}</td>
20 20 <td class="td-prefix issue-tracker-example">#</td>
21 21 <td class="issue-tracker-example"><a href="${h.route_url('enterprise_issue_tracker_settings')}" target="_blank">${_('Read more')}</a></td>
22 22 </tr>
23 23 %for uid, entry in patterns:
24 24 <tr id="entry_${uid}">
25 25 <td class="td-description issuetracker_desc">
26 26 <span class="entry">
27 27 ${entry.desc}
28 28 </span>
29 29 <span class="edit">
30 30 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
31 31 </span>
32 32 </td>
33 33 <td class="td-regex issuetracker_pat">
34 34 <span class="entry">
35 35 ${entry.pat}
36 36 </span>
37 37 <span class="edit">
38 38 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
39 39 </span>
40 40 </td>
41 41 <td class="td-url issuetracker_url">
42 42 <span class="entry">
43 43 ${entry.url}
44 44 </span>
45 45 <span class="edit">
46 46 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
47 47 </span>
48 48 </td>
49 49 <td class="td-prefix issuetracker_pref">
50 50 <span class="entry">
51 51 ${entry.pref}
52 52 </span>
53 53 <span class="edit">
54 54 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
55 55 </span>
56 56 </td>
57 57 <td class="td-action">
58 58 <div class="grid_edit">
59 59 <span class="entry">
60 60 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
61 61 </span>
62 62 <span class="edit">
63 ${h.hidden('uid', uid)}
63 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
64 64 </span>
65 65 </div>
66 66 <div class="grid_delete">
67 67 <span class="entry">
68 68 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
69 69 ${_('Delete')}
70 70 </a>
71 71 </span>
72 72 <span class="edit">
73 73 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
74 74 </span>
75 75 </div>
76 76 </td>
77 77 </tr>
78 78 %endfor
79 79 <tr id="last-row"></tr>
80 80 </table>
81 81 <p>
82 82 <a id="add_pattern" class="link">
83 83 ${_('Add new')}
84 84 </a>
85 85 </p>
86 86
87 87 <script type="text/javascript">
88 88 var newEntryLabel = $('label[for="new_entry"]');
89 89
90 90 var resetEntry = function() {
91 91 newEntryLabel.text("${_('New Entry')}:");
92 92 };
93 93
94 94 var delete_pattern = function(entry) {
95 95 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
96 var request = $.ajax({
96 $.ajax({
97 97 type: "POST",
98 98 url: "${delete_url}",
99 99 data: {
100 '_method': 'delete',
101 100 'csrf_token': CSRF_TOKEN,
102 101 'uid':$(entry).data('uid')
103 102 },
104 103 success: function(){
105 104 location.reload();
106 105 },
107 106 error: function(data, textStatus, errorThrown){
108 107 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
109 108 }
110 109 });
111 110 }
112 111 };
113 112
114 113 $('.delete_issuetracker_entry').on('click', function(e){
115 114 e.preventDefault();
116 115 delete_pattern(this);
117 116 });
118 117
119 118 $('.edit_issuetracker_entry').on('click', function(e){
120 119 e.preventDefault();
121 120 $(this).parents('tr').addClass('editopen');
122 121 });
123 122
124 123 $('.edit_issuetracker_cancel').on('click', function(e){
125 124 e.preventDefault();
126 125 $(this).parents('tr').removeClass('editopen');
127 126 // Reset to original value
128 127 var uid = $(this).data('uid');
129 128 $('#'+uid+' input').each(function(e) {
130 129 this.value = this.defaultValue;
131 130 });
132 131 });
133 132
134 133 $('input#reset').on('click', function(e) {
135 134 resetEntry();
136 135 });
137 136
138 137 $('#add_pattern').on('click', function(e) {
139 138 addNewPatternInput();
140 139 });
141 140 </script>
142 141 </%def>
143 142
144 143 <%def name="issue_tracker_new_row()">
145 144 <table id="add-row-tmpl" style="display: none;">
146 145 <tbody>
147 146 <tr class="new_pattern">
148 147 <td class="td-description issuetracker_desc">
149 148 <span class="entry">
150 149 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
151 150 </span>
152 151 </td>
153 152 <td class="td-regex issuetracker_pat">
154 153 <span class="entry">
155 154 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
156 155 value="##PATTERN##" type="text">
157 156 </span>
158 157 </td>
159 158 <td class="td-url issuetracker_url">
160 159 <span class="entry">
161 160 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
162 161 </span>
163 162 </td>
164 163 <td class="td-prefix issuetracker_pref">
165 164 <span class="entry">
166 165 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
167 166 </span>
168 167 </td>
169 168 <td class="td-action">
170 169 </td>
171 170 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
172 171 </tr>
173 172 </tbody>
174 173 </table>
175 174 </%def>
176 175
177 176 <%def name="issue_tracker_settings_test(test_url)">
178 177 <div class="form-vertical">
179 178 <div class="fields">
180 179 <div class="field">
181 180 <div class='textarea-full'>
182 181 <textarea id="test_pattern_data" >
183 182 This commit fixes ticket #451.
184 183 This is an example text for testing issue tracker patterns, add a pattern here and
185 hit preview to see the link
184 hit preview to see the link.
185 Open a pull request !101 to contribute !
186 186 </textarea>
187 187 </div>
188 188 </div>
189 189 </div>
190 190 <div class="test_pattern_preview">
191 191 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
192 192 <p>${_('Test Pattern Preview')}</p>
193 <div id="test_pattern_result"></div>
193 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
194 194 </div>
195 195 </div>
196 196
197 197 <script type="text/javascript">
198 198 $('#test_pattern').on('click', function(e) {
199 199 $.ajax({
200 200 type: "POST",
201 201 url: "${test_url}",
202 202 data: {
203 203 'test_text': $('#test_pattern_data').val(),
204 204 'csrf_token': CSRF_TOKEN
205 205 },
206 206 success: function(data){
207 207 $('#test_pattern_result').html(data);
208 208 tooltipActivate();
209 209 },
210 210 error: function(jqXHR, textStatus, errorThrown){
211 211 $('#test_pattern_result').html('Error: ' + errorThrown);
212 212 }
213 213 });
214 214 $('#test_pattern_result').show();
215 215 });
216 216 </script>
217 217 </%def>
218 218
219 219
General Comments 0
You need to be logged in to leave comments. Login now