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