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