##// END OF EJS Templates
hovercards: enable hovercards on parsed commits references.
dan -
r4047:4a632ba4 default
parent child Browse files
Show More
@@ -1,601 +1,613 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.apps._base import TemplateArgs
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 from rhodecode.lib.utils2 import (
45 from rhodecode.lib.utils2 import (
46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 def _filter_proxy(ip):
54 def _filter_proxy(ip):
55 """
55 """
56 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
57 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
58 chain of request processing. The left-most being the original client.
58 chain of request processing. The left-most being the original client.
59 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.
60
60
61 :param ip: ip string from headers
61 :param ip: ip string from headers
62 """
62 """
63 if ',' in ip:
63 if ',' in ip:
64 _ips = ip.split(',')
64 _ips = ip.split(',')
65 _first_ip = _ips[0].strip()
65 _first_ip = _ips[0].strip()
66 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)
67 return _first_ip
67 return _first_ip
68 return ip
68 return ip
69
69
70
70
71 def _filter_port(ip):
71 def _filter_port(ip):
72 """
72 """
73 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.
74 - ipv4 eg. 127.0.0.1
74 - ipv4 eg. 127.0.0.1
75 - ipv6 eg. ::1
75 - ipv6 eg. ::1
76 - ipv4+port eg. 127.0.0.1:8080
76 - ipv4+port eg. 127.0.0.1:8080
77 - ipv6+port eg. [::1]:8080
77 - ipv6+port eg. [::1]:8080
78
78
79 :param ip:
79 :param ip:
80 """
80 """
81 def is_ipv6(ip_addr):
81 def is_ipv6(ip_addr):
82 if hasattr(socket, 'inet_pton'):
82 if hasattr(socket, 'inet_pton'):
83 try:
83 try:
84 socket.inet_pton(socket.AF_INET6, ip_addr)
84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 except socket.error:
85 except socket.error:
86 return False
86 return False
87 else:
87 else:
88 # fallback to ipaddress
88 # fallback to ipaddress
89 try:
89 try:
90 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 except Exception:
91 except Exception:
92 return False
92 return False
93 return True
93 return True
94
94
95 if ':' not in ip: # must be ipv4 pure ip
95 if ':' not in ip: # must be ipv4 pure ip
96 return ip
96 return ip
97
97
98 if '[' in ip and ']' in ip: # ipv6 with port
98 if '[' in ip and ']' in ip: # ipv6 with port
99 return ip.split(']')[0][1:].lower()
99 return ip.split(']')[0][1:].lower()
100
100
101 # must be ipv6 or ipv4 with port
101 # must be ipv6 or ipv4 with port
102 if is_ipv6(ip):
102 if is_ipv6(ip):
103 return ip
103 return ip
104 else:
104 else:
105 ip, _port = ip.split(':')[:2] # means ipv4+port
105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 return ip
106 return ip
107
107
108
108
109 def get_ip_addr(environ):
109 def get_ip_addr(environ):
110 proxy_key = 'HTTP_X_REAL_IP'
110 proxy_key = 'HTTP_X_REAL_IP'
111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 def_key = 'REMOTE_ADDR'
112 def_key = 'REMOTE_ADDR'
113 _filters = lambda x: _filter_port(_filter_proxy(x))
113 _filters = lambda x: _filter_port(_filter_proxy(x))
114
114
115 ip = environ.get(proxy_key)
115 ip = environ.get(proxy_key)
116 if ip:
116 if ip:
117 return _filters(ip)
117 return _filters(ip)
118
118
119 ip = environ.get(proxy_key2)
119 ip = environ.get(proxy_key2)
120 if ip:
120 if ip:
121 return _filters(ip)
121 return _filters(ip)
122
122
123 ip = environ.get(def_key, '0.0.0.0')
123 ip = environ.get(def_key, '0.0.0.0')
124 return _filters(ip)
124 return _filters(ip)
125
125
126
126
127 def get_server_ip_addr(environ, log_errors=True):
127 def get_server_ip_addr(environ, log_errors=True):
128 hostname = environ.get('SERVER_NAME')
128 hostname = environ.get('SERVER_NAME')
129 try:
129 try:
130 return socket.gethostbyname(hostname)
130 return socket.gethostbyname(hostname)
131 except Exception as e:
131 except Exception as e:
132 if log_errors:
132 if log_errors:
133 # 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
134 # make it an exception in logs
134 # make it an exception in logs
135 log.exception('Could not retrieve server ip address: %s', e)
135 log.exception('Could not retrieve server ip address: %s', e)
136 return hostname
136 return hostname
137
137
138
138
139 def get_server_port(environ):
139 def get_server_port(environ):
140 return environ.get('SERVER_PORT')
140 return environ.get('SERVER_PORT')
141
141
142
142
143 def get_access_path(environ):
143 def get_access_path(environ):
144 path = environ.get('PATH_INFO')
144 path = environ.get('PATH_INFO')
145 org_req = environ.get('pylons.original_request')
145 org_req = environ.get('pylons.original_request')
146 if org_req:
146 if org_req:
147 path = org_req.environ.get('PATH_INFO')
147 path = org_req.environ.get('PATH_INFO')
148 return path
148 return path
149
149
150
150
151 def get_user_agent(environ):
151 def get_user_agent(environ):
152 return environ.get('HTTP_USER_AGENT')
152 return environ.get('HTTP_USER_AGENT')
153
153
154
154
155 def vcs_operation_context(
155 def vcs_operation_context(
156 environ, repo_name, username, action, scm, check_locking=True,
156 environ, repo_name, username, action, scm, check_locking=True,
157 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):
158 """
158 """
159 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.
160
160
161 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
162 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.
163
163
164 :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
165 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
166 able to disable this for certain operations.
166 able to disable this for certain operations.
167
167
168 """
168 """
169 # Tri-state value: False: unlock, None: nothing, True: lock
169 # Tri-state value: False: unlock, None: nothing, True: lock
170 make_lock = None
170 make_lock = None
171 locked_by = [None, None, None]
171 locked_by = [None, None, None]
172 is_anonymous = username == User.DEFAULT_USER
172 is_anonymous = username == User.DEFAULT_USER
173 user = User.get_by_username(username)
173 user = User.get_by_username(username)
174 if not is_anonymous and check_locking:
174 if not is_anonymous and check_locking:
175 log.debug('Checking locking on repository "%s"', repo_name)
175 log.debug('Checking locking on repository "%s"', repo_name)
176 repo = Repository.get_by_repo_name(repo_name)
176 repo = Repository.get_by_repo_name(repo_name)
177 make_lock, __, locked_by = repo.get_locking_state(
177 make_lock, __, locked_by = repo.get_locking_state(
178 action, user.user_id)
178 action, user.user_id)
179 user_id = user.user_id
179 user_id = user.user_id
180 settings_model = VcsSettingsModel(repo=repo_name)
180 settings_model = VcsSettingsModel(repo=repo_name)
181 ui_settings = settings_model.get_ui_settings()
181 ui_settings = settings_model.get_ui_settings()
182
182
183 # NOTE(marcink): This should be also in sync with
183 # NOTE(marcink): This should be also in sync with
184 # 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
185 store = [x for x in ui_settings if x.key == '/']
185 store = [x for x in ui_settings if x.key == '/']
186 repo_store = ''
186 repo_store = ''
187 if store:
187 if store:
188 repo_store = store[0].value
188 repo_store = store[0].value
189
189
190 scm_data = {
190 scm_data = {
191 'ip': get_ip_addr(environ),
191 'ip': get_ip_addr(environ),
192 'username': username,
192 'username': username,
193 'user_id': user_id,
193 'user_id': user_id,
194 'action': action,
194 'action': action,
195 'repository': repo_name,
195 'repository': repo_name,
196 'scm': scm,
196 'scm': scm,
197 'config': rhodecode.CONFIG['__file__'],
197 'config': rhodecode.CONFIG['__file__'],
198 'repo_store': repo_store,
198 'repo_store': repo_store,
199 'make_lock': make_lock,
199 'make_lock': make_lock,
200 'locked_by': locked_by,
200 'locked_by': locked_by,
201 'server_url': utils2.get_server_url(environ),
201 'server_url': utils2.get_server_url(environ),
202 'user_agent': get_user_agent(environ),
202 'user_agent': get_user_agent(environ),
203 'hooks': get_enabled_hook_classes(ui_settings),
203 'hooks': get_enabled_hook_classes(ui_settings),
204 'is_shadow_repo': is_shadow_repo,
204 'is_shadow_repo': is_shadow_repo,
205 'detect_force_push': detect_force_push,
205 'detect_force_push': detect_force_push,
206 'check_branch_perms': check_branch_perms,
206 'check_branch_perms': check_branch_perms,
207 }
207 }
208 return scm_data
208 return scm_data
209
209
210
210
211 class BasicAuth(AuthBasicAuthenticator):
211 class BasicAuth(AuthBasicAuthenticator):
212
212
213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 initial_call_detection=False, acl_repo_name=None):
214 initial_call_detection=False, acl_repo_name=None):
215 self.realm = realm
215 self.realm = realm
216 self.initial_call = initial_call_detection
216 self.initial_call = initial_call_detection
217 self.authfunc = authfunc
217 self.authfunc = authfunc
218 self.registry = registry
218 self.registry = registry
219 self.acl_repo_name = acl_repo_name
219 self.acl_repo_name = acl_repo_name
220 self._rc_auth_http_code = auth_http_code
220 self._rc_auth_http_code = auth_http_code
221
221
222 def _get_response_from_code(self, http_code):
222 def _get_response_from_code(self, http_code):
223 try:
223 try:
224 return get_exception(safe_int(http_code))
224 return get_exception(safe_int(http_code))
225 except Exception:
225 except Exception:
226 log.exception('Failed to fetch response for code %s', http_code)
226 log.exception('Failed to fetch response for code %s', http_code)
227 return HTTPForbidden
227 return HTTPForbidden
228
228
229 def get_rc_realm(self):
229 def get_rc_realm(self):
230 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
230 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
231
231
232 def build_authentication(self):
232 def build_authentication(self):
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 if self._rc_auth_http_code and not self.initial_call:
234 if self._rc_auth_http_code and not self.initial_call:
235 # return alternative HTTP code if alternative http return code
235 # return alternative HTTP code if alternative http return code
236 # 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
237 # FIRST call
237 # FIRST call
238 custom_response_klass = self._get_response_from_code(
238 custom_response_klass = self._get_response_from_code(
239 self._rc_auth_http_code)
239 self._rc_auth_http_code)
240 return custom_response_klass(headers=head)
240 return custom_response_klass(headers=head)
241 return HTTPUnauthorized(headers=head)
241 return HTTPUnauthorized(headers=head)
242
242
243 def authenticate(self, environ):
243 def authenticate(self, environ):
244 authorization = AUTHORIZATION(environ)
244 authorization = AUTHORIZATION(environ)
245 if not authorization:
245 if not authorization:
246 return self.build_authentication()
246 return self.build_authentication()
247 (authmeth, auth) = authorization.split(' ', 1)
247 (authmeth, auth) = authorization.split(' ', 1)
248 if 'basic' != authmeth.lower():
248 if 'basic' != authmeth.lower():
249 return self.build_authentication()
249 return self.build_authentication()
250 auth = auth.strip().decode('base64')
250 auth = auth.strip().decode('base64')
251 _parts = auth.split(':', 1)
251 _parts = auth.split(':', 1)
252 if len(_parts) == 2:
252 if len(_parts) == 2:
253 username, password = _parts
253 username, password = _parts
254 auth_data = self.authfunc(
254 auth_data = self.authfunc(
255 username, password, environ, VCS_TYPE,
255 username, password, environ, VCS_TYPE,
256 registry=self.registry, acl_repo_name=self.acl_repo_name)
256 registry=self.registry, acl_repo_name=self.acl_repo_name)
257 if auth_data:
257 if auth_data:
258 return {'username': username, 'auth_data': auth_data}
258 return {'username': username, 'auth_data': auth_data}
259 if username and password:
259 if username and password:
260 # we mark that we actually executed authentication once, at
260 # we mark that we actually executed authentication once, at
261 # that point we can use the alternative auth code
261 # that point we can use the alternative auth code
262 self.initial_call = False
262 self.initial_call = False
263
263
264 return self.build_authentication()
264 return self.build_authentication()
265
265
266 __call__ = authenticate
266 __call__ = authenticate
267
267
268
268
269 def calculate_version_hash(config):
269 def calculate_version_hash(config):
270 return sha1(
270 return sha1(
271 config.get('beaker.session.secret', '') +
271 config.get('beaker.session.secret', '') +
272 rhodecode.__version__)[:8]
272 rhodecode.__version__)[:8]
273
273
274
274
275 def get_current_lang(request):
275 def get_current_lang(request):
276 # NOTE(marcink): remove after pyramid move
276 # NOTE(marcink): remove after pyramid move
277 try:
277 try:
278 return translation.get_lang()[0]
278 return translation.get_lang()[0]
279 except:
279 except:
280 pass
280 pass
281
281
282 return getattr(request, '_LOCALE_', request.locale_name)
282 return getattr(request, '_LOCALE_', request.locale_name)
283
283
284
284
285 def attach_context_attributes(context, request, user_id=None):
285 def attach_context_attributes(context, request, user_id=None):
286 """
286 """
287 Attach variables into template context called `c`.
287 Attach variables into template context called `c`.
288 """
288 """
289 config = request.registry.settings
289 config = request.registry.settings
290
290
291 rc_config = SettingsModel().get_all_settings(cache=True)
291 rc_config = SettingsModel().get_all_settings(cache=True)
292 context.rc_config = rc_config
292 context.rc_config = rc_config
293 context.rhodecode_version = rhodecode.__version__
293 context.rhodecode_version = rhodecode.__version__
294 context.rhodecode_edition = config.get('rhodecode.edition')
294 context.rhodecode_edition = config.get('rhodecode.edition')
295 # unique secret + version does not leak the version but keep consistency
295 # unique secret + version does not leak the version but keep consistency
296 context.rhodecode_version_hash = calculate_version_hash(config)
296 context.rhodecode_version_hash = calculate_version_hash(config)
297
297
298 # Default language set for the incoming request
298 # Default language set for the incoming request
299 context.language = get_current_lang(request)
299 context.language = get_current_lang(request)
300
300
301 # Visual options
301 # Visual options
302 context.visual = AttributeDict({})
302 context.visual = AttributeDict({})
303
303
304 # DB stored Visual Items
304 # DB stored Visual Items
305 context.visual.show_public_icon = str2bool(
305 context.visual.show_public_icon = str2bool(
306 rc_config.get('rhodecode_show_public_icon'))
306 rc_config.get('rhodecode_show_public_icon'))
307 context.visual.show_private_icon = str2bool(
307 context.visual.show_private_icon = str2bool(
308 rc_config.get('rhodecode_show_private_icon'))
308 rc_config.get('rhodecode_show_private_icon'))
309 context.visual.stylify_metatags = str2bool(
309 context.visual.stylify_metatags = str2bool(
310 rc_config.get('rhodecode_stylify_metatags'))
310 rc_config.get('rhodecode_stylify_metatags'))
311 context.visual.dashboard_items = safe_int(
311 context.visual.dashboard_items = safe_int(
312 rc_config.get('rhodecode_dashboard_items', 100))
312 rc_config.get('rhodecode_dashboard_items', 100))
313 context.visual.admin_grid_items = safe_int(
313 context.visual.admin_grid_items = safe_int(
314 rc_config.get('rhodecode_admin_grid_items', 100))
314 rc_config.get('rhodecode_admin_grid_items', 100))
315 context.visual.show_revision_number = str2bool(
315 context.visual.show_revision_number = str2bool(
316 rc_config.get('rhodecode_show_revision_number', True))
316 rc_config.get('rhodecode_show_revision_number', True))
317 context.visual.show_sha_length = safe_int(
317 context.visual.show_sha_length = safe_int(
318 rc_config.get('rhodecode_show_sha_length', 100))
318 rc_config.get('rhodecode_show_sha_length', 100))
319 context.visual.repository_fields = str2bool(
319 context.visual.repository_fields = str2bool(
320 rc_config.get('rhodecode_repository_fields'))
320 rc_config.get('rhodecode_repository_fields'))
321 context.visual.show_version = str2bool(
321 context.visual.show_version = str2bool(
322 rc_config.get('rhodecode_show_version'))
322 rc_config.get('rhodecode_show_version'))
323 context.visual.use_gravatar = str2bool(
323 context.visual.use_gravatar = str2bool(
324 rc_config.get('rhodecode_use_gravatar'))
324 rc_config.get('rhodecode_use_gravatar'))
325 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
325 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
326 context.visual.default_renderer = rc_config.get(
326 context.visual.default_renderer = rc_config.get(
327 'rhodecode_markup_renderer', 'rst')
327 'rhodecode_markup_renderer', 'rst')
328 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
328 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
329 context.visual.rhodecode_support_url = \
329 context.visual.rhodecode_support_url = \
330 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
330 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
331
331
332 context.visual.affected_files_cut_off = 60
332 context.visual.affected_files_cut_off = 60
333
333
334 context.pre_code = rc_config.get('rhodecode_pre_code')
334 context.pre_code = rc_config.get('rhodecode_pre_code')
335 context.post_code = rc_config.get('rhodecode_post_code')
335 context.post_code = rc_config.get('rhodecode_post_code')
336 context.rhodecode_name = rc_config.get('rhodecode_title')
336 context.rhodecode_name = rc_config.get('rhodecode_title')
337 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
337 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
338 # if we have specified default_encoding in the request, it has more
338 # if we have specified default_encoding in the request, it has more
339 # priority
339 # priority
340 if request.GET.get('default_encoding'):
340 if request.GET.get('default_encoding'):
341 context.default_encodings.insert(0, request.GET.get('default_encoding'))
341 context.default_encodings.insert(0, request.GET.get('default_encoding'))
342 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
342 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
343 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
343 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
344
344
345 # INI stored
345 # INI stored
346 context.labs_active = str2bool(
346 context.labs_active = str2bool(
347 config.get('labs_settings_active', 'false'))
347 config.get('labs_settings_active', 'false'))
348 context.ssh_enabled = str2bool(
348 context.ssh_enabled = str2bool(
349 config.get('ssh.generate_authorized_keyfile', 'false'))
349 config.get('ssh.generate_authorized_keyfile', 'false'))
350 context.ssh_key_generator_enabled = str2bool(
350 context.ssh_key_generator_enabled = str2bool(
351 config.get('ssh.enable_ui_key_generator', 'true'))
351 config.get('ssh.enable_ui_key_generator', 'true'))
352
352
353 context.visual.allow_repo_location_change = str2bool(
353 context.visual.allow_repo_location_change = str2bool(
354 config.get('allow_repo_location_change', True))
354 config.get('allow_repo_location_change', True))
355 context.visual.allow_custom_hooks_settings = str2bool(
355 context.visual.allow_custom_hooks_settings = str2bool(
356 config.get('allow_custom_hooks_settings', True))
356 config.get('allow_custom_hooks_settings', True))
357 context.debug_style = str2bool(config.get('debug_style', False))
357 context.debug_style = str2bool(config.get('debug_style', False))
358
358
359 context.rhodecode_instanceid = config.get('instance_id')
359 context.rhodecode_instanceid = config.get('instance_id')
360
360
361 context.visual.cut_off_limit_diff = safe_int(
361 context.visual.cut_off_limit_diff = safe_int(
362 config.get('cut_off_limit_diff'))
362 config.get('cut_off_limit_diff'))
363 context.visual.cut_off_limit_file = safe_int(
363 context.visual.cut_off_limit_file = safe_int(
364 config.get('cut_off_limit_file'))
364 config.get('cut_off_limit_file'))
365
365
366 context.license = AttributeDict({})
366 context.license = AttributeDict({})
367 context.license.hide_license_info = str2bool(
367 context.license.hide_license_info = str2bool(
368 config.get('license.hide_license_info', False))
368 config.get('license.hide_license_info', False))
369
369
370 # AppEnlight
370 # AppEnlight
371 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
371 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
372 context.appenlight_api_public_key = config.get(
372 context.appenlight_api_public_key = config.get(
373 'appenlight.api_public_key', '')
373 'appenlight.api_public_key', '')
374 context.appenlight_server_url = config.get('appenlight.server_url', '')
374 context.appenlight_server_url = config.get('appenlight.server_url', '')
375
375
376 diffmode = {
376 diffmode = {
377 "unified": "unified",
377 "unified": "unified",
378 "sideside": "sideside"
378 "sideside": "sideside"
379 }.get(request.GET.get('diffmode'))
379 }.get(request.GET.get('diffmode'))
380
380
381 is_api = hasattr(request, 'rpc_user')
381 is_api = hasattr(request, 'rpc_user')
382 session_attrs = {
382 session_attrs = {
383 # defaults
383 # defaults
384 "clone_url_format": "http",
384 "clone_url_format": "http",
385 "diffmode": "sideside"
385 "diffmode": "sideside"
386 }
386 }
387
387
388 if not is_api:
388 if not is_api:
389 # don't access pyramid session for API calls
389 # don't access pyramid session for API calls
390 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
390 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
391 request.session['rc_user_session_attr.diffmode'] = diffmode
391 request.session['rc_user_session_attr.diffmode'] = diffmode
392
392
393 # session settings per user
393 # session settings per user
394
394
395 for k, v in request.session.items():
395 for k, v in request.session.items():
396 pref = 'rc_user_session_attr.'
396 pref = 'rc_user_session_attr.'
397 if k and k.startswith(pref):
397 if k and k.startswith(pref):
398 k = k[len(pref):]
398 k = k[len(pref):]
399 session_attrs[k] = v
399 session_attrs[k] = v
400
400
401 context.user_session_attrs = session_attrs
401 context.user_session_attrs = session_attrs
402
402
403 # JS template context
403 # JS template context
404 context.template_context = {
404 context.template_context = {
405 'repo_name': None,
405 'repo_name': None,
406 'repo_type': None,
406 'repo_type': None,
407 'repo_landing_commit': None,
407 'repo_landing_commit': None,
408 'rhodecode_user': {
408 'rhodecode_user': {
409 'username': None,
409 'username': None,
410 'email': None,
410 'email': None,
411 'notification_status': False
411 'notification_status': False
412 },
412 },
413 'session_attrs': session_attrs,
413 'session_attrs': session_attrs,
414 'visual': {
414 'visual': {
415 'default_renderer': None
415 'default_renderer': None
416 },
416 },
417 'commit_data': {
417 'commit_data': {
418 'commit_id': None
418 'commit_id': None
419 },
419 },
420 'pull_request_data': {'pull_request_id': None},
420 'pull_request_data': {'pull_request_id': None},
421 'timeago': {
421 'timeago': {
422 'refresh_time': 120 * 1000,
422 'refresh_time': 120 * 1000,
423 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
423 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
424 },
424 },
425 'pyramid_dispatch': {
425 'pyramid_dispatch': {
426
426
427 },
427 },
428 'extra': {'plugins': {}}
428 'extra': {'plugins': {}}
429 }
429 }
430 # END CONFIG VARS
430 # END CONFIG VARS
431 if is_api:
431 if is_api:
432 csrf_token = None
432 csrf_token = None
433 else:
433 else:
434 csrf_token = auth.get_csrf_token(session=request.session)
434 csrf_token = auth.get_csrf_token(session=request.session)
435
435
436 context.csrf_token = csrf_token
436 context.csrf_token = csrf_token
437 context.backends = rhodecode.BACKENDS.keys()
437 context.backends = rhodecode.BACKENDS.keys()
438 context.backends.sort()
438 context.backends.sort()
439 unread_count = 0
439 unread_count = 0
440 user_bookmark_list = []
440 user_bookmark_list = []
441 if user_id:
441 if user_id:
442 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
442 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
443 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
443 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
444 context.unread_notifications = unread_count
444 context.unread_notifications = unread_count
445 context.bookmark_items = user_bookmark_list
445 context.bookmark_items = user_bookmark_list
446
446
447 # web case
447 # web case
448 if hasattr(request, 'user'):
448 if hasattr(request, 'user'):
449 context.auth_user = request.user
449 context.auth_user = request.user
450 context.rhodecode_user = request.user
450 context.rhodecode_user = request.user
451
451
452 # api case
452 # api case
453 if hasattr(request, 'rpc_user'):
453 if hasattr(request, 'rpc_user'):
454 context.auth_user = request.rpc_user
454 context.auth_user = request.rpc_user
455 context.rhodecode_user = request.rpc_user
455 context.rhodecode_user = request.rpc_user
456
456
457 # attach the whole call context to the request
457 # attach the whole call context to the request
458 request.call_context = context
458 request.call_context = context
459
459
460
460
461 def get_auth_user(request):
461 def get_auth_user(request):
462 environ = request.environ
462 environ = request.environ
463 session = request.session
463 session = request.session
464
464
465 ip_addr = get_ip_addr(environ)
465 ip_addr = get_ip_addr(environ)
466
466
467 # make sure that we update permissions each time we call controller
467 # make sure that we update permissions each time we call controller
468 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
468 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
469 if not _auth_token and request.matchdict:
469 if not _auth_token and request.matchdict:
470 url_auth_token = request.matchdict.get('_auth_token')
470 url_auth_token = request.matchdict.get('_auth_token')
471 _auth_token = url_auth_token
471 _auth_token = url_auth_token
472 if _auth_token:
472 if _auth_token:
473 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
473 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
474
474
475 if _auth_token:
475 if _auth_token:
476 # when using API_KEY we assume user exists, and
476 # when using API_KEY we assume user exists, and
477 # doesn't need auth based on cookies.
477 # doesn't need auth based on cookies.
478 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
478 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
479 authenticated = False
479 authenticated = False
480 else:
480 else:
481 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
481 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
482 try:
482 try:
483 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
483 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
484 ip_addr=ip_addr)
484 ip_addr=ip_addr)
485 except UserCreationError as e:
485 except UserCreationError as e:
486 h.flash(e, 'error')
486 h.flash(e, 'error')
487 # container auth or other auth functions that create users
487 # container auth or other auth functions that create users
488 # on the fly can throw this exception signaling that there's
488 # on the fly can throw this exception signaling that there's
489 # issue with user creation, explanation should be provided
489 # issue with user creation, explanation should be provided
490 # in Exception itself. We then create a simple blank
490 # in Exception itself. We then create a simple blank
491 # AuthUser
491 # AuthUser
492 auth_user = AuthUser(ip_addr=ip_addr)
492 auth_user = AuthUser(ip_addr=ip_addr)
493
493
494 # in case someone changes a password for user it triggers session
494 # in case someone changes a password for user it triggers session
495 # flush and forces a re-login
495 # flush and forces a re-login
496 if password_changed(auth_user, session):
496 if password_changed(auth_user, session):
497 session.invalidate()
497 session.invalidate()
498 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
498 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
499 auth_user = AuthUser(ip_addr=ip_addr)
499 auth_user = AuthUser(ip_addr=ip_addr)
500
500
501 authenticated = cookie_store.get('is_authenticated')
501 authenticated = cookie_store.get('is_authenticated')
502
502
503 if not auth_user.is_authenticated and auth_user.is_user_object:
503 if not auth_user.is_authenticated and auth_user.is_user_object:
504 # user is not authenticated and not empty
504 # user is not authenticated and not empty
505 auth_user.set_authenticated(authenticated)
505 auth_user.set_authenticated(authenticated)
506
506
507 return auth_user, _auth_token
507 return auth_user, _auth_token
508
508
509
509
510 def h_filter(s):
510 def h_filter(s):
511 """
511 """
512 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
512 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
513 we wrap this with additional functionality that converts None to empty
513 we wrap this with additional functionality that converts None to empty
514 strings
514 strings
515 """
515 """
516 if s is None:
516 if s is None:
517 return markupsafe.Markup()
517 return markupsafe.Markup()
518 return markupsafe.escape(s)
518 return markupsafe.escape(s)
519
519
520
520
521 def add_events_routes(config):
521 def add_events_routes(config):
522 """
522 """
523 Adds routing that can be used in events. Because some events are triggered
523 Adds routing that can be used in events. Because some events are triggered
524 outside of pyramid context, we need to bootstrap request with some
524 outside of pyramid context, we need to bootstrap request with some
525 routing registered
525 routing registered
526 """
526 """
527
527
528 from rhodecode.apps._base import ADMIN_PREFIX
528 from rhodecode.apps._base import ADMIN_PREFIX
529
529
530 config.add_route(name='home', pattern='/')
530 config.add_route(name='home', pattern='/')
531
531
532 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
532 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
533 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
533 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
534 config.add_route(name='repo_summary', pattern='/{repo_name}')
534 config.add_route(name='repo_summary', pattern='/{repo_name}')
535 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
535 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
536 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
536 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
537
537
538 config.add_route(name='pullrequest_show',
538 config.add_route(name='pullrequest_show',
539 pattern='/{repo_name}/pull-request/{pull_request_id}')
539 pattern='/{repo_name}/pull-request/{pull_request_id}')
540 config.add_route(name='pull_requests_global',
540 config.add_route(name='pull_requests_global',
541 pattern='/pull-request/{pull_request_id}')
541 pattern='/pull-request/{pull_request_id}')
542
542 config.add_route(name='repo_commit',
543 config.add_route(name='repo_commit',
543 pattern='/{repo_name}/changeset/{commit_id}')
544 pattern='/{repo_name}/changeset/{commit_id}')
544
545 config.add_route(name='repo_files',
545 config.add_route(name='repo_files',
546 pattern='/{repo_name}/files/{commit_id}/{f_path}')
546 pattern='/{repo_name}/files/{commit_id}/{f_path}')
547
547
548 config.add_route(name='hovercard_user',
549 pattern='/_hovercard/user/{user_id}')
550
551 config.add_route(name='hovercard_user_group',
552 pattern='/_hovercard/user_group/{user_group_id}')
553
554 config.add_route(name='hovercard_pull_request',
555 pattern='/_hovercard/pull_request/{pull_request_id}')
556
557 config.add_route(name='hovercard_repo_commit',
558 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
559
548
560
549 def bootstrap_config(request):
561 def bootstrap_config(request):
550 import pyramid.testing
562 import pyramid.testing
551 registry = pyramid.testing.Registry('RcTestRegistry')
563 registry = pyramid.testing.Registry('RcTestRegistry')
552
564
553 config = pyramid.testing.setUp(registry=registry, request=request)
565 config = pyramid.testing.setUp(registry=registry, request=request)
554
566
555 # allow pyramid lookup in testing
567 # allow pyramid lookup in testing
556 config.include('pyramid_mako')
568 config.include('pyramid_mako')
557 config.include('rhodecode.lib.rc_beaker')
569 config.include('rhodecode.lib.rc_beaker')
558 config.include('rhodecode.lib.rc_cache')
570 config.include('rhodecode.lib.rc_cache')
559
571
560 add_events_routes(config)
572 add_events_routes(config)
561
573
562 return config
574 return config
563
575
564
576
565 def bootstrap_request(**kwargs):
577 def bootstrap_request(**kwargs):
566 import pyramid.testing
578 import pyramid.testing
567
579
568 class TestRequest(pyramid.testing.DummyRequest):
580 class TestRequest(pyramid.testing.DummyRequest):
569 application_url = kwargs.pop('application_url', 'http://example.com')
581 application_url = kwargs.pop('application_url', 'http://example.com')
570 host = kwargs.pop('host', 'example.com:80')
582 host = kwargs.pop('host', 'example.com:80')
571 domain = kwargs.pop('domain', 'example.com')
583 domain = kwargs.pop('domain', 'example.com')
572
584
573 def translate(self, msg):
585 def translate(self, msg):
574 return msg
586 return msg
575
587
576 def plularize(self, singular, plural, n):
588 def plularize(self, singular, plural, n):
577 return singular
589 return singular
578
590
579 def get_partial_renderer(self, tmpl_name):
591 def get_partial_renderer(self, tmpl_name):
580
592
581 from rhodecode.lib.partial_renderer import get_partial_renderer
593 from rhodecode.lib.partial_renderer import get_partial_renderer
582 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
594 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
583
595
584 _call_context = TemplateArgs()
596 _call_context = TemplateArgs()
585 _call_context.visual = TemplateArgs()
597 _call_context.visual = TemplateArgs()
586 _call_context.visual.show_sha_length = 12
598 _call_context.visual.show_sha_length = 12
587 _call_context.visual.show_revision_number = True
599 _call_context.visual.show_revision_number = True
588
600
589 @property
601 @property
590 def call_context(self):
602 def call_context(self):
591 return self._call_context
603 return self._call_context
592
604
593 class TestDummySession(pyramid.testing.DummySession):
605 class TestDummySession(pyramid.testing.DummySession):
594 def save(*arg, **kw):
606 def save(*arg, **kw):
595 pass
607 pass
596
608
597 request = TestRequest(**kwargs)
609 request = TestRequest(**kwargs)
598 request.session = TestDummySession()
610 request.session = TestDummySession()
599
611
600 return request
612 return request
601
613
@@ -1,2125 +1,2126 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 time
37 import time
38 import string
38 import string
39 import hashlib
39 import hashlib
40 from collections import OrderedDict
40 from collections import OrderedDict
41
41
42 import pygments
42 import pygments
43 import itertools
43 import itertools
44 import fnmatch
44 import fnmatch
45 import bleach
45 import bleach
46
46
47 from pyramid import compat
47 from pyramid import compat
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 (
77 from rhodecode.lib.utils2 import (
78 str2bool, safe_unicode, safe_str,
78 str2bool, safe_unicode, safe_str,
79 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
79 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
80 AttributeDict, safe_int, md5, md5_safe, get_host_info)
80 AttributeDict, safe_int, md5, md5_safe, get_host_info)
81 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
82 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
83 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
84 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 from rhodecode.lib.index.search_utils import get_matching_line_offsets
85 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
86 from rhodecode.model.changeset_status import ChangesetStatusModel
86 from rhodecode.model.changeset_status import ChangesetStatusModel
87 from rhodecode.model.db import Permission, User, Repository
87 from rhodecode.model.db import Permission, User, Repository
88 from rhodecode.model.repo_group import RepoGroupModel
88 from rhodecode.model.repo_group import RepoGroupModel
89 from rhodecode.model.settings import IssueTrackerSettingsModel
89 from rhodecode.model.settings import IssueTrackerSettingsModel
90
90
91
91
92 log = logging.getLogger(__name__)
92 log = logging.getLogger(__name__)
93
93
94
94
95 DEFAULT_USER = User.DEFAULT_USER
95 DEFAULT_USER = User.DEFAULT_USER
96 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
97
97
98
98
99 def asset(path, ver=None, **kwargs):
99 def asset(path, ver=None, **kwargs):
100 """
100 """
101 Helper to generate a static asset file path for rhodecode assets
101 Helper to generate a static asset file path for rhodecode assets
102
102
103 eg. h.asset('images/image.png', ver='3923')
103 eg. h.asset('images/image.png', ver='3923')
104
104
105 :param path: path of asset
105 :param path: path of asset
106 :param ver: optional version query param to append as ?ver=
106 :param ver: optional version query param to append as ?ver=
107 """
107 """
108 request = get_current_request()
108 request = get_current_request()
109 query = {}
109 query = {}
110 query.update(kwargs)
110 query.update(kwargs)
111 if ver:
111 if ver:
112 query = {'ver': ver}
112 query = {'ver': ver}
113 return request.static_path(
113 return request.static_path(
114 'rhodecode:public/{}'.format(path), _query=query)
114 'rhodecode:public/{}'.format(path), _query=query)
115
115
116
116
117 default_html_escape_table = {
117 default_html_escape_table = {
118 ord('&'): u'&amp;',
118 ord('&'): u'&amp;',
119 ord('<'): u'&lt;',
119 ord('<'): u'&lt;',
120 ord('>'): u'&gt;',
120 ord('>'): u'&gt;',
121 ord('"'): u'&quot;',
121 ord('"'): u'&quot;',
122 ord("'"): u'&#39;',
122 ord("'"): u'&#39;',
123 }
123 }
124
124
125
125
126 def html_escape(text, html_escape_table=default_html_escape_table):
126 def html_escape(text, html_escape_table=default_html_escape_table):
127 """Produce entities within text."""
127 """Produce entities within text."""
128 return text.translate(html_escape_table)
128 return text.translate(html_escape_table)
129
129
130
130
131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
132 """
132 """
133 Truncate string ``s`` at the first occurrence of ``sub``.
133 Truncate string ``s`` at the first occurrence of ``sub``.
134
134
135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
136 """
136 """
137 suffix_if_chopped = suffix_if_chopped or ''
137 suffix_if_chopped = suffix_if_chopped or ''
138 pos = s.find(sub)
138 pos = s.find(sub)
139 if pos == -1:
139 if pos == -1:
140 return s
140 return s
141
141
142 if inclusive:
142 if inclusive:
143 pos += len(sub)
143 pos += len(sub)
144
144
145 chopped = s[:pos]
145 chopped = s[:pos]
146 left = s[pos:].strip()
146 left = s[pos:].strip()
147
147
148 if left and suffix_if_chopped:
148 if left and suffix_if_chopped:
149 chopped += suffix_if_chopped
149 chopped += suffix_if_chopped
150
150
151 return chopped
151 return chopped
152
152
153
153
154 def shorter(text, size=20, prefix=False):
154 def shorter(text, size=20, prefix=False):
155 postfix = '...'
155 postfix = '...'
156 if len(text) > size:
156 if len(text) > size:
157 if prefix:
157 if prefix:
158 # shorten in front
158 # shorten in front
159 return postfix + text[-(size - len(postfix)):]
159 return postfix + text[-(size - len(postfix)):]
160 else:
160 else:
161 return text[:size - len(postfix)] + postfix
161 return text[:size - len(postfix)] + postfix
162 return text
162 return text
163
163
164
164
165 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
165 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
166 """
166 """
167 Reset button
167 Reset button
168 """
168 """
169 _set_input_attrs(attrs, type, name, value)
169 _set_input_attrs(attrs, type, name, value)
170 _set_id_attr(attrs, id, name)
170 _set_id_attr(attrs, id, name)
171 convert_boolean_attrs(attrs, ["disabled"])
171 convert_boolean_attrs(attrs, ["disabled"])
172 return HTML.input(**attrs)
172 return HTML.input(**attrs)
173
173
174 reset = _reset
174 reset = _reset
175 safeid = _make_safe_id_component
175 safeid = _make_safe_id_component
176
176
177
177
178 def branding(name, length=40):
178 def branding(name, length=40):
179 return truncate(name, length, indicator="")
179 return truncate(name, length, indicator="")
180
180
181
181
182 def FID(raw_id, path):
182 def FID(raw_id, path):
183 """
183 """
184 Creates a unique ID for filenode based on it's hash of path and commit
184 Creates a unique ID for filenode based on it's hash of path and commit
185 it's safe to use in urls
185 it's safe to use in urls
186
186
187 :param raw_id:
187 :param raw_id:
188 :param path:
188 :param path:
189 """
189 """
190
190
191 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
191 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
192
192
193
193
194 class _GetError(object):
194 class _GetError(object):
195 """Get error from form_errors, and represent it as span wrapped error
195 """Get error from form_errors, and represent it as span wrapped error
196 message
196 message
197
197
198 :param field_name: field to fetch errors for
198 :param field_name: field to fetch errors for
199 :param form_errors: form errors dict
199 :param form_errors: form errors dict
200 """
200 """
201
201
202 def __call__(self, field_name, form_errors):
202 def __call__(self, field_name, form_errors):
203 tmpl = """<span class="error_msg">%s</span>"""
203 tmpl = """<span class="error_msg">%s</span>"""
204 if form_errors and field_name in form_errors:
204 if form_errors and field_name in form_errors:
205 return literal(tmpl % form_errors.get(field_name))
205 return literal(tmpl % form_errors.get(field_name))
206
206
207
207
208 get_error = _GetError()
208 get_error = _GetError()
209
209
210
210
211 class _ToolTip(object):
211 class _ToolTip(object):
212
212
213 def __call__(self, tooltip_title, trim_at=50):
213 def __call__(self, tooltip_title, trim_at=50):
214 """
214 """
215 Special function just to wrap our text into nice formatted
215 Special function just to wrap our text into nice formatted
216 autowrapped text
216 autowrapped text
217
217
218 :param tooltip_title:
218 :param tooltip_title:
219 """
219 """
220 tooltip_title = escape(tooltip_title)
220 tooltip_title = escape(tooltip_title)
221 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
221 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
222 return tooltip_title
222 return tooltip_title
223
223
224
224
225 tooltip = _ToolTip()
225 tooltip = _ToolTip()
226
226
227 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
227 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
228
228
229
229
230 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
230 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
231 if isinstance(file_path, str):
231 if isinstance(file_path, str):
232 file_path = safe_unicode(file_path)
232 file_path = safe_unicode(file_path)
233
233
234 route_qry = {'at': at_ref} if at_ref else None
234 route_qry = {'at': at_ref} if at_ref else None
235
235
236 # first segment is a `..` link to repo files
236 # first segment is a `..` link to repo files
237 root_name = literal(u'<i class="icon-home"></i>')
237 root_name = literal(u'<i class="icon-home"></i>')
238 url_segments = [
238 url_segments = [
239 link_to(
239 link_to(
240 root_name,
240 root_name,
241 route_path(
241 route_path(
242 'repo_files',
242 'repo_files',
243 repo_name=repo_name,
243 repo_name=repo_name,
244 commit_id=commit_id,
244 commit_id=commit_id,
245 f_path='',
245 f_path='',
246 _query=route_qry),
246 _query=route_qry),
247 )]
247 )]
248
248
249 path_segments = file_path.split('/')
249 path_segments = file_path.split('/')
250 last_cnt = len(path_segments) - 1
250 last_cnt = len(path_segments) - 1
251 for cnt, segment in enumerate(path_segments):
251 for cnt, segment in enumerate(path_segments):
252 if not segment:
252 if not segment:
253 continue
253 continue
254 segment_html = escape(segment)
254 segment_html = escape(segment)
255
255
256 last_item = cnt == last_cnt
256 last_item = cnt == last_cnt
257
257
258 if last_item and linkify_last_item is False:
258 if last_item and linkify_last_item is False:
259 # plain version
259 # plain version
260 url_segments.append(segment_html)
260 url_segments.append(segment_html)
261 else:
261 else:
262 url_segments.append(
262 url_segments.append(
263 link_to(
263 link_to(
264 segment_html,
264 segment_html,
265 route_path(
265 route_path(
266 'repo_files',
266 'repo_files',
267 repo_name=repo_name,
267 repo_name=repo_name,
268 commit_id=commit_id,
268 commit_id=commit_id,
269 f_path='/'.join(path_segments[:cnt + 1]),
269 f_path='/'.join(path_segments[:cnt + 1]),
270 _query=route_qry),
270 _query=route_qry),
271 ))
271 ))
272
272
273 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
273 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
274 if limit_items and len(limited_url_segments) < len(url_segments):
274 if limit_items and len(limited_url_segments) < len(url_segments):
275 url_segments = limited_url_segments
275 url_segments = limited_url_segments
276
276
277 full_path = file_path
277 full_path = file_path
278 icon = files_icon.format(escape(full_path))
278 icon = files_icon.format(escape(full_path))
279 if file_path == '':
279 if file_path == '':
280 return root_name
280 return root_name
281 else:
281 else:
282 return literal(' / '.join(url_segments) + icon)
282 return literal(' / '.join(url_segments) + icon)
283
283
284
284
285 def files_url_data(request):
285 def files_url_data(request):
286 matchdict = request.matchdict
286 matchdict = request.matchdict
287
287
288 if 'f_path' not in matchdict:
288 if 'f_path' not in matchdict:
289 matchdict['f_path'] = ''
289 matchdict['f_path'] = ''
290
290
291 if 'commit_id' not in matchdict:
291 if 'commit_id' not in matchdict:
292 matchdict['commit_id'] = 'tip'
292 matchdict['commit_id'] = 'tip'
293
293
294 return json.dumps(matchdict)
294 return json.dumps(matchdict)
295
295
296
296
297 def code_highlight(code, lexer, formatter, use_hl_filter=False):
297 def code_highlight(code, lexer, formatter, use_hl_filter=False):
298 """
298 """
299 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
299 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
300
300
301 If ``outfile`` is given and a valid file object (an object
301 If ``outfile`` is given and a valid file object (an object
302 with a ``write`` method), the result will be written to it, otherwise
302 with a ``write`` method), the result will be written to it, otherwise
303 it is returned as a string.
303 it is returned as a string.
304 """
304 """
305 if use_hl_filter:
305 if use_hl_filter:
306 # add HL filter
306 # add HL filter
307 from rhodecode.lib.index import search_utils
307 from rhodecode.lib.index import search_utils
308 lexer.add_filter(search_utils.ElasticSearchHLFilter())
308 lexer.add_filter(search_utils.ElasticSearchHLFilter())
309 return pygments.format(pygments.lex(code, lexer), formatter)
309 return pygments.format(pygments.lex(code, lexer), formatter)
310
310
311
311
312 class CodeHtmlFormatter(HtmlFormatter):
312 class CodeHtmlFormatter(HtmlFormatter):
313 """
313 """
314 My code Html Formatter for source codes
314 My code Html Formatter for source codes
315 """
315 """
316
316
317 def wrap(self, source, outfile):
317 def wrap(self, source, outfile):
318 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
318 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
319
319
320 def _wrap_code(self, source):
320 def _wrap_code(self, source):
321 for cnt, it in enumerate(source):
321 for cnt, it in enumerate(source):
322 i, t = it
322 i, t = it
323 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
323 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
324 yield i, t
324 yield i, t
325
325
326 def _wrap_tablelinenos(self, inner):
326 def _wrap_tablelinenos(self, inner):
327 dummyoutfile = StringIO.StringIO()
327 dummyoutfile = StringIO.StringIO()
328 lncount = 0
328 lncount = 0
329 for t, line in inner:
329 for t, line in inner:
330 if t:
330 if t:
331 lncount += 1
331 lncount += 1
332 dummyoutfile.write(line)
332 dummyoutfile.write(line)
333
333
334 fl = self.linenostart
334 fl = self.linenostart
335 mw = len(str(lncount + fl - 1))
335 mw = len(str(lncount + fl - 1))
336 sp = self.linenospecial
336 sp = self.linenospecial
337 st = self.linenostep
337 st = self.linenostep
338 la = self.lineanchors
338 la = self.lineanchors
339 aln = self.anchorlinenos
339 aln = self.anchorlinenos
340 nocls = self.noclasses
340 nocls = self.noclasses
341 if sp:
341 if sp:
342 lines = []
342 lines = []
343
343
344 for i in range(fl, fl + lncount):
344 for i in range(fl, fl + lncount):
345 if i % st == 0:
345 if i % st == 0:
346 if i % sp == 0:
346 if i % sp == 0:
347 if aln:
347 if aln:
348 lines.append('<a href="#%s%d" class="special">%*d</a>' %
348 lines.append('<a href="#%s%d" class="special">%*d</a>' %
349 (la, i, mw, i))
349 (la, i, mw, i))
350 else:
350 else:
351 lines.append('<span class="special">%*d</span>' % (mw, i))
351 lines.append('<span class="special">%*d</span>' % (mw, i))
352 else:
352 else:
353 if aln:
353 if aln:
354 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
354 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
355 else:
355 else:
356 lines.append('%*d' % (mw, i))
356 lines.append('%*d' % (mw, i))
357 else:
357 else:
358 lines.append('')
358 lines.append('')
359 ls = '\n'.join(lines)
359 ls = '\n'.join(lines)
360 else:
360 else:
361 lines = []
361 lines = []
362 for i in range(fl, fl + lncount):
362 for i in range(fl, fl + lncount):
363 if i % st == 0:
363 if i % st == 0:
364 if aln:
364 if aln:
365 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
365 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
366 else:
366 else:
367 lines.append('%*d' % (mw, i))
367 lines.append('%*d' % (mw, i))
368 else:
368 else:
369 lines.append('')
369 lines.append('')
370 ls = '\n'.join(lines)
370 ls = '\n'.join(lines)
371
371
372 # in case you wonder about the seemingly redundant <div> here: since the
372 # in case you wonder about the seemingly redundant <div> here: since the
373 # content in the other cell also is wrapped in a div, some browsers in
373 # content in the other cell also is wrapped in a div, some browsers in
374 # some configurations seem to mess up the formatting...
374 # some configurations seem to mess up the formatting...
375 if nocls:
375 if nocls:
376 yield 0, ('<table class="%stable">' % self.cssclass +
376 yield 0, ('<table class="%stable">' % self.cssclass +
377 '<tr><td><div class="linenodiv" '
377 '<tr><td><div class="linenodiv" '
378 'style="background-color: #f0f0f0; padding-right: 10px">'
378 'style="background-color: #f0f0f0; padding-right: 10px">'
379 '<pre style="line-height: 125%">' +
379 '<pre style="line-height: 125%">' +
380 ls + '</pre></div></td><td id="hlcode" class="code">')
380 ls + '</pre></div></td><td id="hlcode" class="code">')
381 else:
381 else:
382 yield 0, ('<table class="%stable">' % self.cssclass +
382 yield 0, ('<table class="%stable">' % self.cssclass +
383 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
383 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
384 ls + '</pre></div></td><td id="hlcode" class="code">')
384 ls + '</pre></div></td><td id="hlcode" class="code">')
385 yield 0, dummyoutfile.getvalue()
385 yield 0, dummyoutfile.getvalue()
386 yield 0, '</td></tr></table>'
386 yield 0, '</td></tr></table>'
387
387
388
388
389 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
389 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
390 def __init__(self, **kw):
390 def __init__(self, **kw):
391 # only show these line numbers if set
391 # only show these line numbers if set
392 self.only_lines = kw.pop('only_line_numbers', [])
392 self.only_lines = kw.pop('only_line_numbers', [])
393 self.query_terms = kw.pop('query_terms', [])
393 self.query_terms = kw.pop('query_terms', [])
394 self.max_lines = kw.pop('max_lines', 5)
394 self.max_lines = kw.pop('max_lines', 5)
395 self.line_context = kw.pop('line_context', 3)
395 self.line_context = kw.pop('line_context', 3)
396 self.url = kw.pop('url', None)
396 self.url = kw.pop('url', None)
397
397
398 super(CodeHtmlFormatter, self).__init__(**kw)
398 super(CodeHtmlFormatter, self).__init__(**kw)
399
399
400 def _wrap_code(self, source):
400 def _wrap_code(self, source):
401 for cnt, it in enumerate(source):
401 for cnt, it in enumerate(source):
402 i, t = it
402 i, t = it
403 t = '<pre>%s</pre>' % t
403 t = '<pre>%s</pre>' % t
404 yield i, t
404 yield i, t
405
405
406 def _wrap_tablelinenos(self, inner):
406 def _wrap_tablelinenos(self, inner):
407 yield 0, '<table class="code-highlight %stable">' % self.cssclass
407 yield 0, '<table class="code-highlight %stable">' % self.cssclass
408
408
409 last_shown_line_number = 0
409 last_shown_line_number = 0
410 current_line_number = 1
410 current_line_number = 1
411
411
412 for t, line in inner:
412 for t, line in inner:
413 if not t:
413 if not t:
414 yield t, line
414 yield t, line
415 continue
415 continue
416
416
417 if current_line_number in self.only_lines:
417 if current_line_number in self.only_lines:
418 if last_shown_line_number + 1 != current_line_number:
418 if last_shown_line_number + 1 != current_line_number:
419 yield 0, '<tr>'
419 yield 0, '<tr>'
420 yield 0, '<td class="line">...</td>'
420 yield 0, '<td class="line">...</td>'
421 yield 0, '<td id="hlcode" class="code"></td>'
421 yield 0, '<td id="hlcode" class="code"></td>'
422 yield 0, '</tr>'
422 yield 0, '</tr>'
423
423
424 yield 0, '<tr>'
424 yield 0, '<tr>'
425 if self.url:
425 if self.url:
426 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
426 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
427 self.url, current_line_number, current_line_number)
427 self.url, current_line_number, current_line_number)
428 else:
428 else:
429 yield 0, '<td class="line"><a href="">%i</a></td>' % (
429 yield 0, '<td class="line"><a href="">%i</a></td>' % (
430 current_line_number)
430 current_line_number)
431 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
431 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
432 yield 0, '</tr>'
432 yield 0, '</tr>'
433
433
434 last_shown_line_number = current_line_number
434 last_shown_line_number = current_line_number
435
435
436 current_line_number += 1
436 current_line_number += 1
437
437
438 yield 0, '</table>'
438 yield 0, '</table>'
439
439
440
440
441 def hsv_to_rgb(h, s, v):
441 def hsv_to_rgb(h, s, v):
442 """ Convert hsv color values to rgb """
442 """ Convert hsv color values to rgb """
443
443
444 if s == 0.0:
444 if s == 0.0:
445 return v, v, v
445 return v, v, v
446 i = int(h * 6.0) # XXX assume int() truncates!
446 i = int(h * 6.0) # XXX assume int() truncates!
447 f = (h * 6.0) - i
447 f = (h * 6.0) - i
448 p = v * (1.0 - s)
448 p = v * (1.0 - s)
449 q = v * (1.0 - s * f)
449 q = v * (1.0 - s * f)
450 t = v * (1.0 - s * (1.0 - f))
450 t = v * (1.0 - s * (1.0 - f))
451 i = i % 6
451 i = i % 6
452 if i == 0:
452 if i == 0:
453 return v, t, p
453 return v, t, p
454 if i == 1:
454 if i == 1:
455 return q, v, p
455 return q, v, p
456 if i == 2:
456 if i == 2:
457 return p, v, t
457 return p, v, t
458 if i == 3:
458 if i == 3:
459 return p, q, v
459 return p, q, v
460 if i == 4:
460 if i == 4:
461 return t, p, v
461 return t, p, v
462 if i == 5:
462 if i == 5:
463 return v, p, q
463 return v, p, q
464
464
465
465
466 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
466 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
467 """
467 """
468 Generator for getting n of evenly distributed colors using
468 Generator for getting n of evenly distributed colors using
469 hsv color and golden ratio. It always return same order of colors
469 hsv color and golden ratio. It always return same order of colors
470
470
471 :param n: number of colors to generate
471 :param n: number of colors to generate
472 :param saturation: saturation of returned colors
472 :param saturation: saturation of returned colors
473 :param lightness: lightness of returned colors
473 :param lightness: lightness of returned colors
474 :returns: RGB tuple
474 :returns: RGB tuple
475 """
475 """
476
476
477 golden_ratio = 0.618033988749895
477 golden_ratio = 0.618033988749895
478 h = 0.22717784590367374
478 h = 0.22717784590367374
479
479
480 for _ in xrange(n):
480 for _ in xrange(n):
481 h += golden_ratio
481 h += golden_ratio
482 h %= 1
482 h %= 1
483 HSV_tuple = [h, saturation, lightness]
483 HSV_tuple = [h, saturation, lightness]
484 RGB_tuple = hsv_to_rgb(*HSV_tuple)
484 RGB_tuple = hsv_to_rgb(*HSV_tuple)
485 yield map(lambda x: str(int(x * 256)), RGB_tuple)
485 yield map(lambda x: str(int(x * 256)), RGB_tuple)
486
486
487
487
488 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
488 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
489 """
489 """
490 Returns a function which when called with an argument returns a unique
490 Returns a function which when called with an argument returns a unique
491 color for that argument, eg.
491 color for that argument, eg.
492
492
493 :param n: number of colors to generate
493 :param n: number of colors to generate
494 :param saturation: saturation of returned colors
494 :param saturation: saturation of returned colors
495 :param lightness: lightness of returned colors
495 :param lightness: lightness of returned colors
496 :returns: css RGB string
496 :returns: css RGB string
497
497
498 >>> color_hash = color_hasher()
498 >>> color_hash = color_hasher()
499 >>> color_hash('hello')
499 >>> color_hash('hello')
500 'rgb(34, 12, 59)'
500 'rgb(34, 12, 59)'
501 >>> color_hash('hello')
501 >>> color_hash('hello')
502 'rgb(34, 12, 59)'
502 'rgb(34, 12, 59)'
503 >>> color_hash('other')
503 >>> color_hash('other')
504 'rgb(90, 224, 159)'
504 'rgb(90, 224, 159)'
505 """
505 """
506
506
507 color_dict = {}
507 color_dict = {}
508 cgenerator = unique_color_generator(
508 cgenerator = unique_color_generator(
509 saturation=saturation, lightness=lightness)
509 saturation=saturation, lightness=lightness)
510
510
511 def get_color_string(thing):
511 def get_color_string(thing):
512 if thing in color_dict:
512 if thing in color_dict:
513 col = color_dict[thing]
513 col = color_dict[thing]
514 else:
514 else:
515 col = color_dict[thing] = cgenerator.next()
515 col = color_dict[thing] = cgenerator.next()
516 return "rgb(%s)" % (', '.join(col))
516 return "rgb(%s)" % (', '.join(col))
517
517
518 return get_color_string
518 return get_color_string
519
519
520
520
521 def get_lexer_safe(mimetype=None, filepath=None):
521 def get_lexer_safe(mimetype=None, filepath=None):
522 """
522 """
523 Tries to return a relevant pygments lexer using mimetype/filepath name,
523 Tries to return a relevant pygments lexer using mimetype/filepath name,
524 defaulting to plain text if none could be found
524 defaulting to plain text if none could be found
525 """
525 """
526 lexer = None
526 lexer = None
527 try:
527 try:
528 if mimetype:
528 if mimetype:
529 lexer = get_lexer_for_mimetype(mimetype)
529 lexer = get_lexer_for_mimetype(mimetype)
530 if not lexer:
530 if not lexer:
531 lexer = get_lexer_for_filename(filepath)
531 lexer = get_lexer_for_filename(filepath)
532 except pygments.util.ClassNotFound:
532 except pygments.util.ClassNotFound:
533 pass
533 pass
534
534
535 if not lexer:
535 if not lexer:
536 lexer = get_lexer_by_name('text')
536 lexer = get_lexer_by_name('text')
537
537
538 return lexer
538 return lexer
539
539
540
540
541 def get_lexer_for_filenode(filenode):
541 def get_lexer_for_filenode(filenode):
542 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
542 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
543 return lexer
543 return lexer
544
544
545
545
546 def pygmentize(filenode, **kwargs):
546 def pygmentize(filenode, **kwargs):
547 """
547 """
548 pygmentize function using pygments
548 pygmentize function using pygments
549
549
550 :param filenode:
550 :param filenode:
551 """
551 """
552 lexer = get_lexer_for_filenode(filenode)
552 lexer = get_lexer_for_filenode(filenode)
553 return literal(code_highlight(filenode.content, lexer,
553 return literal(code_highlight(filenode.content, lexer,
554 CodeHtmlFormatter(**kwargs)))
554 CodeHtmlFormatter(**kwargs)))
555
555
556
556
557 def is_following_repo(repo_name, user_id):
557 def is_following_repo(repo_name, user_id):
558 from rhodecode.model.scm import ScmModel
558 from rhodecode.model.scm import ScmModel
559 return ScmModel().is_following_repo(repo_name, user_id)
559 return ScmModel().is_following_repo(repo_name, user_id)
560
560
561
561
562 class _Message(object):
562 class _Message(object):
563 """A message returned by ``Flash.pop_messages()``.
563 """A message returned by ``Flash.pop_messages()``.
564
564
565 Converting the message to a string returns the message text. Instances
565 Converting the message to a string returns the message text. Instances
566 also have the following attributes:
566 also have the following attributes:
567
567
568 * ``message``: the message text.
568 * ``message``: the message text.
569 * ``category``: the category specified when the message was created.
569 * ``category``: the category specified when the message was created.
570 """
570 """
571
571
572 def __init__(self, category, message):
572 def __init__(self, category, message):
573 self.category = category
573 self.category = category
574 self.message = message
574 self.message = message
575
575
576 def __str__(self):
576 def __str__(self):
577 return self.message
577 return self.message
578
578
579 __unicode__ = __str__
579 __unicode__ = __str__
580
580
581 def __html__(self):
581 def __html__(self):
582 return escape(safe_unicode(self.message))
582 return escape(safe_unicode(self.message))
583
583
584
584
585 class Flash(object):
585 class Flash(object):
586 # List of allowed categories. If None, allow any category.
586 # List of allowed categories. If None, allow any category.
587 categories = ["warning", "notice", "error", "success"]
587 categories = ["warning", "notice", "error", "success"]
588
588
589 # Default category if none is specified.
589 # Default category if none is specified.
590 default_category = "notice"
590 default_category = "notice"
591
591
592 def __init__(self, session_key="flash", categories=None,
592 def __init__(self, session_key="flash", categories=None,
593 default_category=None):
593 default_category=None):
594 """
594 """
595 Instantiate a ``Flash`` object.
595 Instantiate a ``Flash`` object.
596
596
597 ``session_key`` is the key to save the messages under in the user's
597 ``session_key`` is the key to save the messages under in the user's
598 session.
598 session.
599
599
600 ``categories`` is an optional list which overrides the default list
600 ``categories`` is an optional list which overrides the default list
601 of categories.
601 of categories.
602
602
603 ``default_category`` overrides the default category used for messages
603 ``default_category`` overrides the default category used for messages
604 when none is specified.
604 when none is specified.
605 """
605 """
606 self.session_key = session_key
606 self.session_key = session_key
607 if categories is not None:
607 if categories is not None:
608 self.categories = categories
608 self.categories = categories
609 if default_category is not None:
609 if default_category is not None:
610 self.default_category = default_category
610 self.default_category = default_category
611 if self.categories and self.default_category not in self.categories:
611 if self.categories and self.default_category not in self.categories:
612 raise ValueError(
612 raise ValueError(
613 "unrecognized default category %r" % (self.default_category,))
613 "unrecognized default category %r" % (self.default_category,))
614
614
615 def pop_messages(self, session=None, request=None):
615 def pop_messages(self, session=None, request=None):
616 """
616 """
617 Return all accumulated messages and delete them from the session.
617 Return all accumulated messages and delete them from the session.
618
618
619 The return value is a list of ``Message`` objects.
619 The return value is a list of ``Message`` objects.
620 """
620 """
621 messages = []
621 messages = []
622
622
623 if not session:
623 if not session:
624 if not request:
624 if not request:
625 request = get_current_request()
625 request = get_current_request()
626 session = request.session
626 session = request.session
627
627
628 # Pop the 'old' pylons flash messages. They are tuples of the form
628 # Pop the 'old' pylons flash messages. They are tuples of the form
629 # (category, message)
629 # (category, message)
630 for cat, msg in session.pop(self.session_key, []):
630 for cat, msg in session.pop(self.session_key, []):
631 messages.append(_Message(cat, msg))
631 messages.append(_Message(cat, msg))
632
632
633 # Pop the 'new' pyramid flash messages for each category as list
633 # Pop the 'new' pyramid flash messages for each category as list
634 # of strings.
634 # of strings.
635 for cat in self.categories:
635 for cat in self.categories:
636 for msg in session.pop_flash(queue=cat):
636 for msg in session.pop_flash(queue=cat):
637 messages.append(_Message(cat, msg))
637 messages.append(_Message(cat, msg))
638 # Map messages from the default queue to the 'notice' category.
638 # Map messages from the default queue to the 'notice' category.
639 for msg in session.pop_flash():
639 for msg in session.pop_flash():
640 messages.append(_Message('notice', msg))
640 messages.append(_Message('notice', msg))
641
641
642 session.save()
642 session.save()
643 return messages
643 return messages
644
644
645 def json_alerts(self, session=None, request=None):
645 def json_alerts(self, session=None, request=None):
646 payloads = []
646 payloads = []
647 messages = flash.pop_messages(session=session, request=request)
647 messages = flash.pop_messages(session=session, request=request)
648 if messages:
648 if messages:
649 for message in messages:
649 for message in messages:
650 subdata = {}
650 subdata = {}
651 if hasattr(message.message, 'rsplit'):
651 if hasattr(message.message, 'rsplit'):
652 flash_data = message.message.rsplit('|DELIM|', 1)
652 flash_data = message.message.rsplit('|DELIM|', 1)
653 org_message = flash_data[0]
653 org_message = flash_data[0]
654 if len(flash_data) > 1:
654 if len(flash_data) > 1:
655 subdata = json.loads(flash_data[1])
655 subdata = json.loads(flash_data[1])
656 else:
656 else:
657 org_message = message.message
657 org_message = message.message
658 payloads.append({
658 payloads.append({
659 'message': {
659 'message': {
660 'message': u'{}'.format(org_message),
660 'message': u'{}'.format(org_message),
661 'level': message.category,
661 'level': message.category,
662 'force': True,
662 'force': True,
663 'subdata': subdata
663 'subdata': subdata
664 }
664 }
665 })
665 })
666 return json.dumps(payloads)
666 return json.dumps(payloads)
667
667
668 def __call__(self, message, category=None, ignore_duplicate=True,
668 def __call__(self, message, category=None, ignore_duplicate=True,
669 session=None, request=None):
669 session=None, request=None):
670
670
671 if not session:
671 if not session:
672 if not request:
672 if not request:
673 request = get_current_request()
673 request = get_current_request()
674 session = request.session
674 session = request.session
675
675
676 session.flash(
676 session.flash(
677 message, queue=category, allow_duplicate=not ignore_duplicate)
677 message, queue=category, allow_duplicate=not ignore_duplicate)
678
678
679
679
680 flash = Flash()
680 flash = Flash()
681
681
682 #==============================================================================
682 #==============================================================================
683 # SCM FILTERS available via h.
683 # SCM FILTERS available via h.
684 #==============================================================================
684 #==============================================================================
685 from rhodecode.lib.vcs.utils import author_name, author_email
685 from rhodecode.lib.vcs.utils import author_name, author_email
686 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
686 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
687 from rhodecode.model.db import User, ChangesetStatus
687 from rhodecode.model.db import User, ChangesetStatus
688
688
689 capitalize = lambda x: x.capitalize()
689 capitalize = lambda x: x.capitalize()
690 email = author_email
690 email = author_email
691 short_id = lambda x: x[:12]
691 short_id = lambda x: x[:12]
692 hide_credentials = lambda x: ''.join(credentials_filter(x))
692 hide_credentials = lambda x: ''.join(credentials_filter(x))
693
693
694
694
695 import pytz
695 import pytz
696 import tzlocal
696 import tzlocal
697 local_timezone = tzlocal.get_localzone()
697 local_timezone = tzlocal.get_localzone()
698
698
699
699
700 def age_component(datetime_iso, value=None, time_is_local=False):
700 def age_component(datetime_iso, value=None, time_is_local=False):
701 title = value or format_date(datetime_iso)
701 title = value or format_date(datetime_iso)
702 tzinfo = '+00:00'
702 tzinfo = '+00:00'
703
703
704 # detect if we have a timezone info, otherwise, add it
704 # detect if we have a timezone info, otherwise, add it
705 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
705 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
706 force_timezone = os.environ.get('RC_TIMEZONE', '')
706 force_timezone = os.environ.get('RC_TIMEZONE', '')
707 if force_timezone:
707 if force_timezone:
708 force_timezone = pytz.timezone(force_timezone)
708 force_timezone = pytz.timezone(force_timezone)
709 timezone = force_timezone or local_timezone
709 timezone = force_timezone or local_timezone
710 offset = timezone.localize(datetime_iso).strftime('%z')
710 offset = timezone.localize(datetime_iso).strftime('%z')
711 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
711 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
712
712
713 return literal(
713 return literal(
714 '<time class="timeago tooltip" '
714 '<time class="timeago tooltip" '
715 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
715 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
716 datetime_iso, title, tzinfo))
716 datetime_iso, title, tzinfo))
717
717
718
718
719 def _shorten_commit_id(commit_id, commit_len=None):
719 def _shorten_commit_id(commit_id, commit_len=None):
720 if commit_len is None:
720 if commit_len is None:
721 request = get_current_request()
721 request = get_current_request()
722 commit_len = request.call_context.visual.show_sha_length
722 commit_len = request.call_context.visual.show_sha_length
723 return commit_id[:commit_len]
723 return commit_id[:commit_len]
724
724
725
725
726 def show_id(commit, show_idx=None, commit_len=None):
726 def show_id(commit, show_idx=None, commit_len=None):
727 """
727 """
728 Configurable function that shows ID
728 Configurable function that shows ID
729 by default it's r123:fffeeefffeee
729 by default it's r123:fffeeefffeee
730
730
731 :param commit: commit instance
731 :param commit: commit instance
732 """
732 """
733 if show_idx is None:
733 if show_idx is None:
734 request = get_current_request()
734 request = get_current_request()
735 show_idx = request.call_context.visual.show_revision_number
735 show_idx = request.call_context.visual.show_revision_number
736
736
737 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
737 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
738 if show_idx:
738 if show_idx:
739 return 'r%s:%s' % (commit.idx, raw_id)
739 return 'r%s:%s' % (commit.idx, raw_id)
740 else:
740 else:
741 return '%s' % (raw_id, )
741 return '%s' % (raw_id, )
742
742
743
743
744 def format_date(date):
744 def format_date(date):
745 """
745 """
746 use a standardized formatting for dates used in RhodeCode
746 use a standardized formatting for dates used in RhodeCode
747
747
748 :param date: date/datetime object
748 :param date: date/datetime object
749 :return: formatted date
749 :return: formatted date
750 """
750 """
751
751
752 if date:
752 if date:
753 _fmt = "%a, %d %b %Y %H:%M:%S"
753 _fmt = "%a, %d %b %Y %H:%M:%S"
754 return safe_unicode(date.strftime(_fmt))
754 return safe_unicode(date.strftime(_fmt))
755
755
756 return u""
756 return u""
757
757
758
758
759 class _RepoChecker(object):
759 class _RepoChecker(object):
760
760
761 def __init__(self, backend_alias):
761 def __init__(self, backend_alias):
762 self._backend_alias = backend_alias
762 self._backend_alias = backend_alias
763
763
764 def __call__(self, repository):
764 def __call__(self, repository):
765 if hasattr(repository, 'alias'):
765 if hasattr(repository, 'alias'):
766 _type = repository.alias
766 _type = repository.alias
767 elif hasattr(repository, 'repo_type'):
767 elif hasattr(repository, 'repo_type'):
768 _type = repository.repo_type
768 _type = repository.repo_type
769 else:
769 else:
770 _type = repository
770 _type = repository
771 return _type == self._backend_alias
771 return _type == self._backend_alias
772
772
773
773
774 is_git = _RepoChecker('git')
774 is_git = _RepoChecker('git')
775 is_hg = _RepoChecker('hg')
775 is_hg = _RepoChecker('hg')
776 is_svn = _RepoChecker('svn')
776 is_svn = _RepoChecker('svn')
777
777
778
778
779 def get_repo_type_by_name(repo_name):
779 def get_repo_type_by_name(repo_name):
780 repo = Repository.get_by_repo_name(repo_name)
780 repo = Repository.get_by_repo_name(repo_name)
781 if repo:
781 if repo:
782 return repo.repo_type
782 return repo.repo_type
783
783
784
784
785 def is_svn_without_proxy(repository):
785 def is_svn_without_proxy(repository):
786 if is_svn(repository):
786 if is_svn(repository):
787 from rhodecode.model.settings import VcsSettingsModel
787 from rhodecode.model.settings import VcsSettingsModel
788 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
788 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
789 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
789 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
790 return False
790 return False
791
791
792
792
793 def discover_user(author):
793 def discover_user(author):
794 """
794 """
795 Tries to discover RhodeCode User based on the autho string. Author string
795 Tries to discover RhodeCode User based on the autho string. Author string
796 is typically `FirstName LastName <email@address.com>`
796 is typically `FirstName LastName <email@address.com>`
797 """
797 """
798
798
799 # if author is already an instance use it for extraction
799 # if author is already an instance use it for extraction
800 if isinstance(author, User):
800 if isinstance(author, User):
801 return author
801 return author
802
802
803 # Valid email in the attribute passed, see if they're in the system
803 # Valid email in the attribute passed, see if they're in the system
804 _email = author_email(author)
804 _email = author_email(author)
805 if _email != '':
805 if _email != '':
806 user = User.get_by_email(_email, case_insensitive=True, cache=True)
806 user = User.get_by_email(_email, case_insensitive=True, cache=True)
807 if user is not None:
807 if user is not None:
808 return user
808 return user
809
809
810 # Maybe it's a username, we try to extract it and fetch by username ?
810 # Maybe it's a username, we try to extract it and fetch by username ?
811 _author = author_name(author)
811 _author = author_name(author)
812 user = User.get_by_username(_author, case_insensitive=True, cache=True)
812 user = User.get_by_username(_author, case_insensitive=True, cache=True)
813 if user is not None:
813 if user is not None:
814 return user
814 return user
815
815
816 return None
816 return None
817
817
818
818
819 def email_or_none(author):
819 def email_or_none(author):
820 # extract email from the commit string
820 # extract email from the commit string
821 _email = author_email(author)
821 _email = author_email(author)
822
822
823 # If we have an email, use it, otherwise
823 # If we have an email, use it, otherwise
824 # see if it contains a username we can get an email from
824 # see if it contains a username we can get an email from
825 if _email != '':
825 if _email != '':
826 return _email
826 return _email
827 else:
827 else:
828 user = User.get_by_username(
828 user = User.get_by_username(
829 author_name(author), case_insensitive=True, cache=True)
829 author_name(author), case_insensitive=True, cache=True)
830
830
831 if user is not None:
831 if user is not None:
832 return user.email
832 return user.email
833
833
834 # No valid email, not a valid user in the system, none!
834 # No valid email, not a valid user in the system, none!
835 return None
835 return None
836
836
837
837
838 def link_to_user(author, length=0, **kwargs):
838 def link_to_user(author, length=0, **kwargs):
839 user = discover_user(author)
839 user = discover_user(author)
840 # user can be None, but if we have it already it means we can re-use it
840 # user can be None, but if we have it already it means we can re-use it
841 # in the person() function, so we save 1 intensive-query
841 # in the person() function, so we save 1 intensive-query
842 if user:
842 if user:
843 author = user
843 author = user
844
844
845 display_person = person(author, 'username_or_name_or_email')
845 display_person = person(author, 'username_or_name_or_email')
846 if length:
846 if length:
847 display_person = shorter(display_person, length)
847 display_person = shorter(display_person, length)
848
848
849 if user:
849 if user:
850 return link_to(
850 return link_to(
851 escape(display_person),
851 escape(display_person),
852 route_path('user_profile', username=user.username),
852 route_path('user_profile', username=user.username),
853 **kwargs)
853 **kwargs)
854 else:
854 else:
855 return escape(display_person)
855 return escape(display_person)
856
856
857
857
858 def link_to_group(users_group_name, **kwargs):
858 def link_to_group(users_group_name, **kwargs):
859 return link_to(
859 return link_to(
860 escape(users_group_name),
860 escape(users_group_name),
861 route_path('user_group_profile', user_group_name=users_group_name),
861 route_path('user_group_profile', user_group_name=users_group_name),
862 **kwargs)
862 **kwargs)
863
863
864
864
865 def person(author, show_attr="username_and_name"):
865 def person(author, show_attr="username_and_name"):
866 user = discover_user(author)
866 user = discover_user(author)
867 if user:
867 if user:
868 return getattr(user, show_attr)
868 return getattr(user, show_attr)
869 else:
869 else:
870 _author = author_name(author)
870 _author = author_name(author)
871 _email = email(author)
871 _email = email(author)
872 return _author or _email
872 return _author or _email
873
873
874
874
875 def author_string(email):
875 def author_string(email):
876 if email:
876 if email:
877 user = User.get_by_email(email, case_insensitive=True, cache=True)
877 user = User.get_by_email(email, case_insensitive=True, cache=True)
878 if user:
878 if user:
879 if user.first_name or user.last_name:
879 if user.first_name or user.last_name:
880 return '%s %s &lt;%s&gt;' % (
880 return '%s %s &lt;%s&gt;' % (
881 user.first_name, user.last_name, email)
881 user.first_name, user.last_name, email)
882 else:
882 else:
883 return email
883 return email
884 else:
884 else:
885 return email
885 return email
886 else:
886 else:
887 return None
887 return None
888
888
889
889
890 def person_by_id(id_, show_attr="username_and_name"):
890 def person_by_id(id_, show_attr="username_and_name"):
891 # attr to return from fetched user
891 # attr to return from fetched user
892 person_getter = lambda usr: getattr(usr, show_attr)
892 person_getter = lambda usr: getattr(usr, show_attr)
893
893
894 #maybe it's an ID ?
894 #maybe it's an ID ?
895 if str(id_).isdigit() or isinstance(id_, int):
895 if str(id_).isdigit() or isinstance(id_, int):
896 id_ = int(id_)
896 id_ = int(id_)
897 user = User.get(id_)
897 user = User.get(id_)
898 if user is not None:
898 if user is not None:
899 return person_getter(user)
899 return person_getter(user)
900 return id_
900 return id_
901
901
902
902
903 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
904 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
905 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
905 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
906
906
907
907
908 tags_paterns = OrderedDict((
908 tags_paterns = OrderedDict((
909 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
909 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
910 '<div class="metatag" tag="lang">\\2</div>')),
910 '<div class="metatag" tag="lang">\\2</div>')),
911
911
912 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
912 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
913 '<div class="metatag" tag="see">see: \\1 </div>')),
913 '<div class="metatag" tag="see">see: \\1 </div>')),
914
914
915 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
915 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
916 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
916 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
917
917
918 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
918 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
919 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
919 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
920
920
921 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
921 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
922 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
922 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
923
923
924 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
924 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
925 '<div class="metatag" tag="state \\1">\\1</div>')),
925 '<div class="metatag" tag="state \\1">\\1</div>')),
926
926
927 # label in grey
927 # label in grey
928 ('label', (re.compile(r'\[([a-z]+)\]'),
928 ('label', (re.compile(r'\[([a-z]+)\]'),
929 '<div class="metatag" tag="label">\\1</div>')),
929 '<div class="metatag" tag="label">\\1</div>')),
930
930
931 # generic catch all in grey
931 # generic catch all in grey
932 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
932 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
933 '<div class="metatag" tag="generic">\\1</div>')),
933 '<div class="metatag" tag="generic">\\1</div>')),
934 ))
934 ))
935
935
936
936
937 def extract_metatags(value):
937 def extract_metatags(value):
938 """
938 """
939 Extract supported meta-tags from given text value
939 Extract supported meta-tags from given text value
940 """
940 """
941 tags = []
941 tags = []
942 if not value:
942 if not value:
943 return tags, ''
943 return tags, ''
944
944
945 for key, val in tags_paterns.items():
945 for key, val in tags_paterns.items():
946 pat, replace_html = val
946 pat, replace_html = val
947 tags.extend([(key, x.group()) for x in pat.finditer(value)])
947 tags.extend([(key, x.group()) for x in pat.finditer(value)])
948 value = pat.sub('', value)
948 value = pat.sub('', value)
949
949
950 return tags, value
950 return tags, value
951
951
952
952
953 def style_metatag(tag_type, value):
953 def style_metatag(tag_type, value):
954 """
954 """
955 converts tags from value into html equivalent
955 converts tags from value into html equivalent
956 """
956 """
957 if not value:
957 if not value:
958 return ''
958 return ''
959
959
960 html_value = value
960 html_value = value
961 tag_data = tags_paterns.get(tag_type)
961 tag_data = tags_paterns.get(tag_type)
962 if tag_data:
962 if tag_data:
963 pat, replace_html = tag_data
963 pat, replace_html = tag_data
964 # convert to plain `unicode` instead of a markup tag to be used in
964 # convert to plain `unicode` instead of a markup tag to be used in
965 # regex expressions. safe_unicode doesn't work here
965 # regex expressions. safe_unicode doesn't work here
966 html_value = pat.sub(replace_html, unicode(value))
966 html_value = pat.sub(replace_html, unicode(value))
967
967
968 return html_value
968 return html_value
969
969
970
970
971 def bool2icon(value, show_at_false=True):
971 def bool2icon(value, show_at_false=True):
972 """
972 """
973 Returns boolean value of a given value, represented as html element with
973 Returns boolean value of a given value, represented as html element with
974 classes that will represent icons
974 classes that will represent icons
975
975
976 :param value: given value to convert to html node
976 :param value: given value to convert to html node
977 """
977 """
978
978
979 if value: # does bool conversion
979 if value: # does bool conversion
980 return HTML.tag('i', class_="icon-true", title='True')
980 return HTML.tag('i', class_="icon-true", title='True')
981 else: # not true as bool
981 else: # not true as bool
982 if show_at_false:
982 if show_at_false:
983 return HTML.tag('i', class_="icon-false", title='False')
983 return HTML.tag('i', class_="icon-false", title='False')
984 return HTML.tag('i')
984 return HTML.tag('i')
985
985
986 #==============================================================================
986 #==============================================================================
987 # PERMS
987 # PERMS
988 #==============================================================================
988 #==============================================================================
989 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
989 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
990 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
990 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
991 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
991 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
992 csrf_token_key
992 csrf_token_key
993
993
994
994
995 #==============================================================================
995 #==============================================================================
996 # GRAVATAR URL
996 # GRAVATAR URL
997 #==============================================================================
997 #==============================================================================
998 class InitialsGravatar(object):
998 class InitialsGravatar(object):
999 def __init__(self, email_address, first_name, last_name, size=30,
999 def __init__(self, email_address, first_name, last_name, size=30,
1000 background=None, text_color='#fff'):
1000 background=None, text_color='#fff'):
1001 self.size = size
1001 self.size = size
1002 self.first_name = first_name
1002 self.first_name = first_name
1003 self.last_name = last_name
1003 self.last_name = last_name
1004 self.email_address = email_address
1004 self.email_address = email_address
1005 self.background = background or self.str2color(email_address)
1005 self.background = background or self.str2color(email_address)
1006 self.text_color = text_color
1006 self.text_color = text_color
1007
1007
1008 def get_color_bank(self):
1008 def get_color_bank(self):
1009 """
1009 """
1010 returns a predefined list of colors that gravatars can use.
1010 returns a predefined list of colors that gravatars can use.
1011 Those are randomized distinct colors that guarantee readability and
1011 Those are randomized distinct colors that guarantee readability and
1012 uniqueness.
1012 uniqueness.
1013
1013
1014 generated with: http://phrogz.net/css/distinct-colors.html
1014 generated with: http://phrogz.net/css/distinct-colors.html
1015 """
1015 """
1016 return [
1016 return [
1017 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1017 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1018 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1018 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1019 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1019 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1020 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1020 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1021 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1021 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1022 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1022 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1023 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1023 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1024 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1024 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1025 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1025 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1026 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1026 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1027 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1027 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1028 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1028 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1029 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1029 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1030 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1030 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1031 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1031 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1032 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1032 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1033 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1033 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1034 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1034 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1035 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1035 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1036 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1036 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1037 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1037 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1038 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1038 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1039 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1039 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1040 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1040 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1041 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1041 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1042 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1042 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1043 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1043 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1044 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1044 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1045 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1045 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1046 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1046 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1047 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1047 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1048 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1048 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1049 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1049 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1050 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1050 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1051 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1051 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1052 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1052 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1053 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1053 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1054 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1054 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1055 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1055 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1056 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1056 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1057 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1057 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1058 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1058 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1059 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1059 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1060 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1060 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1061 '#4f8c46', '#368dd9', '#5c0073'
1061 '#4f8c46', '#368dd9', '#5c0073'
1062 ]
1062 ]
1063
1063
1064 def rgb_to_hex_color(self, rgb_tuple):
1064 def rgb_to_hex_color(self, rgb_tuple):
1065 """
1065 """
1066 Converts an rgb_tuple passed to an hex color.
1066 Converts an rgb_tuple passed to an hex color.
1067
1067
1068 :param rgb_tuple: tuple with 3 ints represents rgb color space
1068 :param rgb_tuple: tuple with 3 ints represents rgb color space
1069 """
1069 """
1070 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1070 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1071
1071
1072 def email_to_int_list(self, email_str):
1072 def email_to_int_list(self, email_str):
1073 """
1073 """
1074 Get every byte of the hex digest value of email and turn it to integer.
1074 Get every byte of the hex digest value of email and turn it to integer.
1075 It's going to be always between 0-255
1075 It's going to be always between 0-255
1076 """
1076 """
1077 digest = md5_safe(email_str.lower())
1077 digest = md5_safe(email_str.lower())
1078 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1078 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1079
1079
1080 def pick_color_bank_index(self, email_str, color_bank):
1080 def pick_color_bank_index(self, email_str, color_bank):
1081 return self.email_to_int_list(email_str)[0] % len(color_bank)
1081 return self.email_to_int_list(email_str)[0] % len(color_bank)
1082
1082
1083 def str2color(self, email_str):
1083 def str2color(self, email_str):
1084 """
1084 """
1085 Tries to map in a stable algorithm an email to color
1085 Tries to map in a stable algorithm an email to color
1086
1086
1087 :param email_str:
1087 :param email_str:
1088 """
1088 """
1089 color_bank = self.get_color_bank()
1089 color_bank = self.get_color_bank()
1090 # pick position (module it's length so we always find it in the
1090 # pick position (module it's length so we always find it in the
1091 # bank even if it's smaller than 256 values
1091 # bank even if it's smaller than 256 values
1092 pos = self.pick_color_bank_index(email_str, color_bank)
1092 pos = self.pick_color_bank_index(email_str, color_bank)
1093 return color_bank[pos]
1093 return color_bank[pos]
1094
1094
1095 def normalize_email(self, email_address):
1095 def normalize_email(self, email_address):
1096 import unicodedata
1096 import unicodedata
1097 # default host used to fill in the fake/missing email
1097 # default host used to fill in the fake/missing email
1098 default_host = u'localhost'
1098 default_host = u'localhost'
1099
1099
1100 if not email_address:
1100 if not email_address:
1101 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1101 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1102
1102
1103 email_address = safe_unicode(email_address)
1103 email_address = safe_unicode(email_address)
1104
1104
1105 if u'@' not in email_address:
1105 if u'@' not in email_address:
1106 email_address = u'%s@%s' % (email_address, default_host)
1106 email_address = u'%s@%s' % (email_address, default_host)
1107
1107
1108 if email_address.endswith(u'@'):
1108 if email_address.endswith(u'@'):
1109 email_address = u'%s%s' % (email_address, default_host)
1109 email_address = u'%s%s' % (email_address, default_host)
1110
1110
1111 email_address = unicodedata.normalize('NFKD', email_address)\
1111 email_address = unicodedata.normalize('NFKD', email_address)\
1112 .encode('ascii', 'ignore')
1112 .encode('ascii', 'ignore')
1113 return email_address
1113 return email_address
1114
1114
1115 def get_initials(self):
1115 def get_initials(self):
1116 """
1116 """
1117 Returns 2 letter initials calculated based on the input.
1117 Returns 2 letter initials calculated based on the input.
1118 The algorithm picks first given email address, and takes first letter
1118 The algorithm picks first given email address, and takes first letter
1119 of part before @, and then the first letter of server name. In case
1119 of part before @, and then the first letter of server name. In case
1120 the part before @ is in a format of `somestring.somestring2` it replaces
1120 the part before @ is in a format of `somestring.somestring2` it replaces
1121 the server letter with first letter of somestring2
1121 the server letter with first letter of somestring2
1122
1122
1123 In case function was initialized with both first and lastname, this
1123 In case function was initialized with both first and lastname, this
1124 overrides the extraction from email by first letter of the first and
1124 overrides the extraction from email by first letter of the first and
1125 last name. We add special logic to that functionality, In case Full name
1125 last name. We add special logic to that functionality, In case Full name
1126 is compound, like Guido Von Rossum, we use last part of the last name
1126 is compound, like Guido Von Rossum, we use last part of the last name
1127 (Von Rossum) picking `R`.
1127 (Von Rossum) picking `R`.
1128
1128
1129 Function also normalizes the non-ascii characters to they ascii
1129 Function also normalizes the non-ascii characters to they ascii
1130 representation, eg Δ„ => A
1130 representation, eg Δ„ => A
1131 """
1131 """
1132 import unicodedata
1132 import unicodedata
1133 # replace non-ascii to ascii
1133 # replace non-ascii to ascii
1134 first_name = unicodedata.normalize(
1134 first_name = unicodedata.normalize(
1135 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1135 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1136 last_name = unicodedata.normalize(
1136 last_name = unicodedata.normalize(
1137 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1137 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1138
1138
1139 # do NFKD encoding, and also make sure email has proper format
1139 # do NFKD encoding, and also make sure email has proper format
1140 email_address = self.normalize_email(self.email_address)
1140 email_address = self.normalize_email(self.email_address)
1141
1141
1142 # first push the email initials
1142 # first push the email initials
1143 prefix, server = email_address.split('@', 1)
1143 prefix, server = email_address.split('@', 1)
1144
1144
1145 # check if prefix is maybe a 'first_name.last_name' syntax
1145 # check if prefix is maybe a 'first_name.last_name' syntax
1146 _dot_split = prefix.rsplit('.', 1)
1146 _dot_split = prefix.rsplit('.', 1)
1147 if len(_dot_split) == 2 and _dot_split[1]:
1147 if len(_dot_split) == 2 and _dot_split[1]:
1148 initials = [_dot_split[0][0], _dot_split[1][0]]
1148 initials = [_dot_split[0][0], _dot_split[1][0]]
1149 else:
1149 else:
1150 initials = [prefix[0], server[0]]
1150 initials = [prefix[0], server[0]]
1151
1151
1152 # then try to replace either first_name or last_name
1152 # then try to replace either first_name or last_name
1153 fn_letter = (first_name or " ")[0].strip()
1153 fn_letter = (first_name or " ")[0].strip()
1154 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1154 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1155
1155
1156 if fn_letter:
1156 if fn_letter:
1157 initials[0] = fn_letter
1157 initials[0] = fn_letter
1158
1158
1159 if ln_letter:
1159 if ln_letter:
1160 initials[1] = ln_letter
1160 initials[1] = ln_letter
1161
1161
1162 return ''.join(initials).upper()
1162 return ''.join(initials).upper()
1163
1163
1164 def get_img_data_by_type(self, font_family, img_type):
1164 def get_img_data_by_type(self, font_family, img_type):
1165 default_user = """
1165 default_user = """
1166 <svg xmlns="http://www.w3.org/2000/svg"
1166 <svg xmlns="http://www.w3.org/2000/svg"
1167 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1167 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1168 viewBox="-15 -10 439.165 429.164"
1168 viewBox="-15 -10 439.165 429.164"
1169
1169
1170 xml:space="preserve"
1170 xml:space="preserve"
1171 style="background:{background};" >
1171 style="background:{background};" >
1172
1172
1173 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1173 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1174 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1174 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1175 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1175 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1176 168.596,153.916,216.671,
1176 168.596,153.916,216.671,
1177 204.583,216.671z" fill="{text_color}"/>
1177 204.583,216.671z" fill="{text_color}"/>
1178 <path d="M407.164,374.717L360.88,
1178 <path d="M407.164,374.717L360.88,
1179 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1179 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1180 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1180 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1181 15.366-44.203,23.488-69.076,23.488c-24.877,
1181 15.366-44.203,23.488-69.076,23.488c-24.877,
1182 0-48.762-8.122-69.078-23.488
1182 0-48.762-8.122-69.078-23.488
1183 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1183 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1184 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1184 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1185 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1185 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1186 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1186 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1187 19.402-10.527 C409.699,390.129,
1187 19.402-10.527 C409.699,390.129,
1188 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1188 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1189 </svg>""".format(
1189 </svg>""".format(
1190 size=self.size,
1190 size=self.size,
1191 background='#979797', # @grey4
1191 background='#979797', # @grey4
1192 text_color=self.text_color,
1192 text_color=self.text_color,
1193 font_family=font_family)
1193 font_family=font_family)
1194
1194
1195 return {
1195 return {
1196 "default_user": default_user
1196 "default_user": default_user
1197 }[img_type]
1197 }[img_type]
1198
1198
1199 def get_img_data(self, svg_type=None):
1199 def get_img_data(self, svg_type=None):
1200 """
1200 """
1201 generates the svg metadata for image
1201 generates the svg metadata for image
1202 """
1202 """
1203 fonts = [
1203 fonts = [
1204 '-apple-system',
1204 '-apple-system',
1205 'BlinkMacSystemFont',
1205 'BlinkMacSystemFont',
1206 'Segoe UI',
1206 'Segoe UI',
1207 'Roboto',
1207 'Roboto',
1208 'Oxygen-Sans',
1208 'Oxygen-Sans',
1209 'Ubuntu',
1209 'Ubuntu',
1210 'Cantarell',
1210 'Cantarell',
1211 'Helvetica Neue',
1211 'Helvetica Neue',
1212 'sans-serif'
1212 'sans-serif'
1213 ]
1213 ]
1214 font_family = ','.join(fonts)
1214 font_family = ','.join(fonts)
1215 if svg_type:
1215 if svg_type:
1216 return self.get_img_data_by_type(font_family, svg_type)
1216 return self.get_img_data_by_type(font_family, svg_type)
1217
1217
1218 initials = self.get_initials()
1218 initials = self.get_initials()
1219 img_data = """
1219 img_data = """
1220 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1220 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1221 width="{size}" height="{size}"
1221 width="{size}" height="{size}"
1222 style="width: 100%; height: 100%; background-color: {background}"
1222 style="width: 100%; height: 100%; background-color: {background}"
1223 viewBox="0 0 {size} {size}">
1223 viewBox="0 0 {size} {size}">
1224 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1224 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1225 pointer-events="auto" fill="{text_color}"
1225 pointer-events="auto" fill="{text_color}"
1226 font-family="{font_family}"
1226 font-family="{font_family}"
1227 style="font-weight: 400; font-size: {f_size}px;">{text}
1227 style="font-weight: 400; font-size: {f_size}px;">{text}
1228 </text>
1228 </text>
1229 </svg>""".format(
1229 </svg>""".format(
1230 size=self.size,
1230 size=self.size,
1231 f_size=self.size/2.05, # scale the text inside the box nicely
1231 f_size=self.size/2.05, # scale the text inside the box nicely
1232 background=self.background,
1232 background=self.background,
1233 text_color=self.text_color,
1233 text_color=self.text_color,
1234 text=initials.upper(),
1234 text=initials.upper(),
1235 font_family=font_family)
1235 font_family=font_family)
1236
1236
1237 return img_data
1237 return img_data
1238
1238
1239 def generate_svg(self, svg_type=None):
1239 def generate_svg(self, svg_type=None):
1240 img_data = self.get_img_data(svg_type)
1240 img_data = self.get_img_data(svg_type)
1241 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1241 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1242
1242
1243
1243
1244 def initials_gravatar(email_address, first_name, last_name, size=30):
1244 def initials_gravatar(email_address, first_name, last_name, size=30):
1245 svg_type = None
1245 svg_type = None
1246 if email_address == User.DEFAULT_USER_EMAIL:
1246 if email_address == User.DEFAULT_USER_EMAIL:
1247 svg_type = 'default_user'
1247 svg_type = 'default_user'
1248 klass = InitialsGravatar(email_address, first_name, last_name, size)
1248 klass = InitialsGravatar(email_address, first_name, last_name, size)
1249 return klass.generate_svg(svg_type=svg_type)
1249 return klass.generate_svg(svg_type=svg_type)
1250
1250
1251
1251
1252 def gravatar_url(email_address, size=30, request=None):
1252 def gravatar_url(email_address, size=30, request=None):
1253 request = get_current_request()
1253 request = get_current_request()
1254 _use_gravatar = request.call_context.visual.use_gravatar
1254 _use_gravatar = request.call_context.visual.use_gravatar
1255 _gravatar_url = request.call_context.visual.gravatar_url
1255 _gravatar_url = request.call_context.visual.gravatar_url
1256
1256
1257 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1257 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1258
1258
1259 email_address = email_address or User.DEFAULT_USER_EMAIL
1259 email_address = email_address or User.DEFAULT_USER_EMAIL
1260 if isinstance(email_address, unicode):
1260 if isinstance(email_address, unicode):
1261 # hashlib crashes on unicode items
1261 # hashlib crashes on unicode items
1262 email_address = safe_str(email_address)
1262 email_address = safe_str(email_address)
1263
1263
1264 # empty email or default user
1264 # empty email or default user
1265 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1265 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1266 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1266 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1267
1267
1268 if _use_gravatar:
1268 if _use_gravatar:
1269 # TODO: Disuse pyramid thread locals. Think about another solution to
1269 # TODO: Disuse pyramid thread locals. Think about another solution to
1270 # get the host and schema here.
1270 # get the host and schema here.
1271 request = get_current_request()
1271 request = get_current_request()
1272 tmpl = safe_str(_gravatar_url)
1272 tmpl = safe_str(_gravatar_url)
1273 tmpl = tmpl.replace('{email}', email_address)\
1273 tmpl = tmpl.replace('{email}', email_address)\
1274 .replace('{md5email}', md5_safe(email_address.lower())) \
1274 .replace('{md5email}', md5_safe(email_address.lower())) \
1275 .replace('{netloc}', request.host)\
1275 .replace('{netloc}', request.host)\
1276 .replace('{scheme}', request.scheme)\
1276 .replace('{scheme}', request.scheme)\
1277 .replace('{size}', safe_str(size))
1277 .replace('{size}', safe_str(size))
1278 return tmpl
1278 return tmpl
1279 else:
1279 else:
1280 return initials_gravatar(email_address, '', '', size=size)
1280 return initials_gravatar(email_address, '', '', size=size)
1281
1281
1282
1282
1283 class Page(_Page):
1283 class Page(_Page):
1284 """
1284 """
1285 Custom pager to match rendering style with paginator
1285 Custom pager to match rendering style with paginator
1286 """
1286 """
1287
1287
1288 def _get_pos(self, cur_page, max_page, items):
1288 def _get_pos(self, cur_page, max_page, items):
1289 edge = (items / 2) + 1
1289 edge = (items / 2) + 1
1290 if (cur_page <= edge):
1290 if (cur_page <= edge):
1291 radius = max(items / 2, items - cur_page)
1291 radius = max(items / 2, items - cur_page)
1292 elif (max_page - cur_page) < edge:
1292 elif (max_page - cur_page) < edge:
1293 radius = (items - 1) - (max_page - cur_page)
1293 radius = (items - 1) - (max_page - cur_page)
1294 else:
1294 else:
1295 radius = items / 2
1295 radius = items / 2
1296
1296
1297 left = max(1, (cur_page - (radius)))
1297 left = max(1, (cur_page - (radius)))
1298 right = min(max_page, cur_page + (radius))
1298 right = min(max_page, cur_page + (radius))
1299 return left, cur_page, right
1299 return left, cur_page, right
1300
1300
1301 def _range(self, regexp_match):
1301 def _range(self, regexp_match):
1302 """
1302 """
1303 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1303 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1304
1304
1305 Arguments:
1305 Arguments:
1306
1306
1307 regexp_match
1307 regexp_match
1308 A "re" (regular expressions) match object containing the
1308 A "re" (regular expressions) match object containing the
1309 radius of linked pages around the current page in
1309 radius of linked pages around the current page in
1310 regexp_match.group(1) as a string
1310 regexp_match.group(1) as a string
1311
1311
1312 This function is supposed to be called as a callable in
1312 This function is supposed to be called as a callable in
1313 re.sub.
1313 re.sub.
1314
1314
1315 """
1315 """
1316 radius = int(regexp_match.group(1))
1316 radius = int(regexp_match.group(1))
1317
1317
1318 # Compute the first and last page number within the radius
1318 # Compute the first and last page number within the radius
1319 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1319 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1320 # -> leftmost_page = 5
1320 # -> leftmost_page = 5
1321 # -> rightmost_page = 9
1321 # -> rightmost_page = 9
1322 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1322 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1323 self.last_page,
1323 self.last_page,
1324 (radius * 2) + 1)
1324 (radius * 2) + 1)
1325 nav_items = []
1325 nav_items = []
1326
1326
1327 # Create a link to the first page (unless we are on the first page
1327 # Create a link to the first page (unless we are on the first page
1328 # or there would be no need to insert '..' spacers)
1328 # or there would be no need to insert '..' spacers)
1329 if self.page != self.first_page and self.first_page < leftmost_page:
1329 if self.page != self.first_page and self.first_page < leftmost_page:
1330 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1330 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1331
1331
1332 # Insert dots if there are pages between the first page
1332 # Insert dots if there are pages between the first page
1333 # and the currently displayed page range
1333 # and the currently displayed page range
1334 if leftmost_page - self.first_page > 1:
1334 if leftmost_page - self.first_page > 1:
1335 # Wrap in a SPAN tag if nolink_attr is set
1335 # Wrap in a SPAN tag if nolink_attr is set
1336 text = '..'
1336 text = '..'
1337 if self.dotdot_attr:
1337 if self.dotdot_attr:
1338 text = HTML.span(c=text, **self.dotdot_attr)
1338 text = HTML.span(c=text, **self.dotdot_attr)
1339 nav_items.append(text)
1339 nav_items.append(text)
1340
1340
1341 for thispage in xrange(leftmost_page, rightmost_page + 1):
1341 for thispage in xrange(leftmost_page, rightmost_page + 1):
1342 # Hilight the current page number and do not use a link
1342 # Hilight the current page number and do not use a link
1343 if thispage == self.page:
1343 if thispage == self.page:
1344 text = '%s' % (thispage,)
1344 text = '%s' % (thispage,)
1345 # Wrap in a SPAN tag if nolink_attr is set
1345 # Wrap in a SPAN tag if nolink_attr is set
1346 if self.curpage_attr:
1346 if self.curpage_attr:
1347 text = HTML.span(c=text, **self.curpage_attr)
1347 text = HTML.span(c=text, **self.curpage_attr)
1348 nav_items.append(text)
1348 nav_items.append(text)
1349 # Otherwise create just a link to that page
1349 # Otherwise create just a link to that page
1350 else:
1350 else:
1351 text = '%s' % (thispage,)
1351 text = '%s' % (thispage,)
1352 nav_items.append(self._pagerlink(thispage, text))
1352 nav_items.append(self._pagerlink(thispage, text))
1353
1353
1354 # Insert dots if there are pages between the displayed
1354 # Insert dots if there are pages between the displayed
1355 # page numbers and the end of the page range
1355 # page numbers and the end of the page range
1356 if self.last_page - rightmost_page > 1:
1356 if self.last_page - rightmost_page > 1:
1357 text = '..'
1357 text = '..'
1358 # Wrap in a SPAN tag if nolink_attr is set
1358 # Wrap in a SPAN tag if nolink_attr is set
1359 if self.dotdot_attr:
1359 if self.dotdot_attr:
1360 text = HTML.span(c=text, **self.dotdot_attr)
1360 text = HTML.span(c=text, **self.dotdot_attr)
1361 nav_items.append(text)
1361 nav_items.append(text)
1362
1362
1363 # Create a link to the very last page (unless we are on the last
1363 # Create a link to the very last page (unless we are on the last
1364 # page or there would be no need to insert '..' spacers)
1364 # page or there would be no need to insert '..' spacers)
1365 if self.page != self.last_page and rightmost_page < self.last_page:
1365 if self.page != self.last_page and rightmost_page < self.last_page:
1366 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1366 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1367
1367
1368 ## prerender links
1368 ## prerender links
1369 #_page_link = url.current()
1369 #_page_link = url.current()
1370 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1372 return self.separator.join(nav_items)
1372 return self.separator.join(nav_items)
1373
1373
1374 def pager(self, format='~2~', page_param='page', partial_param='partial',
1374 def pager(self, format='~2~', page_param='page', partial_param='partial',
1375 show_if_single_page=False, separator=' ', onclick=None,
1375 show_if_single_page=False, separator=' ', onclick=None,
1376 symbol_first='<<', symbol_last='>>',
1376 symbol_first='<<', symbol_last='>>',
1377 symbol_previous='<', symbol_next='>',
1377 symbol_previous='<', symbol_next='>',
1378 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1378 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1379 curpage_attr={'class': 'pager_curpage'},
1379 curpage_attr={'class': 'pager_curpage'},
1380 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1380 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1381
1381
1382 self.curpage_attr = curpage_attr
1382 self.curpage_attr = curpage_attr
1383 self.separator = separator
1383 self.separator = separator
1384 self.pager_kwargs = kwargs
1384 self.pager_kwargs = kwargs
1385 self.page_param = page_param
1385 self.page_param = page_param
1386 self.partial_param = partial_param
1386 self.partial_param = partial_param
1387 self.onclick = onclick
1387 self.onclick = onclick
1388 self.link_attr = link_attr
1388 self.link_attr = link_attr
1389 self.dotdot_attr = dotdot_attr
1389 self.dotdot_attr = dotdot_attr
1390
1390
1391 # Don't show navigator if there is no more than one page
1391 # Don't show navigator if there is no more than one page
1392 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1392 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1393 return ''
1393 return ''
1394
1394
1395 from string import Template
1395 from string import Template
1396 # Replace ~...~ in token format by range of pages
1396 # Replace ~...~ in token format by range of pages
1397 result = re.sub(r'~(\d+)~', self._range, format)
1397 result = re.sub(r'~(\d+)~', self._range, format)
1398
1398
1399 # Interpolate '%' variables
1399 # Interpolate '%' variables
1400 result = Template(result).safe_substitute({
1400 result = Template(result).safe_substitute({
1401 'first_page': self.first_page,
1401 'first_page': self.first_page,
1402 'last_page': self.last_page,
1402 'last_page': self.last_page,
1403 'page': self.page,
1403 'page': self.page,
1404 'page_count': self.page_count,
1404 'page_count': self.page_count,
1405 'items_per_page': self.items_per_page,
1405 'items_per_page': self.items_per_page,
1406 'first_item': self.first_item,
1406 'first_item': self.first_item,
1407 'last_item': self.last_item,
1407 'last_item': self.last_item,
1408 'item_count': self.item_count,
1408 'item_count': self.item_count,
1409 'link_first': self.page > self.first_page and \
1409 'link_first': self.page > self.first_page and \
1410 self._pagerlink(self.first_page, symbol_first) or '',
1410 self._pagerlink(self.first_page, symbol_first) or '',
1411 'link_last': self.page < self.last_page and \
1411 'link_last': self.page < self.last_page and \
1412 self._pagerlink(self.last_page, symbol_last) or '',
1412 self._pagerlink(self.last_page, symbol_last) or '',
1413 'link_previous': self.previous_page and \
1413 'link_previous': self.previous_page and \
1414 self._pagerlink(self.previous_page, symbol_previous) \
1414 self._pagerlink(self.previous_page, symbol_previous) \
1415 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1415 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1416 'link_next': self.next_page and \
1416 'link_next': self.next_page and \
1417 self._pagerlink(self.next_page, symbol_next) \
1417 self._pagerlink(self.next_page, symbol_next) \
1418 or HTML.span(symbol_next, class_="pg-next disabled")
1418 or HTML.span(symbol_next, class_="pg-next disabled")
1419 })
1419 })
1420
1420
1421 return literal(result)
1421 return literal(result)
1422
1422
1423
1423
1424 #==============================================================================
1424 #==============================================================================
1425 # REPO PAGER, PAGER FOR REPOSITORY
1425 # REPO PAGER, PAGER FOR REPOSITORY
1426 #==============================================================================
1426 #==============================================================================
1427 class RepoPage(Page):
1427 class RepoPage(Page):
1428
1428
1429 def __init__(self, collection, page=1, items_per_page=20,
1429 def __init__(self, collection, page=1, items_per_page=20,
1430 item_count=None, url=None, **kwargs):
1430 item_count=None, url=None, **kwargs):
1431
1431
1432 """Create a "RepoPage" instance. special pager for paging
1432 """Create a "RepoPage" instance. special pager for paging
1433 repository
1433 repository
1434 """
1434 """
1435 self._url_generator = url
1435 self._url_generator = url
1436
1436
1437 # Safe the kwargs class-wide so they can be used in the pager() method
1437 # Safe the kwargs class-wide so they can be used in the pager() method
1438 self.kwargs = kwargs
1438 self.kwargs = kwargs
1439
1439
1440 # Save a reference to the collection
1440 # Save a reference to the collection
1441 self.original_collection = collection
1441 self.original_collection = collection
1442
1442
1443 self.collection = collection
1443 self.collection = collection
1444
1444
1445 # The self.page is the number of the current page.
1445 # The self.page is the number of the current page.
1446 # The first page has the number 1!
1446 # The first page has the number 1!
1447 try:
1447 try:
1448 self.page = int(page) # make it int() if we get it as a string
1448 self.page = int(page) # make it int() if we get it as a string
1449 except (ValueError, TypeError):
1449 except (ValueError, TypeError):
1450 self.page = 1
1450 self.page = 1
1451
1451
1452 self.items_per_page = items_per_page
1452 self.items_per_page = items_per_page
1453
1453
1454 # Unless the user tells us how many items the collections has
1454 # Unless the user tells us how many items the collections has
1455 # we calculate that ourselves.
1455 # we calculate that ourselves.
1456 if item_count is not None:
1456 if item_count is not None:
1457 self.item_count = item_count
1457 self.item_count = item_count
1458 else:
1458 else:
1459 self.item_count = len(self.collection)
1459 self.item_count = len(self.collection)
1460
1460
1461 # Compute the number of the first and last available page
1461 # Compute the number of the first and last available page
1462 if self.item_count > 0:
1462 if self.item_count > 0:
1463 self.first_page = 1
1463 self.first_page = 1
1464 self.page_count = int(math.ceil(float(self.item_count) /
1464 self.page_count = int(math.ceil(float(self.item_count) /
1465 self.items_per_page))
1465 self.items_per_page))
1466 self.last_page = self.first_page + self.page_count - 1
1466 self.last_page = self.first_page + self.page_count - 1
1467
1467
1468 # Make sure that the requested page number is the range of
1468 # Make sure that the requested page number is the range of
1469 # valid pages
1469 # valid pages
1470 if self.page > self.last_page:
1470 if self.page > self.last_page:
1471 self.page = self.last_page
1471 self.page = self.last_page
1472 elif self.page < self.first_page:
1472 elif self.page < self.first_page:
1473 self.page = self.first_page
1473 self.page = self.first_page
1474
1474
1475 # Note: the number of items on this page can be less than
1475 # Note: the number of items on this page can be less than
1476 # items_per_page if the last page is not full
1476 # items_per_page if the last page is not full
1477 self.first_item = max(0, (self.item_count) - (self.page *
1477 self.first_item = max(0, (self.item_count) - (self.page *
1478 items_per_page))
1478 items_per_page))
1479 self.last_item = ((self.item_count - 1) - items_per_page *
1479 self.last_item = ((self.item_count - 1) - items_per_page *
1480 (self.page - 1))
1480 (self.page - 1))
1481
1481
1482 self.items = list(self.collection[self.first_item:self.last_item + 1])
1482 self.items = list(self.collection[self.first_item:self.last_item + 1])
1483
1483
1484 # Links to previous and next page
1484 # Links to previous and next page
1485 if self.page > self.first_page:
1485 if self.page > self.first_page:
1486 self.previous_page = self.page - 1
1486 self.previous_page = self.page - 1
1487 else:
1487 else:
1488 self.previous_page = None
1488 self.previous_page = None
1489
1489
1490 if self.page < self.last_page:
1490 if self.page < self.last_page:
1491 self.next_page = self.page + 1
1491 self.next_page = self.page + 1
1492 else:
1492 else:
1493 self.next_page = None
1493 self.next_page = None
1494
1494
1495 # No items available
1495 # No items available
1496 else:
1496 else:
1497 self.first_page = None
1497 self.first_page = None
1498 self.page_count = 0
1498 self.page_count = 0
1499 self.last_page = None
1499 self.last_page = None
1500 self.first_item = None
1500 self.first_item = None
1501 self.last_item = None
1501 self.last_item = None
1502 self.previous_page = None
1502 self.previous_page = None
1503 self.next_page = None
1503 self.next_page = None
1504 self.items = []
1504 self.items = []
1505
1505
1506 # This is a subclass of the 'list' type. Initialise the list now.
1506 # This is a subclass of the 'list' type. Initialise the list now.
1507 list.__init__(self, reversed(self.items))
1507 list.__init__(self, reversed(self.items))
1508
1508
1509
1509
1510 def breadcrumb_repo_link(repo):
1510 def breadcrumb_repo_link(repo):
1511 """
1511 """
1512 Makes a breadcrumbs path link to repo
1512 Makes a breadcrumbs path link to repo
1513
1513
1514 ex::
1514 ex::
1515 group >> subgroup >> repo
1515 group >> subgroup >> repo
1516
1516
1517 :param repo: a Repository instance
1517 :param repo: a Repository instance
1518 """
1518 """
1519
1519
1520 path = [
1520 path = [
1521 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1521 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1522 title='last change:{}'.format(format_date(group.last_commit_change)))
1522 title='last change:{}'.format(format_date(group.last_commit_change)))
1523 for group in repo.groups_with_parents
1523 for group in repo.groups_with_parents
1524 ] + [
1524 ] + [
1525 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1525 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1526 title='last change:{}'.format(format_date(repo.last_commit_change)))
1526 title='last change:{}'.format(format_date(repo.last_commit_change)))
1527 ]
1527 ]
1528
1528
1529 return literal(' &raquo; '.join(path))
1529 return literal(' &raquo; '.join(path))
1530
1530
1531
1531
1532 def breadcrumb_repo_group_link(repo_group):
1532 def breadcrumb_repo_group_link(repo_group):
1533 """
1533 """
1534 Makes a breadcrumbs path link to repo
1534 Makes a breadcrumbs path link to repo
1535
1535
1536 ex::
1536 ex::
1537 group >> subgroup
1537 group >> subgroup
1538
1538
1539 :param repo_group: a Repository Group instance
1539 :param repo_group: a Repository Group instance
1540 """
1540 """
1541
1541
1542 path = [
1542 path = [
1543 link_to(group.name,
1543 link_to(group.name,
1544 route_path('repo_group_home', repo_group_name=group.group_name),
1544 route_path('repo_group_home', repo_group_name=group.group_name),
1545 title='last change:{}'.format(format_date(group.last_commit_change)))
1545 title='last change:{}'.format(format_date(group.last_commit_change)))
1546 for group in repo_group.parents
1546 for group in repo_group.parents
1547 ] + [
1547 ] + [
1548 link_to(repo_group.name,
1548 link_to(repo_group.name,
1549 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1549 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1550 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1550 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1551 ]
1551 ]
1552
1552
1553 return literal(' &raquo; '.join(path))
1553 return literal(' &raquo; '.join(path))
1554
1554
1555
1555
1556 def format_byte_size_binary(file_size):
1556 def format_byte_size_binary(file_size):
1557 """
1557 """
1558 Formats file/folder sizes to standard.
1558 Formats file/folder sizes to standard.
1559 """
1559 """
1560 if file_size is None:
1560 if file_size is None:
1561 file_size = 0
1561 file_size = 0
1562
1562
1563 formatted_size = format_byte_size(file_size, binary=True)
1563 formatted_size = format_byte_size(file_size, binary=True)
1564 return formatted_size
1564 return formatted_size
1565
1565
1566
1566
1567 def urlify_text(text_, safe=True):
1567 def urlify_text(text_, safe=True):
1568 """
1568 """
1569 Extrac urls from text and make html links out of them
1569 Extrac urls from text and make html links out of them
1570
1570
1571 :param text_:
1571 :param text_:
1572 """
1572 """
1573
1573
1574 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1574 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1575 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1575 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1576
1576
1577 def url_func(match_obj):
1577 def url_func(match_obj):
1578 url_full = match_obj.groups()[0]
1578 url_full = match_obj.groups()[0]
1579 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1579 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1580 _newtext = url_pat.sub(url_func, text_)
1580 _new_text = url_pat.sub(url_func, text_)
1581 if safe:
1581 if safe:
1582 return literal(_newtext)
1582 return literal(_new_text)
1583 return _newtext
1583 return _new_text
1584
1584
1585
1585
1586 def urlify_commits(text_, repository):
1586 def urlify_commits(text_, repo_name):
1587 """
1587 """
1588 Extract commit ids from text and make link from them
1588 Extract commit ids from text and make link from them
1589
1589
1590 :param text_:
1590 :param text_:
1591 :param repository: repo name to build the URL with
1591 :param repo_name: repo name to build the URL with
1592 """
1592 """
1593
1593
1594 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1594 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1595
1595
1596 def url_func(match_obj):
1596 def url_func(match_obj):
1597 commit_id = match_obj.groups()[1]
1597 commit_id = match_obj.groups()[1]
1598 pref = match_obj.groups()[0]
1598 pref = match_obj.groups()[0]
1599 suf = match_obj.groups()[2]
1599 suf = match_obj.groups()[2]
1600
1600
1601 tmpl = (
1601 tmpl = (
1602 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1602 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1603 '%(commit_id)s</a>%(suf)s'
1603 '%(commit_id)s</a>%(suf)s'
1604 )
1604 )
1605 return tmpl % {
1605 return tmpl % {
1606 'pref': pref,
1606 'pref': pref,
1607 'cls': 'revision-link',
1607 'cls': 'revision-link',
1608 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1608 'url': route_url(
1609 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1609 'commit_id': commit_id,
1610 'commit_id': commit_id,
1610 'suf': suf
1611 'suf': suf,
1612 'hovercard_alt': 'Commit: {}'.format(commit_id),
1613 'hovercard_url': route_url(
1614 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1611 }
1615 }
1612
1616
1613 newtext = URL_PAT.sub(url_func, text_)
1617 new_text = url_pat.sub(url_func, text_)
1614
1618
1615 return newtext
1619 return new_text
1616
1620
1617
1621
1618 def _process_url_func(match_obj, repo_name, uid, entry,
1622 def _process_url_func(match_obj, repo_name, uid, entry,
1619 return_raw_data=False, link_format='html'):
1623 return_raw_data=False, link_format='html'):
1620 pref = ''
1624 pref = ''
1621 if match_obj.group().startswith(' '):
1625 if match_obj.group().startswith(' '):
1622 pref = ' '
1626 pref = ' '
1623
1627
1624 issue_id = ''.join(match_obj.groups())
1628 issue_id = ''.join(match_obj.groups())
1625
1629
1626 if link_format == 'html':
1630 if link_format == 'html':
1627 tmpl = (
1631 tmpl = (
1628 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1632 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1629 '%(issue-prefix)s%(id-repr)s'
1633 '%(issue-prefix)s%(id-repr)s'
1630 '</a>')
1634 '</a>')
1631 elif link_format == 'html+hovercard':
1635 elif link_format == 'html+hovercard':
1632 tmpl = (
1636 tmpl = (
1633 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1637 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1634 '%(issue-prefix)s%(id-repr)s'
1638 '%(issue-prefix)s%(id-repr)s'
1635 '</a>')
1639 '</a>')
1636 elif link_format in ['rst', 'rst+hovercard']:
1640 elif link_format in ['rst', 'rst+hovercard']:
1637 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1641 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1638 elif link_format in ['markdown', 'markdown+hovercard']:
1642 elif link_format in ['markdown', 'markdown+hovercard']:
1639 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1643 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1640 else:
1644 else:
1641 raise ValueError('Bad link_format:{}'.format(link_format))
1645 raise ValueError('Bad link_format:{}'.format(link_format))
1642
1646
1643 (repo_name_cleaned,
1647 (repo_name_cleaned,
1644 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1648 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1645
1649
1646 # variables replacement
1650 # variables replacement
1647 named_vars = {
1651 named_vars = {
1648 'id': issue_id,
1652 'id': issue_id,
1649 'repo': repo_name,
1653 'repo': repo_name,
1650 'repo_name': repo_name_cleaned,
1654 'repo_name': repo_name_cleaned,
1651 'group_name': parent_group_name,
1655 'group_name': parent_group_name,
1652 # set dummy keys so we always have them
1656 # set dummy keys so we always have them
1653 'hostname': '',
1657 'hostname': '',
1654 'netloc': '',
1658 'netloc': '',
1655 'scheme': ''
1659 'scheme': ''
1656 }
1660 }
1657
1661
1658 request = get_current_request()
1662 request = get_current_request()
1659 if request:
1663 if request:
1660 # exposes, hostname, netloc, scheme
1664 # exposes, hostname, netloc, scheme
1661 host_data = get_host_info(request)
1665 host_data = get_host_info(request)
1662 named_vars.update(host_data)
1666 named_vars.update(host_data)
1663
1667
1664 # named regex variables
1668 # named regex variables
1665 named_vars.update(match_obj.groupdict())
1669 named_vars.update(match_obj.groupdict())
1666 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1670 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1667 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1671 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1668 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1672 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1669
1673
1670 def quote_cleaner(input_str):
1674 def quote_cleaner(input_str):
1671 """Remove quotes as it's HTML"""
1675 """Remove quotes as it's HTML"""
1672 return input_str.replace('"', '')
1676 return input_str.replace('"', '')
1673
1677
1674 data = {
1678 data = {
1675 'pref': pref,
1679 'pref': pref,
1676 'cls': quote_cleaner('issue-tracker-link'),
1680 'cls': quote_cleaner('issue-tracker-link'),
1677 'url': quote_cleaner(_url),
1681 'url': quote_cleaner(_url),
1678 'id-repr': issue_id,
1682 'id-repr': issue_id,
1679 'issue-prefix': entry['pref'],
1683 'issue-prefix': entry['pref'],
1680 'serv': entry['url'],
1684 'serv': entry['url'],
1681 'title': desc,
1685 'title': desc,
1682 'hovercard_url': hovercard_url
1686 'hovercard_url': hovercard_url
1683 }
1687 }
1684
1688
1685 if return_raw_data:
1689 if return_raw_data:
1686 return {
1690 return {
1687 'id': issue_id,
1691 'id': issue_id,
1688 'url': _url
1692 'url': _url
1689 }
1693 }
1690 return tmpl % data
1694 return tmpl % data
1691
1695
1692
1696
1693 def get_active_pattern_entries(repo_name):
1697 def get_active_pattern_entries(repo_name):
1694 repo = None
1698 repo = None
1695 if repo_name:
1699 if repo_name:
1696 # Retrieving repo_name to avoid invalid repo_name to explode on
1700 # Retrieving repo_name to avoid invalid repo_name to explode on
1697 # IssueTrackerSettingsModel but still passing invalid name further down
1701 # IssueTrackerSettingsModel but still passing invalid name further down
1698 repo = Repository.get_by_repo_name(repo_name, cache=True)
1702 repo = Repository.get_by_repo_name(repo_name, cache=True)
1699
1703
1700 settings_model = IssueTrackerSettingsModel(repo=repo)
1704 settings_model = IssueTrackerSettingsModel(repo=repo)
1701 active_entries = settings_model.get_settings(cache=True)
1705 active_entries = settings_model.get_settings(cache=True)
1702 return active_entries
1706 return active_entries
1703
1707
1704
1708
1705 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1709 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1706
1710
1707 allowed_formats = ['html', 'rst', 'markdown',
1711 allowed_formats = ['html', 'rst', 'markdown',
1708 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1712 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1709 if link_format not in allowed_formats:
1713 if link_format not in allowed_formats:
1710 raise ValueError('Link format can be only one of:{} got {}'.format(
1714 raise ValueError('Link format can be only one of:{} got {}'.format(
1711 allowed_formats, link_format))
1715 allowed_formats, link_format))
1712
1716
1713 active_entries = active_entries or get_active_pattern_entries(repo_name)
1717 active_entries = active_entries or get_active_pattern_entries(repo_name)
1714 issues_data = []
1718 issues_data = []
1715 new_text = text_string
1719 new_text = text_string
1716
1720
1717 log.debug('Got %s entries to process', len(active_entries))
1721 log.debug('Got %s entries to process', len(active_entries))
1718 for uid, entry in active_entries.items():
1722 for uid, entry in active_entries.items():
1719 log.debug('found issue tracker entry with uid %s', uid)
1723 log.debug('found issue tracker entry with uid %s', uid)
1720
1724
1721 if not (entry['pat'] and entry['url']):
1725 if not (entry['pat'] and entry['url']):
1722 log.debug('skipping due to missing data')
1726 log.debug('skipping due to missing data')
1723 continue
1727 continue
1724
1728
1725 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1729 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1726 uid, entry['pat'], entry['url'], entry['pref'])
1730 uid, entry['pat'], entry['url'], entry['pref'])
1727
1731
1728 try:
1732 try:
1729 pattern = re.compile(r'%s' % entry['pat'])
1733 pattern = re.compile(r'%s' % entry['pat'])
1730 except re.error:
1734 except re.error:
1731 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1735 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1732 continue
1736 continue
1733
1737
1734 data_func = partial(
1738 data_func = partial(
1735 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1739 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1736 return_raw_data=True)
1740 return_raw_data=True)
1737
1741
1738 for match_obj in pattern.finditer(text_string):
1742 for match_obj in pattern.finditer(text_string):
1739 issues_data.append(data_func(match_obj))
1743 issues_data.append(data_func(match_obj))
1740
1744
1741 url_func = partial(
1745 url_func = partial(
1742 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1746 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1743 link_format=link_format)
1747 link_format=link_format)
1744
1748
1745 new_text = pattern.sub(url_func, new_text)
1749 new_text = pattern.sub(url_func, new_text)
1746 log.debug('processed prefix:uid `%s`', uid)
1750 log.debug('processed prefix:uid `%s`', uid)
1747
1751
1748 # finally use global replace, eg !123 -> pr-link, those will not catch
1752 # finally use global replace, eg !123 -> pr-link, those will not catch
1749 # if already similar pattern exists
1753 # if already similar pattern exists
1750 server_url = '${scheme}://${netloc}'
1754 server_url = '${scheme}://${netloc}'
1751 pr_entry = {
1755 pr_entry = {
1752 'pref': '!',
1756 'pref': '!',
1753 'url': server_url + '/_admin/pull-requests/${id}',
1757 'url': server_url + '/_admin/pull-requests/${id}',
1754 'desc': 'Pull Request !${id}',
1758 'desc': 'Pull Request !${id}',
1755 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1759 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1756 }
1760 }
1757 pr_url_func = partial(
1761 pr_url_func = partial(
1758 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1762 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1759 link_format=link_format+'+hovercard')
1763 link_format=link_format+'+hovercard')
1760 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1764 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1761 log.debug('processed !pr pattern')
1765 log.debug('processed !pr pattern')
1762
1766
1763 return new_text, issues_data
1767 return new_text, issues_data
1764
1768
1765
1769
1766 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1770 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1767 """
1771 """
1768 Parses given text message and makes proper links.
1772 Parses given text message and makes proper links.
1769 issues are linked to given issue-server, and rest is a commit link
1773 issues are linked to given issue-server, and rest is a commit link
1770
1771 :param commit_text:
1772 :param repository:
1773 """
1774 """
1774 def escaper(_text):
1775 def escaper(_text):
1775 return _text.replace('<', '&lt;').replace('>', '&gt;')
1776 return _text.replace('<', '&lt;').replace('>', '&gt;')
1776
1777
1777 new_text = escaper(commit_text)
1778 new_text = escaper(commit_text)
1778
1779
1779 # extract http/https links and make them real urls
1780 # extract http/https links and make them real urls
1780 new_text = urlify_text(new_text, safe=False)
1781 new_text = urlify_text(new_text, safe=False)
1781
1782
1782 # urlify commits - extract commit ids and make link out of them, if we have
1783 # urlify commits - extract commit ids and make link out of them, if we have
1783 # the scope of repository present.
1784 # the scope of repository present.
1784 if repository:
1785 if repository:
1785 new_text = urlify_commits(new_text, repository)
1786 new_text = urlify_commits(new_text, repository)
1786
1787
1787 # process issue tracker patterns
1788 # process issue tracker patterns
1788 new_text, issues = process_patterns(new_text, repository or '',
1789 new_text, issues = process_patterns(new_text, repository or '',
1789 active_entries=active_pattern_entries)
1790 active_entries=active_pattern_entries)
1790
1791
1791 return literal(new_text)
1792 return literal(new_text)
1792
1793
1793
1794
1794 def render_binary(repo_name, file_obj):
1795 def render_binary(repo_name, file_obj):
1795 """
1796 """
1796 Choose how to render a binary file
1797 Choose how to render a binary file
1797 """
1798 """
1798
1799
1799 filename = file_obj.name
1800 filename = file_obj.name
1800
1801
1801 # images
1802 # images
1802 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1803 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1803 if fnmatch.fnmatch(filename, pat=ext):
1804 if fnmatch.fnmatch(filename, pat=ext):
1804 alt = escape(filename)
1805 alt = escape(filename)
1805 src = route_path(
1806 src = route_path(
1806 'repo_file_raw', repo_name=repo_name,
1807 'repo_file_raw', repo_name=repo_name,
1807 commit_id=file_obj.commit.raw_id,
1808 commit_id=file_obj.commit.raw_id,
1808 f_path=file_obj.path)
1809 f_path=file_obj.path)
1809 return literal(
1810 return literal(
1810 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1811 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1811
1812
1812
1813
1813 def renderer_from_filename(filename, exclude=None):
1814 def renderer_from_filename(filename, exclude=None):
1814 """
1815 """
1815 choose a renderer based on filename, this works only for text based files
1816 choose a renderer based on filename, this works only for text based files
1816 """
1817 """
1817
1818
1818 # ipython
1819 # ipython
1819 for ext in ['*.ipynb']:
1820 for ext in ['*.ipynb']:
1820 if fnmatch.fnmatch(filename, pat=ext):
1821 if fnmatch.fnmatch(filename, pat=ext):
1821 return 'jupyter'
1822 return 'jupyter'
1822
1823
1823 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1824 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1824 if is_markup:
1825 if is_markup:
1825 return is_markup
1826 return is_markup
1826 return None
1827 return None
1827
1828
1828
1829
1829 def render(source, renderer='rst', mentions=False, relative_urls=None,
1830 def render(source, renderer='rst', mentions=False, relative_urls=None,
1830 repo_name=None):
1831 repo_name=None):
1831
1832
1832 def maybe_convert_relative_links(html_source):
1833 def maybe_convert_relative_links(html_source):
1833 if relative_urls:
1834 if relative_urls:
1834 return relative_links(html_source, relative_urls)
1835 return relative_links(html_source, relative_urls)
1835 return html_source
1836 return html_source
1836
1837
1837 if renderer == 'plain':
1838 if renderer == 'plain':
1838 return literal(
1839 return literal(
1839 MarkupRenderer.plain(source, leading_newline=False))
1840 MarkupRenderer.plain(source, leading_newline=False))
1840
1841
1841 elif renderer == 'rst':
1842 elif renderer == 'rst':
1842 if repo_name:
1843 if repo_name:
1843 # process patterns on comments if we pass in repo name
1844 # process patterns on comments if we pass in repo name
1844 source, issues = process_patterns(
1845 source, issues = process_patterns(
1845 source, repo_name, link_format='rst')
1846 source, repo_name, link_format='rst')
1846
1847
1847 return literal(
1848 return literal(
1848 '<div class="rst-block">%s</div>' %
1849 '<div class="rst-block">%s</div>' %
1849 maybe_convert_relative_links(
1850 maybe_convert_relative_links(
1850 MarkupRenderer.rst(source, mentions=mentions)))
1851 MarkupRenderer.rst(source, mentions=mentions)))
1851
1852
1852 elif renderer == 'markdown':
1853 elif renderer == 'markdown':
1853 if repo_name:
1854 if repo_name:
1854 # process patterns on comments if we pass in repo name
1855 # process patterns on comments if we pass in repo name
1855 source, issues = process_patterns(
1856 source, issues = process_patterns(
1856 source, repo_name, link_format='markdown')
1857 source, repo_name, link_format='markdown')
1857
1858
1858 return literal(
1859 return literal(
1859 '<div class="markdown-block">%s</div>' %
1860 '<div class="markdown-block">%s</div>' %
1860 maybe_convert_relative_links(
1861 maybe_convert_relative_links(
1861 MarkupRenderer.markdown(source, flavored=True,
1862 MarkupRenderer.markdown(source, flavored=True,
1862 mentions=mentions)))
1863 mentions=mentions)))
1863
1864
1864 elif renderer == 'jupyter':
1865 elif renderer == 'jupyter':
1865 return literal(
1866 return literal(
1866 '<div class="ipynb">%s</div>' %
1867 '<div class="ipynb">%s</div>' %
1867 maybe_convert_relative_links(
1868 maybe_convert_relative_links(
1868 MarkupRenderer.jupyter(source)))
1869 MarkupRenderer.jupyter(source)))
1869
1870
1870 # None means just show the file-source
1871 # None means just show the file-source
1871 return None
1872 return None
1872
1873
1873
1874
1874 def commit_status(repo, commit_id):
1875 def commit_status(repo, commit_id):
1875 return ChangesetStatusModel().get_status(repo, commit_id)
1876 return ChangesetStatusModel().get_status(repo, commit_id)
1876
1877
1877
1878
1878 def commit_status_lbl(commit_status):
1879 def commit_status_lbl(commit_status):
1879 return dict(ChangesetStatus.STATUSES).get(commit_status)
1880 return dict(ChangesetStatus.STATUSES).get(commit_status)
1880
1881
1881
1882
1882 def commit_time(repo_name, commit_id):
1883 def commit_time(repo_name, commit_id):
1883 repo = Repository.get_by_repo_name(repo_name)
1884 repo = Repository.get_by_repo_name(repo_name)
1884 commit = repo.get_commit(commit_id=commit_id)
1885 commit = repo.get_commit(commit_id=commit_id)
1885 return commit.date
1886 return commit.date
1886
1887
1887
1888
1888 def get_permission_name(key):
1889 def get_permission_name(key):
1889 return dict(Permission.PERMS).get(key)
1890 return dict(Permission.PERMS).get(key)
1890
1891
1891
1892
1892 def journal_filter_help(request):
1893 def journal_filter_help(request):
1893 _ = request.translate
1894 _ = request.translate
1894 from rhodecode.lib.audit_logger import ACTIONS
1895 from rhodecode.lib.audit_logger import ACTIONS
1895 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1896 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1896
1897
1897 return _(
1898 return _(
1898 'Example filter terms:\n' +
1899 'Example filter terms:\n' +
1899 ' repository:vcs\n' +
1900 ' repository:vcs\n' +
1900 ' username:marcin\n' +
1901 ' username:marcin\n' +
1901 ' username:(NOT marcin)\n' +
1902 ' username:(NOT marcin)\n' +
1902 ' action:*push*\n' +
1903 ' action:*push*\n' +
1903 ' ip:127.0.0.1\n' +
1904 ' ip:127.0.0.1\n' +
1904 ' date:20120101\n' +
1905 ' date:20120101\n' +
1905 ' date:[20120101100000 TO 20120102]\n' +
1906 ' date:[20120101100000 TO 20120102]\n' +
1906 '\n' +
1907 '\n' +
1907 'Actions: {actions}\n' +
1908 'Actions: {actions}\n' +
1908 '\n' +
1909 '\n' +
1909 'Generate wildcards using \'*\' character:\n' +
1910 'Generate wildcards using \'*\' character:\n' +
1910 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1911 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1911 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1912 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1912 '\n' +
1913 '\n' +
1913 'Optional AND / OR operators in queries\n' +
1914 'Optional AND / OR operators in queries\n' +
1914 ' "repository:vcs OR repository:test"\n' +
1915 ' "repository:vcs OR repository:test"\n' +
1915 ' "username:test AND repository:test*"\n'
1916 ' "username:test AND repository:test*"\n'
1916 ).format(actions=actions)
1917 ).format(actions=actions)
1917
1918
1918
1919
1919 def not_mapped_error(repo_name):
1920 def not_mapped_error(repo_name):
1920 from rhodecode.translation import _
1921 from rhodecode.translation import _
1921 flash(_('%s repository is not mapped to db perhaps'
1922 flash(_('%s repository is not mapped to db perhaps'
1922 ' it was created or renamed from the filesystem'
1923 ' it was created or renamed from the filesystem'
1923 ' please run the application again'
1924 ' please run the application again'
1924 ' in order to rescan repositories') % repo_name, category='error')
1925 ' in order to rescan repositories') % repo_name, category='error')
1925
1926
1926
1927
1927 def ip_range(ip_addr):
1928 def ip_range(ip_addr):
1928 from rhodecode.model.db import UserIpMap
1929 from rhodecode.model.db import UserIpMap
1929 s, e = UserIpMap._get_ip_range(ip_addr)
1930 s, e = UserIpMap._get_ip_range(ip_addr)
1930 return '%s - %s' % (s, e)
1931 return '%s - %s' % (s, e)
1931
1932
1932
1933
1933 def form(url, method='post', needs_csrf_token=True, **attrs):
1934 def form(url, method='post', needs_csrf_token=True, **attrs):
1934 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1935 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1935 if method.lower() != 'get' and needs_csrf_token:
1936 if method.lower() != 'get' and needs_csrf_token:
1936 raise Exception(
1937 raise Exception(
1937 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1938 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1938 'CSRF token. If the endpoint does not require such token you can ' +
1939 'CSRF token. If the endpoint does not require such token you can ' +
1939 'explicitly set the parameter needs_csrf_token to false.')
1940 'explicitly set the parameter needs_csrf_token to false.')
1940
1941
1941 return wh_form(url, method=method, **attrs)
1942 return wh_form(url, method=method, **attrs)
1942
1943
1943
1944
1944 def secure_form(form_url, method="POST", multipart=False, **attrs):
1945 def secure_form(form_url, method="POST", multipart=False, **attrs):
1945 """Start a form tag that points the action to an url. This
1946 """Start a form tag that points the action to an url. This
1946 form tag will also include the hidden field containing
1947 form tag will also include the hidden field containing
1947 the auth token.
1948 the auth token.
1948
1949
1949 The url options should be given either as a string, or as a
1950 The url options should be given either as a string, or as a
1950 ``url()`` function. The method for the form defaults to POST.
1951 ``url()`` function. The method for the form defaults to POST.
1951
1952
1952 Options:
1953 Options:
1953
1954
1954 ``multipart``
1955 ``multipart``
1955 If set to True, the enctype is set to "multipart/form-data".
1956 If set to True, the enctype is set to "multipart/form-data".
1956 ``method``
1957 ``method``
1957 The method to use when submitting the form, usually either
1958 The method to use when submitting the form, usually either
1958 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1959 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1959 hidden input with name _method is added to simulate the verb
1960 hidden input with name _method is added to simulate the verb
1960 over POST.
1961 over POST.
1961
1962
1962 """
1963 """
1963 from webhelpers.pylonslib.secure_form import insecure_form
1964 from webhelpers.pylonslib.secure_form import insecure_form
1964
1965
1965 if 'request' in attrs:
1966 if 'request' in attrs:
1966 session = attrs['request'].session
1967 session = attrs['request'].session
1967 del attrs['request']
1968 del attrs['request']
1968 else:
1969 else:
1969 raise ValueError(
1970 raise ValueError(
1970 'Calling this form requires request= to be passed as argument')
1971 'Calling this form requires request= to be passed as argument')
1971
1972
1972 form = insecure_form(form_url, method, multipart, **attrs)
1973 form = insecure_form(form_url, method, multipart, **attrs)
1973 token = literal(
1974 token = literal(
1974 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1975 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1975 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1976 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1976
1977
1977 return literal("%s\n%s" % (form, token))
1978 return literal("%s\n%s" % (form, token))
1978
1979
1979
1980
1980 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1981 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1981 select_html = select(name, selected, options, **attrs)
1982 select_html = select(name, selected, options, **attrs)
1982
1983
1983 select2 = """
1984 select2 = """
1984 <script>
1985 <script>
1985 $(document).ready(function() {
1986 $(document).ready(function() {
1986 $('#%s').select2({
1987 $('#%s').select2({
1987 containerCssClass: 'drop-menu %s',
1988 containerCssClass: 'drop-menu %s',
1988 dropdownCssClass: 'drop-menu-dropdown',
1989 dropdownCssClass: 'drop-menu-dropdown',
1989 dropdownAutoWidth: true%s
1990 dropdownAutoWidth: true%s
1990 });
1991 });
1991 });
1992 });
1992 </script>
1993 </script>
1993 """
1994 """
1994
1995
1995 filter_option = """,
1996 filter_option = """,
1996 minimumResultsForSearch: -1
1997 minimumResultsForSearch: -1
1997 """
1998 """
1998 input_id = attrs.get('id') or name
1999 input_id = attrs.get('id') or name
1999 extra_classes = ' '.join(attrs.pop('extra_classes', []))
2000 extra_classes = ' '.join(attrs.pop('extra_classes', []))
2000 filter_enabled = "" if enable_filter else filter_option
2001 filter_enabled = "" if enable_filter else filter_option
2001 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
2002 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
2002
2003
2003 return literal(select_html+select_script)
2004 return literal(select_html+select_script)
2004
2005
2005
2006
2006 def get_visual_attr(tmpl_context_var, attr_name):
2007 def get_visual_attr(tmpl_context_var, attr_name):
2007 """
2008 """
2008 A safe way to get a variable from visual variable of template context
2009 A safe way to get a variable from visual variable of template context
2009
2010
2010 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2011 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2011 :param attr_name: name of the attribute we fetch from the c.visual
2012 :param attr_name: name of the attribute we fetch from the c.visual
2012 """
2013 """
2013 visual = getattr(tmpl_context_var, 'visual', None)
2014 visual = getattr(tmpl_context_var, 'visual', None)
2014 if not visual:
2015 if not visual:
2015 return
2016 return
2016 else:
2017 else:
2017 return getattr(visual, attr_name, None)
2018 return getattr(visual, attr_name, None)
2018
2019
2019
2020
2020 def get_last_path_part(file_node):
2021 def get_last_path_part(file_node):
2021 if not file_node.path:
2022 if not file_node.path:
2022 return u'/'
2023 return u'/'
2023
2024
2024 path = safe_unicode(file_node.path.split('/')[-1])
2025 path = safe_unicode(file_node.path.split('/')[-1])
2025 return u'../' + path
2026 return u'../' + path
2026
2027
2027
2028
2028 def route_url(*args, **kwargs):
2029 def route_url(*args, **kwargs):
2029 """
2030 """
2030 Wrapper around pyramids `route_url` (fully qualified url) function.
2031 Wrapper around pyramids `route_url` (fully qualified url) function.
2031 """
2032 """
2032 req = get_current_request()
2033 req = get_current_request()
2033 return req.route_url(*args, **kwargs)
2034 return req.route_url(*args, **kwargs)
2034
2035
2035
2036
2036 def route_path(*args, **kwargs):
2037 def route_path(*args, **kwargs):
2037 """
2038 """
2038 Wrapper around pyramids `route_path` function.
2039 Wrapper around pyramids `route_path` function.
2039 """
2040 """
2040 req = get_current_request()
2041 req = get_current_request()
2041 return req.route_path(*args, **kwargs)
2042 return req.route_path(*args, **kwargs)
2042
2043
2043
2044
2044 def route_path_or_none(*args, **kwargs):
2045 def route_path_or_none(*args, **kwargs):
2045 try:
2046 try:
2046 return route_path(*args, **kwargs)
2047 return route_path(*args, **kwargs)
2047 except KeyError:
2048 except KeyError:
2048 return None
2049 return None
2049
2050
2050
2051
2051 def current_route_path(request, **kw):
2052 def current_route_path(request, **kw):
2052 new_args = request.GET.mixed()
2053 new_args = request.GET.mixed()
2053 new_args.update(kw)
2054 new_args.update(kw)
2054 return request.current_route_path(_query=new_args)
2055 return request.current_route_path(_query=new_args)
2055
2056
2056
2057
2057 def curl_api_example(method, args):
2058 def curl_api_example(method, args):
2058 args_json = json.dumps(OrderedDict([
2059 args_json = json.dumps(OrderedDict([
2059 ('id', 1),
2060 ('id', 1),
2060 ('auth_token', 'SECRET'),
2061 ('auth_token', 'SECRET'),
2061 ('method', method),
2062 ('method', method),
2062 ('args', args)
2063 ('args', args)
2063 ]))
2064 ]))
2064
2065
2065 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2066 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2066 api_url=route_url('apiv2'),
2067 api_url=route_url('apiv2'),
2067 args_json=args_json
2068 args_json=args_json
2068 )
2069 )
2069
2070
2070
2071
2071 def api_call_example(method, args):
2072 def api_call_example(method, args):
2072 """
2073 """
2073 Generates an API call example via CURL
2074 Generates an API call example via CURL
2074 """
2075 """
2075 curl_call = curl_api_example(method, args)
2076 curl_call = curl_api_example(method, args)
2076
2077
2077 return literal(
2078 return literal(
2078 curl_call +
2079 curl_call +
2079 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2080 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2080 "and needs to be of `api calls` role."
2081 "and needs to be of `api calls` role."
2081 .format(token_url=route_url('my_account_auth_tokens')))
2082 .format(token_url=route_url('my_account_auth_tokens')))
2082
2083
2083
2084
2084 def notification_description(notification, request):
2085 def notification_description(notification, request):
2085 """
2086 """
2086 Generate notification human readable description based on notification type
2087 Generate notification human readable description based on notification type
2087 """
2088 """
2088 from rhodecode.model.notification import NotificationModel
2089 from rhodecode.model.notification import NotificationModel
2089 return NotificationModel().make_description(
2090 return NotificationModel().make_description(
2090 notification, translate=request.translate)
2091 notification, translate=request.translate)
2091
2092
2092
2093
2093 def go_import_header(request, db_repo=None):
2094 def go_import_header(request, db_repo=None):
2094 """
2095 """
2095 Creates a header for go-import functionality in Go Lang
2096 Creates a header for go-import functionality in Go Lang
2096 """
2097 """
2097
2098
2098 if not db_repo:
2099 if not db_repo:
2099 return
2100 return
2100 if 'go-get' not in request.GET:
2101 if 'go-get' not in request.GET:
2101 return
2102 return
2102
2103
2103 clone_url = db_repo.clone_url()
2104 clone_url = db_repo.clone_url()
2104 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2105 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2105 # we have a repo and go-get flag,
2106 # we have a repo and go-get flag,
2106 return literal('<meta name="go-import" content="{} {} {}">'.format(
2107 return literal('<meta name="go-import" content="{} {} {}">'.format(
2107 prefix, db_repo.repo_type, clone_url))
2108 prefix, db_repo.repo_type, clone_url))
2108
2109
2109
2110
2110 def reviewer_as_json(*args, **kwargs):
2111 def reviewer_as_json(*args, **kwargs):
2111 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2112 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2112 return _reviewer_as_json(*args, **kwargs)
2113 return _reviewer_as_json(*args, **kwargs)
2113
2114
2114
2115
2115 def get_repo_view_type(request):
2116 def get_repo_view_type(request):
2116 route_name = request.matched_route.name
2117 route_name = request.matched_route.name
2117 route_to_view_type = {
2118 route_to_view_type = {
2118 'repo_changelog': 'commits',
2119 'repo_changelog': 'commits',
2119 'repo_commits': 'commits',
2120 'repo_commits': 'commits',
2120 'repo_files': 'files',
2121 'repo_files': 'files',
2121 'repo_summary': 'summary',
2122 'repo_summary': 'summary',
2122 'repo_commit': 'commit'
2123 'repo_commit': 'commit'
2123 }
2124 }
2124
2125
2125 return route_to_view_type.get(route_name)
2126 return route_to_view_type.get(route_name)
@@ -1,663 +1,669 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('repo_compare', url_data);
61 window.location = pyroutes.url('repo_compare', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 container.show();
79 container.show();
80 if (!container.hasClass('loaded')) {
80 if (!container.hasClass('loaded')) {
81 $.ajax({url: url})
81 $.ajax({url: url})
82 .complete(function (data) {
82 .complete(function (data) {
83 var responseJSON = data.responseJSON;
83 var responseJSON = data.responseJSON;
84 container.addClass('loaded');
84 container.addClass('loaded');
85 container.html(responseJSON.size);
85 container.html(responseJSON.size);
86 callback(responseJSON.code_stats)
86 callback(responseJSON.code_stats)
87 })
87 })
88 .fail(function (data) {
88 .fail(function (data) {
89 console.log('failed to load repo stats');
89 console.log('failed to load repo stats');
90 });
90 });
91 }
91 }
92
92
93 };
93 };
94
94
95 var showRepoStats = function(target, data){
95 var showRepoStats = function(target, data){
96 var container = $('#' + target);
96 var container = $('#' + target);
97
97
98 if (container.hasClass('loaded')) {
98 if (container.hasClass('loaded')) {
99 return
99 return
100 }
100 }
101
101
102 var total = 0;
102 var total = 0;
103 var no_data = true;
103 var no_data = true;
104 var tbl = document.createElement('table');
104 var tbl = document.createElement('table');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106
106
107 $.each(data, function(key, val){
107 $.each(data, function(key, val){
108 total += val.count;
108 total += val.count;
109 });
109 });
110
110
111 var sortedStats = [];
111 var sortedStats = [];
112 for (var obj in data){
112 for (var obj in data){
113 sortedStats.push([obj, data[obj]])
113 sortedStats.push([obj, data[obj]])
114 }
114 }
115 var sortedData = sortedStats.sort(function (a, b) {
115 var sortedData = sortedStats.sort(function (a, b) {
116 return b[1].count - a[1].count
116 return b[1].count - a[1].count
117 });
117 });
118 var cnt = 0;
118 var cnt = 0;
119 $.each(sortedData, function(idx, val){
119 $.each(sortedData, function(idx, val){
120 cnt += 1;
120 cnt += 1;
121 no_data = false;
121 no_data = false;
122
122
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124
124
125 var key = val[0];
125 var key = val[0];
126 var obj = {"desc": val[1].desc, "count": val[1].count};
126 var obj = {"desc": val[1].desc, "count": val[1].count};
127
127
128 // meta language names
128 // meta language names
129 var td1 = document.createElement('td');
129 var td1 = document.createElement('td');
130 var trending_language_label = document.createElement('div');
130 var trending_language_label = document.createElement('div');
131 trending_language_label.innerHTML = obj.desc;
131 trending_language_label.innerHTML = obj.desc;
132 td1.appendChild(trending_language_label);
132 td1.appendChild(trending_language_label);
133
133
134 // extensions
134 // extensions
135 var td2 = document.createElement('td');
135 var td2 = document.createElement('td');
136 var extension = document.createElement('div');
136 var extension = document.createElement('div');
137 extension.innerHTML = ".{0}".format(key)
137 extension.innerHTML = ".{0}".format(key)
138 td2.appendChild(extension);
138 td2.appendChild(extension);
139
139
140 // number of files
140 // number of files
141 var td3 = document.createElement('td');
141 var td3 = document.createElement('td');
142 var file_count = document.createElement('div');
142 var file_count = document.createElement('div');
143 var percentage_num = Math.round((obj.count / total * 100), 2);
143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 var label = _ngettext('file', 'files', obj.count);
144 var label = _ngettext('file', 'files', obj.count);
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 td3.appendChild(file_count);
146 td3.appendChild(file_count);
147
147
148 // percentage
148 // percentage
149 var td4 = document.createElement('td');
149 var td4 = document.createElement('td');
150 td4.setAttribute("class", 'trending_language');
150 td4.setAttribute("class", 'trending_language');
151
151
152 var percentage = document.createElement('div');
152 var percentage = document.createElement('div');
153 percentage.setAttribute('class', 'lang-bar');
153 percentage.setAttribute('class', 'lang-bar');
154 percentage.innerHTML = "&nbsp;";
154 percentage.innerHTML = "&nbsp;";
155 percentage.style.width = percentage_num + '%';
155 percentage.style.width = percentage_num + '%';
156 td4.appendChild(percentage);
156 td4.appendChild(percentage);
157
157
158 tr.appendChild(td1);
158 tr.appendChild(td1);
159 tr.appendChild(td2);
159 tr.appendChild(td2);
160 tr.appendChild(td3);
160 tr.appendChild(td3);
161 tr.appendChild(td4);
161 tr.appendChild(td4);
162 tbl.appendChild(tr);
162 tbl.appendChild(tr);
163
163
164 });
164 });
165
165
166 $(container).html(tbl);
166 $(container).html(tbl);
167 $(container).addClass('loaded');
167 $(container).addClass('loaded');
168
168
169 $('#code_stats_show_more').on('click', function (e) {
169 $('#code_stats_show_more').on('click', function (e) {
170 e.preventDefault();
170 e.preventDefault();
171 $('.stats_hidden').each(function (idx) {
171 $('.stats_hidden').each(function (idx) {
172 $(this).css("display", "");
172 $(this).css("display", "");
173 });
173 });
174 $('#code_stats_show_more').hide();
174 $('#code_stats_show_more').hide();
175 });
175 });
176
176
177 };
177 };
178
178
179 // returns a node from given html;
179 // returns a node from given html;
180 var fromHTML = function(html){
180 var fromHTML = function(html){
181 var _html = document.createElement('element');
181 var _html = document.createElement('element');
182 _html.innerHTML = html;
182 _html.innerHTML = html;
183 return _html;
183 return _html;
184 };
184 };
185
185
186 // Toggle Collapsable Content
186 // Toggle Collapsable Content
187 function collapsableContent() {
187 function collapsableContent() {
188
188
189 $('.collapsable-content').not('.no-hide').hide();
189 $('.collapsable-content').not('.no-hide').hide();
190
190
191 $('.btn-collapse').unbind(); //in case we've been here before
191 $('.btn-collapse').unbind(); //in case we've been here before
192 $('.btn-collapse').click(function() {
192 $('.btn-collapse').click(function() {
193 var button = $(this);
193 var button = $(this);
194 var togglename = $(this).data("toggle");
194 var togglename = $(this).data("toggle");
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 if ($(this).html()=="Show Less")
196 if ($(this).html()=="Show Less")
197 $(this).html("Show More");
197 $(this).html("Show More");
198 else
198 else
199 $(this).html("Show Less");
199 $(this).html("Show Less");
200 });
200 });
201 };
201 };
202
202
203 var timeagoActivate = function() {
203 var timeagoActivate = function() {
204 $("time.timeago").timeago();
204 $("time.timeago").timeago();
205 };
205 };
206
206
207
207
208 var clipboardActivate = function() {
208 var clipboardActivate = function() {
209 /*
209 /*
210 *
210 *
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 * */
212 * */
213 var clipboard = new ClipboardJS('.clipboard-action');
213 var clipboard = new ClipboardJS('.clipboard-action');
214
214
215 clipboard.on('success', function(e) {
215 clipboard.on('success', function(e) {
216 var callback = function () {
216 var callback = function () {
217 $(e.trigger).animate({'opacity': 1.00}, 200)
217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 };
218 };
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 e.clearSelection();
220 e.clearSelection();
221 });
221 });
222 };
222 };
223
223
224 var tooltipActivate = function () {
224 var tooltipActivate = function () {
225 var delay = 50;
225 var delay = 50;
226 var animation = 'fade';
226 var animation = 'fade';
227 var theme = 'tooltipster-shadow';
227 var theme = 'tooltipster-shadow';
228 var debug = false;
228 var debug = false;
229
229
230 $('.tooltip').tooltipster({
230 $('.tooltip').tooltipster({
231 debug: debug,
231 debug: debug,
232 theme: theme,
232 theme: theme,
233 animation: animation,
233 animation: animation,
234 delay: delay,
234 delay: delay,
235 contentCloning: true,
235 contentCloning: true,
236 contentAsHTML: true,
236 contentAsHTML: true,
237
237
238 functionBefore: function (instance, helper) {
238 functionBefore: function (instance, helper) {
239 var $origin = $(helper.origin);
239 var $origin = $(helper.origin);
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 instance.content(data);
241 instance.content(data);
242 }
242 }
243 });
243 });
244 var hovercardCache = {};
244 var hovercardCache = {};
245
245
246 var loadHoverCard = function (url, callback) {
246 var loadHoverCard = function (url, altHovercard, callback) {
247 var id = url;
247 var id = url;
248
248
249 if (hovercardCache[id] !== undefined) {
249 if (hovercardCache[id] !== undefined) {
250 callback(hovercardCache[id]);
250 callback(hovercardCache[id]);
251 return true;
251 return true;
252 }
252 }
253
253
254 hovercardCache[id] = undefined;
254 hovercardCache[id] = undefined;
255 $.get(url, function (data) {
255 $.get(url, function (data) {
256 hovercardCache[id] = data;
256 hovercardCache[id] = data;
257 callback(hovercardCache[id]);
257 callback(hovercardCache[id]);
258 return true;
258 return true;
259 }).fail(function (data, textStatus, errorThrown) {
259 }).fail(function (data, textStatus, errorThrown) {
260 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
260
261 if (parseInt(data.status) === 404) {
262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 } else {
264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 }
261 callback(msg);
266 callback(msg);
262 return false
267 return false
263 });
268 });
264 };
269 };
265
270
266 $('.tooltip-hovercard').tooltipster({
271 $('.tooltip-hovercard').tooltipster({
267 debug: debug,
272 debug: debug,
268 theme: theme,
273 theme: theme,
269 animation: animation,
274 animation: animation,
270 delay: delay,
275 delay: delay,
271 interactive: true,
276 interactive: true,
272 contentCloning: true,
277 contentCloning: true,
273
278
274 trigger: 'custom',
279 trigger: 'custom',
275 triggerOpen: {
280 triggerOpen: {
276 mouseenter: true,
281 mouseenter: true,
277 },
282 },
278 triggerClose: {
283 triggerClose: {
279 mouseleave: true,
284 mouseleave: true,
280 originClick: true,
285 originClick: true,
281 touchleave: true
286 touchleave: true
282 },
287 },
283 content: _gettext('Loading...'),
288 content: _gettext('Loading...'),
284 contentAsHTML: true,
289 contentAsHTML: true,
285 updateAnimation: null,
290 updateAnimation: null,
286
291
287 functionBefore: function (instance, helper) {
292 functionBefore: function (instance, helper) {
288
293
289 var $origin = $(helper.origin);
294 var $origin = $(helper.origin);
290
295
291 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
292 if ($origin.data('loaded') !== true) {
297 if ($origin.data('loaded') !== true) {
293 var hovercardUrl = $origin.data('hovercardUrl');
298 var hovercardUrl = $origin.data('hovercardUrl');
299 var altHovercard =$origin.data('hovercardAlt');
294
300
295 if (hovercardUrl !== undefined && hovercardUrl !== "") {
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
296 var loaded = loadHoverCard(hovercardUrl, function (data) {
302 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
297 instance.content(data);
303 instance.content(data);
298 })
304 })
299 } else {
305 } else {
300 if ($origin.data('hovercardAltHtml')) {
306 if ($origin.data('hovercardAltHtml')) {
301 var data = atob($origin.data('hovercardAltHtml'));
307 var data = atob($origin.data('hovercardAltHtml'));
302 } else {
308 } else {
303 var data = '<div style="white-space: pre-wrap">{0}</div>'.format($origin.data('hovercardAlt'))
309 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
304 }
310 }
305 var loaded = true;
311 var loaded = true;
306 instance.content(data);
312 instance.content(data);
307 }
313 }
308
314
309 // to remember that the data has been loaded
315 // to remember that the data has been loaded
310 $origin.data('loaded', loaded);
316 $origin.data('loaded', loaded);
311 }
317 }
312 }
318 }
313 })
319 })
314 };
320 };
315
321
316 // Formatting values in a Select2 dropdown of commit references
322 // Formatting values in a Select2 dropdown of commit references
317 var formatSelect2SelectionRefs = function(commit_ref){
323 var formatSelect2SelectionRefs = function(commit_ref){
318 var tmpl = '';
324 var tmpl = '';
319 if (!commit_ref.text || commit_ref.type === 'sha'){
325 if (!commit_ref.text || commit_ref.type === 'sha'){
320 return commit_ref.text;
326 return commit_ref.text;
321 }
327 }
322 if (commit_ref.type === 'branch'){
328 if (commit_ref.type === 'branch'){
323 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
329 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
324 } else if (commit_ref.type === 'tag'){
330 } else if (commit_ref.type === 'tag'){
325 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
331 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
326 } else if (commit_ref.type === 'book'){
332 } else if (commit_ref.type === 'book'){
327 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
333 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
328 }
334 }
329 return tmpl.concat(escapeHtml(commit_ref.text));
335 return tmpl.concat(escapeHtml(commit_ref.text));
330 };
336 };
331
337
332 // takes a given html element and scrolls it down offset pixels
338 // takes a given html element and scrolls it down offset pixels
333 function offsetScroll(element, offset) {
339 function offsetScroll(element, offset) {
334 setTimeout(function() {
340 setTimeout(function() {
335 var location = element.offset().top;
341 var location = element.offset().top;
336 // some browsers use body, some use html
342 // some browsers use body, some use html
337 $('html, body').animate({ scrollTop: (location - offset) });
343 $('html, body').animate({ scrollTop: (location - offset) });
338 }, 100);
344 }, 100);
339 }
345 }
340
346
341 // scroll an element `percent`% from the top of page in `time` ms
347 // scroll an element `percent`% from the top of page in `time` ms
342 function scrollToElement(element, percent, time) {
348 function scrollToElement(element, percent, time) {
343 percent = (percent === undefined ? 25 : percent);
349 percent = (percent === undefined ? 25 : percent);
344 time = (time === undefined ? 100 : time);
350 time = (time === undefined ? 100 : time);
345
351
346 var $element = $(element);
352 var $element = $(element);
347 if ($element.length == 0) {
353 if ($element.length == 0) {
348 throw('Cannot scroll to {0}'.format(element))
354 throw('Cannot scroll to {0}'.format(element))
349 }
355 }
350 var elOffset = $element.offset().top;
356 var elOffset = $element.offset().top;
351 var elHeight = $element.height();
357 var elHeight = $element.height();
352 var windowHeight = $(window).height();
358 var windowHeight = $(window).height();
353 var offset = elOffset;
359 var offset = elOffset;
354 if (elHeight < windowHeight) {
360 if (elHeight < windowHeight) {
355 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
361 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
356 }
362 }
357 setTimeout(function() {
363 setTimeout(function() {
358 $('html, body').animate({ scrollTop: offset});
364 $('html, body').animate({ scrollTop: offset});
359 }, time);
365 }, time);
360 }
366 }
361
367
362 /**
368 /**
363 * global hooks after DOM is loaded
369 * global hooks after DOM is loaded
364 */
370 */
365 $(document).ready(function() {
371 $(document).ready(function() {
366 firefoxAnchorFix();
372 firefoxAnchorFix();
367
373
368 $('.navigation a.menulink').on('click', function(e){
374 $('.navigation a.menulink').on('click', function(e){
369 var menuitem = $(this).parent('li');
375 var menuitem = $(this).parent('li');
370 if (menuitem.hasClass('open')) {
376 if (menuitem.hasClass('open')) {
371 menuitem.removeClass('open');
377 menuitem.removeClass('open');
372 } else {
378 } else {
373 menuitem.addClass('open');
379 menuitem.addClass('open');
374 $(document).on('click', function(event) {
380 $(document).on('click', function(event) {
375 if (!$(event.target).closest(menuitem).length) {
381 if (!$(event.target).closest(menuitem).length) {
376 menuitem.removeClass('open');
382 menuitem.removeClass('open');
377 }
383 }
378 });
384 });
379 }
385 }
380 });
386 });
381
387
382 $('body').on('click', '.cb-lineno a', function(event) {
388 $('body').on('click', '.cb-lineno a', function(event) {
383 function sortNumber(a,b) {
389 function sortNumber(a,b) {
384 return a - b;
390 return a - b;
385 }
391 }
386
392
387 var lineNo = $(this).data('lineNo');
393 var lineNo = $(this).data('lineNo');
388 var lineName = $(this).attr('name');
394 var lineName = $(this).attr('name');
389
395
390 if (lineNo) {
396 if (lineNo) {
391 var prevLine = $('.cb-line-selected a').data('lineNo');
397 var prevLine = $('.cb-line-selected a').data('lineNo');
392
398
393 // on shift, we do a range selection, if we got previous line
399 // on shift, we do a range selection, if we got previous line
394 if (event.shiftKey && prevLine !== undefined) {
400 if (event.shiftKey && prevLine !== undefined) {
395 var prevLine = parseInt(prevLine);
401 var prevLine = parseInt(prevLine);
396 var nextLine = parseInt(lineNo);
402 var nextLine = parseInt(lineNo);
397 var pos = [prevLine, nextLine].sort(sortNumber);
403 var pos = [prevLine, nextLine].sort(sortNumber);
398 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
404 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
399
405
400 // single click
406 // single click
401 } else {
407 } else {
402 var nextLine = parseInt(lineNo);
408 var nextLine = parseInt(lineNo);
403 var pos = [nextLine, nextLine];
409 var pos = [nextLine, nextLine];
404 var anchor = '#L{0}'.format(pos[0]);
410 var anchor = '#L{0}'.format(pos[0]);
405
411
406 }
412 }
407 // highlight
413 // highlight
408 var range = [];
414 var range = [];
409 for (var i = pos[0]; i <= pos[1]; i++) {
415 for (var i = pos[0]; i <= pos[1]; i++) {
410 range.push(i);
416 range.push(i);
411 }
417 }
412 // clear old selected lines
418 // clear old selected lines
413 $('.cb-line-selected').removeClass('cb-line-selected');
419 $('.cb-line-selected').removeClass('cb-line-selected');
414
420
415 $.each(range, function (i, lineNo) {
421 $.each(range, function (i, lineNo) {
416 var line_td = $('td.cb-lineno#L' + lineNo);
422 var line_td = $('td.cb-lineno#L' + lineNo);
417
423
418 if (line_td.length) {
424 if (line_td.length) {
419 line_td.addClass('cb-line-selected'); // line number td
425 line_td.addClass('cb-line-selected'); // line number td
420 line_td.prev().addClass('cb-line-selected'); // line data
426 line_td.prev().addClass('cb-line-selected'); // line data
421 line_td.next().addClass('cb-line-selected'); // line content
427 line_td.next().addClass('cb-line-selected'); // line content
422 }
428 }
423 });
429 });
424
430
425 } else if (lineName !== undefined) { // lineName only occurs in diffs
431 } else if (lineName !== undefined) { // lineName only occurs in diffs
426 // clear old selected lines
432 // clear old selected lines
427 $('td.cb-line-selected').removeClass('cb-line-selected');
433 $('td.cb-line-selected').removeClass('cb-line-selected');
428 var anchor = '#{0}'.format(lineName);
434 var anchor = '#{0}'.format(lineName);
429 var diffmode = templateContext.session_attrs.diffmode || "sideside";
435 var diffmode = templateContext.session_attrs.diffmode || "sideside";
430
436
431 if (diffmode === "unified") {
437 if (diffmode === "unified") {
432 $(this).closest('tr').find('td').addClass('cb-line-selected');
438 $(this).closest('tr').find('td').addClass('cb-line-selected');
433 } else {
439 } else {
434 var activeTd = $(this).closest('td');
440 var activeTd = $(this).closest('td');
435 activeTd.addClass('cb-line-selected');
441 activeTd.addClass('cb-line-selected');
436 activeTd.next('td').addClass('cb-line-selected');
442 activeTd.next('td').addClass('cb-line-selected');
437 }
443 }
438
444
439 }
445 }
440
446
441 // Replace URL without jumping to it if browser supports.
447 // Replace URL without jumping to it if browser supports.
442 // Default otherwise
448 // Default otherwise
443 if (history.pushState && anchor !== undefined) {
449 if (history.pushState && anchor !== undefined) {
444 var new_location = location.href.rstrip('#');
450 var new_location = location.href.rstrip('#');
445 if (location.hash) {
451 if (location.hash) {
446 // location without hash
452 // location without hash
447 new_location = new_location.replace(location.hash, "");
453 new_location = new_location.replace(location.hash, "");
448 }
454 }
449
455
450 // Make new anchor url
456 // Make new anchor url
451 new_location = new_location + anchor;
457 new_location = new_location + anchor;
452 history.pushState(true, document.title, new_location);
458 history.pushState(true, document.title, new_location);
453
459
454 return false;
460 return false;
455 }
461 }
456
462
457 });
463 });
458
464
459 $('.collapse_file').on('click', function(e) {
465 $('.collapse_file').on('click', function(e) {
460 e.stopPropagation();
466 e.stopPropagation();
461 if ($(e.target).is('a')) { return; }
467 if ($(e.target).is('a')) { return; }
462 var node = $(e.delegateTarget).first();
468 var node = $(e.delegateTarget).first();
463 var icon = $($(node.children().first()).children().first());
469 var icon = $($(node.children().first()).children().first());
464 var id = node.attr('fid');
470 var id = node.attr('fid');
465 var target = $('#'+id);
471 var target = $('#'+id);
466 var tr = $('#tr_'+id);
472 var tr = $('#tr_'+id);
467 var diff = $('#diff_'+id);
473 var diff = $('#diff_'+id);
468 if(node.hasClass('expand_file')){
474 if(node.hasClass('expand_file')){
469 node.removeClass('expand_file');
475 node.removeClass('expand_file');
470 icon.removeClass('expand_file_icon');
476 icon.removeClass('expand_file_icon');
471 node.addClass('collapse_file');
477 node.addClass('collapse_file');
472 icon.addClass('collapse_file_icon');
478 icon.addClass('collapse_file_icon');
473 diff.show();
479 diff.show();
474 tr.show();
480 tr.show();
475 target.show();
481 target.show();
476 } else {
482 } else {
477 node.removeClass('collapse_file');
483 node.removeClass('collapse_file');
478 icon.removeClass('collapse_file_icon');
484 icon.removeClass('collapse_file_icon');
479 node.addClass('expand_file');
485 node.addClass('expand_file');
480 icon.addClass('expand_file_icon');
486 icon.addClass('expand_file_icon');
481 diff.hide();
487 diff.hide();
482 tr.hide();
488 tr.hide();
483 target.hide();
489 target.hide();
484 }
490 }
485 });
491 });
486
492
487 $('#expand_all_files').click(function() {
493 $('#expand_all_files').click(function() {
488 $('.expand_file').each(function() {
494 $('.expand_file').each(function() {
489 var node = $(this);
495 var node = $(this);
490 var icon = $($(node.children().first()).children().first());
496 var icon = $($(node.children().first()).children().first());
491 var id = $(this).attr('fid');
497 var id = $(this).attr('fid');
492 var target = $('#'+id);
498 var target = $('#'+id);
493 var tr = $('#tr_'+id);
499 var tr = $('#tr_'+id);
494 var diff = $('#diff_'+id);
500 var diff = $('#diff_'+id);
495 node.removeClass('expand_file');
501 node.removeClass('expand_file');
496 icon.removeClass('expand_file_icon');
502 icon.removeClass('expand_file_icon');
497 node.addClass('collapse_file');
503 node.addClass('collapse_file');
498 icon.addClass('collapse_file_icon');
504 icon.addClass('collapse_file_icon');
499 diff.show();
505 diff.show();
500 tr.show();
506 tr.show();
501 target.show();
507 target.show();
502 });
508 });
503 });
509 });
504
510
505 $('#collapse_all_files').click(function() {
511 $('#collapse_all_files').click(function() {
506 $('.collapse_file').each(function() {
512 $('.collapse_file').each(function() {
507 var node = $(this);
513 var node = $(this);
508 var icon = $($(node.children().first()).children().first());
514 var icon = $($(node.children().first()).children().first());
509 var id = $(this).attr('fid');
515 var id = $(this).attr('fid');
510 var target = $('#'+id);
516 var target = $('#'+id);
511 var tr = $('#tr_'+id);
517 var tr = $('#tr_'+id);
512 var diff = $('#diff_'+id);
518 var diff = $('#diff_'+id);
513 node.removeClass('collapse_file');
519 node.removeClass('collapse_file');
514 icon.removeClass('collapse_file_icon');
520 icon.removeClass('collapse_file_icon');
515 node.addClass('expand_file');
521 node.addClass('expand_file');
516 icon.addClass('expand_file_icon');
522 icon.addClass('expand_file_icon');
517 diff.hide();
523 diff.hide();
518 tr.hide();
524 tr.hide();
519 target.hide();
525 target.hide();
520 });
526 });
521 });
527 });
522
528
523 // Mouse over behavior for comments and line selection
529 // Mouse over behavior for comments and line selection
524
530
525 // Select the line that comes from the url anchor
531 // Select the line that comes from the url anchor
526 // At the time of development, Chrome didn't seem to support jquery's :target
532 // At the time of development, Chrome didn't seem to support jquery's :target
527 // element, so I had to scroll manually
533 // element, so I had to scroll manually
528
534
529 if (location.hash) {
535 if (location.hash) {
530 var result = splitDelimitedHash(location.hash);
536 var result = splitDelimitedHash(location.hash);
531 var loc = result.loc;
537 var loc = result.loc;
532 if (loc.length > 1) {
538 if (loc.length > 1) {
533
539
534 var highlightable_line_tds = [];
540 var highlightable_line_tds = [];
535
541
536 // source code line format
542 // source code line format
537 var page_highlights = loc.substring(
543 var page_highlights = loc.substring(
538 loc.indexOf('#') + 1).split('L');
544 loc.indexOf('#') + 1).split('L');
539
545
540 if (page_highlights.length > 1) {
546 if (page_highlights.length > 1) {
541 var highlight_ranges = page_highlights[1].split(",");
547 var highlight_ranges = page_highlights[1].split(",");
542 var h_lines = [];
548 var h_lines = [];
543 for (var pos in highlight_ranges) {
549 for (var pos in highlight_ranges) {
544 var _range = highlight_ranges[pos].split('-');
550 var _range = highlight_ranges[pos].split('-');
545 if (_range.length === 2) {
551 if (_range.length === 2) {
546 var start = parseInt(_range[0]);
552 var start = parseInt(_range[0]);
547 var end = parseInt(_range[1]);
553 var end = parseInt(_range[1]);
548 if (start < end) {
554 if (start < end) {
549 for (var i = start; i <= end; i++) {
555 for (var i = start; i <= end; i++) {
550 h_lines.push(i);
556 h_lines.push(i);
551 }
557 }
552 }
558 }
553 }
559 }
554 else {
560 else {
555 h_lines.push(parseInt(highlight_ranges[pos]));
561 h_lines.push(parseInt(highlight_ranges[pos]));
556 }
562 }
557 }
563 }
558 for (pos in h_lines) {
564 for (pos in h_lines) {
559 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
565 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
560 if (line_td.length) {
566 if (line_td.length) {
561 highlightable_line_tds.push(line_td);
567 highlightable_line_tds.push(line_td);
562 }
568 }
563 }
569 }
564 }
570 }
565
571
566 // now check a direct id reference (diff page)
572 // now check a direct id reference (diff page)
567 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
573 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
568 highlightable_line_tds.push($(loc));
574 highlightable_line_tds.push($(loc));
569 }
575 }
570 $.each(highlightable_line_tds, function (i, $td) {
576 $.each(highlightable_line_tds, function (i, $td) {
571 $td.addClass('cb-line-selected'); // line number td
577 $td.addClass('cb-line-selected'); // line number td
572 $td.prev().addClass('cb-line-selected'); // line data
578 $td.prev().addClass('cb-line-selected'); // line data
573 $td.next().addClass('cb-line-selected'); // line content
579 $td.next().addClass('cb-line-selected'); // line content
574 });
580 });
575
581
576 if (highlightable_line_tds.length) {
582 if (highlightable_line_tds.length) {
577 var $first_line_td = highlightable_line_tds[0];
583 var $first_line_td = highlightable_line_tds[0];
578 scrollToElement($first_line_td);
584 scrollToElement($first_line_td);
579 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
585 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
580 td: $first_line_td,
586 td: $first_line_td,
581 remainder: result.remainder
587 remainder: result.remainder
582 });
588 });
583 }
589 }
584 }
590 }
585 }
591 }
586 collapsableContent();
592 collapsableContent();
587 });
593 });
588
594
589 var feedLifetimeOptions = function(query, initialData){
595 var feedLifetimeOptions = function(query, initialData){
590 var data = {results: []};
596 var data = {results: []};
591 var isQuery = typeof query.term !== 'undefined';
597 var isQuery = typeof query.term !== 'undefined';
592
598
593 var section = _gettext('Lifetime');
599 var section = _gettext('Lifetime');
594 var children = [];
600 var children = [];
595
601
596 //filter results
602 //filter results
597 $.each(initialData.results, function(idx, value) {
603 $.each(initialData.results, function(idx, value) {
598
604
599 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
605 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
600 children.push({
606 children.push({
601 'id': this.id,
607 'id': this.id,
602 'text': this.text
608 'text': this.text
603 })
609 })
604 }
610 }
605
611
606 });
612 });
607 data.results.push({
613 data.results.push({
608 'text': section,
614 'text': section,
609 'children': children
615 'children': children
610 });
616 });
611
617
612 if (isQuery) {
618 if (isQuery) {
613
619
614 var now = moment.utc();
620 var now = moment.utc();
615
621
616 var parseQuery = function(entry, now){
622 var parseQuery = function(entry, now){
617 var fmt = 'DD/MM/YYYY H:mm';
623 var fmt = 'DD/MM/YYYY H:mm';
618 var parsed = moment.utc(entry, fmt);
624 var parsed = moment.utc(entry, fmt);
619 var diffInMin = parsed.diff(now, 'minutes');
625 var diffInMin = parsed.diff(now, 'minutes');
620
626
621 if (diffInMin > 0){
627 if (diffInMin > 0){
622 return {
628 return {
623 id: diffInMin,
629 id: diffInMin,
624 text: parsed.format(fmt)
630 text: parsed.format(fmt)
625 }
631 }
626 } else {
632 } else {
627 return {
633 return {
628 id: undefined,
634 id: undefined,
629 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
635 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
630 }
636 }
631 }
637 }
632
638
633
639
634 };
640 };
635
641
636 data.results.push({
642 data.results.push({
637 'text': _gettext('Specified expiration date'),
643 'text': _gettext('Specified expiration date'),
638 'children': [{
644 'children': [{
639 'id': parseQuery(query.term, now).id,
645 'id': parseQuery(query.term, now).id,
640 'text': parseQuery(query.term, now).text
646 'text': parseQuery(query.term, now).text
641 }]
647 }]
642 });
648 });
643 }
649 }
644
650
645 query.callback(data);
651 query.callback(data);
646 };
652 };
647
653
648
654
649 var storeUserSessionAttr = function (key, val) {
655 var storeUserSessionAttr = function (key, val) {
650
656
651 var postData = {
657 var postData = {
652 'key': key,
658 'key': key,
653 'val': val,
659 'val': val,
654 'csrf_token': CSRF_TOKEN
660 'csrf_token': CSRF_TOKEN
655 };
661 };
656
662
657 var success = function(o) {
663 var success = function(o) {
658 return true
664 return true
659 };
665 };
660
666
661 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
667 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
662 return false;
668 return false;
663 };
669 };
@@ -1,219 +1,221 b''
1 ## snippet for displaying issue tracker settings
1 ## snippet for displaying issue tracker settings
2 ## usage:
2 ## usage:
3 ## <%namespace name="its" file="/base/issue_tracker_settings.mako"/>
3 ## <%namespace name="its" file="/base/issue_tracker_settings.mako"/>
4 ## ${its.issue_tracker_settings_table(patterns, form_url, delete_url)}
4 ## ${its.issue_tracker_settings_table(patterns, form_url, delete_url)}
5 ## ${its.issue_tracker_settings_test(test_url)}
5 ## ${its.issue_tracker_settings_test(test_url)}
6
6
7 <%def name="issue_tracker_settings_table(patterns, form_url, delete_url)">
7 <%def name="issue_tracker_settings_table(patterns, form_url, delete_url)">
8 <table class="rctable issuetracker">
8 <table class="rctable issuetracker">
9 <tr>
9 <tr>
10 <th>${_('Description')}</th>
10 <th>${_('Description')}</th>
11 <th>${_('Pattern')}</th>
11 <th>${_('Pattern')}</th>
12 <th>${_('Url')}</th>
12 <th>${_('Url')}</th>
13 <th>${_('Prefix')}</th>
13 <th>${_('Prefix')}</th>
14 <th ></th>
14 <th ></th>
15 </tr>
15 </tr>
16 <tr>
16 <tr>
17 <td class="td-description issue-tracker-example">Example</td>
17 <td class="td-description issue-tracker-example">Example</td>
18 <td class="td-regex issue-tracker-example">${'(?:#)(?P<issue_id>\d+)'}</td>
18 <td class="td-regex issue-tracker-example">${'(?:#)(?P<issue_id>\d+)'}</td>
19 <td class="td-url issue-tracker-example">${'https://myissueserver.com/${repo}/issue/${issue_id}'}</td>
19 <td class="td-url issue-tracker-example">${'https://myissueserver.com/${repo}/issue/${issue_id}'}</td>
20 <td class="td-prefix issue-tracker-example">#</td>
20 <td class="td-prefix issue-tracker-example">#</td>
21 <td class="issue-tracker-example"><a href="${h.route_url('enterprise_issue_tracker_settings')}" target="_blank">${_('Read more')}</a></td>
21 <td class="issue-tracker-example"><a href="${h.route_url('enterprise_issue_tracker_settings')}" target="_blank">${_('Read more')}</a></td>
22 </tr>
22 </tr>
23 %for uid, entry in patterns:
23 %for uid, entry in patterns:
24 <tr id="entry_${uid}">
24 <tr id="entry_${uid}">
25 <td class="td-description issuetracker_desc">
25 <td class="td-description issuetracker_desc">
26 <span class="entry">
26 <span class="entry">
27 ${entry.desc}
27 ${entry.desc}
28 </span>
28 </span>
29 <span class="edit">
29 <span class="edit">
30 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
30 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
31 </span>
31 </span>
32 </td>
32 </td>
33 <td class="td-regex issuetracker_pat">
33 <td class="td-regex issuetracker_pat">
34 <span class="entry">
34 <span class="entry">
35 ${entry.pat}
35 ${entry.pat}
36 </span>
36 </span>
37 <span class="edit">
37 <span class="edit">
38 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
38 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
39 </span>
39 </span>
40 </td>
40 </td>
41 <td class="td-url issuetracker_url">
41 <td class="td-url issuetracker_url">
42 <span class="entry">
42 <span class="entry">
43 ${entry.url}
43 ${entry.url}
44 </span>
44 </span>
45 <span class="edit">
45 <span class="edit">
46 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
46 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
47 </span>
47 </span>
48 </td>
48 </td>
49 <td class="td-prefix issuetracker_pref">
49 <td class="td-prefix issuetracker_pref">
50 <span class="entry">
50 <span class="entry">
51 ${entry.pref}
51 ${entry.pref}
52 </span>
52 </span>
53 <span class="edit">
53 <span class="edit">
54 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
54 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
55 </span>
55 </span>
56 </td>
56 </td>
57 <td class="td-action">
57 <td class="td-action">
58 <div class="grid_edit">
58 <div class="grid_edit">
59 <span class="entry">
59 <span class="entry">
60 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
60 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
61 </span>
61 </span>
62 <span class="edit">
62 <span class="edit">
63 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
63 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
64 </span>
64 </span>
65 </div>
65 </div>
66 <div class="grid_delete">
66 <div class="grid_delete">
67 <span class="entry">
67 <span class="entry">
68 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
68 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
69 ${_('Delete')}
69 ${_('Delete')}
70 </a>
70 </a>
71 </span>
71 </span>
72 <span class="edit">
72 <span class="edit">
73 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
73 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
74 </span>
74 </span>
75 </div>
75 </div>
76 </td>
76 </td>
77 </tr>
77 </tr>
78 %endfor
78 %endfor
79 <tr id="last-row"></tr>
79 <tr id="last-row"></tr>
80 </table>
80 </table>
81 <p>
81 <p>
82 <a id="add_pattern" class="link">
82 <a id="add_pattern" class="link">
83 ${_('Add new')}
83 ${_('Add new')}
84 </a>
84 </a>
85 </p>
85 </p>
86
86
87 <script type="text/javascript">
87 <script type="text/javascript">
88 var newEntryLabel = $('label[for="new_entry"]');
88 var newEntryLabel = $('label[for="new_entry"]');
89
89
90 var resetEntry = function() {
90 var resetEntry = function() {
91 newEntryLabel.text("${_('New Entry')}:");
91 newEntryLabel.text("${_('New Entry')}:");
92 };
92 };
93
93
94 var delete_pattern = function(entry) {
94 var delete_pattern = function(entry) {
95 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
95 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
96 $.ajax({
96 $.ajax({
97 type: "POST",
97 type: "POST",
98 url: "${delete_url}",
98 url: "${delete_url}",
99 data: {
99 data: {
100 'csrf_token': CSRF_TOKEN,
100 'csrf_token': CSRF_TOKEN,
101 'uid':$(entry).data('uid')
101 'uid':$(entry).data('uid')
102 },
102 },
103 success: function(){
103 success: function(){
104 location.reload();
104 location.reload();
105 },
105 },
106 error: function(data, textStatus, errorThrown){
106 error: function(data, textStatus, errorThrown){
107 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
107 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
108 }
108 }
109 });
109 });
110 }
110 }
111 };
111 };
112
112
113 $('.delete_issuetracker_entry').on('click', function(e){
113 $('.delete_issuetracker_entry').on('click', function(e){
114 e.preventDefault();
114 e.preventDefault();
115 delete_pattern(this);
115 delete_pattern(this);
116 });
116 });
117
117
118 $('.edit_issuetracker_entry').on('click', function(e){
118 $('.edit_issuetracker_entry').on('click', function(e){
119 e.preventDefault();
119 e.preventDefault();
120 $(this).parents('tr').addClass('editopen');
120 $(this).parents('tr').addClass('editopen');
121 });
121 });
122
122
123 $('.edit_issuetracker_cancel').on('click', function(e){
123 $('.edit_issuetracker_cancel').on('click', function(e){
124 e.preventDefault();
124 e.preventDefault();
125 $(this).parents('tr').removeClass('editopen');
125 $(this).parents('tr').removeClass('editopen');
126 // Reset to original value
126 // Reset to original value
127 var uid = $(this).data('uid');
127 var uid = $(this).data('uid');
128 $('#'+uid+' input').each(function(e) {
128 $('#'+uid+' input').each(function(e) {
129 this.value = this.defaultValue;
129 this.value = this.defaultValue;
130 });
130 });
131 });
131 });
132
132
133 $('input#reset').on('click', function(e) {
133 $('input#reset').on('click', function(e) {
134 resetEntry();
134 resetEntry();
135 });
135 });
136
136
137 $('#add_pattern').on('click', function(e) {
137 $('#add_pattern').on('click', function(e) {
138 addNewPatternInput();
138 addNewPatternInput();
139 });
139 });
140 </script>
140 </script>
141 </%def>
141 </%def>
142
142
143 <%def name="issue_tracker_new_row()">
143 <%def name="issue_tracker_new_row()">
144 <table id="add-row-tmpl" style="display: none;">
144 <table id="add-row-tmpl" style="display: none;">
145 <tbody>
145 <tbody>
146 <tr class="new_pattern">
146 <tr class="new_pattern">
147 <td class="td-description issuetracker_desc">
147 <td class="td-description issuetracker_desc">
148 <span class="entry">
148 <span class="entry">
149 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
149 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
150 </span>
150 </span>
151 </td>
151 </td>
152 <td class="td-regex issuetracker_pat">
152 <td class="td-regex issuetracker_pat">
153 <span class="entry">
153 <span class="entry">
154 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
154 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
155 value="##PATTERN##" type="text">
155 value="##PATTERN##" type="text">
156 </span>
156 </span>
157 </td>
157 </td>
158 <td class="td-url issuetracker_url">
158 <td class="td-url issuetracker_url">
159 <span class="entry">
159 <span class="entry">
160 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
160 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
161 </span>
161 </span>
162 </td>
162 </td>
163 <td class="td-prefix issuetracker_pref">
163 <td class="td-prefix issuetracker_pref">
164 <span class="entry">
164 <span class="entry">
165 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
165 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
166 </span>
166 </span>
167 </td>
167 </td>
168 <td class="td-action">
168 <td class="td-action">
169 </td>
169 </td>
170 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
170 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
171 </tr>
171 </tr>
172 </tbody>
172 </tbody>
173 </table>
173 </table>
174 </%def>
174 </%def>
175
175
176 <%def name="issue_tracker_settings_test(test_url)">
176 <%def name="issue_tracker_settings_test(test_url)">
177 <div class="form-vertical">
177 <div class="form-vertical">
178 <div class="fields">
178 <div class="fields">
179 <div class="field">
179 <div class="field">
180 <div class='textarea-full'>
180 <div class='textarea-full'>
181 <textarea id="test_pattern_data" >
181 <textarea id="test_pattern_data" rows="10">
182 This is an example text for testing issue tracker patterns.
182 This commit fixes ticket #451.
183 This commit fixes ticket #451.
183 This is an example text for testing issue tracker patterns, add a pattern here and
184 hit preview to see the link.
185 Open a pull request !101 to contribute !
184 Open a pull request !101 to contribute !
185 Added tag v1.3.0 for commit 0f3b629be725
186
187 Add a test pattern here and hit preview to see the link.
186 </textarea>
188 </textarea>
187 </div>
189 </div>
188 </div>
190 </div>
189 </div>
191 </div>
190 <div class="test_pattern_preview">
192 <div class="test_pattern_preview">
191 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
193 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
192 <p>${_('Test Pattern Preview')}</p>
194 <p>${_('Test Pattern Preview')}</p>
193 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
195 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
194 </div>
196 </div>
195 </div>
197 </div>
196
198
197 <script type="text/javascript">
199 <script type="text/javascript">
198 $('#test_pattern').on('click', function(e) {
200 $('#test_pattern').on('click', function(e) {
199 $.ajax({
201 $.ajax({
200 type: "POST",
202 type: "POST",
201 url: "${test_url}",
203 url: "${test_url}",
202 data: {
204 data: {
203 'test_text': $('#test_pattern_data').val(),
205 'test_text': $('#test_pattern_data').val(),
204 'csrf_token': CSRF_TOKEN
206 'csrf_token': CSRF_TOKEN
205 },
207 },
206 success: function(data){
208 success: function(data){
207 $('#test_pattern_result').html(data);
209 $('#test_pattern_result').html(data);
208 tooltipActivate();
210 tooltipActivate();
209 },
211 },
210 error: function(jqXHR, textStatus, errorThrown){
212 error: function(jqXHR, textStatus, errorThrown){
211 $('#test_pattern_result').html('Error: ' + errorThrown);
213 $('#test_pattern_result').html('Error: ' + errorThrown);
212 }
214 }
213 });
215 });
214 $('#test_pattern_result').show();
216 $('#test_pattern_result').show();
215 });
217 });
216 </script>
218 </script>
217 </%def>
219 </%def>
218
220
219
221
@@ -1,687 +1,729 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 """
22 """
23 Package for testing various lib/helper functions in rhodecode
23 Package for testing various lib/helper functions in rhodecode
24 """
24 """
25
25
26 import datetime
26 import datetime
27 import string
27 import string
28 import mock
28 import mock
29 import pytest
29 import pytest
30
30
31 from rhodecode.tests import no_newline_id_generator
31 from rhodecode.tests import no_newline_id_generator
32 from rhodecode.tests.utils import run_test_concurrently
32 from rhodecode.tests.utils import run_test_concurrently
33
33
34 from rhodecode.lib import rc_cache
34 from rhodecode.lib import rc_cache
35 from rhodecode.lib.helpers import InitialsGravatar
35 from rhodecode.lib.helpers import InitialsGravatar
36 from rhodecode.lib.utils2 import AttributeDict
36 from rhodecode.lib.utils2 import AttributeDict
37
37
38 from rhodecode.model.db import Repository, CacheKey
38 from rhodecode.model.db import Repository, CacheKey
39
39
40
40
41 def _urls_for_proto(proto):
41 def _urls_for_proto(proto):
42 return [
42 return [
43 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
43 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
44 '%s://127.0.0.1' % proto),
44 '%s://127.0.0.1' % proto),
45 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
45 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
46 '%s://127.0.0.1' % proto),
46 '%s://127.0.0.1' % proto),
47 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
47 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
48 '%s://127.0.0.1' % proto),
48 '%s://127.0.0.1' % proto),
49 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
49 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
50 '%s://127.0.0.1:8080' % proto),
50 '%s://127.0.0.1:8080' % proto),
51 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
51 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
52 '%s://domain.org' % proto),
52 '%s://domain.org' % proto),
53 ('%s://user:pass@domain.org:8080' % proto,
53 ('%s://user:pass@domain.org:8080' % proto,
54 ['%s://' % proto, 'domain.org', '8080'],
54 ['%s://' % proto, 'domain.org', '8080'],
55 '%s://domain.org:8080' % proto),
55 '%s://domain.org:8080' % proto),
56 ]
56 ]
57
57
58 TEST_URLS = _urls_for_proto('http') + _urls_for_proto('https')
58 TEST_URLS = _urls_for_proto('http') + _urls_for_proto('https')
59
59
60
60
61 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
61 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
62 def test_uri_filter(test_url, expected, expected_creds):
62 def test_uri_filter(test_url, expected, expected_creds):
63 from rhodecode.lib.utils2 import uri_filter
63 from rhodecode.lib.utils2 import uri_filter
64 assert uri_filter(test_url) == expected
64 assert uri_filter(test_url) == expected
65
65
66
66
67 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
67 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
68 def test_credentials_filter(test_url, expected, expected_creds):
68 def test_credentials_filter(test_url, expected, expected_creds):
69 from rhodecode.lib.utils2 import credentials_filter
69 from rhodecode.lib.utils2 import credentials_filter
70 assert credentials_filter(test_url) == expected_creds
70 assert credentials_filter(test_url) == expected_creds
71
71
72
72
73 @pytest.mark.parametrize("str_bool, expected", [
73 @pytest.mark.parametrize("str_bool, expected", [
74 ('t', True),
74 ('t', True),
75 ('true', True),
75 ('true', True),
76 ('y', True),
76 ('y', True),
77 ('yes', True),
77 ('yes', True),
78 ('on', True),
78 ('on', True),
79 ('1', True),
79 ('1', True),
80 ('Y', True),
80 ('Y', True),
81 ('yeS', True),
81 ('yeS', True),
82 ('Y', True),
82 ('Y', True),
83 ('TRUE', True),
83 ('TRUE', True),
84 ('T', True),
84 ('T', True),
85 ('False', False),
85 ('False', False),
86 ('F', False),
86 ('F', False),
87 ('FALSE', False),
87 ('FALSE', False),
88 ('0', False),
88 ('0', False),
89 ('-1', False),
89 ('-1', False),
90 ('', False)
90 ('', False)
91 ])
91 ])
92 def test_str2bool(str_bool, expected):
92 def test_str2bool(str_bool, expected):
93 from rhodecode.lib.utils2 import str2bool
93 from rhodecode.lib.utils2 import str2bool
94 assert str2bool(str_bool) == expected
94 assert str2bool(str_bool) == expected
95
95
96
96
97 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
97 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
98 [
98 [
99 (pref+"", []),
99 (pref+"", []),
100 (pref+"Hi there @marcink", ['marcink']),
100 (pref+"Hi there @marcink", ['marcink']),
101 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
101 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
102 (pref+"Hi there @marcink\n", ['marcink']),
102 (pref+"Hi there @marcink\n", ['marcink']),
103 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
103 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
104 (pref+"Hi there marcin@rhodecode.com", []),
104 (pref+"Hi there marcin@rhodecode.com", []),
105 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
105 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
106 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
106 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
107 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
107 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
108 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
108 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
109 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
109 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
110 (pref+"@john @mary, please review", ["john", "mary"]),
110 (pref+"@john @mary, please review", ["john", "mary"]),
111 (pref+"@john,@mary, please review", ["john", "mary"]),
111 (pref+"@john,@mary, please review", ["john", "mary"]),
112 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
112 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
113 (pref+"@first hi there @marcink here's my email marcin@email.com "
113 (pref+"@first hi there @marcink here's my email marcin@email.com "
114 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
114 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
115 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
115 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
116 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
116 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
117 (pref+"user.dot hej ! not-needed maril@domain.org", []),
117 (pref+"user.dot hej ! not-needed maril@domain.org", []),
118 (pref+"\n@marcin", ['marcin']),
118 (pref+"\n@marcin", ['marcin']),
119 ]
119 ]
120 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
120 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
121 def test_mention_extractor(text, expected):
121 def test_mention_extractor(text, expected):
122 from rhodecode.lib.utils2 import extract_mentioned_users
122 from rhodecode.lib.utils2 import extract_mentioned_users
123 got = extract_mentioned_users(text)
123 got = extract_mentioned_users(text)
124 assert sorted(got, key=lambda x: x.lower()) == got
124 assert sorted(got, key=lambda x: x.lower()) == got
125 assert set(expected) == set(got)
125 assert set(expected) == set(got)
126
126
127 @pytest.mark.parametrize("age_args, expected, kw", [
127 @pytest.mark.parametrize("age_args, expected, kw", [
128 ({}, u'just now', {}),
128 ({}, u'just now', {}),
129 ({'seconds': -1}, u'1 second ago', {}),
129 ({'seconds': -1}, u'1 second ago', {}),
130 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
130 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
131 ({'hours': -1}, u'1 hour ago', {}),
131 ({'hours': -1}, u'1 hour ago', {}),
132 ({'hours': -24}, u'1 day ago', {}),
132 ({'hours': -24}, u'1 day ago', {}),
133 ({'hours': -24 * 5}, u'5 days ago', {}),
133 ({'hours': -24 * 5}, u'5 days ago', {}),
134 ({'months': -1}, u'1 month ago', {}),
134 ({'months': -1}, u'1 month ago', {}),
135 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
135 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
136 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
136 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
137 ({}, u'just now', {'short_format': True}),
137 ({}, u'just now', {'short_format': True}),
138 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
138 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
139 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
139 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
140 ({'hours': -1}, u'1h ago', {'short_format': True}),
140 ({'hours': -1}, u'1h ago', {'short_format': True}),
141 ({'hours': -24}, u'1d ago', {'short_format': True}),
141 ({'hours': -24}, u'1d ago', {'short_format': True}),
142 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
142 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
143 ({'months': -1}, u'1m ago', {'short_format': True}),
143 ({'months': -1}, u'1m ago', {'short_format': True}),
144 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
144 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
145 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
145 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
146 ])
146 ])
147 def test_age(age_args, expected, kw, baseapp):
147 def test_age(age_args, expected, kw, baseapp):
148 from rhodecode.lib.utils2 import age
148 from rhodecode.lib.utils2 import age
149 from dateutil import relativedelta
149 from dateutil import relativedelta
150 n = datetime.datetime(year=2012, month=5, day=17)
150 n = datetime.datetime(year=2012, month=5, day=17)
151 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
151 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
152
152
153 def translate(elem):
153 def translate(elem):
154 return elem.interpolate()
154 return elem.interpolate()
155
155
156 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
156 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
157
157
158
158
159 @pytest.mark.parametrize("age_args, expected, kw", [
159 @pytest.mark.parametrize("age_args, expected, kw", [
160 ({}, u'just now', {}),
160 ({}, u'just now', {}),
161 ({'seconds': 1}, u'in 1 second', {}),
161 ({'seconds': 1}, u'in 1 second', {}),
162 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
162 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
163 ({'hours': 1}, u'in 1 hour', {}),
163 ({'hours': 1}, u'in 1 hour', {}),
164 ({'hours': 24}, u'in 1 day', {}),
164 ({'hours': 24}, u'in 1 day', {}),
165 ({'hours': 24 * 5}, u'in 5 days', {}),
165 ({'hours': 24 * 5}, u'in 5 days', {}),
166 ({'months': 1}, u'in 1 month', {}),
166 ({'months': 1}, u'in 1 month', {}),
167 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
167 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
168 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
168 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
169 ({}, u'just now', {'short_format': True}),
169 ({}, u'just now', {'short_format': True}),
170 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
170 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
171 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
171 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
172 ({'hours': 1}, u'in 1h', {'short_format': True}),
172 ({'hours': 1}, u'in 1h', {'short_format': True}),
173 ({'hours': 24}, u'in 1d', {'short_format': True}),
173 ({'hours': 24}, u'in 1d', {'short_format': True}),
174 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
174 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
175 ({'months': 1}, u'in 1m', {'short_format': True}),
175 ({'months': 1}, u'in 1m', {'short_format': True}),
176 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
176 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
177 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
177 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
178 ])
178 ])
179 def test_age_in_future(age_args, expected, kw, baseapp):
179 def test_age_in_future(age_args, expected, kw, baseapp):
180 from rhodecode.lib.utils2 import age
180 from rhodecode.lib.utils2 import age
181 from dateutil import relativedelta
181 from dateutil import relativedelta
182 n = datetime.datetime(year=2012, month=5, day=17)
182 n = datetime.datetime(year=2012, month=5, day=17)
183 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
183 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
184
184
185 def translate(elem):
185 def translate(elem):
186 return elem.interpolate()
186 return elem.interpolate()
187
187
188 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
188 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
189
189
190
190
191 @pytest.mark.parametrize("sample, expected_tags", [
191 @pytest.mark.parametrize("sample, expected_tags", [
192 # entry
192 # entry
193 ((
193 ((
194 ""
194 ""
195 ),
195 ),
196 [
196 [
197
197
198 ]),
198 ]),
199 # entry
199 # entry
200 ((
200 ((
201 "hello world [stale]"
201 "hello world [stale]"
202 ),
202 ),
203 [
203 [
204 ('state', '[stale]'),
204 ('state', '[stale]'),
205 ]),
205 ]),
206 # entry
206 # entry
207 ((
207 ((
208 "hello world [v2.0.0] [v1.0.0]"
208 "hello world [v2.0.0] [v1.0.0]"
209 ),
209 ),
210 [
210 [
211 ('generic', '[v2.0.0]'),
211 ('generic', '[v2.0.0]'),
212 ('generic', '[v1.0.0]'),
212 ('generic', '[v1.0.0]'),
213 ]),
213 ]),
214 # entry
214 # entry
215 ((
215 ((
216 "he[ll]o wo[rl]d"
216 "he[ll]o wo[rl]d"
217 ),
217 ),
218 [
218 [
219 ('label', '[ll]'),
219 ('label', '[ll]'),
220 ('label', '[rl]'),
220 ('label', '[rl]'),
221 ]),
221 ]),
222 # entry
222 # entry
223 ((
223 ((
224 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
224 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
225 ),
225 ),
226 [
226 [
227 ('state', '[stale]'),
227 ('state', '[stale]'),
228 ('state', '[featured]'),
228 ('state', '[featured]'),
229 ('state', '[stale]'),
229 ('state', '[stale]'),
230 ('state', '[dead]'),
230 ('state', '[dead]'),
231 ('state', '[dev]'),
231 ('state', '[dev]'),
232 ]),
232 ]),
233 # entry
233 # entry
234 ((
234 ((
235 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
235 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
236 ),
236 ),
237 [
237 [
238 ('state', '[stale]'),
238 ('state', '[stale]'),
239 ('url', '[url =&gt; [name](http://rc.com)]'),
239 ('url', '[url =&gt; [name](http://rc.com)]'),
240 ]),
240 ]),
241 # entry
241 # entry
242 ((
242 ((
243 "[url =&gt; [linkNameJS](javascript:alert(document.domain))]\n"
243 "[url =&gt; [linkNameJS](javascript:alert(document.domain))]\n"
244 "[url =&gt; [linkNameHTTP](http://rhodecode.com)]\n"
244 "[url =&gt; [linkNameHTTP](http://rhodecode.com)]\n"
245 "[url =&gt; [linkNameHTTPS](https://rhodecode.com)]\n"
245 "[url =&gt; [linkNameHTTPS](https://rhodecode.com)]\n"
246 "[url =&gt; [linkNamePath](/repo_group)]\n"
246 "[url =&gt; [linkNamePath](/repo_group)]\n"
247 ),
247 ),
248 [
248 [
249 ('generic', '[linkNameJS]'),
249 ('generic', '[linkNameJS]'),
250 ('url', '[url =&gt; [linkNameHTTP](http://rhodecode.com)]'),
250 ('url', '[url =&gt; [linkNameHTTP](http://rhodecode.com)]'),
251 ('url', '[url =&gt; [linkNameHTTPS](https://rhodecode.com)]'),
251 ('url', '[url =&gt; [linkNameHTTPS](https://rhodecode.com)]'),
252 ('url', '[url =&gt; [linkNamePath](/repo_group)]'),
252 ('url', '[url =&gt; [linkNamePath](/repo_group)]'),
253 ]),
253 ]),
254 # entry
254 # entry
255 ((
255 ((
256 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
256 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
257 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
257 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
258 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
258 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
259 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
259 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
260 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
260 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
261 ),
261 ),
262 [
262 [
263 ('label', '[desc]'),
263 ('label', '[desc]'),
264 ('label', '[obsolete]'),
264 ('label', '[obsolete]'),
265 ('label', '[or]'),
265 ('label', '[or]'),
266 ('label', '[requires]'),
266 ('label', '[requires]'),
267 ('label', '[tag]'),
267 ('label', '[tag]'),
268 ('state', '[stale]'),
268 ('state', '[stale]'),
269 ('lang', '[lang =&gt; python]'),
269 ('lang', '[lang =&gt; python]'),
270 ('ref', '[requires =&gt; url]'),
270 ('ref', '[requires =&gt; url]'),
271 ('see', '[see =&gt; http://url.com]'),
271 ('see', '[see =&gt; http://url.com]'),
272
272
273 ]),
273 ]),
274
274
275 ], ids=no_newline_id_generator)
275 ], ids=no_newline_id_generator)
276 def test_metatag_extraction(sample, expected_tags):
276 def test_metatag_extraction(sample, expected_tags):
277 from rhodecode.lib.helpers import extract_metatags
277 from rhodecode.lib.helpers import extract_metatags
278 tags, value = extract_metatags(sample)
278 tags, value = extract_metatags(sample)
279 assert sorted(tags) == sorted(expected_tags)
279 assert sorted(tags) == sorted(expected_tags)
280
280
281
281
282 @pytest.mark.parametrize("tag_data, expected_html", [
282 @pytest.mark.parametrize("tag_data, expected_html", [
283
283
284 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
284 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
285 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
285 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
286 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
286 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
287 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
287 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
288 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
288 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
289
289
290 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
290 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
291 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
291 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
292
292
293 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
293 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
294 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
294 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
295 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
295 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
296 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
296 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
297 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
297 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
298 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
298 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
299
299
300 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
300 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
301 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
301 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
302 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
302 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
303 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
303 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
304
304
305 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires: <a href="/RepoName">RepoName</a></div>'),
305 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires: <a href="/RepoName">RepoName</a></div>'),
306 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends: <a href="/GroupName">GroupName</a></div>'),
306 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends: <a href="/GroupName">GroupName</a></div>'),
307 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts: <a href="/SomeName">SomeName</a></div>'),
307 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts: <a href="/SomeName">SomeName</a></div>'),
308 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base: <a href="/SomeName">SomeName</a></div>'),
308 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base: <a href="/SomeName">SomeName</a></div>'),
309
309
310 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see: http://rhodecode.com </div>'),
310 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see: http://rhodecode.com </div>'),
311
311
312 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
312 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
313 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
313 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
314 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
314 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
315
315
316 ])
316 ])
317 def test_metatags_stylize(tag_data, expected_html):
317 def test_metatags_stylize(tag_data, expected_html):
318 from rhodecode.lib.helpers import style_metatag
318 from rhodecode.lib.helpers import style_metatag
319 tag_type,value = tag_data
319 tag_type,value = tag_data
320 assert style_metatag(tag_type, value) == expected_html
320 assert style_metatag(tag_type, value) == expected_html
321
321
322
322
323 @pytest.mark.parametrize("tmpl_url, email, expected", [
323 @pytest.mark.parametrize("tmpl_url, email, expected", [
324 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
324 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
325
325
326 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
326 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
327 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
327 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
328
328
329 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
329 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
330 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
330 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
331
331
332 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
332 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
333 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
333 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
334
334
335 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
335 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
336 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
336 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
337 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
337 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
338 ])
338 ])
339 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
339 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
340 from rhodecode.lib.helpers import gravatar_url
340 from rhodecode.lib.helpers import gravatar_url
341
341
342 def fake_tmpl_context(_url):
342 def fake_tmpl_context(_url):
343 _c = AttributeDict()
343 _c = AttributeDict()
344 _c.visual = AttributeDict()
344 _c.visual = AttributeDict()
345 _c.visual.use_gravatar = True
345 _c.visual.use_gravatar = True
346 _c.visual.gravatar_url = _url
346 _c.visual.gravatar_url = _url
347 return _c
347 return _c
348
348
349 # mock pyramid.threadlocals
349 # mock pyramid.threadlocals
350 def fake_get_current_request():
350 def fake_get_current_request():
351 request_stub.scheme = 'https'
351 request_stub.scheme = 'https'
352 request_stub.host = 'server.com'
352 request_stub.host = 'server.com'
353
353
354 request_stub._call_context = fake_tmpl_context(tmpl_url)
354 request_stub._call_context = fake_tmpl_context(tmpl_url)
355 return request_stub
355 return request_stub
356
356
357 with mock.patch('rhodecode.lib.helpers.get_current_request',
357 with mock.patch('rhodecode.lib.helpers.get_current_request',
358 fake_get_current_request):
358 fake_get_current_request):
359
359
360 grav = gravatar_url(email_address=email, size=24)
360 grav = gravatar_url(email_address=email, size=24)
361 assert grav == expected
361 assert grav == expected
362
362
363
363
364 @pytest.mark.parametrize(
364 @pytest.mark.parametrize(
365 "email, first_name, last_name, expected_initials, expected_color", [
365 "email, first_name, last_name, expected_initials, expected_color", [
366
366
367 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
367 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
368 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
368 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
369 # special cases of email
369 # special cases of email
370 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
370 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
371 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
371 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
372 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
372 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
373
373
374 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
374 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
375 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
375 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
376
376
377 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
377 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
378 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
378 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
379 # partials
379 # partials
380 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
380 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
381 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
381 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
382 # non-ascii
382 # non-ascii
383 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
383 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
384 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
384 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
385
385
386 # special cases, LDAP can provide those...
386 # special cases, LDAP can provide those...
387 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
387 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
388 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
388 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
389 ('null', '', '', 'NL', '#8c4646'),
389 ('null', '', '', 'NL', '#8c4646'),
390 ('some.@abc.com', 'some', '', 'SA', '#664e33')
390 ('some.@abc.com', 'some', '', 'SA', '#664e33')
391 ])
391 ])
392 def test_initials_gravatar_pick_of_initials_and_color_algo(
392 def test_initials_gravatar_pick_of_initials_and_color_algo(
393 email, first_name, last_name, expected_initials, expected_color):
393 email, first_name, last_name, expected_initials, expected_color):
394 instance = InitialsGravatar(email, first_name, last_name)
394 instance = InitialsGravatar(email, first_name, last_name)
395 assert instance.get_initials() == expected_initials
395 assert instance.get_initials() == expected_initials
396 assert instance.str2color(email) == expected_color
396 assert instance.str2color(email) == expected_color
397
397
398
398
399 def test_initials_gravatar_mapping_algo():
399 def test_initials_gravatar_mapping_algo():
400 pos = set()
400 pos = set()
401 instance = InitialsGravatar('', '', '')
401 instance = InitialsGravatar('', '', '')
402 iterations = 0
402 iterations = 0
403
403
404 variations = []
404 variations = []
405 for letter1 in string.ascii_letters:
405 for letter1 in string.ascii_letters:
406 for letter2 in string.ascii_letters[::-1][:10]:
406 for letter2 in string.ascii_letters[::-1][:10]:
407 for letter3 in string.ascii_letters[:10]:
407 for letter3 in string.ascii_letters[:10]:
408 variations.append(
408 variations.append(
409 '%s@rhodecode.com' % (letter1+letter2+letter3))
409 '%s@rhodecode.com' % (letter1+letter2+letter3))
410
410
411 max_variations = 4096
411 max_variations = 4096
412 for email in variations[:max_variations]:
412 for email in variations[:max_variations]:
413 iterations += 1
413 iterations += 1
414 pos.add(
414 pos.add(
415 instance.pick_color_bank_index(email,
415 instance.pick_color_bank_index(email,
416 instance.get_color_bank()))
416 instance.get_color_bank()))
417
417
418 # we assume that we have match all 256 possible positions,
418 # we assume that we have match all 256 possible positions,
419 # in reasonable amount of different email addresses
419 # in reasonable amount of different email addresses
420 assert len(pos) == 256
420 assert len(pos) == 256
421 assert iterations == max_variations
421 assert iterations == max_variations
422
422
423
423
424 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
424 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
425 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
425 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
426 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
426 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
427 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
427 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
428 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
428 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
429 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
429 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
430 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
430 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
431 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
431 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
432 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
432 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
433 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
433 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
434 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
434 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
435 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
435 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
436 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
436 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
437 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
437 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
438 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
438 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
439 ])
439 ])
440 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
440 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
441 from rhodecode.lib.utils2 import get_clone_url
441 from rhodecode.lib.utils2 import get_clone_url
442
442
443 class RequestStub(object):
443 class RequestStub(object):
444 def request_url(self, name):
444 def request_url(self, name):
445 return 'http://vps1:8000' + prefix
445 return 'http://vps1:8000' + prefix
446
446
447 def route_url(self, name):
447 def route_url(self, name):
448 return self.request_url(name)
448 return self.request_url(name)
449
449
450 clone_url = get_clone_url(
450 clone_url = get_clone_url(
451 request=RequestStub(),
451 request=RequestStub(),
452 uri_tmpl=tmpl,
452 uri_tmpl=tmpl,
453 repo_name=repo_name, repo_id=23, **overrides)
453 repo_name=repo_name, repo_id=23, **overrides)
454 assert clone_url == expected
454 assert clone_url == expected
455
455
456
456
457 def _quick_url(text, tmpl="""<a class="revision-link" href="%s">%s</a>""", url_=None):
457 idx = 0
458
459
460 def _quick_url(text, tmpl="""<a class="tooltip-hovercard revision-link" href="%s" data-hovercard-alt="Commit: %s" data-hovercard-url="/some-url">%s</a>""", url_=None, commits=''):
458 """
461 """
459 Changes `some text url[foo]` => `some text <a href="/">foo</a>
462 Changes `some text url[foo]` => `some text <a href="/">foo</a>
460
463
461 :param text:
464 :param text:
462 """
465 """
463 import re
466 import re
464 # quickly change expected url[] into a link
467 # quickly change expected url[] into a link
465 URL_PAT = re.compile(r'(?:url\[)(.+?)(?:\])')
468 url_pat = re.compile(r'(?:url\[)(.+?)(?:\])')
469 commits = commits or []
470
471 global idx
472 idx = 0
466
473
467 def url_func(match_obj):
474 def url_func(match_obj):
475 global idx
468 _url = match_obj.groups()[0]
476 _url = match_obj.groups()[0]
469 return tmpl % (url_ or '/some-url', _url)
477 if commits:
470 return URL_PAT.sub(url_func, text)
478 commit = commits[idx]
479 idx += 1
480 return tmpl % (url_ or '/some-url', _url, commit)
481 else:
482 return tmpl % (url_ or '/some-url', _url)
483
484 return url_pat.sub(url_func, text)
471
485
472
486
473 @pytest.mark.parametrize("sample, expected", [
487 @pytest.mark.parametrize("sample, expected, commits", [
474 ("",
488 (
475 ""),
489 "",
476 ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
490 "",
477 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68"),
491 [""]
478 ("from rev 000000000000",
492 ),
479 "from rev url[000000000000]"),
493 (
480 ("from rev 000000000000123123 also rev 000000000000",
494 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
481 "from rev url[000000000000123123] also rev url[000000000000]"),
495 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
482 ("this should-000 00",
496 [""]
483 "this should-000 00"),
497 ),
484 ("longtextffffffffff rev 123123123123",
498 (
485 "longtextffffffffff rev url[123123123123]"),
499 "from rev 000000000000",
486 ("rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
500 "from rev url[000000000000]",
487 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff"),
501 ["000000000000"]
488 ("ffffffffffff some text traalaa",
502 ),
489 "url[ffffffffffff] some text traalaa"),
503
490 ("""Multi line
504 (
491 123123123123
505 "from rev 000000000000123123 also rev 000000000000",
492 some text 123123123123
506 "from rev url[000000000000123123] also rev url[000000000000]",
493 sometimes !
507 ["000000000000123123", "000000000000"]
494 """,
508 ),
495 """Multi line
509 (
496 url[123123123123]
510 "this should-000 00",
497 some text url[123123123123]
511 "this should-000 00",
498 sometimes !
512 [""]
499 """)
513 ),
514 (
515 "longtextffffffffff rev 123123123123",
516 "longtextffffffffff rev url[123123123123]",
517 ["123123123123"]
518 ),
519 (
520 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
521 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
522 ["ffffffffffffffffffffffffffffffffffffffffffffffffff"]
523 ),
524 (
525 "ffffffffffff some text traalaa",
526 "url[ffffffffffff] some text traalaa",
527 ["ffffffffffff"]
528 ),
529 (
530 """Multi line
531 123123123123
532 some text 000000000000
533 sometimes !
534 """,
535 """Multi line
536 url[123123123123]
537 some text url[000000000000]
538 sometimes !
539 """,
540 ["123123123123", "000000000000"]
541 )
500 ], ids=no_newline_id_generator)
542 ], ids=no_newline_id_generator)
501 def test_urlify_commits(sample, expected):
543 def test_urlify_commits(sample, expected, commits):
502 def fake_url(self, *args, **kwargs):
544 def fake_url(self, *args, **kwargs):
503 return '/some-url'
545 return '/some-url'
504
546
505 expected = _quick_url(expected)
547 expected = _quick_url(expected, commits=commits)
506
548
507 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
549 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
508 from rhodecode.lib.helpers import urlify_commits
550 from rhodecode.lib.helpers import urlify_commits
509 assert urlify_commits(sample, 'repo_name') == expected
551 assert urlify_commits(sample, 'repo_name') == expected
510
552
511
553
512 @pytest.mark.parametrize("sample, expected, url_", [
554 @pytest.mark.parametrize("sample, expected, url_", [
513 ("",
555 ("",
514 "",
556 "",
515 ""),
557 ""),
516 ("https://svn.apache.org/repos",
558 ("https://svn.apache.org/repos",
517 "url[https://svn.apache.org/repos]",
559 "url[https://svn.apache.org/repos]",
518 "https://svn.apache.org/repos"),
560 "https://svn.apache.org/repos"),
519 ("http://svn.apache.org/repos",
561 ("http://svn.apache.org/repos",
520 "url[http://svn.apache.org/repos]",
562 "url[http://svn.apache.org/repos]",
521 "http://svn.apache.org/repos"),
563 "http://svn.apache.org/repos"),
522 ("from rev a also rev http://google.com",
564 ("from rev a also rev http://google.com",
523 "from rev a also rev url[http://google.com]",
565 "from rev a also rev url[http://google.com]",
524 "http://google.com"),
566 "http://google.com"),
525 ("""Multi line
567 ("""Multi line
526 https://foo.bar.com
568 https://foo.bar.com
527 some text lalala""",
569 some text lalala""",
528 """Multi line
570 """Multi line
529 url[https://foo.bar.com]
571 url[https://foo.bar.com]
530 some text lalala""",
572 some text lalala""",
531 "https://foo.bar.com")
573 "https://foo.bar.com")
532 ], ids=no_newline_id_generator)
574 ], ids=no_newline_id_generator)
533 def test_urlify_test(sample, expected, url_):
575 def test_urlify_test(sample, expected, url_):
534 from rhodecode.lib.helpers import urlify_text
576 from rhodecode.lib.helpers import urlify_text
535 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
577 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
536 assert urlify_text(sample) == expected
578 assert urlify_text(sample) == expected
537
579
538
580
539 @pytest.mark.parametrize("test, expected", [
581 @pytest.mark.parametrize("test, expected", [
540 ("", None),
582 ("", None),
541 ("/_2", '2'),
583 ("/_2", '2'),
542 ("_2", '2'),
584 ("_2", '2'),
543 ("/_2/", '2'),
585 ("/_2/", '2'),
544 ("_2/", '2'),
586 ("_2/", '2'),
545
587
546 ("/_21", '21'),
588 ("/_21", '21'),
547 ("_21", '21'),
589 ("_21", '21'),
548 ("/_21/", '21'),
590 ("/_21/", '21'),
549 ("_21/", '21'),
591 ("_21/", '21'),
550
592
551 ("/_21/foobar", '21'),
593 ("/_21/foobar", '21'),
552 ("_21/121", '21'),
594 ("_21/121", '21'),
553 ("/_21/_12", '21'),
595 ("/_21/_12", '21'),
554 ("_21/rc/foo", '21'),
596 ("_21/rc/foo", '21'),
555
597
556 ])
598 ])
557 def test_get_repo_by_id(test, expected):
599 def test_get_repo_by_id(test, expected):
558 from rhodecode.model.repo import RepoModel
600 from rhodecode.model.repo import RepoModel
559 _test = RepoModel()._extract_id_from_repo_name(test)
601 _test = RepoModel()._extract_id_from_repo_name(test)
560 assert _test == expected
602 assert _test == expected
561
603
562
604
563 def test_invalidation_context(baseapp):
605 def test_invalidation_context(baseapp):
564 repo_id = 9999
606 repo_id = 9999
565
607
566 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
608 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
567 repo_id, CacheKey.CACHE_TYPE_FEED)
609 repo_id, CacheKey.CACHE_TYPE_FEED)
568 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
610 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
569 repo_id=repo_id)
611 repo_id=repo_id)
570 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
612 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
571
613
572 calls = [1, 2]
614 calls = [1, 2]
573
615
574 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
616 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
575 def _dummy_func(cache_key):
617 def _dummy_func(cache_key):
576 val = calls.pop(0)
618 val = calls.pop(0)
577 return 'result:{}'.format(val)
619 return 'result:{}'.format(val)
578
620
579 inv_context_manager = rc_cache.InvalidationContext(
621 inv_context_manager = rc_cache.InvalidationContext(
580 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
622 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
581
623
582 # 1st call, fresh caches
624 # 1st call, fresh caches
583 with inv_context_manager as invalidation_context:
625 with inv_context_manager as invalidation_context:
584 should_invalidate = invalidation_context.should_invalidate()
626 should_invalidate = invalidation_context.should_invalidate()
585 if should_invalidate:
627 if should_invalidate:
586 result = _dummy_func.refresh('some-key')
628 result = _dummy_func.refresh('some-key')
587 else:
629 else:
588 result = _dummy_func('some-key')
630 result = _dummy_func('some-key')
589
631
590 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
632 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
591 assert should_invalidate is True
633 assert should_invalidate is True
592
634
593 assert 'result:1' == result
635 assert 'result:1' == result
594 # should be cached so calling it twice will give the same result !
636 # should be cached so calling it twice will give the same result !
595 result = _dummy_func('some-key')
637 result = _dummy_func('some-key')
596 assert 'result:1' == result
638 assert 'result:1' == result
597
639
598 # 2nd call, we create a new context manager, this should be now key aware, and
640 # 2nd call, we create a new context manager, this should be now key aware, and
599 # return an active cache region
641 # return an active cache region
600 with inv_context_manager as invalidation_context:
642 with inv_context_manager as invalidation_context:
601 should_invalidate = invalidation_context.should_invalidate()
643 should_invalidate = invalidation_context.should_invalidate()
602 assert isinstance(invalidation_context, rc_cache.ActiveRegionCache)
644 assert isinstance(invalidation_context, rc_cache.ActiveRegionCache)
603 assert should_invalidate is False
645 assert should_invalidate is False
604
646
605 # Mark invalidation
647 # Mark invalidation
606 CacheKey.set_invalidate(invalidation_namespace)
648 CacheKey.set_invalidate(invalidation_namespace)
607
649
608 # 3nd call, fresh caches
650 # 3nd call, fresh caches
609 with inv_context_manager as invalidation_context:
651 with inv_context_manager as invalidation_context:
610 should_invalidate = invalidation_context.should_invalidate()
652 should_invalidate = invalidation_context.should_invalidate()
611 if should_invalidate:
653 if should_invalidate:
612 result = _dummy_func.refresh('some-key')
654 result = _dummy_func.refresh('some-key')
613 else:
655 else:
614 result = _dummy_func('some-key')
656 result = _dummy_func('some-key')
615
657
616 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
658 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
617 assert should_invalidate is True
659 assert should_invalidate is True
618
660
619 assert 'result:2' == result
661 assert 'result:2' == result
620
662
621 # cached again, same result
663 # cached again, same result
622 result = _dummy_func('some-key')
664 result = _dummy_func('some-key')
623 assert 'result:2' == result
665 assert 'result:2' == result
624
666
625
667
626 def test_invalidation_context_exception_in_compute(baseapp):
668 def test_invalidation_context_exception_in_compute(baseapp):
627 repo_id = 888
669 repo_id = 888
628
670
629 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
671 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
630 repo_id, CacheKey.CACHE_TYPE_FEED)
672 repo_id, CacheKey.CACHE_TYPE_FEED)
631 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
673 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
632 repo_id=repo_id)
674 repo_id=repo_id)
633 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
675 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
634
676
635 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
677 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
636 def _dummy_func(cache_key):
678 def _dummy_func(cache_key):
637 raise Exception('Error in cache func')
679 raise Exception('Error in cache func')
638
680
639 with pytest.raises(Exception):
681 with pytest.raises(Exception):
640 inv_context_manager = rc_cache.InvalidationContext(
682 inv_context_manager = rc_cache.InvalidationContext(
641 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
683 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
642
684
643 # 1st call, fresh caches
685 # 1st call, fresh caches
644 with inv_context_manager as invalidation_context:
686 with inv_context_manager as invalidation_context:
645 should_invalidate = invalidation_context.should_invalidate()
687 should_invalidate = invalidation_context.should_invalidate()
646 if should_invalidate:
688 if should_invalidate:
647 _dummy_func.refresh('some-key-2')
689 _dummy_func.refresh('some-key-2')
648 else:
690 else:
649 _dummy_func('some-key-2')
691 _dummy_func('some-key-2')
650
692
651
693
652 @pytest.mark.parametrize('execution_number', range(5))
694 @pytest.mark.parametrize('execution_number', range(5))
653 def test_cache_invalidation_race_condition(execution_number, baseapp):
695 def test_cache_invalidation_race_condition(execution_number, baseapp):
654 import time
696 import time
655
697
656 repo_id = 777
698 repo_id = 777
657
699
658 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
700 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
659 repo_id, CacheKey.CACHE_TYPE_FEED)
701 repo_id, CacheKey.CACHE_TYPE_FEED)
660 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
702 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
661 repo_id=repo_id)
703 repo_id=repo_id)
662 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
704 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
663
705
664 @run_test_concurrently(25)
706 @run_test_concurrently(25)
665 def test_create_and_delete_cache_keys():
707 def test_create_and_delete_cache_keys():
666 time.sleep(0.2)
708 time.sleep(0.2)
667
709
668 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
710 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
669 def _dummy_func(cache_key):
711 def _dummy_func(cache_key):
670 val = 'async'
712 val = 'async'
671 return 'result:{}'.format(val)
713 return 'result:{}'.format(val)
672
714
673 inv_context_manager = rc_cache.InvalidationContext(
715 inv_context_manager = rc_cache.InvalidationContext(
674 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
716 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
675
717
676 # 1st call, fresh caches
718 # 1st call, fresh caches
677 with inv_context_manager as invalidation_context:
719 with inv_context_manager as invalidation_context:
678 should_invalidate = invalidation_context.should_invalidate()
720 should_invalidate = invalidation_context.should_invalidate()
679 if should_invalidate:
721 if should_invalidate:
680 _dummy_func.refresh('some-key-3')
722 _dummy_func.refresh('some-key-3')
681 else:
723 else:
682 _dummy_func('some-key-3')
724 _dummy_func('some-key-3')
683
725
684 # Mark invalidation
726 # Mark invalidation
685 CacheKey.set_invalidate(invalidation_namespace)
727 CacheKey.set_invalidate(invalidation_namespace)
686
728
687 test_create_and_delete_cache_keys()
729 test_create_and_delete_cache_keys()
General Comments 0
You need to be logged in to leave comments. Login now