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