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