##// END OF EJS Templates
issue-tracker: moved example to the input box...
dan -
r4111:e62fd087 default
parent child Browse files
Show More
@@ -1,81 +1,76 b''
1 1 .. _rhodecode-issue-trackers-ref:
2 2
3 3 Issue Tracker Integration
4 4 =========================
5 5
6 6 You can set an issue tracker connection in two ways with |RCE|.
7 7
8 8 * At the instance level, you can set a default issue tracker.
9 9 * At the |repo| level, you can configure an integration with a different issue
10 10 tracker.
11 11
12 12 To integrate |RCE| with an issue tracker, you need to define a regular
13 13 expression that will fetch the issue ID stored in commit messages, and replace
14 14 it with a URL. This enables |RCE| to generate a link matching each issue to the
15 15 target |repo|.
16 16
17 17 Default Issue Tracker Configuration
18 18 -----------------------------------
19 19
20 20 To integrate your issue tracker, use the following steps:
21 21
22 22 1. Open :menuselection:`Admin --> Settings --> Issue Tracker`.
23 23 2. In the new entry field, enter the following information:
24 24
25 25 * :guilabel:`Description`: A name for this set of rules.
26 26 * :guilabel:`Pattern`: The regular expression that will match issues
27 27 tagged in commit messages, or more see :ref:`issue-tr-eg-ref`.
28 28 * :guilabel:`URL`: The URL to your issue tracker.
29 29 * :guilabel:`Prefix`: The prefix with which you want to mark issues.
30 30
31 31 3. Select **Add** so save the rule to your issue tracker configuration.
32 32
33 33 Repository Issue Tracker Configuration
34 34 --------------------------------------
35 35
36 36 You can configure specific |repos| to use a different issue tracker than the
37 37 default one. See the instructions in :ref:`repo-it`
38 38
39 39 .. _issue-tr-eg-ref:
40 40
41
41 42 Jira Integration
42 43 ----------------
43 44
44 * Regex = ``(?:^#|\s#)(\w+-\d+)``
45 * URL = ``https://myissueserver.com/browse/${id}``
46 * Issue Prefix = ``#``
45 Please check examples in the view for configuration the issue trackers.
46
47 47
48 48 Confluence (Wiki)
49 49 -----------------
50 50
51 * Regex = ``(?:conf-)([A-Z0-9]+)``
52 * URL = ``https://example.atlassian.net/display/wiki/${id}/${repo_name}``
53 * issue prefix = ``CONF-``
51 Please check examples in the view for configuration the issue trackers.
52
54 53
55 54 Redmine Integration
56 55 -------------------
57 56
58 * Regex = ``(issue-+\d+)``
59 * URL = ``https://myissueserver.com/redmine/issue/${id}``
60 * Issue Prefix = ``issue-``
57 Please check examples in the view for configuration the issue trackers.
58
61 59
62 Redmine (wiki)
63 --------------
60 Redmine wiki Integration
61 ------------------------
64 62
65 * Regex = ``(?:wiki-)([a-zA-Z0-9]+)``
66 * URL = ``https://example.com/redmine/projects/wiki/${repo_name}``
67 * Issue prefix = ``Issue-``
63 Please check examples in the view for configuration the issue trackers.
64
68 65
69 66 Pivotal Tracker
70 67 ---------------
71 68
72 * Regex = ``(?:pivot-)(?<project_id>\d+)-(?<story>\d+)``
73 * URL = ``https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}``
74 * Issue prefix = ``Piv-``
69 Please check examples in the view for configuration the issue trackers.
70
75 71
76 72 Trello
77 73 ------
78 74
79 * Regex = ``(?:trello-)(?<card_id>[a-zA-Z0-9]+)``
80 * URL = ``https://trello.com/example.com/${card_id}``
81 * Issue prefix = ``Trello-``
75 Please check examples in the view for configuration the issue trackers.
76
@@ -1,742 +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 self, autologin_user, backend, csrf_token, settings_util):
729 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
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 737 'uid': uid,
738 738 'csrf_token': csrf_token
739 739 }
740 self.app.post(post_url, post_data, status=302)
740 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
741 741 settings = SettingsModel().get_all_settings()
742 742 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
@@ -1,780 +1,783 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 import logging
23 23 import collections
24 24
25 25 import datetime
26 26 import formencode
27 27 import formencode.htmlfill
28 28
29 29 import rhodecode
30 30 from pyramid.view import view_config
31 31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 from rhodecode.apps._base import BaseAppView
36 36 from rhodecode.apps._base.navigation import navigation_list
37 37 from rhodecode.apps.svn_support.config_keys import generate_config
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 41 from rhodecode.lib.celerylib import tasks, run_task
42 42 from rhodecode.lib.utils import repo2db_mapper
43 43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 44 from rhodecode.lib.index import searcher_from_config
45 45
46 46 from rhodecode.model.db import RhodeCodeUi, Repository
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70
71 71 return c
72 72
73 73 @classmethod
74 74 def _get_ui_settings(cls):
75 75 ret = RhodeCodeUi.query().all()
76 76
77 77 if not ret:
78 78 raise Exception('Could not get application ui settings !')
79 79 settings = {}
80 80 for each in ret:
81 81 k = each.ui_key
82 82 v = each.ui_value
83 83 if k == '/':
84 84 k = 'root_path'
85 85
86 86 if k in ['push_ssl', 'publish', 'enabled']:
87 87 v = str2bool(v)
88 88
89 89 if k.find('.') != -1:
90 90 k = k.replace('.', '_')
91 91
92 92 if each.ui_section in ['hooks', 'extensions']:
93 93 v = each.ui_active
94 94
95 95 settings[each.ui_section + '_' + k] = v
96 96 return settings
97 97
98 98 @classmethod
99 99 def _form_defaults(cls):
100 100 defaults = SettingsModel().get_all_settings()
101 101 defaults.update(cls._get_ui_settings())
102 102
103 103 defaults.update({
104 104 'new_svn_branch': '',
105 105 'new_svn_tag': '',
106 106 })
107 107 return defaults
108 108
109 109 @LoginRequired()
110 110 @HasPermissionAllDecorator('hg.admin')
111 111 @view_config(
112 112 route_name='admin_settings_vcs', request_method='GET',
113 113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 114 def settings_vcs(self):
115 115 c = self.load_default_context()
116 116 c.active = 'vcs'
117 117 model = VcsSettingsModel()
118 118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120 120
121 121 settings = self.request.registry.settings
122 122 c.svn_proxy_generate_config = settings[generate_config]
123 123
124 124 defaults = self._form_defaults()
125 125
126 126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127 127
128 128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 129 self._get_template_context(c), self.request)
130 130 html = formencode.htmlfill.render(
131 131 data,
132 132 defaults=defaults,
133 133 encoding="UTF-8",
134 134 force_defaults=False
135 135 )
136 136 return Response(html)
137 137
138 138 @LoginRequired()
139 139 @HasPermissionAllDecorator('hg.admin')
140 140 @CSRFRequired()
141 141 @view_config(
142 142 route_name='admin_settings_vcs_update', request_method='POST',
143 143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 144 def settings_vcs_update(self):
145 145 _ = self.request.translate
146 146 c = self.load_default_context()
147 147 c.active = 'vcs'
148 148
149 149 model = VcsSettingsModel()
150 150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152 152
153 153 settings = self.request.registry.settings
154 154 c.svn_proxy_generate_config = settings[generate_config]
155 155
156 156 application_form = ApplicationUiSettingsForm(self.request.translate)()
157 157
158 158 try:
159 159 form_result = application_form.to_python(dict(self.request.POST))
160 160 except formencode.Invalid as errors:
161 161 h.flash(
162 162 _("Some form inputs contain invalid data."),
163 163 category='error')
164 164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 165 self._get_template_context(c), self.request)
166 166 html = formencode.htmlfill.render(
167 167 data,
168 168 defaults=errors.value,
169 169 errors=errors.error_dict or {},
170 170 prefix_error=False,
171 171 encoding="UTF-8",
172 172 force_defaults=False
173 173 )
174 174 return Response(html)
175 175
176 176 try:
177 177 if c.visual.allow_repo_location_change:
178 178 model.update_global_path_setting(form_result['paths_root_path'])
179 179
180 180 model.update_global_ssl_setting(form_result['web_push_ssl'])
181 181 model.update_global_hook_settings(form_result)
182 182
183 183 model.create_or_update_global_svn_settings(form_result)
184 184 model.create_or_update_global_hg_settings(form_result)
185 185 model.create_or_update_global_git_settings(form_result)
186 186 model.create_or_update_global_pr_settings(form_result)
187 187 except Exception:
188 188 log.exception("Exception while updating settings")
189 189 h.flash(_('Error occurred during updating '
190 190 'application settings'), category='error')
191 191 else:
192 192 Session().commit()
193 193 h.flash(_('Updated VCS settings'), category='success')
194 194 raise HTTPFound(h.route_path('admin_settings_vcs'))
195 195
196 196 data = render('rhodecode:templates/admin/settings/settings.mako',
197 197 self._get_template_context(c), self.request)
198 198 html = formencode.htmlfill.render(
199 199 data,
200 200 defaults=self._form_defaults(),
201 201 encoding="UTF-8",
202 202 force_defaults=False
203 203 )
204 204 return Response(html)
205 205
206 206 @LoginRequired()
207 207 @HasPermissionAllDecorator('hg.admin')
208 208 @CSRFRequired()
209 209 @view_config(
210 210 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
211 211 renderer='json_ext', xhr=True)
212 212 def settings_vcs_delete_svn_pattern(self):
213 213 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
214 214 model = VcsSettingsModel()
215 215 try:
216 216 model.delete_global_svn_pattern(delete_pattern_id)
217 217 except SettingNotFound:
218 218 log.exception(
219 219 'Failed to delete svn_pattern with id %s', delete_pattern_id)
220 220 raise HTTPNotFound()
221 221
222 222 Session().commit()
223 223 return True
224 224
225 225 @LoginRequired()
226 226 @HasPermissionAllDecorator('hg.admin')
227 227 @view_config(
228 228 route_name='admin_settings_mapping', request_method='GET',
229 229 renderer='rhodecode:templates/admin/settings/settings.mako')
230 230 def settings_mapping(self):
231 231 c = self.load_default_context()
232 232 c.active = 'mapping'
233 233
234 234 data = render('rhodecode:templates/admin/settings/settings.mako',
235 235 self._get_template_context(c), self.request)
236 236 html = formencode.htmlfill.render(
237 237 data,
238 238 defaults=self._form_defaults(),
239 239 encoding="UTF-8",
240 240 force_defaults=False
241 241 )
242 242 return Response(html)
243 243
244 244 @LoginRequired()
245 245 @HasPermissionAllDecorator('hg.admin')
246 246 @CSRFRequired()
247 247 @view_config(
248 248 route_name='admin_settings_mapping_update', request_method='POST',
249 249 renderer='rhodecode:templates/admin/settings/settings.mako')
250 250 def settings_mapping_update(self):
251 251 _ = self.request.translate
252 252 c = self.load_default_context()
253 253 c.active = 'mapping'
254 254 rm_obsolete = self.request.POST.get('destroy', False)
255 255 invalidate_cache = self.request.POST.get('invalidate', False)
256 256 log.debug(
257 257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
258 258
259 259 if invalidate_cache:
260 260 log.debug('invalidating all repositories cache')
261 261 for repo in Repository.get_all():
262 262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263 263
264 264 filesystem_repos = ScmModel().repo_scan()
265 265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 266 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 267 h.flash(_('Repositories successfully '
268 268 'rescanned added: %s ; removed: %s') %
269 269 (_repr(added), _repr(removed)),
270 270 category='success')
271 271 raise HTTPFound(h.route_path('admin_settings_mapping'))
272 272
273 273 @LoginRequired()
274 274 @HasPermissionAllDecorator('hg.admin')
275 275 @view_config(
276 276 route_name='admin_settings', request_method='GET',
277 277 renderer='rhodecode:templates/admin/settings/settings.mako')
278 278 @view_config(
279 279 route_name='admin_settings_global', request_method='GET',
280 280 renderer='rhodecode:templates/admin/settings/settings.mako')
281 281 def settings_global(self):
282 282 c = self.load_default_context()
283 283 c.active = 'global'
284 284 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 285 .get_personal_group_name_pattern()
286 286
287 287 data = render('rhodecode:templates/admin/settings/settings.mako',
288 288 self._get_template_context(c), self.request)
289 289 html = formencode.htmlfill.render(
290 290 data,
291 291 defaults=self._form_defaults(),
292 292 encoding="UTF-8",
293 293 force_defaults=False
294 294 )
295 295 return Response(html)
296 296
297 297 @LoginRequired()
298 298 @HasPermissionAllDecorator('hg.admin')
299 299 @CSRFRequired()
300 300 @view_config(
301 301 route_name='admin_settings_update', request_method='POST',
302 302 renderer='rhodecode:templates/admin/settings/settings.mako')
303 303 @view_config(
304 304 route_name='admin_settings_global_update', request_method='POST',
305 305 renderer='rhodecode:templates/admin/settings/settings.mako')
306 306 def settings_global_update(self):
307 307 _ = self.request.translate
308 308 c = self.load_default_context()
309 309 c.active = 'global'
310 310 c.personal_repo_group_default_pattern = RepoGroupModel()\
311 311 .get_personal_group_name_pattern()
312 312 application_form = ApplicationSettingsForm(self.request.translate)()
313 313 try:
314 314 form_result = application_form.to_python(dict(self.request.POST))
315 315 except formencode.Invalid as errors:
316 316 h.flash(
317 317 _("Some form inputs contain invalid data."),
318 318 category='error')
319 319 data = render('rhodecode:templates/admin/settings/settings.mako',
320 320 self._get_template_context(c), self.request)
321 321 html = formencode.htmlfill.render(
322 322 data,
323 323 defaults=errors.value,
324 324 errors=errors.error_dict or {},
325 325 prefix_error=False,
326 326 encoding="UTF-8",
327 327 force_defaults=False
328 328 )
329 329 return Response(html)
330 330
331 331 settings = [
332 332 ('title', 'rhodecode_title', 'unicode'),
333 333 ('realm', 'rhodecode_realm', 'unicode'),
334 334 ('pre_code', 'rhodecode_pre_code', 'unicode'),
335 335 ('post_code', 'rhodecode_post_code', 'unicode'),
336 336 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
337 337 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
338 338 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
339 339 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
340 340 ]
341 341 try:
342 342 for setting, form_key, type_ in settings:
343 343 sett = SettingsModel().create_or_update_setting(
344 344 setting, form_result[form_key], type_)
345 345 Session().add(sett)
346 346
347 347 Session().commit()
348 348 SettingsModel().invalidate_settings_cache()
349 349 h.flash(_('Updated application settings'), category='success')
350 350 except Exception:
351 351 log.exception("Exception while updating application settings")
352 352 h.flash(
353 353 _('Error occurred during updating application settings'),
354 354 category='error')
355 355
356 356 raise HTTPFound(h.route_path('admin_settings_global'))
357 357
358 358 @LoginRequired()
359 359 @HasPermissionAllDecorator('hg.admin')
360 360 @view_config(
361 361 route_name='admin_settings_visual', request_method='GET',
362 362 renderer='rhodecode:templates/admin/settings/settings.mako')
363 363 def settings_visual(self):
364 364 c = self.load_default_context()
365 365 c.active = 'visual'
366 366
367 367 data = render('rhodecode:templates/admin/settings/settings.mako',
368 368 self._get_template_context(c), self.request)
369 369 html = formencode.htmlfill.render(
370 370 data,
371 371 defaults=self._form_defaults(),
372 372 encoding="UTF-8",
373 373 force_defaults=False
374 374 )
375 375 return Response(html)
376 376
377 377 @LoginRequired()
378 378 @HasPermissionAllDecorator('hg.admin')
379 379 @CSRFRequired()
380 380 @view_config(
381 381 route_name='admin_settings_visual_update', request_method='POST',
382 382 renderer='rhodecode:templates/admin/settings/settings.mako')
383 383 def settings_visual_update(self):
384 384 _ = self.request.translate
385 385 c = self.load_default_context()
386 386 c.active = 'visual'
387 387 application_form = ApplicationVisualisationForm(self.request.translate)()
388 388 try:
389 389 form_result = application_form.to_python(dict(self.request.POST))
390 390 except formencode.Invalid as errors:
391 391 h.flash(
392 392 _("Some form inputs contain invalid data."),
393 393 category='error')
394 394 data = render('rhodecode:templates/admin/settings/settings.mako',
395 395 self._get_template_context(c), self.request)
396 396 html = formencode.htmlfill.render(
397 397 data,
398 398 defaults=errors.value,
399 399 errors=errors.error_dict or {},
400 400 prefix_error=False,
401 401 encoding="UTF-8",
402 402 force_defaults=False
403 403 )
404 404 return Response(html)
405 405
406 406 try:
407 407 settings = [
408 408 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
409 409 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
410 410 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
411 411 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
412 412 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
413 413 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
414 414 ('show_version', 'rhodecode_show_version', 'bool'),
415 415 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
416 416 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
417 417 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
418 418 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
419 419 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
420 420 ('support_url', 'rhodecode_support_url', 'unicode'),
421 421 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
422 422 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
423 423 ]
424 424 for setting, form_key, type_ in settings:
425 425 sett = SettingsModel().create_or_update_setting(
426 426 setting, form_result[form_key], type_)
427 427 Session().add(sett)
428 428
429 429 Session().commit()
430 430 SettingsModel().invalidate_settings_cache()
431 431 h.flash(_('Updated visualisation settings'), category='success')
432 432 except Exception:
433 433 log.exception("Exception updating visualization settings")
434 434 h.flash(_('Error occurred during updating '
435 435 'visualisation settings'),
436 436 category='error')
437 437
438 438 raise HTTPFound(h.route_path('admin_settings_visual'))
439 439
440 440 @LoginRequired()
441 441 @HasPermissionAllDecorator('hg.admin')
442 442 @view_config(
443 443 route_name='admin_settings_issuetracker', request_method='GET',
444 444 renderer='rhodecode:templates/admin/settings/settings.mako')
445 445 def settings_issuetracker(self):
446 446 c = self.load_default_context()
447 447 c.active = 'issuetracker'
448 448 defaults = c.rc_config
449 449
450 450 entry_key = 'rhodecode_issuetracker_pat_'
451 451
452 452 c.issuetracker_entries = {}
453 453 for k, v in defaults.items():
454 454 if k.startswith(entry_key):
455 455 uid = k[len(entry_key):]
456 456 c.issuetracker_entries[uid] = None
457 457
458 458 for uid in c.issuetracker_entries:
459 459 c.issuetracker_entries[uid] = AttributeDict({
460 460 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
461 461 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
462 462 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
463 463 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
464 464 })
465 465
466 466 return self._get_template_context(c)
467 467
468 468 @LoginRequired()
469 469 @HasPermissionAllDecorator('hg.admin')
470 470 @CSRFRequired()
471 471 @view_config(
472 472 route_name='admin_settings_issuetracker_test', request_method='POST',
473 473 renderer='string', xhr=True)
474 474 def settings_issuetracker_test(self):
475 475 return h.urlify_commit_message(
476 476 self.request.POST.get('test_text', ''),
477 477 'repo_group/test_repo1')
478 478
479 479 @LoginRequired()
480 480 @HasPermissionAllDecorator('hg.admin')
481 481 @CSRFRequired()
482 482 @view_config(
483 483 route_name='admin_settings_issuetracker_update', request_method='POST',
484 484 renderer='rhodecode:templates/admin/settings/settings.mako')
485 485 def settings_issuetracker_update(self):
486 486 _ = self.request.translate
487 487 self.load_default_context()
488 488 settings_model = IssueTrackerSettingsModel()
489 489
490 490 try:
491 491 form = IssueTrackerPatternsForm(self.request.translate)()
492 492 data = form.to_python(self.request.POST)
493 493 except formencode.Invalid as errors:
494 494 log.exception('Failed to add new pattern')
495 495 error = errors
496 496 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
497 497 category='error')
498 498 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
499 499
500 500 if data:
501 501 for uid in data.get('delete_patterns', []):
502 502 settings_model.delete_entries(uid)
503 503
504 504 for pattern in data.get('patterns', []):
505 505 for setting, value, type_ in pattern:
506 506 sett = settings_model.create_or_update_setting(
507 507 setting, value, type_)
508 508 Session().add(sett)
509 509
510 510 Session().commit()
511 511
512 512 SettingsModel().invalidate_settings_cache()
513 513 h.flash(_('Updated issue tracker entries'), category='success')
514 514 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
515 515
516 516 @LoginRequired()
517 517 @HasPermissionAllDecorator('hg.admin')
518 518 @CSRFRequired()
519 519 @view_config(
520 520 route_name='admin_settings_issuetracker_delete', request_method='POST',
521 renderer='rhodecode:templates/admin/settings/settings.mako')
521 renderer='json_ext', xhr=True)
522 522 def settings_issuetracker_delete(self):
523 523 _ = self.request.translate
524 524 self.load_default_context()
525 525 uid = self.request.POST.get('uid')
526 526 try:
527 527 IssueTrackerSettingsModel().delete_entries(uid)
528 528 except Exception:
529 529 log.exception('Failed to delete issue tracker setting %s', uid)
530 530 raise HTTPNotFound()
531 h.flash(_('Removed issue tracker entry'), category='success')
532 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
531
532 SettingsModel().invalidate_settings_cache()
533 h.flash(_('Removed issue tracker entry.'), category='success')
534
535 return {'deleted': uid}
533 536
534 537 @LoginRequired()
535 538 @HasPermissionAllDecorator('hg.admin')
536 539 @view_config(
537 540 route_name='admin_settings_email', request_method='GET',
538 541 renderer='rhodecode:templates/admin/settings/settings.mako')
539 542 def settings_email(self):
540 543 c = self.load_default_context()
541 544 c.active = 'email'
542 545 c.rhodecode_ini = rhodecode.CONFIG
543 546
544 547 data = render('rhodecode:templates/admin/settings/settings.mako',
545 548 self._get_template_context(c), self.request)
546 549 html = formencode.htmlfill.render(
547 550 data,
548 551 defaults=self._form_defaults(),
549 552 encoding="UTF-8",
550 553 force_defaults=False
551 554 )
552 555 return Response(html)
553 556
554 557 @LoginRequired()
555 558 @HasPermissionAllDecorator('hg.admin')
556 559 @CSRFRequired()
557 560 @view_config(
558 561 route_name='admin_settings_email_update', request_method='POST',
559 562 renderer='rhodecode:templates/admin/settings/settings.mako')
560 563 def settings_email_update(self):
561 564 _ = self.request.translate
562 565 c = self.load_default_context()
563 566 c.active = 'email'
564 567
565 568 test_email = self.request.POST.get('test_email')
566 569
567 570 if not test_email:
568 571 h.flash(_('Please enter email address'), category='error')
569 572 raise HTTPFound(h.route_path('admin_settings_email'))
570 573
571 574 email_kwargs = {
572 575 'date': datetime.datetime.now(),
573 576 'user': c.rhodecode_user,
574 577 'rhodecode_version': c.rhodecode_version
575 578 }
576 579
577 580 (subject, headers, email_body,
578 581 email_body_plaintext) = EmailNotificationModel().render_email(
579 582 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
580 583
581 584 recipients = [test_email] if test_email else None
582 585
583 586 run_task(tasks.send_email, recipients, subject,
584 587 email_body_plaintext, email_body)
585 588
586 589 h.flash(_('Send email task created'), category='success')
587 590 raise HTTPFound(h.route_path('admin_settings_email'))
588 591
589 592 @LoginRequired()
590 593 @HasPermissionAllDecorator('hg.admin')
591 594 @view_config(
592 595 route_name='admin_settings_hooks', request_method='GET',
593 596 renderer='rhodecode:templates/admin/settings/settings.mako')
594 597 def settings_hooks(self):
595 598 c = self.load_default_context()
596 599 c.active = 'hooks'
597 600
598 601 model = SettingsModel()
599 602 c.hooks = model.get_builtin_hooks()
600 603 c.custom_hooks = model.get_custom_hooks()
601 604
602 605 data = render('rhodecode:templates/admin/settings/settings.mako',
603 606 self._get_template_context(c), self.request)
604 607 html = formencode.htmlfill.render(
605 608 data,
606 609 defaults=self._form_defaults(),
607 610 encoding="UTF-8",
608 611 force_defaults=False
609 612 )
610 613 return Response(html)
611 614
612 615 @LoginRequired()
613 616 @HasPermissionAllDecorator('hg.admin')
614 617 @CSRFRequired()
615 618 @view_config(
616 619 route_name='admin_settings_hooks_update', request_method='POST',
617 620 renderer='rhodecode:templates/admin/settings/settings.mako')
618 621 @view_config(
619 622 route_name='admin_settings_hooks_delete', request_method='POST',
620 623 renderer='rhodecode:templates/admin/settings/settings.mako')
621 624 def settings_hooks_update(self):
622 625 _ = self.request.translate
623 626 c = self.load_default_context()
624 627 c.active = 'hooks'
625 628 if c.visual.allow_custom_hooks_settings:
626 629 ui_key = self.request.POST.get('new_hook_ui_key')
627 630 ui_value = self.request.POST.get('new_hook_ui_value')
628 631
629 632 hook_id = self.request.POST.get('hook_id')
630 633 new_hook = False
631 634
632 635 model = SettingsModel()
633 636 try:
634 637 if ui_value and ui_key:
635 638 model.create_or_update_hook(ui_key, ui_value)
636 639 h.flash(_('Added new hook'), category='success')
637 640 new_hook = True
638 641 elif hook_id:
639 642 RhodeCodeUi.delete(hook_id)
640 643 Session().commit()
641 644
642 645 # check for edits
643 646 update = False
644 647 _d = self.request.POST.dict_of_lists()
645 648 for k, v in zip(_d.get('hook_ui_key', []),
646 649 _d.get('hook_ui_value_new', [])):
647 650 model.create_or_update_hook(k, v)
648 651 update = True
649 652
650 653 if update and not new_hook:
651 654 h.flash(_('Updated hooks'), category='success')
652 655 Session().commit()
653 656 except Exception:
654 657 log.exception("Exception during hook creation")
655 658 h.flash(_('Error occurred during hook creation'),
656 659 category='error')
657 660
658 661 raise HTTPFound(h.route_path('admin_settings_hooks'))
659 662
660 663 @LoginRequired()
661 664 @HasPermissionAllDecorator('hg.admin')
662 665 @view_config(
663 666 route_name='admin_settings_search', request_method='GET',
664 667 renderer='rhodecode:templates/admin/settings/settings.mako')
665 668 def settings_search(self):
666 669 c = self.load_default_context()
667 670 c.active = 'search'
668 671
669 672 c.searcher = searcher_from_config(self.request.registry.settings)
670 673 c.statistics = c.searcher.statistics(self.request.translate)
671 674
672 675 return self._get_template_context(c)
673 676
674 677 @LoginRequired()
675 678 @HasPermissionAllDecorator('hg.admin')
676 679 @view_config(
677 680 route_name='admin_settings_automation', request_method='GET',
678 681 renderer='rhodecode:templates/admin/settings/settings.mako')
679 682 def settings_automation(self):
680 683 c = self.load_default_context()
681 684 c.active = 'automation'
682 685
683 686 return self._get_template_context(c)
684 687
685 688 @LoginRequired()
686 689 @HasPermissionAllDecorator('hg.admin')
687 690 @view_config(
688 691 route_name='admin_settings_labs', request_method='GET',
689 692 renderer='rhodecode:templates/admin/settings/settings.mako')
690 693 def settings_labs(self):
691 694 c = self.load_default_context()
692 695 if not c.labs_active:
693 696 raise HTTPFound(h.route_path('admin_settings'))
694 697
695 698 c.active = 'labs'
696 699 c.lab_settings = _LAB_SETTINGS
697 700
698 701 data = render('rhodecode:templates/admin/settings/settings.mako',
699 702 self._get_template_context(c), self.request)
700 703 html = formencode.htmlfill.render(
701 704 data,
702 705 defaults=self._form_defaults(),
703 706 encoding="UTF-8",
704 707 force_defaults=False
705 708 )
706 709 return Response(html)
707 710
708 711 @LoginRequired()
709 712 @HasPermissionAllDecorator('hg.admin')
710 713 @CSRFRequired()
711 714 @view_config(
712 715 route_name='admin_settings_labs_update', request_method='POST',
713 716 renderer='rhodecode:templates/admin/settings/settings.mako')
714 717 def settings_labs_update(self):
715 718 _ = self.request.translate
716 719 c = self.load_default_context()
717 720 c.active = 'labs'
718 721
719 722 application_form = LabsSettingsForm(self.request.translate)()
720 723 try:
721 724 form_result = application_form.to_python(dict(self.request.POST))
722 725 except formencode.Invalid as errors:
723 726 h.flash(
724 727 _("Some form inputs contain invalid data."),
725 728 category='error')
726 729 data = render('rhodecode:templates/admin/settings/settings.mako',
727 730 self._get_template_context(c), self.request)
728 731 html = formencode.htmlfill.render(
729 732 data,
730 733 defaults=errors.value,
731 734 errors=errors.error_dict or {},
732 735 prefix_error=False,
733 736 encoding="UTF-8",
734 737 force_defaults=False
735 738 )
736 739 return Response(html)
737 740
738 741 try:
739 742 session = Session()
740 743 for setting in _LAB_SETTINGS:
741 744 setting_name = setting.key[len('rhodecode_'):]
742 745 sett = SettingsModel().create_or_update_setting(
743 746 setting_name, form_result[setting.key], setting.type)
744 747 session.add(sett)
745 748
746 749 except Exception:
747 750 log.exception('Exception while updating lab settings')
748 751 h.flash(_('Error occurred during updating labs settings'),
749 752 category='error')
750 753 else:
751 754 Session().commit()
752 755 SettingsModel().invalidate_settings_cache()
753 756 h.flash(_('Updated Labs settings'), category='success')
754 757 raise HTTPFound(h.route_path('admin_settings_labs'))
755 758
756 759 data = render('rhodecode:templates/admin/settings/settings.mako',
757 760 self._get_template_context(c), self.request)
758 761 html = formencode.htmlfill.render(
759 762 data,
760 763 defaults=self._form_defaults(),
761 764 encoding="UTF-8",
762 765 force_defaults=False
763 766 )
764 767 return Response(html)
765 768
766 769
767 770 # :param key: name of the setting including the 'rhodecode_' prefix
768 771 # :param type: the RhodeCodeSetting type to use.
769 772 # :param group: the i18ned group in which we should dispaly this setting
770 773 # :param label: the i18ned label we should display for this setting
771 774 # :param help: the i18ned help we should dispaly for this setting
772 775 LabSetting = collections.namedtuple(
773 776 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
774 777
775 778
776 779 # This list has to be kept in sync with the form
777 780 # rhodecode.model.forms.LabsSettingsForm.
778 781 _LAB_SETTINGS = [
779 782
780 783 ]
@@ -1,148 +1,149 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 pytest
22 22
23 23 from rhodecode.lib.utils2 import md5
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 30 import urllib
31 31
32 32 base_url = {
33 33 'repo_summary': '/{repo_name}',
34 34 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 35 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 36 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 37 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 @pytest.mark.usefixtures("app")
46 46 class TestRepoIssueTracker(object):
47 47 def test_issuetracker_index(self, autologin_user, backend):
48 48 repo = backend.create_repo()
49 49 response = self.app.get(route_path('edit_repo_issuetracker',
50 50 repo_name=repo.repo_name))
51 51 assert response.status_code == 200
52 52
53 53 def test_add_and_test_issuetracker_patterns(
54 54 self, autologin_user, backend, csrf_token, request, xhr_header):
55 55 pattern = 'issuetracker_pat'
56 56 another_pattern = pattern+'1'
57 57 post_url = route_path(
58 58 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 59 post_data = {
60 60 'new_pattern_pattern_0': pattern,
61 61 'new_pattern_url_0': 'http://url',
62 62 'new_pattern_prefix_0': 'prefix',
63 63 'new_pattern_description_0': 'description',
64 64 'new_pattern_pattern_1': another_pattern,
65 65 'new_pattern_url_1': '/url1',
66 66 'new_pattern_prefix_1': 'prefix1',
67 67 'new_pattern_description_1': 'description1',
68 68 'csrf_token': csrf_token
69 69 }
70 70 self.app.post(post_url, post_data, status=302)
71 71 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 72 settings = self.settings_model.get_repo_settings()
73 73 self.uid = md5(pattern)
74 74 assert settings[self.uid]['pat'] == pattern
75 75 self.another_uid = md5(another_pattern)
76 76 assert settings[self.another_uid]['pat'] == another_pattern
77 77
78 78 # test pattern
79 79 data = {'test_text': 'example of issuetracker_pat replacement',
80 80 'csrf_token': csrf_token}
81 81 response = self.app.post(
82 82 route_path('edit_repo_issuetracker_test',
83 83 repo_name=backend.repo.repo_name),
84 84 extra_environ=xhr_header, params=data)
85 85
86 86 assert response.body == \
87 87 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
88 88
89 89 @request.addfinalizer
90 90 def cleanup():
91 91 self.settings_model.delete_entries(self.uid)
92 92 self.settings_model.delete_entries(self.another_uid)
93 93
94 94 def test_edit_issuetracker_pattern(
95 95 self, autologin_user, backend, csrf_token, request):
96 96 entry_key = 'issuetracker_pat_'
97 97 pattern = 'issuetracker_pat2'
98 98 old_pattern = 'issuetracker_pat'
99 99 old_uid = md5(old_pattern)
100 100
101 101 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 102 entry_key+old_uid, old_pattern, 'unicode')
103 103 Session().add(sett)
104 104 Session().commit()
105 105 post_url = route_path(
106 106 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 107 post_data = {
108 108 'new_pattern_pattern_0': pattern,
109 109 'new_pattern_url_0': '/url',
110 110 'new_pattern_prefix_0': 'prefix',
111 111 'new_pattern_description_0': 'description',
112 112 'uid': old_uid,
113 113 'csrf_token': csrf_token
114 114 }
115 115 self.app.post(post_url, post_data, status=302)
116 116 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 117 settings = self.settings_model.get_repo_settings()
118 118 self.uid = md5(pattern)
119 119 assert settings[self.uid]['pat'] == pattern
120 120 with pytest.raises(KeyError):
121 121 key = settings[old_uid]
122 122
123 123 @request.addfinalizer
124 124 def cleanup():
125 125 self.settings_model.delete_entries(self.uid)
126 126
127 127 def test_delete_issuetracker_pattern(
128 self, autologin_user, backend, csrf_token, settings_util):
128 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
129 129 repo = backend.create_repo()
130 130 repo_name = repo.repo_name
131 131 entry_key = 'issuetracker_pat_'
132 132 pattern = 'issuetracker_pat3'
133 133 uid = md5(pattern)
134 134 settings_util.create_repo_rhodecode_setting(
135 135 repo=backend.repo, name=entry_key+uid,
136 136 value=entry_key, type_='unicode', cleanup=False)
137 137
138 138 self.app.post(
139 139 route_path(
140 140 'edit_repo_issuetracker_delete',
141 141 repo_name=backend.repo.repo_name),
142 142 {
143 143 'uid': uid,
144 'csrf_token': csrf_token
145 }, status=302)
144 'csrf_token': csrf_token,
145 '': ''
146 }, extra_environ=xhr_header, status=200)
146 147 settings = IssueTrackerSettingsModel(
147 148 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
148 149 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,137 +1,139 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-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 logging
22 22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 24 from pyramid.view import view_config
25 25 import formencode
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 32 from rhodecode.model.forms import IssueTrackerPatternsForm
33 33 from rhodecode.model.meta import Session
34 from rhodecode.model.settings import IssueTrackerSettingsModel
34 from rhodecode.model.settings import IssueTrackerSettingsModel, SettingsModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class RepoSettingsIssueTrackersView(RepoAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42
43 43
44 44 return c
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.admin')
48 48 @view_config(
49 49 route_name='edit_repo_issuetracker', request_method='GET',
50 50 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
51 51 def repo_issuetracker(self):
52 52 c = self.load_default_context()
53 53 c.active = 'issuetracker'
54 54 c.data = 'data'
55 55
56 56 c.settings_model = IssueTrackerSettingsModel(repo=self.db_repo)
57 57 c.global_patterns = c.settings_model.get_global_settings()
58 58 c.repo_patterns = c.settings_model.get_repo_settings()
59 59
60 60 return self._get_template_context(c)
61 61
62 62 @LoginRequired()
63 63 @HasRepoPermissionAnyDecorator('repository.admin')
64 64 @CSRFRequired()
65 65 @view_config(
66 66 route_name='edit_repo_issuetracker_test', request_method='POST',
67 xhr=True, renderer='string')
67 renderer='string', xhr=True)
68 68 def repo_issuetracker_test(self):
69 69 return h.urlify_commit_message(
70 70 self.request.POST.get('test_text', ''),
71 71 self.db_repo_name)
72 72
73 73 @LoginRequired()
74 74 @HasRepoPermissionAnyDecorator('repository.admin')
75 75 @CSRFRequired()
76 76 @view_config(
77 77 route_name='edit_repo_issuetracker_delete', request_method='POST',
78 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
78 renderer='json_ext', xhr=True)
79 79 def repo_issuetracker_delete(self):
80 80 _ = self.request.translate
81 81 uid = self.request.POST.get('uid')
82 82 repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name)
83 83 try:
84 84 repo_settings.delete_entries(uid)
85 85 except Exception:
86 86 h.flash(_('Error occurred during deleting issue tracker entry'),
87 87 category='error')
88 else:
89 h.flash(_('Removed issue tracker entry'), category='success')
90 raise HTTPFound(
91 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
88 raise HTTPNotFound()
89
90 SettingsModel().invalidate_settings_cache()
91 h.flash(_('Removed issue tracker entry.'), category='success')
92
93 return {'deleted': uid}
92 94
93 95 def _update_patterns(self, form, repo_settings):
94 96 for uid in form['delete_patterns']:
95 97 repo_settings.delete_entries(uid)
96 98
97 99 for pattern_data in form['patterns']:
98 100 for setting_key, pattern, type_ in pattern_data:
99 101 sett = repo_settings.create_or_update_setting(
100 102 setting_key, pattern.strip(), type_)
101 103 Session().add(sett)
102 104
103 105 Session().commit()
104 106
105 107 @LoginRequired()
106 108 @HasRepoPermissionAnyDecorator('repository.admin')
107 109 @CSRFRequired()
108 110 @view_config(
109 111 route_name='edit_repo_issuetracker_update', request_method='POST',
110 112 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
111 113 def repo_issuetracker_update(self):
112 114 _ = self.request.translate
113 115 # Save inheritance
114 116 repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name)
115 117 inherited = (
116 118 self.request.POST.get('inherit_global_issuetracker') == "inherited")
117 119 repo_settings.inherit_global_settings = inherited
118 120 Session().commit()
119 121
120 122 try:
121 123 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
122 124 except formencode.Invalid as errors:
123 125 log.exception('Failed to add new pattern')
124 126 error = errors
125 127 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
126 128 category='error')
127 129 raise HTTPFound(
128 130 h.route_path('edit_repo_issuetracker',
129 131 repo_name=self.db_repo_name))
130 132
131 133 if form:
132 134 self._update_patterns(form, repo_settings)
133 135
134 136 h.flash(_('Updated issue tracker entries'), category='success')
135 137 raise HTTPFound(
136 138 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
137 139
@@ -1,524 +1,542 b''
1 1
2 2 // tables.less
3 3 // For use in RhodeCode application tables;
4 4 // see style guide documentation for guidelines.
5 5
6 6 // TABLES
7 7
8 8 .rctable,
9 9 table.rctable,
10 10 table.dataTable {
11 11 clear:both;
12 12 width: 100%;
13 13 margin: 0 auto @padding;
14 14 padding: 0;
15 15 vertical-align: baseline;
16 16 line-height:1.5em;
17 17 border: none;
18 18 outline: none;
19 19 border-collapse: collapse;
20 20 border-spacing: 0;
21 21 color: @grey2;
22 22
23 23 b {
24 24 font-weight: normal;
25 25 }
26 26
27 27 em {
28 28 font-weight: bold;
29 29 font-style: normal;
30 30 }
31 31
32 32 .td-user {
33 33 .rc-user {
34 34 white-space: nowrap;
35 35 }
36 36 }
37 37
38 38 .td-email {
39 39 white-space: nowrap;
40 40 }
41 41
42 42 th,
43 43 td {
44 44 height: auto;
45 45 max-width: 20%;
46 46 padding: .65em 0 .65em 1em;
47 47 vertical-align: middle;
48 48 border-bottom: @border-thickness solid @grey5;
49 49 white-space: normal;
50 50
51 51 &.td-radio,
52 52 &.td-checkbox {
53 53 padding-right: 0;
54 54 text-align: center;
55 55
56 56 input {
57 57 margin: 0 1em;
58 58 }
59 59 }
60 60
61 61 &.truncate-wrap {
62 62 white-space: nowrap !important;
63 63 }
64 64
65 65 pre {
66 66 margin: 0;
67 67 }
68 68
69 69 .show_more {
70 70 height: inherit;
71 71 }
72 72 }
73 73
74 74 .expired td {
75 75 background-color: @grey7;
76 76 }
77 77 .inactive td {
78 78 background-color: @grey6;
79 79 }
80 80 th {
81 81 text-align: left;
82 82 font-weight: @text-semibold-weight;
83 83 font-family: @text-semibold;
84 84 }
85 85
86 86 .hl {
87 87 td {
88 88 background-color: lighten(@alert4,25%);
89 89 }
90 90 }
91 91
92 92 // Special Data Cell Types
93 93 // See style guide for desciptions and examples.
94 94
95 95 td {
96 96
97 97 &.user {
98 98 padding-left: 1em;
99 99 }
100 100
101 101 &.td-rss {
102 102 width: 20px;
103 103 min-width: 0;
104 104 margin: 0;
105 105 }
106 106
107 107 &.quick_repo_menu {
108 108 width: 15px;
109 109 text-align: center;
110 110
111 111 &:hover {
112 112 background-color: @grey5;
113 113 }
114 114 }
115 115
116 116 &.td-icon {
117 117 min-width: 20px;
118 118 width: 20px;
119 119 }
120 120
121 121 &.td-hash {
122 122 min-width: 80px;
123 123 width: 200px;
124 124
125 125 .obsolete {
126 126 text-decoration: line-through;
127 127 color: lighten(@grey2,25%);
128 128 }
129 129 }
130 130
131 131 &.td-sha {
132 132 white-space: nowrap;
133 133 }
134 134
135 135 &.td-graphbox {
136 136 width: 100px;
137 137 max-width: 100px;
138 138 min-width: 100px;
139 139 }
140 140
141 141 &.td-time {
142 142 width: 160px;
143 143 white-space: nowrap;
144 144 }
145 145
146 146 &.annotate{
147 147 padding-right: 0;
148 148
149 149 div.annotatediv{
150 150 margin: 0 0.7em;
151 151 }
152 152 }
153 153
154 154 &.tags-col {
155 155 padding-right: 0;
156 156 }
157 157
158 158 &.td-description {
159 159 min-width: 350px;
160 160
161 161 &.truncate, .truncate-wrap {
162 162 white-space: nowrap;
163 163 overflow: hidden;
164 164 text-overflow: ellipsis;
165 165 max-width: 350px;
166 166 }
167 167 }
168 168
169 169 &.td-grid-name {
170 170 white-space: nowrap;
171 171 min-width: 300px;
172 172 }
173 173
174 174 &.td-componentname {
175 175 white-space: nowrap;
176 176 }
177 177
178 178 &.td-name {
179 179
180 180 }
181 181
182 182 &.td-journalaction {
183 183 min-width: 300px;
184 184
185 185 .journal_action_params {
186 186 // waiting for feedback
187 187 }
188 188 }
189 189
190 190 &.td-active {
191 191 padding-left: .65em;
192 192 }
193 193
194 &.td-issue-tracker-name {
195 width: 180px;
196 input {
197 width: 180px;
198 }
199
200 }
201
202 &.td-issue-tracker-regex {
203 white-space: nowrap;
204
205 min-width: 300px;
206 input {
207 min-width: 300px;
208 }
209
210 }
211
194 212 &.td-url {
195 213 white-space: nowrap;
196 214 }
197 215
198 216 &.td-comments {
199 217 min-width: 3em;
200 218 }
201 219
202 220 &.td-buttons {
203 221 padding: .3em 0;
204 222 }
205 223 &.td-align-top {
206 224 vertical-align: text-top
207 225 }
208 226 &.td-action {
209 227 // this is for the remove/delete/edit buttons
210 228 padding-right: 0;
211 229 min-width: 95px;
212 230 text-transform: capitalize;
213 231
214 232 i {
215 233 display: none;
216 234 }
217 235 }
218 236
219 237 // TODO: lisa: this needs to be cleaned up with the buttons
220 238 .grid_edit,
221 239 .grid_delete {
222 240 display: inline-block;
223 241 margin: 0 @padding/3 0 0;
224 242 font-family: @text-light;
225 243
226 244 i {
227 245 display: none;
228 246 }
229 247 }
230 248
231 249 .grid_edit + .grid_delete {
232 250 border-left: @border-thickness solid @grey5;
233 251 padding-left: @padding/2;
234 252 }
235 253
236 254 &.td-compare {
237 255
238 256 input {
239 257 margin-right: 1em;
240 258 }
241 259
242 260 .compare-radio-button {
243 261 margin: 0 1em 0 0;
244 262 }
245 263
246 264
247 265 }
248 266
249 267 &.td-tags {
250 268 padding: .5em 1em .5em 0;
251 269 width: 140px;
252 270
253 271 .tag {
254 272 margin: 1px;
255 273 float: left;
256 274 }
257 275 }
258 276
259 277 .icon-svn, .icon-hg, .icon-git {
260 278 font-size: 1.4em;
261 279 }
262 280
263 281 &.collapse_commit,
264 282 &.expand_commit {
265 283 padding-right: 0;
266 284 padding-left: 1em;
267 285 cursor: pointer;
268 286 width: 20px;
269 287 }
270 288 }
271 289
272 290 .perm_admin_row {
273 291 color: @grey4;
274 292 background-color: @grey6;
275 293 }
276 294
277 295 .noborder {
278 296 border: none;
279 297
280 298 td {
281 299 border: none;
282 300 }
283 301 }
284 302 }
285 303 .rctable.audit-log {
286 304 td {
287 305 vertical-align: top;
288 306 }
289 307 }
290 308
291 309 // TRUNCATING
292 310 // TODO: lisaq: should this possibly be moved out of tables.less?
293 311 // for truncated text
294 312 // used inside of table cells and in code block headers
295 313 .truncate-wrap {
296 314 white-space: nowrap !important;
297 315
298 316 //truncated text
299 317 .truncate {
300 318 max-width: 450px;
301 319 width: 300px;
302 320 overflow: hidden;
303 321 text-overflow: ellipsis;
304 322 -o-text-overflow: ellipsis;
305 323 -ms-text-overflow: ellipsis;
306 324
307 325 &.autoexpand {
308 326 width: 120px;
309 327 margin-right: 200px;
310 328 }
311 329 }
312 330 &:hover .truncate.autoexpand {
313 331 overflow: visible;
314 332 }
315 333
316 334 .tags-truncate {
317 335 width: 150px;
318 336 height: 22px;
319 337 overflow: hidden;
320 338
321 339 .tag {
322 340 display: inline-block;
323 341 }
324 342
325 343 &.truncate {
326 344 height: 22px;
327 345 max-height:2em;
328 346 width: 140px;
329 347 }
330 348 }
331 349 }
332 350
333 351 .apikeys_wrap {
334 352 margin-bottom: @padding;
335 353
336 354 table.rctable td:first-child {
337 355 width: 340px;
338 356 }
339 357 }
340 358
341 359
342 360
343 361 // SPECIAL CASES
344 362
345 363 // Repository Followers
346 364 table.rctable.followers_data {
347 365 width: 75%;
348 366 margin: 0;
349 367 }
350 368
351 369 // Repository List
352 370 // Group Members List
353 371 table.rctable.group_members,
354 372 table#repo_list_table {
355 373 min-width: 600px;
356 374 }
357 375
358 376 // Keyboard mappings
359 377 table.keyboard-mappings {
360 378 th {
361 379 text-align: left;
362 380 font-weight: @text-semibold-weight;
363 381 font-family: @text-semibold;
364 382 }
365 383 }
366 384
367 385 // Branches, Tags, and Bookmarks
368 386 #obj_list_table.dataTable {
369 387 td.td-time {
370 388 padding-right: 1em;
371 389 }
372 390 }
373 391
374 392 // User Admin
375 393 .rctable.useremails,
376 394 .rctable.account_emails {
377 395 .tag,
378 396 .btn {
379 397 float: right;
380 398 }
381 399 .btn { //to line up with tags
382 400 margin-right: 1.65em;
383 401 }
384 402 }
385 403
386 404 // User List
387 405 #user_list_table {
388 406
389 407 td.td-user {
390 408 min-width: 100px;
391 409 }
392 410 }
393 411
394 412 // Pull Request List Table
395 413 #pull_request_list_table.dataTable {
396 414
397 415 //TODO: lisa: This needs to be removed once the description is adjusted
398 416 // for using an expand_commit button (see issue 765)
399 417 td {
400 418 vertical-align: middle;
401 419 }
402 420 }
403 421
404 422 // Settings (no border)
405 423 table.rctable.dl-settings {
406 424 td {
407 425 border: none;
408 426 vertical-align: baseline;
409 427 }
410 428 }
411 429
412 430
413 431 // Statistics
414 432 table.trending_language_tbl {
415 433 width: 100%;
416 434 line-height: 1em;
417 435
418 436 td div {
419 437 overflow: visible;
420 438 }
421 439 }
422 440
423 441 .trending_language_tbl, .trending_language_tbl td {
424 442 border: 0;
425 443 margin: 0;
426 444 padding: 0;
427 445 background: transparent;
428 446 }
429 447
430 448 .trending_language_tbl, .trending_language_tbl tr {
431 449 border-spacing: 0 3px;
432 450 }
433 451
434 452 .trending_language {
435 453 position: relative;
436 454 overflow: hidden;
437 455 color: @text-color;
438 456 width: 400px;
439 457
440 458 .lang-bar {
441 459 z-index: 1;
442 460 overflow: hidden;
443 461 background-color: @rcblue;
444 462 color: #FFF;
445 463 text-decoration: none;
446 464 }
447 465
448 466 }
449 467
450 468 // Changesets
451 469 #changesets.rctable {
452 470 th {
453 471 padding: 0 1em 0.65em 0;
454 472 }
455 473
456 474 // td must be fixed height for graph
457 475 td {
458 476 height: 32px;
459 477 padding: 0 1em 0 0;
460 478 vertical-align: middle;
461 479 white-space: nowrap;
462 480
463 481 &.td-description {
464 482 white-space: normal;
465 483 }
466 484
467 485 &.expand_commit {
468 486 padding-right: 0;
469 487 cursor: pointer;
470 488 width: 20px;
471 489 }
472 490 }
473 491 }
474 492
475 493 // Compare
476 494 table.compare_view_commits {
477 495 margin-top: @space;
478 496
479 497 td.td-time {
480 498 padding-left: .5em;
481 499 }
482 500
483 501 // special case to not show hover actions on hidden indicator
484 502 tr.compare_select_hidden:hover {
485 503 cursor: inherit;
486 504
487 505 td {
488 506 background-color: inherit;
489 507 }
490 508 }
491 509
492 510 tr:hover {
493 511 cursor: pointer;
494 512
495 513 td {
496 514 background-color: lighten(@alert4,25%);
497 515 }
498 516 }
499 517
500 518
501 519 }
502 520
503 521 .file_history {
504 522 td.td-actions {
505 523 text-align: right;
506 524 }
507 525 }
508 526
509 527
510 528 // Gist List
511 529 #gist_list_table {
512 530 td {
513 531 vertical-align: middle;
514 532
515 533 div{
516 534 display: inline-block;
517 535 vertical-align: middle;
518 536 }
519 537
520 538 img{
521 539 vertical-align: middle;
522 540 }
523 541 }
524 542 }
@@ -1,221 +1,321 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 <%
9 # Name/desc, pattern, issue prefix
10 examples = [
11 (
12 ' ',
13 ' ',
14 ' ',
15 ' '
16 ),
17
18 (
19 'Redmine',
20 '(^#|\s#)(?P<issue_id>\d+)',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
22 ''
23 ),
24
25 (
26 'Redmine - Alternative',
27 '(?:issue-)(\d+)',
28 'https://myissueserver.com/redmine/issue/${id}',
29 ''
30 ),
31
32 (
33 'Redmine - Wiki',
34 '(?:wiki-)([a-zA-Z0-9]+)',
35 'http://example.org/projects/${repo_name}/wiki/${id}',
36 'wiki-'
37 ),
38
39 (
40 'JIRA - All tickets',
41 '(^|\s\w+-\d+)',
42 'https://myjira.com/browse/${id}',
43 ''
44 ),
45
46 (
47 'JIRA - Project (JRA)',
48 '(?:(^|\s)(?P<issue_id>(?:JRA-|JRA-)(?:\d+)))',
49 'https://myjira.com/${issue_id}',
50 ''
51 ),
52
53 (
54 'Confluence WIKI',
55 '(?:conf-)([A-Z0-9]+)',
56 'https://example.atlassian.net/display/wiki/${id}/${repo_name}',
57 'CONF-',
58 ),
59
60 (
61 'Pivotal Tracker',
62 '(?:pivot-)(?<project_id>\d+)-(?<story>\d+)',
63 'https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}',
64 'PIV-',
65 ),
66
67 (
68 'Trello',
69 '(?:trello-)(?<card_id>[a-zA-Z0-9]+)',
70 'https://trello.com/example.com/${card_id}',
71 'TRELLO-',
72 ),
73 ]
74 %>
75
8 76 <table class="rctable issuetracker">
9 77 <tr>
10 78 <th>${_('Description')}</th>
11 79 <th>${_('Pattern')}</th>
12 80 <th>${_('Url')}</th>
13 <th>${_('Prefix')}</th>
81 <th>${_('Extra Prefix')}</th>
14 82 <th ></th>
15 83 </tr>
16 <tr>
17 <td class="td-description issue-tracker-example">Example</td>
18 <td class="td-regex issue-tracker-example">${'(?:#)(?P<issue_id>\d+)'}</td>
19 <td class="td-url issue-tracker-example">${'https://myissueserver.com/${repo}/issue/${issue_id}'}</td>
20 <td class="td-prefix issue-tracker-example">#</td>
21 <td class="issue-tracker-example"><a href="${h.route_url('enterprise_issue_tracker_settings')}" target="_blank">${_('Read more')}</a></td>
22 </tr>
84 % for name, pat, url, pref in examples:
85 <tr class="it-examples" style="${'' if loop.index == 0 else 'display:none'}">
86 <td class="td-issue-tracker-name issue-tracker-example">${name}</td>
87 <td class="td-regex issue-tracker-example">${pat}</td>
88 <td class="td-url issue-tracker-example">${url}</td>
89 <td class="td-prefix issue-tracker-example">${pref}</td>
90 <td>
91 % if loop.index == 0:
92 <a href="#showMore" onclick="$('.it-examples').toggle(); return false">${_('show examples')}</a>
93 % else:
94 <a href="#copyToInput" onclick="copyToInput(this, '${h.json.dumps(name)}', '${h.json.dumps(pat)}', '${h.json.dumps(url)}', '${h.json.dumps(pref)}'); return false">copy to input</a>
95 % endif
96 </td>
97 </tr>
98 % endfor
99
23 100 %for uid, entry in patterns:
24 101 <tr id="entry_${uid}">
25 <td class="td-description issuetracker_desc">
102 <td class="td-issue-tracker-name issuetracker_desc">
26 103 <span class="entry">
27 104 ${entry.desc}
28 105 </span>
29 106 <span class="edit">
30 107 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
31 108 </span>
32 109 </td>
33 <td class="td-regex issuetracker_pat">
110 <td class="td-issue-tracker-regex issuetracker_pat">
34 111 <span class="entry">
35 112 ${entry.pat}
36 113 </span>
37 114 <span class="edit">
38 115 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
39 116 </span>
40 117 </td>
41 118 <td class="td-url issuetracker_url">
42 119 <span class="entry">
43 120 ${entry.url}
44 121 </span>
45 122 <span class="edit">
46 123 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
47 124 </span>
48 125 </td>
49 126 <td class="td-prefix issuetracker_pref">
50 127 <span class="entry">
51 128 ${entry.pref}
52 129 </span>
53 130 <span class="edit">
54 131 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
55 132 </span>
56 133 </td>
57 134 <td class="td-action">
58 135 <div class="grid_edit">
59 136 <span class="entry">
60 137 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
61 138 </span>
62 139 <span class="edit">
63 140 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
64 141 </span>
65 142 </div>
66 143 <div class="grid_delete">
67 144 <span class="entry">
68 145 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
69 146 ${_('Delete')}
70 147 </a>
71 148 </span>
72 149 <span class="edit">
73 150 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
74 151 </span>
75 152 </div>
76 153 </td>
77 154 </tr>
78 155 %endfor
79 156 <tr id="last-row"></tr>
80 157 </table>
81 158 <p>
82 159 <a id="add_pattern" class="link">
83 160 ${_('Add new')}
84 161 </a>
85 162 </p>
86 163
87 164 <script type="text/javascript">
88 165 var newEntryLabel = $('label[for="new_entry"]');
89 166
90 167 var resetEntry = function() {
91 168 newEntryLabel.text("${_('New Entry')}:");
92 169 };
93 170
94 171 var delete_pattern = function(entry) {
95 172 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
96 173 $.ajax({
97 174 type: "POST",
98 url: "${delete_url}",
175 url: "${delete_url}",
99 176 data: {
100 177 'csrf_token': CSRF_TOKEN,
101 178 'uid':$(entry).data('uid')
102 179 },
103 180 success: function(){
104 location.reload();
181 window.location.reload();
105 182 },
106 183 error: function(data, textStatus, errorThrown){
107 184 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
108 185 }
109 186 });
110 187 }
111 188 };
112 189
113 190 $('.delete_issuetracker_entry').on('click', function(e){
114 191 e.preventDefault();
115 192 delete_pattern(this);
116 193 });
117 194
118 195 $('.edit_issuetracker_entry').on('click', function(e){
119 196 e.preventDefault();
120 197 $(this).parents('tr').addClass('editopen');
121 198 });
122 199
123 200 $('.edit_issuetracker_cancel').on('click', function(e){
124 201 e.preventDefault();
125 202 $(this).parents('tr').removeClass('editopen');
126 203 // Reset to original value
127 204 var uid = $(this).data('uid');
128 205 $('#'+uid+' input').each(function(e) {
129 206 this.value = this.defaultValue;
130 207 });
131 208 });
132 209
133 210 $('input#reset').on('click', function(e) {
134 211 resetEntry();
135 212 });
136 213
137 214 $('#add_pattern').on('click', function(e) {
138 215 addNewPatternInput();
139 216 });
217
218 var copied = false;
219 copyToInput = function (elem, name, pat, url, pref) {
220 if (copied === false) {
221 addNewPatternInput();
222 copied = true;
223 }
224 $(elem).hide();
225 var load = function(text){
226 return text.replace(/["]/g, "")
227 };
228 $('#description_1').val(load(name));
229 $('#pattern_1').val(load(pat));
230 $('#url_1').val(load(url));
231 $('#prefix_1').val(load(pref));
232
233 }
234
140 235 </script>
141 236 </%def>
142 237
143 238 <%def name="issue_tracker_new_row()">
144 239 <table id="add-row-tmpl" style="display: none;">
145 240 <tbody>
146 241 <tr class="new_pattern">
147 <td class="td-description issuetracker_desc">
242 <td class="td-issue-tracker-name issuetracker_desc">
148 243 <span class="entry">
149 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
244 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
150 245 </span>
151 246 </td>
152 <td class="td-regex issuetracker_pat">
247 <td class="td-issue-tracker-regex issuetracker_pat">
153 248 <span class="entry">
154 249 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
155 250 value="##PATTERN##" type="text">
156 251 </span>
157 252 </td>
158 253 <td class="td-url issuetracker_url">
159 254 <span class="entry">
160 255 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
161 256 </span>
162 257 </td>
163 258 <td class="td-prefix issuetracker_pref">
164 259 <span class="entry">
165 260 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
166 261 </span>
167 262 </td>
168 263 <td class="td-action">
169 264 </td>
170 265 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
171 266 </tr>
172 267 </tbody>
173 268 </table>
174 269 </%def>
175 270
176 271 <%def name="issue_tracker_settings_test(test_url)">
177 272 <div class="form-vertical">
178 273 <div class="fields">
179 274 <div class="field">
180 275 <div class='textarea-full'>
181 <textarea id="test_pattern_data" rows="10">
276 <textarea id="test_pattern_data" rows="12">
182 277 This is an example text for testing issue tracker patterns.
183 This commit fixes ticket #451.
278 This commit fixes ticket #451 and ticket #910.
279 Following tickets will get mentioned:
280 #123
281 #456
282 JRA-123
283 JRA-456
184 284 Open a pull request !101 to contribute !
185 285 Added tag v1.3.0 for commit 0f3b629be725
186 286
187 287 Add a test pattern here and hit preview to see the link.
188 288 </textarea>
189 289 </div>
190 290 </div>
191 291 </div>
192 292 <div class="test_pattern_preview">
193 293 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
194 294 <p>${_('Test Pattern Preview')}</p>
195 295 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
196 296 </div>
197 297 </div>
198 298
199 299 <script type="text/javascript">
200 300 $('#test_pattern').on('click', function(e) {
201 301 $.ajax({
202 302 type: "POST",
203 303 url: "${test_url}",
204 304 data: {
205 305 'test_text': $('#test_pattern_data').val(),
206 306 'csrf_token': CSRF_TOKEN
207 307 },
208 308 success: function(data){
209 309 $('#test_pattern_result').html(data);
210 310 tooltipActivate();
211 311 },
212 312 error: function(jqXHR, textStatus, errorThrown){
213 313 $('#test_pattern_result').html('Error: ' + errorThrown);
214 314 }
215 315 });
216 316 $('#test_pattern_result').show();
217 317 });
218 318 </script>
219 319 </%def>
220 320
221 321
General Comments 0
You need to be logged in to leave comments. Login now