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