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