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