##// END OF EJS Templates
auth: login/registration changes for upcomming new rules for login using external identities....
marcink -
r3386:e8cf67e0 default
parent child Browse files
Show More
@@ -1,121 +1,118 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.tests import (
25 25 TestController, clear_cache_regions,
26 26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 27 from rhodecode.tests.fixture import Fixture
28 28 from rhodecode.tests.utils import AssertResponse
29 29
30 30 fixture = Fixture()
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 34 import urllib
35 35 from rhodecode.apps._base import ADMIN_PREFIX
36 36
37 37 base_url = {
38 38 'login': ADMIN_PREFIX + '/login',
39 39 'logout': ADMIN_PREFIX + '/logout',
40 40 'register': ADMIN_PREFIX + '/register',
41 41 'reset_password':
42 42 ADMIN_PREFIX + '/password_reset',
43 43 'reset_password_confirmation':
44 44 ADMIN_PREFIX + '/password_reset_confirmation',
45 45
46 46 'admin_permissions_application':
47 47 ADMIN_PREFIX + '/permissions/application',
48 48 'admin_permissions_application_update':
49 49 ADMIN_PREFIX + '/permissions/application/update',
50 50 }[name].format(**kwargs)
51 51
52 52 if params:
53 53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
54 54 return base_url
55 55
56 56
57 57 class TestPasswordReset(TestController):
58 58
59 59 @pytest.mark.parametrize(
60 60 'pwd_reset_setting, show_link, show_reset', [
61 61 ('hg.password_reset.enabled', True, True),
62 62 ('hg.password_reset.hidden', False, True),
63 63 ('hg.password_reset.disabled', False, False),
64 64 ])
65 65 def test_password_reset_settings(
66 66 self, pwd_reset_setting, show_link, show_reset):
67 67 clear_cache_regions()
68 68 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 69 params = {
70 70 'csrf_token': self.csrf_token,
71 71 'anonymous': 'True',
72 72 'default_register': 'hg.register.auto_activate',
73 73 'default_register_message': '',
74 74 'default_password_reset': pwd_reset_setting,
75 75 'default_extern_activate': 'hg.extern_activate.auto',
76 76 }
77 resp = self.app.post(route_path('admin_permissions_application_update'), params=params)
77 resp = self.app.post(
78 route_path('admin_permissions_application_update'), params=params)
78 79 self.logout_user()
79 80
80 81 login_page = self.app.get(route_path('login'))
81 82 asr_login = AssertResponse(login_page)
82 index_page = self.app.get(h.route_path('home'))
83 asr_index = AssertResponse(index_page)
84 83
85 84 if show_link:
86 85 asr_login.one_element_exists('a.pwd_reset')
87 asr_index.one_element_exists('a.pwd_reset')
88 86 else:
89 87 asr_login.no_element_exists('a.pwd_reset')
90 asr_index.no_element_exists('a.pwd_reset')
91 88
92 89 response = self.app.get(route_path('reset_password'))
93 90
94 91 assert_response = AssertResponse(response)
95 92 if show_reset:
96 93 response.mustcontain('Send password reset email')
97 94 assert_response.one_element_exists('#email')
98 95 assert_response.one_element_exists('#send')
99 96 else:
100 97 response.mustcontain('Password reset is disabled.')
101 98 assert_response.no_element_exists('#email')
102 99 assert_response.no_element_exists('#send')
103 100
104 101 def test_password_form_disabled(self):
105 102 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
106 103 params = {
107 104 'csrf_token': self.csrf_token,
108 105 'anonymous': 'True',
109 106 'default_register': 'hg.register.auto_activate',
110 107 'default_register_message': '',
111 108 'default_password_reset': 'hg.password_reset.disabled',
112 109 'default_extern_activate': 'hg.extern_activate.auto',
113 110 }
114 111 self.app.post(route_path('admin_permissions_application_update'), params=params)
115 112 self.logout_user()
116 113
117 114 response = self.app.post(
118 115 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
119 116 )
120 117 response = response.follow()
121 118 response.mustcontain('Password reset is disabled.')
@@ -1,153 +1,171 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 import colander
28
29 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 30 from rhodecode.translation import _
28 31
29 32 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 34 from rhodecode.lib.utils2 import safe_str
32 35 from rhodecode.model.db import User
33 36
34 37 log = logging.getLogger(__name__)
35 38
36 39
37 40 def plugin_factory(plugin_id, *args, **kwargs):
38 41 plugin = RhodeCodeAuthPlugin(plugin_id)
39 42 return plugin
40 43
41 44
42 45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 46 pass
44 47
45 48
49 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
50
51 superadmin_restriction = colander.SchemaNode(
52 colander.Bool(),
53 default=False,
54 description=_('Only allow super-admins to log-in using this plugin.'),
55 missing=False,
56 title=_('Enabled'),
57 widget='bool',
58 )
59
60
46 61 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 62 uid = 'rhodecode'
48 63
49 64 def includeme(self, config):
50 65 config.add_authn_plugin(self)
51 66 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
52 67 config.add_view(
53 68 'rhodecode.authentication.views.AuthnPluginViewBase',
54 69 attr='settings_get',
55 70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
56 71 request_method='GET',
57 72 route_name='auth_home',
58 73 context=RhodecodeAuthnResource)
59 74 config.add_view(
60 75 'rhodecode.authentication.views.AuthnPluginViewBase',
61 76 attr='settings_post',
62 77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 78 request_method='POST',
64 79 route_name='auth_home',
65 80 context=RhodecodeAuthnResource)
66 81
82 def get_settings_schema(self):
83 return RhodeCodeSettingsSchema()
84
67 85 def get_display_name(self):
68 86 return _('RhodeCode Internal')
69 87
70 88 @classmethod
71 89 def docs(cls):
72 90 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
73 91
74 92 @hybrid_property
75 93 def name(self):
76 94 return u"rhodecode"
77 95
78 96 def user_activation_state(self):
79 97 def_user_perms = User.get_default_user().AuthUser().permissions['global']
80 98 return 'hg.register.auto_activate' in def_user_perms
81 99
82 100 def allows_authentication_from(
83 101 self, user, allows_non_existing_user=True,
84 102 allowed_auth_plugins=None, allowed_auth_sources=None):
85 103 """
86 104 Custom method for this auth that doesn't accept non existing users.
87 105 We know that user exists in our database.
88 106 """
89 107 allows_non_existing_user = False
90 108 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
91 109 user, allows_non_existing_user=allows_non_existing_user)
92 110
93 111 def auth(self, userobj, username, password, settings, **kwargs):
94 112 if not userobj:
95 113 log.debug('userobj was:%s skipping', userobj)
96 114 return None
97 115 if userobj.extern_type != self.name:
98 116 log.warning(
99 117 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
100 118 userobj, userobj.extern_type, self.name)
101 119 return None
102 120
103 121 user_attrs = {
104 122 "username": userobj.username,
105 123 "firstname": userobj.firstname,
106 124 "lastname": userobj.lastname,
107 125 "groups": [],
108 126 'user_group_sync': False,
109 127 "email": userobj.email,
110 128 "admin": userobj.admin,
111 129 "active": userobj.active,
112 130 "active_from_extern": userobj.active,
113 131 "extern_name": userobj.user_id,
114 132 "extern_type": userobj.extern_type,
115 133 }
116 134
117 135 log.debug("User attributes:%s", user_attrs)
118 136 if userobj.active:
119 137 from rhodecode.lib import auth
120 138 crypto_backend = auth.crypto_backend()
121 139 password_encoded = safe_str(password)
122 140 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
123 141 password_encoded, userobj.password or '')
124 142
125 143 if password_match and new_hash:
126 144 log.debug('user %s properly authenticated, but '
127 145 'requires hash change to bcrypt', userobj)
128 146 # if password match, and we use OLD deprecated hash,
129 147 # we should migrate this user hash password to the new hash
130 148 # we store the new returned by hash_check_with_upgrade function
131 149 user_attrs['_hash_migrate'] = new_hash
132 150
133 151 if userobj.username == User.DEFAULT_USER and userobj.active:
134 152 log.info(
135 153 'user `%s` authenticated correctly as anonymous user', userobj.username)
136 154 return user_attrs
137 155
138 156 elif userobj.username == username and password_match:
139 157 log.info('user `%s` authenticated correctly', userobj.username)
140 158 return user_attrs
141 159 log.warn("user `%s` used a wrong password when "
142 160 "authenticating on this plugin", userobj.username)
143 161 return None
144 162 else:
145 163 log.warning(
146 164 'user `%s` failed to authenticate via %s, reason: account not '
147 165 'active.', username, self.name)
148 166 return None
149 167
150 168
151 169 def includeme(config):
152 170 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
153 171 plugin_factory(plugin_id).includeme(config)
@@ -1,2019 +1,2018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import urlparse
38 38 import time
39 39 import string
40 40 import hashlib
41 41 from collections import OrderedDict
42 42
43 43 import pygments
44 44 import itertools
45 45 import fnmatch
46 46 import bleach
47 47
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202 get_error = _GetError()
203 203
204 204
205 205 class _ToolTip(object):
206 206
207 207 def __call__(self, tooltip_title, trim_at=50):
208 208 """
209 209 Special function just to wrap our text into nice formatted
210 210 autowrapped text
211 211
212 212 :param tooltip_title:
213 213 """
214 214 tooltip_title = escape(tooltip_title)
215 215 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
216 216 return tooltip_title
217 217 tooltip = _ToolTip()
218 218
219 219
220 220 def files_breadcrumbs(repo_name, commit_id, file_path):
221 221 if isinstance(file_path, str):
222 222 file_path = safe_unicode(file_path)
223 223
224 224 # TODO: johbo: Is this always a url like path, or is this operating
225 225 # system dependent?
226 226 path_segments = file_path.split('/')
227 227
228 228 repo_name_html = escape(repo_name)
229 229 if len(path_segments) == 1 and path_segments[0] == '':
230 230 url_segments = [repo_name_html]
231 231 else:
232 232 url_segments = [
233 233 link_to(
234 234 repo_name_html,
235 235 route_path(
236 236 'repo_files',
237 237 repo_name=repo_name,
238 238 commit_id=commit_id,
239 239 f_path=''),
240 240 class_='pjax-link')]
241 241
242 242 last_cnt = len(path_segments) - 1
243 243 for cnt, segment in enumerate(path_segments):
244 244 if not segment:
245 245 continue
246 246 segment_html = escape(segment)
247 247
248 248 if cnt != last_cnt:
249 249 url_segments.append(
250 250 link_to(
251 251 segment_html,
252 252 route_path(
253 253 'repo_files',
254 254 repo_name=repo_name,
255 255 commit_id=commit_id,
256 256 f_path='/'.join(path_segments[:cnt + 1])),
257 257 class_='pjax-link'))
258 258 else:
259 259 url_segments.append(segment_html)
260 260
261 261 return literal('/'.join(url_segments))
262 262
263 263
264 264 def code_highlight(code, lexer, formatter, use_hl_filter=False):
265 265 """
266 266 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
267 267
268 268 If ``outfile`` is given and a valid file object (an object
269 269 with a ``write`` method), the result will be written to it, otherwise
270 270 it is returned as a string.
271 271 """
272 272 if use_hl_filter:
273 273 # add HL filter
274 274 from rhodecode.lib.index import search_utils
275 275 lexer.add_filter(search_utils.ElasticSearchHLFilter())
276 276 return pygments.format(pygments.lex(code, lexer), formatter)
277 277
278 278
279 279 class CodeHtmlFormatter(HtmlFormatter):
280 280 """
281 281 My code Html Formatter for source codes
282 282 """
283 283
284 284 def wrap(self, source, outfile):
285 285 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
286 286
287 287 def _wrap_code(self, source):
288 288 for cnt, it in enumerate(source):
289 289 i, t = it
290 290 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
291 291 yield i, t
292 292
293 293 def _wrap_tablelinenos(self, inner):
294 294 dummyoutfile = StringIO.StringIO()
295 295 lncount = 0
296 296 for t, line in inner:
297 297 if t:
298 298 lncount += 1
299 299 dummyoutfile.write(line)
300 300
301 301 fl = self.linenostart
302 302 mw = len(str(lncount + fl - 1))
303 303 sp = self.linenospecial
304 304 st = self.linenostep
305 305 la = self.lineanchors
306 306 aln = self.anchorlinenos
307 307 nocls = self.noclasses
308 308 if sp:
309 309 lines = []
310 310
311 311 for i in range(fl, fl + lncount):
312 312 if i % st == 0:
313 313 if i % sp == 0:
314 314 if aln:
315 315 lines.append('<a href="#%s%d" class="special">%*d</a>' %
316 316 (la, i, mw, i))
317 317 else:
318 318 lines.append('<span class="special">%*d</span>' % (mw, i))
319 319 else:
320 320 if aln:
321 321 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
322 322 else:
323 323 lines.append('%*d' % (mw, i))
324 324 else:
325 325 lines.append('')
326 326 ls = '\n'.join(lines)
327 327 else:
328 328 lines = []
329 329 for i in range(fl, fl + lncount):
330 330 if i % st == 0:
331 331 if aln:
332 332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 333 else:
334 334 lines.append('%*d' % (mw, i))
335 335 else:
336 336 lines.append('')
337 337 ls = '\n'.join(lines)
338 338
339 339 # in case you wonder about the seemingly redundant <div> here: since the
340 340 # content in the other cell also is wrapped in a div, some browsers in
341 341 # some configurations seem to mess up the formatting...
342 342 if nocls:
343 343 yield 0, ('<table class="%stable">' % self.cssclass +
344 344 '<tr><td><div class="linenodiv" '
345 345 'style="background-color: #f0f0f0; padding-right: 10px">'
346 346 '<pre style="line-height: 125%">' +
347 347 ls + '</pre></div></td><td id="hlcode" class="code">')
348 348 else:
349 349 yield 0, ('<table class="%stable">' % self.cssclass +
350 350 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
351 351 ls + '</pre></div></td><td id="hlcode" class="code">')
352 352 yield 0, dummyoutfile.getvalue()
353 353 yield 0, '</td></tr></table>'
354 354
355 355
356 356 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
357 357 def __init__(self, **kw):
358 358 # only show these line numbers if set
359 359 self.only_lines = kw.pop('only_line_numbers', [])
360 360 self.query_terms = kw.pop('query_terms', [])
361 361 self.max_lines = kw.pop('max_lines', 5)
362 362 self.line_context = kw.pop('line_context', 3)
363 363 self.url = kw.pop('url', None)
364 364
365 365 super(CodeHtmlFormatter, self).__init__(**kw)
366 366
367 367 def _wrap_code(self, source):
368 368 for cnt, it in enumerate(source):
369 369 i, t = it
370 370 t = '<pre>%s</pre>' % t
371 371 yield i, t
372 372
373 373 def _wrap_tablelinenos(self, inner):
374 374 yield 0, '<table class="code-highlight %stable">' % self.cssclass
375 375
376 376 last_shown_line_number = 0
377 377 current_line_number = 1
378 378
379 379 for t, line in inner:
380 380 if not t:
381 381 yield t, line
382 382 continue
383 383
384 384 if current_line_number in self.only_lines:
385 385 if last_shown_line_number + 1 != current_line_number:
386 386 yield 0, '<tr>'
387 387 yield 0, '<td class="line">...</td>'
388 388 yield 0, '<td id="hlcode" class="code"></td>'
389 389 yield 0, '</tr>'
390 390
391 391 yield 0, '<tr>'
392 392 if self.url:
393 393 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
394 394 self.url, current_line_number, current_line_number)
395 395 else:
396 396 yield 0, '<td class="line"><a href="">%i</a></td>' % (
397 397 current_line_number)
398 398 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
399 399 yield 0, '</tr>'
400 400
401 401 last_shown_line_number = current_line_number
402 402
403 403 current_line_number += 1
404 404
405 405 yield 0, '</table>'
406 406
407 407
408 408 def hsv_to_rgb(h, s, v):
409 409 """ Convert hsv color values to rgb """
410 410
411 411 if s == 0.0:
412 412 return v, v, v
413 413 i = int(h * 6.0) # XXX assume int() truncates!
414 414 f = (h * 6.0) - i
415 415 p = v * (1.0 - s)
416 416 q = v * (1.0 - s * f)
417 417 t = v * (1.0 - s * (1.0 - f))
418 418 i = i % 6
419 419 if i == 0:
420 420 return v, t, p
421 421 if i == 1:
422 422 return q, v, p
423 423 if i == 2:
424 424 return p, v, t
425 425 if i == 3:
426 426 return p, q, v
427 427 if i == 4:
428 428 return t, p, v
429 429 if i == 5:
430 430 return v, p, q
431 431
432 432
433 433 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
434 434 """
435 435 Generator for getting n of evenly distributed colors using
436 436 hsv color and golden ratio. It always return same order of colors
437 437
438 438 :param n: number of colors to generate
439 439 :param saturation: saturation of returned colors
440 440 :param lightness: lightness of returned colors
441 441 :returns: RGB tuple
442 442 """
443 443
444 444 golden_ratio = 0.618033988749895
445 445 h = 0.22717784590367374
446 446
447 447 for _ in xrange(n):
448 448 h += golden_ratio
449 449 h %= 1
450 450 HSV_tuple = [h, saturation, lightness]
451 451 RGB_tuple = hsv_to_rgb(*HSV_tuple)
452 452 yield map(lambda x: str(int(x * 256)), RGB_tuple)
453 453
454 454
455 455 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
456 456 """
457 457 Returns a function which when called with an argument returns a unique
458 458 color for that argument, eg.
459 459
460 460 :param n: number of colors to generate
461 461 :param saturation: saturation of returned colors
462 462 :param lightness: lightness of returned colors
463 463 :returns: css RGB string
464 464
465 465 >>> color_hash = color_hasher()
466 466 >>> color_hash('hello')
467 467 'rgb(34, 12, 59)'
468 468 >>> color_hash('hello')
469 469 'rgb(34, 12, 59)'
470 470 >>> color_hash('other')
471 471 'rgb(90, 224, 159)'
472 472 """
473 473
474 474 color_dict = {}
475 475 cgenerator = unique_color_generator(
476 476 saturation=saturation, lightness=lightness)
477 477
478 478 def get_color_string(thing):
479 479 if thing in color_dict:
480 480 col = color_dict[thing]
481 481 else:
482 482 col = color_dict[thing] = cgenerator.next()
483 483 return "rgb(%s)" % (', '.join(col))
484 484
485 485 return get_color_string
486 486
487 487
488 488 def get_lexer_safe(mimetype=None, filepath=None):
489 489 """
490 490 Tries to return a relevant pygments lexer using mimetype/filepath name,
491 491 defaulting to plain text if none could be found
492 492 """
493 493 lexer = None
494 494 try:
495 495 if mimetype:
496 496 lexer = get_lexer_for_mimetype(mimetype)
497 497 if not lexer:
498 498 lexer = get_lexer_for_filename(filepath)
499 499 except pygments.util.ClassNotFound:
500 500 pass
501 501
502 502 if not lexer:
503 503 lexer = get_lexer_by_name('text')
504 504
505 505 return lexer
506 506
507 507
508 508 def get_lexer_for_filenode(filenode):
509 509 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
510 510 return lexer
511 511
512 512
513 513 def pygmentize(filenode, **kwargs):
514 514 """
515 515 pygmentize function using pygments
516 516
517 517 :param filenode:
518 518 """
519 519 lexer = get_lexer_for_filenode(filenode)
520 520 return literal(code_highlight(filenode.content, lexer,
521 521 CodeHtmlFormatter(**kwargs)))
522 522
523 523
524 524 def is_following_repo(repo_name, user_id):
525 525 from rhodecode.model.scm import ScmModel
526 526 return ScmModel().is_following_repo(repo_name, user_id)
527 527
528 528
529 529 class _Message(object):
530 530 """A message returned by ``Flash.pop_messages()``.
531 531
532 532 Converting the message to a string returns the message text. Instances
533 533 also have the following attributes:
534 534
535 535 * ``message``: the message text.
536 536 * ``category``: the category specified when the message was created.
537 537 """
538 538
539 539 def __init__(self, category, message):
540 540 self.category = category
541 541 self.message = message
542 542
543 543 def __str__(self):
544 544 return self.message
545 545
546 546 __unicode__ = __str__
547 547
548 548 def __html__(self):
549 549 return escape(safe_unicode(self.message))
550 550
551 551
552 552 class Flash(object):
553 553 # List of allowed categories. If None, allow any category.
554 554 categories = ["warning", "notice", "error", "success"]
555 555
556 556 # Default category if none is specified.
557 557 default_category = "notice"
558 558
559 559 def __init__(self, session_key="flash", categories=None,
560 560 default_category=None):
561 561 """
562 562 Instantiate a ``Flash`` object.
563 563
564 564 ``session_key`` is the key to save the messages under in the user's
565 565 session.
566 566
567 567 ``categories`` is an optional list which overrides the default list
568 568 of categories.
569 569
570 570 ``default_category`` overrides the default category used for messages
571 571 when none is specified.
572 572 """
573 573 self.session_key = session_key
574 574 if categories is not None:
575 575 self.categories = categories
576 576 if default_category is not None:
577 577 self.default_category = default_category
578 578 if self.categories and self.default_category not in self.categories:
579 579 raise ValueError(
580 580 "unrecognized default category %r" % (self.default_category,))
581 581
582 582 def pop_messages(self, session=None, request=None):
583 583 """
584 584 Return all accumulated messages and delete them from the session.
585 585
586 586 The return value is a list of ``Message`` objects.
587 587 """
588 588 messages = []
589 589
590 590 if not session:
591 591 if not request:
592 592 request = get_current_request()
593 593 session = request.session
594 594
595 595 # Pop the 'old' pylons flash messages. They are tuples of the form
596 596 # (category, message)
597 597 for cat, msg in session.pop(self.session_key, []):
598 598 messages.append(_Message(cat, msg))
599 599
600 600 # Pop the 'new' pyramid flash messages for each category as list
601 601 # of strings.
602 602 for cat in self.categories:
603 603 for msg in session.pop_flash(queue=cat):
604 604 messages.append(_Message(cat, msg))
605 605 # Map messages from the default queue to the 'notice' category.
606 606 for msg in session.pop_flash():
607 607 messages.append(_Message('notice', msg))
608 608
609 609 session.save()
610 610 return messages
611 611
612 612 def json_alerts(self, session=None, request=None):
613 613 payloads = []
614 614 messages = flash.pop_messages(session=session, request=request)
615 615 if messages:
616 616 for message in messages:
617 617 subdata = {}
618 618 if hasattr(message.message, 'rsplit'):
619 619 flash_data = message.message.rsplit('|DELIM|', 1)
620 620 org_message = flash_data[0]
621 621 if len(flash_data) > 1:
622 622 subdata = json.loads(flash_data[1])
623 623 else:
624 624 org_message = message.message
625 625 payloads.append({
626 626 'message': {
627 627 'message': u'{}'.format(org_message),
628 628 'level': message.category,
629 629 'force': True,
630 630 'subdata': subdata
631 631 }
632 632 })
633 633 return json.dumps(payloads)
634 634
635 635 def __call__(self, message, category=None, ignore_duplicate=False,
636 636 session=None, request=None):
637 637
638 638 if not session:
639 639 if not request:
640 640 request = get_current_request()
641 641 session = request.session
642 642
643 643 session.flash(
644 644 message, queue=category, allow_duplicate=not ignore_duplicate)
645 645
646 646
647 647 flash = Flash()
648 648
649 649 #==============================================================================
650 650 # SCM FILTERS available via h.
651 651 #==============================================================================
652 652 from rhodecode.lib.vcs.utils import author_name, author_email
653 from rhodecode.lib.utils2 import credentials_filter, age as _age
653 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
654 654 from rhodecode.model.db import User, ChangesetStatus
655 655
656 age = _age
657 656 capitalize = lambda x: x.capitalize()
658 657 email = author_email
659 658 short_id = lambda x: x[:12]
660 659 hide_credentials = lambda x: ''.join(credentials_filter(x))
661 660
662 661
663 662 import pytz
664 663 import tzlocal
665 664 local_timezone = tzlocal.get_localzone()
666 665
667 666
668 667 def age_component(datetime_iso, value=None, time_is_local=False):
669 668 title = value or format_date(datetime_iso)
670 669 tzinfo = '+00:00'
671 670
672 671 # detect if we have a timezone info, otherwise, add it
673 672 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
674 673 force_timezone = os.environ.get('RC_TIMEZONE', '')
675 674 if force_timezone:
676 675 force_timezone = pytz.timezone(force_timezone)
677 676 timezone = force_timezone or local_timezone
678 677 offset = timezone.localize(datetime_iso).strftime('%z')
679 678 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
680 679
681 680 return literal(
682 681 '<time class="timeago tooltip" '
683 682 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
684 683 datetime_iso, title, tzinfo))
685 684
686 685
687 686 def _shorten_commit_id(commit_id):
688 687 from rhodecode import CONFIG
689 688 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
690 689 return commit_id[:def_len]
691 690
692 691
693 692 def show_id(commit):
694 693 """
695 694 Configurable function that shows ID
696 695 by default it's r123:fffeeefffeee
697 696
698 697 :param commit: commit instance
699 698 """
700 699 from rhodecode import CONFIG
701 700 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
702 701
703 702 raw_id = _shorten_commit_id(commit.raw_id)
704 703 if show_idx:
705 704 return 'r%s:%s' % (commit.idx, raw_id)
706 705 else:
707 706 return '%s' % (raw_id, )
708 707
709 708
710 709 def format_date(date):
711 710 """
712 711 use a standardized formatting for dates used in RhodeCode
713 712
714 713 :param date: date/datetime object
715 714 :return: formatted date
716 715 """
717 716
718 717 if date:
719 718 _fmt = "%a, %d %b %Y %H:%M:%S"
720 719 return safe_unicode(date.strftime(_fmt))
721 720
722 721 return u""
723 722
724 723
725 724 class _RepoChecker(object):
726 725
727 726 def __init__(self, backend_alias):
728 727 self._backend_alias = backend_alias
729 728
730 729 def __call__(self, repository):
731 730 if hasattr(repository, 'alias'):
732 731 _type = repository.alias
733 732 elif hasattr(repository, 'repo_type'):
734 733 _type = repository.repo_type
735 734 else:
736 735 _type = repository
737 736 return _type == self._backend_alias
738 737
739 738
740 739 is_git = _RepoChecker('git')
741 740 is_hg = _RepoChecker('hg')
742 741 is_svn = _RepoChecker('svn')
743 742
744 743
745 744 def get_repo_type_by_name(repo_name):
746 745 repo = Repository.get_by_repo_name(repo_name)
747 746 if repo:
748 747 return repo.repo_type
749 748
750 749
751 750 def is_svn_without_proxy(repository):
752 751 if is_svn(repository):
753 752 from rhodecode.model.settings import VcsSettingsModel
754 753 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
755 754 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
756 755 return False
757 756
758 757
759 758 def discover_user(author):
760 759 """
761 760 Tries to discover RhodeCode User based on the autho string. Author string
762 761 is typically `FirstName LastName <email@address.com>`
763 762 """
764 763
765 764 # if author is already an instance use it for extraction
766 765 if isinstance(author, User):
767 766 return author
768 767
769 768 # Valid email in the attribute passed, see if they're in the system
770 769 _email = author_email(author)
771 770 if _email != '':
772 771 user = User.get_by_email(_email, case_insensitive=True, cache=True)
773 772 if user is not None:
774 773 return user
775 774
776 775 # Maybe it's a username, we try to extract it and fetch by username ?
777 776 _author = author_name(author)
778 777 user = User.get_by_username(_author, case_insensitive=True, cache=True)
779 778 if user is not None:
780 779 return user
781 780
782 781 return None
783 782
784 783
785 784 def email_or_none(author):
786 785 # extract email from the commit string
787 786 _email = author_email(author)
788 787
789 788 # If we have an email, use it, otherwise
790 789 # see if it contains a username we can get an email from
791 790 if _email != '':
792 791 return _email
793 792 else:
794 793 user = User.get_by_username(
795 794 author_name(author), case_insensitive=True, cache=True)
796 795
797 796 if user is not None:
798 797 return user.email
799 798
800 799 # No valid email, not a valid user in the system, none!
801 800 return None
802 801
803 802
804 803 def link_to_user(author, length=0, **kwargs):
805 804 user = discover_user(author)
806 805 # user can be None, but if we have it already it means we can re-use it
807 806 # in the person() function, so we save 1 intensive-query
808 807 if user:
809 808 author = user
810 809
811 810 display_person = person(author, 'username_or_name_or_email')
812 811 if length:
813 812 display_person = shorter(display_person, length)
814 813
815 814 if user:
816 815 return link_to(
817 816 escape(display_person),
818 817 route_path('user_profile', username=user.username),
819 818 **kwargs)
820 819 else:
821 820 return escape(display_person)
822 821
823 822
824 823 def link_to_group(users_group_name, **kwargs):
825 824 return link_to(
826 825 escape(users_group_name),
827 826 route_path('user_group_profile', user_group_name=users_group_name),
828 827 **kwargs)
829 828
830 829
831 830 def person(author, show_attr="username_and_name"):
832 831 user = discover_user(author)
833 832 if user:
834 833 return getattr(user, show_attr)
835 834 else:
836 835 _author = author_name(author)
837 836 _email = email(author)
838 837 return _author or _email
839 838
840 839
841 840 def author_string(email):
842 841 if email:
843 842 user = User.get_by_email(email, case_insensitive=True, cache=True)
844 843 if user:
845 844 if user.first_name or user.last_name:
846 845 return '%s %s &lt;%s&gt;' % (
847 846 user.first_name, user.last_name, email)
848 847 else:
849 848 return email
850 849 else:
851 850 return email
852 851 else:
853 852 return None
854 853
855 854
856 855 def person_by_id(id_, show_attr="username_and_name"):
857 856 # attr to return from fetched user
858 857 person_getter = lambda usr: getattr(usr, show_attr)
859 858
860 859 #maybe it's an ID ?
861 860 if str(id_).isdigit() or isinstance(id_, int):
862 861 id_ = int(id_)
863 862 user = User.get(id_)
864 863 if user is not None:
865 864 return person_getter(user)
866 865 return id_
867 866
868 867
869 868 def gravatar_with_user(request, author, show_disabled=False):
870 869 _render = request.get_partial_renderer(
871 870 'rhodecode:templates/base/base.mako')
872 871 return _render('gravatar_with_user', author, show_disabled=show_disabled)
873 872
874 873
875 874 tags_paterns = OrderedDict((
876 875 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
877 876 '<div class="metatag" tag="lang">\\2</div>')),
878 877
879 878 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
880 879 '<div class="metatag" tag="see">see: \\1 </div>')),
881 880
882 881 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
883 882 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
884 883
885 884 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
886 885 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
887 886
888 887 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
889 888 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
890 889
891 890 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
892 891 '<div class="metatag" tag="state \\1">\\1</div>')),
893 892
894 893 # label in grey
895 894 ('label', (re.compile(r'\[([a-z]+)\]'),
896 895 '<div class="metatag" tag="label">\\1</div>')),
897 896
898 897 # generic catch all in grey
899 898 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
900 899 '<div class="metatag" tag="generic">\\1</div>')),
901 900 ))
902 901
903 902
904 903 def extract_metatags(value):
905 904 """
906 905 Extract supported meta-tags from given text value
907 906 """
908 907 tags = []
909 908 if not value:
910 909 return tags, ''
911 910
912 911 for key, val in tags_paterns.items():
913 912 pat, replace_html = val
914 913 tags.extend([(key, x.group()) for x in pat.finditer(value)])
915 914 value = pat.sub('', value)
916 915
917 916 return tags, value
918 917
919 918
920 919 def style_metatag(tag_type, value):
921 920 """
922 921 converts tags from value into html equivalent
923 922 """
924 923 if not value:
925 924 return ''
926 925
927 926 html_value = value
928 927 tag_data = tags_paterns.get(tag_type)
929 928 if tag_data:
930 929 pat, replace_html = tag_data
931 930 # convert to plain `unicode` instead of a markup tag to be used in
932 931 # regex expressions. safe_unicode doesn't work here
933 932 html_value = pat.sub(replace_html, unicode(value))
934 933
935 934 return html_value
936 935
937 936
938 937 def bool2icon(value, show_at_false=True):
939 938 """
940 939 Returns boolean value of a given value, represented as html element with
941 940 classes that will represent icons
942 941
943 942 :param value: given value to convert to html node
944 943 """
945 944
946 945 if value: # does bool conversion
947 946 return HTML.tag('i', class_="icon-true")
948 947 else: # not true as bool
949 948 if show_at_false:
950 949 return HTML.tag('i', class_="icon-false")
951 950 return HTML.tag('i')
952 951
953 952 #==============================================================================
954 953 # PERMS
955 954 #==============================================================================
956 955 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
957 956 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
958 957 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
959 958 csrf_token_key
960 959
961 960
962 961 #==============================================================================
963 962 # GRAVATAR URL
964 963 #==============================================================================
965 964 class InitialsGravatar(object):
966 965 def __init__(self, email_address, first_name, last_name, size=30,
967 966 background=None, text_color='#fff'):
968 967 self.size = size
969 968 self.first_name = first_name
970 969 self.last_name = last_name
971 970 self.email_address = email_address
972 971 self.background = background or self.str2color(email_address)
973 972 self.text_color = text_color
974 973
975 974 def get_color_bank(self):
976 975 """
977 976 returns a predefined list of colors that gravatars can use.
978 977 Those are randomized distinct colors that guarantee readability and
979 978 uniqueness.
980 979
981 980 generated with: http://phrogz.net/css/distinct-colors.html
982 981 """
983 982 return [
984 983 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
985 984 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
986 985 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
987 986 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
988 987 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
989 988 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
990 989 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
991 990 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
992 991 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
993 992 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
994 993 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
995 994 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
996 995 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
997 996 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
998 997 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
999 998 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1000 999 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1001 1000 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1002 1001 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1003 1002 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1004 1003 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1005 1004 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1006 1005 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1007 1006 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1008 1007 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1009 1008 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1010 1009 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1011 1010 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1012 1011 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1013 1012 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1014 1013 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1015 1014 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1016 1015 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1017 1016 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1018 1017 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1019 1018 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1020 1019 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1021 1020 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1022 1021 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1023 1022 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1024 1023 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1025 1024 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1026 1025 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1027 1026 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1028 1027 '#4f8c46', '#368dd9', '#5c0073'
1029 1028 ]
1030 1029
1031 1030 def rgb_to_hex_color(self, rgb_tuple):
1032 1031 """
1033 1032 Converts an rgb_tuple passed to an hex color.
1034 1033
1035 1034 :param rgb_tuple: tuple with 3 ints represents rgb color space
1036 1035 """
1037 1036 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1038 1037
1039 1038 def email_to_int_list(self, email_str):
1040 1039 """
1041 1040 Get every byte of the hex digest value of email and turn it to integer.
1042 1041 It's going to be always between 0-255
1043 1042 """
1044 1043 digest = md5_safe(email_str.lower())
1045 1044 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1046 1045
1047 1046 def pick_color_bank_index(self, email_str, color_bank):
1048 1047 return self.email_to_int_list(email_str)[0] % len(color_bank)
1049 1048
1050 1049 def str2color(self, email_str):
1051 1050 """
1052 1051 Tries to map in a stable algorithm an email to color
1053 1052
1054 1053 :param email_str:
1055 1054 """
1056 1055 color_bank = self.get_color_bank()
1057 1056 # pick position (module it's length so we always find it in the
1058 1057 # bank even if it's smaller than 256 values
1059 1058 pos = self.pick_color_bank_index(email_str, color_bank)
1060 1059 return color_bank[pos]
1061 1060
1062 1061 def normalize_email(self, email_address):
1063 1062 import unicodedata
1064 1063 # default host used to fill in the fake/missing email
1065 1064 default_host = u'localhost'
1066 1065
1067 1066 if not email_address:
1068 1067 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1069 1068
1070 1069 email_address = safe_unicode(email_address)
1071 1070
1072 1071 if u'@' not in email_address:
1073 1072 email_address = u'%s@%s' % (email_address, default_host)
1074 1073
1075 1074 if email_address.endswith(u'@'):
1076 1075 email_address = u'%s%s' % (email_address, default_host)
1077 1076
1078 1077 email_address = unicodedata.normalize('NFKD', email_address)\
1079 1078 .encode('ascii', 'ignore')
1080 1079 return email_address
1081 1080
1082 1081 def get_initials(self):
1083 1082 """
1084 1083 Returns 2 letter initials calculated based on the input.
1085 1084 The algorithm picks first given email address, and takes first letter
1086 1085 of part before @, and then the first letter of server name. In case
1087 1086 the part before @ is in a format of `somestring.somestring2` it replaces
1088 1087 the server letter with first letter of somestring2
1089 1088
1090 1089 In case function was initialized with both first and lastname, this
1091 1090 overrides the extraction from email by first letter of the first and
1092 1091 last name. We add special logic to that functionality, In case Full name
1093 1092 is compound, like Guido Von Rossum, we use last part of the last name
1094 1093 (Von Rossum) picking `R`.
1095 1094
1096 1095 Function also normalizes the non-ascii characters to they ascii
1097 1096 representation, eg Δ„ => A
1098 1097 """
1099 1098 import unicodedata
1100 1099 # replace non-ascii to ascii
1101 1100 first_name = unicodedata.normalize(
1102 1101 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1103 1102 last_name = unicodedata.normalize(
1104 1103 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1105 1104
1106 1105 # do NFKD encoding, and also make sure email has proper format
1107 1106 email_address = self.normalize_email(self.email_address)
1108 1107
1109 1108 # first push the email initials
1110 1109 prefix, server = email_address.split('@', 1)
1111 1110
1112 1111 # check if prefix is maybe a 'first_name.last_name' syntax
1113 1112 _dot_split = prefix.rsplit('.', 1)
1114 1113 if len(_dot_split) == 2 and _dot_split[1]:
1115 1114 initials = [_dot_split[0][0], _dot_split[1][0]]
1116 1115 else:
1117 1116 initials = [prefix[0], server[0]]
1118 1117
1119 1118 # then try to replace either first_name or last_name
1120 1119 fn_letter = (first_name or " ")[0].strip()
1121 1120 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1122 1121
1123 1122 if fn_letter:
1124 1123 initials[0] = fn_letter
1125 1124
1126 1125 if ln_letter:
1127 1126 initials[1] = ln_letter
1128 1127
1129 1128 return ''.join(initials).upper()
1130 1129
1131 1130 def get_img_data_by_type(self, font_family, img_type):
1132 1131 default_user = """
1133 1132 <svg xmlns="http://www.w3.org/2000/svg"
1134 1133 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1135 1134 viewBox="-15 -10 439.165 429.164"
1136 1135
1137 1136 xml:space="preserve"
1138 1137 style="background:{background};" >
1139 1138
1140 1139 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1141 1140 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1142 1141 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1143 1142 168.596,153.916,216.671,
1144 1143 204.583,216.671z" fill="{text_color}"/>
1145 1144 <path d="M407.164,374.717L360.88,
1146 1145 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1147 1146 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1148 1147 15.366-44.203,23.488-69.076,23.488c-24.877,
1149 1148 0-48.762-8.122-69.078-23.488
1150 1149 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1151 1150 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1152 1151 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1153 1152 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1154 1153 19.402-10.527 C409.699,390.129,
1155 1154 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1156 1155 </svg>""".format(
1157 1156 size=self.size,
1158 1157 background='#979797', # @grey4
1159 1158 text_color=self.text_color,
1160 1159 font_family=font_family)
1161 1160
1162 1161 return {
1163 1162 "default_user": default_user
1164 1163 }[img_type]
1165 1164
1166 1165 def get_img_data(self, svg_type=None):
1167 1166 """
1168 1167 generates the svg metadata for image
1169 1168 """
1170 1169 fonts = [
1171 1170 '-apple-system',
1172 1171 'BlinkMacSystemFont',
1173 1172 'Segoe UI',
1174 1173 'Roboto',
1175 1174 'Oxygen-Sans',
1176 1175 'Ubuntu',
1177 1176 'Cantarell',
1178 1177 'Helvetica Neue',
1179 1178 'sans-serif'
1180 1179 ]
1181 1180 font_family = ','.join(fonts)
1182 1181 if svg_type:
1183 1182 return self.get_img_data_by_type(font_family, svg_type)
1184 1183
1185 1184 initials = self.get_initials()
1186 1185 img_data = """
1187 1186 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1188 1187 width="{size}" height="{size}"
1189 1188 style="width: 100%; height: 100%; background-color: {background}"
1190 1189 viewBox="0 0 {size} {size}">
1191 1190 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1192 1191 pointer-events="auto" fill="{text_color}"
1193 1192 font-family="{font_family}"
1194 1193 style="font-weight: 400; font-size: {f_size}px;">{text}
1195 1194 </text>
1196 1195 </svg>""".format(
1197 1196 size=self.size,
1198 1197 f_size=self.size/1.85, # scale the text inside the box nicely
1199 1198 background=self.background,
1200 1199 text_color=self.text_color,
1201 1200 text=initials.upper(),
1202 1201 font_family=font_family)
1203 1202
1204 1203 return img_data
1205 1204
1206 1205 def generate_svg(self, svg_type=None):
1207 1206 img_data = self.get_img_data(svg_type)
1208 1207 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1209 1208
1210 1209
1211 1210 def initials_gravatar(email_address, first_name, last_name, size=30):
1212 1211 svg_type = None
1213 1212 if email_address == User.DEFAULT_USER_EMAIL:
1214 1213 svg_type = 'default_user'
1215 1214 klass = InitialsGravatar(email_address, first_name, last_name, size)
1216 1215 return klass.generate_svg(svg_type=svg_type)
1217 1216
1218 1217
1219 1218 def gravatar_url(email_address, size=30, request=None):
1220 1219 request = get_current_request()
1221 1220 _use_gravatar = request.call_context.visual.use_gravatar
1222 1221 _gravatar_url = request.call_context.visual.gravatar_url
1223 1222
1224 1223 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1225 1224
1226 1225 email_address = email_address or User.DEFAULT_USER_EMAIL
1227 1226 if isinstance(email_address, unicode):
1228 1227 # hashlib crashes on unicode items
1229 1228 email_address = safe_str(email_address)
1230 1229
1231 1230 # empty email or default user
1232 1231 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1233 1232 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1234 1233
1235 1234 if _use_gravatar:
1236 1235 # TODO: Disuse pyramid thread locals. Think about another solution to
1237 1236 # get the host and schema here.
1238 1237 request = get_current_request()
1239 1238 tmpl = safe_str(_gravatar_url)
1240 1239 tmpl = tmpl.replace('{email}', email_address)\
1241 1240 .replace('{md5email}', md5_safe(email_address.lower())) \
1242 1241 .replace('{netloc}', request.host)\
1243 1242 .replace('{scheme}', request.scheme)\
1244 1243 .replace('{size}', safe_str(size))
1245 1244 return tmpl
1246 1245 else:
1247 1246 return initials_gravatar(email_address, '', '', size=size)
1248 1247
1249 1248
1250 1249 class Page(_Page):
1251 1250 """
1252 1251 Custom pager to match rendering style with paginator
1253 1252 """
1254 1253
1255 1254 def _get_pos(self, cur_page, max_page, items):
1256 1255 edge = (items / 2) + 1
1257 1256 if (cur_page <= edge):
1258 1257 radius = max(items / 2, items - cur_page)
1259 1258 elif (max_page - cur_page) < edge:
1260 1259 radius = (items - 1) - (max_page - cur_page)
1261 1260 else:
1262 1261 radius = items / 2
1263 1262
1264 1263 left = max(1, (cur_page - (radius)))
1265 1264 right = min(max_page, cur_page + (radius))
1266 1265 return left, cur_page, right
1267 1266
1268 1267 def _range(self, regexp_match):
1269 1268 """
1270 1269 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1271 1270
1272 1271 Arguments:
1273 1272
1274 1273 regexp_match
1275 1274 A "re" (regular expressions) match object containing the
1276 1275 radius of linked pages around the current page in
1277 1276 regexp_match.group(1) as a string
1278 1277
1279 1278 This function is supposed to be called as a callable in
1280 1279 re.sub.
1281 1280
1282 1281 """
1283 1282 radius = int(regexp_match.group(1))
1284 1283
1285 1284 # Compute the first and last page number within the radius
1286 1285 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1287 1286 # -> leftmost_page = 5
1288 1287 # -> rightmost_page = 9
1289 1288 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1290 1289 self.last_page,
1291 1290 (radius * 2) + 1)
1292 1291 nav_items = []
1293 1292
1294 1293 # Create a link to the first page (unless we are on the first page
1295 1294 # or there would be no need to insert '..' spacers)
1296 1295 if self.page != self.first_page and self.first_page < leftmost_page:
1297 1296 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1298 1297
1299 1298 # Insert dots if there are pages between the first page
1300 1299 # and the currently displayed page range
1301 1300 if leftmost_page - self.first_page > 1:
1302 1301 # Wrap in a SPAN tag if nolink_attr is set
1303 1302 text = '..'
1304 1303 if self.dotdot_attr:
1305 1304 text = HTML.span(c=text, **self.dotdot_attr)
1306 1305 nav_items.append(text)
1307 1306
1308 1307 for thispage in xrange(leftmost_page, rightmost_page + 1):
1309 1308 # Hilight the current page number and do not use a link
1310 1309 if thispage == self.page:
1311 1310 text = '%s' % (thispage,)
1312 1311 # Wrap in a SPAN tag if nolink_attr is set
1313 1312 if self.curpage_attr:
1314 1313 text = HTML.span(c=text, **self.curpage_attr)
1315 1314 nav_items.append(text)
1316 1315 # Otherwise create just a link to that page
1317 1316 else:
1318 1317 text = '%s' % (thispage,)
1319 1318 nav_items.append(self._pagerlink(thispage, text))
1320 1319
1321 1320 # Insert dots if there are pages between the displayed
1322 1321 # page numbers and the end of the page range
1323 1322 if self.last_page - rightmost_page > 1:
1324 1323 text = '..'
1325 1324 # Wrap in a SPAN tag if nolink_attr is set
1326 1325 if self.dotdot_attr:
1327 1326 text = HTML.span(c=text, **self.dotdot_attr)
1328 1327 nav_items.append(text)
1329 1328
1330 1329 # Create a link to the very last page (unless we are on the last
1331 1330 # page or there would be no need to insert '..' spacers)
1332 1331 if self.page != self.last_page and rightmost_page < self.last_page:
1333 1332 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1334 1333
1335 1334 ## prerender links
1336 1335 #_page_link = url.current()
1337 1336 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1338 1337 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1339 1338 return self.separator.join(nav_items)
1340 1339
1341 1340 def pager(self, format='~2~', page_param='page', partial_param='partial',
1342 1341 show_if_single_page=False, separator=' ', onclick=None,
1343 1342 symbol_first='<<', symbol_last='>>',
1344 1343 symbol_previous='<', symbol_next='>',
1345 1344 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1346 1345 curpage_attr={'class': 'pager_curpage'},
1347 1346 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1348 1347
1349 1348 self.curpage_attr = curpage_attr
1350 1349 self.separator = separator
1351 1350 self.pager_kwargs = kwargs
1352 1351 self.page_param = page_param
1353 1352 self.partial_param = partial_param
1354 1353 self.onclick = onclick
1355 1354 self.link_attr = link_attr
1356 1355 self.dotdot_attr = dotdot_attr
1357 1356
1358 1357 # Don't show navigator if there is no more than one page
1359 1358 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1360 1359 return ''
1361 1360
1362 1361 from string import Template
1363 1362 # Replace ~...~ in token format by range of pages
1364 1363 result = re.sub(r'~(\d+)~', self._range, format)
1365 1364
1366 1365 # Interpolate '%' variables
1367 1366 result = Template(result).safe_substitute({
1368 1367 'first_page': self.first_page,
1369 1368 'last_page': self.last_page,
1370 1369 'page': self.page,
1371 1370 'page_count': self.page_count,
1372 1371 'items_per_page': self.items_per_page,
1373 1372 'first_item': self.first_item,
1374 1373 'last_item': self.last_item,
1375 1374 'item_count': self.item_count,
1376 1375 'link_first': self.page > self.first_page and \
1377 1376 self._pagerlink(self.first_page, symbol_first) or '',
1378 1377 'link_last': self.page < self.last_page and \
1379 1378 self._pagerlink(self.last_page, symbol_last) or '',
1380 1379 'link_previous': self.previous_page and \
1381 1380 self._pagerlink(self.previous_page, symbol_previous) \
1382 1381 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1383 1382 'link_next': self.next_page and \
1384 1383 self._pagerlink(self.next_page, symbol_next) \
1385 1384 or HTML.span(symbol_next, class_="pg-next disabled")
1386 1385 })
1387 1386
1388 1387 return literal(result)
1389 1388
1390 1389
1391 1390 #==============================================================================
1392 1391 # REPO PAGER, PAGER FOR REPOSITORY
1393 1392 #==============================================================================
1394 1393 class RepoPage(Page):
1395 1394
1396 1395 def __init__(self, collection, page=1, items_per_page=20,
1397 1396 item_count=None, url=None, **kwargs):
1398 1397
1399 1398 """Create a "RepoPage" instance. special pager for paging
1400 1399 repository
1401 1400 """
1402 1401 self._url_generator = url
1403 1402
1404 1403 # Safe the kwargs class-wide so they can be used in the pager() method
1405 1404 self.kwargs = kwargs
1406 1405
1407 1406 # Save a reference to the collection
1408 1407 self.original_collection = collection
1409 1408
1410 1409 self.collection = collection
1411 1410
1412 1411 # The self.page is the number of the current page.
1413 1412 # The first page has the number 1!
1414 1413 try:
1415 1414 self.page = int(page) # make it int() if we get it as a string
1416 1415 except (ValueError, TypeError):
1417 1416 self.page = 1
1418 1417
1419 1418 self.items_per_page = items_per_page
1420 1419
1421 1420 # Unless the user tells us how many items the collections has
1422 1421 # we calculate that ourselves.
1423 1422 if item_count is not None:
1424 1423 self.item_count = item_count
1425 1424 else:
1426 1425 self.item_count = len(self.collection)
1427 1426
1428 1427 # Compute the number of the first and last available page
1429 1428 if self.item_count > 0:
1430 1429 self.first_page = 1
1431 1430 self.page_count = int(math.ceil(float(self.item_count) /
1432 1431 self.items_per_page))
1433 1432 self.last_page = self.first_page + self.page_count - 1
1434 1433
1435 1434 # Make sure that the requested page number is the range of
1436 1435 # valid pages
1437 1436 if self.page > self.last_page:
1438 1437 self.page = self.last_page
1439 1438 elif self.page < self.first_page:
1440 1439 self.page = self.first_page
1441 1440
1442 1441 # Note: the number of items on this page can be less than
1443 1442 # items_per_page if the last page is not full
1444 1443 self.first_item = max(0, (self.item_count) - (self.page *
1445 1444 items_per_page))
1446 1445 self.last_item = ((self.item_count - 1) - items_per_page *
1447 1446 (self.page - 1))
1448 1447
1449 1448 self.items = list(self.collection[self.first_item:self.last_item + 1])
1450 1449
1451 1450 # Links to previous and next page
1452 1451 if self.page > self.first_page:
1453 1452 self.previous_page = self.page - 1
1454 1453 else:
1455 1454 self.previous_page = None
1456 1455
1457 1456 if self.page < self.last_page:
1458 1457 self.next_page = self.page + 1
1459 1458 else:
1460 1459 self.next_page = None
1461 1460
1462 1461 # No items available
1463 1462 else:
1464 1463 self.first_page = None
1465 1464 self.page_count = 0
1466 1465 self.last_page = None
1467 1466 self.first_item = None
1468 1467 self.last_item = None
1469 1468 self.previous_page = None
1470 1469 self.next_page = None
1471 1470 self.items = []
1472 1471
1473 1472 # This is a subclass of the 'list' type. Initialise the list now.
1474 1473 list.__init__(self, reversed(self.items))
1475 1474
1476 1475
1477 1476 def breadcrumb_repo_link(repo):
1478 1477 """
1479 1478 Makes a breadcrumbs path link to repo
1480 1479
1481 1480 ex::
1482 1481 group >> subgroup >> repo
1483 1482
1484 1483 :param repo: a Repository instance
1485 1484 """
1486 1485
1487 1486 path = [
1488 1487 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1489 1488 for group in repo.groups_with_parents
1490 1489 ] + [
1491 1490 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1492 1491 ]
1493 1492
1494 1493 return literal(' &raquo; '.join(path))
1495 1494
1496 1495
1497 1496 def format_byte_size_binary(file_size):
1498 1497 """
1499 1498 Formats file/folder sizes to standard.
1500 1499 """
1501 1500 if file_size is None:
1502 1501 file_size = 0
1503 1502
1504 1503 formatted_size = format_byte_size(file_size, binary=True)
1505 1504 return formatted_size
1506 1505
1507 1506
1508 1507 def urlify_text(text_, safe=True):
1509 1508 """
1510 1509 Extrac urls from text and make html links out of them
1511 1510
1512 1511 :param text_:
1513 1512 """
1514 1513
1515 1514 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1516 1515 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1517 1516
1518 1517 def url_func(match_obj):
1519 1518 url_full = match_obj.groups()[0]
1520 1519 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1521 1520 _newtext = url_pat.sub(url_func, text_)
1522 1521 if safe:
1523 1522 return literal(_newtext)
1524 1523 return _newtext
1525 1524
1526 1525
1527 1526 def urlify_commits(text_, repository):
1528 1527 """
1529 1528 Extract commit ids from text and make link from them
1530 1529
1531 1530 :param text_:
1532 1531 :param repository: repo name to build the URL with
1533 1532 """
1534 1533
1535 1534 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1536 1535
1537 1536 def url_func(match_obj):
1538 1537 commit_id = match_obj.groups()[1]
1539 1538 pref = match_obj.groups()[0]
1540 1539 suf = match_obj.groups()[2]
1541 1540
1542 1541 tmpl = (
1543 1542 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1544 1543 '%(commit_id)s</a>%(suf)s'
1545 1544 )
1546 1545 return tmpl % {
1547 1546 'pref': pref,
1548 1547 'cls': 'revision-link',
1549 1548 'url': route_url('repo_commit', repo_name=repository,
1550 1549 commit_id=commit_id),
1551 1550 'commit_id': commit_id,
1552 1551 'suf': suf
1553 1552 }
1554 1553
1555 1554 newtext = URL_PAT.sub(url_func, text_)
1556 1555
1557 1556 return newtext
1558 1557
1559 1558
1560 1559 def _process_url_func(match_obj, repo_name, uid, entry,
1561 1560 return_raw_data=False, link_format='html'):
1562 1561 pref = ''
1563 1562 if match_obj.group().startswith(' '):
1564 1563 pref = ' '
1565 1564
1566 1565 issue_id = ''.join(match_obj.groups())
1567 1566
1568 1567 if link_format == 'html':
1569 1568 tmpl = (
1570 1569 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1571 1570 '%(issue-prefix)s%(id-repr)s'
1572 1571 '</a>')
1573 1572 elif link_format == 'rst':
1574 1573 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1575 1574 elif link_format == 'markdown':
1576 1575 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1577 1576 else:
1578 1577 raise ValueError('Bad link_format:{}'.format(link_format))
1579 1578
1580 1579 (repo_name_cleaned,
1581 1580 parent_group_name) = RepoGroupModel().\
1582 1581 _get_group_name_and_parent(repo_name)
1583 1582
1584 1583 # variables replacement
1585 1584 named_vars = {
1586 1585 'id': issue_id,
1587 1586 'repo': repo_name,
1588 1587 'repo_name': repo_name_cleaned,
1589 1588 'group_name': parent_group_name
1590 1589 }
1591 1590 # named regex variables
1592 1591 named_vars.update(match_obj.groupdict())
1593 1592 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1594 1593
1595 1594 data = {
1596 1595 'pref': pref,
1597 1596 'cls': 'issue-tracker-link',
1598 1597 'url': _url,
1599 1598 'id-repr': issue_id,
1600 1599 'issue-prefix': entry['pref'],
1601 1600 'serv': entry['url'],
1602 1601 }
1603 1602 if return_raw_data:
1604 1603 return {
1605 1604 'id': issue_id,
1606 1605 'url': _url
1607 1606 }
1608 1607 return tmpl % data
1609 1608
1610 1609
1611 1610 def get_active_pattern_entries(repo_name):
1612 1611 repo = None
1613 1612 if repo_name:
1614 1613 # Retrieving repo_name to avoid invalid repo_name to explode on
1615 1614 # IssueTrackerSettingsModel but still passing invalid name further down
1616 1615 repo = Repository.get_by_repo_name(repo_name, cache=True)
1617 1616
1618 1617 settings_model = IssueTrackerSettingsModel(repo=repo)
1619 1618 active_entries = settings_model.get_settings(cache=True)
1620 1619 return active_entries
1621 1620
1622 1621
1623 1622 def process_patterns(text_string, repo_name, link_format='html',
1624 1623 active_entries=None):
1625 1624
1626 1625 allowed_formats = ['html', 'rst', 'markdown']
1627 1626 if link_format not in allowed_formats:
1628 1627 raise ValueError('Link format can be only one of:{} got {}'.format(
1629 1628 allowed_formats, link_format))
1630 1629
1631 1630 active_entries = active_entries or get_active_pattern_entries(repo_name)
1632 1631 issues_data = []
1633 1632 newtext = text_string
1634 1633
1635 1634 for uid, entry in active_entries.items():
1636 1635 log.debug('found issue tracker entry with uid %s', uid)
1637 1636
1638 1637 if not (entry['pat'] and entry['url']):
1639 1638 log.debug('skipping due to missing data')
1640 1639 continue
1641 1640
1642 1641 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1643 1642 uid, entry['pat'], entry['url'], entry['pref'])
1644 1643
1645 1644 try:
1646 1645 pattern = re.compile(r'%s' % entry['pat'])
1647 1646 except re.error:
1648 1647 log.exception(
1649 1648 'issue tracker pattern: `%s` failed to compile',
1650 1649 entry['pat'])
1651 1650 continue
1652 1651
1653 1652 data_func = partial(
1654 1653 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1655 1654 return_raw_data=True)
1656 1655
1657 1656 for match_obj in pattern.finditer(text_string):
1658 1657 issues_data.append(data_func(match_obj))
1659 1658
1660 1659 url_func = partial(
1661 1660 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1662 1661 link_format=link_format)
1663 1662
1664 1663 newtext = pattern.sub(url_func, newtext)
1665 1664 log.debug('processed prefix:uid `%s`', uid)
1666 1665
1667 1666 return newtext, issues_data
1668 1667
1669 1668
1670 1669 def urlify_commit_message(commit_text, repository=None,
1671 1670 active_pattern_entries=None):
1672 1671 """
1673 1672 Parses given text message and makes proper links.
1674 1673 issues are linked to given issue-server, and rest is a commit link
1675 1674
1676 1675 :param commit_text:
1677 1676 :param repository:
1678 1677 """
1679 1678 def escaper(string):
1680 1679 return string.replace('<', '&lt;').replace('>', '&gt;')
1681 1680
1682 1681 newtext = escaper(commit_text)
1683 1682
1684 1683 # extract http/https links and make them real urls
1685 1684 newtext = urlify_text(newtext, safe=False)
1686 1685
1687 1686 # urlify commits - extract commit ids and make link out of them, if we have
1688 1687 # the scope of repository present.
1689 1688 if repository:
1690 1689 newtext = urlify_commits(newtext, repository)
1691 1690
1692 1691 # process issue tracker patterns
1693 1692 newtext, issues = process_patterns(newtext, repository or '',
1694 1693 active_entries=active_pattern_entries)
1695 1694
1696 1695 return literal(newtext)
1697 1696
1698 1697
1699 1698 def render_binary(repo_name, file_obj):
1700 1699 """
1701 1700 Choose how to render a binary file
1702 1701 """
1703 1702
1704 1703 filename = file_obj.name
1705 1704
1706 1705 # images
1707 1706 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1708 1707 if fnmatch.fnmatch(filename, pat=ext):
1709 1708 alt = escape(filename)
1710 1709 src = route_path(
1711 1710 'repo_file_raw', repo_name=repo_name,
1712 1711 commit_id=file_obj.commit.raw_id,
1713 1712 f_path=file_obj.path)
1714 1713 return literal(
1715 1714 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1716 1715
1717 1716
1718 1717 def renderer_from_filename(filename, exclude=None):
1719 1718 """
1720 1719 choose a renderer based on filename, this works only for text based files
1721 1720 """
1722 1721
1723 1722 # ipython
1724 1723 for ext in ['*.ipynb']:
1725 1724 if fnmatch.fnmatch(filename, pat=ext):
1726 1725 return 'jupyter'
1727 1726
1728 1727 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1729 1728 if is_markup:
1730 1729 return is_markup
1731 1730 return None
1732 1731
1733 1732
1734 1733 def render(source, renderer='rst', mentions=False, relative_urls=None,
1735 1734 repo_name=None):
1736 1735
1737 1736 def maybe_convert_relative_links(html_source):
1738 1737 if relative_urls:
1739 1738 return relative_links(html_source, relative_urls)
1740 1739 return html_source
1741 1740
1742 1741 if renderer == 'plain':
1743 1742 return literal(
1744 1743 MarkupRenderer.plain(source, leading_newline=False))
1745 1744
1746 1745 elif renderer == 'rst':
1747 1746 if repo_name:
1748 1747 # process patterns on comments if we pass in repo name
1749 1748 source, issues = process_patterns(
1750 1749 source, repo_name, link_format='rst')
1751 1750
1752 1751 return literal(
1753 1752 '<div class="rst-block">%s</div>' %
1754 1753 maybe_convert_relative_links(
1755 1754 MarkupRenderer.rst(source, mentions=mentions)))
1756 1755
1757 1756 elif renderer == 'markdown':
1758 1757 if repo_name:
1759 1758 # process patterns on comments if we pass in repo name
1760 1759 source, issues = process_patterns(
1761 1760 source, repo_name, link_format='markdown')
1762 1761
1763 1762 return literal(
1764 1763 '<div class="markdown-block">%s</div>' %
1765 1764 maybe_convert_relative_links(
1766 1765 MarkupRenderer.markdown(source, flavored=True,
1767 1766 mentions=mentions)))
1768 1767
1769 1768 elif renderer == 'jupyter':
1770 1769 return literal(
1771 1770 '<div class="ipynb">%s</div>' %
1772 1771 maybe_convert_relative_links(
1773 1772 MarkupRenderer.jupyter(source)))
1774 1773
1775 1774 # None means just show the file-source
1776 1775 return None
1777 1776
1778 1777
1779 1778 def commit_status(repo, commit_id):
1780 1779 return ChangesetStatusModel().get_status(repo, commit_id)
1781 1780
1782 1781
1783 1782 def commit_status_lbl(commit_status):
1784 1783 return dict(ChangesetStatus.STATUSES).get(commit_status)
1785 1784
1786 1785
1787 1786 def commit_time(repo_name, commit_id):
1788 1787 repo = Repository.get_by_repo_name(repo_name)
1789 1788 commit = repo.get_commit(commit_id=commit_id)
1790 1789 return commit.date
1791 1790
1792 1791
1793 1792 def get_permission_name(key):
1794 1793 return dict(Permission.PERMS).get(key)
1795 1794
1796 1795
1797 1796 def journal_filter_help(request):
1798 1797 _ = request.translate
1799 1798 from rhodecode.lib.audit_logger import ACTIONS
1800 1799 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1801 1800
1802 1801 return _(
1803 1802 'Example filter terms:\n' +
1804 1803 ' repository:vcs\n' +
1805 1804 ' username:marcin\n' +
1806 1805 ' username:(NOT marcin)\n' +
1807 1806 ' action:*push*\n' +
1808 1807 ' ip:127.0.0.1\n' +
1809 1808 ' date:20120101\n' +
1810 1809 ' date:[20120101100000 TO 20120102]\n' +
1811 1810 '\n' +
1812 1811 'Actions: {actions}\n' +
1813 1812 '\n' +
1814 1813 'Generate wildcards using \'*\' character:\n' +
1815 1814 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1816 1815 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1817 1816 '\n' +
1818 1817 'Optional AND / OR operators in queries\n' +
1819 1818 ' "repository:vcs OR repository:test"\n' +
1820 1819 ' "username:test AND repository:test*"\n'
1821 1820 ).format(actions=actions)
1822 1821
1823 1822
1824 1823 def not_mapped_error(repo_name):
1825 1824 from rhodecode.translation import _
1826 1825 flash(_('%s repository is not mapped to db perhaps'
1827 1826 ' it was created or renamed from the filesystem'
1828 1827 ' please run the application again'
1829 1828 ' in order to rescan repositories') % repo_name, category='error')
1830 1829
1831 1830
1832 1831 def ip_range(ip_addr):
1833 1832 from rhodecode.model.db import UserIpMap
1834 1833 s, e = UserIpMap._get_ip_range(ip_addr)
1835 1834 return '%s - %s' % (s, e)
1836 1835
1837 1836
1838 1837 def form(url, method='post', needs_csrf_token=True, **attrs):
1839 1838 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1840 1839 if method.lower() != 'get' and needs_csrf_token:
1841 1840 raise Exception(
1842 1841 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1843 1842 'CSRF token. If the endpoint does not require such token you can ' +
1844 1843 'explicitly set the parameter needs_csrf_token to false.')
1845 1844
1846 1845 return wh_form(url, method=method, **attrs)
1847 1846
1848 1847
1849 1848 def secure_form(form_url, method="POST", multipart=False, **attrs):
1850 1849 """Start a form tag that points the action to an url. This
1851 1850 form tag will also include the hidden field containing
1852 1851 the auth token.
1853 1852
1854 1853 The url options should be given either as a string, or as a
1855 1854 ``url()`` function. The method for the form defaults to POST.
1856 1855
1857 1856 Options:
1858 1857
1859 1858 ``multipart``
1860 1859 If set to True, the enctype is set to "multipart/form-data".
1861 1860 ``method``
1862 1861 The method to use when submitting the form, usually either
1863 1862 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1864 1863 hidden input with name _method is added to simulate the verb
1865 1864 over POST.
1866 1865
1867 1866 """
1868 1867 from webhelpers.pylonslib.secure_form import insecure_form
1869 1868
1870 1869 if 'request' in attrs:
1871 1870 session = attrs['request'].session
1872 1871 del attrs['request']
1873 1872 else:
1874 1873 raise ValueError(
1875 1874 'Calling this form requires request= to be passed as argument')
1876 1875
1877 1876 form = insecure_form(form_url, method, multipart, **attrs)
1878 1877 token = literal(
1879 1878 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1880 1879 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1881 1880
1882 1881 return literal("%s\n%s" % (form, token))
1883 1882
1884 1883
1885 1884 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1886 1885 select_html = select(name, selected, options, **attrs)
1887 1886 select2 = """
1888 1887 <script>
1889 1888 $(document).ready(function() {
1890 1889 $('#%s').select2({
1891 1890 containerCssClass: 'drop-menu',
1892 1891 dropdownCssClass: 'drop-menu-dropdown',
1893 1892 dropdownAutoWidth: true%s
1894 1893 });
1895 1894 });
1896 1895 </script>
1897 1896 """
1898 1897 filter_option = """,
1899 1898 minimumResultsForSearch: -1
1900 1899 """
1901 1900 input_id = attrs.get('id') or name
1902 1901 filter_enabled = "" if enable_filter else filter_option
1903 1902 select_script = literal(select2 % (input_id, filter_enabled))
1904 1903
1905 1904 return literal(select_html+select_script)
1906 1905
1907 1906
1908 1907 def get_visual_attr(tmpl_context_var, attr_name):
1909 1908 """
1910 1909 A safe way to get a variable from visual variable of template context
1911 1910
1912 1911 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1913 1912 :param attr_name: name of the attribute we fetch from the c.visual
1914 1913 """
1915 1914 visual = getattr(tmpl_context_var, 'visual', None)
1916 1915 if not visual:
1917 1916 return
1918 1917 else:
1919 1918 return getattr(visual, attr_name, None)
1920 1919
1921 1920
1922 1921 def get_last_path_part(file_node):
1923 1922 if not file_node.path:
1924 1923 return u''
1925 1924
1926 1925 path = safe_unicode(file_node.path.split('/')[-1])
1927 1926 return u'../' + path
1928 1927
1929 1928
1930 1929 def route_url(*args, **kwargs):
1931 1930 """
1932 1931 Wrapper around pyramids `route_url` (fully qualified url) function.
1933 1932 """
1934 1933 req = get_current_request()
1935 1934 return req.route_url(*args, **kwargs)
1936 1935
1937 1936
1938 1937 def route_path(*args, **kwargs):
1939 1938 """
1940 1939 Wrapper around pyramids `route_path` function.
1941 1940 """
1942 1941 req = get_current_request()
1943 1942 return req.route_path(*args, **kwargs)
1944 1943
1945 1944
1946 1945 def route_path_or_none(*args, **kwargs):
1947 1946 try:
1948 1947 return route_path(*args, **kwargs)
1949 1948 except KeyError:
1950 1949 return None
1951 1950
1952 1951
1953 1952 def current_route_path(request, **kw):
1954 1953 new_args = request.GET.mixed()
1955 1954 new_args.update(kw)
1956 1955 return request.current_route_path(_query=new_args)
1957 1956
1958 1957
1959 1958 def api_call_example(method, args):
1960 1959 """
1961 1960 Generates an API call example via CURL
1962 1961 """
1963 1962 args_json = json.dumps(OrderedDict([
1964 1963 ('id', 1),
1965 1964 ('auth_token', 'SECRET'),
1966 1965 ('method', method),
1967 1966 ('args', args)
1968 1967 ]))
1969 1968 return literal(
1970 1969 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
1971 1970 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1972 1971 "and needs to be of `api calls` role."
1973 1972 .format(
1974 1973 api_url=route_url('apiv2'),
1975 1974 token_url=route_url('my_account_auth_tokens'),
1976 1975 data=args_json))
1977 1976
1978 1977
1979 1978 def notification_description(notification, request):
1980 1979 """
1981 1980 Generate notification human readable description based on notification type
1982 1981 """
1983 1982 from rhodecode.model.notification import NotificationModel
1984 1983 return NotificationModel().make_description(
1985 1984 notification, translate=request.translate)
1986 1985
1987 1986
1988 1987 def go_import_header(request, db_repo=None):
1989 1988 """
1990 1989 Creates a header for go-import functionality in Go Lang
1991 1990 """
1992 1991
1993 1992 if not db_repo:
1994 1993 return
1995 1994 if 'go-get' not in request.GET:
1996 1995 return
1997 1996
1998 1997 clone_url = db_repo.clone_url()
1999 1998 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2000 1999 # we have a repo and go-get flag,
2001 2000 return literal('<meta name="go-import" content="{} {} {}">'.format(
2002 2001 prefix, db_repo.repo_type, clone_url))
2003 2002
2004 2003
2005 2004 def reviewer_as_json(*args, **kwargs):
2006 2005 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2007 2006 return _reviewer_as_json(*args, **kwargs)
2008 2007
2009 2008
2010 2009 def get_repo_view_type(request):
2011 2010 route_name = request.matched_route.name
2012 2011 route_to_view_type = {
2013 2012 'repo_changelog': 'changelog',
2014 2013 'repo_files': 'files',
2015 2014 'repo_summary': 'summary',
2016 2015 'repo_commit': 'commit'
2017 2016
2018 2017 }
2019 2018 return route_to_view_type.get(route_name)
@@ -1,1022 +1,1028 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 38
39 39 import pygments.lexers
40 40 import sqlalchemy
41 41 import sqlalchemy.engine.url
42 42 import sqlalchemy.exc
43 43 import sqlalchemy.sql
44 44 import webob
45 45 import pyramid.threadlocal
46 46 from pyramid.settings import asbool
47 47
48 48 import rhodecode
49 49 from rhodecode.translation import _, _pluralize
50 50
51 51
52 52 def md5(s):
53 53 return hashlib.md5(s).hexdigest()
54 54
55 55
56 56 def md5_safe(s):
57 57 return md5(safe_str(s))
58 58
59 59
60 60 def sha1(s):
61 61 return hashlib.sha1(s).hexdigest()
62 62
63 63
64 64 def sha1_safe(s):
65 65 return sha1(safe_str(s))
66 66
67 67
68 68 def __get_lem(extra_mapping=None):
69 69 """
70 70 Get language extension map based on what's inside pygments lexers
71 71 """
72 72 d = collections.defaultdict(lambda: [])
73 73
74 74 def __clean(s):
75 75 s = s.lstrip('*')
76 76 s = s.lstrip('.')
77 77
78 78 if s.find('[') != -1:
79 79 exts = []
80 80 start, stop = s.find('['), s.find(']')
81 81
82 82 for suffix in s[start + 1:stop]:
83 83 exts.append(s[:s.find('[')] + suffix)
84 84 return [e.lower() for e in exts]
85 85 else:
86 86 return [s.lower()]
87 87
88 88 for lx, t in sorted(pygments.lexers.LEXERS.items()):
89 89 m = map(__clean, t[-2])
90 90 if m:
91 91 m = reduce(lambda x, y: x + y, m)
92 92 for ext in m:
93 93 desc = lx.replace('Lexer', '')
94 94 d[ext].append(desc)
95 95
96 96 data = dict(d)
97 97
98 98 extra_mapping = extra_mapping or {}
99 99 if extra_mapping:
100 100 for k, v in extra_mapping.items():
101 101 if k not in data:
102 102 # register new mapping2lexer
103 103 data[k] = [v]
104 104
105 105 return data
106 106
107 107
108 108 def str2bool(_str):
109 109 """
110 110 returns True/False value from given string, it tries to translate the
111 111 string into boolean
112 112
113 113 :param _str: string value to translate into boolean
114 114 :rtype: boolean
115 115 :returns: boolean from given string
116 116 """
117 117 if _str is None:
118 118 return False
119 119 if _str in (True, False):
120 120 return _str
121 121 _str = str(_str).strip().lower()
122 122 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
123 123
124 124
125 125 def aslist(obj, sep=None, strip=True):
126 126 """
127 127 Returns given string separated by sep as list
128 128
129 129 :param obj:
130 130 :param sep:
131 131 :param strip:
132 132 """
133 133 if isinstance(obj, (basestring,)):
134 134 lst = obj.split(sep)
135 135 if strip:
136 136 lst = [v.strip() for v in lst]
137 137 return lst
138 138 elif isinstance(obj, (list, tuple)):
139 139 return obj
140 140 elif obj is None:
141 141 return []
142 142 else:
143 143 return [obj]
144 144
145 145
146 146 def convert_line_endings(line, mode):
147 147 """
148 148 Converts a given line "line end" accordingly to given mode
149 149
150 150 Available modes are::
151 151 0 - Unix
152 152 1 - Mac
153 153 2 - DOS
154 154
155 155 :param line: given line to convert
156 156 :param mode: mode to convert to
157 157 :rtype: str
158 158 :return: converted line according to mode
159 159 """
160 160 if mode == 0:
161 161 line = line.replace('\r\n', '\n')
162 162 line = line.replace('\r', '\n')
163 163 elif mode == 1:
164 164 line = line.replace('\r\n', '\r')
165 165 line = line.replace('\n', '\r')
166 166 elif mode == 2:
167 167 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
168 168 return line
169 169
170 170
171 171 def detect_mode(line, default):
172 172 """
173 173 Detects line break for given line, if line break couldn't be found
174 174 given default value is returned
175 175
176 176 :param line: str line
177 177 :param default: default
178 178 :rtype: int
179 179 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
180 180 """
181 181 if line.endswith('\r\n'):
182 182 return 2
183 183 elif line.endswith('\n'):
184 184 return 0
185 185 elif line.endswith('\r'):
186 186 return 1
187 187 else:
188 188 return default
189 189
190 190
191 191 def safe_int(val, default=None):
192 192 """
193 193 Returns int() of val if val is not convertable to int use default
194 194 instead
195 195
196 196 :param val:
197 197 :param default:
198 198 """
199 199
200 200 try:
201 201 val = int(val)
202 202 except (ValueError, TypeError):
203 203 val = default
204 204
205 205 return val
206 206
207 207
208 208 def safe_unicode(str_, from_encoding=None):
209 209 """
210 210 safe unicode function. Does few trick to turn str_ into unicode
211 211
212 212 In case of UnicodeDecode error, we try to return it with encoding detected
213 213 by chardet library if it fails fallback to unicode with errors replaced
214 214
215 215 :param str_: string to decode
216 216 :rtype: unicode
217 217 :returns: unicode object
218 218 """
219 219 if isinstance(str_, unicode):
220 220 return str_
221 221
222 222 if not from_encoding:
223 223 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
224 224 'utf8'), sep=',')
225 225 from_encoding = DEFAULT_ENCODINGS
226 226
227 227 if not isinstance(from_encoding, (list, tuple)):
228 228 from_encoding = [from_encoding]
229 229
230 230 try:
231 231 return unicode(str_)
232 232 except UnicodeDecodeError:
233 233 pass
234 234
235 235 for enc in from_encoding:
236 236 try:
237 237 return unicode(str_, enc)
238 238 except UnicodeDecodeError:
239 239 pass
240 240
241 241 try:
242 242 import chardet
243 243 encoding = chardet.detect(str_)['encoding']
244 244 if encoding is None:
245 245 raise Exception()
246 246 return str_.decode(encoding)
247 247 except (ImportError, UnicodeDecodeError, Exception):
248 248 return unicode(str_, from_encoding[0], 'replace')
249 249
250 250
251 251 def safe_str(unicode_, to_encoding=None):
252 252 """
253 253 safe str function. Does few trick to turn unicode_ into string
254 254
255 255 In case of UnicodeEncodeError, we try to return it with encoding detected
256 256 by chardet library if it fails fallback to string with errors replaced
257 257
258 258 :param unicode_: unicode to encode
259 259 :rtype: str
260 260 :returns: str object
261 261 """
262 262
263 263 # if it's not basestr cast to str
264 264 if not isinstance(unicode_, basestring):
265 265 return str(unicode_)
266 266
267 267 if isinstance(unicode_, str):
268 268 return unicode_
269 269
270 270 if not to_encoding:
271 271 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
272 272 'utf8'), sep=',')
273 273 to_encoding = DEFAULT_ENCODINGS
274 274
275 275 if not isinstance(to_encoding, (list, tuple)):
276 276 to_encoding = [to_encoding]
277 277
278 278 for enc in to_encoding:
279 279 try:
280 280 return unicode_.encode(enc)
281 281 except UnicodeEncodeError:
282 282 pass
283 283
284 284 try:
285 285 import chardet
286 286 encoding = chardet.detect(unicode_)['encoding']
287 287 if encoding is None:
288 288 raise UnicodeEncodeError()
289 289
290 290 return unicode_.encode(encoding)
291 291 except (ImportError, UnicodeEncodeError):
292 292 return unicode_.encode(to_encoding[0], 'replace')
293 293
294 294
295 295 def remove_suffix(s, suffix):
296 296 if s.endswith(suffix):
297 297 s = s[:-1 * len(suffix)]
298 298 return s
299 299
300 300
301 301 def remove_prefix(s, prefix):
302 302 if s.startswith(prefix):
303 303 s = s[len(prefix):]
304 304 return s
305 305
306 306
307 307 def find_calling_context(ignore_modules=None):
308 308 """
309 309 Look through the calling stack and return the frame which called
310 310 this function and is part of core module ( ie. rhodecode.* )
311 311
312 312 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
313 313 """
314 314
315 315 ignore_modules = ignore_modules or []
316 316
317 317 f = sys._getframe(2)
318 318 while f.f_back is not None:
319 319 name = f.f_globals.get('__name__')
320 320 if name and name.startswith(__name__.split('.')[0]):
321 321 if name not in ignore_modules:
322 322 return f
323 323 f = f.f_back
324 324 return None
325 325
326 326
327 327 def ping_connection(connection, branch):
328 328 if branch:
329 329 # "branch" refers to a sub-connection of a connection,
330 330 # we don't want to bother pinging on these.
331 331 return
332 332
333 333 # turn off "close with result". This flag is only used with
334 334 # "connectionless" execution, otherwise will be False in any case
335 335 save_should_close_with_result = connection.should_close_with_result
336 336 connection.should_close_with_result = False
337 337
338 338 try:
339 339 # run a SELECT 1. use a core select() so that
340 340 # the SELECT of a scalar value without a table is
341 341 # appropriately formatted for the backend
342 342 connection.scalar(sqlalchemy.sql.select([1]))
343 343 except sqlalchemy.exc.DBAPIError as err:
344 344 # catch SQLAlchemy's DBAPIError, which is a wrapper
345 345 # for the DBAPI's exception. It includes a .connection_invalidated
346 346 # attribute which specifies if this connection is a "disconnect"
347 347 # condition, which is based on inspection of the original exception
348 348 # by the dialect in use.
349 349 if err.connection_invalidated:
350 350 # run the same SELECT again - the connection will re-validate
351 351 # itself and establish a new connection. The disconnect detection
352 352 # here also causes the whole connection pool to be invalidated
353 353 # so that all stale connections are discarded.
354 354 connection.scalar(sqlalchemy.sql.select([1]))
355 355 else:
356 356 raise
357 357 finally:
358 358 # restore "close with result"
359 359 connection.should_close_with_result = save_should_close_with_result
360 360
361 361
362 362 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
363 363 """Custom engine_from_config functions."""
364 364 log = logging.getLogger('sqlalchemy.engine')
365 365 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
366 366 debug = asbool(configuration.get('debug'))
367 367
368 368 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
369 369
370 370 def color_sql(sql):
371 371 color_seq = '\033[1;33m' # This is yellow: code 33
372 372 normal = '\x1b[0m'
373 373 return ''.join([color_seq, sql, normal])
374 374
375 375 if use_ping_connection:
376 376 log.debug('Adding ping_connection on the engine config.')
377 377 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
378 378
379 379 if debug:
380 380 # attach events only for debug configuration
381 381 def before_cursor_execute(conn, cursor, statement,
382 382 parameters, context, executemany):
383 383 setattr(conn, 'query_start_time', time.time())
384 384 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
385 385 calling_context = find_calling_context(ignore_modules=[
386 386 'rhodecode.lib.caching_query',
387 387 'rhodecode.model.settings',
388 388 ])
389 389 if calling_context:
390 390 log.info(color_sql('call context %s:%s' % (
391 391 calling_context.f_code.co_filename,
392 392 calling_context.f_lineno,
393 393 )))
394 394
395 395 def after_cursor_execute(conn, cursor, statement,
396 396 parameters, context, executemany):
397 397 delattr(conn, 'query_start_time')
398 398
399 399 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
400 400 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
401 401
402 402 return engine
403 403
404 404
405 405 def get_encryption_key(config):
406 406 secret = config.get('rhodecode.encrypted_values.secret')
407 407 default = config['beaker.session.secret']
408 408 return secret or default
409 409
410 410
411 411 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
412 412 short_format=False):
413 413 """
414 414 Turns a datetime into an age string.
415 415 If show_short_version is True, this generates a shorter string with
416 416 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
417 417
418 418 * IMPORTANT*
419 419 Code of this function is written in special way so it's easier to
420 420 backport it to javascript. If you mean to update it, please also update
421 421 `jquery.timeago-extension.js` file
422 422
423 423 :param prevdate: datetime object
424 424 :param now: get current time, if not define we use
425 425 `datetime.datetime.now()`
426 426 :param show_short_version: if it should approximate the date and
427 427 return a shorter string
428 428 :param show_suffix:
429 429 :param short_format: show short format, eg 2D instead of 2 days
430 430 :rtype: unicode
431 431 :returns: unicode words describing age
432 432 """
433 433
434 434 def _get_relative_delta(now, prevdate):
435 435 base = dateutil.relativedelta.relativedelta(now, prevdate)
436 436 return {
437 437 'year': base.years,
438 438 'month': base.months,
439 439 'day': base.days,
440 440 'hour': base.hours,
441 441 'minute': base.minutes,
442 442 'second': base.seconds,
443 443 }
444 444
445 445 def _is_leap_year(year):
446 446 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
447 447
448 448 def get_month(prevdate):
449 449 return prevdate.month
450 450
451 451 def get_year(prevdate):
452 452 return prevdate.year
453 453
454 454 now = now or datetime.datetime.now()
455 455 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
456 456 deltas = {}
457 457 future = False
458 458
459 459 if prevdate > now:
460 460 now_old = now
461 461 now = prevdate
462 462 prevdate = now_old
463 463 future = True
464 464 if future:
465 465 prevdate = prevdate.replace(microsecond=0)
466 466 # Get date parts deltas
467 467 for part in order:
468 468 rel_delta = _get_relative_delta(now, prevdate)
469 469 deltas[part] = rel_delta[part]
470 470
471 471 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
472 472 # not 1 hour, -59 minutes and -59 seconds)
473 473 offsets = [[5, 60], [4, 60], [3, 24]]
474 474 for element in offsets: # seconds, minutes, hours
475 475 num = element[0]
476 476 length = element[1]
477 477
478 478 part = order[num]
479 479 carry_part = order[num - 1]
480 480
481 481 if deltas[part] < 0:
482 482 deltas[part] += length
483 483 deltas[carry_part] -= 1
484 484
485 485 # Same thing for days except that the increment depends on the (variable)
486 486 # number of days in the month
487 487 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
488 488 if deltas['day'] < 0:
489 489 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
490 490 deltas['day'] += 29
491 491 else:
492 492 deltas['day'] += month_lengths[get_month(prevdate) - 1]
493 493
494 494 deltas['month'] -= 1
495 495
496 496 if deltas['month'] < 0:
497 497 deltas['month'] += 12
498 498 deltas['year'] -= 1
499 499
500 500 # Format the result
501 501 if short_format:
502 502 fmt_funcs = {
503 503 'year': lambda d: u'%dy' % d,
504 504 'month': lambda d: u'%dm' % d,
505 505 'day': lambda d: u'%dd' % d,
506 506 'hour': lambda d: u'%dh' % d,
507 507 'minute': lambda d: u'%dmin' % d,
508 508 'second': lambda d: u'%dsec' % d,
509 509 }
510 510 else:
511 511 fmt_funcs = {
512 512 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
513 513 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
514 514 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
515 515 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
516 516 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
517 517 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
518 518 }
519 519
520 520 i = 0
521 521 for part in order:
522 522 value = deltas[part]
523 523 if value != 0:
524 524
525 525 if i < 5:
526 526 sub_part = order[i + 1]
527 527 sub_value = deltas[sub_part]
528 528 else:
529 529 sub_value = 0
530 530
531 531 if sub_value == 0 or show_short_version:
532 532 _val = fmt_funcs[part](value)
533 533 if future:
534 534 if show_suffix:
535 535 return _(u'in ${ago}', mapping={'ago': _val})
536 536 else:
537 537 return _(_val)
538 538
539 539 else:
540 540 if show_suffix:
541 541 return _(u'${ago} ago', mapping={'ago': _val})
542 542 else:
543 543 return _(_val)
544 544
545 545 val = fmt_funcs[part](value)
546 546 val_detail = fmt_funcs[sub_part](sub_value)
547 547 mapping = {'val': val, 'detail': val_detail}
548 548
549 549 if short_format:
550 550 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
551 551 if show_suffix:
552 552 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
553 553 if future:
554 554 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
555 555 else:
556 556 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
557 557 if show_suffix:
558 558 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
559 559 if future:
560 560 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
561 561
562 562 return datetime_tmpl
563 563 i += 1
564 564 return _(u'just now')
565 565
566 566
567 def age_from_seconds(seconds):
568 seconds = safe_int(seconds) or 0
569 prevdate = time_to_datetime(time.time() + seconds)
570 return age(prevdate, show_suffix=False, show_short_version=True)
571
572
567 573 def cleaned_uri(uri):
568 574 """
569 575 Quotes '[' and ']' from uri if there is only one of them.
570 576 according to RFC3986 we cannot use such chars in uri
571 577 :param uri:
572 578 :return: uri without this chars
573 579 """
574 580 return urllib.quote(uri, safe='@$:/')
575 581
576 582
577 583 def uri_filter(uri):
578 584 """
579 585 Removes user:password from given url string
580 586
581 587 :param uri:
582 588 :rtype: unicode
583 589 :returns: filtered list of strings
584 590 """
585 591 if not uri:
586 592 return ''
587 593
588 594 proto = ''
589 595
590 596 for pat in ('https://', 'http://'):
591 597 if uri.startswith(pat):
592 598 uri = uri[len(pat):]
593 599 proto = pat
594 600 break
595 601
596 602 # remove passwords and username
597 603 uri = uri[uri.find('@') + 1:]
598 604
599 605 # get the port
600 606 cred_pos = uri.find(':')
601 607 if cred_pos == -1:
602 608 host, port = uri, None
603 609 else:
604 610 host, port = uri[:cred_pos], uri[cred_pos + 1:]
605 611
606 612 return filter(None, [proto, host, port])
607 613
608 614
609 615 def credentials_filter(uri):
610 616 """
611 617 Returns a url with removed credentials
612 618
613 619 :param uri:
614 620 """
615 621
616 622 uri = uri_filter(uri)
617 623 # check if we have port
618 624 if len(uri) > 2 and uri[2]:
619 625 uri[2] = ':' + uri[2]
620 626
621 627 return ''.join(uri)
622 628
623 629
624 630 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
625 631 qualifed_home_url = request.route_url('home')
626 632 parsed_url = urlobject.URLObject(qualifed_home_url)
627 633 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
628 634
629 635 args = {
630 636 'scheme': parsed_url.scheme,
631 637 'user': '',
632 638 'sys_user': getpass.getuser(),
633 639 # path if we use proxy-prefix
634 640 'netloc': parsed_url.netloc+decoded_path,
635 641 'hostname': parsed_url.hostname,
636 642 'prefix': decoded_path,
637 643 'repo': repo_name,
638 644 'repoid': str(repo_id)
639 645 }
640 646 args.update(override)
641 647 args['user'] = urllib.quote(safe_str(args['user']))
642 648
643 649 for k, v in args.items():
644 650 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
645 651
646 652 # remove leading @ sign if it's present. Case of empty user
647 653 url_obj = urlobject.URLObject(uri_tmpl)
648 654 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
649 655
650 656 return safe_unicode(url)
651 657
652 658
653 659 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
654 660 """
655 661 Safe version of get_commit if this commit doesn't exists for a
656 662 repository it returns a Dummy one instead
657 663
658 664 :param repo: repository instance
659 665 :param commit_id: commit id as str
660 666 :param pre_load: optional list of commit attributes to load
661 667 """
662 668 # TODO(skreft): remove these circular imports
663 669 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
664 670 from rhodecode.lib.vcs.exceptions import RepositoryError
665 671 if not isinstance(repo, BaseRepository):
666 672 raise Exception('You must pass an Repository '
667 673 'object as first argument got %s', type(repo))
668 674
669 675 try:
670 676 commit = repo.get_commit(
671 677 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
672 678 except (RepositoryError, LookupError):
673 679 commit = EmptyCommit()
674 680 return commit
675 681
676 682
677 683 def datetime_to_time(dt):
678 684 if dt:
679 685 return time.mktime(dt.timetuple())
680 686
681 687
682 688 def time_to_datetime(tm):
683 689 if tm:
684 690 if isinstance(tm, basestring):
685 691 try:
686 692 tm = float(tm)
687 693 except ValueError:
688 694 return
689 695 return datetime.datetime.fromtimestamp(tm)
690 696
691 697
692 698 def time_to_utcdatetime(tm):
693 699 if tm:
694 700 if isinstance(tm, basestring):
695 701 try:
696 702 tm = float(tm)
697 703 except ValueError:
698 704 return
699 705 return datetime.datetime.utcfromtimestamp(tm)
700 706
701 707
702 708 MENTIONS_REGEX = re.compile(
703 709 # ^@ or @ without any special chars in front
704 710 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
705 711 # main body starts with letter, then can be . - _
706 712 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
707 713 re.VERBOSE | re.MULTILINE)
708 714
709 715
710 716 def extract_mentioned_users(s):
711 717 """
712 718 Returns unique usernames from given string s that have @mention
713 719
714 720 :param s: string to get mentions
715 721 """
716 722 usrs = set()
717 723 for username in MENTIONS_REGEX.findall(s):
718 724 usrs.add(username)
719 725
720 726 return sorted(list(usrs), key=lambda k: k.lower())
721 727
722 728
723 729 class AttributeDictBase(dict):
724 730 def __getstate__(self):
725 731 odict = self.__dict__ # get attribute dictionary
726 732 return odict
727 733
728 734 def __setstate__(self, dict):
729 735 self.__dict__ = dict
730 736
731 737 __setattr__ = dict.__setitem__
732 738 __delattr__ = dict.__delitem__
733 739
734 740
735 741 class StrictAttributeDict(AttributeDictBase):
736 742 """
737 743 Strict Version of Attribute dict which raises an Attribute error when
738 744 requested attribute is not set
739 745 """
740 746 def __getattr__(self, attr):
741 747 try:
742 748 return self[attr]
743 749 except KeyError:
744 750 raise AttributeError('%s object has no attribute %s' % (
745 751 self.__class__, attr))
746 752
747 753
748 754 class AttributeDict(AttributeDictBase):
749 755 def __getattr__(self, attr):
750 756 return self.get(attr, None)
751 757
752 758
753 759
754 760 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
755 761 def __init__(self, default_factory=None, *args, **kwargs):
756 762 # in python3 you can omit the args to super
757 763 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
758 764 self.default_factory = default_factory
759 765
760 766
761 767 def fix_PATH(os_=None):
762 768 """
763 769 Get current active python path, and append it to PATH variable to fix
764 770 issues of subprocess calls and different python versions
765 771 """
766 772 if os_ is None:
767 773 import os
768 774 else:
769 775 os = os_
770 776
771 777 cur_path = os.path.split(sys.executable)[0]
772 778 if not os.environ['PATH'].startswith(cur_path):
773 779 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
774 780
775 781
776 782 def obfuscate_url_pw(engine):
777 783 _url = engine or ''
778 784 try:
779 785 _url = sqlalchemy.engine.url.make_url(engine)
780 786 if _url.password:
781 787 _url.password = 'XXXXX'
782 788 except Exception:
783 789 pass
784 790 return unicode(_url)
785 791
786 792
787 793 def get_server_url(environ):
788 794 req = webob.Request(environ)
789 795 return req.host_url + req.script_name
790 796
791 797
792 798 def unique_id(hexlen=32):
793 799 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
794 800 return suuid(truncate_to=hexlen, alphabet=alphabet)
795 801
796 802
797 803 def suuid(url=None, truncate_to=22, alphabet=None):
798 804 """
799 805 Generate and return a short URL safe UUID.
800 806
801 807 If the url parameter is provided, set the namespace to the provided
802 808 URL and generate a UUID.
803 809
804 810 :param url to get the uuid for
805 811 :truncate_to: truncate the basic 22 UUID to shorter version
806 812
807 813 The IDs won't be universally unique any longer, but the probability of
808 814 a collision will still be very low.
809 815 """
810 816 # Define our alphabet.
811 817 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
812 818
813 819 # If no URL is given, generate a random UUID.
814 820 if url is None:
815 821 unique_id = uuid.uuid4().int
816 822 else:
817 823 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
818 824
819 825 alphabet_length = len(_ALPHABET)
820 826 output = []
821 827 while unique_id > 0:
822 828 digit = unique_id % alphabet_length
823 829 output.append(_ALPHABET[digit])
824 830 unique_id = int(unique_id / alphabet_length)
825 831 return "".join(output)[:truncate_to]
826 832
827 833
828 834 def get_current_rhodecode_user(request=None):
829 835 """
830 836 Gets rhodecode user from request
831 837 """
832 838 pyramid_request = request or pyramid.threadlocal.get_current_request()
833 839
834 840 # web case
835 841 if pyramid_request and hasattr(pyramid_request, 'user'):
836 842 return pyramid_request.user
837 843
838 844 # api case
839 845 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
840 846 return pyramid_request.rpc_user
841 847
842 848 return None
843 849
844 850
845 851 def action_logger_generic(action, namespace=''):
846 852 """
847 853 A generic logger for actions useful to the system overview, tries to find
848 854 an acting user for the context of the call otherwise reports unknown user
849 855
850 856 :param action: logging message eg 'comment 5 deleted'
851 857 :param type: string
852 858
853 859 :param namespace: namespace of the logging message eg. 'repo.comments'
854 860 :param type: string
855 861
856 862 """
857 863
858 864 logger_name = 'rhodecode.actions'
859 865
860 866 if namespace:
861 867 logger_name += '.' + namespace
862 868
863 869 log = logging.getLogger(logger_name)
864 870
865 871 # get a user if we can
866 872 user = get_current_rhodecode_user()
867 873
868 874 logfunc = log.info
869 875
870 876 if not user:
871 877 user = '<unknown user>'
872 878 logfunc = log.warning
873 879
874 880 logfunc('Logging action by {}: {}'.format(user, action))
875 881
876 882
877 883 def escape_split(text, sep=',', maxsplit=-1):
878 884 r"""
879 885 Allows for escaping of the separator: e.g. arg='foo\, bar'
880 886
881 887 It should be noted that the way bash et. al. do command line parsing, those
882 888 single quotes are required.
883 889 """
884 890 escaped_sep = r'\%s' % sep
885 891
886 892 if escaped_sep not in text:
887 893 return text.split(sep, maxsplit)
888 894
889 895 before, _mid, after = text.partition(escaped_sep)
890 896 startlist = before.split(sep, maxsplit) # a regular split is fine here
891 897 unfinished = startlist[-1]
892 898 startlist = startlist[:-1]
893 899
894 900 # recurse because there may be more escaped separators
895 901 endlist = escape_split(after, sep, maxsplit)
896 902
897 903 # finish building the escaped value. we use endlist[0] becaue the first
898 904 # part of the string sent in recursion is the rest of the escaped value.
899 905 unfinished += sep + endlist[0]
900 906
901 907 return startlist + [unfinished] + endlist[1:] # put together all the parts
902 908
903 909
904 910 class OptionalAttr(object):
905 911 """
906 912 Special Optional Option that defines other attribute. Example::
907 913
908 914 def test(apiuser, userid=Optional(OAttr('apiuser')):
909 915 user = Optional.extract(userid)
910 916 # calls
911 917
912 918 """
913 919
914 920 def __init__(self, attr_name):
915 921 self.attr_name = attr_name
916 922
917 923 def __repr__(self):
918 924 return '<OptionalAttr:%s>' % self.attr_name
919 925
920 926 def __call__(self):
921 927 return self
922 928
923 929
924 930 # alias
925 931 OAttr = OptionalAttr
926 932
927 933
928 934 class Optional(object):
929 935 """
930 936 Defines an optional parameter::
931 937
932 938 param = param.getval() if isinstance(param, Optional) else param
933 939 param = param() if isinstance(param, Optional) else param
934 940
935 941 is equivalent of::
936 942
937 943 param = Optional.extract(param)
938 944
939 945 """
940 946
941 947 def __init__(self, type_):
942 948 self.type_ = type_
943 949
944 950 def __repr__(self):
945 951 return '<Optional:%s>' % self.type_.__repr__()
946 952
947 953 def __call__(self):
948 954 return self.getval()
949 955
950 956 def getval(self):
951 957 """
952 958 returns value from this Optional instance
953 959 """
954 960 if isinstance(self.type_, OAttr):
955 961 # use params name
956 962 return self.type_.attr_name
957 963 return self.type_
958 964
959 965 @classmethod
960 966 def extract(cls, val):
961 967 """
962 968 Extracts value from Optional() instance
963 969
964 970 :param val:
965 971 :return: original value if it's not Optional instance else
966 972 value of instance
967 973 """
968 974 if isinstance(val, cls):
969 975 return val.getval()
970 976 return val
971 977
972 978
973 979 def glob2re(pat):
974 980 """
975 981 Translate a shell PATTERN to a regular expression.
976 982
977 983 There is no way to quote meta-characters.
978 984 """
979 985
980 986 i, n = 0, len(pat)
981 987 res = ''
982 988 while i < n:
983 989 c = pat[i]
984 990 i = i+1
985 991 if c == '*':
986 992 #res = res + '.*'
987 993 res = res + '[^/]*'
988 994 elif c == '?':
989 995 #res = res + '.'
990 996 res = res + '[^/]'
991 997 elif c == '[':
992 998 j = i
993 999 if j < n and pat[j] == '!':
994 1000 j = j+1
995 1001 if j < n and pat[j] == ']':
996 1002 j = j+1
997 1003 while j < n and pat[j] != ']':
998 1004 j = j+1
999 1005 if j >= n:
1000 1006 res = res + '\\['
1001 1007 else:
1002 1008 stuff = pat[i:j].replace('\\','\\\\')
1003 1009 i = j+1
1004 1010 if stuff[0] == '!':
1005 1011 stuff = '^' + stuff[1:]
1006 1012 elif stuff[0] == '^':
1007 1013 stuff = '\\' + stuff
1008 1014 res = '%s[%s]' % (res, stuff)
1009 1015 else:
1010 1016 res = res + re.escape(c)
1011 1017 return res + '\Z(?ms)'
1012 1018
1013 1019
1014 1020 def parse_byte_string(size_str):
1015 1021 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1016 1022 if not match:
1017 1023 raise ValueError('Given size:%s is invalid, please make sure '
1018 1024 'to use format of <num>(MB|KB)' % size_str)
1019 1025
1020 1026 _parts = match.groups()
1021 1027 num, type_ = _parts
1022 1028 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
@@ -1,301 +1,298 b''
1 1 //LOGIN
2 2
3 3
4 4 .loginbox {
5 5 max-width: 65%;
6 6 margin: @pagepadding auto;
7 7 font-family: @text-light;
8 8 border: @border-thickness solid @grey5;
9 9 box-sizing: border-box;
10 10
11 11 @media (max-width:1200px) {
12 12 max-width: 85%;
13 13 }
14 14
15 15 @media (max-width:768px) {
16 16 max-width: 100%;
17 17 width: 100%;
18 18 margin: 0;
19 19 }
20 20
21 21 .title {
22 22 float: none;
23 23 }
24 24
25 25 .header {
26 26 width: 100%;
27 27 padding: 0 35px;
28 28 box-sizing: border-box;
29 29
30 30 .title {
31 31 padding: 0;
32 32 }
33 33 }
34 34
35 35 .loginwrapper {
36 36 float: left;
37 37 height: 100%;
38 38 width: 100%;
39 39 padding: 35px 55px 35px 35px;
40 40 background-color: white;
41 41 box-sizing: border-box;
42 42
43 43 @media (max-width:414px) {
44 44 padding: 35px;
45 45 }
46 46 }
47 47
48 48 .left-column {
49 49 float: left;
50 50 position: relative;
51 51 width: 50%;
52 52 height: 100%;
53 53
54 54 @media (max-width:414px) {
55 55 display:none;
56 56 }
57 57 }
58 58
59 59 .right-column {
60 60 float: right;
61 61 position: relative;
62 62 width: 50%;
63 63
64 64 @media (max-width:414px) {
65 65 width: 100%;
66 66 }
67 67 }
68 68
69 69 .sign-in-image {
70 70 display: block;
71 71 width: 65%;
72 72 margin: 5% auto;
73 73 }
74 74
75 75 .sign-in-title {
76 h1 {
77 margin: 0;
78 }
79 76
80 77 h4 {
81 78 margin: @padding*2 0;
82 79 }
83 80 }
84 81
85 82 .form {
86 83 label {
87 84 display: block;
88 85 }
89 86
90 87 input {
91 88 width: 100%;
92 89 margin: 0 10% @padding 0;
93 90 .box-sizing(border-box) ;
94 91
95 92 &[type="checkbox"] {
96 93 clear: both;
97 94 width: auto;
98 95 margin: 0 1em @padding 0;
99 96 }
100 97 }
101 98
102 99 .checkbox {
103 100 display: inline;
104 101 width: auto;
105 102 }
106 103
107 104 .sign-in {
108 105 clear: both;
109 106 width: 100%;
110 107 margin: @padding 0;
111 108 }
112 109 }
113 110 .register_message,
114 111 .activation_msg {
115 112 padding: 0 0 @padding;
116 113 }
117 114
118 115 .buttons,
119 116 .links {
120 117 padding: 0;
121 118 }
122 119
123 120 .buttons {
124 121 input {
125 122 margin-right: 0;
126 123 .box-sizing(border-box);
127 124 }
128 125
129 126 #sign_up, #send {
130 127 width: 100%;
131 128 }
132 129 }
133 130
134 131 .fields {
135 132 .field.field-compact {
136 133 margin-bottom: 0px;
137 134 }
138 135
139 136 .buttons {
140 137 margin: 0;
141 138 }
142 139
143 140 .field {
144 141 margin-bottom: 15px;
145 142
146 143 input {
147 144 width: 100%;
148 145 .box-sizing(border-box);
149 146 }
150 147
151 148 .input {
152 149 margin-left: 0;
153 150 }
154 151
155 152 .label {
156 153 padding-top: 0;
157 154 }
158 155 }
159 156 }
160 157
161 158 .checkbox {
162 159 margin: 0 0 @textmargin 0;
163 160
164 161 input[type="checkbox"] {
165 162 width: auto;
166 163 }
167 164
168 165 label {
169 166 padding: 0;
170 167 min-height: 0;
171 168 }
172 169 }
173 170
174 171 .activation_msg {
175 172 padding: @padding 0 0;
176 173 color: @grey4;
177 174 }
178 175
179 176 .links {
180 177 float: right;
181 178 margin: 0;
182 179 padding: 0;
183 180 line-height: 1;
184 181
185 182 p {
186 183 float: right;
187 184 margin: 0;
188 185 line-height: 1.5em;
189 186 }
190 187 }
191 188
192 189 p.help-block {
193 190 margin-left: 0;
194 191 }
195 192 }
196 193
197 194 .user-menu.submenu {
198 195 right: 0;
199 196 left: auto;
200 197 }
201 198 #quick_login {
202 199 left: auto;
203 200 right: 0;
204 201 padding: @menupadding;
205 202 z-index: 999;
206 203 overflow: hidden;
207 204 background-color: @grey6;
208 205 color: @grey2;
209 206
210 207 h4 {
211 208 margin-bottom: 12px;
212 209 }
213 210
214 211 .form {
215 212 width: auto;
216 213 }
217 214
218 215 label, .field {
219 216 margin-bottom: 0;
220 217 }
221 218
222 219 .label {
223 220 padding-top: 0;
224 221 }
225 222
226 223 input {
227 224 min-width: 215px;
228 225 margin: 8px 0 @padding;
229 226 }
230 227
231 228 input[type="submit"] {
232 229 &:extend(.btn-primary);
233 230 width:100%;
234 231 min-width: 0;
235 232 }
236 233
237 234 .forgot_password,
238 235 .buttons .register {
239 236 a {
240 237 color: @rcblue;
241 238
242 239 &:hover {
243 240 color: @rcdarkblue;
244 241 }
245 242 }
246 243 }
247 244
248 245 .buttons {
249 246 margin: 0;
250 247 }
251 248
252 249 .buttons a {
253 250 padding: 8px 0;
254 251 line-height: 1.4em;
255 252 color: @grey4;
256 253
257 254 &:hover {
258 255 color: @grey2;
259 256 }
260 257 }
261 258
262 259 #sign_in {
263 260 margin-bottom: 0
264 261 }
265 262
266 263 .big_gravatar {
267 264 float: left;
268 265 display: block;
269 266 margin-top: .5em;
270 267 }
271 268
272 269 .full_name,
273 270 .email {
274 271 margin: 0 0 0 65px;
275 272 }
276 273
277 274 .email {
278 275 font-family: @text-light;
279 276 }
280 277
281 278 ol.links {
282 279 clear:both;
283 280 margin: 0;
284 281 padding: @padding 0 0 0;
285 282
286 283 li {
287 284 border-top: @border-thickness solid @grey5;
288 285
289 286 input {
290 287 margin: @padding 0 0 0;
291 288 }
292 289 }
293 290 }
294 291 }
295 292 .submenu #quick_login li:hover {
296 293 background-color: transparent;
297 294 }
298 295
299 296 #quick_login_link:hover + #quick_login {
300 297 display: block;
301 298 }
@@ -1,696 +1,663 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 %if c.rhodecode_name:
15 15 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
16 16 %endif
17 17 </div>
18 18 <!-- MENU BAR NAV -->
19 19 ${self.menu_bar_nav()}
20 20 <!-- END MENU BAR NAV -->
21 21 </div>
22 22 </div>
23 23 ${self.menu_bar_subnav()}
24 24 <!-- END HEADER -->
25 25
26 26 <!-- CONTENT -->
27 27 <div id="content" class="wrapper">
28 28
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 30
31 31 <div class="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 </div>
38 38 <!-- FOOTER -->
39 39 <div id="footer">
40 40 <div id="footer-inner" class="title wrapper">
41 41 <div>
42 42 <p class="footer-link-right">
43 43 % if c.visual.show_version:
44 44 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
45 45 % endif
46 46 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
47 47 % if c.visual.rhodecode_support_url:
48 48 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
49 49 % endif
50 50 </p>
51 51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52 52 <p class="server-instance" style="display:${sid}">
53 53 ## display hidden instance ID if specially defined
54 54 % if c.rhodecode_instanceid:
55 55 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
56 56 % endif
57 57 </p>
58 58 </div>
59 59 </div>
60 60 </div>
61 61
62 62 <!-- END FOOTER -->
63 63
64 64 ### MAKO DEFS ###
65 65
66 66 <%def name="menu_bar_subnav()">
67 67 </%def>
68 68
69 69 <%def name="breadcrumbs(class_='breadcrumbs')">
70 70 <div class="${class_}">
71 71 ${self.breadcrumbs_links()}
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="admin_menu()">
76 76 <ul class="admin_menu submenu">
77 77 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
78 78 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
79 79 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
80 80 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
81 81 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
82 82 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
83 83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
84 84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
85 85 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
86 86 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
87 87 </ul>
88 88 </%def>
89 89
90 90
91 91 <%def name="dt_info_panel(elements)">
92 92 <dl class="dl-horizontal">
93 93 %for dt, dd, title, show_items in elements:
94 94 <dt>${dt}:</dt>
95 95 <dd title="${h.tooltip(title)}">
96 96 %if callable(dd):
97 97 ## allow lazy evaluation of elements
98 98 ${dd()}
99 99 %else:
100 100 ${dd}
101 101 %endif
102 102 %if show_items:
103 103 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
104 104 %endif
105 105 </dd>
106 106
107 107 %if show_items:
108 108 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
109 109 %for item in show_items:
110 110 <dt></dt>
111 111 <dd>${item}</dd>
112 112 %endfor
113 113 </div>
114 114 %endif
115 115
116 116 %endfor
117 117 </dl>
118 118 </%def>
119 119
120 120
121 121 <%def name="gravatar(email, size=16)">
122 122 <%
123 123 if (size > 16):
124 124 gravatar_class = 'gravatar gravatar-large'
125 125 else:
126 126 gravatar_class = 'gravatar'
127 127 %>
128 128 <%doc>
129 129 TODO: johbo: For now we serve double size images to make it smooth
130 130 for retina. This is how it worked until now. Should be replaced
131 131 with a better solution at some point.
132 132 </%doc>
133 133 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
134 134 </%def>
135 135
136 136
137 137 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
138 138 <% email = h.email_or_none(contact) %>
139 139 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
140 140 ${self.gravatar(email, size)}
141 141 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
142 142 </div>
143 143 </%def>
144 144
145 145
146 146 ## admin menu used for people that have some admin resources
147 147 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
148 148 <ul class="submenu">
149 149 %if repositories:
150 150 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
151 151 %endif
152 152 %if repository_groups:
153 153 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
154 154 %endif
155 155 %if user_groups:
156 156 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
157 157 %endif
158 158 </ul>
159 159 </%def>
160 160
161 161 <%def name="repo_page_title(repo_instance)">
162 162 <div class="title-content">
163 163 <div class="title-main">
164 164 ## SVN/HG/GIT icons
165 165 %if h.is_hg(repo_instance):
166 166 <i class="icon-hg"></i>
167 167 %endif
168 168 %if h.is_git(repo_instance):
169 169 <i class="icon-git"></i>
170 170 %endif
171 171 %if h.is_svn(repo_instance):
172 172 <i class="icon-svn"></i>
173 173 %endif
174 174
175 175 ## public/private
176 176 %if repo_instance.private:
177 177 <i class="icon-repo-private"></i>
178 178 %else:
179 179 <i class="icon-repo-public"></i>
180 180 %endif
181 181
182 182 ## repo name with group name
183 183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
184 184
185 185 </div>
186 186
187 187 ## FORKED
188 188 %if repo_instance.fork:
189 189 <p>
190 190 <i class="icon-code-fork"></i> ${_('Fork of')}
191 191 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
192 192 </p>
193 193 %endif
194 194
195 195 ## IMPORTED FROM REMOTE
196 196 %if repo_instance.clone_uri:
197 197 <p>
198 198 <i class="icon-code-fork"></i> ${_('Clone from')}
199 199 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
200 200 </p>
201 201 %endif
202 202
203 203 ## LOCKING STATUS
204 204 %if repo_instance.locked[0]:
205 205 <p class="locking_locked">
206 206 <i class="icon-repo-lock"></i>
207 207 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
208 208 </p>
209 209 %elif repo_instance.enable_locking:
210 210 <p class="locking_unlocked">
211 211 <i class="icon-repo-unlock"></i>
212 212 ${_('Repository not locked. Pull repository to lock it.')}
213 213 </p>
214 214 %endif
215 215
216 216 </div>
217 217 </%def>
218 218
219 219 <%def name="repo_menu(active=None)">
220 220 <%
221 221 def is_active(selected):
222 222 if selected == active:
223 223 return "active"
224 224 %>
225 225
226 226 <!--- CONTEXT BAR -->
227 227 <div id="context-bar">
228 228 <div class="wrapper">
229 229 <ul id="context-pages" class="navigation horizontal-list">
230 230 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
231 231 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
232 232 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
233 233 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li>
255 255 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
256 256 href="${h.route_path('repo_compare',
257 257 repo_name=c.rhodecode_db_repo.fork.repo_name,
258 258 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
259 259 source_ref=c.rhodecode_db_repo.landing_rev[1],
260 260 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
261 261 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
262 262 _query=dict(merge=1))}"
263 263 >
264 264 ${_('Compare fork')}
265 265 </a>
266 266 </li>
267 267 %endif
268 268
269 269 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
270 270
271 271 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
272 272 %if c.rhodecode_db_repo.locked[0]:
273 273 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
274 274 %else:
275 275 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
276 276 %endif
277 277 %endif
278 278 %if c.rhodecode_user.username != h.DEFAULT_USER:
279 279 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
280 280 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
281 281 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
282 282 %endif
283 283 %endif
284 284 </ul>
285 285 </li>
286 286 </ul>
287 287 </div>
288 288 <div class="clear"></div>
289 289 </div>
290 290 % if c.rhodecode_db_repo.archived:
291 291 <div class="alert alert-warning text-center">
292 292 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
293 293 </div>
294 294 % endif
295 295 <!--- END CONTEXT BAR -->
296 296
297 297 </%def>
298 298
299 299 <%def name="usermenu(active=False)">
300 300 ## USER MENU
301 301 <li id="quick_login_li" class="${'active' if active else ''}">
302 <a id="quick_login_link" class="menulink childs">
303 ${gravatar(c.rhodecode_user.email, 20)}
304 <span class="user">
305 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
307 %else:
308 <span>${_('Sign in')}</span>
309 %endif
310 </span>
311 </a>
312
313 <div class="user-menu submenu">
314 <div id="quick_login">
315 %if c.rhodecode_user.username == h.DEFAULT_USER:
316 <h4>${_('Sign in to your account')}</h4>
317 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
318 <div class="form form-vertical">
319 <div class="fields">
320 <div class="field">
321 <div class="label">
322 <label for="username">${_('Username')}:</label>
323 </div>
324 <div class="input">
325 ${h.text('username',class_='focus',tabindex=1)}
326 </div>
327
328 </div>
329 <div class="field">
330 <div class="label">
331 <label for="password">${_('Password')}:</label>
332 %if h.HasPermissionAny('hg.password_reset.enabled')():
333 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
334 %endif
335 </div>
336 <div class="input">
337 ${h.password('password',class_='focus',tabindex=2)}
338 </div>
302 % if c.rhodecode_user.username == h.DEFAULT_USER:
303 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
304 ${gravatar(c.rhodecode_user.email, 20)}
305 <span class="user">
306 <span>${_('Sign in')}</span>
307 </span>
308 </a>
309 % else:
310 ## logged in user
311 <a id="quick_login_link" class="menulink childs">
312 ${gravatar(c.rhodecode_user.email, 20)}
313 <span class="user">
314 <span class="menu_link_user">${c.rhodecode_user.username}</span>
315 <div class="show_more"></div>
316 </span>
317 </a>
318 ## subnav with menu for logged in user
319 <div class="user-menu submenu">
320 <div id="quick_login">
321 %if c.rhodecode_user.username != h.DEFAULT_USER:
322 <div class="">
323 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
324 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
325 <div class="email">${c.rhodecode_user.email}</div>
339 326 </div>
340 <div class="buttons">
341 <div class="register">
342 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
343 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
344 %endif
345 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
346 </div>
347 <div class="submit">
348 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
349 </div>
327 <div class="">
328 <ol class="links">
329 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
330 % if c.rhodecode_user.personal_repo_group:
331 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
332 % endif
333 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
334
335 <li class="logout">
336 ${h.secure_form(h.route_path('logout'), request=request)}
337 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
338 ${h.end_form()}
339 </li>
340 </ol>
350 341 </div>
351 </div>
352 </div>
353 ${h.end_form()}
354 %else:
355 <div class="">
356 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
357 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
358 <div class="email">${c.rhodecode_user.email}</div>
359 </div>
360 <div class="">
361 <ol class="links">
362 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
363 % if c.rhodecode_user.personal_repo_group:
364 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
365 % endif
366 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
367
368 <li class="logout">
369 ${h.secure_form(h.route_path('logout'), request=request)}
370 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
371 ${h.end_form()}
372 </li>
373 </ol>
374 </div>
375 %endif
376 </div>
377 </div>
378 %if c.rhodecode_user.username != h.DEFAULT_USER:
379 <div class="pill_container">
380 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
381 </div>
382 % endif
342 %endif
343 </div>
344 </div>
345 ## unread counter
346 <div class="pill_container">
347 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
348 </div>
349 % endif
383 350 </li>
384 351 </%def>
385 352
386 353 <%def name="menu_items(active=None)">
387 354 <%
388 355 def is_active(selected):
389 356 if selected == active:
390 357 return "active"
391 358 return ""
392 359 %>
393 360
394 361 <ul id="quick" class="main_nav navigation horizontal-list">
395 362 ## notice box for important system messages
396 363 <li style="display: none">
397 364 <a class="notice-box" href="#openNotice" onclick="showNoticeBox(); return false">
398 365 <div class="menulabel-notice" >
399 366 0
400 367 </div>
401 368 </a>
402 369 </li>
403 370
404 371 ## Main filter
405 372 <li>
406 373 <div class="menulabel main_filter_box">
407 374 <div class="main_filter_input_box">
408 375 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/>
409 376 </div>
410 377 <div class="main_filter_help_box">
411 378 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
412 379 </div>
413 380 </div>
414 381
415 382 <div id="main_filter_help" style="display: none">
416 383 Use '/' key to quickly access this field.
417 384 Enter name of repository, or repository group for quick search.
418 385
419 386 Prefix query to allow special search:
420 387
421 388 user:admin, to search for usernames
422 389
423 390 user_group:devops, to search for user groups
424 391
425 392 commit:efced4, to search for commits
426 393
427 394 </div>
428 395 </li>
429 396
430 397 ## ROOT MENU
431 398 %if c.rhodecode_user.username != h.DEFAULT_USER:
432 399 <li class="${is_active('journal')}">
433 400 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
434 401 <div class="menulabel">${_('Journal')}</div>
435 402 </a>
436 403 </li>
437 404 %else:
438 405 <li class="${is_active('journal')}">
439 406 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
440 407 <div class="menulabel">${_('Public journal')}</div>
441 408 </a>
442 409 </li>
443 410 %endif
444 411 <li class="${is_active('gists')}">
445 412 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
446 413 <div class="menulabel">${_('Gists')}</div>
447 414 </a>
448 415 </li>
449 416 <li class="${is_active('search')}">
450 417 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
451 418 <div class="menulabel">${_('Search')}</div>
452 419 </a>
453 420 </li>
454 421 % if h.HasPermissionAll('hg.admin')('access admin main page'):
455 422 <li class="${is_active('admin')}">
456 423 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
457 424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
458 425 </a>
459 426 ${admin_menu()}
460 427 </li>
461 428 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
462 429 <li class="${is_active('admin')}">
463 430 <a class="menulink childs" title="${_('Delegated Admin settings')}">
464 431 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
465 432 </a>
466 433 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
467 434 c.rhodecode_user.repository_groups_admin,
468 435 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
469 436 </li>
470 437 % endif
471 438 ## render extra user menu
472 439 ${usermenu(active=(active=='my_account'))}
473 440
474 441 % if c.debug_style:
475 442 <li>
476 443 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
477 444 <div class="menulabel">${_('[Style]')}</div>
478 445 </a>
479 446 </li>
480 447 % endif
481 448 </ul>
482 449
483 450 <script type="text/javascript">
484 451 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
485 452
486 453 var formatRepoResult = function(result, container, query, escapeMarkup) {
487 454 return function(data, escapeMarkup) {
488 455 if (!data.repo_id){
489 456 return data.text; // optgroup text Repositories
490 457 }
491 458
492 459 var tmpl = '';
493 460 var repoType = data['repo_type'];
494 461 var repoName = data['text'];
495 462
496 463 if(data && data.type == 'repo'){
497 464 if(repoType === 'hg'){
498 465 tmpl += '<i class="icon-hg"></i> ';
499 466 }
500 467 else if(repoType === 'git'){
501 468 tmpl += '<i class="icon-git"></i> ';
502 469 }
503 470 else if(repoType === 'svn'){
504 471 tmpl += '<i class="icon-svn"></i> ';
505 472 }
506 473 if(data['private']){
507 474 tmpl += '<i class="icon-lock" ></i> ';
508 475 }
509 476 else if(visualShowPublicIcon){
510 477 tmpl += '<i class="icon-unlock-alt"></i> ';
511 478 }
512 479 }
513 480 tmpl += escapeMarkup(repoName);
514 481 return tmpl;
515 482
516 483 }(result, escapeMarkup);
517 484 };
518 485
519 486
520 487 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
521 488
522 489 if (value.split(':').length === 2) {
523 490 value = value.split(':')[1]
524 491 }
525 492
526 493 var searchType = data['type'];
527 494 var valueDisplay = data['value_display'];
528 495
529 496 var escapeRegExChars = function (value) {
530 497 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
531 498 };
532 499 var pattern = '(' + escapeRegExChars(value) + ')';
533 500
534 501 // highlight match
535 502 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
536 503 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
537 504
538 505 var icon = '';
539 506
540 507 if (searchType === 'hint') {
541 508 icon += '<i class="icon-folder-close"></i> ';
542 509 }
543 510 else if (searchType === 'search') {
544 511 icon += '<i class="icon-more"></i> ';
545 512 }
546 513 else if (searchType === 'repo') {
547 514 if (data['repo_type'] === 'hg') {
548 515 icon += '<i class="icon-hg"></i> ';
549 516 }
550 517 else if (data['repo_type'] === 'git') {
551 518 icon += '<i class="icon-git"></i> ';
552 519 }
553 520 else if (data['repo_type'] === 'svn') {
554 521 icon += '<i class="icon-svn"></i> ';
555 522 }
556 523 if (data['private']) {
557 524 icon += '<i class="icon-lock" ></i> ';
558 525 }
559 526 else if (visualShowPublicIcon) {
560 527 icon += '<i class="icon-unlock-alt"></i> ';
561 528 }
562 529 }
563 530 else if (searchType === 'repo_group') {
564 531 icon += '<i class="icon-folder-close"></i> ';
565 532 }
566 533 else if (searchType === 'user_group') {
567 534 icon += '<i class="icon-group"></i> ';
568 535 }
569 536 else if (searchType === 'user') {
570 537 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
571 538 }
572 539 else if (searchType === 'commit') {
573 540 icon += '<i class="icon-tag"></i>';
574 541 }
575 542
576 543 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
577 544 return tmpl.format(icon, valueDisplay);
578 545 };
579 546
580 547 var handleSelect = function(element, suggestion) {
581 548 if (suggestion.type === "hint") {
582 549 // we skip action
583 550 $('#main_filter').focus();
584 551 } else {
585 552 window.location = suggestion['url'];
586 553 }
587 554 };
588 555 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
589 556 if (queryLowerCase.split(':').length === 2) {
590 557 queryLowerCase = queryLowerCase.split(':')[1]
591 558 }
592 559 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
593 560 };
594 561
595 562 $('#main_filter').autocomplete({
596 563 serviceUrl: pyroutes.url('goto_switcher_data'),
597 564 params: {"search_context": templateContext.search_context},
598 565 minChars:2,
599 566 maxHeight:400,
600 567 deferRequestBy: 300, //miliseconds
601 568 tabDisabled: true,
602 569 autoSelectFirst: true,
603 570 formatResult: autocompleteMainFilterFormatResult,
604 571 lookupFilter: autocompleteMainFilterResult,
605 572 onSelect: function (element, suggestion) {
606 573 handleSelect(element, suggestion);
607 574 return false;
608 575 },
609 576 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
610 577 if (jqXHR !== 'abort') {
611 578 alert("Error during search.\nError code: {0}".format(textStatus));
612 579 window.location = '';
613 580 }
614 581 }
615 582 });
616 583
617 584 showMainFilterBox = function () {
618 585 $('#main_filter_help').toggle();
619 586 }
620 587
621 588 </script>
622 589 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
623 590 </%def>
624 591
625 592 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
626 593 <div class="modal-dialog">
627 594 <div class="modal-content">
628 595 <div class="modal-header">
629 596 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
630 597 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
631 598 </div>
632 599 <div class="modal-body">
633 600 <div class="block-left">
634 601 <table class="keyboard-mappings">
635 602 <tbody>
636 603 <tr>
637 604 <th></th>
638 605 <th>${_('Site-wide shortcuts')}</th>
639 606 </tr>
640 607 <%
641 608 elems = [
642 609 ('/', 'Use quick search box'),
643 610 ('g h', 'Goto home page'),
644 611 ('g g', 'Goto my private gists page'),
645 612 ('g G', 'Goto my public gists page'),
646 613 ('n r', 'New repository page'),
647 614 ('n g', 'New gist page'),
648 615 ]
649 616 %>
650 617 %for key, desc in elems:
651 618 <tr>
652 619 <td class="keys">
653 620 <span class="key tag">${key}</span>
654 621 </td>
655 622 <td>${desc}</td>
656 623 </tr>
657 624 %endfor
658 625 </tbody>
659 626 </table>
660 627 </div>
661 628 <div class="block-left">
662 629 <table class="keyboard-mappings">
663 630 <tbody>
664 631 <tr>
665 632 <th></th>
666 633 <th>${_('Repositories')}</th>
667 634 </tr>
668 635 <%
669 636 elems = [
670 637 ('g s', 'Goto summary page'),
671 638 ('g c', 'Goto changelog page'),
672 639 ('g f', 'Goto files page'),
673 640 ('g F', 'Goto files page with file search activated'),
674 641 ('g p', 'Goto pull requests page'),
675 642 ('g o', 'Goto repository settings'),
676 643 ('g O', 'Goto repository permissions settings'),
677 644 ]
678 645 %>
679 646 %for key, desc in elems:
680 647 <tr>
681 648 <td class="keys">
682 649 <span class="key tag">${key}</span>
683 650 </td>
684 651 <td>${desc}</td>
685 652 </tr>
686 653 %endfor
687 654 </tbody>
688 655 </table>
689 656 </div>
690 657 </div>
691 658 <div class="modal-footer">
692 659 </div>
693 660 </div><!-- /.modal-content -->
694 661 </div><!-- /.modal-dialog -->
695 662 </div><!-- /.modal -->
696 663
@@ -1,90 +1,104 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base/root.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Sign In')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <style>body{background-color:#eeeeee;}</style>
12 12 <div class="loginbox">
13 13 <div class="header">
14 14 <div id="header-inner" class="title">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
17 17 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
18 18 </div>
19 19 %if c.rhodecode_name:
20 20 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
21 21 %endif
22 22 </div>
23 23 </div>
24 24 </div>
25 25
26 26 <div class="loginwrapper">
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28
28 29 <div class="left-column">
29 30 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
30 31 </div>
32
31 33 <%block name="above_login_button" />
32 34 <div id="login" class="right-column">
33 35 <!-- login -->
34 36 <div class="sign-in-title">
35 <h1>${_('Sign In')}</h1>
36 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
37 <h4>${h.link_to(_("Go to the registration page to create a new account."), request.route_path('register'))}</h4>
38 %endif
37 <h1>${_('Sign In using username/password')}</h1>
39 38 </div>
40 39 <div class="inner form">
41 40 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
42 41
43 42 <label for="username">${_('Username')}:</label>
44 43 ${h.text('username', class_='focus', value=defaults.get('username'))}
45 44 %if 'username' in errors:
46 45 <span class="error-message">${errors.get('username')}</span>
47 46 <br />
48 47 %endif
49 48
50 <label for="password">${_('Password')}:</label>
49 <label for="password">${_('Password')}:
50 %if h.HasPermissionAny('hg.password_reset.enabled')():
51 <div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
52 %endif
53
54 </label>
51 55 ${h.password('password', class_='focus')}
52 56 %if 'password' in errors:
53 57 <span class="error-message">${errors.get('password')}</span>
54 58 <br />
55 59 %endif
56 60
57 61 ${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
58 <label class="checkbox" for="remember">${_('Remember me')}</label>
62 <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %>
63 % if timeout == '0':
64 <% remember_label = _('Remember my indefinitely') %>
65 % else:
66 <% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
67 % endif
68 <label class="checkbox" for="remember">${remember_label}</label>
59 69
60 %if h.HasPermissionAny('hg.password_reset.enabled')():
61 <p class="links">
62 ${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset')}
63 </p>
64 %elif h.HasPermissionAny('hg.password_reset.hidden')():
70 <p class="links">
71 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
72 ${h.link_to(_("Create a new account."), request.route_path('register'))}
73 %endif
74 </p>
75
76 %if not h.HasPermissionAny('hg.password_reset.enabled')():
77 ## password reset hidden or disabled.
65 78 <p class="help-block">
66 ${_('Password reset is disabled. Please contact ')}
79 ${_('Password reset is disabled.')} <br/>
80 ${_('Please contact ')}
67 81 % if c.visual.rhodecode_support_url:
68 82 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
69 83 ${_('or')}
70 84 % endif
71 85 ${_('an administrator if you need help.')}
72 86 </p>
73 87 %endif
74 88
75 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in")}
76 <p class="help-block pull-right">
77 RhodeCode ${c.rhodecode_edition}
78 </p>
89 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
90
79 91 ${h.end_form()}
80 92 <script type="text/javascript">
81 93 $(document).ready(function(){
82 94 $('#username').focus();
83 95 })
84 96 </script>
97
85 98 </div>
86 99 <!-- end login -->
100
87 101 <%block name="below_login_button" />
88 102 </div>
89 103 </div>
90 104 </div>
@@ -1,149 +1,146 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base/root.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Create an Account')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10 <style>body{background-color:#eeeeee;}</style>
11 11
12 12 <div class="loginbox">
13 13 <div class="header">
14 14 <div id="header-inner" class="title">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
17 17 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
18 18 </div>
19 19 %if c.rhodecode_name:
20 20 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
21 21 %endif
22 22 </div>
23 23 </div>
24 24 </div>
25 25
26 26 <div class="loginwrapper">
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28 <div class="left-column">
29 29 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
30 30 </div>
31 31 <%block name="above_register_button" />
32 32 <div id="register" class="right-column">
33 33 <!-- login -->
34 34 <div class="sign-in-title">
35 35 % if external_auth_provider:
36 36 <h1>${_('Create an account linked with {}').format(external_auth_provider)}</h1>
37 37 % else:
38 38 <h1>${_('Create an account')}</h1>
39 39 % endif
40 40
41 41 <h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4>
42 42 </div>
43 43 <div class="inner form">
44 44 ${h.form(request.route_path('register'), needs_csrf_token=False)}
45 45
46 46 <label for="username">${_('Username')}:</label>
47 47 ${h.text('username', defaults.get('username'))}
48 48 %if 'username' in errors:
49 49 <span class="error-message">${errors.get('username')}</span>
50 50 <br />
51 51 %endif
52 52
53 53 % if external_auth_provider:
54 54 ## store internal marker about external identity
55 55 ${h.hidden('external_identity', external_auth_provider)}
56 56 ## hide password prompts for social auth
57 57 <div style="display: none">
58 58 % endif
59 59
60 60 <label for="password">${_('Password')}:</label>
61 61 ${h.password('password', defaults.get('password'))}
62 62 %if 'password' in errors:
63 63 <span class="error-message">${errors.get('password')}</span>
64 64 <br />
65 65 %endif
66 66
67 67 <label for="password_confirmation">${_('Re-enter password')}:</label>
68 68 ${h.password('password_confirmation', defaults.get('password_confirmation'))}
69 69 %if 'password_confirmation' in errors:
70 70 <span class="error-message">${errors.get('password_confirmation')}</span>
71 71 <br />
72 72 %endif
73 73
74 74 % if external_auth_provider:
75 75 ## hide password prompts for social auth
76 76 </div>
77 77 % endif
78 78
79 79 <label for="firstname">${_('First Name')}:</label>
80 80 ${h.text('firstname', defaults.get('firstname'))}
81 81 %if 'firstname' in errors:
82 82 <span class="error-message">${errors.get('firstname')}</span>
83 83 <br />
84 84 %endif
85 85
86 86 <label for="lastname">${_('Last Name')}:</label>
87 87 ${h.text('lastname', defaults.get('lastname'))}
88 88 %if 'lastname' in errors:
89 89 <span class="error-message">${errors.get('lastname')}</span>
90 90 <br />
91 91 %endif
92 92
93 93 <label for="email">${_('Email')}:</label>
94 94 ${h.text('email', defaults.get('email'))}
95 95 %if 'email' in errors:
96 96 <span class="error-message">${errors.get('email')}</span>
97 97 <br />
98 98 %endif
99 99
100 100 %if captcha_active:
101 101 <div>
102 102 <label for="recaptcha">${_('Captcha')}:</label>
103 103 ${h.hidden('recaptcha_field')}
104 104 <div id="recaptcha"></div>
105 105 %if 'recaptcha_field' in errors:
106 106 <span class="error-message">${errors.get('recaptcha_field')}</span>
107 107 <br />
108 108 %endif
109 109 </div>
110 110 %endif
111 111
112 112 %if not auto_active:
113 113 <p class="activation_msg">
114 114 ${_('Account activation requires admin approval.')}
115 115 </p>
116 116 %endif
117 117 <p class="register_message">
118 118 ${register_message|n}
119 119 </p>
120 120
121 ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")}
122 <p class="help-block pull-right">
123 RhodeCode ${c.rhodecode_edition}
124 </p>
121 ${h.submit('sign_up',_('Create Account'), class_="btn sign-in", title=_('Create Account in {}').format(c.rhodecode_edition))}
125 122 ${h.end_form()}
126 123 </div>
127 124 <%block name="below_register_button" />
128 125 </div>
129 126 </div>
130 127 </div>
131 128
132 129
133 130 <script type="text/javascript">
134 131 $(document).ready(function(){
135 132 $('#username').focus();
136 133 });
137 134 </script>
138 135
139 136 % if captcha_active:
140 137 <script type="text/javascript">
141 138 var onloadCallback = function() {
142 139 grecaptcha.render('recaptcha', {
143 140 'sitekey' : "${captcha_public_key}"
144 141 });
145 142 };
146 143 </script>
147 144 <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
148 145 % endif
149 146
General Comments 0
You need to be logged in to leave comments. Login now