##// END OF EJS Templates
pyramid: allows easier turning off the pylons components.
marcink -
r1912:f0e0aac6 default
parent child Browse files
Show More
@@ -1,368 +1,372 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-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 import time
21 import time
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.model import repo
29 from rhodecode.model import repo
30 from rhodecode.model import repo_group
30 from rhodecode.model import repo_group
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.model.scm import ScmModel
32 from rhodecode.model.scm import ScmModel
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 ADMIN_PREFIX = '/_admin'
37 ADMIN_PREFIX = '/_admin'
38 STATIC_FILE_PREFIX = '/_static'
38 STATIC_FILE_PREFIX = '/_static'
39
39
40
40
41 def add_route_with_slash(config,name, pattern, **kw):
41 def add_route_with_slash(config,name, pattern, **kw):
42 config.add_route(name, pattern, **kw)
42 config.add_route(name, pattern, **kw)
43 if not pattern.endswith('/'):
43 if not pattern.endswith('/'):
44 config.add_route(name + '_slash', pattern + '/', **kw)
44 config.add_route(name + '_slash', pattern + '/', **kw)
45
45
46
46
47 def get_format_ref_id(repo):
47 def get_format_ref_id(repo):
48 """Returns a `repo` specific reference formatter function"""
48 """Returns a `repo` specific reference formatter function"""
49 if h.is_svn(repo):
49 if h.is_svn(repo):
50 return _format_ref_id_svn
50 return _format_ref_id_svn
51 else:
51 else:
52 return _format_ref_id
52 return _format_ref_id
53
53
54
54
55 def _format_ref_id(name, raw_id):
55 def _format_ref_id(name, raw_id):
56 """Default formatting of a given reference `name`"""
56 """Default formatting of a given reference `name`"""
57 return name
57 return name
58
58
59
59
60 def _format_ref_id_svn(name, raw_id):
60 def _format_ref_id_svn(name, raw_id):
61 """Special way of formatting a reference for Subversion including path"""
61 """Special way of formatting a reference for Subversion including path"""
62 return '%s@%s' % (name, raw_id)
62 return '%s@%s' % (name, raw_id)
63
63
64
64
65 class TemplateArgs(StrictAttributeDict):
65 class TemplateArgs(StrictAttributeDict):
66 pass
66 pass
67
67
68
68
69 class BaseAppView(object):
69 class BaseAppView(object):
70
70
71 def __init__(self, context, request):
71 def __init__(self, context, request):
72 self.request = request
72 self.request = request
73 self.context = context
73 self.context = context
74 self.session = request.session
74 self.session = request.session
75 self._rhodecode_user = request.user # auth user
75 self._rhodecode_user = request.user # auth user
76 self._rhodecode_db_user = self._rhodecode_user.get_instance()
76 self._rhodecode_db_user = self._rhodecode_user.get_instance()
77 self._maybe_needs_password_change(
77 self._maybe_needs_password_change(
78 request.matched_route.name, self._rhodecode_db_user)
78 request.matched_route.name, self._rhodecode_db_user)
79
79
80 def _maybe_needs_password_change(self, view_name, user_obj):
80 def _maybe_needs_password_change(self, view_name, user_obj):
81 log.debug('Checking if user %s needs password change on view %s',
81 log.debug('Checking if user %s needs password change on view %s',
82 user_obj, view_name)
82 user_obj, view_name)
83 skip_user_views = [
83 skip_user_views = [
84 'logout', 'login',
84 'logout', 'login',
85 'my_account_password', 'my_account_password_update'
85 'my_account_password', 'my_account_password_update'
86 ]
86 ]
87
87
88 if not user_obj:
88 if not user_obj:
89 return
89 return
90
90
91 if user_obj.username == User.DEFAULT_USER:
91 if user_obj.username == User.DEFAULT_USER:
92 return
92 return
93
93
94 now = time.time()
94 now = time.time()
95 should_change = user_obj.user_data.get('force_password_change')
95 should_change = user_obj.user_data.get('force_password_change')
96 change_after = safe_int(should_change) or 0
96 change_after = safe_int(should_change) or 0
97 if should_change and now > change_after:
97 if should_change and now > change_after:
98 log.debug('User %s requires password change', user_obj)
98 log.debug('User %s requires password change', user_obj)
99 h.flash('You are required to change your password', 'warning',
99 h.flash('You are required to change your password', 'warning',
100 ignore_duplicate=True)
100 ignore_duplicate=True)
101
101
102 if view_name not in skip_user_views:
102 if view_name not in skip_user_views:
103 raise HTTPFound(
103 raise HTTPFound(
104 self.request.route_path('my_account_password'))
104 self.request.route_path('my_account_password'))
105
105
106 def _get_local_tmpl_context(self, include_app_defaults=False):
106 def _get_local_tmpl_context(self, include_app_defaults=False):
107 c = TemplateArgs()
107 c = TemplateArgs()
108 c.auth_user = self.request.user
108 c.auth_user = self.request.user
109 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
109 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
110 c.rhodecode_user = self.request.user
110 c.rhodecode_user = self.request.user
111
111
112 if include_app_defaults:
112 if include_app_defaults:
113 # NOTE(marcink): after full pyramid migration include_app_defaults
113 # NOTE(marcink): after full pyramid migration include_app_defaults
114 # should be turned on by default
114 # should be turned on by default
115 from rhodecode.lib.base import attach_context_attributes
115 from rhodecode.lib.base import attach_context_attributes
116 attach_context_attributes(c, self.request, self.request.user.user_id)
116 attach_context_attributes(c, self.request, self.request.user.user_id)
117
117
118 return c
118 return c
119
119
120 def _register_global_c(self, tmpl_args):
120 def _register_global_c(self, tmpl_args):
121 """
121 """
122 Registers attributes to pylons global `c`
122 Registers attributes to pylons global `c`
123 """
123 """
124
124
125 # TODO(marcink): remove once pyramid migration is finished
125 # TODO(marcink): remove once pyramid migration is finished
126 from pylons import tmpl_context as c
126 from pylons import tmpl_context as c
127 try:
127 for k, v in tmpl_args.items():
128 for k, v in tmpl_args.items():
128 setattr(c, k, v)
129 setattr(c, k, v)
130 except TypeError:
131 log.exception('Failed to register pylons C')
132 pass
129
133
130 def _get_template_context(self, tmpl_args):
134 def _get_template_context(self, tmpl_args):
131 self._register_global_c(tmpl_args)
135 self._register_global_c(tmpl_args)
132
136
133 local_tmpl_args = {
137 local_tmpl_args = {
134 'defaults': {},
138 'defaults': {},
135 'errors': {},
139 'errors': {},
136 }
140 }
137 local_tmpl_args.update(tmpl_args)
141 local_tmpl_args.update(tmpl_args)
138 return local_tmpl_args
142 return local_tmpl_args
139
143
140 def load_default_context(self):
144 def load_default_context(self):
141 """
145 """
142 example:
146 example:
143
147
144 def load_default_context(self):
148 def load_default_context(self):
145 c = self._get_local_tmpl_context()
149 c = self._get_local_tmpl_context()
146 c.custom_var = 'foobar'
150 c.custom_var = 'foobar'
147 self._register_global_c(c)
151 self._register_global_c(c)
148 return c
152 return c
149 """
153 """
150 raise NotImplementedError('Needs implementation in view class')
154 raise NotImplementedError('Needs implementation in view class')
151
155
152
156
153 class RepoAppView(BaseAppView):
157 class RepoAppView(BaseAppView):
154
158
155 def __init__(self, context, request):
159 def __init__(self, context, request):
156 super(RepoAppView, self).__init__(context, request)
160 super(RepoAppView, self).__init__(context, request)
157 self.db_repo = request.db_repo
161 self.db_repo = request.db_repo
158 self.db_repo_name = self.db_repo.repo_name
162 self.db_repo_name = self.db_repo.repo_name
159 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
163 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
160
164
161 def _handle_missing_requirements(self, error):
165 def _handle_missing_requirements(self, error):
162 log.error(
166 log.error(
163 'Requirements are missing for repository %s: %s',
167 'Requirements are missing for repository %s: %s',
164 self.db_repo_name, error.message)
168 self.db_repo_name, error.message)
165
169
166 def _get_local_tmpl_context(self, include_app_defaults=False):
170 def _get_local_tmpl_context(self, include_app_defaults=False):
167 c = super(RepoAppView, self)._get_local_tmpl_context(
171 c = super(RepoAppView, self)._get_local_tmpl_context(
168 include_app_defaults=include_app_defaults)
172 include_app_defaults=include_app_defaults)
169
173
170 # register common vars for this type of view
174 # register common vars for this type of view
171 c.rhodecode_db_repo = self.db_repo
175 c.rhodecode_db_repo = self.db_repo
172 c.repo_name = self.db_repo_name
176 c.repo_name = self.db_repo_name
173 c.repository_pull_requests = self.db_repo_pull_requests
177 c.repository_pull_requests = self.db_repo_pull_requests
174
178
175 c.repository_requirements_missing = False
179 c.repository_requirements_missing = False
176 try:
180 try:
177 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
181 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
178 except RepositoryRequirementError as e:
182 except RepositoryRequirementError as e:
179 c.repository_requirements_missing = True
183 c.repository_requirements_missing = True
180 self._handle_missing_requirements(e)
184 self._handle_missing_requirements(e)
181
185
182 return c
186 return c
183
187
184
188
185 class DataGridAppView(object):
189 class DataGridAppView(object):
186 """
190 """
187 Common class to have re-usable grid rendering components
191 Common class to have re-usable grid rendering components
188 """
192 """
189
193
190 def _extract_ordering(self, request, column_map=None):
194 def _extract_ordering(self, request, column_map=None):
191 column_map = column_map or {}
195 column_map = column_map or {}
192 column_index = safe_int(request.GET.get('order[0][column]'))
196 column_index = safe_int(request.GET.get('order[0][column]'))
193 order_dir = request.GET.get(
197 order_dir = request.GET.get(
194 'order[0][dir]', 'desc')
198 'order[0][dir]', 'desc')
195 order_by = request.GET.get(
199 order_by = request.GET.get(
196 'columns[%s][data][sort]' % column_index, 'name_raw')
200 'columns[%s][data][sort]' % column_index, 'name_raw')
197
201
198 # translate datatable to DB columns
202 # translate datatable to DB columns
199 order_by = column_map.get(order_by) or order_by
203 order_by = column_map.get(order_by) or order_by
200
204
201 search_q = request.GET.get('search[value]')
205 search_q = request.GET.get('search[value]')
202 return search_q, order_by, order_dir
206 return search_q, order_by, order_dir
203
207
204 def _extract_chunk(self, request):
208 def _extract_chunk(self, request):
205 start = safe_int(request.GET.get('start'), 0)
209 start = safe_int(request.GET.get('start'), 0)
206 length = safe_int(request.GET.get('length'), 25)
210 length = safe_int(request.GET.get('length'), 25)
207 draw = safe_int(request.GET.get('draw'))
211 draw = safe_int(request.GET.get('draw'))
208 return draw, start, length
212 return draw, start, length
209
213
210
214
211 class BaseReferencesView(RepoAppView):
215 class BaseReferencesView(RepoAppView):
212 """
216 """
213 Base for reference view for branches, tags and bookmarks.
217 Base for reference view for branches, tags and bookmarks.
214 """
218 """
215 def load_default_context(self):
219 def load_default_context(self):
216 c = self._get_local_tmpl_context()
220 c = self._get_local_tmpl_context()
217
221
218 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
222 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
219 c.repo_info = self.db_repo
223 c.repo_info = self.db_repo
220
224
221 self._register_global_c(c)
225 self._register_global_c(c)
222 return c
226 return c
223
227
224 def load_refs_context(self, ref_items, partials_template):
228 def load_refs_context(self, ref_items, partials_template):
225 _render = self.request.get_partial_renderer(partials_template)
229 _render = self.request.get_partial_renderer(partials_template)
226 pre_load = ["author", "date", "message"]
230 pre_load = ["author", "date", "message"]
227
231
228 is_svn = h.is_svn(self.rhodecode_vcs_repo)
232 is_svn = h.is_svn(self.rhodecode_vcs_repo)
229 is_hg = h.is_hg(self.rhodecode_vcs_repo)
233 is_hg = h.is_hg(self.rhodecode_vcs_repo)
230
234
231 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
235 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
232
236
233 closed_refs = {}
237 closed_refs = {}
234 if is_hg:
238 if is_hg:
235 closed_refs = self.rhodecode_vcs_repo.branches_closed
239 closed_refs = self.rhodecode_vcs_repo.branches_closed
236
240
237 data = []
241 data = []
238 for ref_name, commit_id in ref_items:
242 for ref_name, commit_id in ref_items:
239 commit = self.rhodecode_vcs_repo.get_commit(
243 commit = self.rhodecode_vcs_repo.get_commit(
240 commit_id=commit_id, pre_load=pre_load)
244 commit_id=commit_id, pre_load=pre_load)
241 closed = ref_name in closed_refs
245 closed = ref_name in closed_refs
242
246
243 # TODO: johbo: Unify generation of reference links
247 # TODO: johbo: Unify generation of reference links
244 use_commit_id = '/' in ref_name or is_svn
248 use_commit_id = '/' in ref_name or is_svn
245 files_url = h.url(
249 files_url = h.url(
246 'files_home',
250 'files_home',
247 repo_name=self.db_repo_name,
251 repo_name=self.db_repo_name,
248 f_path=ref_name if is_svn else '',
252 f_path=ref_name if is_svn else '',
249 revision=commit_id if use_commit_id else ref_name,
253 revision=commit_id if use_commit_id else ref_name,
250 at=ref_name)
254 at=ref_name)
251
255
252 data.append({
256 data.append({
253 "name": _render('name', ref_name, files_url, closed),
257 "name": _render('name', ref_name, files_url, closed),
254 "name_raw": ref_name,
258 "name_raw": ref_name,
255 "date": _render('date', commit.date),
259 "date": _render('date', commit.date),
256 "date_raw": datetime_to_time(commit.date),
260 "date_raw": datetime_to_time(commit.date),
257 "author": _render('author', commit.author),
261 "author": _render('author', commit.author),
258 "commit": _render(
262 "commit": _render(
259 'commit', commit.message, commit.raw_id, commit.idx),
263 'commit', commit.message, commit.raw_id, commit.idx),
260 "commit_raw": commit.idx,
264 "commit_raw": commit.idx,
261 "compare": _render(
265 "compare": _render(
262 'compare', format_ref_id(ref_name, commit.raw_id)),
266 'compare', format_ref_id(ref_name, commit.raw_id)),
263 })
267 })
264
268
265 return data
269 return data
266
270
267
271
268 class RepoRoutePredicate(object):
272 class RepoRoutePredicate(object):
269 def __init__(self, val, config):
273 def __init__(self, val, config):
270 self.val = val
274 self.val = val
271
275
272 def text(self):
276 def text(self):
273 return 'repo_route = %s' % self.val
277 return 'repo_route = %s' % self.val
274
278
275 phash = text
279 phash = text
276
280
277 def __call__(self, info, request):
281 def __call__(self, info, request):
278
282
279 if hasattr(request, 'vcs_call'):
283 if hasattr(request, 'vcs_call'):
280 # skip vcs calls
284 # skip vcs calls
281 return
285 return
282
286
283 repo_name = info['match']['repo_name']
287 repo_name = info['match']['repo_name']
284 repo_model = repo.RepoModel()
288 repo_model = repo.RepoModel()
285 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
289 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
286
290
287 if by_name_match:
291 if by_name_match:
288 # register this as request object we can re-use later
292 # register this as request object we can re-use later
289 request.db_repo = by_name_match
293 request.db_repo = by_name_match
290 return True
294 return True
291
295
292 by_id_match = repo_model.get_repo_by_id(repo_name)
296 by_id_match = repo_model.get_repo_by_id(repo_name)
293 if by_id_match:
297 if by_id_match:
294 request.db_repo = by_id_match
298 request.db_repo = by_id_match
295 return True
299 return True
296
300
297 return False
301 return False
298
302
299
303
300 class RepoTypeRoutePredicate(object):
304 class RepoTypeRoutePredicate(object):
301 def __init__(self, val, config):
305 def __init__(self, val, config):
302 self.val = val or ['hg', 'git', 'svn']
306 self.val = val or ['hg', 'git', 'svn']
303
307
304 def text(self):
308 def text(self):
305 return 'repo_accepted_type = %s' % self.val
309 return 'repo_accepted_type = %s' % self.val
306
310
307 phash = text
311 phash = text
308
312
309 def __call__(self, info, request):
313 def __call__(self, info, request):
310 if hasattr(request, 'vcs_call'):
314 if hasattr(request, 'vcs_call'):
311 # skip vcs calls
315 # skip vcs calls
312 return
316 return
313
317
314 rhodecode_db_repo = request.db_repo
318 rhodecode_db_repo = request.db_repo
315
319
316 log.debug(
320 log.debug(
317 '%s checking repo type for %s in %s',
321 '%s checking repo type for %s in %s',
318 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
322 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
319
323
320 if rhodecode_db_repo.repo_type in self.val:
324 if rhodecode_db_repo.repo_type in self.val:
321 return True
325 return True
322 else:
326 else:
323 log.warning('Current view is not supported for repo type:%s',
327 log.warning('Current view is not supported for repo type:%s',
324 rhodecode_db_repo.repo_type)
328 rhodecode_db_repo.repo_type)
325 #
329 #
326 # h.flash(h.literal(
330 # h.flash(h.literal(
327 # _('Action not supported for %s.' % rhodecode_repo.alias)),
331 # _('Action not supported for %s.' % rhodecode_repo.alias)),
328 # category='warning')
332 # category='warning')
329 # return redirect(
333 # return redirect(
330 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
334 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
331
335
332 return False
336 return False
333
337
334
338
335 class RepoGroupRoutePredicate(object):
339 class RepoGroupRoutePredicate(object):
336 def __init__(self, val, config):
340 def __init__(self, val, config):
337 self.val = val
341 self.val = val
338
342
339 def text(self):
343 def text(self):
340 return 'repo_group_route = %s' % self.val
344 return 'repo_group_route = %s' % self.val
341
345
342 phash = text
346 phash = text
343
347
344 def __call__(self, info, request):
348 def __call__(self, info, request):
345 if hasattr(request, 'vcs_call'):
349 if hasattr(request, 'vcs_call'):
346 # skip vcs calls
350 # skip vcs calls
347 return
351 return
348
352
349 repo_group_name = info['match']['repo_group_name']
353 repo_group_name = info['match']['repo_group_name']
350 repo_group_model = repo_group.RepoGroupModel()
354 repo_group_model = repo_group.RepoGroupModel()
351 by_name_match = repo_group_model.get_by_group_name(
355 by_name_match = repo_group_model.get_by_group_name(
352 repo_group_name, cache=True)
356 repo_group_name, cache=True)
353
357
354 if by_name_match:
358 if by_name_match:
355 # register this as request object we can re-use later
359 # register this as request object we can re-use later
356 request.db_repo_group = by_name_match
360 request.db_repo_group = by_name_match
357 return True
361 return True
358
362
359 return False
363 return False
360
364
361
365
362 def includeme(config):
366 def includeme(config):
363 config.add_route_predicate(
367 config.add_route_predicate(
364 'repo_route', RepoRoutePredicate)
368 'repo_route', RepoRoutePredicate)
365 config.add_route_predicate(
369 config.add_route_predicate(
366 'repo_accepted_types', RepoTypeRoutePredicate)
370 'repo_accepted_types', RepoTypeRoutePredicate)
367 config.add_route_predicate(
371 config.add_route_predicate(
368 'repo_group_route', RepoGroupRoutePredicate)
372 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,525 +1,529 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 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25 from collections import OrderedDict
25 from collections import OrderedDict
26
26
27 from paste.registry import RegistryManager
27 from paste.registry import RegistryManager
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 from pylons.wsgiapp import PylonsApp
29 from pylons.wsgiapp import PylonsApp
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.config import Configurator
31 from pyramid.config import Configurator
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 from pyramid.events import ApplicationCreated
36 from pyramid.events import ApplicationCreated
37 from pyramid.renderers import render_to_response
37 from pyramid.renderers import render_to_response
38 from routes.middleware import RoutesMiddleware
38 from routes.middleware import RoutesMiddleware
39 import routes.util
39 import routes.util
40
40
41 import rhodecode
41 import rhodecode
42
42
43 from rhodecode.model import meta
43 from rhodecode.model import meta
44 from rhodecode.config import patches
44 from rhodecode.config import patches
45 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 from rhodecode.config.routing import STATIC_FILE_PREFIX
46 from rhodecode.config.environment import (
46 from rhodecode.config.environment import (
47 load_environment, load_pyramid_environment)
47 load_environment, load_pyramid_environment)
48
48
49 from rhodecode.lib.vcs import VCSCommunicationError
49 from rhodecode.lib.vcs import VCSCommunicationError
50 from rhodecode.lib.exceptions import VCSServerUnavailable
50 from rhodecode.lib.exceptions import VCSServerUnavailable
51 from rhodecode.lib.middleware import csrf
51 from rhodecode.lib.middleware import csrf
52 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
52 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
53 from rhodecode.lib.middleware.error_handling import (
53 from rhodecode.lib.middleware.error_handling import (
54 PylonsErrorHandlingMiddleware)
54 PylonsErrorHandlingMiddleware)
55 from rhodecode.lib.middleware.https_fixup import HttpsFixup
55 from rhodecode.lib.middleware.https_fixup import HttpsFixup
56 from rhodecode.lib.middleware.vcs import VCSMiddleware
56 from rhodecode.lib.middleware.vcs import VCSMiddleware
57 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
57 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
58 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
58 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
59 from rhodecode.subscribers import (
59 from rhodecode.subscribers import (
60 scan_repositories_if_enabled, write_js_routes_if_enabled,
60 scan_repositories_if_enabled, write_js_routes_if_enabled,
61 write_metadata_if_needed)
61 write_metadata_if_needed)
62
62
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 # this is used to avoid avoid the route lookup overhead in routesmiddleware
67 # this is used to avoid avoid the route lookup overhead in routesmiddleware
68 # for certain routes which won't go to pylons to - eg. static files, debugger
68 # for certain routes which won't go to pylons to - eg. static files, debugger
69 # it is only needed for the pylons migration and can be removed once complete
69 # it is only needed for the pylons migration and can be removed once complete
70 class SkippableRoutesMiddleware(RoutesMiddleware):
70 class SkippableRoutesMiddleware(RoutesMiddleware):
71 """ Routes middleware that allows you to skip prefixes """
71 """ Routes middleware that allows you to skip prefixes """
72
72
73 def __init__(self, *args, **kw):
73 def __init__(self, *args, **kw):
74 self.skip_prefixes = kw.pop('skip_prefixes', [])
74 self.skip_prefixes = kw.pop('skip_prefixes', [])
75 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
75 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
76
76
77 def __call__(self, environ, start_response):
77 def __call__(self, environ, start_response):
78 for prefix in self.skip_prefixes:
78 for prefix in self.skip_prefixes:
79 if environ['PATH_INFO'].startswith(prefix):
79 if environ['PATH_INFO'].startswith(prefix):
80 # added to avoid the case when a missing /_static route falls
80 # added to avoid the case when a missing /_static route falls
81 # through to pylons and causes an exception as pylons is
81 # through to pylons and causes an exception as pylons is
82 # expecting wsgiorg.routingargs to be set in the environ
82 # expecting wsgiorg.routingargs to be set in the environ
83 # by RoutesMiddleware.
83 # by RoutesMiddleware.
84 if 'wsgiorg.routing_args' not in environ:
84 if 'wsgiorg.routing_args' not in environ:
85 environ['wsgiorg.routing_args'] = (None, {})
85 environ['wsgiorg.routing_args'] = (None, {})
86 return self.app(environ, start_response)
86 return self.app(environ, start_response)
87
87
88 return super(SkippableRoutesMiddleware, self).__call__(
88 return super(SkippableRoutesMiddleware, self).__call__(
89 environ, start_response)
89 environ, start_response)
90
90
91
91
92 def make_app(global_conf, static_files=True, **app_conf):
92 def make_app(global_conf, static_files=True, **app_conf):
93 """Create a Pylons WSGI application and return it
93 """Create a Pylons WSGI application and return it
94
94
95 ``global_conf``
95 ``global_conf``
96 The inherited configuration for this application. Normally from
96 The inherited configuration for this application. Normally from
97 the [DEFAULT] section of the Paste ini file.
97 the [DEFAULT] section of the Paste ini file.
98
98
99 ``app_conf``
99 ``app_conf``
100 The application's local configuration. Normally specified in
100 The application's local configuration. Normally specified in
101 the [app:<name>] section of the Paste ini file (where <name>
101 the [app:<name>] section of the Paste ini file (where <name>
102 defaults to main).
102 defaults to main).
103
103
104 """
104 """
105 # Apply compatibility patches
105 # Apply compatibility patches
106 patches.kombu_1_5_1_python_2_7_11()
106 patches.kombu_1_5_1_python_2_7_11()
107 patches.inspect_getargspec()
107 patches.inspect_getargspec()
108
108
109 # Configure the Pylons environment
109 # Configure the Pylons environment
110 config = load_environment(global_conf, app_conf)
110 config = load_environment(global_conf, app_conf)
111
111
112 # The Pylons WSGI app
112 # The Pylons WSGI app
113 app = PylonsApp(config=config)
113 app = PylonsApp(config=config)
114
114
115 # Establish the Registry for this application
115 # Establish the Registry for this application
116 app = RegistryManager(app)
116 app = RegistryManager(app)
117
117
118 app.config = config
118 app.config = config
119
119
120 return app
120 return app
121
121
122
122
123 def make_pyramid_app(global_config, **settings):
123 def make_pyramid_app(global_config, **settings):
124 """
124 """
125 Constructs the WSGI application based on Pyramid and wraps the Pylons based
125 Constructs the WSGI application based on Pyramid and wraps the Pylons based
126 application.
126 application.
127
127
128 Specials:
128 Specials:
129
129
130 * We migrate from Pylons to Pyramid. While doing this, we keep both
130 * We migrate from Pylons to Pyramid. While doing this, we keep both
131 frameworks functional. This involves moving some WSGI middlewares around
131 frameworks functional. This involves moving some WSGI middlewares around
132 and providing access to some data internals, so that the old code is
132 and providing access to some data internals, so that the old code is
133 still functional.
133 still functional.
134
134
135 * The application can also be integrated like a plugin via the call to
135 * The application can also be integrated like a plugin via the call to
136 `includeme`. This is accompanied with the other utility functions which
136 `includeme`. This is accompanied with the other utility functions which
137 are called. Changing this should be done with great care to not break
137 are called. Changing this should be done with great care to not break
138 cases when these fragments are assembled from another place.
138 cases when these fragments are assembled from another place.
139
139
140 """
140 """
141 # The edition string should be available in pylons too, so we add it here
141 # The edition string should be available in pylons too, so we add it here
142 # before copying the settings.
142 # before copying the settings.
143 settings.setdefault('rhodecode.edition', 'Community Edition')
143 settings.setdefault('rhodecode.edition', 'Community Edition')
144
144
145 # As long as our Pylons application does expect "unprepared" settings, make
145 # As long as our Pylons application does expect "unprepared" settings, make
146 # sure that we keep an unmodified copy. This avoids unintentional change of
146 # sure that we keep an unmodified copy. This avoids unintentional change of
147 # behavior in the old application.
147 # behavior in the old application.
148 settings_pylons = settings.copy()
148 settings_pylons = settings.copy()
149
149
150 sanitize_settings_and_apply_defaults(settings)
150 sanitize_settings_and_apply_defaults(settings)
151 config = Configurator(settings=settings)
151 config = Configurator(settings=settings)
152 add_pylons_compat_data(config.registry, global_config, settings_pylons)
152 add_pylons_compat_data(config.registry, global_config, settings_pylons)
153
153
154 load_pyramid_environment(global_config, settings)
154 load_pyramid_environment(global_config, settings)
155
155
156 includeme_first(config)
156 includeme_first(config)
157 includeme(config)
157 includeme(config)
158 pyramid_app = config.make_wsgi_app()
158 pyramid_app = config.make_wsgi_app()
159 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
159 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
160 pyramid_app.config = config
160 pyramid_app.config = config
161
161
162 # creating the app uses a connection - return it after we are done
162 # creating the app uses a connection - return it after we are done
163 meta.Session.remove()
163 meta.Session.remove()
164
164
165 return pyramid_app
165 return pyramid_app
166
166
167
167
168 def make_not_found_view(config):
168 def make_not_found_view(config):
169 """
169 """
170 This creates the view which should be registered as not-found-view to
170 This creates the view which should be registered as not-found-view to
171 pyramid. Basically it contains of the old pylons app, converted to a view.
171 pyramid. Basically it contains of the old pylons app, converted to a view.
172 Additionally it is wrapped by some other middlewares.
172 Additionally it is wrapped by some other middlewares.
173 """
173 """
174 settings = config.registry.settings
174 settings = config.registry.settings
175 vcs_server_enabled = settings['vcs.server.enable']
175 vcs_server_enabled = settings['vcs.server.enable']
176
176
177 # Make pylons app from unprepared settings.
177 # Make pylons app from unprepared settings.
178 pylons_app = make_app(
178 pylons_app = make_app(
179 config.registry._pylons_compat_global_config,
179 config.registry._pylons_compat_global_config,
180 **config.registry._pylons_compat_settings)
180 **config.registry._pylons_compat_settings)
181 config.registry._pylons_compat_config = pylons_app.config
181 config.registry._pylons_compat_config = pylons_app.config
182
182
183 # Appenlight monitoring.
183 # Appenlight monitoring.
184 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
184 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
185 pylons_app, settings)
185 pylons_app, settings)
186
186
187 # The pylons app is executed inside of the pyramid 404 exception handler.
187 # The pylons app is executed inside of the pyramid 404 exception handler.
188 # Exceptions which are raised inside of it are not handled by pyramid
188 # Exceptions which are raised inside of it are not handled by pyramid
189 # again. Therefore we add a middleware that invokes the error handler in
189 # again. Therefore we add a middleware that invokes the error handler in
190 # case of an exception or error response. This way we return proper error
190 # case of an exception or error response. This way we return proper error
191 # HTML pages in case of an error.
191 # HTML pages in case of an error.
192 reraise = (settings.get('debugtoolbar.enabled', False) or
192 reraise = (settings.get('debugtoolbar.enabled', False) or
193 rhodecode.disable_error_handler)
193 rhodecode.disable_error_handler)
194 pylons_app = PylonsErrorHandlingMiddleware(
194 pylons_app = PylonsErrorHandlingMiddleware(
195 pylons_app, error_handler, reraise)
195 pylons_app, error_handler, reraise)
196
196
197 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
197 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
198 # view to handle the request. Therefore it is wrapped around the pylons
198 # view to handle the request. Therefore it is wrapped around the pylons
199 # app. It has to be outside of the error handling otherwise error responses
199 # app. It has to be outside of the error handling otherwise error responses
200 # from the vcsserver are converted to HTML error pages. This confuses the
200 # from the vcsserver are converted to HTML error pages. This confuses the
201 # command line tools and the user won't get a meaningful error message.
201 # command line tools and the user won't get a meaningful error message.
202 if vcs_server_enabled:
202 if vcs_server_enabled:
203 pylons_app = VCSMiddleware(
203 pylons_app = VCSMiddleware(
204 pylons_app, settings, appenlight_client, registry=config.registry)
204 pylons_app, settings, appenlight_client, registry=config.registry)
205
205
206 # Convert WSGI app to pyramid view and return it.
206 # Convert WSGI app to pyramid view and return it.
207 return wsgiapp(pylons_app)
207 return wsgiapp(pylons_app)
208
208
209
209
210 def add_pylons_compat_data(registry, global_config, settings):
210 def add_pylons_compat_data(registry, global_config, settings):
211 """
211 """
212 Attach data to the registry to support the Pylons integration.
212 Attach data to the registry to support the Pylons integration.
213 """
213 """
214 registry._pylons_compat_global_config = global_config
214 registry._pylons_compat_global_config = global_config
215 registry._pylons_compat_settings = settings
215 registry._pylons_compat_settings = settings
216
216
217
217
218 def error_handler(exception, request):
218 def error_handler(exception, request):
219 import rhodecode
219 import rhodecode
220 from rhodecode.lib import helpers
220 from rhodecode.lib import helpers
221
221
222 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
222 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
223
223
224 base_response = HTTPInternalServerError()
224 base_response = HTTPInternalServerError()
225 # prefer original exception for the response since it may have headers set
225 # prefer original exception for the response since it may have headers set
226 if isinstance(exception, HTTPException):
226 if isinstance(exception, HTTPException):
227 base_response = exception
227 base_response = exception
228 elif isinstance(exception, VCSCommunicationError):
228 elif isinstance(exception, VCSCommunicationError):
229 base_response = VCSServerUnavailable()
229 base_response = VCSServerUnavailable()
230
230
231 def is_http_error(response):
231 def is_http_error(response):
232 # error which should have traceback
232 # error which should have traceback
233 return response.status_code > 499
233 return response.status_code > 499
234
234
235 if is_http_error(base_response):
235 if is_http_error(base_response):
236 log.exception(
236 log.exception(
237 'error occurred handling this request for path: %s', request.path)
237 'error occurred handling this request for path: %s', request.path)
238
238
239 c = AttributeDict()
239 c = AttributeDict()
240 c.error_message = base_response.status
240 c.error_message = base_response.status
241 c.error_explanation = base_response.explanation or str(base_response)
241 c.error_explanation = base_response.explanation or str(base_response)
242 c.visual = AttributeDict()
242 c.visual = AttributeDict()
243
243
244 c.visual.rhodecode_support_url = (
244 c.visual.rhodecode_support_url = (
245 request.registry.settings.get('rhodecode_support_url') or
245 request.registry.settings.get('rhodecode_support_url') or
246 request.route_url('rhodecode_support')
246 request.route_url('rhodecode_support')
247 )
247 )
248 c.redirect_time = 0
248 c.redirect_time = 0
249 c.rhodecode_name = rhodecode_title
249 c.rhodecode_name = rhodecode_title
250 if not c.rhodecode_name:
250 if not c.rhodecode_name:
251 c.rhodecode_name = 'Rhodecode'
251 c.rhodecode_name = 'Rhodecode'
252
252
253 c.causes = []
253 c.causes = []
254 if hasattr(base_response, 'causes'):
254 if hasattr(base_response, 'causes'):
255 c.causes = base_response.causes
255 c.causes = base_response.causes
256 c.messages = helpers.flash.pop_messages(request=request)
256 c.messages = helpers.flash.pop_messages(request=request)
257
257
258 response = render_to_response(
258 response = render_to_response(
259 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
259 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
260 response=base_response)
260 response=base_response)
261
261
262 return response
262 return response
263
263
264
264
265 def includeme(config):
265 def includeme(config):
266 settings = config.registry.settings
266 settings = config.registry.settings
267
267
268 # plugin information
268 # plugin information
269 config.registry.rhodecode_plugins = OrderedDict()
269 config.registry.rhodecode_plugins = OrderedDict()
270
270
271 config.add_directive(
271 config.add_directive(
272 'register_rhodecode_plugin', register_rhodecode_plugin)
272 'register_rhodecode_plugin', register_rhodecode_plugin)
273
273
274 if asbool(settings.get('appenlight', 'false')):
274 if asbool(settings.get('appenlight', 'false')):
275 config.include('appenlight_client.ext.pyramid_tween')
275 config.include('appenlight_client.ext.pyramid_tween')
276
276
277 # Includes which are required. The application would fail without them.
277 # Includes which are required. The application would fail without them.
278 config.include('pyramid_mako')
278 config.include('pyramid_mako')
279 config.include('pyramid_beaker')
279 config.include('pyramid_beaker')
280
280
281 config.include('rhodecode.authentication')
281 config.include('rhodecode.authentication')
282 config.include('rhodecode.integrations')
282 config.include('rhodecode.integrations')
283
283
284 # apps
284 # apps
285 config.include('rhodecode.apps._base')
285 config.include('rhodecode.apps._base')
286 config.include('rhodecode.apps.ops')
286 config.include('rhodecode.apps.ops')
287
287
288 config.include('rhodecode.apps.admin')
288 config.include('rhodecode.apps.admin')
289 config.include('rhodecode.apps.channelstream')
289 config.include('rhodecode.apps.channelstream')
290 config.include('rhodecode.apps.login')
290 config.include('rhodecode.apps.login')
291 config.include('rhodecode.apps.home')
291 config.include('rhodecode.apps.home')
292 config.include('rhodecode.apps.repository')
292 config.include('rhodecode.apps.repository')
293 config.include('rhodecode.apps.repo_group')
293 config.include('rhodecode.apps.repo_group')
294 config.include('rhodecode.apps.search')
294 config.include('rhodecode.apps.search')
295 config.include('rhodecode.apps.user_profile')
295 config.include('rhodecode.apps.user_profile')
296 config.include('rhodecode.apps.my_account')
296 config.include('rhodecode.apps.my_account')
297 config.include('rhodecode.apps.svn_support')
297 config.include('rhodecode.apps.svn_support')
298 config.include('rhodecode.apps.gist')
298 config.include('rhodecode.apps.gist')
299
299
300 config.include('rhodecode.apps.debug_style')
300 config.include('rhodecode.apps.debug_style')
301 config.include('rhodecode.tweens')
301 config.include('rhodecode.tweens')
302 config.include('rhodecode.api')
302 config.include('rhodecode.api')
303
303
304 config.add_route(
304 config.add_route(
305 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
305 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
306
306
307 config.add_translation_dirs('rhodecode:i18n/')
307 config.add_translation_dirs('rhodecode:i18n/')
308 settings['default_locale_name'] = settings.get('lang', 'en')
308 settings['default_locale_name'] = settings.get('lang', 'en')
309
309
310 # Add subscribers.
310 # Add subscribers.
311 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
312 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
312 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
313 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
313 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
314
314
315 config.add_request_method(
315 config.add_request_method(
316 'rhodecode.lib.partial_renderer.get_partial_renderer',
316 'rhodecode.lib.partial_renderer.get_partial_renderer',
317 'get_partial_renderer')
317 'get_partial_renderer')
318
318
319 # events
319 # events
320 # TODO(marcink): this should be done when pyramid migration is finished
320 # TODO(marcink): this should be done when pyramid migration is finished
321 # config.add_subscriber(
321 # config.add_subscriber(
322 # 'rhodecode.integrations.integrations_event_handler',
322 # 'rhodecode.integrations.integrations_event_handler',
323 # 'rhodecode.events.RhodecodeEvent')
323 # 'rhodecode.events.RhodecodeEvent')
324
324
325 # Set the authorization policy.
325 # Set the authorization policy.
326 authz_policy = ACLAuthorizationPolicy()
326 authz_policy = ACLAuthorizationPolicy()
327 config.set_authorization_policy(authz_policy)
327 config.set_authorization_policy(authz_policy)
328
328
329 # Set the default renderer for HTML templates to mako.
329 # Set the default renderer for HTML templates to mako.
330 config.add_mako_renderer('.html')
330 config.add_mako_renderer('.html')
331
331
332 config.add_renderer(
332 config.add_renderer(
333 name='json_ext',
333 name='json_ext',
334 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
334 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
335
335
336 # include RhodeCode plugins
336 # include RhodeCode plugins
337 includes = aslist(settings.get('rhodecode.includes', []))
337 includes = aslist(settings.get('rhodecode.includes', []))
338 for inc in includes:
338 for inc in includes:
339 config.include(inc)
339 config.include(inc)
340
340
341 # This is the glue which allows us to migrate in chunks. By registering the
341 # This is the glue which allows us to migrate in chunks. By registering the
342 # pylons based application as the "Not Found" view in Pyramid, we will
342 # pylons based application as the "Not Found" view in Pyramid, we will
343 # fallback to the old application each time the new one does not yet know
343 # fallback to the old application each time the new one does not yet know
344 # how to handle a request.
344 # how to handle a request.
345 config.add_notfound_view(make_not_found_view(config))
345 config.add_notfound_view(make_not_found_view(config))
346
346
347 if not settings.get('debugtoolbar.enabled', False):
347 if not settings.get('debugtoolbar.enabled', False):
348 # if no toolbar, then any exception gets caught and rendered
348 # disabled debugtoolbar handle all exceptions via the error_handlers
349 config.add_view(error_handler, context=Exception)
349 config.add_view(error_handler, context=Exception)
350
350
351 config.add_view(error_handler, context=HTTPError)
351 config.add_view(error_handler, context=HTTPError)
352
352
353
353
354 def includeme_first(config):
354 def includeme_first(config):
355 # redirect automatic browser favicon.ico requests to correct place
355 # redirect automatic browser favicon.ico requests to correct place
356 def favicon_redirect(context, request):
356 def favicon_redirect(context, request):
357 return HTTPFound(
357 return HTTPFound(
358 request.static_path('rhodecode:public/images/favicon.ico'))
358 request.static_path('rhodecode:public/images/favicon.ico'))
359
359
360 config.add_view(favicon_redirect, route_name='favicon')
360 config.add_view(favicon_redirect, route_name='favicon')
361 config.add_route('favicon', '/favicon.ico')
361 config.add_route('favicon', '/favicon.ico')
362
362
363 def robots_redirect(context, request):
363 def robots_redirect(context, request):
364 return HTTPFound(
364 return HTTPFound(
365 request.static_path('rhodecode:public/robots.txt'))
365 request.static_path('rhodecode:public/robots.txt'))
366
366
367 config.add_view(robots_redirect, route_name='robots')
367 config.add_view(robots_redirect, route_name='robots')
368 config.add_route('robots', '/robots.txt')
368 config.add_route('robots', '/robots.txt')
369
369
370 config.add_static_view(
370 config.add_static_view(
371 '_static/deform', 'deform:static')
371 '_static/deform', 'deform:static')
372 config.add_static_view(
372 config.add_static_view(
373 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
373 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
374
374
375
375
376 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
376 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
377 """
377 """
378 Apply outer WSGI middlewares around the application.
378 Apply outer WSGI middlewares around the application.
379
379
380 Part of this has been moved up from the Pylons layer, so that the
380 Part of this has been moved up from the Pylons layer, so that the
381 data is also available if old Pylons code is hit through an already ported
381 data is also available if old Pylons code is hit through an already ported
382 view.
382 view.
383 """
383 """
384 settings = config.registry.settings
384 settings = config.registry.settings
385
385
386 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
386 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
387 pyramid_app = HttpsFixup(pyramid_app, settings)
387 pyramid_app = HttpsFixup(pyramid_app, settings)
388
388
389 # Add RoutesMiddleware to support the pylons compatibility tween during
389 # Add RoutesMiddleware to support the pylons compatibility tween during
390 # migration to pyramid.
390 # migration to pyramid.
391
392 # TODO(marcink): remove after migration to pyramid
393 if hasattr(config.registry, '_pylons_compat_config'):
394 routes_map = config.registry._pylons_compat_config['routes.map']
391 pyramid_app = SkippableRoutesMiddleware(
395 pyramid_app = SkippableRoutesMiddleware(
392 pyramid_app, config.registry._pylons_compat_config['routes.map'],
396 pyramid_app, routes_map,
393 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
397 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
394
398
395 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
399 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
396
400
397 if settings['gzip_responses']:
401 if settings['gzip_responses']:
398 pyramid_app = make_gzip_middleware(
402 pyramid_app = make_gzip_middleware(
399 pyramid_app, settings, compress_level=1)
403 pyramid_app, settings, compress_level=1)
400
404
401 # this should be the outer most middleware in the wsgi stack since
405 # this should be the outer most middleware in the wsgi stack since
402 # middleware like Routes make database calls
406 # middleware like Routes make database calls
403 def pyramid_app_with_cleanup(environ, start_response):
407 def pyramid_app_with_cleanup(environ, start_response):
404 try:
408 try:
405 return pyramid_app(environ, start_response)
409 return pyramid_app(environ, start_response)
406 finally:
410 finally:
407 # Dispose current database session and rollback uncommitted
411 # Dispose current database session and rollback uncommitted
408 # transactions.
412 # transactions.
409 meta.Session.remove()
413 meta.Session.remove()
410
414
411 # In a single threaded mode server, on non sqlite db we should have
415 # In a single threaded mode server, on non sqlite db we should have
412 # '0 Current Checked out connections' at the end of a request,
416 # '0 Current Checked out connections' at the end of a request,
413 # if not, then something, somewhere is leaving a connection open
417 # if not, then something, somewhere is leaving a connection open
414 pool = meta.Base.metadata.bind.engine.pool
418 pool = meta.Base.metadata.bind.engine.pool
415 log.debug('sa pool status: %s', pool.status())
419 log.debug('sa pool status: %s', pool.status())
416
420
417 return pyramid_app_with_cleanup
421 return pyramid_app_with_cleanup
418
422
419
423
420 def sanitize_settings_and_apply_defaults(settings):
424 def sanitize_settings_and_apply_defaults(settings):
421 """
425 """
422 Applies settings defaults and does all type conversion.
426 Applies settings defaults and does all type conversion.
423
427
424 We would move all settings parsing and preparation into this place, so that
428 We would move all settings parsing and preparation into this place, so that
425 we have only one place left which deals with this part. The remaining parts
429 we have only one place left which deals with this part. The remaining parts
426 of the application would start to rely fully on well prepared settings.
430 of the application would start to rely fully on well prepared settings.
427
431
428 This piece would later be split up per topic to avoid a big fat monster
432 This piece would later be split up per topic to avoid a big fat monster
429 function.
433 function.
430 """
434 """
431
435
432 # Pyramid's mako renderer has to search in the templates folder so that the
436 # Pyramid's mako renderer has to search in the templates folder so that the
433 # old templates still work. Ported and new templates are expected to use
437 # old templates still work. Ported and new templates are expected to use
434 # real asset specifications for the includes.
438 # real asset specifications for the includes.
435 mako_directories = settings.setdefault('mako.directories', [
439 mako_directories = settings.setdefault('mako.directories', [
436 # Base templates of the original Pylons application
440 # Base templates of the original Pylons application
437 'rhodecode:templates',
441 'rhodecode:templates',
438 ])
442 ])
439 log.debug(
443 log.debug(
440 "Using the following Mako template directories: %s",
444 "Using the following Mako template directories: %s",
441 mako_directories)
445 mako_directories)
442
446
443 # Default includes, possible to change as a user
447 # Default includes, possible to change as a user
444 pyramid_includes = settings.setdefault('pyramid.includes', [
448 pyramid_includes = settings.setdefault('pyramid.includes', [
445 'rhodecode.lib.middleware.request_wrapper',
449 'rhodecode.lib.middleware.request_wrapper',
446 ])
450 ])
447 log.debug(
451 log.debug(
448 "Using the following pyramid.includes: %s",
452 "Using the following pyramid.includes: %s",
449 pyramid_includes)
453 pyramid_includes)
450
454
451 # TODO: johbo: Re-think this, usually the call to config.include
455 # TODO: johbo: Re-think this, usually the call to config.include
452 # should allow to pass in a prefix.
456 # should allow to pass in a prefix.
453 settings.setdefault('rhodecode.api.url', '/_admin/api')
457 settings.setdefault('rhodecode.api.url', '/_admin/api')
454
458
455 # Sanitize generic settings.
459 # Sanitize generic settings.
456 _list_setting(settings, 'default_encoding', 'UTF-8')
460 _list_setting(settings, 'default_encoding', 'UTF-8')
457 _bool_setting(settings, 'is_test', 'false')
461 _bool_setting(settings, 'is_test', 'false')
458 _bool_setting(settings, 'gzip_responses', 'false')
462 _bool_setting(settings, 'gzip_responses', 'false')
459
463
460 # Call split out functions that sanitize settings for each topic.
464 # Call split out functions that sanitize settings for each topic.
461 _sanitize_appenlight_settings(settings)
465 _sanitize_appenlight_settings(settings)
462 _sanitize_vcs_settings(settings)
466 _sanitize_vcs_settings(settings)
463
467
464 return settings
468 return settings
465
469
466
470
467 def _sanitize_appenlight_settings(settings):
471 def _sanitize_appenlight_settings(settings):
468 _bool_setting(settings, 'appenlight', 'false')
472 _bool_setting(settings, 'appenlight', 'false')
469
473
470
474
471 def _sanitize_vcs_settings(settings):
475 def _sanitize_vcs_settings(settings):
472 """
476 """
473 Applies settings defaults and does type conversion for all VCS related
477 Applies settings defaults and does type conversion for all VCS related
474 settings.
478 settings.
475 """
479 """
476 _string_setting(settings, 'vcs.svn.compatible_version', '')
480 _string_setting(settings, 'vcs.svn.compatible_version', '')
477 _string_setting(settings, 'git_rev_filter', '--all')
481 _string_setting(settings, 'git_rev_filter', '--all')
478 _string_setting(settings, 'vcs.hooks.protocol', 'http')
482 _string_setting(settings, 'vcs.hooks.protocol', 'http')
479 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
483 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
480 _string_setting(settings, 'vcs.server', '')
484 _string_setting(settings, 'vcs.server', '')
481 _string_setting(settings, 'vcs.server.log_level', 'debug')
485 _string_setting(settings, 'vcs.server.log_level', 'debug')
482 _string_setting(settings, 'vcs.server.protocol', 'http')
486 _string_setting(settings, 'vcs.server.protocol', 'http')
483 _bool_setting(settings, 'startup.import_repos', 'false')
487 _bool_setting(settings, 'startup.import_repos', 'false')
484 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
488 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
485 _bool_setting(settings, 'vcs.server.enable', 'true')
489 _bool_setting(settings, 'vcs.server.enable', 'true')
486 _bool_setting(settings, 'vcs.start_server', 'false')
490 _bool_setting(settings, 'vcs.start_server', 'false')
487 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
491 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
488 _int_setting(settings, 'vcs.connection_timeout', 3600)
492 _int_setting(settings, 'vcs.connection_timeout', 3600)
489
493
490 # Support legacy values of vcs.scm_app_implementation. Legacy
494 # Support legacy values of vcs.scm_app_implementation. Legacy
491 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
495 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
492 # which is now mapped to 'http'.
496 # which is now mapped to 'http'.
493 scm_app_impl = settings['vcs.scm_app_implementation']
497 scm_app_impl = settings['vcs.scm_app_implementation']
494 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
498 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
495 settings['vcs.scm_app_implementation'] = 'http'
499 settings['vcs.scm_app_implementation'] = 'http'
496
500
497
501
498 def _int_setting(settings, name, default):
502 def _int_setting(settings, name, default):
499 settings[name] = int(settings.get(name, default))
503 settings[name] = int(settings.get(name, default))
500
504
501
505
502 def _bool_setting(settings, name, default):
506 def _bool_setting(settings, name, default):
503 input = settings.get(name, default)
507 input = settings.get(name, default)
504 if isinstance(input, unicode):
508 if isinstance(input, unicode):
505 input = input.encode('utf8')
509 input = input.encode('utf8')
506 settings[name] = asbool(input)
510 settings[name] = asbool(input)
507
511
508
512
509 def _list_setting(settings, name, default):
513 def _list_setting(settings, name, default):
510 raw_value = settings.get(name, default)
514 raw_value = settings.get(name, default)
511
515
512 old_separator = ','
516 old_separator = ','
513 if old_separator in raw_value:
517 if old_separator in raw_value:
514 # If we get a comma separated list, pass it to our own function.
518 # If we get a comma separated list, pass it to our own function.
515 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
519 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
516 else:
520 else:
517 # Otherwise we assume it uses pyramids space/newline separation.
521 # Otherwise we assume it uses pyramids space/newline separation.
518 settings[name] = aslist(raw_value)
522 settings[name] = aslist(raw_value)
519
523
520
524
521 def _string_setting(settings, name, default, lower=True):
525 def _string_setting(settings, name, default, lower=True):
522 value = settings.get(name, default)
526 value = settings.get(name, default)
523 if lower:
527 if lower:
524 value = value.lower()
528 value = value.lower()
525 settings[name] = value
529 settings[name] = value
@@ -1,2039 +1,2040 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 from pylons import url as pylons_url
52 from pylons.i18n.translation import _, ungettext
51 from pylons.i18n.translation import _, ungettext
53 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
54
53
55 from webhelpers.html import literal, HTML, escape
54 from webhelpers.html import literal, HTML, escape
56 from webhelpers.html.tools import *
55 from webhelpers.html.tools import *
57 from webhelpers.html.builder import make_tag
56 from webhelpers.html.builder import make_tag
58 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
59 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, \
60 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, \
61 submit, text, password, textarea, title, ul, xml_declaration, radio
60 submit, text, password, textarea, title, ul, xml_declaration, radio
62 from webhelpers.html.tools import auto_link, button_to, highlight, \
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
63 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
64 from webhelpers.pylonslib import Flash as _Flash
63 from webhelpers.pylonslib import Flash as _Flash
65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 replace_whitespace, urlify, truncate, wrap_paragraphs
66 replace_whitespace, urlify, truncate, wrap_paragraphs
68 from webhelpers.date import time_ago_in_words
67 from webhelpers.date import time_ago_in_words
69 from webhelpers.paginate import Page as _Page
68 from webhelpers.paginate import Page as _Page
70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 from webhelpers2.number import format_byte_size
71 from webhelpers2.number import format_byte_size
73
72
74 from rhodecode.lib.action_parser import action_parser
73 from rhodecode.lib.action_parser import action_parser
75 from rhodecode.lib.ext_json import json
74 from rhodecode.lib.ext_json import json
76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 AttributeDict, safe_int, md5, md5_safe
78 AttributeDict, safe_int, md5, md5_safe
80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
84 from rhodecode.model.changeset_status import ChangesetStatusModel
83 from rhodecode.model.changeset_status import ChangesetStatusModel
85 from rhodecode.model.db import Permission, User, Repository
84 from rhodecode.model.db import Permission, User, Repository
86 from rhodecode.model.repo_group import RepoGroupModel
85 from rhodecode.model.repo_group import RepoGroupModel
87 from rhodecode.model.settings import IssueTrackerSettingsModel
86 from rhodecode.model.settings import IssueTrackerSettingsModel
88
87
89 log = logging.getLogger(__name__)
88 log = logging.getLogger(__name__)
90
89
91
90
92 DEFAULT_USER = User.DEFAULT_USER
91 DEFAULT_USER = User.DEFAULT_USER
93 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
94
93
95
94
96 def url(*args, **kw):
95 def url(*args, **kw):
96 from pylons import url as pylons_url
97 return pylons_url(*args, **kw)
97 return pylons_url(*args, **kw)
98
98
99
99
100 def pylons_url_current(*args, **kw):
100 def pylons_url_current(*args, **kw):
101 """
101 """
102 This function overrides pylons.url.current() which returns the current
102 This function overrides pylons.url.current() which returns the current
103 path so that it will also work from a pyramid only context. This
103 path so that it will also work from a pyramid only context. This
104 should be removed once port to pyramid is complete.
104 should be removed once port to pyramid is complete.
105 """
105 """
106 from pylons import url as pylons_url
106 if not args and not kw:
107 if not args and not kw:
107 request = get_current_request()
108 request = get_current_request()
108 return request.path
109 return request.path
109 return pylons_url.current(*args, **kw)
110 return pylons_url.current(*args, **kw)
110
111
111 url.current = pylons_url_current
112 url.current = pylons_url_current
112
113
113
114
114 def url_replace(**qargs):
115 def url_replace(**qargs):
115 """ Returns the current request url while replacing query string args """
116 """ Returns the current request url while replacing query string args """
116
117
117 request = get_current_request()
118 request = get_current_request()
118 new_args = request.GET.mixed()
119 new_args = request.GET.mixed()
119 new_args.update(qargs)
120 new_args.update(qargs)
120 return url('', **new_args)
121 return url('', **new_args)
121
122
122
123
123 def asset(path, ver=None, **kwargs):
124 def asset(path, ver=None, **kwargs):
124 """
125 """
125 Helper to generate a static asset file path for rhodecode assets
126 Helper to generate a static asset file path for rhodecode assets
126
127
127 eg. h.asset('images/image.png', ver='3923')
128 eg. h.asset('images/image.png', ver='3923')
128
129
129 :param path: path of asset
130 :param path: path of asset
130 :param ver: optional version query param to append as ?ver=
131 :param ver: optional version query param to append as ?ver=
131 """
132 """
132 request = get_current_request()
133 request = get_current_request()
133 query = {}
134 query = {}
134 query.update(kwargs)
135 query.update(kwargs)
135 if ver:
136 if ver:
136 query = {'ver': ver}
137 query = {'ver': ver}
137 return request.static_path(
138 return request.static_path(
138 'rhodecode:public/{}'.format(path), _query=query)
139 'rhodecode:public/{}'.format(path), _query=query)
139
140
140
141
141 default_html_escape_table = {
142 default_html_escape_table = {
142 ord('&'): u'&amp;',
143 ord('&'): u'&amp;',
143 ord('<'): u'&lt;',
144 ord('<'): u'&lt;',
144 ord('>'): u'&gt;',
145 ord('>'): u'&gt;',
145 ord('"'): u'&quot;',
146 ord('"'): u'&quot;',
146 ord("'"): u'&#39;',
147 ord("'"): u'&#39;',
147 }
148 }
148
149
149
150
150 def html_escape(text, html_escape_table=default_html_escape_table):
151 def html_escape(text, html_escape_table=default_html_escape_table):
151 """Produce entities within text."""
152 """Produce entities within text."""
152 return text.translate(html_escape_table)
153 return text.translate(html_escape_table)
153
154
154
155
155 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
156 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
156 """
157 """
157 Truncate string ``s`` at the first occurrence of ``sub``.
158 Truncate string ``s`` at the first occurrence of ``sub``.
158
159
159 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
160 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
160 """
161 """
161 suffix_if_chopped = suffix_if_chopped or ''
162 suffix_if_chopped = suffix_if_chopped or ''
162 pos = s.find(sub)
163 pos = s.find(sub)
163 if pos == -1:
164 if pos == -1:
164 return s
165 return s
165
166
166 if inclusive:
167 if inclusive:
167 pos += len(sub)
168 pos += len(sub)
168
169
169 chopped = s[:pos]
170 chopped = s[:pos]
170 left = s[pos:].strip()
171 left = s[pos:].strip()
171
172
172 if left and suffix_if_chopped:
173 if left and suffix_if_chopped:
173 chopped += suffix_if_chopped
174 chopped += suffix_if_chopped
174
175
175 return chopped
176 return chopped
176
177
177
178
178 def shorter(text, size=20):
179 def shorter(text, size=20):
179 postfix = '...'
180 postfix = '...'
180 if len(text) > size:
181 if len(text) > size:
181 return text[:size - len(postfix)] + postfix
182 return text[:size - len(postfix)] + postfix
182 return text
183 return text
183
184
184
185
185 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 """
187 """
187 Reset button
188 Reset button
188 """
189 """
189 _set_input_attrs(attrs, type, name, value)
190 _set_input_attrs(attrs, type, name, value)
190 _set_id_attr(attrs, id, name)
191 _set_id_attr(attrs, id, name)
191 convert_boolean_attrs(attrs, ["disabled"])
192 convert_boolean_attrs(attrs, ["disabled"])
192 return HTML.input(**attrs)
193 return HTML.input(**attrs)
193
194
194 reset = _reset
195 reset = _reset
195 safeid = _make_safe_id_component
196 safeid = _make_safe_id_component
196
197
197
198
198 def branding(name, length=40):
199 def branding(name, length=40):
199 return truncate(name, length, indicator="")
200 return truncate(name, length, indicator="")
200
201
201
202
202 def FID(raw_id, path):
203 def FID(raw_id, path):
203 """
204 """
204 Creates a unique ID for filenode based on it's hash of path and commit
205 Creates a unique ID for filenode based on it's hash of path and commit
205 it's safe to use in urls
206 it's safe to use in urls
206
207
207 :param raw_id:
208 :param raw_id:
208 :param path:
209 :param path:
209 """
210 """
210
211
211 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
212 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
212
213
213
214
214 class _GetError(object):
215 class _GetError(object):
215 """Get error from form_errors, and represent it as span wrapped error
216 """Get error from form_errors, and represent it as span wrapped error
216 message
217 message
217
218
218 :param field_name: field to fetch errors for
219 :param field_name: field to fetch errors for
219 :param form_errors: form errors dict
220 :param form_errors: form errors dict
220 """
221 """
221
222
222 def __call__(self, field_name, form_errors):
223 def __call__(self, field_name, form_errors):
223 tmpl = """<span class="error_msg">%s</span>"""
224 tmpl = """<span class="error_msg">%s</span>"""
224 if form_errors and field_name in form_errors:
225 if form_errors and field_name in form_errors:
225 return literal(tmpl % form_errors.get(field_name))
226 return literal(tmpl % form_errors.get(field_name))
226
227
227 get_error = _GetError()
228 get_error = _GetError()
228
229
229
230
230 class _ToolTip(object):
231 class _ToolTip(object):
231
232
232 def __call__(self, tooltip_title, trim_at=50):
233 def __call__(self, tooltip_title, trim_at=50):
233 """
234 """
234 Special function just to wrap our text into nice formatted
235 Special function just to wrap our text into nice formatted
235 autowrapped text
236 autowrapped text
236
237
237 :param tooltip_title:
238 :param tooltip_title:
238 """
239 """
239 tooltip_title = escape(tooltip_title)
240 tooltip_title = escape(tooltip_title)
240 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
241 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
241 return tooltip_title
242 return tooltip_title
242 tooltip = _ToolTip()
243 tooltip = _ToolTip()
243
244
244
245
245 def files_breadcrumbs(repo_name, commit_id, file_path):
246 def files_breadcrumbs(repo_name, commit_id, file_path):
246 if isinstance(file_path, str):
247 if isinstance(file_path, str):
247 file_path = safe_unicode(file_path)
248 file_path = safe_unicode(file_path)
248
249
249 # TODO: johbo: Is this always a url like path, or is this operating
250 # TODO: johbo: Is this always a url like path, or is this operating
250 # system dependent?
251 # system dependent?
251 path_segments = file_path.split('/')
252 path_segments = file_path.split('/')
252
253
253 repo_name_html = escape(repo_name)
254 repo_name_html = escape(repo_name)
254 if len(path_segments) == 1 and path_segments[0] == '':
255 if len(path_segments) == 1 and path_segments[0] == '':
255 url_segments = [repo_name_html]
256 url_segments = [repo_name_html]
256 else:
257 else:
257 url_segments = [
258 url_segments = [
258 link_to(
259 link_to(
259 repo_name_html,
260 repo_name_html,
260 url('files_home',
261 url('files_home',
261 repo_name=repo_name,
262 repo_name=repo_name,
262 revision=commit_id,
263 revision=commit_id,
263 f_path=''),
264 f_path=''),
264 class_='pjax-link')]
265 class_='pjax-link')]
265
266
266 last_cnt = len(path_segments) - 1
267 last_cnt = len(path_segments) - 1
267 for cnt, segment in enumerate(path_segments):
268 for cnt, segment in enumerate(path_segments):
268 if not segment:
269 if not segment:
269 continue
270 continue
270 segment_html = escape(segment)
271 segment_html = escape(segment)
271
272
272 if cnt != last_cnt:
273 if cnt != last_cnt:
273 url_segments.append(
274 url_segments.append(
274 link_to(
275 link_to(
275 segment_html,
276 segment_html,
276 url('files_home',
277 url('files_home',
277 repo_name=repo_name,
278 repo_name=repo_name,
278 revision=commit_id,
279 revision=commit_id,
279 f_path='/'.join(path_segments[:cnt + 1])),
280 f_path='/'.join(path_segments[:cnt + 1])),
280 class_='pjax-link'))
281 class_='pjax-link'))
281 else:
282 else:
282 url_segments.append(segment_html)
283 url_segments.append(segment_html)
283
284
284 return literal('/'.join(url_segments))
285 return literal('/'.join(url_segments))
285
286
286
287
287 class CodeHtmlFormatter(HtmlFormatter):
288 class CodeHtmlFormatter(HtmlFormatter):
288 """
289 """
289 My code Html Formatter for source codes
290 My code Html Formatter for source codes
290 """
291 """
291
292
292 def wrap(self, source, outfile):
293 def wrap(self, source, outfile):
293 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
294 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
294
295
295 def _wrap_code(self, source):
296 def _wrap_code(self, source):
296 for cnt, it in enumerate(source):
297 for cnt, it in enumerate(source):
297 i, t = it
298 i, t = it
298 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
299 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
299 yield i, t
300 yield i, t
300
301
301 def _wrap_tablelinenos(self, inner):
302 def _wrap_tablelinenos(self, inner):
302 dummyoutfile = StringIO.StringIO()
303 dummyoutfile = StringIO.StringIO()
303 lncount = 0
304 lncount = 0
304 for t, line in inner:
305 for t, line in inner:
305 if t:
306 if t:
306 lncount += 1
307 lncount += 1
307 dummyoutfile.write(line)
308 dummyoutfile.write(line)
308
309
309 fl = self.linenostart
310 fl = self.linenostart
310 mw = len(str(lncount + fl - 1))
311 mw = len(str(lncount + fl - 1))
311 sp = self.linenospecial
312 sp = self.linenospecial
312 st = self.linenostep
313 st = self.linenostep
313 la = self.lineanchors
314 la = self.lineanchors
314 aln = self.anchorlinenos
315 aln = self.anchorlinenos
315 nocls = self.noclasses
316 nocls = self.noclasses
316 if sp:
317 if sp:
317 lines = []
318 lines = []
318
319
319 for i in range(fl, fl + lncount):
320 for i in range(fl, fl + lncount):
320 if i % st == 0:
321 if i % st == 0:
321 if i % sp == 0:
322 if i % sp == 0:
322 if aln:
323 if aln:
323 lines.append('<a href="#%s%d" class="special">%*d</a>' %
324 lines.append('<a href="#%s%d" class="special">%*d</a>' %
324 (la, i, mw, i))
325 (la, i, mw, i))
325 else:
326 else:
326 lines.append('<span class="special">%*d</span>' % (mw, i))
327 lines.append('<span class="special">%*d</span>' % (mw, i))
327 else:
328 else:
328 if aln:
329 if aln:
329 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
330 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
330 else:
331 else:
331 lines.append('%*d' % (mw, i))
332 lines.append('%*d' % (mw, i))
332 else:
333 else:
333 lines.append('')
334 lines.append('')
334 ls = '\n'.join(lines)
335 ls = '\n'.join(lines)
335 else:
336 else:
336 lines = []
337 lines = []
337 for i in range(fl, fl + lncount):
338 for i in range(fl, fl + lncount):
338 if i % st == 0:
339 if i % st == 0:
339 if aln:
340 if aln:
340 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
341 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
341 else:
342 else:
342 lines.append('%*d' % (mw, i))
343 lines.append('%*d' % (mw, i))
343 else:
344 else:
344 lines.append('')
345 lines.append('')
345 ls = '\n'.join(lines)
346 ls = '\n'.join(lines)
346
347
347 # in case you wonder about the seemingly redundant <div> here: since the
348 # in case you wonder about the seemingly redundant <div> here: since the
348 # content in the other cell also is wrapped in a div, some browsers in
349 # content in the other cell also is wrapped in a div, some browsers in
349 # some configurations seem to mess up the formatting...
350 # some configurations seem to mess up the formatting...
350 if nocls:
351 if nocls:
351 yield 0, ('<table class="%stable">' % self.cssclass +
352 yield 0, ('<table class="%stable">' % self.cssclass +
352 '<tr><td><div class="linenodiv" '
353 '<tr><td><div class="linenodiv" '
353 'style="background-color: #f0f0f0; padding-right: 10px">'
354 'style="background-color: #f0f0f0; padding-right: 10px">'
354 '<pre style="line-height: 125%">' +
355 '<pre style="line-height: 125%">' +
355 ls + '</pre></div></td><td id="hlcode" class="code">')
356 ls + '</pre></div></td><td id="hlcode" class="code">')
356 else:
357 else:
357 yield 0, ('<table class="%stable">' % self.cssclass +
358 yield 0, ('<table class="%stable">' % self.cssclass +
358 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
359 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
359 ls + '</pre></div></td><td id="hlcode" class="code">')
360 ls + '</pre></div></td><td id="hlcode" class="code">')
360 yield 0, dummyoutfile.getvalue()
361 yield 0, dummyoutfile.getvalue()
361 yield 0, '</td></tr></table>'
362 yield 0, '</td></tr></table>'
362
363
363
364
364 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
365 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
365 def __init__(self, **kw):
366 def __init__(self, **kw):
366 # only show these line numbers if set
367 # only show these line numbers if set
367 self.only_lines = kw.pop('only_line_numbers', [])
368 self.only_lines = kw.pop('only_line_numbers', [])
368 self.query_terms = kw.pop('query_terms', [])
369 self.query_terms = kw.pop('query_terms', [])
369 self.max_lines = kw.pop('max_lines', 5)
370 self.max_lines = kw.pop('max_lines', 5)
370 self.line_context = kw.pop('line_context', 3)
371 self.line_context = kw.pop('line_context', 3)
371 self.url = kw.pop('url', None)
372 self.url = kw.pop('url', None)
372
373
373 super(CodeHtmlFormatter, self).__init__(**kw)
374 super(CodeHtmlFormatter, self).__init__(**kw)
374
375
375 def _wrap_code(self, source):
376 def _wrap_code(self, source):
376 for cnt, it in enumerate(source):
377 for cnt, it in enumerate(source):
377 i, t = it
378 i, t = it
378 t = '<pre>%s</pre>' % t
379 t = '<pre>%s</pre>' % t
379 yield i, t
380 yield i, t
380
381
381 def _wrap_tablelinenos(self, inner):
382 def _wrap_tablelinenos(self, inner):
382 yield 0, '<table class="code-highlight %stable">' % self.cssclass
383 yield 0, '<table class="code-highlight %stable">' % self.cssclass
383
384
384 last_shown_line_number = 0
385 last_shown_line_number = 0
385 current_line_number = 1
386 current_line_number = 1
386
387
387 for t, line in inner:
388 for t, line in inner:
388 if not t:
389 if not t:
389 yield t, line
390 yield t, line
390 continue
391 continue
391
392
392 if current_line_number in self.only_lines:
393 if current_line_number in self.only_lines:
393 if last_shown_line_number + 1 != current_line_number:
394 if last_shown_line_number + 1 != current_line_number:
394 yield 0, '<tr>'
395 yield 0, '<tr>'
395 yield 0, '<td class="line">...</td>'
396 yield 0, '<td class="line">...</td>'
396 yield 0, '<td id="hlcode" class="code"></td>'
397 yield 0, '<td id="hlcode" class="code"></td>'
397 yield 0, '</tr>'
398 yield 0, '</tr>'
398
399
399 yield 0, '<tr>'
400 yield 0, '<tr>'
400 if self.url:
401 if self.url:
401 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
402 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
402 self.url, current_line_number, current_line_number)
403 self.url, current_line_number, current_line_number)
403 else:
404 else:
404 yield 0, '<td class="line"><a href="">%i</a></td>' % (
405 yield 0, '<td class="line"><a href="">%i</a></td>' % (
405 current_line_number)
406 current_line_number)
406 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
407 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
407 yield 0, '</tr>'
408 yield 0, '</tr>'
408
409
409 last_shown_line_number = current_line_number
410 last_shown_line_number = current_line_number
410
411
411 current_line_number += 1
412 current_line_number += 1
412
413
413
414
414 yield 0, '</table>'
415 yield 0, '</table>'
415
416
416
417
417 def extract_phrases(text_query):
418 def extract_phrases(text_query):
418 """
419 """
419 Extracts phrases from search term string making sure phrases
420 Extracts phrases from search term string making sure phrases
420 contained in double quotes are kept together - and discarding empty values
421 contained in double quotes are kept together - and discarding empty values
421 or fully whitespace values eg.
422 or fully whitespace values eg.
422
423
423 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
424 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
424
425
425 """
426 """
426
427
427 in_phrase = False
428 in_phrase = False
428 buf = ''
429 buf = ''
429 phrases = []
430 phrases = []
430 for char in text_query:
431 for char in text_query:
431 if in_phrase:
432 if in_phrase:
432 if char == '"': # end phrase
433 if char == '"': # end phrase
433 phrases.append(buf)
434 phrases.append(buf)
434 buf = ''
435 buf = ''
435 in_phrase = False
436 in_phrase = False
436 continue
437 continue
437 else:
438 else:
438 buf += char
439 buf += char
439 continue
440 continue
440 else:
441 else:
441 if char == '"': # start phrase
442 if char == '"': # start phrase
442 in_phrase = True
443 in_phrase = True
443 phrases.append(buf)
444 phrases.append(buf)
444 buf = ''
445 buf = ''
445 continue
446 continue
446 elif char == ' ':
447 elif char == ' ':
447 phrases.append(buf)
448 phrases.append(buf)
448 buf = ''
449 buf = ''
449 continue
450 continue
450 else:
451 else:
451 buf += char
452 buf += char
452
453
453 phrases.append(buf)
454 phrases.append(buf)
454 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
455 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
455 return phrases
456 return phrases
456
457
457
458
458 def get_matching_offsets(text, phrases):
459 def get_matching_offsets(text, phrases):
459 """
460 """
460 Returns a list of string offsets in `text` that the list of `terms` match
461 Returns a list of string offsets in `text` that the list of `terms` match
461
462
462 >>> get_matching_offsets('some text here', ['some', 'here'])
463 >>> get_matching_offsets('some text here', ['some', 'here'])
463 [(0, 4), (10, 14)]
464 [(0, 4), (10, 14)]
464
465
465 """
466 """
466 offsets = []
467 offsets = []
467 for phrase in phrases:
468 for phrase in phrases:
468 for match in re.finditer(phrase, text):
469 for match in re.finditer(phrase, text):
469 offsets.append((match.start(), match.end()))
470 offsets.append((match.start(), match.end()))
470
471
471 return offsets
472 return offsets
472
473
473
474
474 def normalize_text_for_matching(x):
475 def normalize_text_for_matching(x):
475 """
476 """
476 Replaces all non alnum characters to spaces and lower cases the string,
477 Replaces all non alnum characters to spaces and lower cases the string,
477 useful for comparing two text strings without punctuation
478 useful for comparing two text strings without punctuation
478 """
479 """
479 return re.sub(r'[^\w]', ' ', x.lower())
480 return re.sub(r'[^\w]', ' ', x.lower())
480
481
481
482
482 def get_matching_line_offsets(lines, terms):
483 def get_matching_line_offsets(lines, terms):
483 """ Return a set of `lines` indices (starting from 1) matching a
484 """ Return a set of `lines` indices (starting from 1) matching a
484 text search query, along with `context` lines above/below matching lines
485 text search query, along with `context` lines above/below matching lines
485
486
486 :param lines: list of strings representing lines
487 :param lines: list of strings representing lines
487 :param terms: search term string to match in lines eg. 'some text'
488 :param terms: search term string to match in lines eg. 'some text'
488 :param context: number of lines above/below a matching line to add to result
489 :param context: number of lines above/below a matching line to add to result
489 :param max_lines: cut off for lines of interest
490 :param max_lines: cut off for lines of interest
490 eg.
491 eg.
491
492
492 text = '''
493 text = '''
493 words words words
494 words words words
494 words words words
495 words words words
495 some text some
496 some text some
496 words words words
497 words words words
497 words words words
498 words words words
498 text here what
499 text here what
499 '''
500 '''
500 get_matching_line_offsets(text, 'text', context=1)
501 get_matching_line_offsets(text, 'text', context=1)
501 {3: [(5, 9)], 6: [(0, 4)]]
502 {3: [(5, 9)], 6: [(0, 4)]]
502
503
503 """
504 """
504 matching_lines = {}
505 matching_lines = {}
505 phrases = [normalize_text_for_matching(phrase)
506 phrases = [normalize_text_for_matching(phrase)
506 for phrase in extract_phrases(terms)]
507 for phrase in extract_phrases(terms)]
507
508
508 for line_index, line in enumerate(lines, start=1):
509 for line_index, line in enumerate(lines, start=1):
509 match_offsets = get_matching_offsets(
510 match_offsets = get_matching_offsets(
510 normalize_text_for_matching(line), phrases)
511 normalize_text_for_matching(line), phrases)
511 if match_offsets:
512 if match_offsets:
512 matching_lines[line_index] = match_offsets
513 matching_lines[line_index] = match_offsets
513
514
514 return matching_lines
515 return matching_lines
515
516
516
517
517 def hsv_to_rgb(h, s, v):
518 def hsv_to_rgb(h, s, v):
518 """ Convert hsv color values to rgb """
519 """ Convert hsv color values to rgb """
519
520
520 if s == 0.0:
521 if s == 0.0:
521 return v, v, v
522 return v, v, v
522 i = int(h * 6.0) # XXX assume int() truncates!
523 i = int(h * 6.0) # XXX assume int() truncates!
523 f = (h * 6.0) - i
524 f = (h * 6.0) - i
524 p = v * (1.0 - s)
525 p = v * (1.0 - s)
525 q = v * (1.0 - s * f)
526 q = v * (1.0 - s * f)
526 t = v * (1.0 - s * (1.0 - f))
527 t = v * (1.0 - s * (1.0 - f))
527 i = i % 6
528 i = i % 6
528 if i == 0:
529 if i == 0:
529 return v, t, p
530 return v, t, p
530 if i == 1:
531 if i == 1:
531 return q, v, p
532 return q, v, p
532 if i == 2:
533 if i == 2:
533 return p, v, t
534 return p, v, t
534 if i == 3:
535 if i == 3:
535 return p, q, v
536 return p, q, v
536 if i == 4:
537 if i == 4:
537 return t, p, v
538 return t, p, v
538 if i == 5:
539 if i == 5:
539 return v, p, q
540 return v, p, q
540
541
541
542
542 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
543 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
543 """
544 """
544 Generator for getting n of evenly distributed colors using
545 Generator for getting n of evenly distributed colors using
545 hsv color and golden ratio. It always return same order of colors
546 hsv color and golden ratio. It always return same order of colors
546
547
547 :param n: number of colors to generate
548 :param n: number of colors to generate
548 :param saturation: saturation of returned colors
549 :param saturation: saturation of returned colors
549 :param lightness: lightness of returned colors
550 :param lightness: lightness of returned colors
550 :returns: RGB tuple
551 :returns: RGB tuple
551 """
552 """
552
553
553 golden_ratio = 0.618033988749895
554 golden_ratio = 0.618033988749895
554 h = 0.22717784590367374
555 h = 0.22717784590367374
555
556
556 for _ in xrange(n):
557 for _ in xrange(n):
557 h += golden_ratio
558 h += golden_ratio
558 h %= 1
559 h %= 1
559 HSV_tuple = [h, saturation, lightness]
560 HSV_tuple = [h, saturation, lightness]
560 RGB_tuple = hsv_to_rgb(*HSV_tuple)
561 RGB_tuple = hsv_to_rgb(*HSV_tuple)
561 yield map(lambda x: str(int(x * 256)), RGB_tuple)
562 yield map(lambda x: str(int(x * 256)), RGB_tuple)
562
563
563
564
564 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
565 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
565 """
566 """
566 Returns a function which when called with an argument returns a unique
567 Returns a function which when called with an argument returns a unique
567 color for that argument, eg.
568 color for that argument, eg.
568
569
569 :param n: number of colors to generate
570 :param n: number of colors to generate
570 :param saturation: saturation of returned colors
571 :param saturation: saturation of returned colors
571 :param lightness: lightness of returned colors
572 :param lightness: lightness of returned colors
572 :returns: css RGB string
573 :returns: css RGB string
573
574
574 >>> color_hash = color_hasher()
575 >>> color_hash = color_hasher()
575 >>> color_hash('hello')
576 >>> color_hash('hello')
576 'rgb(34, 12, 59)'
577 'rgb(34, 12, 59)'
577 >>> color_hash('hello')
578 >>> color_hash('hello')
578 'rgb(34, 12, 59)'
579 'rgb(34, 12, 59)'
579 >>> color_hash('other')
580 >>> color_hash('other')
580 'rgb(90, 224, 159)'
581 'rgb(90, 224, 159)'
581 """
582 """
582
583
583 color_dict = {}
584 color_dict = {}
584 cgenerator = unique_color_generator(
585 cgenerator = unique_color_generator(
585 saturation=saturation, lightness=lightness)
586 saturation=saturation, lightness=lightness)
586
587
587 def get_color_string(thing):
588 def get_color_string(thing):
588 if thing in color_dict:
589 if thing in color_dict:
589 col = color_dict[thing]
590 col = color_dict[thing]
590 else:
591 else:
591 col = color_dict[thing] = cgenerator.next()
592 col = color_dict[thing] = cgenerator.next()
592 return "rgb(%s)" % (', '.join(col))
593 return "rgb(%s)" % (', '.join(col))
593
594
594 return get_color_string
595 return get_color_string
595
596
596
597
597 def get_lexer_safe(mimetype=None, filepath=None):
598 def get_lexer_safe(mimetype=None, filepath=None):
598 """
599 """
599 Tries to return a relevant pygments lexer using mimetype/filepath name,
600 Tries to return a relevant pygments lexer using mimetype/filepath name,
600 defaulting to plain text if none could be found
601 defaulting to plain text if none could be found
601 """
602 """
602 lexer = None
603 lexer = None
603 try:
604 try:
604 if mimetype:
605 if mimetype:
605 lexer = get_lexer_for_mimetype(mimetype)
606 lexer = get_lexer_for_mimetype(mimetype)
606 if not lexer:
607 if not lexer:
607 lexer = get_lexer_for_filename(filepath)
608 lexer = get_lexer_for_filename(filepath)
608 except pygments.util.ClassNotFound:
609 except pygments.util.ClassNotFound:
609 pass
610 pass
610
611
611 if not lexer:
612 if not lexer:
612 lexer = get_lexer_by_name('text')
613 lexer = get_lexer_by_name('text')
613
614
614 return lexer
615 return lexer
615
616
616
617
617 def get_lexer_for_filenode(filenode):
618 def get_lexer_for_filenode(filenode):
618 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
619 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
619 return lexer
620 return lexer
620
621
621
622
622 def pygmentize(filenode, **kwargs):
623 def pygmentize(filenode, **kwargs):
623 """
624 """
624 pygmentize function using pygments
625 pygmentize function using pygments
625
626
626 :param filenode:
627 :param filenode:
627 """
628 """
628 lexer = get_lexer_for_filenode(filenode)
629 lexer = get_lexer_for_filenode(filenode)
629 return literal(code_highlight(filenode.content, lexer,
630 return literal(code_highlight(filenode.content, lexer,
630 CodeHtmlFormatter(**kwargs)))
631 CodeHtmlFormatter(**kwargs)))
631
632
632
633
633 def is_following_repo(repo_name, user_id):
634 def is_following_repo(repo_name, user_id):
634 from rhodecode.model.scm import ScmModel
635 from rhodecode.model.scm import ScmModel
635 return ScmModel().is_following_repo(repo_name, user_id)
636 return ScmModel().is_following_repo(repo_name, user_id)
636
637
637
638
638 class _Message(object):
639 class _Message(object):
639 """A message returned by ``Flash.pop_messages()``.
640 """A message returned by ``Flash.pop_messages()``.
640
641
641 Converting the message to a string returns the message text. Instances
642 Converting the message to a string returns the message text. Instances
642 also have the following attributes:
643 also have the following attributes:
643
644
644 * ``message``: the message text.
645 * ``message``: the message text.
645 * ``category``: the category specified when the message was created.
646 * ``category``: the category specified when the message was created.
646 """
647 """
647
648
648 def __init__(self, category, message):
649 def __init__(self, category, message):
649 self.category = category
650 self.category = category
650 self.message = message
651 self.message = message
651
652
652 def __str__(self):
653 def __str__(self):
653 return self.message
654 return self.message
654
655
655 __unicode__ = __str__
656 __unicode__ = __str__
656
657
657 def __html__(self):
658 def __html__(self):
658 return escape(safe_unicode(self.message))
659 return escape(safe_unicode(self.message))
659
660
660
661
661 class Flash(_Flash):
662 class Flash(_Flash):
662
663
663 def pop_messages(self, request=None):
664 def pop_messages(self, request=None):
664 """Return all accumulated messages and delete them from the session.
665 """Return all accumulated messages and delete them from the session.
665
666
666 The return value is a list of ``Message`` objects.
667 The return value is a list of ``Message`` objects.
667 """
668 """
668 messages = []
669 messages = []
669
670
670 if request:
671 if request:
671 session = request.session
672 session = request.session
672 else:
673 else:
673 from pylons import session
674 from pylons import session
674
675
675 # Pop the 'old' pylons flash messages. They are tuples of the form
676 # Pop the 'old' pylons flash messages. They are tuples of the form
676 # (category, message)
677 # (category, message)
677 for cat, msg in session.pop(self.session_key, []):
678 for cat, msg in session.pop(self.session_key, []):
678 messages.append(_Message(cat, msg))
679 messages.append(_Message(cat, msg))
679
680
680 # Pop the 'new' pyramid flash messages for each category as list
681 # Pop the 'new' pyramid flash messages for each category as list
681 # of strings.
682 # of strings.
682 for cat in self.categories:
683 for cat in self.categories:
683 for msg in session.pop_flash(queue=cat):
684 for msg in session.pop_flash(queue=cat):
684 messages.append(_Message(cat, msg))
685 messages.append(_Message(cat, msg))
685 # Map messages from the default queue to the 'notice' category.
686 # Map messages from the default queue to the 'notice' category.
686 for msg in session.pop_flash():
687 for msg in session.pop_flash():
687 messages.append(_Message('notice', msg))
688 messages.append(_Message('notice', msg))
688
689
689 session.save()
690 session.save()
690 return messages
691 return messages
691
692
692 def json_alerts(self):
693 def json_alerts(self):
693 payloads = []
694 payloads = []
694 messages = flash.pop_messages()
695 messages = flash.pop_messages()
695 if messages:
696 if messages:
696 for message in messages:
697 for message in messages:
697 subdata = {}
698 subdata = {}
698 if hasattr(message.message, 'rsplit'):
699 if hasattr(message.message, 'rsplit'):
699 flash_data = message.message.rsplit('|DELIM|', 1)
700 flash_data = message.message.rsplit('|DELIM|', 1)
700 org_message = flash_data[0]
701 org_message = flash_data[0]
701 if len(flash_data) > 1:
702 if len(flash_data) > 1:
702 subdata = json.loads(flash_data[1])
703 subdata = json.loads(flash_data[1])
703 else:
704 else:
704 org_message = message.message
705 org_message = message.message
705 payloads.append({
706 payloads.append({
706 'message': {
707 'message': {
707 'message': u'{}'.format(org_message),
708 'message': u'{}'.format(org_message),
708 'level': message.category,
709 'level': message.category,
709 'force': True,
710 'force': True,
710 'subdata': subdata
711 'subdata': subdata
711 }
712 }
712 })
713 })
713 return json.dumps(payloads)
714 return json.dumps(payloads)
714
715
715 flash = Flash()
716 flash = Flash()
716
717
717 #==============================================================================
718 #==============================================================================
718 # SCM FILTERS available via h.
719 # SCM FILTERS available via h.
719 #==============================================================================
720 #==============================================================================
720 from rhodecode.lib.vcs.utils import author_name, author_email
721 from rhodecode.lib.vcs.utils import author_name, author_email
721 from rhodecode.lib.utils2 import credentials_filter, age as _age
722 from rhodecode.lib.utils2 import credentials_filter, age as _age
722 from rhodecode.model.db import User, ChangesetStatus
723 from rhodecode.model.db import User, ChangesetStatus
723
724
724 age = _age
725 age = _age
725 capitalize = lambda x: x.capitalize()
726 capitalize = lambda x: x.capitalize()
726 email = author_email
727 email = author_email
727 short_id = lambda x: x[:12]
728 short_id = lambda x: x[:12]
728 hide_credentials = lambda x: ''.join(credentials_filter(x))
729 hide_credentials = lambda x: ''.join(credentials_filter(x))
729
730
730
731
731 def age_component(datetime_iso, value=None, time_is_local=False):
732 def age_component(datetime_iso, value=None, time_is_local=False):
732 title = value or format_date(datetime_iso)
733 title = value or format_date(datetime_iso)
733 tzinfo = '+00:00'
734 tzinfo = '+00:00'
734
735
735 # detect if we have a timezone info, otherwise, add it
736 # detect if we have a timezone info, otherwise, add it
736 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
737 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
737 if time_is_local:
738 if time_is_local:
738 tzinfo = time.strftime("+%H:%M",
739 tzinfo = time.strftime("+%H:%M",
739 time.gmtime(
740 time.gmtime(
740 (datetime.now() - datetime.utcnow()).seconds + 1
741 (datetime.now() - datetime.utcnow()).seconds + 1
741 )
742 )
742 )
743 )
743
744
744 return literal(
745 return literal(
745 '<time class="timeago tooltip" '
746 '<time class="timeago tooltip" '
746 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
747 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
747 datetime_iso, title, tzinfo))
748 datetime_iso, title, tzinfo))
748
749
749
750
750 def _shorten_commit_id(commit_id):
751 def _shorten_commit_id(commit_id):
751 from rhodecode import CONFIG
752 from rhodecode import CONFIG
752 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
753 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
753 return commit_id[:def_len]
754 return commit_id[:def_len]
754
755
755
756
756 def show_id(commit):
757 def show_id(commit):
757 """
758 """
758 Configurable function that shows ID
759 Configurable function that shows ID
759 by default it's r123:fffeeefffeee
760 by default it's r123:fffeeefffeee
760
761
761 :param commit: commit instance
762 :param commit: commit instance
762 """
763 """
763 from rhodecode import CONFIG
764 from rhodecode import CONFIG
764 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
765 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
765
766
766 raw_id = _shorten_commit_id(commit.raw_id)
767 raw_id = _shorten_commit_id(commit.raw_id)
767 if show_idx:
768 if show_idx:
768 return 'r%s:%s' % (commit.idx, raw_id)
769 return 'r%s:%s' % (commit.idx, raw_id)
769 else:
770 else:
770 return '%s' % (raw_id, )
771 return '%s' % (raw_id, )
771
772
772
773
773 def format_date(date):
774 def format_date(date):
774 """
775 """
775 use a standardized formatting for dates used in RhodeCode
776 use a standardized formatting for dates used in RhodeCode
776
777
777 :param date: date/datetime object
778 :param date: date/datetime object
778 :return: formatted date
779 :return: formatted date
779 """
780 """
780
781
781 if date:
782 if date:
782 _fmt = "%a, %d %b %Y %H:%M:%S"
783 _fmt = "%a, %d %b %Y %H:%M:%S"
783 return safe_unicode(date.strftime(_fmt))
784 return safe_unicode(date.strftime(_fmt))
784
785
785 return u""
786 return u""
786
787
787
788
788 class _RepoChecker(object):
789 class _RepoChecker(object):
789
790
790 def __init__(self, backend_alias):
791 def __init__(self, backend_alias):
791 self._backend_alias = backend_alias
792 self._backend_alias = backend_alias
792
793
793 def __call__(self, repository):
794 def __call__(self, repository):
794 if hasattr(repository, 'alias'):
795 if hasattr(repository, 'alias'):
795 _type = repository.alias
796 _type = repository.alias
796 elif hasattr(repository, 'repo_type'):
797 elif hasattr(repository, 'repo_type'):
797 _type = repository.repo_type
798 _type = repository.repo_type
798 else:
799 else:
799 _type = repository
800 _type = repository
800 return _type == self._backend_alias
801 return _type == self._backend_alias
801
802
802 is_git = _RepoChecker('git')
803 is_git = _RepoChecker('git')
803 is_hg = _RepoChecker('hg')
804 is_hg = _RepoChecker('hg')
804 is_svn = _RepoChecker('svn')
805 is_svn = _RepoChecker('svn')
805
806
806
807
807 def get_repo_type_by_name(repo_name):
808 def get_repo_type_by_name(repo_name):
808 repo = Repository.get_by_repo_name(repo_name)
809 repo = Repository.get_by_repo_name(repo_name)
809 return repo.repo_type
810 return repo.repo_type
810
811
811
812
812 def is_svn_without_proxy(repository):
813 def is_svn_without_proxy(repository):
813 if is_svn(repository):
814 if is_svn(repository):
814 from rhodecode.model.settings import VcsSettingsModel
815 from rhodecode.model.settings import VcsSettingsModel
815 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
816 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
816 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
817 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
817 return False
818 return False
818
819
819
820
820 def discover_user(author):
821 def discover_user(author):
821 """
822 """
822 Tries to discover RhodeCode User based on the autho string. Author string
823 Tries to discover RhodeCode User based on the autho string. Author string
823 is typically `FirstName LastName <email@address.com>`
824 is typically `FirstName LastName <email@address.com>`
824 """
825 """
825
826
826 # if author is already an instance use it for extraction
827 # if author is already an instance use it for extraction
827 if isinstance(author, User):
828 if isinstance(author, User):
828 return author
829 return author
829
830
830 # Valid email in the attribute passed, see if they're in the system
831 # Valid email in the attribute passed, see if they're in the system
831 _email = author_email(author)
832 _email = author_email(author)
832 if _email != '':
833 if _email != '':
833 user = User.get_by_email(_email, case_insensitive=True, cache=True)
834 user = User.get_by_email(_email, case_insensitive=True, cache=True)
834 if user is not None:
835 if user is not None:
835 return user
836 return user
836
837
837 # Maybe it's a username, we try to extract it and fetch by username ?
838 # Maybe it's a username, we try to extract it and fetch by username ?
838 _author = author_name(author)
839 _author = author_name(author)
839 user = User.get_by_username(_author, case_insensitive=True, cache=True)
840 user = User.get_by_username(_author, case_insensitive=True, cache=True)
840 if user is not None:
841 if user is not None:
841 return user
842 return user
842
843
843 return None
844 return None
844
845
845
846
846 def email_or_none(author):
847 def email_or_none(author):
847 # extract email from the commit string
848 # extract email from the commit string
848 _email = author_email(author)
849 _email = author_email(author)
849
850
850 # If we have an email, use it, otherwise
851 # If we have an email, use it, otherwise
851 # see if it contains a username we can get an email from
852 # see if it contains a username we can get an email from
852 if _email != '':
853 if _email != '':
853 return _email
854 return _email
854 else:
855 else:
855 user = User.get_by_username(
856 user = User.get_by_username(
856 author_name(author), case_insensitive=True, cache=True)
857 author_name(author), case_insensitive=True, cache=True)
857
858
858 if user is not None:
859 if user is not None:
859 return user.email
860 return user.email
860
861
861 # No valid email, not a valid user in the system, none!
862 # No valid email, not a valid user in the system, none!
862 return None
863 return None
863
864
864
865
865 def link_to_user(author, length=0, **kwargs):
866 def link_to_user(author, length=0, **kwargs):
866 user = discover_user(author)
867 user = discover_user(author)
867 # user can be None, but if we have it already it means we can re-use it
868 # user can be None, but if we have it already it means we can re-use it
868 # in the person() function, so we save 1 intensive-query
869 # in the person() function, so we save 1 intensive-query
869 if user:
870 if user:
870 author = user
871 author = user
871
872
872 display_person = person(author, 'username_or_name_or_email')
873 display_person = person(author, 'username_or_name_or_email')
873 if length:
874 if length:
874 display_person = shorter(display_person, length)
875 display_person = shorter(display_person, length)
875
876
876 if user:
877 if user:
877 return link_to(
878 return link_to(
878 escape(display_person),
879 escape(display_person),
879 route_path('user_profile', username=user.username),
880 route_path('user_profile', username=user.username),
880 **kwargs)
881 **kwargs)
881 else:
882 else:
882 return escape(display_person)
883 return escape(display_person)
883
884
884
885
885 def person(author, show_attr="username_and_name"):
886 def person(author, show_attr="username_and_name"):
886 user = discover_user(author)
887 user = discover_user(author)
887 if user:
888 if user:
888 return getattr(user, show_attr)
889 return getattr(user, show_attr)
889 else:
890 else:
890 _author = author_name(author)
891 _author = author_name(author)
891 _email = email(author)
892 _email = email(author)
892 return _author or _email
893 return _author or _email
893
894
894
895
895 def author_string(email):
896 def author_string(email):
896 if email:
897 if email:
897 user = User.get_by_email(email, case_insensitive=True, cache=True)
898 user = User.get_by_email(email, case_insensitive=True, cache=True)
898 if user:
899 if user:
899 if user.first_name or user.last_name:
900 if user.first_name or user.last_name:
900 return '%s %s &lt;%s&gt;' % (
901 return '%s %s &lt;%s&gt;' % (
901 user.first_name, user.last_name, email)
902 user.first_name, user.last_name, email)
902 else:
903 else:
903 return email
904 return email
904 else:
905 else:
905 return email
906 return email
906 else:
907 else:
907 return None
908 return None
908
909
909
910
910 def person_by_id(id_, show_attr="username_and_name"):
911 def person_by_id(id_, show_attr="username_and_name"):
911 # attr to return from fetched user
912 # attr to return from fetched user
912 person_getter = lambda usr: getattr(usr, show_attr)
913 person_getter = lambda usr: getattr(usr, show_attr)
913
914
914 #maybe it's an ID ?
915 #maybe it's an ID ?
915 if str(id_).isdigit() or isinstance(id_, int):
916 if str(id_).isdigit() or isinstance(id_, int):
916 id_ = int(id_)
917 id_ = int(id_)
917 user = User.get(id_)
918 user = User.get(id_)
918 if user is not None:
919 if user is not None:
919 return person_getter(user)
920 return person_getter(user)
920 return id_
921 return id_
921
922
922
923
923 def gravatar_with_user(author, show_disabled=False):
924 def gravatar_with_user(author, show_disabled=False):
924 from rhodecode.lib.utils import PartialRenderer
925 from rhodecode.lib.utils import PartialRenderer
925 _render = PartialRenderer('base/base.mako')
926 _render = PartialRenderer('base/base.mako')
926 return _render('gravatar_with_user', author, show_disabled=show_disabled)
927 return _render('gravatar_with_user', author, show_disabled=show_disabled)
927
928
928
929
929 def desc_stylize(value):
930 def desc_stylize(value):
930 """
931 """
931 converts tags from value into html equivalent
932 converts tags from value into html equivalent
932
933
933 :param value:
934 :param value:
934 """
935 """
935 if not value:
936 if not value:
936 return ''
937 return ''
937
938
938 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
939 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
939 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
940 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
940 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
941 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
941 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
942 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
942 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
943 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
943 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
944 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
944 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
945 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
945 '<div class="metatag" tag="lang">\\2</div>', value)
946 '<div class="metatag" tag="lang">\\2</div>', value)
946 value = re.sub(r'\[([a-z]+)\]',
947 value = re.sub(r'\[([a-z]+)\]',
947 '<div class="metatag" tag="\\1">\\1</div>', value)
948 '<div class="metatag" tag="\\1">\\1</div>', value)
948
949
949 return value
950 return value
950
951
951
952
952 def escaped_stylize(value):
953 def escaped_stylize(value):
953 """
954 """
954 converts tags from value into html equivalent, but escaping its value first
955 converts tags from value into html equivalent, but escaping its value first
955 """
956 """
956 if not value:
957 if not value:
957 return ''
958 return ''
958
959
959 # Using default webhelper escape method, but has to force it as a
960 # Using default webhelper escape method, but has to force it as a
960 # plain unicode instead of a markup tag to be used in regex expressions
961 # plain unicode instead of a markup tag to be used in regex expressions
961 value = unicode(escape(safe_unicode(value)))
962 value = unicode(escape(safe_unicode(value)))
962
963
963 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
964 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
964 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
965 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
965 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
966 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
966 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
967 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
967 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
968 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
968 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
969 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
969 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
970 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
970 '<div class="metatag" tag="lang">\\2</div>', value)
971 '<div class="metatag" tag="lang">\\2</div>', value)
971 value = re.sub(r'\[([a-z]+)\]',
972 value = re.sub(r'\[([a-z]+)\]',
972 '<div class="metatag" tag="\\1">\\1</div>', value)
973 '<div class="metatag" tag="\\1">\\1</div>', value)
973
974
974 return value
975 return value
975
976
976
977
977 def bool2icon(value):
978 def bool2icon(value):
978 """
979 """
979 Returns boolean value of a given value, represented as html element with
980 Returns boolean value of a given value, represented as html element with
980 classes that will represent icons
981 classes that will represent icons
981
982
982 :param value: given value to convert to html node
983 :param value: given value to convert to html node
983 """
984 """
984
985
985 if value: # does bool conversion
986 if value: # does bool conversion
986 return HTML.tag('i', class_="icon-true")
987 return HTML.tag('i', class_="icon-true")
987 else: # not true as bool
988 else: # not true as bool
988 return HTML.tag('i', class_="icon-false")
989 return HTML.tag('i', class_="icon-false")
989
990
990
991
991 #==============================================================================
992 #==============================================================================
992 # PERMS
993 # PERMS
993 #==============================================================================
994 #==============================================================================
994 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
995 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
995 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
996 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
996 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
997 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
997 csrf_token_key
998 csrf_token_key
998
999
999
1000
1000 #==============================================================================
1001 #==============================================================================
1001 # GRAVATAR URL
1002 # GRAVATAR URL
1002 #==============================================================================
1003 #==============================================================================
1003 class InitialsGravatar(object):
1004 class InitialsGravatar(object):
1004 def __init__(self, email_address, first_name, last_name, size=30,
1005 def __init__(self, email_address, first_name, last_name, size=30,
1005 background=None, text_color='#fff'):
1006 background=None, text_color='#fff'):
1006 self.size = size
1007 self.size = size
1007 self.first_name = first_name
1008 self.first_name = first_name
1008 self.last_name = last_name
1009 self.last_name = last_name
1009 self.email_address = email_address
1010 self.email_address = email_address
1010 self.background = background or self.str2color(email_address)
1011 self.background = background or self.str2color(email_address)
1011 self.text_color = text_color
1012 self.text_color = text_color
1012
1013
1013 def get_color_bank(self):
1014 def get_color_bank(self):
1014 """
1015 """
1015 returns a predefined list of colors that gravatars can use.
1016 returns a predefined list of colors that gravatars can use.
1016 Those are randomized distinct colors that guarantee readability and
1017 Those are randomized distinct colors that guarantee readability and
1017 uniqueness.
1018 uniqueness.
1018
1019
1019 generated with: http://phrogz.net/css/distinct-colors.html
1020 generated with: http://phrogz.net/css/distinct-colors.html
1020 """
1021 """
1021 return [
1022 return [
1022 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1023 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1023 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1024 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1024 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1025 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1025 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1026 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1026 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1027 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1027 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1028 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1028 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1029 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1029 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1030 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1030 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1031 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1031 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1032 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1032 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1033 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1033 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1034 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1034 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1035 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1035 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1036 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1036 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1037 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1037 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1038 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1038 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1039 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1039 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1040 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1040 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1041 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1041 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1042 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1042 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1043 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1043 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1044 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1044 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1045 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1045 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1046 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1046 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1047 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1047 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1048 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1048 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1049 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1049 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1050 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1050 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1051 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1051 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1052 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1052 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1053 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1053 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1054 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1054 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1055 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1055 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1056 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1056 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1057 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1057 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1058 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1058 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1059 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1059 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1060 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1060 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1061 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1061 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1062 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1062 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1063 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1063 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1064 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1064 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1065 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1065 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1066 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1066 '#4f8c46', '#368dd9', '#5c0073'
1067 '#4f8c46', '#368dd9', '#5c0073'
1067 ]
1068 ]
1068
1069
1069 def rgb_to_hex_color(self, rgb_tuple):
1070 def rgb_to_hex_color(self, rgb_tuple):
1070 """
1071 """
1071 Converts an rgb_tuple passed to an hex color.
1072 Converts an rgb_tuple passed to an hex color.
1072
1073
1073 :param rgb_tuple: tuple with 3 ints represents rgb color space
1074 :param rgb_tuple: tuple with 3 ints represents rgb color space
1074 """
1075 """
1075 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1076 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1076
1077
1077 def email_to_int_list(self, email_str):
1078 def email_to_int_list(self, email_str):
1078 """
1079 """
1079 Get every byte of the hex digest value of email and turn it to integer.
1080 Get every byte of the hex digest value of email and turn it to integer.
1080 It's going to be always between 0-255
1081 It's going to be always between 0-255
1081 """
1082 """
1082 digest = md5_safe(email_str.lower())
1083 digest = md5_safe(email_str.lower())
1083 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1084 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1084
1085
1085 def pick_color_bank_index(self, email_str, color_bank):
1086 def pick_color_bank_index(self, email_str, color_bank):
1086 return self.email_to_int_list(email_str)[0] % len(color_bank)
1087 return self.email_to_int_list(email_str)[0] % len(color_bank)
1087
1088
1088 def str2color(self, email_str):
1089 def str2color(self, email_str):
1089 """
1090 """
1090 Tries to map in a stable algorithm an email to color
1091 Tries to map in a stable algorithm an email to color
1091
1092
1092 :param email_str:
1093 :param email_str:
1093 """
1094 """
1094 color_bank = self.get_color_bank()
1095 color_bank = self.get_color_bank()
1095 # pick position (module it's length so we always find it in the
1096 # pick position (module it's length so we always find it in the
1096 # bank even if it's smaller than 256 values
1097 # bank even if it's smaller than 256 values
1097 pos = self.pick_color_bank_index(email_str, color_bank)
1098 pos = self.pick_color_bank_index(email_str, color_bank)
1098 return color_bank[pos]
1099 return color_bank[pos]
1099
1100
1100 def normalize_email(self, email_address):
1101 def normalize_email(self, email_address):
1101 import unicodedata
1102 import unicodedata
1102 # default host used to fill in the fake/missing email
1103 # default host used to fill in the fake/missing email
1103 default_host = u'localhost'
1104 default_host = u'localhost'
1104
1105
1105 if not email_address:
1106 if not email_address:
1106 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1107 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1107
1108
1108 email_address = safe_unicode(email_address)
1109 email_address = safe_unicode(email_address)
1109
1110
1110 if u'@' not in email_address:
1111 if u'@' not in email_address:
1111 email_address = u'%s@%s' % (email_address, default_host)
1112 email_address = u'%s@%s' % (email_address, default_host)
1112
1113
1113 if email_address.endswith(u'@'):
1114 if email_address.endswith(u'@'):
1114 email_address = u'%s%s' % (email_address, default_host)
1115 email_address = u'%s%s' % (email_address, default_host)
1115
1116
1116 email_address = unicodedata.normalize('NFKD', email_address)\
1117 email_address = unicodedata.normalize('NFKD', email_address)\
1117 .encode('ascii', 'ignore')
1118 .encode('ascii', 'ignore')
1118 return email_address
1119 return email_address
1119
1120
1120 def get_initials(self):
1121 def get_initials(self):
1121 """
1122 """
1122 Returns 2 letter initials calculated based on the input.
1123 Returns 2 letter initials calculated based on the input.
1123 The algorithm picks first given email address, and takes first letter
1124 The algorithm picks first given email address, and takes first letter
1124 of part before @, and then the first letter of server name. In case
1125 of part before @, and then the first letter of server name. In case
1125 the part before @ is in a format of `somestring.somestring2` it replaces
1126 the part before @ is in a format of `somestring.somestring2` it replaces
1126 the server letter with first letter of somestring2
1127 the server letter with first letter of somestring2
1127
1128
1128 In case function was initialized with both first and lastname, this
1129 In case function was initialized with both first and lastname, this
1129 overrides the extraction from email by first letter of the first and
1130 overrides the extraction from email by first letter of the first and
1130 last name. We add special logic to that functionality, In case Full name
1131 last name. We add special logic to that functionality, In case Full name
1131 is compound, like Guido Von Rossum, we use last part of the last name
1132 is compound, like Guido Von Rossum, we use last part of the last name
1132 (Von Rossum) picking `R`.
1133 (Von Rossum) picking `R`.
1133
1134
1134 Function also normalizes the non-ascii characters to they ascii
1135 Function also normalizes the non-ascii characters to they ascii
1135 representation, eg Δ„ => A
1136 representation, eg Δ„ => A
1136 """
1137 """
1137 import unicodedata
1138 import unicodedata
1138 # replace non-ascii to ascii
1139 # replace non-ascii to ascii
1139 first_name = unicodedata.normalize(
1140 first_name = unicodedata.normalize(
1140 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1141 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1141 last_name = unicodedata.normalize(
1142 last_name = unicodedata.normalize(
1142 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1143 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1143
1144
1144 # do NFKD encoding, and also make sure email has proper format
1145 # do NFKD encoding, and also make sure email has proper format
1145 email_address = self.normalize_email(self.email_address)
1146 email_address = self.normalize_email(self.email_address)
1146
1147
1147 # first push the email initials
1148 # first push the email initials
1148 prefix, server = email_address.split('@', 1)
1149 prefix, server = email_address.split('@', 1)
1149
1150
1150 # check if prefix is maybe a 'first_name.last_name' syntax
1151 # check if prefix is maybe a 'first_name.last_name' syntax
1151 _dot_split = prefix.rsplit('.', 1)
1152 _dot_split = prefix.rsplit('.', 1)
1152 if len(_dot_split) == 2:
1153 if len(_dot_split) == 2:
1153 initials = [_dot_split[0][0], _dot_split[1][0]]
1154 initials = [_dot_split[0][0], _dot_split[1][0]]
1154 else:
1155 else:
1155 initials = [prefix[0], server[0]]
1156 initials = [prefix[0], server[0]]
1156
1157
1157 # then try to replace either first_name or last_name
1158 # then try to replace either first_name or last_name
1158 fn_letter = (first_name or " ")[0].strip()
1159 fn_letter = (first_name or " ")[0].strip()
1159 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1160 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1160
1161
1161 if fn_letter:
1162 if fn_letter:
1162 initials[0] = fn_letter
1163 initials[0] = fn_letter
1163
1164
1164 if ln_letter:
1165 if ln_letter:
1165 initials[1] = ln_letter
1166 initials[1] = ln_letter
1166
1167
1167 return ''.join(initials).upper()
1168 return ''.join(initials).upper()
1168
1169
1169 def get_img_data_by_type(self, font_family, img_type):
1170 def get_img_data_by_type(self, font_family, img_type):
1170 default_user = """
1171 default_user = """
1171 <svg xmlns="http://www.w3.org/2000/svg"
1172 <svg xmlns="http://www.w3.org/2000/svg"
1172 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1173 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1173 viewBox="-15 -10 439.165 429.164"
1174 viewBox="-15 -10 439.165 429.164"
1174
1175
1175 xml:space="preserve"
1176 xml:space="preserve"
1176 style="background:{background};" >
1177 style="background:{background};" >
1177
1178
1178 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1179 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1179 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1180 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1180 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1181 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1181 168.596,153.916,216.671,
1182 168.596,153.916,216.671,
1182 204.583,216.671z" fill="{text_color}"/>
1183 204.583,216.671z" fill="{text_color}"/>
1183 <path d="M407.164,374.717L360.88,
1184 <path d="M407.164,374.717L360.88,
1184 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1185 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1185 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1186 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1186 15.366-44.203,23.488-69.076,23.488c-24.877,
1187 15.366-44.203,23.488-69.076,23.488c-24.877,
1187 0-48.762-8.122-69.078-23.488
1188 0-48.762-8.122-69.078-23.488
1188 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1189 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1189 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1190 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1190 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1191 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1191 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1192 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1192 19.402-10.527 C409.699,390.129,
1193 19.402-10.527 C409.699,390.129,
1193 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1194 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1194 </svg>""".format(
1195 </svg>""".format(
1195 size=self.size,
1196 size=self.size,
1196 background='#979797', # @grey4
1197 background='#979797', # @grey4
1197 text_color=self.text_color,
1198 text_color=self.text_color,
1198 font_family=font_family)
1199 font_family=font_family)
1199
1200
1200 return {
1201 return {
1201 "default_user": default_user
1202 "default_user": default_user
1202 }[img_type]
1203 }[img_type]
1203
1204
1204 def get_img_data(self, svg_type=None):
1205 def get_img_data(self, svg_type=None):
1205 """
1206 """
1206 generates the svg metadata for image
1207 generates the svg metadata for image
1207 """
1208 """
1208
1209
1209 font_family = ','.join([
1210 font_family = ','.join([
1210 'proximanovaregular',
1211 'proximanovaregular',
1211 'Proxima Nova Regular',
1212 'Proxima Nova Regular',
1212 'Proxima Nova',
1213 'Proxima Nova',
1213 'Arial',
1214 'Arial',
1214 'Lucida Grande',
1215 'Lucida Grande',
1215 'sans-serif'
1216 'sans-serif'
1216 ])
1217 ])
1217 if svg_type:
1218 if svg_type:
1218 return self.get_img_data_by_type(font_family, svg_type)
1219 return self.get_img_data_by_type(font_family, svg_type)
1219
1220
1220 initials = self.get_initials()
1221 initials = self.get_initials()
1221 img_data = """
1222 img_data = """
1222 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1223 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1223 width="{size}" height="{size}"
1224 width="{size}" height="{size}"
1224 style="width: 100%; height: 100%; background-color: {background}"
1225 style="width: 100%; height: 100%; background-color: {background}"
1225 viewBox="0 0 {size} {size}">
1226 viewBox="0 0 {size} {size}">
1226 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1227 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1227 pointer-events="auto" fill="{text_color}"
1228 pointer-events="auto" fill="{text_color}"
1228 font-family="{font_family}"
1229 font-family="{font_family}"
1229 style="font-weight: 400; font-size: {f_size}px;">{text}
1230 style="font-weight: 400; font-size: {f_size}px;">{text}
1230 </text>
1231 </text>
1231 </svg>""".format(
1232 </svg>""".format(
1232 size=self.size,
1233 size=self.size,
1233 f_size=self.size/1.85, # scale the text inside the box nicely
1234 f_size=self.size/1.85, # scale the text inside the box nicely
1234 background=self.background,
1235 background=self.background,
1235 text_color=self.text_color,
1236 text_color=self.text_color,
1236 text=initials.upper(),
1237 text=initials.upper(),
1237 font_family=font_family)
1238 font_family=font_family)
1238
1239
1239 return img_data
1240 return img_data
1240
1241
1241 def generate_svg(self, svg_type=None):
1242 def generate_svg(self, svg_type=None):
1242 img_data = self.get_img_data(svg_type)
1243 img_data = self.get_img_data(svg_type)
1243 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1244 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1244
1245
1245
1246
1246 def initials_gravatar(email_address, first_name, last_name, size=30):
1247 def initials_gravatar(email_address, first_name, last_name, size=30):
1247 svg_type = None
1248 svg_type = None
1248 if email_address == User.DEFAULT_USER_EMAIL:
1249 if email_address == User.DEFAULT_USER_EMAIL:
1249 svg_type = 'default_user'
1250 svg_type = 'default_user'
1250 klass = InitialsGravatar(email_address, first_name, last_name, size)
1251 klass = InitialsGravatar(email_address, first_name, last_name, size)
1251 return klass.generate_svg(svg_type=svg_type)
1252 return klass.generate_svg(svg_type=svg_type)
1252
1253
1253
1254
1254 def gravatar_url(email_address, size=30, request=None):
1255 def gravatar_url(email_address, size=30, request=None):
1255 request = get_current_request()
1256 request = get_current_request()
1256 if request and hasattr(request, 'call_context'):
1257 if request and hasattr(request, 'call_context'):
1257 _use_gravatar = request.call_context.visual.use_gravatar
1258 _use_gravatar = request.call_context.visual.use_gravatar
1258 _gravatar_url = request.call_context.visual.gravatar_url
1259 _gravatar_url = request.call_context.visual.gravatar_url
1259 else:
1260 else:
1260 # doh, we need to re-import those to mock it later
1261 # doh, we need to re-import those to mock it later
1261 from pylons import tmpl_context as c
1262 from pylons import tmpl_context as c
1262
1263
1263 _use_gravatar = c.visual.use_gravatar
1264 _use_gravatar = c.visual.use_gravatar
1264 _gravatar_url = c.visual.gravatar_url
1265 _gravatar_url = c.visual.gravatar_url
1265
1266
1266 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1267 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1267
1268
1268 email_address = email_address or User.DEFAULT_USER_EMAIL
1269 email_address = email_address or User.DEFAULT_USER_EMAIL
1269 if isinstance(email_address, unicode):
1270 if isinstance(email_address, unicode):
1270 # hashlib crashes on unicode items
1271 # hashlib crashes on unicode items
1271 email_address = safe_str(email_address)
1272 email_address = safe_str(email_address)
1272
1273
1273 # empty email or default user
1274 # empty email or default user
1274 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1275 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1275 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1276 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1276
1277
1277 if _use_gravatar:
1278 if _use_gravatar:
1278 # TODO: Disuse pyramid thread locals. Think about another solution to
1279 # TODO: Disuse pyramid thread locals. Think about another solution to
1279 # get the host and schema here.
1280 # get the host and schema here.
1280 request = get_current_request()
1281 request = get_current_request()
1281 tmpl = safe_str(_gravatar_url)
1282 tmpl = safe_str(_gravatar_url)
1282 tmpl = tmpl.replace('{email}', email_address)\
1283 tmpl = tmpl.replace('{email}', email_address)\
1283 .replace('{md5email}', md5_safe(email_address.lower())) \
1284 .replace('{md5email}', md5_safe(email_address.lower())) \
1284 .replace('{netloc}', request.host)\
1285 .replace('{netloc}', request.host)\
1285 .replace('{scheme}', request.scheme)\
1286 .replace('{scheme}', request.scheme)\
1286 .replace('{size}', safe_str(size))
1287 .replace('{size}', safe_str(size))
1287 return tmpl
1288 return tmpl
1288 else:
1289 else:
1289 return initials_gravatar(email_address, '', '', size=size)
1290 return initials_gravatar(email_address, '', '', size=size)
1290
1291
1291
1292
1292 class Page(_Page):
1293 class Page(_Page):
1293 """
1294 """
1294 Custom pager to match rendering style with paginator
1295 Custom pager to match rendering style with paginator
1295 """
1296 """
1296
1297
1297 def _get_pos(self, cur_page, max_page, items):
1298 def _get_pos(self, cur_page, max_page, items):
1298 edge = (items / 2) + 1
1299 edge = (items / 2) + 1
1299 if (cur_page <= edge):
1300 if (cur_page <= edge):
1300 radius = max(items / 2, items - cur_page)
1301 radius = max(items / 2, items - cur_page)
1301 elif (max_page - cur_page) < edge:
1302 elif (max_page - cur_page) < edge:
1302 radius = (items - 1) - (max_page - cur_page)
1303 radius = (items - 1) - (max_page - cur_page)
1303 else:
1304 else:
1304 radius = items / 2
1305 radius = items / 2
1305
1306
1306 left = max(1, (cur_page - (radius)))
1307 left = max(1, (cur_page - (radius)))
1307 right = min(max_page, cur_page + (radius))
1308 right = min(max_page, cur_page + (radius))
1308 return left, cur_page, right
1309 return left, cur_page, right
1309
1310
1310 def _range(self, regexp_match):
1311 def _range(self, regexp_match):
1311 """
1312 """
1312 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1313 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1313
1314
1314 Arguments:
1315 Arguments:
1315
1316
1316 regexp_match
1317 regexp_match
1317 A "re" (regular expressions) match object containing the
1318 A "re" (regular expressions) match object containing the
1318 radius of linked pages around the current page in
1319 radius of linked pages around the current page in
1319 regexp_match.group(1) as a string
1320 regexp_match.group(1) as a string
1320
1321
1321 This function is supposed to be called as a callable in
1322 This function is supposed to be called as a callable in
1322 re.sub.
1323 re.sub.
1323
1324
1324 """
1325 """
1325 radius = int(regexp_match.group(1))
1326 radius = int(regexp_match.group(1))
1326
1327
1327 # Compute the first and last page number within the radius
1328 # Compute the first and last page number within the radius
1328 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1329 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1329 # -> leftmost_page = 5
1330 # -> leftmost_page = 5
1330 # -> rightmost_page = 9
1331 # -> rightmost_page = 9
1331 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1332 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1332 self.last_page,
1333 self.last_page,
1333 (radius * 2) + 1)
1334 (radius * 2) + 1)
1334 nav_items = []
1335 nav_items = []
1335
1336
1336 # Create a link to the first page (unless we are on the first page
1337 # Create a link to the first page (unless we are on the first page
1337 # or there would be no need to insert '..' spacers)
1338 # or there would be no need to insert '..' spacers)
1338 if self.page != self.first_page and self.first_page < leftmost_page:
1339 if self.page != self.first_page and self.first_page < leftmost_page:
1339 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1340 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1340
1341
1341 # Insert dots if there are pages between the first page
1342 # Insert dots if there are pages between the first page
1342 # and the currently displayed page range
1343 # and the currently displayed page range
1343 if leftmost_page - self.first_page > 1:
1344 if leftmost_page - self.first_page > 1:
1344 # Wrap in a SPAN tag if nolink_attr is set
1345 # Wrap in a SPAN tag if nolink_attr is set
1345 text = '..'
1346 text = '..'
1346 if self.dotdot_attr:
1347 if self.dotdot_attr:
1347 text = HTML.span(c=text, **self.dotdot_attr)
1348 text = HTML.span(c=text, **self.dotdot_attr)
1348 nav_items.append(text)
1349 nav_items.append(text)
1349
1350
1350 for thispage in xrange(leftmost_page, rightmost_page + 1):
1351 for thispage in xrange(leftmost_page, rightmost_page + 1):
1351 # Hilight the current page number and do not use a link
1352 # Hilight the current page number and do not use a link
1352 if thispage == self.page:
1353 if thispage == self.page:
1353 text = '%s' % (thispage,)
1354 text = '%s' % (thispage,)
1354 # Wrap in a SPAN tag if nolink_attr is set
1355 # Wrap in a SPAN tag if nolink_attr is set
1355 if self.curpage_attr:
1356 if self.curpage_attr:
1356 text = HTML.span(c=text, **self.curpage_attr)
1357 text = HTML.span(c=text, **self.curpage_attr)
1357 nav_items.append(text)
1358 nav_items.append(text)
1358 # Otherwise create just a link to that page
1359 # Otherwise create just a link to that page
1359 else:
1360 else:
1360 text = '%s' % (thispage,)
1361 text = '%s' % (thispage,)
1361 nav_items.append(self._pagerlink(thispage, text))
1362 nav_items.append(self._pagerlink(thispage, text))
1362
1363
1363 # Insert dots if there are pages between the displayed
1364 # Insert dots if there are pages between the displayed
1364 # page numbers and the end of the page range
1365 # page numbers and the end of the page range
1365 if self.last_page - rightmost_page > 1:
1366 if self.last_page - rightmost_page > 1:
1366 text = '..'
1367 text = '..'
1367 # Wrap in a SPAN tag if nolink_attr is set
1368 # Wrap in a SPAN tag if nolink_attr is set
1368 if self.dotdot_attr:
1369 if self.dotdot_attr:
1369 text = HTML.span(c=text, **self.dotdot_attr)
1370 text = HTML.span(c=text, **self.dotdot_attr)
1370 nav_items.append(text)
1371 nav_items.append(text)
1371
1372
1372 # Create a link to the very last page (unless we are on the last
1373 # Create a link to the very last page (unless we are on the last
1373 # page or there would be no need to insert '..' spacers)
1374 # page or there would be no need to insert '..' spacers)
1374 if self.page != self.last_page and rightmost_page < self.last_page:
1375 if self.page != self.last_page and rightmost_page < self.last_page:
1375 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1376 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1376
1377
1377 ## prerender links
1378 ## prerender links
1378 #_page_link = url.current()
1379 #_page_link = url.current()
1379 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1380 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1380 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1381 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1381 return self.separator.join(nav_items)
1382 return self.separator.join(nav_items)
1382
1383
1383 def pager(self, format='~2~', page_param='page', partial_param='partial',
1384 def pager(self, format='~2~', page_param='page', partial_param='partial',
1384 show_if_single_page=False, separator=' ', onclick=None,
1385 show_if_single_page=False, separator=' ', onclick=None,
1385 symbol_first='<<', symbol_last='>>',
1386 symbol_first='<<', symbol_last='>>',
1386 symbol_previous='<', symbol_next='>',
1387 symbol_previous='<', symbol_next='>',
1387 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1388 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1388 curpage_attr={'class': 'pager_curpage'},
1389 curpage_attr={'class': 'pager_curpage'},
1389 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1390 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1390
1391
1391 self.curpage_attr = curpage_attr
1392 self.curpage_attr = curpage_attr
1392 self.separator = separator
1393 self.separator = separator
1393 self.pager_kwargs = kwargs
1394 self.pager_kwargs = kwargs
1394 self.page_param = page_param
1395 self.page_param = page_param
1395 self.partial_param = partial_param
1396 self.partial_param = partial_param
1396 self.onclick = onclick
1397 self.onclick = onclick
1397 self.link_attr = link_attr
1398 self.link_attr = link_attr
1398 self.dotdot_attr = dotdot_attr
1399 self.dotdot_attr = dotdot_attr
1399
1400
1400 # Don't show navigator if there is no more than one page
1401 # Don't show navigator if there is no more than one page
1401 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1402 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1402 return ''
1403 return ''
1403
1404
1404 from string import Template
1405 from string import Template
1405 # Replace ~...~ in token format by range of pages
1406 # Replace ~...~ in token format by range of pages
1406 result = re.sub(r'~(\d+)~', self._range, format)
1407 result = re.sub(r'~(\d+)~', self._range, format)
1407
1408
1408 # Interpolate '%' variables
1409 # Interpolate '%' variables
1409 result = Template(result).safe_substitute({
1410 result = Template(result).safe_substitute({
1410 'first_page': self.first_page,
1411 'first_page': self.first_page,
1411 'last_page': self.last_page,
1412 'last_page': self.last_page,
1412 'page': self.page,
1413 'page': self.page,
1413 'page_count': self.page_count,
1414 'page_count': self.page_count,
1414 'items_per_page': self.items_per_page,
1415 'items_per_page': self.items_per_page,
1415 'first_item': self.first_item,
1416 'first_item': self.first_item,
1416 'last_item': self.last_item,
1417 'last_item': self.last_item,
1417 'item_count': self.item_count,
1418 'item_count': self.item_count,
1418 'link_first': self.page > self.first_page and \
1419 'link_first': self.page > self.first_page and \
1419 self._pagerlink(self.first_page, symbol_first) or '',
1420 self._pagerlink(self.first_page, symbol_first) or '',
1420 'link_last': self.page < self.last_page and \
1421 'link_last': self.page < self.last_page and \
1421 self._pagerlink(self.last_page, symbol_last) or '',
1422 self._pagerlink(self.last_page, symbol_last) or '',
1422 'link_previous': self.previous_page and \
1423 'link_previous': self.previous_page and \
1423 self._pagerlink(self.previous_page, symbol_previous) \
1424 self._pagerlink(self.previous_page, symbol_previous) \
1424 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1425 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1425 'link_next': self.next_page and \
1426 'link_next': self.next_page and \
1426 self._pagerlink(self.next_page, symbol_next) \
1427 self._pagerlink(self.next_page, symbol_next) \
1427 or HTML.span(symbol_next, class_="pg-next disabled")
1428 or HTML.span(symbol_next, class_="pg-next disabled")
1428 })
1429 })
1429
1430
1430 return literal(result)
1431 return literal(result)
1431
1432
1432
1433
1433 #==============================================================================
1434 #==============================================================================
1434 # REPO PAGER, PAGER FOR REPOSITORY
1435 # REPO PAGER, PAGER FOR REPOSITORY
1435 #==============================================================================
1436 #==============================================================================
1436 class RepoPage(Page):
1437 class RepoPage(Page):
1437
1438
1438 def __init__(self, collection, page=1, items_per_page=20,
1439 def __init__(self, collection, page=1, items_per_page=20,
1439 item_count=None, url=None, **kwargs):
1440 item_count=None, url=None, **kwargs):
1440
1441
1441 """Create a "RepoPage" instance. special pager for paging
1442 """Create a "RepoPage" instance. special pager for paging
1442 repository
1443 repository
1443 """
1444 """
1444 self._url_generator = url
1445 self._url_generator = url
1445
1446
1446 # Safe the kwargs class-wide so they can be used in the pager() method
1447 # Safe the kwargs class-wide so they can be used in the pager() method
1447 self.kwargs = kwargs
1448 self.kwargs = kwargs
1448
1449
1449 # Save a reference to the collection
1450 # Save a reference to the collection
1450 self.original_collection = collection
1451 self.original_collection = collection
1451
1452
1452 self.collection = collection
1453 self.collection = collection
1453
1454
1454 # The self.page is the number of the current page.
1455 # The self.page is the number of the current page.
1455 # The first page has the number 1!
1456 # The first page has the number 1!
1456 try:
1457 try:
1457 self.page = int(page) # make it int() if we get it as a string
1458 self.page = int(page) # make it int() if we get it as a string
1458 except (ValueError, TypeError):
1459 except (ValueError, TypeError):
1459 self.page = 1
1460 self.page = 1
1460
1461
1461 self.items_per_page = items_per_page
1462 self.items_per_page = items_per_page
1462
1463
1463 # Unless the user tells us how many items the collections has
1464 # Unless the user tells us how many items the collections has
1464 # we calculate that ourselves.
1465 # we calculate that ourselves.
1465 if item_count is not None:
1466 if item_count is not None:
1466 self.item_count = item_count
1467 self.item_count = item_count
1467 else:
1468 else:
1468 self.item_count = len(self.collection)
1469 self.item_count = len(self.collection)
1469
1470
1470 # Compute the number of the first and last available page
1471 # Compute the number of the first and last available page
1471 if self.item_count > 0:
1472 if self.item_count > 0:
1472 self.first_page = 1
1473 self.first_page = 1
1473 self.page_count = int(math.ceil(float(self.item_count) /
1474 self.page_count = int(math.ceil(float(self.item_count) /
1474 self.items_per_page))
1475 self.items_per_page))
1475 self.last_page = self.first_page + self.page_count - 1
1476 self.last_page = self.first_page + self.page_count - 1
1476
1477
1477 # Make sure that the requested page number is the range of
1478 # Make sure that the requested page number is the range of
1478 # valid pages
1479 # valid pages
1479 if self.page > self.last_page:
1480 if self.page > self.last_page:
1480 self.page = self.last_page
1481 self.page = self.last_page
1481 elif self.page < self.first_page:
1482 elif self.page < self.first_page:
1482 self.page = self.first_page
1483 self.page = self.first_page
1483
1484
1484 # Note: the number of items on this page can be less than
1485 # Note: the number of items on this page can be less than
1485 # items_per_page if the last page is not full
1486 # items_per_page if the last page is not full
1486 self.first_item = max(0, (self.item_count) - (self.page *
1487 self.first_item = max(0, (self.item_count) - (self.page *
1487 items_per_page))
1488 items_per_page))
1488 self.last_item = ((self.item_count - 1) - items_per_page *
1489 self.last_item = ((self.item_count - 1) - items_per_page *
1489 (self.page - 1))
1490 (self.page - 1))
1490
1491
1491 self.items = list(self.collection[self.first_item:self.last_item + 1])
1492 self.items = list(self.collection[self.first_item:self.last_item + 1])
1492
1493
1493 # Links to previous and next page
1494 # Links to previous and next page
1494 if self.page > self.first_page:
1495 if self.page > self.first_page:
1495 self.previous_page = self.page - 1
1496 self.previous_page = self.page - 1
1496 else:
1497 else:
1497 self.previous_page = None
1498 self.previous_page = None
1498
1499
1499 if self.page < self.last_page:
1500 if self.page < self.last_page:
1500 self.next_page = self.page + 1
1501 self.next_page = self.page + 1
1501 else:
1502 else:
1502 self.next_page = None
1503 self.next_page = None
1503
1504
1504 # No items available
1505 # No items available
1505 else:
1506 else:
1506 self.first_page = None
1507 self.first_page = None
1507 self.page_count = 0
1508 self.page_count = 0
1508 self.last_page = None
1509 self.last_page = None
1509 self.first_item = None
1510 self.first_item = None
1510 self.last_item = None
1511 self.last_item = None
1511 self.previous_page = None
1512 self.previous_page = None
1512 self.next_page = None
1513 self.next_page = None
1513 self.items = []
1514 self.items = []
1514
1515
1515 # This is a subclass of the 'list' type. Initialise the list now.
1516 # This is a subclass of the 'list' type. Initialise the list now.
1516 list.__init__(self, reversed(self.items))
1517 list.__init__(self, reversed(self.items))
1517
1518
1518
1519
1519 def changed_tooltip(nodes):
1520 def changed_tooltip(nodes):
1520 """
1521 """
1521 Generates a html string for changed nodes in commit page.
1522 Generates a html string for changed nodes in commit page.
1522 It limits the output to 30 entries
1523 It limits the output to 30 entries
1523
1524
1524 :param nodes: LazyNodesGenerator
1525 :param nodes: LazyNodesGenerator
1525 """
1526 """
1526 if nodes:
1527 if nodes:
1527 pref = ': <br/> '
1528 pref = ': <br/> '
1528 suf = ''
1529 suf = ''
1529 if len(nodes) > 30:
1530 if len(nodes) > 30:
1530 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1531 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1531 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1532 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1532 for x in nodes[:30]]) + suf)
1533 for x in nodes[:30]]) + suf)
1533 else:
1534 else:
1534 return ': ' + _('No Files')
1535 return ': ' + _('No Files')
1535
1536
1536
1537
1537 def breadcrumb_repo_link(repo):
1538 def breadcrumb_repo_link(repo):
1538 """
1539 """
1539 Makes a breadcrumbs path link to repo
1540 Makes a breadcrumbs path link to repo
1540
1541
1541 ex::
1542 ex::
1542 group >> subgroup >> repo
1543 group >> subgroup >> repo
1543
1544
1544 :param repo: a Repository instance
1545 :param repo: a Repository instance
1545 """
1546 """
1546
1547
1547 path = [
1548 path = [
1548 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1549 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1549 for group in repo.groups_with_parents
1550 for group in repo.groups_with_parents
1550 ] + [
1551 ] + [
1551 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1552 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1552 ]
1553 ]
1553
1554
1554 return literal(' &raquo; '.join(path))
1555 return literal(' &raquo; '.join(path))
1555
1556
1556
1557
1557 def format_byte_size_binary(file_size):
1558 def format_byte_size_binary(file_size):
1558 """
1559 """
1559 Formats file/folder sizes to standard.
1560 Formats file/folder sizes to standard.
1560 """
1561 """
1561 formatted_size = format_byte_size(file_size, binary=True)
1562 formatted_size = format_byte_size(file_size, binary=True)
1562 return formatted_size
1563 return formatted_size
1563
1564
1564
1565
1565 def urlify_text(text_, safe=True):
1566 def urlify_text(text_, safe=True):
1566 """
1567 """
1567 Extrac urls from text and make html links out of them
1568 Extrac urls from text and make html links out of them
1568
1569
1569 :param text_:
1570 :param text_:
1570 """
1571 """
1571
1572
1572 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1573 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1573 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1574 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1574
1575
1575 def url_func(match_obj):
1576 def url_func(match_obj):
1576 url_full = match_obj.groups()[0]
1577 url_full = match_obj.groups()[0]
1577 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1578 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1578 _newtext = url_pat.sub(url_func, text_)
1579 _newtext = url_pat.sub(url_func, text_)
1579 if safe:
1580 if safe:
1580 return literal(_newtext)
1581 return literal(_newtext)
1581 return _newtext
1582 return _newtext
1582
1583
1583
1584
1584 def urlify_commits(text_, repository):
1585 def urlify_commits(text_, repository):
1585 """
1586 """
1586 Extract commit ids from text and make link from them
1587 Extract commit ids from text and make link from them
1587
1588
1588 :param text_:
1589 :param text_:
1589 :param repository: repo name to build the URL with
1590 :param repository: repo name to build the URL with
1590 """
1591 """
1591 from pylons import url # doh, we need to re-import url to mock it later
1592 from pylons import url # doh, we need to re-import url to mock it later
1592 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1593 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1593
1594
1594 def url_func(match_obj):
1595 def url_func(match_obj):
1595 commit_id = match_obj.groups()[1]
1596 commit_id = match_obj.groups()[1]
1596 pref = match_obj.groups()[0]
1597 pref = match_obj.groups()[0]
1597 suf = match_obj.groups()[2]
1598 suf = match_obj.groups()[2]
1598
1599
1599 tmpl = (
1600 tmpl = (
1600 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1601 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1601 '%(commit_id)s</a>%(suf)s'
1602 '%(commit_id)s</a>%(suf)s'
1602 )
1603 )
1603 return tmpl % {
1604 return tmpl % {
1604 'pref': pref,
1605 'pref': pref,
1605 'cls': 'revision-link',
1606 'cls': 'revision-link',
1606 'url': url('changeset_home', repo_name=repository,
1607 'url': url('changeset_home', repo_name=repository,
1607 revision=commit_id, qualified=True),
1608 revision=commit_id, qualified=True),
1608 'commit_id': commit_id,
1609 'commit_id': commit_id,
1609 'suf': suf
1610 'suf': suf
1610 }
1611 }
1611
1612
1612 newtext = URL_PAT.sub(url_func, text_)
1613 newtext = URL_PAT.sub(url_func, text_)
1613
1614
1614 return newtext
1615 return newtext
1615
1616
1616
1617
1617 def _process_url_func(match_obj, repo_name, uid, entry,
1618 def _process_url_func(match_obj, repo_name, uid, entry,
1618 return_raw_data=False, link_format='html'):
1619 return_raw_data=False, link_format='html'):
1619 pref = ''
1620 pref = ''
1620 if match_obj.group().startswith(' '):
1621 if match_obj.group().startswith(' '):
1621 pref = ' '
1622 pref = ' '
1622
1623
1623 issue_id = ''.join(match_obj.groups())
1624 issue_id = ''.join(match_obj.groups())
1624
1625
1625 if link_format == 'html':
1626 if link_format == 'html':
1626 tmpl = (
1627 tmpl = (
1627 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1628 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1628 '%(issue-prefix)s%(id-repr)s'
1629 '%(issue-prefix)s%(id-repr)s'
1629 '</a>')
1630 '</a>')
1630 elif link_format == 'rst':
1631 elif link_format == 'rst':
1631 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1632 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1632 elif link_format == 'markdown':
1633 elif link_format == 'markdown':
1633 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1634 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1634 else:
1635 else:
1635 raise ValueError('Bad link_format:{}'.format(link_format))
1636 raise ValueError('Bad link_format:{}'.format(link_format))
1636
1637
1637 (repo_name_cleaned,
1638 (repo_name_cleaned,
1638 parent_group_name) = RepoGroupModel().\
1639 parent_group_name) = RepoGroupModel().\
1639 _get_group_name_and_parent(repo_name)
1640 _get_group_name_and_parent(repo_name)
1640
1641
1641 # variables replacement
1642 # variables replacement
1642 named_vars = {
1643 named_vars = {
1643 'id': issue_id,
1644 'id': issue_id,
1644 'repo': repo_name,
1645 'repo': repo_name,
1645 'repo_name': repo_name_cleaned,
1646 'repo_name': repo_name_cleaned,
1646 'group_name': parent_group_name
1647 'group_name': parent_group_name
1647 }
1648 }
1648 # named regex variables
1649 # named regex variables
1649 named_vars.update(match_obj.groupdict())
1650 named_vars.update(match_obj.groupdict())
1650 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1651 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1651
1652
1652 data = {
1653 data = {
1653 'pref': pref,
1654 'pref': pref,
1654 'cls': 'issue-tracker-link',
1655 'cls': 'issue-tracker-link',
1655 'url': _url,
1656 'url': _url,
1656 'id-repr': issue_id,
1657 'id-repr': issue_id,
1657 'issue-prefix': entry['pref'],
1658 'issue-prefix': entry['pref'],
1658 'serv': entry['url'],
1659 'serv': entry['url'],
1659 }
1660 }
1660 if return_raw_data:
1661 if return_raw_data:
1661 return {
1662 return {
1662 'id': issue_id,
1663 'id': issue_id,
1663 'url': _url
1664 'url': _url
1664 }
1665 }
1665 return tmpl % data
1666 return tmpl % data
1666
1667
1667
1668
1668 def process_patterns(text_string, repo_name, link_format='html'):
1669 def process_patterns(text_string, repo_name, link_format='html'):
1669 allowed_formats = ['html', 'rst', 'markdown']
1670 allowed_formats = ['html', 'rst', 'markdown']
1670 if link_format not in allowed_formats:
1671 if link_format not in allowed_formats:
1671 raise ValueError('Link format can be only one of:{} got {}'.format(
1672 raise ValueError('Link format can be only one of:{} got {}'.format(
1672 allowed_formats, link_format))
1673 allowed_formats, link_format))
1673
1674
1674 repo = None
1675 repo = None
1675 if repo_name:
1676 if repo_name:
1676 # Retrieving repo_name to avoid invalid repo_name to explode on
1677 # Retrieving repo_name to avoid invalid repo_name to explode on
1677 # IssueTrackerSettingsModel but still passing invalid name further down
1678 # IssueTrackerSettingsModel but still passing invalid name further down
1678 repo = Repository.get_by_repo_name(repo_name, cache=True)
1679 repo = Repository.get_by_repo_name(repo_name, cache=True)
1679
1680
1680 settings_model = IssueTrackerSettingsModel(repo=repo)
1681 settings_model = IssueTrackerSettingsModel(repo=repo)
1681 active_entries = settings_model.get_settings(cache=True)
1682 active_entries = settings_model.get_settings(cache=True)
1682
1683
1683 issues_data = []
1684 issues_data = []
1684 newtext = text_string
1685 newtext = text_string
1685
1686
1686 for uid, entry in active_entries.items():
1687 for uid, entry in active_entries.items():
1687 log.debug('found issue tracker entry with uid %s' % (uid,))
1688 log.debug('found issue tracker entry with uid %s' % (uid,))
1688
1689
1689 if not (entry['pat'] and entry['url']):
1690 if not (entry['pat'] and entry['url']):
1690 log.debug('skipping due to missing data')
1691 log.debug('skipping due to missing data')
1691 continue
1692 continue
1692
1693
1693 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1694 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1694 % (uid, entry['pat'], entry['url'], entry['pref']))
1695 % (uid, entry['pat'], entry['url'], entry['pref']))
1695
1696
1696 try:
1697 try:
1697 pattern = re.compile(r'%s' % entry['pat'])
1698 pattern = re.compile(r'%s' % entry['pat'])
1698 except re.error:
1699 except re.error:
1699 log.exception(
1700 log.exception(
1700 'issue tracker pattern: `%s` failed to compile',
1701 'issue tracker pattern: `%s` failed to compile',
1701 entry['pat'])
1702 entry['pat'])
1702 continue
1703 continue
1703
1704
1704 data_func = partial(
1705 data_func = partial(
1705 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1706 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1706 return_raw_data=True)
1707 return_raw_data=True)
1707
1708
1708 for match_obj in pattern.finditer(text_string):
1709 for match_obj in pattern.finditer(text_string):
1709 issues_data.append(data_func(match_obj))
1710 issues_data.append(data_func(match_obj))
1710
1711
1711 url_func = partial(
1712 url_func = partial(
1712 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1713 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1713 link_format=link_format)
1714 link_format=link_format)
1714
1715
1715 newtext = pattern.sub(url_func, newtext)
1716 newtext = pattern.sub(url_func, newtext)
1716 log.debug('processed prefix:uid `%s`' % (uid,))
1717 log.debug('processed prefix:uid `%s`' % (uid,))
1717
1718
1718 return newtext, issues_data
1719 return newtext, issues_data
1719
1720
1720
1721
1721 def urlify_commit_message(commit_text, repository=None):
1722 def urlify_commit_message(commit_text, repository=None):
1722 """
1723 """
1723 Parses given text message and makes proper links.
1724 Parses given text message and makes proper links.
1724 issues are linked to given issue-server, and rest is a commit link
1725 issues are linked to given issue-server, and rest is a commit link
1725
1726
1726 :param commit_text:
1727 :param commit_text:
1727 :param repository:
1728 :param repository:
1728 """
1729 """
1729 from pylons import url # doh, we need to re-import url to mock it later
1730 from pylons import url # doh, we need to re-import url to mock it later
1730
1731
1731 def escaper(string):
1732 def escaper(string):
1732 return string.replace('<', '&lt;').replace('>', '&gt;')
1733 return string.replace('<', '&lt;').replace('>', '&gt;')
1733
1734
1734 newtext = escaper(commit_text)
1735 newtext = escaper(commit_text)
1735
1736
1736 # extract http/https links and make them real urls
1737 # extract http/https links and make them real urls
1737 newtext = urlify_text(newtext, safe=False)
1738 newtext = urlify_text(newtext, safe=False)
1738
1739
1739 # urlify commits - extract commit ids and make link out of them, if we have
1740 # urlify commits - extract commit ids and make link out of them, if we have
1740 # the scope of repository present.
1741 # the scope of repository present.
1741 if repository:
1742 if repository:
1742 newtext = urlify_commits(newtext, repository)
1743 newtext = urlify_commits(newtext, repository)
1743
1744
1744 # process issue tracker patterns
1745 # process issue tracker patterns
1745 newtext, issues = process_patterns(newtext, repository or '')
1746 newtext, issues = process_patterns(newtext, repository or '')
1746
1747
1747 return literal(newtext)
1748 return literal(newtext)
1748
1749
1749
1750
1750 def render_binary(repo_name, file_obj):
1751 def render_binary(repo_name, file_obj):
1751 """
1752 """
1752 Choose how to render a binary file
1753 Choose how to render a binary file
1753 """
1754 """
1754 filename = file_obj.name
1755 filename = file_obj.name
1755
1756
1756 # images
1757 # images
1757 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1758 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1758 if fnmatch.fnmatch(filename, pat=ext):
1759 if fnmatch.fnmatch(filename, pat=ext):
1759 alt = filename
1760 alt = filename
1760 src = url('files_raw_home', repo_name=repo_name,
1761 src = url('files_raw_home', repo_name=repo_name,
1761 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1762 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1762 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1763 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1763
1764
1764
1765
1765 def renderer_from_filename(filename, exclude=None):
1766 def renderer_from_filename(filename, exclude=None):
1766 """
1767 """
1767 choose a renderer based on filename, this works only for text based files
1768 choose a renderer based on filename, this works only for text based files
1768 """
1769 """
1769
1770
1770 # ipython
1771 # ipython
1771 for ext in ['*.ipynb']:
1772 for ext in ['*.ipynb']:
1772 if fnmatch.fnmatch(filename, pat=ext):
1773 if fnmatch.fnmatch(filename, pat=ext):
1773 return 'jupyter'
1774 return 'jupyter'
1774
1775
1775 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1776 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1776 if is_markup:
1777 if is_markup:
1777 return is_markup
1778 return is_markup
1778 return None
1779 return None
1779
1780
1780
1781
1781 def render(source, renderer='rst', mentions=False, relative_url=None,
1782 def render(source, renderer='rst', mentions=False, relative_url=None,
1782 repo_name=None):
1783 repo_name=None):
1783
1784
1784 def maybe_convert_relative_links(html_source):
1785 def maybe_convert_relative_links(html_source):
1785 if relative_url:
1786 if relative_url:
1786 return relative_links(html_source, relative_url)
1787 return relative_links(html_source, relative_url)
1787 return html_source
1788 return html_source
1788
1789
1789 if renderer == 'rst':
1790 if renderer == 'rst':
1790 if repo_name:
1791 if repo_name:
1791 # process patterns on comments if we pass in repo name
1792 # process patterns on comments if we pass in repo name
1792 source, issues = process_patterns(
1793 source, issues = process_patterns(
1793 source, repo_name, link_format='rst')
1794 source, repo_name, link_format='rst')
1794
1795
1795 return literal(
1796 return literal(
1796 '<div class="rst-block">%s</div>' %
1797 '<div class="rst-block">%s</div>' %
1797 maybe_convert_relative_links(
1798 maybe_convert_relative_links(
1798 MarkupRenderer.rst(source, mentions=mentions)))
1799 MarkupRenderer.rst(source, mentions=mentions)))
1799 elif renderer == 'markdown':
1800 elif renderer == 'markdown':
1800 if repo_name:
1801 if repo_name:
1801 # process patterns on comments if we pass in repo name
1802 # process patterns on comments if we pass in repo name
1802 source, issues = process_patterns(
1803 source, issues = process_patterns(
1803 source, repo_name, link_format='markdown')
1804 source, repo_name, link_format='markdown')
1804
1805
1805 return literal(
1806 return literal(
1806 '<div class="markdown-block">%s</div>' %
1807 '<div class="markdown-block">%s</div>' %
1807 maybe_convert_relative_links(
1808 maybe_convert_relative_links(
1808 MarkupRenderer.markdown(source, flavored=True,
1809 MarkupRenderer.markdown(source, flavored=True,
1809 mentions=mentions)))
1810 mentions=mentions)))
1810 elif renderer == 'jupyter':
1811 elif renderer == 'jupyter':
1811 return literal(
1812 return literal(
1812 '<div class="ipynb">%s</div>' %
1813 '<div class="ipynb">%s</div>' %
1813 maybe_convert_relative_links(
1814 maybe_convert_relative_links(
1814 MarkupRenderer.jupyter(source)))
1815 MarkupRenderer.jupyter(source)))
1815
1816
1816 # None means just show the file-source
1817 # None means just show the file-source
1817 return None
1818 return None
1818
1819
1819
1820
1820 def commit_status(repo, commit_id):
1821 def commit_status(repo, commit_id):
1821 return ChangesetStatusModel().get_status(repo, commit_id)
1822 return ChangesetStatusModel().get_status(repo, commit_id)
1822
1823
1823
1824
1824 def commit_status_lbl(commit_status):
1825 def commit_status_lbl(commit_status):
1825 return dict(ChangesetStatus.STATUSES).get(commit_status)
1826 return dict(ChangesetStatus.STATUSES).get(commit_status)
1826
1827
1827
1828
1828 def commit_time(repo_name, commit_id):
1829 def commit_time(repo_name, commit_id):
1829 repo = Repository.get_by_repo_name(repo_name)
1830 repo = Repository.get_by_repo_name(repo_name)
1830 commit = repo.get_commit(commit_id=commit_id)
1831 commit = repo.get_commit(commit_id=commit_id)
1831 return commit.date
1832 return commit.date
1832
1833
1833
1834
1834 def get_permission_name(key):
1835 def get_permission_name(key):
1835 return dict(Permission.PERMS).get(key)
1836 return dict(Permission.PERMS).get(key)
1836
1837
1837
1838
1838 def journal_filter_help():
1839 def journal_filter_help():
1839 return _(
1840 return _(
1840 'Example filter terms:\n' +
1841 'Example filter terms:\n' +
1841 ' repository:vcs\n' +
1842 ' repository:vcs\n' +
1842 ' username:marcin\n' +
1843 ' username:marcin\n' +
1843 ' username:(NOT marcin)\n' +
1844 ' username:(NOT marcin)\n' +
1844 ' action:*push*\n' +
1845 ' action:*push*\n' +
1845 ' ip:127.0.0.1\n' +
1846 ' ip:127.0.0.1\n' +
1846 ' date:20120101\n' +
1847 ' date:20120101\n' +
1847 ' date:[20120101100000 TO 20120102]\n' +
1848 ' date:[20120101100000 TO 20120102]\n' +
1848 '\n' +
1849 '\n' +
1849 'Generate wildcards using \'*\' character:\n' +
1850 'Generate wildcards using \'*\' character:\n' +
1850 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1851 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1851 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1852 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1852 '\n' +
1853 '\n' +
1853 'Optional AND / OR operators in queries\n' +
1854 'Optional AND / OR operators in queries\n' +
1854 ' "repository:vcs OR repository:test"\n' +
1855 ' "repository:vcs OR repository:test"\n' +
1855 ' "username:test AND repository:test*"\n'
1856 ' "username:test AND repository:test*"\n'
1856 )
1857 )
1857
1858
1858
1859
1859 def search_filter_help(searcher):
1860 def search_filter_help(searcher):
1860
1861
1861 terms = ''
1862 terms = ''
1862 return _(
1863 return _(
1863 'Example filter terms for `{searcher}` search:\n' +
1864 'Example filter terms for `{searcher}` search:\n' +
1864 '{terms}\n' +
1865 '{terms}\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1866 'Generate wildcards using \'*\' character:\n' +
1866 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1868 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1868 '\n' +
1869 '\n' +
1869 'Optional AND / OR operators in queries\n' +
1870 'Optional AND / OR operators in queries\n' +
1870 ' "repo_name:vcs OR repo_name:test"\n' +
1871 ' "repo_name:vcs OR repo_name:test"\n' +
1871 ' "owner:test AND repo_name:test*"\n' +
1872 ' "owner:test AND repo_name:test*"\n' +
1872 'More: {search_doc}'
1873 'More: {search_doc}'
1873 ).format(searcher=searcher.name,
1874 ).format(searcher=searcher.name,
1874 terms=terms, search_doc=searcher.query_lang_doc)
1875 terms=terms, search_doc=searcher.query_lang_doc)
1875
1876
1876
1877
1877 def not_mapped_error(repo_name):
1878 def not_mapped_error(repo_name):
1878 flash(_('%s repository is not mapped to db perhaps'
1879 flash(_('%s repository is not mapped to db perhaps'
1879 ' it was created or renamed from the filesystem'
1880 ' it was created or renamed from the filesystem'
1880 ' please run the application again'
1881 ' please run the application again'
1881 ' in order to rescan repositories') % repo_name, category='error')
1882 ' in order to rescan repositories') % repo_name, category='error')
1882
1883
1883
1884
1884 def ip_range(ip_addr):
1885 def ip_range(ip_addr):
1885 from rhodecode.model.db import UserIpMap
1886 from rhodecode.model.db import UserIpMap
1886 s, e = UserIpMap._get_ip_range(ip_addr)
1887 s, e = UserIpMap._get_ip_range(ip_addr)
1887 return '%s - %s' % (s, e)
1888 return '%s - %s' % (s, e)
1888
1889
1889
1890
1890 def form(url, method='post', needs_csrf_token=True, **attrs):
1891 def form(url, method='post', needs_csrf_token=True, **attrs):
1891 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1892 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1892 if method.lower() != 'get' and needs_csrf_token:
1893 if method.lower() != 'get' and needs_csrf_token:
1893 raise Exception(
1894 raise Exception(
1894 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1895 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1895 'CSRF token. If the endpoint does not require such token you can ' +
1896 'CSRF token. If the endpoint does not require such token you can ' +
1896 'explicitly set the parameter needs_csrf_token to false.')
1897 'explicitly set the parameter needs_csrf_token to false.')
1897
1898
1898 return wh_form(url, method=method, **attrs)
1899 return wh_form(url, method=method, **attrs)
1899
1900
1900
1901
1901 def secure_form(url, method="POST", multipart=False, **attrs):
1902 def secure_form(url, method="POST", multipart=False, **attrs):
1902 """Start a form tag that points the action to an url. This
1903 """Start a form tag that points the action to an url. This
1903 form tag will also include the hidden field containing
1904 form tag will also include the hidden field containing
1904 the auth token.
1905 the auth token.
1905
1906
1906 The url options should be given either as a string, or as a
1907 The url options should be given either as a string, or as a
1907 ``url()`` function. The method for the form defaults to POST.
1908 ``url()`` function. The method for the form defaults to POST.
1908
1909
1909 Options:
1910 Options:
1910
1911
1911 ``multipart``
1912 ``multipart``
1912 If set to True, the enctype is set to "multipart/form-data".
1913 If set to True, the enctype is set to "multipart/form-data".
1913 ``method``
1914 ``method``
1914 The method to use when submitting the form, usually either
1915 The method to use when submitting the form, usually either
1915 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1916 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1916 hidden input with name _method is added to simulate the verb
1917 hidden input with name _method is added to simulate the verb
1917 over POST.
1918 over POST.
1918
1919
1919 """
1920 """
1920 from webhelpers.pylonslib.secure_form import insecure_form
1921 from webhelpers.pylonslib.secure_form import insecure_form
1921 form = insecure_form(url, method, multipart, **attrs)
1922 form = insecure_form(url, method, multipart, **attrs)
1922 token = csrf_input()
1923 token = csrf_input()
1923 return literal("%s\n%s" % (form, token))
1924 return literal("%s\n%s" % (form, token))
1924
1925
1925 def csrf_input():
1926 def csrf_input():
1926 return literal(
1927 return literal(
1927 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1928 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1928 csrf_token_key, csrf_token_key, get_csrf_token()))
1929 csrf_token_key, csrf_token_key, get_csrf_token()))
1929
1930
1930 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1931 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1931 select_html = select(name, selected, options, **attrs)
1932 select_html = select(name, selected, options, **attrs)
1932 select2 = """
1933 select2 = """
1933 <script>
1934 <script>
1934 $(document).ready(function() {
1935 $(document).ready(function() {
1935 $('#%s').select2({
1936 $('#%s').select2({
1936 containerCssClass: 'drop-menu',
1937 containerCssClass: 'drop-menu',
1937 dropdownCssClass: 'drop-menu-dropdown',
1938 dropdownCssClass: 'drop-menu-dropdown',
1938 dropdownAutoWidth: true%s
1939 dropdownAutoWidth: true%s
1939 });
1940 });
1940 });
1941 });
1941 </script>
1942 </script>
1942 """
1943 """
1943 filter_option = """,
1944 filter_option = """,
1944 minimumResultsForSearch: -1
1945 minimumResultsForSearch: -1
1945 """
1946 """
1946 input_id = attrs.get('id') or name
1947 input_id = attrs.get('id') or name
1947 filter_enabled = "" if enable_filter else filter_option
1948 filter_enabled = "" if enable_filter else filter_option
1948 select_script = literal(select2 % (input_id, filter_enabled))
1949 select_script = literal(select2 % (input_id, filter_enabled))
1949
1950
1950 return literal(select_html+select_script)
1951 return literal(select_html+select_script)
1951
1952
1952
1953
1953 def get_visual_attr(tmpl_context_var, attr_name):
1954 def get_visual_attr(tmpl_context_var, attr_name):
1954 """
1955 """
1955 A safe way to get a variable from visual variable of template context
1956 A safe way to get a variable from visual variable of template context
1956
1957
1957 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1958 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1958 :param attr_name: name of the attribute we fetch from the c.visual
1959 :param attr_name: name of the attribute we fetch from the c.visual
1959 """
1960 """
1960 visual = getattr(tmpl_context_var, 'visual', None)
1961 visual = getattr(tmpl_context_var, 'visual', None)
1961 if not visual:
1962 if not visual:
1962 return
1963 return
1963 else:
1964 else:
1964 return getattr(visual, attr_name, None)
1965 return getattr(visual, attr_name, None)
1965
1966
1966
1967
1967 def get_last_path_part(file_node):
1968 def get_last_path_part(file_node):
1968 if not file_node.path:
1969 if not file_node.path:
1969 return u''
1970 return u''
1970
1971
1971 path = safe_unicode(file_node.path.split('/')[-1])
1972 path = safe_unicode(file_node.path.split('/')[-1])
1972 return u'../' + path
1973 return u'../' + path
1973
1974
1974
1975
1975 def route_url(*args, **kwargs):
1976 def route_url(*args, **kwargs):
1976 """
1977 """
1977 Wrapper around pyramids `route_url` (fully qualified url) function.
1978 Wrapper around pyramids `route_url` (fully qualified url) function.
1978 It is used to generate URLs from within pylons views or templates.
1979 It is used to generate URLs from within pylons views or templates.
1979 This will be removed when pyramid migration if finished.
1980 This will be removed when pyramid migration if finished.
1980 """
1981 """
1981 req = get_current_request()
1982 req = get_current_request()
1982 return req.route_url(*args, **kwargs)
1983 return req.route_url(*args, **kwargs)
1983
1984
1984
1985
1985 def route_path(*args, **kwargs):
1986 def route_path(*args, **kwargs):
1986 """
1987 """
1987 Wrapper around pyramids `route_path` function. It is used to generate
1988 Wrapper around pyramids `route_path` function. It is used to generate
1988 URLs from within pylons views or templates. This will be removed when
1989 URLs from within pylons views or templates. This will be removed when
1989 pyramid migration if finished.
1990 pyramid migration if finished.
1990 """
1991 """
1991 req = get_current_request()
1992 req = get_current_request()
1992 return req.route_path(*args, **kwargs)
1993 return req.route_path(*args, **kwargs)
1993
1994
1994
1995
1995 def route_path_or_none(*args, **kwargs):
1996 def route_path_or_none(*args, **kwargs):
1996 try:
1997 try:
1997 return route_path(*args, **kwargs)
1998 return route_path(*args, **kwargs)
1998 except KeyError:
1999 except KeyError:
1999 return None
2000 return None
2000
2001
2001
2002
2002 def static_url(*args, **kwds):
2003 def static_url(*args, **kwds):
2003 """
2004 """
2004 Wrapper around pyramids `route_path` function. It is used to generate
2005 Wrapper around pyramids `route_path` function. It is used to generate
2005 URLs from within pylons views or templates. This will be removed when
2006 URLs from within pylons views or templates. This will be removed when
2006 pyramid migration if finished.
2007 pyramid migration if finished.
2007 """
2008 """
2008 req = get_current_request()
2009 req = get_current_request()
2009 return req.static_url(*args, **kwds)
2010 return req.static_url(*args, **kwds)
2010
2011
2011
2012
2012 def resource_path(*args, **kwds):
2013 def resource_path(*args, **kwds):
2013 """
2014 """
2014 Wrapper around pyramids `route_path` function. It is used to generate
2015 Wrapper around pyramids `route_path` function. It is used to generate
2015 URLs from within pylons views or templates. This will be removed when
2016 URLs from within pylons views or templates. This will be removed when
2016 pyramid migration if finished.
2017 pyramid migration if finished.
2017 """
2018 """
2018 req = get_current_request()
2019 req = get_current_request()
2019 return req.resource_path(*args, **kwds)
2020 return req.resource_path(*args, **kwds)
2020
2021
2021
2022
2022 def api_call_example(method, args):
2023 def api_call_example(method, args):
2023 """
2024 """
2024 Generates an API call example via CURL
2025 Generates an API call example via CURL
2025 """
2026 """
2026 args_json = json.dumps(OrderedDict([
2027 args_json = json.dumps(OrderedDict([
2027 ('id', 1),
2028 ('id', 1),
2028 ('auth_token', 'SECRET'),
2029 ('auth_token', 'SECRET'),
2029 ('method', method),
2030 ('method', method),
2030 ('args', args)
2031 ('args', args)
2031 ]))
2032 ]))
2032 return literal(
2033 return literal(
2033 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2034 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2034 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2035 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2035 "and needs to be of `api calls` role."
2036 "and needs to be of `api calls` role."
2036 .format(
2037 .format(
2037 api_url=route_url('apiv2'),
2038 api_url=route_url('apiv2'),
2038 token_url=route_url('my_account_auth_tokens'),
2039 token_url=route_url('my_account_auth_tokens'),
2039 data=args_json))
2040 data=args_json))
@@ -1,330 +1,332 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 import io
20 import io
21 import re
21 import re
22 import datetime
22 import datetime
23 import logging
23 import logging
24 import pylons
24 import pylons
25 import Queue
25 import Queue
26 import subprocess32
26 import subprocess32
27 import os
27 import os
28
28
29 from pyramid.i18n import get_localizer
29 from pyramid.i18n import get_localizer
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from pyramid.interfaces import IRoutesMapper
31 from pyramid.interfaces import IRoutesMapper
32 from pyramid.settings import asbool
32 from pyramid.settings import asbool
33 from pyramid.path import AssetResolver
33 from pyramid.path import AssetResolver
34 from threading import Thread
34 from threading import Thread
35
35
36 from rhodecode.translation import _ as tsf
36 from rhodecode.translation import _ as tsf
37 from rhodecode.config.jsroutes import generate_jsroutes_content
37 from rhodecode.config.jsroutes import generate_jsroutes_content
38
38
39 import rhodecode
39 import rhodecode
40
40
41 from pylons.i18n.translation import _get_translator
41 from pylons.i18n.translation import _get_translator
42 from pylons.util import ContextObj
42 from pylons.util import ContextObj
43 from routes.util import URLGenerator
43 from routes.util import URLGenerator
44
44
45 from rhodecode.lib.base import attach_context_attributes, get_auth_user
45 from rhodecode.lib.base import attach_context_attributes, get_auth_user
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 def add_renderer_globals(event):
50 def add_renderer_globals(event):
51 from rhodecode.lib import helpers
51 from rhodecode.lib import helpers
52
52
53 # NOTE(marcink):
53 # NOTE(marcink):
54 # Put pylons stuff into the context. This will be removed as soon as
54 # Put pylons stuff into the context. This will be removed as soon as
55 # migration to pyramid is finished.
55 # migration to pyramid is finished.
56 event['c'] = pylons.tmpl_context
56 event['c'] = pylons.tmpl_context
57
57
58 # TODO: When executed in pyramid view context the request is not available
58 # TODO: When executed in pyramid view context the request is not available
59 # in the event. Find a better solution to get the request.
59 # in the event. Find a better solution to get the request.
60 request = event['request'] or get_current_request()
60 request = event['request'] or get_current_request()
61
61
62 # Add Pyramid translation as '_' to context
62 # Add Pyramid translation as '_' to context
63 event['_'] = request.translate
63 event['_'] = request.translate
64 event['_ungettext'] = request.plularize
64 event['_ungettext'] = request.plularize
65 event['h'] = helpers
65 event['h'] = helpers
66
66
67
67
68 def add_localizer(event):
68 def add_localizer(event):
69 request = event.request
69 request = event.request
70 localizer = get_localizer(request)
70 localizer = get_localizer(request)
71
71
72 def auto_translate(*args, **kwargs):
72 def auto_translate(*args, **kwargs):
73 return localizer.translate(tsf(*args, **kwargs))
73 return localizer.translate(tsf(*args, **kwargs))
74
74
75 request.localizer = localizer
75 request.localizer = localizer
76 request.translate = auto_translate
76 request.translate = auto_translate
77 request.plularize = localizer.pluralize
77 request.plularize = localizer.pluralize
78
78
79
79
80 def set_user_lang(event):
80 def set_user_lang(event):
81 request = event.request
81 request = event.request
82 cur_user = getattr(request, 'user', None)
82 cur_user = getattr(request, 'user', None)
83
83
84 if cur_user:
84 if cur_user:
85 user_lang = cur_user.get_instance().user_data.get('language')
85 user_lang = cur_user.get_instance().user_data.get('language')
86 if user_lang:
86 if user_lang:
87 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
87 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
88 event.request._LOCALE_ = user_lang
88 event.request._LOCALE_ = user_lang
89
89
90
90
91 def add_request_user_context(event):
91 def add_request_user_context(event):
92 """
92 """
93 Adds auth user into request context
93 Adds auth user into request context
94 """
94 """
95 request = event.request
95 request = event.request
96
96
97 if hasattr(request, 'vcs_call'):
97 if hasattr(request, 'vcs_call'):
98 # skip vcs calls
98 # skip vcs calls
99 return
99 return
100
100
101 if hasattr(request, 'rpc_method'):
101 if hasattr(request, 'rpc_method'):
102 # skip api calls
102 # skip api calls
103 return
103 return
104
104
105 auth_user = get_auth_user(request)
105 auth_user = get_auth_user(request)
106 request.user = auth_user
106 request.user = auth_user
107 request.environ['rc_auth_user'] = auth_user
107 request.environ['rc_auth_user'] = auth_user
108
108
109
109
110 def add_pylons_context(event):
110 def add_pylons_context(event):
111 request = event.request
111 request = event.request
112
112
113 config = rhodecode.CONFIG
113 config = rhodecode.CONFIG
114 environ = request.environ
114 environ = request.environ
115 session = request.session
115 session = request.session
116
116
117 if hasattr(request, 'vcs_call'):
117 if hasattr(request, 'vcs_call'):
118 # skip vcs calls
118 # skip vcs calls
119 return
119 return
120
120
121 # Setup pylons globals.
121 # Setup pylons globals.
122 pylons.config._push_object(config)
122 pylons.config._push_object(config)
123 pylons.request._push_object(request)
123 pylons.request._push_object(request)
124 pylons.session._push_object(session)
124 pylons.session._push_object(session)
125 pylons.translator._push_object(_get_translator(config.get('lang')))
125 pylons.translator._push_object(_get_translator(config.get('lang')))
126
126
127 pylons.url._push_object(URLGenerator(config['routes.map'], environ))
127 pylons.url._push_object(URLGenerator(config['routes.map'], environ))
128 session_key = (
128 session_key = (
129 config['pylons.environ_config'].get('session', 'beaker.session'))
129 config['pylons.environ_config'].get('session', 'beaker.session'))
130 environ[session_key] = session
130 environ[session_key] = session
131
131
132 if hasattr(request, 'rpc_method'):
132 if hasattr(request, 'rpc_method'):
133 # skip api calls
133 # skip api calls
134 return
134 return
135
135
136 # Setup the pylons context object ('c')
136 # Setup the pylons context object ('c')
137 context = ContextObj()
137 context = ContextObj()
138 context.rhodecode_user = request.user
138 context.rhodecode_user = request.user
139 attach_context_attributes(context, request, request.user.user_id)
139 attach_context_attributes(context, request, request.user.user_id)
140 pylons.tmpl_context._push_object(context)
140 pylons.tmpl_context._push_object(context)
141
141
142
142
143 def scan_repositories_if_enabled(event):
143 def scan_repositories_if_enabled(event):
144 """
144 """
145 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
145 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
146 does a repository scan if enabled in the settings.
146 does a repository scan if enabled in the settings.
147 """
147 """
148 settings = event.app.registry.settings
148 settings = event.app.registry.settings
149 vcs_server_enabled = settings['vcs.server.enable']
149 vcs_server_enabled = settings['vcs.server.enable']
150 import_on_startup = settings['startup.import_repos']
150 import_on_startup = settings['startup.import_repos']
151 if vcs_server_enabled and import_on_startup:
151 if vcs_server_enabled and import_on_startup:
152 from rhodecode.model.scm import ScmModel
152 from rhodecode.model.scm import ScmModel
153 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
153 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
154 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
154 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
155 repo2db_mapper(repositories, remove_obsolete=False)
155 repo2db_mapper(repositories, remove_obsolete=False)
156
156
157
157
158 def write_metadata_if_needed(event):
158 def write_metadata_if_needed(event):
159 """
159 """
160 Writes upgrade metadata
160 Writes upgrade metadata
161 """
161 """
162 import rhodecode
162 import rhodecode
163 from rhodecode.lib import system_info
163 from rhodecode.lib import system_info
164 from rhodecode.lib import ext_json
164 from rhodecode.lib import ext_json
165
165
166 def write():
166 def write():
167 fname = '.rcmetadata.json'
167 fname = '.rcmetadata.json'
168 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
168 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
169 metadata_destination = os.path.join(ini_loc, fname)
169 metadata_destination = os.path.join(ini_loc, fname)
170
170
171 configuration = system_info.SysInfo(
171 configuration = system_info.SysInfo(
172 system_info.rhodecode_config)()['value']
172 system_info.rhodecode_config)()['value']
173 license_token = configuration['config']['license_token']
173 license_token = configuration['config']['license_token']
174 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
174 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
175 del dbinfo['url']
175 del dbinfo['url']
176 metadata = dict(
176 metadata = dict(
177 desc='upgrade metadata info',
177 desc='upgrade metadata info',
178 license_token=license_token,
178 license_token=license_token,
179 created_on=datetime.datetime.utcnow().isoformat(),
179 created_on=datetime.datetime.utcnow().isoformat(),
180 usage=system_info.SysInfo(system_info.usage_info)()['value'],
180 usage=system_info.SysInfo(system_info.usage_info)()['value'],
181 platform=system_info.SysInfo(system_info.platform_type)()['value'],
181 platform=system_info.SysInfo(system_info.platform_type)()['value'],
182 database=dbinfo,
182 database=dbinfo,
183 cpu=system_info.SysInfo(system_info.cpu)()['value'],
183 cpu=system_info.SysInfo(system_info.cpu)()['value'],
184 memory=system_info.SysInfo(system_info.memory)()['value'],
184 memory=system_info.SysInfo(system_info.memory)()['value'],
185 )
185 )
186
186
187 with open(metadata_destination, 'wb') as f:
187 with open(metadata_destination, 'wb') as f:
188 f.write(ext_json.json.dumps(metadata))
188 f.write(ext_json.json.dumps(metadata))
189
189
190 settings = event.app.registry.settings
190 settings = event.app.registry.settings
191 if settings.get('metadata.skip'):
191 if settings.get('metadata.skip'):
192 return
192 return
193
193
194 try:
194 try:
195 write()
195 write()
196 except Exception:
196 except Exception:
197 pass
197 pass
198
198
199
199
200 def write_js_routes_if_enabled(event):
200 def write_js_routes_if_enabled(event):
201 registry = event.app.registry
201 registry = event.app.registry
202
202
203 mapper = registry.queryUtility(IRoutesMapper)
203 mapper = registry.queryUtility(IRoutesMapper)
204 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
205
205
206 def _extract_route_information(route):
206 def _extract_route_information(route):
207 """
207 """
208 Convert a route into tuple(name, path, args), eg:
208 Convert a route into tuple(name, path, args), eg:
209 ('show_user', '/profile/%(username)s', ['username'])
209 ('show_user', '/profile/%(username)s', ['username'])
210 """
210 """
211
211
212 routepath = route.pattern
212 routepath = route.pattern
213 pattern = route.pattern
213 pattern = route.pattern
214
214
215 def replace(matchobj):
215 def replace(matchobj):
216 if matchobj.group(1):
216 if matchobj.group(1):
217 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 return "%%(%s)s" % matchobj.group(1).split(':')[0]
218 else:
218 else:
219 return "%%(%s)s" % matchobj.group(2)
219 return "%%(%s)s" % matchobj.group(2)
220
220
221 routepath = _argument_prog.sub(replace, routepath)
221 routepath = _argument_prog.sub(replace, routepath)
222
222
223 if not routepath.startswith('/'):
223 if not routepath.startswith('/'):
224 routepath = '/'+routepath
224 routepath = '/'+routepath
225
225
226 return (
226 return (
227 route.name,
227 route.name,
228 routepath,
228 routepath,
229 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
230 for arg in _argument_prog.findall(pattern)]
230 for arg in _argument_prog.findall(pattern)]
231 )
231 )
232
232
233 def get_routes():
233 def get_routes():
234 # pylons routes
234 # pylons routes
235 # TODO(marcink): remove when pyramid migration is finished
236 if 'routes.map' in rhodecode.CONFIG:
235 for route in rhodecode.CONFIG['routes.map'].jsroutes():
237 for route in rhodecode.CONFIG['routes.map'].jsroutes():
236 yield route
238 yield route
237
239
238 # pyramid routes
240 # pyramid routes
239 for route in mapper.get_routes():
241 for route in mapper.get_routes():
240 if not route.name.startswith('__'):
242 if not route.name.startswith('__'):
241 yield _extract_route_information(route)
243 yield _extract_route_information(route)
242
244
243 if asbool(registry.settings.get('generate_js_files', 'false')):
245 if asbool(registry.settings.get('generate_js_files', 'false')):
244 static_path = AssetResolver().resolve('rhodecode:public').abspath()
246 static_path = AssetResolver().resolve('rhodecode:public').abspath()
245 jsroutes = get_routes()
247 jsroutes = get_routes()
246 jsroutes_file_content = generate_jsroutes_content(jsroutes)
248 jsroutes_file_content = generate_jsroutes_content(jsroutes)
247 jsroutes_file_path = os.path.join(
249 jsroutes_file_path = os.path.join(
248 static_path, 'js', 'rhodecode', 'routes.js')
250 static_path, 'js', 'rhodecode', 'routes.js')
249
251
250 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
252 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
251 f.write(jsroutes_file_content)
253 f.write(jsroutes_file_content)
252
254
253
255
254 class Subscriber(object):
256 class Subscriber(object):
255 """
257 """
256 Base class for subscribers to the pyramid event system.
258 Base class for subscribers to the pyramid event system.
257 """
259 """
258 def __call__(self, event):
260 def __call__(self, event):
259 self.run(event)
261 self.run(event)
260
262
261 def run(self, event):
263 def run(self, event):
262 raise NotImplementedError('Subclass has to implement this.')
264 raise NotImplementedError('Subclass has to implement this.')
263
265
264
266
265 class AsyncSubscriber(Subscriber):
267 class AsyncSubscriber(Subscriber):
266 """
268 """
267 Subscriber that handles the execution of events in a separate task to not
269 Subscriber that handles the execution of events in a separate task to not
268 block the execution of the code which triggers the event. It puts the
270 block the execution of the code which triggers the event. It puts the
269 received events into a queue from which the worker process takes them in
271 received events into a queue from which the worker process takes them in
270 order.
272 order.
271 """
273 """
272 def __init__(self):
274 def __init__(self):
273 self._stop = False
275 self._stop = False
274 self._eventq = Queue.Queue()
276 self._eventq = Queue.Queue()
275 self._worker = self.create_worker()
277 self._worker = self.create_worker()
276 self._worker.start()
278 self._worker.start()
277
279
278 def __call__(self, event):
280 def __call__(self, event):
279 self._eventq.put(event)
281 self._eventq.put(event)
280
282
281 def create_worker(self):
283 def create_worker(self):
282 worker = Thread(target=self.do_work)
284 worker = Thread(target=self.do_work)
283 worker.daemon = True
285 worker.daemon = True
284 return worker
286 return worker
285
287
286 def stop_worker(self):
288 def stop_worker(self):
287 self._stop = False
289 self._stop = False
288 self._eventq.put(None)
290 self._eventq.put(None)
289 self._worker.join()
291 self._worker.join()
290
292
291 def do_work(self):
293 def do_work(self):
292 while not self._stop:
294 while not self._stop:
293 event = self._eventq.get()
295 event = self._eventq.get()
294 if event is not None:
296 if event is not None:
295 self.run(event)
297 self.run(event)
296
298
297
299
298 class AsyncSubprocessSubscriber(AsyncSubscriber):
300 class AsyncSubprocessSubscriber(AsyncSubscriber):
299 """
301 """
300 Subscriber that uses the subprocess32 module to execute a command if an
302 Subscriber that uses the subprocess32 module to execute a command if an
301 event is received. Events are handled asynchronously.
303 event is received. Events are handled asynchronously.
302 """
304 """
303
305
304 def __init__(self, cmd, timeout=None):
306 def __init__(self, cmd, timeout=None):
305 super(AsyncSubprocessSubscriber, self).__init__()
307 super(AsyncSubprocessSubscriber, self).__init__()
306 self._cmd = cmd
308 self._cmd = cmd
307 self._timeout = timeout
309 self._timeout = timeout
308
310
309 def run(self, event):
311 def run(self, event):
310 cmd = self._cmd
312 cmd = self._cmd
311 timeout = self._timeout
313 timeout = self._timeout
312 log.debug('Executing command %s.', cmd)
314 log.debug('Executing command %s.', cmd)
313
315
314 try:
316 try:
315 output = subprocess32.check_output(
317 output = subprocess32.check_output(
316 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
318 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
317 log.debug('Command finished %s', cmd)
319 log.debug('Command finished %s', cmd)
318 if output:
320 if output:
319 log.debug('Command output: %s', output)
321 log.debug('Command output: %s', output)
320 except subprocess32.TimeoutExpired as e:
322 except subprocess32.TimeoutExpired as e:
321 log.exception('Timeout while executing command.')
323 log.exception('Timeout while executing command.')
322 if e.output:
324 if e.output:
323 log.error('Command output: %s', e.output)
325 log.error('Command output: %s', e.output)
324 except subprocess32.CalledProcessError as e:
326 except subprocess32.CalledProcessError as e:
325 log.exception('Error while executing command.')
327 log.exception('Error while executing command.')
326 if e.output:
328 if e.output:
327 log.error('Command output: %s', e.output)
329 log.error('Command output: %s', e.output)
328 except:
330 except:
329 log.exception(
331 log.exception(
330 'Exception while executing command %s.', cmd)
332 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now