##// END OF EJS Templates
Public user group profile Task #5326
Bartłomiej Wołyńczyk -
r2638:01feb8aa default
parent child Browse files
Show More
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def includeme(config):
23 config.add_route(
24 name='user_group_profile',
25 pattern='/_profile_user_group/{user_group_name}')
26 # Scan module for configuration decorators.
27 config.scan('.views', ignore='.tests')
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.model.user_group import UserGroupModel
21 from rhodecode.tests import (
22 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.utils import AssertResponse
25
26 fixture = Fixture()
27
28
29 def route_path(name, **kwargs):
30 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
31
32
33 class TestUsersController(TestController):
34
35 def test_user_group_profile(self, user_util):
36 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
37 user, usergroup = user_util.create_user_with_group()
38
39 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
40 response.mustcontain(usergroup.users_group_name)
41 response.mustcontain(user.username)
42
43 def test_user_can_check_own_group(self, user_util):
44 user = user_util.create_user(
45 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
46 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
47 usergroup = user_util.create_user_group(owner=user)
48 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
49 response.mustcontain(usergroup.users_group_name)
50 response.mustcontain(user.username)
51
52 def test_user_can_not_check_other_group(self, user_util):
53 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
54 user_group = user_util.create_user_group()
55 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
56 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
57 assert response.status_code == 404
58
59 def test_another_user_can_check_if_he_is_in_group(self, user_util):
60 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
61 user = user_util.create_user(
62 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
63 user_group = user_util.create_user_group()
64 UserGroupModel().add_user_to_group(user_group, user)
65 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
66 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
67 response.mustcontain(user_group.users_group_name)
68 response.mustcontain(user.username)
69
70 def test_with_anonymous_user(self, user_util):
71 user = user_util.create_user(
72 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
73 user_group = user_util.create_user_group()
74 UserGroupModel().add_user_to_group(user_group, user)
75 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
76 assert response.status_code == 302 No newline at end of file
@@ -0,0 +1,53 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPNotFound
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.auth import HasUserGroupPermissionAnyDecorator, LoginRequired, NotAnonymous
28 from rhodecode.model.db import UserGroup, User
29
30
31 log = logging.getLogger(__name__)
32
33
34 class UserGroupProfileView(BaseAppView):
35
36 @LoginRequired()
37 @NotAnonymous()
38 @HasUserGroupPermissionAnyDecorator('usergroup.read', 'usergroup.write', 'usergroup.admin',)
39 @view_config(
40 route_name='user_group_profile', request_method='GET',
41 renderer='rhodecode:templates/user_group/user_group.mako')
42 def user_group_profile(self):
43 c = self._get_local_tmpl_context()
44 c.active = 'profile'
45 self.db_user_group_name = self.request.matchdict.get('user_group_name')
46 c.user_group = UserGroup().get_by_group_name(self.db_user_group_name)
47 if not c.user_group:
48 raise HTTPNotFound()
49 group_members_obj = sorted((x.user for x in c.user_group.members),
50 key=lambda u: u.username.lower())
51 c.group_members = group_members_obj
52 c.anonymous = self._rhodecode_user.username == User.DEFAULT_USER
53 return self._get_template_context(c)
@@ -0,0 +1,70 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 <div class="panel panel-default user-profile">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('User group profile')}</h3>
6 %if h.HasPermissionAny('hg.admin')():
7 ${h.link_to(_('Edit'), h.route_path('edit_user_group', user_group_id=c.user_group.users_group_id), class_='panel-edit')}
8 %endif
9 </div>
10
11 <div class="panel-body user-profile-content">
12
13 <div class="fieldset">
14 <div class="left-label">
15 ${_('Group Name')}:
16 </div>
17 <div class="right-content">
18 ${c.user_group.users_group_name}
19 </div>
20 </div>
21 <div class="fieldset">
22 <div class="left-label">
23 ${_('Owner')}:
24 </div>
25 <div class="group_member">
26 ${base.gravatar(c.user_group.user.email, 16)}
27 <span class="username user">${h.link_to_user(c.user_group.user)}</span>
28
29 </div>
30 </div>
31 <div class="fieldset">
32 <div class="left-label">
33 ${_('Active')}:
34 </div>
35 <div class="right-content">
36 ${c.user_group.users_group_active}
37 </div>
38 </div>
39 % if not c.anonymous:
40 <div class="fieldset">
41 <div class="left-label">
42 ${_('Members')}:
43 </div>
44 <div class="right-content">
45 <table id="group_members_placeholder" class="rctable group_members">
46 <th>${_('Username')}</th>
47 % if c.group_members:
48 % for user in c.group_members:
49 <tr>
50 <td id="member_user_${user.user_id}" class="td-author">
51 <div class="group_member">
52 ${base.gravatar(user.email, 16)}
53 <span class="username user">${h.link_to(h.person(user), h.route_path('user_edit',user_id=user.user_id))}</span>
54 <input type="hidden" name="__start__" value="member:mapping">
55 <input type="hidden" name="member_user_id" value="${user.user_id}">
56 <input type="hidden" name="type" value="existing" id="member_${user.user_id}">
57 <input type="hidden" name="__end__" value="member:mapping">
58 </div>
59 </td>
60 </tr>
61 % endfor
62 % else:
63 <tr><td colspan="2">${_('No members yet')}</td></tr>
64 % endif
65 </table>
66 </div>
67 </div>
68 % endif
69 </div>
70 </div> No newline at end of file
@@ -0,0 +1,46 b''
1 <%inherit file="/base/base.mako"/>
2
3 <%def name="title()">
4 ${_('User group')}: ${c.user_group.users_group_name}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9
10 <%def name="breadcrumbs_links()">
11 ${_('User group')}: ${c.user_group.users_group_name}
12 </%def>
13
14 <%def name="menu_bar_nav()">
15 ${self.menu_items(active='my_account')}
16 </%def>
17
18 <%def name="main()">
19 <div class="box">
20 <div class="title">
21 ${self.breadcrumbs()}
22 </div>
23
24 <div class="sidebar-col-wrapper scw-small">
25 ##main
26 <div class="sidebar">
27 <ul class="nav nav-pills nav-stacked">
28 <li class="${'active' if c.active=='profile' else ''}">
29 <a href="${h.route_path('user_group_profile', user_group_name=c.user_group.users_group_name)}">${_('User Group Profile')}</a></li>
30 ## These placeholders are here only for styling purposes. For every new item added to the list, you should remove one placeholder
31 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
32 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
33 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
34 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
35 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
36 <li class="placeholder"><a href="#" style="visibility: hidden;">placeholder</a></li>
37 </ul>
38 </div>
39
40 <div class="main-content-full-width">
41 <%include file="/user_group/${c.active}.mako"/>
42 </div>
43 </div>
44 </div>
45
46 </%def>
@@ -1,437 +1,438 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 logging
21 import logging
22 import traceback
22 import traceback
23 import collections
23 import collections
24
24
25 from paste.gzipper import make_gzip_middleware
25 from paste.gzipper import make_gzip_middleware
26 from pyramid.wsgi import wsgiapp
26 from pyramid.wsgi import wsgiapp
27 from pyramid.authorization import ACLAuthorizationPolicy
27 from pyramid.authorization import ACLAuthorizationPolicy
28 from pyramid.config import Configurator
28 from pyramid.config import Configurator
29 from pyramid.settings import asbool, aslist
29 from pyramid.settings import asbool, aslist
30 from pyramid.httpexceptions import (
30 from pyramid.httpexceptions import (
31 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
31 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
32 from pyramid.events import ApplicationCreated
32 from pyramid.events import ApplicationCreated
33 from pyramid.renderers import render_to_response
33 from pyramid.renderers import render_to_response
34
34
35 from rhodecode.model import meta
35 from rhodecode.model import meta
36 from rhodecode.config import patches
36 from rhodecode.config import patches
37 from rhodecode.config import utils as config_utils
37 from rhodecode.config import utils as config_utils
38 from rhodecode.config.environment import load_pyramid_environment
38 from rhodecode.config.environment import load_pyramid_environment
39
39
40 from rhodecode.lib.middleware.vcs import VCSMiddleware
40 from rhodecode.lib.middleware.vcs import VCSMiddleware
41 from rhodecode.lib.vcs import VCSCommunicationError
41 from rhodecode.lib.vcs import VCSCommunicationError
42 from rhodecode.lib.exceptions import VCSServerUnavailable
42 from rhodecode.lib.exceptions import VCSServerUnavailable
43 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
43 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
44 from rhodecode.lib.middleware.https_fixup import HttpsFixup
44 from rhodecode.lib.middleware.https_fixup import HttpsFixup
45 from rhodecode.lib.celerylib.loader import configure_celery
45 from rhodecode.lib.celerylib.loader import configure_celery
46 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
46 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
47 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
47 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
48 from rhodecode.subscribers import (
48 from rhodecode.subscribers import (
49 scan_repositories_if_enabled, write_js_routes_if_enabled,
49 scan_repositories_if_enabled, write_js_routes_if_enabled,
50 write_metadata_if_needed, inject_app_settings)
50 write_metadata_if_needed, inject_app_settings)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 def is_http_error(response):
56 def is_http_error(response):
57 # error which should have traceback
57 # error which should have traceback
58 return response.status_code > 499
58 return response.status_code > 499
59
59
60
60
61 def make_pyramid_app(global_config, **settings):
61 def make_pyramid_app(global_config, **settings):
62 """
62 """
63 Constructs the WSGI application based on Pyramid.
63 Constructs the WSGI application based on Pyramid.
64
64
65 Specials:
65 Specials:
66
66
67 * The application can also be integrated like a plugin via the call to
67 * The application can also be integrated like a plugin via the call to
68 `includeme`. This is accompanied with the other utility functions which
68 `includeme`. This is accompanied with the other utility functions which
69 are called. Changing this should be done with great care to not break
69 are called. Changing this should be done with great care to not break
70 cases when these fragments are assembled from another place.
70 cases when these fragments are assembled from another place.
71
71
72 """
72 """
73 sanitize_settings_and_apply_defaults(settings)
73 sanitize_settings_and_apply_defaults(settings)
74
74
75 config = Configurator(settings=settings)
75 config = Configurator(settings=settings)
76
76
77 # Apply compatibility patches
77 # Apply compatibility patches
78 patches.inspect_getargspec()
78 patches.inspect_getargspec()
79
79
80 load_pyramid_environment(global_config, settings)
80 load_pyramid_environment(global_config, settings)
81
81
82 # Static file view comes first
82 # Static file view comes first
83 includeme_first(config)
83 includeme_first(config)
84
84
85 includeme(config)
85 includeme(config)
86
86
87 pyramid_app = config.make_wsgi_app()
87 pyramid_app = config.make_wsgi_app()
88 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
88 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
89 pyramid_app.config = config
89 pyramid_app.config = config
90
90
91 config.configure_celery(global_config['__file__'])
91 config.configure_celery(global_config['__file__'])
92 # creating the app uses a connection - return it after we are done
92 # creating the app uses a connection - return it after we are done
93 meta.Session.remove()
93 meta.Session.remove()
94
94
95 log.info('Pyramid app %s created and configured.', pyramid_app)
95 log.info('Pyramid app %s created and configured.', pyramid_app)
96 return pyramid_app
96 return pyramid_app
97
97
98
98
99 def not_found_view(request):
99 def not_found_view(request):
100 """
100 """
101 This creates the view which should be registered as not-found-view to
101 This creates the view which should be registered as not-found-view to
102 pyramid.
102 pyramid.
103 """
103 """
104
104
105 if not getattr(request, 'vcs_call', None):
105 if not getattr(request, 'vcs_call', None):
106 # handle like regular case with our error_handler
106 # handle like regular case with our error_handler
107 return error_handler(HTTPNotFound(), request)
107 return error_handler(HTTPNotFound(), request)
108
108
109 # handle not found view as a vcs call
109 # handle not found view as a vcs call
110 settings = request.registry.settings
110 settings = request.registry.settings
111 ae_client = getattr(request, 'ae_client', None)
111 ae_client = getattr(request, 'ae_client', None)
112 vcs_app = VCSMiddleware(
112 vcs_app = VCSMiddleware(
113 HTTPNotFound(), request.registry, settings,
113 HTTPNotFound(), request.registry, settings,
114 appenlight_client=ae_client)
114 appenlight_client=ae_client)
115
115
116 return wsgiapp(vcs_app)(None, request)
116 return wsgiapp(vcs_app)(None, request)
117
117
118
118
119 def error_handler(exception, request):
119 def error_handler(exception, request):
120 import rhodecode
120 import rhodecode
121 from rhodecode.lib import helpers
121 from rhodecode.lib import helpers
122
122
123 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
123 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
124
124
125 base_response = HTTPInternalServerError()
125 base_response = HTTPInternalServerError()
126 # prefer original exception for the response since it may have headers set
126 # prefer original exception for the response since it may have headers set
127 if isinstance(exception, HTTPException):
127 if isinstance(exception, HTTPException):
128 base_response = exception
128 base_response = exception
129 elif isinstance(exception, VCSCommunicationError):
129 elif isinstance(exception, VCSCommunicationError):
130 base_response = VCSServerUnavailable()
130 base_response = VCSServerUnavailable()
131
131
132 if is_http_error(base_response):
132 if is_http_error(base_response):
133 log.exception(
133 log.exception(
134 'error occurred handling this request for path: %s', request.path)
134 'error occurred handling this request for path: %s', request.path)
135
135
136 error_explanation = base_response.explanation or str(base_response)
136 error_explanation = base_response.explanation or str(base_response)
137 if base_response.status_code == 404:
137 if base_response.status_code == 404:
138 error_explanation += " Or you don't have permission to access it."
138 error_explanation += " Or you don't have permission to access it."
139 c = AttributeDict()
139 c = AttributeDict()
140 c.error_message = base_response.status
140 c.error_message = base_response.status
141 c.error_explanation = error_explanation
141 c.error_explanation = error_explanation
142 c.visual = AttributeDict()
142 c.visual = AttributeDict()
143
143
144 c.visual.rhodecode_support_url = (
144 c.visual.rhodecode_support_url = (
145 request.registry.settings.get('rhodecode_support_url') or
145 request.registry.settings.get('rhodecode_support_url') or
146 request.route_url('rhodecode_support')
146 request.route_url('rhodecode_support')
147 )
147 )
148 c.redirect_time = 0
148 c.redirect_time = 0
149 c.rhodecode_name = rhodecode_title
149 c.rhodecode_name = rhodecode_title
150 if not c.rhodecode_name:
150 if not c.rhodecode_name:
151 c.rhodecode_name = 'Rhodecode'
151 c.rhodecode_name = 'Rhodecode'
152
152
153 c.causes = []
153 c.causes = []
154 if is_http_error(base_response):
154 if is_http_error(base_response):
155 c.causes.append('Server is overloaded.')
155 c.causes.append('Server is overloaded.')
156 c.causes.append('Server database connection is lost.')
156 c.causes.append('Server database connection is lost.')
157 c.causes.append('Server expected unhandled error.')
157 c.causes.append('Server expected unhandled error.')
158
158
159 if hasattr(base_response, 'causes'):
159 if hasattr(base_response, 'causes'):
160 c.causes = base_response.causes
160 c.causes = base_response.causes
161
161
162 c.messages = helpers.flash.pop_messages(request=request)
162 c.messages = helpers.flash.pop_messages(request=request)
163 c.traceback = traceback.format_exc()
163 c.traceback = traceback.format_exc()
164 response = render_to_response(
164 response = render_to_response(
165 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
165 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
166 response=base_response)
166 response=base_response)
167
167
168 return response
168 return response
169
169
170
170
171 def includeme_first(config):
171 def includeme_first(config):
172 # redirect automatic browser favicon.ico requests to correct place
172 # redirect automatic browser favicon.ico requests to correct place
173 def favicon_redirect(context, request):
173 def favicon_redirect(context, request):
174 return HTTPFound(
174 return HTTPFound(
175 request.static_path('rhodecode:public/images/favicon.ico'))
175 request.static_path('rhodecode:public/images/favicon.ico'))
176
176
177 config.add_view(favicon_redirect, route_name='favicon')
177 config.add_view(favicon_redirect, route_name='favicon')
178 config.add_route('favicon', '/favicon.ico')
178 config.add_route('favicon', '/favicon.ico')
179
179
180 def robots_redirect(context, request):
180 def robots_redirect(context, request):
181 return HTTPFound(
181 return HTTPFound(
182 request.static_path('rhodecode:public/robots.txt'))
182 request.static_path('rhodecode:public/robots.txt'))
183
183
184 config.add_view(robots_redirect, route_name='robots')
184 config.add_view(robots_redirect, route_name='robots')
185 config.add_route('robots', '/robots.txt')
185 config.add_route('robots', '/robots.txt')
186
186
187 config.add_static_view(
187 config.add_static_view(
188 '_static/deform', 'deform:static')
188 '_static/deform', 'deform:static')
189 config.add_static_view(
189 config.add_static_view(
190 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
190 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
191
191
192
192
193 def includeme(config):
193 def includeme(config):
194 settings = config.registry.settings
194 settings = config.registry.settings
195
195
196 # plugin information
196 # plugin information
197 config.registry.rhodecode_plugins = collections.OrderedDict()
197 config.registry.rhodecode_plugins = collections.OrderedDict()
198
198
199 config.add_directive(
199 config.add_directive(
200 'register_rhodecode_plugin', register_rhodecode_plugin)
200 'register_rhodecode_plugin', register_rhodecode_plugin)
201
201
202 config.add_directive('configure_celery', configure_celery)
202 config.add_directive('configure_celery', configure_celery)
203
203
204 if asbool(settings.get('appenlight', 'false')):
204 if asbool(settings.get('appenlight', 'false')):
205 config.include('appenlight_client.ext.pyramid_tween')
205 config.include('appenlight_client.ext.pyramid_tween')
206
206
207 # Includes which are required. The application would fail without them.
207 # Includes which are required. The application would fail without them.
208 config.include('pyramid_mako')
208 config.include('pyramid_mako')
209 config.include('pyramid_beaker')
209 config.include('pyramid_beaker')
210 config.include('rhodecode.lib.caches')
210 config.include('rhodecode.lib.caches')
211
211
212 config.include('rhodecode.authentication')
212 config.include('rhodecode.authentication')
213 config.include('rhodecode.integrations')
213 config.include('rhodecode.integrations')
214
214
215 # apps
215 # apps
216 config.include('rhodecode.apps._base')
216 config.include('rhodecode.apps._base')
217 config.include('rhodecode.apps.ops')
217 config.include('rhodecode.apps.ops')
218
218
219 config.include('rhodecode.apps.admin')
219 config.include('rhodecode.apps.admin')
220 config.include('rhodecode.apps.channelstream')
220 config.include('rhodecode.apps.channelstream')
221 config.include('rhodecode.apps.login')
221 config.include('rhodecode.apps.login')
222 config.include('rhodecode.apps.home')
222 config.include('rhodecode.apps.home')
223 config.include('rhodecode.apps.journal')
223 config.include('rhodecode.apps.journal')
224 config.include('rhodecode.apps.repository')
224 config.include('rhodecode.apps.repository')
225 config.include('rhodecode.apps.repo_group')
225 config.include('rhodecode.apps.repo_group')
226 config.include('rhodecode.apps.user_group')
226 config.include('rhodecode.apps.user_group')
227 config.include('rhodecode.apps.search')
227 config.include('rhodecode.apps.search')
228 config.include('rhodecode.apps.user_profile')
228 config.include('rhodecode.apps.user_profile')
229 config.include('rhodecode.apps.user_group_profile')
229 config.include('rhodecode.apps.my_account')
230 config.include('rhodecode.apps.my_account')
230 config.include('rhodecode.apps.svn_support')
231 config.include('rhodecode.apps.svn_support')
231 config.include('rhodecode.apps.ssh_support')
232 config.include('rhodecode.apps.ssh_support')
232 config.include('rhodecode.apps.gist')
233 config.include('rhodecode.apps.gist')
233
234
234 config.include('rhodecode.apps.debug_style')
235 config.include('rhodecode.apps.debug_style')
235 config.include('rhodecode.tweens')
236 config.include('rhodecode.tweens')
236 config.include('rhodecode.api')
237 config.include('rhodecode.api')
237
238
238 config.add_route(
239 config.add_route(
239 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
240 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
240
241
241 config.add_translation_dirs('rhodecode:i18n/')
242 config.add_translation_dirs('rhodecode:i18n/')
242 settings['default_locale_name'] = settings.get('lang', 'en')
243 settings['default_locale_name'] = settings.get('lang', 'en')
243
244
244 # Add subscribers.
245 # Add subscribers.
245 config.add_subscriber(inject_app_settings, ApplicationCreated)
246 config.add_subscriber(inject_app_settings, ApplicationCreated)
246 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
247 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
247 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
248 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
248 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
249 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
249
250
250 # events
251 # events
251 # TODO(marcink): this should be done when pyramid migration is finished
252 # TODO(marcink): this should be done when pyramid migration is finished
252 # config.add_subscriber(
253 # config.add_subscriber(
253 # 'rhodecode.integrations.integrations_event_handler',
254 # 'rhodecode.integrations.integrations_event_handler',
254 # 'rhodecode.events.RhodecodeEvent')
255 # 'rhodecode.events.RhodecodeEvent')
255
256
256 # request custom methods
257 # request custom methods
257 config.add_request_method(
258 config.add_request_method(
258 'rhodecode.lib.partial_renderer.get_partial_renderer',
259 'rhodecode.lib.partial_renderer.get_partial_renderer',
259 'get_partial_renderer')
260 'get_partial_renderer')
260
261
261 # Set the authorization policy.
262 # Set the authorization policy.
262 authz_policy = ACLAuthorizationPolicy()
263 authz_policy = ACLAuthorizationPolicy()
263 config.set_authorization_policy(authz_policy)
264 config.set_authorization_policy(authz_policy)
264
265
265 # Set the default renderer for HTML templates to mako.
266 # Set the default renderer for HTML templates to mako.
266 config.add_mako_renderer('.html')
267 config.add_mako_renderer('.html')
267
268
268 config.add_renderer(
269 config.add_renderer(
269 name='json_ext',
270 name='json_ext',
270 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
271 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
271
272
272 # include RhodeCode plugins
273 # include RhodeCode plugins
273 includes = aslist(settings.get('rhodecode.includes', []))
274 includes = aslist(settings.get('rhodecode.includes', []))
274 for inc in includes:
275 for inc in includes:
275 config.include(inc)
276 config.include(inc)
276
277
277 # custom not found view, if our pyramid app doesn't know how to handle
278 # custom not found view, if our pyramid app doesn't know how to handle
278 # the request pass it to potential VCS handling ap
279 # the request pass it to potential VCS handling ap
279 config.add_notfound_view(not_found_view)
280 config.add_notfound_view(not_found_view)
280 if not settings.get('debugtoolbar.enabled', False):
281 if not settings.get('debugtoolbar.enabled', False):
281 # disabled debugtoolbar handle all exceptions via the error_handlers
282 # disabled debugtoolbar handle all exceptions via the error_handlers
282 config.add_view(error_handler, context=Exception)
283 config.add_view(error_handler, context=Exception)
283
284
284 # all errors including 403/404/50X
285 # all errors including 403/404/50X
285 config.add_view(error_handler, context=HTTPError)
286 config.add_view(error_handler, context=HTTPError)
286
287
287
288
288 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
289 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
289 """
290 """
290 Apply outer WSGI middlewares around the application.
291 Apply outer WSGI middlewares around the application.
291 """
292 """
292 settings = config.registry.settings
293 settings = config.registry.settings
293
294
294 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
295 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
295 pyramid_app = HttpsFixup(pyramid_app, settings)
296 pyramid_app = HttpsFixup(pyramid_app, settings)
296
297
297 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
298 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
298 pyramid_app, settings)
299 pyramid_app, settings)
299 config.registry.ae_client = _ae_client
300 config.registry.ae_client = _ae_client
300
301
301 if settings['gzip_responses']:
302 if settings['gzip_responses']:
302 pyramid_app = make_gzip_middleware(
303 pyramid_app = make_gzip_middleware(
303 pyramid_app, settings, compress_level=1)
304 pyramid_app, settings, compress_level=1)
304
305
305 # this should be the outer most middleware in the wsgi stack since
306 # this should be the outer most middleware in the wsgi stack since
306 # middleware like Routes make database calls
307 # middleware like Routes make database calls
307 def pyramid_app_with_cleanup(environ, start_response):
308 def pyramid_app_with_cleanup(environ, start_response):
308 try:
309 try:
309 return pyramid_app(environ, start_response)
310 return pyramid_app(environ, start_response)
310 finally:
311 finally:
311 # Dispose current database session and rollback uncommitted
312 # Dispose current database session and rollback uncommitted
312 # transactions.
313 # transactions.
313 meta.Session.remove()
314 meta.Session.remove()
314
315
315 # In a single threaded mode server, on non sqlite db we should have
316 # In a single threaded mode server, on non sqlite db we should have
316 # '0 Current Checked out connections' at the end of a request,
317 # '0 Current Checked out connections' at the end of a request,
317 # if not, then something, somewhere is leaving a connection open
318 # if not, then something, somewhere is leaving a connection open
318 pool = meta.Base.metadata.bind.engine.pool
319 pool = meta.Base.metadata.bind.engine.pool
319 log.debug('sa pool status: %s', pool.status())
320 log.debug('sa pool status: %s', pool.status())
320
321
321 return pyramid_app_with_cleanup
322 return pyramid_app_with_cleanup
322
323
323
324
324 def sanitize_settings_and_apply_defaults(settings):
325 def sanitize_settings_and_apply_defaults(settings):
325 """
326 """
326 Applies settings defaults and does all type conversion.
327 Applies settings defaults and does all type conversion.
327
328
328 We would move all settings parsing and preparation into this place, so that
329 We would move all settings parsing and preparation into this place, so that
329 we have only one place left which deals with this part. The remaining parts
330 we have only one place left which deals with this part. The remaining parts
330 of the application would start to rely fully on well prepared settings.
331 of the application would start to rely fully on well prepared settings.
331
332
332 This piece would later be split up per topic to avoid a big fat monster
333 This piece would later be split up per topic to avoid a big fat monster
333 function.
334 function.
334 """
335 """
335
336
336 settings.setdefault('rhodecode.edition', 'Community Edition')
337 settings.setdefault('rhodecode.edition', 'Community Edition')
337
338
338 if 'mako.default_filters' not in settings:
339 if 'mako.default_filters' not in settings:
339 # set custom default filters if we don't have it defined
340 # set custom default filters if we don't have it defined
340 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
341 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
341 settings['mako.default_filters'] = 'h_filter'
342 settings['mako.default_filters'] = 'h_filter'
342
343
343 if 'mako.directories' not in settings:
344 if 'mako.directories' not in settings:
344 mako_directories = settings.setdefault('mako.directories', [
345 mako_directories = settings.setdefault('mako.directories', [
345 # Base templates of the original application
346 # Base templates of the original application
346 'rhodecode:templates',
347 'rhodecode:templates',
347 ])
348 ])
348 log.debug(
349 log.debug(
349 "Using the following Mako template directories: %s",
350 "Using the following Mako template directories: %s",
350 mako_directories)
351 mako_directories)
351
352
352 # Default includes, possible to change as a user
353 # Default includes, possible to change as a user
353 pyramid_includes = settings.setdefault('pyramid.includes', [
354 pyramid_includes = settings.setdefault('pyramid.includes', [
354 'rhodecode.lib.middleware.request_wrapper',
355 'rhodecode.lib.middleware.request_wrapper',
355 ])
356 ])
356 log.debug(
357 log.debug(
357 "Using the following pyramid.includes: %s",
358 "Using the following pyramid.includes: %s",
358 pyramid_includes)
359 pyramid_includes)
359
360
360 # TODO: johbo: Re-think this, usually the call to config.include
361 # TODO: johbo: Re-think this, usually the call to config.include
361 # should allow to pass in a prefix.
362 # should allow to pass in a prefix.
362 settings.setdefault('rhodecode.api.url', '/_admin/api')
363 settings.setdefault('rhodecode.api.url', '/_admin/api')
363
364
364 # Sanitize generic settings.
365 # Sanitize generic settings.
365 _list_setting(settings, 'default_encoding', 'UTF-8')
366 _list_setting(settings, 'default_encoding', 'UTF-8')
366 _bool_setting(settings, 'is_test', 'false')
367 _bool_setting(settings, 'is_test', 'false')
367 _bool_setting(settings, 'gzip_responses', 'false')
368 _bool_setting(settings, 'gzip_responses', 'false')
368
369
369 # Call split out functions that sanitize settings for each topic.
370 # Call split out functions that sanitize settings for each topic.
370 _sanitize_appenlight_settings(settings)
371 _sanitize_appenlight_settings(settings)
371 _sanitize_vcs_settings(settings)
372 _sanitize_vcs_settings(settings)
372
373
373 # configure instance id
374 # configure instance id
374 config_utils.set_instance_id(settings)
375 config_utils.set_instance_id(settings)
375
376
376 return settings
377 return settings
377
378
378
379
379 def _sanitize_appenlight_settings(settings):
380 def _sanitize_appenlight_settings(settings):
380 _bool_setting(settings, 'appenlight', 'false')
381 _bool_setting(settings, 'appenlight', 'false')
381
382
382
383
383 def _sanitize_vcs_settings(settings):
384 def _sanitize_vcs_settings(settings):
384 """
385 """
385 Applies settings defaults and does type conversion for all VCS related
386 Applies settings defaults and does type conversion for all VCS related
386 settings.
387 settings.
387 """
388 """
388 _string_setting(settings, 'vcs.svn.compatible_version', '')
389 _string_setting(settings, 'vcs.svn.compatible_version', '')
389 _string_setting(settings, 'git_rev_filter', '--all')
390 _string_setting(settings, 'git_rev_filter', '--all')
390 _string_setting(settings, 'vcs.hooks.protocol', 'http')
391 _string_setting(settings, 'vcs.hooks.protocol', 'http')
391 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
392 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
392 _string_setting(settings, 'vcs.server', '')
393 _string_setting(settings, 'vcs.server', '')
393 _string_setting(settings, 'vcs.server.log_level', 'debug')
394 _string_setting(settings, 'vcs.server.log_level', 'debug')
394 _string_setting(settings, 'vcs.server.protocol', 'http')
395 _string_setting(settings, 'vcs.server.protocol', 'http')
395 _bool_setting(settings, 'startup.import_repos', 'false')
396 _bool_setting(settings, 'startup.import_repos', 'false')
396 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
397 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
397 _bool_setting(settings, 'vcs.server.enable', 'true')
398 _bool_setting(settings, 'vcs.server.enable', 'true')
398 _bool_setting(settings, 'vcs.start_server', 'false')
399 _bool_setting(settings, 'vcs.start_server', 'false')
399 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
400 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
400 _int_setting(settings, 'vcs.connection_timeout', 3600)
401 _int_setting(settings, 'vcs.connection_timeout', 3600)
401
402
402 # Support legacy values of vcs.scm_app_implementation. Legacy
403 # Support legacy values of vcs.scm_app_implementation. Legacy
403 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
404 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
404 # which is now mapped to 'http'.
405 # which is now mapped to 'http'.
405 scm_app_impl = settings['vcs.scm_app_implementation']
406 scm_app_impl = settings['vcs.scm_app_implementation']
406 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
407 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
407 settings['vcs.scm_app_implementation'] = 'http'
408 settings['vcs.scm_app_implementation'] = 'http'
408
409
409
410
410 def _int_setting(settings, name, default):
411 def _int_setting(settings, name, default):
411 settings[name] = int(settings.get(name, default))
412 settings[name] = int(settings.get(name, default))
412
413
413
414
414 def _bool_setting(settings, name, default):
415 def _bool_setting(settings, name, default):
415 input_val = settings.get(name, default)
416 input_val = settings.get(name, default)
416 if isinstance(input_val, unicode):
417 if isinstance(input_val, unicode):
417 input_val = input_val.encode('utf8')
418 input_val = input_val.encode('utf8')
418 settings[name] = asbool(input_val)
419 settings[name] = asbool(input_val)
419
420
420
421
421 def _list_setting(settings, name, default):
422 def _list_setting(settings, name, default):
422 raw_value = settings.get(name, default)
423 raw_value = settings.get(name, default)
423
424
424 old_separator = ','
425 old_separator = ','
425 if old_separator in raw_value:
426 if old_separator in raw_value:
426 # If we get a comma separated list, pass it to our own function.
427 # If we get a comma separated list, pass it to our own function.
427 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
428 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
428 else:
429 else:
429 # Otherwise we assume it uses pyramids space/newline separation.
430 # Otherwise we assume it uses pyramids space/newline separation.
430 settings[name] = aslist(raw_value)
431 settings[name] = aslist(raw_value)
431
432
432
433
433 def _string_setting(settings, name, default, lower=True):
434 def _string_setting(settings, name, default, lower=True):
434 value = settings.get(name, default)
435 value = settings.get(name, default)
435 if lower:
436 if lower:
436 value = value.lower()
437 value = value.lower()
437 settings[name] = value
438 settings[name] = value
@@ -1,2077 +1,2084 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 from collections import OrderedDict
39 from collections import OrderedDict
40
40
41 import pygments
41 import pygments
42 import itertools
42 import itertools
43 import fnmatch
43 import fnmatch
44
44
45 from datetime import datetime
45 from datetime import datetime
46 from functools import partial
46 from functools import partial
47 from pygments.formatters.html import HtmlFormatter
47 from pygments.formatters.html import HtmlFormatter
48 from pygments import highlight as code_highlight
48 from pygments import highlight as code_highlight
49 from pygments.lexers import (
49 from pygments.lexers import (
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51
51
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53
53
54 from webhelpers.html import literal, HTML, escape
54 from webhelpers.html import literal, HTML, escape
55 from webhelpers.html.tools import *
55 from webhelpers.html.tools import *
56 from webhelpers.html.builder import make_tag
56 from webhelpers.html.builder import make_tag
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 submit, text, password, textarea, title, ul, xml_declaration, radio
60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 replace_whitespace, urlify, truncate, wrap_paragraphs
65 replace_whitespace, urlify, truncate, wrap_paragraphs
66 from webhelpers.date import time_ago_in_words
66 from webhelpers.date import time_ago_in_words
67 from webhelpers.paginate import Page as _Page
67 from webhelpers.paginate import Page as _Page
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 from webhelpers2.number import format_byte_size
70 from webhelpers2.number import format_byte_size
71
71
72 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.action_parser import action_parser
73 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.ext_json import json
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 AttributeDict, safe_int, md5, md5_safe
77 AttributeDict, safe_int, md5, md5_safe
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.changeset_status import ChangesetStatusModel
83 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.db import Permission, User, Repository
84 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.repo_group import RepoGroupModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
86
86
87 log = logging.getLogger(__name__)
87 log = logging.getLogger(__name__)
88
88
89
89
90 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER = User.DEFAULT_USER
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92
92
93
93
94 def asset(path, ver=None, **kwargs):
94 def asset(path, ver=None, **kwargs):
95 """
95 """
96 Helper to generate a static asset file path for rhodecode assets
96 Helper to generate a static asset file path for rhodecode assets
97
97
98 eg. h.asset('images/image.png', ver='3923')
98 eg. h.asset('images/image.png', ver='3923')
99
99
100 :param path: path of asset
100 :param path: path of asset
101 :param ver: optional version query param to append as ?ver=
101 :param ver: optional version query param to append as ?ver=
102 """
102 """
103 request = get_current_request()
103 request = get_current_request()
104 query = {}
104 query = {}
105 query.update(kwargs)
105 query.update(kwargs)
106 if ver:
106 if ver:
107 query = {'ver': ver}
107 query = {'ver': ver}
108 return request.static_path(
108 return request.static_path(
109 'rhodecode:public/{}'.format(path), _query=query)
109 'rhodecode:public/{}'.format(path), _query=query)
110
110
111
111
112 default_html_escape_table = {
112 default_html_escape_table = {
113 ord('&'): u'&amp;',
113 ord('&'): u'&amp;',
114 ord('<'): u'&lt;',
114 ord('<'): u'&lt;',
115 ord('>'): u'&gt;',
115 ord('>'): u'&gt;',
116 ord('"'): u'&quot;',
116 ord('"'): u'&quot;',
117 ord("'"): u'&#39;',
117 ord("'"): u'&#39;',
118 }
118 }
119
119
120
120
121 def html_escape(text, html_escape_table=default_html_escape_table):
121 def html_escape(text, html_escape_table=default_html_escape_table):
122 """Produce entities within text."""
122 """Produce entities within text."""
123 return text.translate(html_escape_table)
123 return text.translate(html_escape_table)
124
124
125
125
126 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
126 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
127 """
127 """
128 Truncate string ``s`` at the first occurrence of ``sub``.
128 Truncate string ``s`` at the first occurrence of ``sub``.
129
129
130 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
130 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
131 """
131 """
132 suffix_if_chopped = suffix_if_chopped or ''
132 suffix_if_chopped = suffix_if_chopped or ''
133 pos = s.find(sub)
133 pos = s.find(sub)
134 if pos == -1:
134 if pos == -1:
135 return s
135 return s
136
136
137 if inclusive:
137 if inclusive:
138 pos += len(sub)
138 pos += len(sub)
139
139
140 chopped = s[:pos]
140 chopped = s[:pos]
141 left = s[pos:].strip()
141 left = s[pos:].strip()
142
142
143 if left and suffix_if_chopped:
143 if left and suffix_if_chopped:
144 chopped += suffix_if_chopped
144 chopped += suffix_if_chopped
145
145
146 return chopped
146 return chopped
147
147
148
148
149 def shorter(text, size=20):
149 def shorter(text, size=20):
150 postfix = '...'
150 postfix = '...'
151 if len(text) > size:
151 if len(text) > size:
152 return text[:size - len(postfix)] + postfix
152 return text[:size - len(postfix)] + postfix
153 return text
153 return text
154
154
155
155
156 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
156 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
157 """
157 """
158 Reset button
158 Reset button
159 """
159 """
160 _set_input_attrs(attrs, type, name, value)
160 _set_input_attrs(attrs, type, name, value)
161 _set_id_attr(attrs, id, name)
161 _set_id_attr(attrs, id, name)
162 convert_boolean_attrs(attrs, ["disabled"])
162 convert_boolean_attrs(attrs, ["disabled"])
163 return HTML.input(**attrs)
163 return HTML.input(**attrs)
164
164
165 reset = _reset
165 reset = _reset
166 safeid = _make_safe_id_component
166 safeid = _make_safe_id_component
167
167
168
168
169 def branding(name, length=40):
169 def branding(name, length=40):
170 return truncate(name, length, indicator="")
170 return truncate(name, length, indicator="")
171
171
172
172
173 def FID(raw_id, path):
173 def FID(raw_id, path):
174 """
174 """
175 Creates a unique ID for filenode based on it's hash of path and commit
175 Creates a unique ID for filenode based on it's hash of path and commit
176 it's safe to use in urls
176 it's safe to use in urls
177
177
178 :param raw_id:
178 :param raw_id:
179 :param path:
179 :param path:
180 """
180 """
181
181
182 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
182 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
183
183
184
184
185 class _GetError(object):
185 class _GetError(object):
186 """Get error from form_errors, and represent it as span wrapped error
186 """Get error from form_errors, and represent it as span wrapped error
187 message
187 message
188
188
189 :param field_name: field to fetch errors for
189 :param field_name: field to fetch errors for
190 :param form_errors: form errors dict
190 :param form_errors: form errors dict
191 """
191 """
192
192
193 def __call__(self, field_name, form_errors):
193 def __call__(self, field_name, form_errors):
194 tmpl = """<span class="error_msg">%s</span>"""
194 tmpl = """<span class="error_msg">%s</span>"""
195 if form_errors and field_name in form_errors:
195 if form_errors and field_name in form_errors:
196 return literal(tmpl % form_errors.get(field_name))
196 return literal(tmpl % form_errors.get(field_name))
197
197
198 get_error = _GetError()
198 get_error = _GetError()
199
199
200
200
201 class _ToolTip(object):
201 class _ToolTip(object):
202
202
203 def __call__(self, tooltip_title, trim_at=50):
203 def __call__(self, tooltip_title, trim_at=50):
204 """
204 """
205 Special function just to wrap our text into nice formatted
205 Special function just to wrap our text into nice formatted
206 autowrapped text
206 autowrapped text
207
207
208 :param tooltip_title:
208 :param tooltip_title:
209 """
209 """
210 tooltip_title = escape(tooltip_title)
210 tooltip_title = escape(tooltip_title)
211 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
211 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
212 return tooltip_title
212 return tooltip_title
213 tooltip = _ToolTip()
213 tooltip = _ToolTip()
214
214
215
215
216 def files_breadcrumbs(repo_name, commit_id, file_path):
216 def files_breadcrumbs(repo_name, commit_id, file_path):
217 if isinstance(file_path, str):
217 if isinstance(file_path, str):
218 file_path = safe_unicode(file_path)
218 file_path = safe_unicode(file_path)
219
219
220 # TODO: johbo: Is this always a url like path, or is this operating
220 # TODO: johbo: Is this always a url like path, or is this operating
221 # system dependent?
221 # system dependent?
222 path_segments = file_path.split('/')
222 path_segments = file_path.split('/')
223
223
224 repo_name_html = escape(repo_name)
224 repo_name_html = escape(repo_name)
225 if len(path_segments) == 1 and path_segments[0] == '':
225 if len(path_segments) == 1 and path_segments[0] == '':
226 url_segments = [repo_name_html]
226 url_segments = [repo_name_html]
227 else:
227 else:
228 url_segments = [
228 url_segments = [
229 link_to(
229 link_to(
230 repo_name_html,
230 repo_name_html,
231 route_path(
231 route_path(
232 'repo_files',
232 'repo_files',
233 repo_name=repo_name,
233 repo_name=repo_name,
234 commit_id=commit_id,
234 commit_id=commit_id,
235 f_path=''),
235 f_path=''),
236 class_='pjax-link')]
236 class_='pjax-link')]
237
237
238 last_cnt = len(path_segments) - 1
238 last_cnt = len(path_segments) - 1
239 for cnt, segment in enumerate(path_segments):
239 for cnt, segment in enumerate(path_segments):
240 if not segment:
240 if not segment:
241 continue
241 continue
242 segment_html = escape(segment)
242 segment_html = escape(segment)
243
243
244 if cnt != last_cnt:
244 if cnt != last_cnt:
245 url_segments.append(
245 url_segments.append(
246 link_to(
246 link_to(
247 segment_html,
247 segment_html,
248 route_path(
248 route_path(
249 'repo_files',
249 'repo_files',
250 repo_name=repo_name,
250 repo_name=repo_name,
251 commit_id=commit_id,
251 commit_id=commit_id,
252 f_path='/'.join(path_segments[:cnt + 1])),
252 f_path='/'.join(path_segments[:cnt + 1])),
253 class_='pjax-link'))
253 class_='pjax-link'))
254 else:
254 else:
255 url_segments.append(segment_html)
255 url_segments.append(segment_html)
256
256
257 return literal('/'.join(url_segments))
257 return literal('/'.join(url_segments))
258
258
259
259
260 class CodeHtmlFormatter(HtmlFormatter):
260 class CodeHtmlFormatter(HtmlFormatter):
261 """
261 """
262 My code Html Formatter for source codes
262 My code Html Formatter for source codes
263 """
263 """
264
264
265 def wrap(self, source, outfile):
265 def wrap(self, source, outfile):
266 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
266 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
267
267
268 def _wrap_code(self, source):
268 def _wrap_code(self, source):
269 for cnt, it in enumerate(source):
269 for cnt, it in enumerate(source):
270 i, t = it
270 i, t = it
271 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
271 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
272 yield i, t
272 yield i, t
273
273
274 def _wrap_tablelinenos(self, inner):
274 def _wrap_tablelinenos(self, inner):
275 dummyoutfile = StringIO.StringIO()
275 dummyoutfile = StringIO.StringIO()
276 lncount = 0
276 lncount = 0
277 for t, line in inner:
277 for t, line in inner:
278 if t:
278 if t:
279 lncount += 1
279 lncount += 1
280 dummyoutfile.write(line)
280 dummyoutfile.write(line)
281
281
282 fl = self.linenostart
282 fl = self.linenostart
283 mw = len(str(lncount + fl - 1))
283 mw = len(str(lncount + fl - 1))
284 sp = self.linenospecial
284 sp = self.linenospecial
285 st = self.linenostep
285 st = self.linenostep
286 la = self.lineanchors
286 la = self.lineanchors
287 aln = self.anchorlinenos
287 aln = self.anchorlinenos
288 nocls = self.noclasses
288 nocls = self.noclasses
289 if sp:
289 if sp:
290 lines = []
290 lines = []
291
291
292 for i in range(fl, fl + lncount):
292 for i in range(fl, fl + lncount):
293 if i % st == 0:
293 if i % st == 0:
294 if i % sp == 0:
294 if i % sp == 0:
295 if aln:
295 if aln:
296 lines.append('<a href="#%s%d" class="special">%*d</a>' %
296 lines.append('<a href="#%s%d" class="special">%*d</a>' %
297 (la, i, mw, i))
297 (la, i, mw, i))
298 else:
298 else:
299 lines.append('<span class="special">%*d</span>' % (mw, i))
299 lines.append('<span class="special">%*d</span>' % (mw, i))
300 else:
300 else:
301 if aln:
301 if aln:
302 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
302 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
303 else:
303 else:
304 lines.append('%*d' % (mw, i))
304 lines.append('%*d' % (mw, i))
305 else:
305 else:
306 lines.append('')
306 lines.append('')
307 ls = '\n'.join(lines)
307 ls = '\n'.join(lines)
308 else:
308 else:
309 lines = []
309 lines = []
310 for i in range(fl, fl + lncount):
310 for i in range(fl, fl + lncount):
311 if i % st == 0:
311 if i % st == 0:
312 if aln:
312 if aln:
313 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
313 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
314 else:
314 else:
315 lines.append('%*d' % (mw, i))
315 lines.append('%*d' % (mw, i))
316 else:
316 else:
317 lines.append('')
317 lines.append('')
318 ls = '\n'.join(lines)
318 ls = '\n'.join(lines)
319
319
320 # in case you wonder about the seemingly redundant <div> here: since the
320 # in case you wonder about the seemingly redundant <div> here: since the
321 # content in the other cell also is wrapped in a div, some browsers in
321 # content in the other cell also is wrapped in a div, some browsers in
322 # some configurations seem to mess up the formatting...
322 # some configurations seem to mess up the formatting...
323 if nocls:
323 if nocls:
324 yield 0, ('<table class="%stable">' % self.cssclass +
324 yield 0, ('<table class="%stable">' % self.cssclass +
325 '<tr><td><div class="linenodiv" '
325 '<tr><td><div class="linenodiv" '
326 'style="background-color: #f0f0f0; padding-right: 10px">'
326 'style="background-color: #f0f0f0; padding-right: 10px">'
327 '<pre style="line-height: 125%">' +
327 '<pre style="line-height: 125%">' +
328 ls + '</pre></div></td><td id="hlcode" class="code">')
328 ls + '</pre></div></td><td id="hlcode" class="code">')
329 else:
329 else:
330 yield 0, ('<table class="%stable">' % self.cssclass +
330 yield 0, ('<table class="%stable">' % self.cssclass +
331 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
331 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
332 ls + '</pre></div></td><td id="hlcode" class="code">')
332 ls + '</pre></div></td><td id="hlcode" class="code">')
333 yield 0, dummyoutfile.getvalue()
333 yield 0, dummyoutfile.getvalue()
334 yield 0, '</td></tr></table>'
334 yield 0, '</td></tr></table>'
335
335
336
336
337 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
337 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
338 def __init__(self, **kw):
338 def __init__(self, **kw):
339 # only show these line numbers if set
339 # only show these line numbers if set
340 self.only_lines = kw.pop('only_line_numbers', [])
340 self.only_lines = kw.pop('only_line_numbers', [])
341 self.query_terms = kw.pop('query_terms', [])
341 self.query_terms = kw.pop('query_terms', [])
342 self.max_lines = kw.pop('max_lines', 5)
342 self.max_lines = kw.pop('max_lines', 5)
343 self.line_context = kw.pop('line_context', 3)
343 self.line_context = kw.pop('line_context', 3)
344 self.url = kw.pop('url', None)
344 self.url = kw.pop('url', None)
345
345
346 super(CodeHtmlFormatter, self).__init__(**kw)
346 super(CodeHtmlFormatter, self).__init__(**kw)
347
347
348 def _wrap_code(self, source):
348 def _wrap_code(self, source):
349 for cnt, it in enumerate(source):
349 for cnt, it in enumerate(source):
350 i, t = it
350 i, t = it
351 t = '<pre>%s</pre>' % t
351 t = '<pre>%s</pre>' % t
352 yield i, t
352 yield i, t
353
353
354 def _wrap_tablelinenos(self, inner):
354 def _wrap_tablelinenos(self, inner):
355 yield 0, '<table class="code-highlight %stable">' % self.cssclass
355 yield 0, '<table class="code-highlight %stable">' % self.cssclass
356
356
357 last_shown_line_number = 0
357 last_shown_line_number = 0
358 current_line_number = 1
358 current_line_number = 1
359
359
360 for t, line in inner:
360 for t, line in inner:
361 if not t:
361 if not t:
362 yield t, line
362 yield t, line
363 continue
363 continue
364
364
365 if current_line_number in self.only_lines:
365 if current_line_number in self.only_lines:
366 if last_shown_line_number + 1 != current_line_number:
366 if last_shown_line_number + 1 != current_line_number:
367 yield 0, '<tr>'
367 yield 0, '<tr>'
368 yield 0, '<td class="line">...</td>'
368 yield 0, '<td class="line">...</td>'
369 yield 0, '<td id="hlcode" class="code"></td>'
369 yield 0, '<td id="hlcode" class="code"></td>'
370 yield 0, '</tr>'
370 yield 0, '</tr>'
371
371
372 yield 0, '<tr>'
372 yield 0, '<tr>'
373 if self.url:
373 if self.url:
374 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
374 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
375 self.url, current_line_number, current_line_number)
375 self.url, current_line_number, current_line_number)
376 else:
376 else:
377 yield 0, '<td class="line"><a href="">%i</a></td>' % (
377 yield 0, '<td class="line"><a href="">%i</a></td>' % (
378 current_line_number)
378 current_line_number)
379 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
379 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
380 yield 0, '</tr>'
380 yield 0, '</tr>'
381
381
382 last_shown_line_number = current_line_number
382 last_shown_line_number = current_line_number
383
383
384 current_line_number += 1
384 current_line_number += 1
385
385
386
386
387 yield 0, '</table>'
387 yield 0, '</table>'
388
388
389
389
390 def extract_phrases(text_query):
390 def extract_phrases(text_query):
391 """
391 """
392 Extracts phrases from search term string making sure phrases
392 Extracts phrases from search term string making sure phrases
393 contained in double quotes are kept together - and discarding empty values
393 contained in double quotes are kept together - and discarding empty values
394 or fully whitespace values eg.
394 or fully whitespace values eg.
395
395
396 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
396 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
397
397
398 """
398 """
399
399
400 in_phrase = False
400 in_phrase = False
401 buf = ''
401 buf = ''
402 phrases = []
402 phrases = []
403 for char in text_query:
403 for char in text_query:
404 if in_phrase:
404 if in_phrase:
405 if char == '"': # end phrase
405 if char == '"': # end phrase
406 phrases.append(buf)
406 phrases.append(buf)
407 buf = ''
407 buf = ''
408 in_phrase = False
408 in_phrase = False
409 continue
409 continue
410 else:
410 else:
411 buf += char
411 buf += char
412 continue
412 continue
413 else:
413 else:
414 if char == '"': # start phrase
414 if char == '"': # start phrase
415 in_phrase = True
415 in_phrase = True
416 phrases.append(buf)
416 phrases.append(buf)
417 buf = ''
417 buf = ''
418 continue
418 continue
419 elif char == ' ':
419 elif char == ' ':
420 phrases.append(buf)
420 phrases.append(buf)
421 buf = ''
421 buf = ''
422 continue
422 continue
423 else:
423 else:
424 buf += char
424 buf += char
425
425
426 phrases.append(buf)
426 phrases.append(buf)
427 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
427 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
428 return phrases
428 return phrases
429
429
430
430
431 def get_matching_offsets(text, phrases):
431 def get_matching_offsets(text, phrases):
432 """
432 """
433 Returns a list of string offsets in `text` that the list of `terms` match
433 Returns a list of string offsets in `text` that the list of `terms` match
434
434
435 >>> get_matching_offsets('some text here', ['some', 'here'])
435 >>> get_matching_offsets('some text here', ['some', 'here'])
436 [(0, 4), (10, 14)]
436 [(0, 4), (10, 14)]
437
437
438 """
438 """
439 offsets = []
439 offsets = []
440 for phrase in phrases:
440 for phrase in phrases:
441 for match in re.finditer(phrase, text):
441 for match in re.finditer(phrase, text):
442 offsets.append((match.start(), match.end()))
442 offsets.append((match.start(), match.end()))
443
443
444 return offsets
444 return offsets
445
445
446
446
447 def normalize_text_for_matching(x):
447 def normalize_text_for_matching(x):
448 """
448 """
449 Replaces all non alnum characters to spaces and lower cases the string,
449 Replaces all non alnum characters to spaces and lower cases the string,
450 useful for comparing two text strings without punctuation
450 useful for comparing two text strings without punctuation
451 """
451 """
452 return re.sub(r'[^\w]', ' ', x.lower())
452 return re.sub(r'[^\w]', ' ', x.lower())
453
453
454
454
455 def get_matching_line_offsets(lines, terms):
455 def get_matching_line_offsets(lines, terms):
456 """ Return a set of `lines` indices (starting from 1) matching a
456 """ Return a set of `lines` indices (starting from 1) matching a
457 text search query, along with `context` lines above/below matching lines
457 text search query, along with `context` lines above/below matching lines
458
458
459 :param lines: list of strings representing lines
459 :param lines: list of strings representing lines
460 :param terms: search term string to match in lines eg. 'some text'
460 :param terms: search term string to match in lines eg. 'some text'
461 :param context: number of lines above/below a matching line to add to result
461 :param context: number of lines above/below a matching line to add to result
462 :param max_lines: cut off for lines of interest
462 :param max_lines: cut off for lines of interest
463 eg.
463 eg.
464
464
465 text = '''
465 text = '''
466 words words words
466 words words words
467 words words words
467 words words words
468 some text some
468 some text some
469 words words words
469 words words words
470 words words words
470 words words words
471 text here what
471 text here what
472 '''
472 '''
473 get_matching_line_offsets(text, 'text', context=1)
473 get_matching_line_offsets(text, 'text', context=1)
474 {3: [(5, 9)], 6: [(0, 4)]]
474 {3: [(5, 9)], 6: [(0, 4)]]
475
475
476 """
476 """
477 matching_lines = {}
477 matching_lines = {}
478 phrases = [normalize_text_for_matching(phrase)
478 phrases = [normalize_text_for_matching(phrase)
479 for phrase in extract_phrases(terms)]
479 for phrase in extract_phrases(terms)]
480
480
481 for line_index, line in enumerate(lines, start=1):
481 for line_index, line in enumerate(lines, start=1):
482 match_offsets = get_matching_offsets(
482 match_offsets = get_matching_offsets(
483 normalize_text_for_matching(line), phrases)
483 normalize_text_for_matching(line), phrases)
484 if match_offsets:
484 if match_offsets:
485 matching_lines[line_index] = match_offsets
485 matching_lines[line_index] = match_offsets
486
486
487 return matching_lines
487 return matching_lines
488
488
489
489
490 def hsv_to_rgb(h, s, v):
490 def hsv_to_rgb(h, s, v):
491 """ Convert hsv color values to rgb """
491 """ Convert hsv color values to rgb """
492
492
493 if s == 0.0:
493 if s == 0.0:
494 return v, v, v
494 return v, v, v
495 i = int(h * 6.0) # XXX assume int() truncates!
495 i = int(h * 6.0) # XXX assume int() truncates!
496 f = (h * 6.0) - i
496 f = (h * 6.0) - i
497 p = v * (1.0 - s)
497 p = v * (1.0 - s)
498 q = v * (1.0 - s * f)
498 q = v * (1.0 - s * f)
499 t = v * (1.0 - s * (1.0 - f))
499 t = v * (1.0 - s * (1.0 - f))
500 i = i % 6
500 i = i % 6
501 if i == 0:
501 if i == 0:
502 return v, t, p
502 return v, t, p
503 if i == 1:
503 if i == 1:
504 return q, v, p
504 return q, v, p
505 if i == 2:
505 if i == 2:
506 return p, v, t
506 return p, v, t
507 if i == 3:
507 if i == 3:
508 return p, q, v
508 return p, q, v
509 if i == 4:
509 if i == 4:
510 return t, p, v
510 return t, p, v
511 if i == 5:
511 if i == 5:
512 return v, p, q
512 return v, p, q
513
513
514
514
515 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
515 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
516 """
516 """
517 Generator for getting n of evenly distributed colors using
517 Generator for getting n of evenly distributed colors using
518 hsv color and golden ratio. It always return same order of colors
518 hsv color and golden ratio. It always return same order of colors
519
519
520 :param n: number of colors to generate
520 :param n: number of colors to generate
521 :param saturation: saturation of returned colors
521 :param saturation: saturation of returned colors
522 :param lightness: lightness of returned colors
522 :param lightness: lightness of returned colors
523 :returns: RGB tuple
523 :returns: RGB tuple
524 """
524 """
525
525
526 golden_ratio = 0.618033988749895
526 golden_ratio = 0.618033988749895
527 h = 0.22717784590367374
527 h = 0.22717784590367374
528
528
529 for _ in xrange(n):
529 for _ in xrange(n):
530 h += golden_ratio
530 h += golden_ratio
531 h %= 1
531 h %= 1
532 HSV_tuple = [h, saturation, lightness]
532 HSV_tuple = [h, saturation, lightness]
533 RGB_tuple = hsv_to_rgb(*HSV_tuple)
533 RGB_tuple = hsv_to_rgb(*HSV_tuple)
534 yield map(lambda x: str(int(x * 256)), RGB_tuple)
534 yield map(lambda x: str(int(x * 256)), RGB_tuple)
535
535
536
536
537 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
537 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
538 """
538 """
539 Returns a function which when called with an argument returns a unique
539 Returns a function which when called with an argument returns a unique
540 color for that argument, eg.
540 color for that argument, eg.
541
541
542 :param n: number of colors to generate
542 :param n: number of colors to generate
543 :param saturation: saturation of returned colors
543 :param saturation: saturation of returned colors
544 :param lightness: lightness of returned colors
544 :param lightness: lightness of returned colors
545 :returns: css RGB string
545 :returns: css RGB string
546
546
547 >>> color_hash = color_hasher()
547 >>> color_hash = color_hasher()
548 >>> color_hash('hello')
548 >>> color_hash('hello')
549 'rgb(34, 12, 59)'
549 'rgb(34, 12, 59)'
550 >>> color_hash('hello')
550 >>> color_hash('hello')
551 'rgb(34, 12, 59)'
551 'rgb(34, 12, 59)'
552 >>> color_hash('other')
552 >>> color_hash('other')
553 'rgb(90, 224, 159)'
553 'rgb(90, 224, 159)'
554 """
554 """
555
555
556 color_dict = {}
556 color_dict = {}
557 cgenerator = unique_color_generator(
557 cgenerator = unique_color_generator(
558 saturation=saturation, lightness=lightness)
558 saturation=saturation, lightness=lightness)
559
559
560 def get_color_string(thing):
560 def get_color_string(thing):
561 if thing in color_dict:
561 if thing in color_dict:
562 col = color_dict[thing]
562 col = color_dict[thing]
563 else:
563 else:
564 col = color_dict[thing] = cgenerator.next()
564 col = color_dict[thing] = cgenerator.next()
565 return "rgb(%s)" % (', '.join(col))
565 return "rgb(%s)" % (', '.join(col))
566
566
567 return get_color_string
567 return get_color_string
568
568
569
569
570 def get_lexer_safe(mimetype=None, filepath=None):
570 def get_lexer_safe(mimetype=None, filepath=None):
571 """
571 """
572 Tries to return a relevant pygments lexer using mimetype/filepath name,
572 Tries to return a relevant pygments lexer using mimetype/filepath name,
573 defaulting to plain text if none could be found
573 defaulting to plain text if none could be found
574 """
574 """
575 lexer = None
575 lexer = None
576 try:
576 try:
577 if mimetype:
577 if mimetype:
578 lexer = get_lexer_for_mimetype(mimetype)
578 lexer = get_lexer_for_mimetype(mimetype)
579 if not lexer:
579 if not lexer:
580 lexer = get_lexer_for_filename(filepath)
580 lexer = get_lexer_for_filename(filepath)
581 except pygments.util.ClassNotFound:
581 except pygments.util.ClassNotFound:
582 pass
582 pass
583
583
584 if not lexer:
584 if not lexer:
585 lexer = get_lexer_by_name('text')
585 lexer = get_lexer_by_name('text')
586
586
587 return lexer
587 return lexer
588
588
589
589
590 def get_lexer_for_filenode(filenode):
590 def get_lexer_for_filenode(filenode):
591 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
591 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
592 return lexer
592 return lexer
593
593
594
594
595 def pygmentize(filenode, **kwargs):
595 def pygmentize(filenode, **kwargs):
596 """
596 """
597 pygmentize function using pygments
597 pygmentize function using pygments
598
598
599 :param filenode:
599 :param filenode:
600 """
600 """
601 lexer = get_lexer_for_filenode(filenode)
601 lexer = get_lexer_for_filenode(filenode)
602 return literal(code_highlight(filenode.content, lexer,
602 return literal(code_highlight(filenode.content, lexer,
603 CodeHtmlFormatter(**kwargs)))
603 CodeHtmlFormatter(**kwargs)))
604
604
605
605
606 def is_following_repo(repo_name, user_id):
606 def is_following_repo(repo_name, user_id):
607 from rhodecode.model.scm import ScmModel
607 from rhodecode.model.scm import ScmModel
608 return ScmModel().is_following_repo(repo_name, user_id)
608 return ScmModel().is_following_repo(repo_name, user_id)
609
609
610
610
611 class _Message(object):
611 class _Message(object):
612 """A message returned by ``Flash.pop_messages()``.
612 """A message returned by ``Flash.pop_messages()``.
613
613
614 Converting the message to a string returns the message text. Instances
614 Converting the message to a string returns the message text. Instances
615 also have the following attributes:
615 also have the following attributes:
616
616
617 * ``message``: the message text.
617 * ``message``: the message text.
618 * ``category``: the category specified when the message was created.
618 * ``category``: the category specified when the message was created.
619 """
619 """
620
620
621 def __init__(self, category, message):
621 def __init__(self, category, message):
622 self.category = category
622 self.category = category
623 self.message = message
623 self.message = message
624
624
625 def __str__(self):
625 def __str__(self):
626 return self.message
626 return self.message
627
627
628 __unicode__ = __str__
628 __unicode__ = __str__
629
629
630 def __html__(self):
630 def __html__(self):
631 return escape(safe_unicode(self.message))
631 return escape(safe_unicode(self.message))
632
632
633
633
634 class Flash(object):
634 class Flash(object):
635 # List of allowed categories. If None, allow any category.
635 # List of allowed categories. If None, allow any category.
636 categories = ["warning", "notice", "error", "success"]
636 categories = ["warning", "notice", "error", "success"]
637
637
638 # Default category if none is specified.
638 # Default category if none is specified.
639 default_category = "notice"
639 default_category = "notice"
640
640
641 def __init__(self, session_key="flash", categories=None,
641 def __init__(self, session_key="flash", categories=None,
642 default_category=None):
642 default_category=None):
643 """
643 """
644 Instantiate a ``Flash`` object.
644 Instantiate a ``Flash`` object.
645
645
646 ``session_key`` is the key to save the messages under in the user's
646 ``session_key`` is the key to save the messages under in the user's
647 session.
647 session.
648
648
649 ``categories`` is an optional list which overrides the default list
649 ``categories`` is an optional list which overrides the default list
650 of categories.
650 of categories.
651
651
652 ``default_category`` overrides the default category used for messages
652 ``default_category`` overrides the default category used for messages
653 when none is specified.
653 when none is specified.
654 """
654 """
655 self.session_key = session_key
655 self.session_key = session_key
656 if categories is not None:
656 if categories is not None:
657 self.categories = categories
657 self.categories = categories
658 if default_category is not None:
658 if default_category is not None:
659 self.default_category = default_category
659 self.default_category = default_category
660 if self.categories and self.default_category not in self.categories:
660 if self.categories and self.default_category not in self.categories:
661 raise ValueError(
661 raise ValueError(
662 "unrecognized default category %r" % (self.default_category,))
662 "unrecognized default category %r" % (self.default_category,))
663
663
664 def pop_messages(self, session=None, request=None):
664 def pop_messages(self, session=None, request=None):
665 """
665 """
666 Return all accumulated messages and delete them from the session.
666 Return all accumulated messages and delete them from the session.
667
667
668 The return value is a list of ``Message`` objects.
668 The return value is a list of ``Message`` objects.
669 """
669 """
670 messages = []
670 messages = []
671
671
672 if not session:
672 if not session:
673 if not request:
673 if not request:
674 request = get_current_request()
674 request = get_current_request()
675 session = request.session
675 session = request.session
676
676
677 # Pop the 'old' pylons flash messages. They are tuples of the form
677 # Pop the 'old' pylons flash messages. They are tuples of the form
678 # (category, message)
678 # (category, message)
679 for cat, msg in session.pop(self.session_key, []):
679 for cat, msg in session.pop(self.session_key, []):
680 messages.append(_Message(cat, msg))
680 messages.append(_Message(cat, msg))
681
681
682 # Pop the 'new' pyramid flash messages for each category as list
682 # Pop the 'new' pyramid flash messages for each category as list
683 # of strings.
683 # of strings.
684 for cat in self.categories:
684 for cat in self.categories:
685 for msg in session.pop_flash(queue=cat):
685 for msg in session.pop_flash(queue=cat):
686 messages.append(_Message(cat, msg))
686 messages.append(_Message(cat, msg))
687 # Map messages from the default queue to the 'notice' category.
687 # Map messages from the default queue to the 'notice' category.
688 for msg in session.pop_flash():
688 for msg in session.pop_flash():
689 messages.append(_Message('notice', msg))
689 messages.append(_Message('notice', msg))
690
690
691 session.save()
691 session.save()
692 return messages
692 return messages
693
693
694 def json_alerts(self, session=None, request=None):
694 def json_alerts(self, session=None, request=None):
695 payloads = []
695 payloads = []
696 messages = flash.pop_messages(session=session, request=request)
696 messages = flash.pop_messages(session=session, request=request)
697 if messages:
697 if messages:
698 for message in messages:
698 for message in messages:
699 subdata = {}
699 subdata = {}
700 if hasattr(message.message, 'rsplit'):
700 if hasattr(message.message, 'rsplit'):
701 flash_data = message.message.rsplit('|DELIM|', 1)
701 flash_data = message.message.rsplit('|DELIM|', 1)
702 org_message = flash_data[0]
702 org_message = flash_data[0]
703 if len(flash_data) > 1:
703 if len(flash_data) > 1:
704 subdata = json.loads(flash_data[1])
704 subdata = json.loads(flash_data[1])
705 else:
705 else:
706 org_message = message.message
706 org_message = message.message
707 payloads.append({
707 payloads.append({
708 'message': {
708 'message': {
709 'message': u'{}'.format(org_message),
709 'message': u'{}'.format(org_message),
710 'level': message.category,
710 'level': message.category,
711 'force': True,
711 'force': True,
712 'subdata': subdata
712 'subdata': subdata
713 }
713 }
714 })
714 })
715 return json.dumps(payloads)
715 return json.dumps(payloads)
716
716
717 def __call__(self, message, category=None, ignore_duplicate=False,
717 def __call__(self, message, category=None, ignore_duplicate=False,
718 session=None, request=None):
718 session=None, request=None):
719
719
720 if not session:
720 if not session:
721 if not request:
721 if not request:
722 request = get_current_request()
722 request = get_current_request()
723 session = request.session
723 session = request.session
724
724
725 session.flash(
725 session.flash(
726 message, queue=category, allow_duplicate=not ignore_duplicate)
726 message, queue=category, allow_duplicate=not ignore_duplicate)
727
727
728
728
729 flash = Flash()
729 flash = Flash()
730
730
731 #==============================================================================
731 #==============================================================================
732 # SCM FILTERS available via h.
732 # SCM FILTERS available via h.
733 #==============================================================================
733 #==============================================================================
734 from rhodecode.lib.vcs.utils import author_name, author_email
734 from rhodecode.lib.vcs.utils import author_name, author_email
735 from rhodecode.lib.utils2 import credentials_filter, age as _age
735 from rhodecode.lib.utils2 import credentials_filter, age as _age
736 from rhodecode.model.db import User, ChangesetStatus
736 from rhodecode.model.db import User, ChangesetStatus
737
737
738 age = _age
738 age = _age
739 capitalize = lambda x: x.capitalize()
739 capitalize = lambda x: x.capitalize()
740 email = author_email
740 email = author_email
741 short_id = lambda x: x[:12]
741 short_id = lambda x: x[:12]
742 hide_credentials = lambda x: ''.join(credentials_filter(x))
742 hide_credentials = lambda x: ''.join(credentials_filter(x))
743
743
744
744
745 def age_component(datetime_iso, value=None, time_is_local=False):
745 def age_component(datetime_iso, value=None, time_is_local=False):
746 title = value or format_date(datetime_iso)
746 title = value or format_date(datetime_iso)
747 tzinfo = '+00:00'
747 tzinfo = '+00:00'
748
748
749 # detect if we have a timezone info, otherwise, add it
749 # detect if we have a timezone info, otherwise, add it
750 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
750 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
751 if time_is_local:
751 if time_is_local:
752 tzinfo = time.strftime("+%H:%M",
752 tzinfo = time.strftime("+%H:%M",
753 time.gmtime(
753 time.gmtime(
754 (datetime.now() - datetime.utcnow()).seconds + 1
754 (datetime.now() - datetime.utcnow()).seconds + 1
755 )
755 )
756 )
756 )
757
757
758 return literal(
758 return literal(
759 '<time class="timeago tooltip" '
759 '<time class="timeago tooltip" '
760 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
760 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
761 datetime_iso, title, tzinfo))
761 datetime_iso, title, tzinfo))
762
762
763
763
764 def _shorten_commit_id(commit_id):
764 def _shorten_commit_id(commit_id):
765 from rhodecode import CONFIG
765 from rhodecode import CONFIG
766 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
766 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
767 return commit_id[:def_len]
767 return commit_id[:def_len]
768
768
769
769
770 def show_id(commit):
770 def show_id(commit):
771 """
771 """
772 Configurable function that shows ID
772 Configurable function that shows ID
773 by default it's r123:fffeeefffeee
773 by default it's r123:fffeeefffeee
774
774
775 :param commit: commit instance
775 :param commit: commit instance
776 """
776 """
777 from rhodecode import CONFIG
777 from rhodecode import CONFIG
778 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
778 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
779
779
780 raw_id = _shorten_commit_id(commit.raw_id)
780 raw_id = _shorten_commit_id(commit.raw_id)
781 if show_idx:
781 if show_idx:
782 return 'r%s:%s' % (commit.idx, raw_id)
782 return 'r%s:%s' % (commit.idx, raw_id)
783 else:
783 else:
784 return '%s' % (raw_id, )
784 return '%s' % (raw_id, )
785
785
786
786
787 def format_date(date):
787 def format_date(date):
788 """
788 """
789 use a standardized formatting for dates used in RhodeCode
789 use a standardized formatting for dates used in RhodeCode
790
790
791 :param date: date/datetime object
791 :param date: date/datetime object
792 :return: formatted date
792 :return: formatted date
793 """
793 """
794
794
795 if date:
795 if date:
796 _fmt = "%a, %d %b %Y %H:%M:%S"
796 _fmt = "%a, %d %b %Y %H:%M:%S"
797 return safe_unicode(date.strftime(_fmt))
797 return safe_unicode(date.strftime(_fmt))
798
798
799 return u""
799 return u""
800
800
801
801
802 class _RepoChecker(object):
802 class _RepoChecker(object):
803
803
804 def __init__(self, backend_alias):
804 def __init__(self, backend_alias):
805 self._backend_alias = backend_alias
805 self._backend_alias = backend_alias
806
806
807 def __call__(self, repository):
807 def __call__(self, repository):
808 if hasattr(repository, 'alias'):
808 if hasattr(repository, 'alias'):
809 _type = repository.alias
809 _type = repository.alias
810 elif hasattr(repository, 'repo_type'):
810 elif hasattr(repository, 'repo_type'):
811 _type = repository.repo_type
811 _type = repository.repo_type
812 else:
812 else:
813 _type = repository
813 _type = repository
814 return _type == self._backend_alias
814 return _type == self._backend_alias
815
815
816 is_git = _RepoChecker('git')
816 is_git = _RepoChecker('git')
817 is_hg = _RepoChecker('hg')
817 is_hg = _RepoChecker('hg')
818 is_svn = _RepoChecker('svn')
818 is_svn = _RepoChecker('svn')
819
819
820
820
821 def get_repo_type_by_name(repo_name):
821 def get_repo_type_by_name(repo_name):
822 repo = Repository.get_by_repo_name(repo_name)
822 repo = Repository.get_by_repo_name(repo_name)
823 return repo.repo_type
823 return repo.repo_type
824
824
825
825
826 def is_svn_without_proxy(repository):
826 def is_svn_without_proxy(repository):
827 if is_svn(repository):
827 if is_svn(repository):
828 from rhodecode.model.settings import VcsSettingsModel
828 from rhodecode.model.settings import VcsSettingsModel
829 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
829 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
830 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
830 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
831 return False
831 return False
832
832
833
833
834 def discover_user(author):
834 def discover_user(author):
835 """
835 """
836 Tries to discover RhodeCode User based on the autho string. Author string
836 Tries to discover RhodeCode User based on the autho string. Author string
837 is typically `FirstName LastName <email@address.com>`
837 is typically `FirstName LastName <email@address.com>`
838 """
838 """
839
839
840 # if author is already an instance use it for extraction
840 # if author is already an instance use it for extraction
841 if isinstance(author, User):
841 if isinstance(author, User):
842 return author
842 return author
843
843
844 # Valid email in the attribute passed, see if they're in the system
844 # Valid email in the attribute passed, see if they're in the system
845 _email = author_email(author)
845 _email = author_email(author)
846 if _email != '':
846 if _email != '':
847 user = User.get_by_email(_email, case_insensitive=True, cache=True)
847 user = User.get_by_email(_email, case_insensitive=True, cache=True)
848 if user is not None:
848 if user is not None:
849 return user
849 return user
850
850
851 # Maybe it's a username, we try to extract it and fetch by username ?
851 # Maybe it's a username, we try to extract it and fetch by username ?
852 _author = author_name(author)
852 _author = author_name(author)
853 user = User.get_by_username(_author, case_insensitive=True, cache=True)
853 user = User.get_by_username(_author, case_insensitive=True, cache=True)
854 if user is not None:
854 if user is not None:
855 return user
855 return user
856
856
857 return None
857 return None
858
858
859
859
860 def email_or_none(author):
860 def email_or_none(author):
861 # extract email from the commit string
861 # extract email from the commit string
862 _email = author_email(author)
862 _email = author_email(author)
863
863
864 # If we have an email, use it, otherwise
864 # If we have an email, use it, otherwise
865 # see if it contains a username we can get an email from
865 # see if it contains a username we can get an email from
866 if _email != '':
866 if _email != '':
867 return _email
867 return _email
868 else:
868 else:
869 user = User.get_by_username(
869 user = User.get_by_username(
870 author_name(author), case_insensitive=True, cache=True)
870 author_name(author), case_insensitive=True, cache=True)
871
871
872 if user is not None:
872 if user is not None:
873 return user.email
873 return user.email
874
874
875 # No valid email, not a valid user in the system, none!
875 # No valid email, not a valid user in the system, none!
876 return None
876 return None
877
877
878
878
879 def link_to_user(author, length=0, **kwargs):
879 def link_to_user(author, length=0, **kwargs):
880 user = discover_user(author)
880 user = discover_user(author)
881 # user can be None, but if we have it already it means we can re-use it
881 # user can be None, but if we have it already it means we can re-use it
882 # in the person() function, so we save 1 intensive-query
882 # in the person() function, so we save 1 intensive-query
883 if user:
883 if user:
884 author = user
884 author = user
885
885
886 display_person = person(author, 'username_or_name_or_email')
886 display_person = person(author, 'username_or_name_or_email')
887 if length:
887 if length:
888 display_person = shorter(display_person, length)
888 display_person = shorter(display_person, length)
889
889
890 if user:
890 if user:
891 return link_to(
891 return link_to(
892 escape(display_person),
892 escape(display_person),
893 route_path('user_profile', username=user.username),
893 route_path('user_profile', username=user.username),
894 **kwargs)
894 **kwargs)
895 else:
895 else:
896 return escape(display_person)
896 return escape(display_person)
897
897
898
898
899 def link_to_group(users_group_name, **kwargs):
900 return link_to(
901 escape(users_group_name),
902 route_path('user_group_profile', user_group_name=users_group_name),
903 **kwargs)
904
905
899 def person(author, show_attr="username_and_name"):
906 def person(author, show_attr="username_and_name"):
900 user = discover_user(author)
907 user = discover_user(author)
901 if user:
908 if user:
902 return getattr(user, show_attr)
909 return getattr(user, show_attr)
903 else:
910 else:
904 _author = author_name(author)
911 _author = author_name(author)
905 _email = email(author)
912 _email = email(author)
906 return _author or _email
913 return _author or _email
907
914
908
915
909 def author_string(email):
916 def author_string(email):
910 if email:
917 if email:
911 user = User.get_by_email(email, case_insensitive=True, cache=True)
918 user = User.get_by_email(email, case_insensitive=True, cache=True)
912 if user:
919 if user:
913 if user.first_name or user.last_name:
920 if user.first_name or user.last_name:
914 return '%s %s &lt;%s&gt;' % (
921 return '%s %s &lt;%s&gt;' % (
915 user.first_name, user.last_name, email)
922 user.first_name, user.last_name, email)
916 else:
923 else:
917 return email
924 return email
918 else:
925 else:
919 return email
926 return email
920 else:
927 else:
921 return None
928 return None
922
929
923
930
924 def person_by_id(id_, show_attr="username_and_name"):
931 def person_by_id(id_, show_attr="username_and_name"):
925 # attr to return from fetched user
932 # attr to return from fetched user
926 person_getter = lambda usr: getattr(usr, show_attr)
933 person_getter = lambda usr: getattr(usr, show_attr)
927
934
928 #maybe it's an ID ?
935 #maybe it's an ID ?
929 if str(id_).isdigit() or isinstance(id_, int):
936 if str(id_).isdigit() or isinstance(id_, int):
930 id_ = int(id_)
937 id_ = int(id_)
931 user = User.get(id_)
938 user = User.get(id_)
932 if user is not None:
939 if user is not None:
933 return person_getter(user)
940 return person_getter(user)
934 return id_
941 return id_
935
942
936
943
937 def gravatar_with_user(request, author, show_disabled=False):
944 def gravatar_with_user(request, author, show_disabled=False):
938 _render = request.get_partial_renderer(
945 _render = request.get_partial_renderer(
939 'rhodecode:templates/base/base.mako')
946 'rhodecode:templates/base/base.mako')
940 return _render('gravatar_with_user', author, show_disabled=show_disabled)
947 return _render('gravatar_with_user', author, show_disabled=show_disabled)
941
948
942
949
943 tags_paterns = OrderedDict((
950 tags_paterns = OrderedDict((
944 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
951 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
945 '<div class="metatag" tag="lang">\\2</div>')),
952 '<div class="metatag" tag="lang">\\2</div>')),
946
953
947 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
954 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
948 '<div class="metatag" tag="see">see: \\1 </div>')),
955 '<div class="metatag" tag="see">see: \\1 </div>')),
949
956
950 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
957 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
951 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
958 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
952
959
953 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
960 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
954 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
961 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
955
962
956 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
963 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
957 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
964 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
958
965
959 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
966 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
960 '<div class="metatag" tag="state \\1">\\1</div>')),
967 '<div class="metatag" tag="state \\1">\\1</div>')),
961
968
962 # label in grey
969 # label in grey
963 ('label', (re.compile(r'\[([a-z]+)\]'),
970 ('label', (re.compile(r'\[([a-z]+)\]'),
964 '<div class="metatag" tag="label">\\1</div>')),
971 '<div class="metatag" tag="label">\\1</div>')),
965
972
966 # generic catch all in grey
973 # generic catch all in grey
967 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
974 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
968 '<div class="metatag" tag="generic">\\1</div>')),
975 '<div class="metatag" tag="generic">\\1</div>')),
969 ))
976 ))
970
977
971
978
972 def extract_metatags(value):
979 def extract_metatags(value):
973 """
980 """
974 Extract supported meta-tags from given text value
981 Extract supported meta-tags from given text value
975 """
982 """
976 tags = []
983 tags = []
977 if not value:
984 if not value:
978 return tags, ''
985 return tags, ''
979
986
980 for key, val in tags_paterns.items():
987 for key, val in tags_paterns.items():
981 pat, replace_html = val
988 pat, replace_html = val
982 tags.extend([(key, x.group()) for x in pat.finditer(value)])
989 tags.extend([(key, x.group()) for x in pat.finditer(value)])
983 value = pat.sub('', value)
990 value = pat.sub('', value)
984
991
985 return tags, value
992 return tags, value
986
993
987
994
988 def style_metatag(tag_type, value):
995 def style_metatag(tag_type, value):
989 """
996 """
990 converts tags from value into html equivalent
997 converts tags from value into html equivalent
991 """
998 """
992 if not value:
999 if not value:
993 return ''
1000 return ''
994
1001
995 html_value = value
1002 html_value = value
996 tag_data = tags_paterns.get(tag_type)
1003 tag_data = tags_paterns.get(tag_type)
997 if tag_data:
1004 if tag_data:
998 pat, replace_html = tag_data
1005 pat, replace_html = tag_data
999 # convert to plain `unicode` instead of a markup tag to be used in
1006 # convert to plain `unicode` instead of a markup tag to be used in
1000 # regex expressions. safe_unicode doesn't work here
1007 # regex expressions. safe_unicode doesn't work here
1001 html_value = pat.sub(replace_html, unicode(value))
1008 html_value = pat.sub(replace_html, unicode(value))
1002
1009
1003 return html_value
1010 return html_value
1004
1011
1005
1012
1006 def bool2icon(value):
1013 def bool2icon(value):
1007 """
1014 """
1008 Returns boolean value of a given value, represented as html element with
1015 Returns boolean value of a given value, represented as html element with
1009 classes that will represent icons
1016 classes that will represent icons
1010
1017
1011 :param value: given value to convert to html node
1018 :param value: given value to convert to html node
1012 """
1019 """
1013
1020
1014 if value: # does bool conversion
1021 if value: # does bool conversion
1015 return HTML.tag('i', class_="icon-true")
1022 return HTML.tag('i', class_="icon-true")
1016 else: # not true as bool
1023 else: # not true as bool
1017 return HTML.tag('i', class_="icon-false")
1024 return HTML.tag('i', class_="icon-false")
1018
1025
1019
1026
1020 #==============================================================================
1027 #==============================================================================
1021 # PERMS
1028 # PERMS
1022 #==============================================================================
1029 #==============================================================================
1023 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1030 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1024 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1031 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1025 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1032 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1026 csrf_token_key
1033 csrf_token_key
1027
1034
1028
1035
1029 #==============================================================================
1036 #==============================================================================
1030 # GRAVATAR URL
1037 # GRAVATAR URL
1031 #==============================================================================
1038 #==============================================================================
1032 class InitialsGravatar(object):
1039 class InitialsGravatar(object):
1033 def __init__(self, email_address, first_name, last_name, size=30,
1040 def __init__(self, email_address, first_name, last_name, size=30,
1034 background=None, text_color='#fff'):
1041 background=None, text_color='#fff'):
1035 self.size = size
1042 self.size = size
1036 self.first_name = first_name
1043 self.first_name = first_name
1037 self.last_name = last_name
1044 self.last_name = last_name
1038 self.email_address = email_address
1045 self.email_address = email_address
1039 self.background = background or self.str2color(email_address)
1046 self.background = background or self.str2color(email_address)
1040 self.text_color = text_color
1047 self.text_color = text_color
1041
1048
1042 def get_color_bank(self):
1049 def get_color_bank(self):
1043 """
1050 """
1044 returns a predefined list of colors that gravatars can use.
1051 returns a predefined list of colors that gravatars can use.
1045 Those are randomized distinct colors that guarantee readability and
1052 Those are randomized distinct colors that guarantee readability and
1046 uniqueness.
1053 uniqueness.
1047
1054
1048 generated with: http://phrogz.net/css/distinct-colors.html
1055 generated with: http://phrogz.net/css/distinct-colors.html
1049 """
1056 """
1050 return [
1057 return [
1051 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1058 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1052 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1059 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1053 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1060 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1054 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1061 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1055 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1062 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1056 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1063 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1057 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1064 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1058 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1065 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1059 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1066 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1060 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1067 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1061 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1068 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1062 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1069 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1063 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1070 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1064 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1071 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1065 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1072 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1066 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1073 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1067 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1074 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1068 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1075 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1069 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1076 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1070 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1077 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1071 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1078 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1072 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1079 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1073 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1080 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1074 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1081 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1075 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1082 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1076 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1083 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1077 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1084 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1078 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1085 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1079 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1086 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1080 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1087 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1081 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1088 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1082 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1089 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1083 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1090 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1084 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1091 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1085 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1092 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1086 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1093 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1087 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1094 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1088 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1095 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1089 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1096 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1090 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1097 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1091 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1098 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1092 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1099 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1093 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1100 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1094 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1101 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1095 '#4f8c46', '#368dd9', '#5c0073'
1102 '#4f8c46', '#368dd9', '#5c0073'
1096 ]
1103 ]
1097
1104
1098 def rgb_to_hex_color(self, rgb_tuple):
1105 def rgb_to_hex_color(self, rgb_tuple):
1099 """
1106 """
1100 Converts an rgb_tuple passed to an hex color.
1107 Converts an rgb_tuple passed to an hex color.
1101
1108
1102 :param rgb_tuple: tuple with 3 ints represents rgb color space
1109 :param rgb_tuple: tuple with 3 ints represents rgb color space
1103 """
1110 """
1104 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1111 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1105
1112
1106 def email_to_int_list(self, email_str):
1113 def email_to_int_list(self, email_str):
1107 """
1114 """
1108 Get every byte of the hex digest value of email and turn it to integer.
1115 Get every byte of the hex digest value of email and turn it to integer.
1109 It's going to be always between 0-255
1116 It's going to be always between 0-255
1110 """
1117 """
1111 digest = md5_safe(email_str.lower())
1118 digest = md5_safe(email_str.lower())
1112 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1119 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1113
1120
1114 def pick_color_bank_index(self, email_str, color_bank):
1121 def pick_color_bank_index(self, email_str, color_bank):
1115 return self.email_to_int_list(email_str)[0] % len(color_bank)
1122 return self.email_to_int_list(email_str)[0] % len(color_bank)
1116
1123
1117 def str2color(self, email_str):
1124 def str2color(self, email_str):
1118 """
1125 """
1119 Tries to map in a stable algorithm an email to color
1126 Tries to map in a stable algorithm an email to color
1120
1127
1121 :param email_str:
1128 :param email_str:
1122 """
1129 """
1123 color_bank = self.get_color_bank()
1130 color_bank = self.get_color_bank()
1124 # pick position (module it's length so we always find it in the
1131 # pick position (module it's length so we always find it in the
1125 # bank even if it's smaller than 256 values
1132 # bank even if it's smaller than 256 values
1126 pos = self.pick_color_bank_index(email_str, color_bank)
1133 pos = self.pick_color_bank_index(email_str, color_bank)
1127 return color_bank[pos]
1134 return color_bank[pos]
1128
1135
1129 def normalize_email(self, email_address):
1136 def normalize_email(self, email_address):
1130 import unicodedata
1137 import unicodedata
1131 # default host used to fill in the fake/missing email
1138 # default host used to fill in the fake/missing email
1132 default_host = u'localhost'
1139 default_host = u'localhost'
1133
1140
1134 if not email_address:
1141 if not email_address:
1135 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1142 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1136
1143
1137 email_address = safe_unicode(email_address)
1144 email_address = safe_unicode(email_address)
1138
1145
1139 if u'@' not in email_address:
1146 if u'@' not in email_address:
1140 email_address = u'%s@%s' % (email_address, default_host)
1147 email_address = u'%s@%s' % (email_address, default_host)
1141
1148
1142 if email_address.endswith(u'@'):
1149 if email_address.endswith(u'@'):
1143 email_address = u'%s%s' % (email_address, default_host)
1150 email_address = u'%s%s' % (email_address, default_host)
1144
1151
1145 email_address = unicodedata.normalize('NFKD', email_address)\
1152 email_address = unicodedata.normalize('NFKD', email_address)\
1146 .encode('ascii', 'ignore')
1153 .encode('ascii', 'ignore')
1147 return email_address
1154 return email_address
1148
1155
1149 def get_initials(self):
1156 def get_initials(self):
1150 """
1157 """
1151 Returns 2 letter initials calculated based on the input.
1158 Returns 2 letter initials calculated based on the input.
1152 The algorithm picks first given email address, and takes first letter
1159 The algorithm picks first given email address, and takes first letter
1153 of part before @, and then the first letter of server name. In case
1160 of part before @, and then the first letter of server name. In case
1154 the part before @ is in a format of `somestring.somestring2` it replaces
1161 the part before @ is in a format of `somestring.somestring2` it replaces
1155 the server letter with first letter of somestring2
1162 the server letter with first letter of somestring2
1156
1163
1157 In case function was initialized with both first and lastname, this
1164 In case function was initialized with both first and lastname, this
1158 overrides the extraction from email by first letter of the first and
1165 overrides the extraction from email by first letter of the first and
1159 last name. We add special logic to that functionality, In case Full name
1166 last name. We add special logic to that functionality, In case Full name
1160 is compound, like Guido Von Rossum, we use last part of the last name
1167 is compound, like Guido Von Rossum, we use last part of the last name
1161 (Von Rossum) picking `R`.
1168 (Von Rossum) picking `R`.
1162
1169
1163 Function also normalizes the non-ascii characters to they ascii
1170 Function also normalizes the non-ascii characters to they ascii
1164 representation, eg Ą => A
1171 representation, eg Ą => A
1165 """
1172 """
1166 import unicodedata
1173 import unicodedata
1167 # replace non-ascii to ascii
1174 # replace non-ascii to ascii
1168 first_name = unicodedata.normalize(
1175 first_name = unicodedata.normalize(
1169 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1176 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1170 last_name = unicodedata.normalize(
1177 last_name = unicodedata.normalize(
1171 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1178 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1172
1179
1173 # do NFKD encoding, and also make sure email has proper format
1180 # do NFKD encoding, and also make sure email has proper format
1174 email_address = self.normalize_email(self.email_address)
1181 email_address = self.normalize_email(self.email_address)
1175
1182
1176 # first push the email initials
1183 # first push the email initials
1177 prefix, server = email_address.split('@', 1)
1184 prefix, server = email_address.split('@', 1)
1178
1185
1179 # check if prefix is maybe a 'first_name.last_name' syntax
1186 # check if prefix is maybe a 'first_name.last_name' syntax
1180 _dot_split = prefix.rsplit('.', 1)
1187 _dot_split = prefix.rsplit('.', 1)
1181 if len(_dot_split) == 2 and _dot_split[1]:
1188 if len(_dot_split) == 2 and _dot_split[1]:
1182 initials = [_dot_split[0][0], _dot_split[1][0]]
1189 initials = [_dot_split[0][0], _dot_split[1][0]]
1183 else:
1190 else:
1184 initials = [prefix[0], server[0]]
1191 initials = [prefix[0], server[0]]
1185
1192
1186 # then try to replace either first_name or last_name
1193 # then try to replace either first_name or last_name
1187 fn_letter = (first_name or " ")[0].strip()
1194 fn_letter = (first_name or " ")[0].strip()
1188 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1195 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1189
1196
1190 if fn_letter:
1197 if fn_letter:
1191 initials[0] = fn_letter
1198 initials[0] = fn_letter
1192
1199
1193 if ln_letter:
1200 if ln_letter:
1194 initials[1] = ln_letter
1201 initials[1] = ln_letter
1195
1202
1196 return ''.join(initials).upper()
1203 return ''.join(initials).upper()
1197
1204
1198 def get_img_data_by_type(self, font_family, img_type):
1205 def get_img_data_by_type(self, font_family, img_type):
1199 default_user = """
1206 default_user = """
1200 <svg xmlns="http://www.w3.org/2000/svg"
1207 <svg xmlns="http://www.w3.org/2000/svg"
1201 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1208 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1202 viewBox="-15 -10 439.165 429.164"
1209 viewBox="-15 -10 439.165 429.164"
1203
1210
1204 xml:space="preserve"
1211 xml:space="preserve"
1205 style="background:{background};" >
1212 style="background:{background};" >
1206
1213
1207 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1214 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1208 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1215 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1209 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1216 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1210 168.596,153.916,216.671,
1217 168.596,153.916,216.671,
1211 204.583,216.671z" fill="{text_color}"/>
1218 204.583,216.671z" fill="{text_color}"/>
1212 <path d="M407.164,374.717L360.88,
1219 <path d="M407.164,374.717L360.88,
1213 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1220 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1214 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1221 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1215 15.366-44.203,23.488-69.076,23.488c-24.877,
1222 15.366-44.203,23.488-69.076,23.488c-24.877,
1216 0-48.762-8.122-69.078-23.488
1223 0-48.762-8.122-69.078-23.488
1217 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1224 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1218 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1225 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1219 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1226 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1220 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1227 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1221 19.402-10.527 C409.699,390.129,
1228 19.402-10.527 C409.699,390.129,
1222 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1229 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1223 </svg>""".format(
1230 </svg>""".format(
1224 size=self.size,
1231 size=self.size,
1225 background='#979797', # @grey4
1232 background='#979797', # @grey4
1226 text_color=self.text_color,
1233 text_color=self.text_color,
1227 font_family=font_family)
1234 font_family=font_family)
1228
1235
1229 return {
1236 return {
1230 "default_user": default_user
1237 "default_user": default_user
1231 }[img_type]
1238 }[img_type]
1232
1239
1233 def get_img_data(self, svg_type=None):
1240 def get_img_data(self, svg_type=None):
1234 """
1241 """
1235 generates the svg metadata for image
1242 generates the svg metadata for image
1236 """
1243 """
1237
1244
1238 font_family = ','.join([
1245 font_family = ','.join([
1239 'proximanovaregular',
1246 'proximanovaregular',
1240 'Proxima Nova Regular',
1247 'Proxima Nova Regular',
1241 'Proxima Nova',
1248 'Proxima Nova',
1242 'Arial',
1249 'Arial',
1243 'Lucida Grande',
1250 'Lucida Grande',
1244 'sans-serif'
1251 'sans-serif'
1245 ])
1252 ])
1246 if svg_type:
1253 if svg_type:
1247 return self.get_img_data_by_type(font_family, svg_type)
1254 return self.get_img_data_by_type(font_family, svg_type)
1248
1255
1249 initials = self.get_initials()
1256 initials = self.get_initials()
1250 img_data = """
1257 img_data = """
1251 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1258 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1252 width="{size}" height="{size}"
1259 width="{size}" height="{size}"
1253 style="width: 100%; height: 100%; background-color: {background}"
1260 style="width: 100%; height: 100%; background-color: {background}"
1254 viewBox="0 0 {size} {size}">
1261 viewBox="0 0 {size} {size}">
1255 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1262 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1256 pointer-events="auto" fill="{text_color}"
1263 pointer-events="auto" fill="{text_color}"
1257 font-family="{font_family}"
1264 font-family="{font_family}"
1258 style="font-weight: 400; font-size: {f_size}px;">{text}
1265 style="font-weight: 400; font-size: {f_size}px;">{text}
1259 </text>
1266 </text>
1260 </svg>""".format(
1267 </svg>""".format(
1261 size=self.size,
1268 size=self.size,
1262 f_size=self.size/1.85, # scale the text inside the box nicely
1269 f_size=self.size/1.85, # scale the text inside the box nicely
1263 background=self.background,
1270 background=self.background,
1264 text_color=self.text_color,
1271 text_color=self.text_color,
1265 text=initials.upper(),
1272 text=initials.upper(),
1266 font_family=font_family)
1273 font_family=font_family)
1267
1274
1268 return img_data
1275 return img_data
1269
1276
1270 def generate_svg(self, svg_type=None):
1277 def generate_svg(self, svg_type=None):
1271 img_data = self.get_img_data(svg_type)
1278 img_data = self.get_img_data(svg_type)
1272 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1279 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1273
1280
1274
1281
1275 def initials_gravatar(email_address, first_name, last_name, size=30):
1282 def initials_gravatar(email_address, first_name, last_name, size=30):
1276 svg_type = None
1283 svg_type = None
1277 if email_address == User.DEFAULT_USER_EMAIL:
1284 if email_address == User.DEFAULT_USER_EMAIL:
1278 svg_type = 'default_user'
1285 svg_type = 'default_user'
1279 klass = InitialsGravatar(email_address, first_name, last_name, size)
1286 klass = InitialsGravatar(email_address, first_name, last_name, size)
1280 return klass.generate_svg(svg_type=svg_type)
1287 return klass.generate_svg(svg_type=svg_type)
1281
1288
1282
1289
1283 def gravatar_url(email_address, size=30, request=None):
1290 def gravatar_url(email_address, size=30, request=None):
1284 request = get_current_request()
1291 request = get_current_request()
1285 _use_gravatar = request.call_context.visual.use_gravatar
1292 _use_gravatar = request.call_context.visual.use_gravatar
1286 _gravatar_url = request.call_context.visual.gravatar_url
1293 _gravatar_url = request.call_context.visual.gravatar_url
1287
1294
1288 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1295 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1289
1296
1290 email_address = email_address or User.DEFAULT_USER_EMAIL
1297 email_address = email_address or User.DEFAULT_USER_EMAIL
1291 if isinstance(email_address, unicode):
1298 if isinstance(email_address, unicode):
1292 # hashlib crashes on unicode items
1299 # hashlib crashes on unicode items
1293 email_address = safe_str(email_address)
1300 email_address = safe_str(email_address)
1294
1301
1295 # empty email or default user
1302 # empty email or default user
1296 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1303 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1297 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1304 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1298
1305
1299 if _use_gravatar:
1306 if _use_gravatar:
1300 # TODO: Disuse pyramid thread locals. Think about another solution to
1307 # TODO: Disuse pyramid thread locals. Think about another solution to
1301 # get the host and schema here.
1308 # get the host and schema here.
1302 request = get_current_request()
1309 request = get_current_request()
1303 tmpl = safe_str(_gravatar_url)
1310 tmpl = safe_str(_gravatar_url)
1304 tmpl = tmpl.replace('{email}', email_address)\
1311 tmpl = tmpl.replace('{email}', email_address)\
1305 .replace('{md5email}', md5_safe(email_address.lower())) \
1312 .replace('{md5email}', md5_safe(email_address.lower())) \
1306 .replace('{netloc}', request.host)\
1313 .replace('{netloc}', request.host)\
1307 .replace('{scheme}', request.scheme)\
1314 .replace('{scheme}', request.scheme)\
1308 .replace('{size}', safe_str(size))
1315 .replace('{size}', safe_str(size))
1309 return tmpl
1316 return tmpl
1310 else:
1317 else:
1311 return initials_gravatar(email_address, '', '', size=size)
1318 return initials_gravatar(email_address, '', '', size=size)
1312
1319
1313
1320
1314 class Page(_Page):
1321 class Page(_Page):
1315 """
1322 """
1316 Custom pager to match rendering style with paginator
1323 Custom pager to match rendering style with paginator
1317 """
1324 """
1318
1325
1319 def _get_pos(self, cur_page, max_page, items):
1326 def _get_pos(self, cur_page, max_page, items):
1320 edge = (items / 2) + 1
1327 edge = (items / 2) + 1
1321 if (cur_page <= edge):
1328 if (cur_page <= edge):
1322 radius = max(items / 2, items - cur_page)
1329 radius = max(items / 2, items - cur_page)
1323 elif (max_page - cur_page) < edge:
1330 elif (max_page - cur_page) < edge:
1324 radius = (items - 1) - (max_page - cur_page)
1331 radius = (items - 1) - (max_page - cur_page)
1325 else:
1332 else:
1326 radius = items / 2
1333 radius = items / 2
1327
1334
1328 left = max(1, (cur_page - (radius)))
1335 left = max(1, (cur_page - (radius)))
1329 right = min(max_page, cur_page + (radius))
1336 right = min(max_page, cur_page + (radius))
1330 return left, cur_page, right
1337 return left, cur_page, right
1331
1338
1332 def _range(self, regexp_match):
1339 def _range(self, regexp_match):
1333 """
1340 """
1334 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1341 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1335
1342
1336 Arguments:
1343 Arguments:
1337
1344
1338 regexp_match
1345 regexp_match
1339 A "re" (regular expressions) match object containing the
1346 A "re" (regular expressions) match object containing the
1340 radius of linked pages around the current page in
1347 radius of linked pages around the current page in
1341 regexp_match.group(1) as a string
1348 regexp_match.group(1) as a string
1342
1349
1343 This function is supposed to be called as a callable in
1350 This function is supposed to be called as a callable in
1344 re.sub.
1351 re.sub.
1345
1352
1346 """
1353 """
1347 radius = int(regexp_match.group(1))
1354 radius = int(regexp_match.group(1))
1348
1355
1349 # Compute the first and last page number within the radius
1356 # Compute the first and last page number within the radius
1350 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1357 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1351 # -> leftmost_page = 5
1358 # -> leftmost_page = 5
1352 # -> rightmost_page = 9
1359 # -> rightmost_page = 9
1353 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1360 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1354 self.last_page,
1361 self.last_page,
1355 (radius * 2) + 1)
1362 (radius * 2) + 1)
1356 nav_items = []
1363 nav_items = []
1357
1364
1358 # Create a link to the first page (unless we are on the first page
1365 # Create a link to the first page (unless we are on the first page
1359 # or there would be no need to insert '..' spacers)
1366 # or there would be no need to insert '..' spacers)
1360 if self.page != self.first_page and self.first_page < leftmost_page:
1367 if self.page != self.first_page and self.first_page < leftmost_page:
1361 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1368 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1362
1369
1363 # Insert dots if there are pages between the first page
1370 # Insert dots if there are pages between the first page
1364 # and the currently displayed page range
1371 # and the currently displayed page range
1365 if leftmost_page - self.first_page > 1:
1372 if leftmost_page - self.first_page > 1:
1366 # Wrap in a SPAN tag if nolink_attr is set
1373 # Wrap in a SPAN tag if nolink_attr is set
1367 text = '..'
1374 text = '..'
1368 if self.dotdot_attr:
1375 if self.dotdot_attr:
1369 text = HTML.span(c=text, **self.dotdot_attr)
1376 text = HTML.span(c=text, **self.dotdot_attr)
1370 nav_items.append(text)
1377 nav_items.append(text)
1371
1378
1372 for thispage in xrange(leftmost_page, rightmost_page + 1):
1379 for thispage in xrange(leftmost_page, rightmost_page + 1):
1373 # Hilight the current page number and do not use a link
1380 # Hilight the current page number and do not use a link
1374 if thispage == self.page:
1381 if thispage == self.page:
1375 text = '%s' % (thispage,)
1382 text = '%s' % (thispage,)
1376 # Wrap in a SPAN tag if nolink_attr is set
1383 # Wrap in a SPAN tag if nolink_attr is set
1377 if self.curpage_attr:
1384 if self.curpage_attr:
1378 text = HTML.span(c=text, **self.curpage_attr)
1385 text = HTML.span(c=text, **self.curpage_attr)
1379 nav_items.append(text)
1386 nav_items.append(text)
1380 # Otherwise create just a link to that page
1387 # Otherwise create just a link to that page
1381 else:
1388 else:
1382 text = '%s' % (thispage,)
1389 text = '%s' % (thispage,)
1383 nav_items.append(self._pagerlink(thispage, text))
1390 nav_items.append(self._pagerlink(thispage, text))
1384
1391
1385 # Insert dots if there are pages between the displayed
1392 # Insert dots if there are pages between the displayed
1386 # page numbers and the end of the page range
1393 # page numbers and the end of the page range
1387 if self.last_page - rightmost_page > 1:
1394 if self.last_page - rightmost_page > 1:
1388 text = '..'
1395 text = '..'
1389 # Wrap in a SPAN tag if nolink_attr is set
1396 # Wrap in a SPAN tag if nolink_attr is set
1390 if self.dotdot_attr:
1397 if self.dotdot_attr:
1391 text = HTML.span(c=text, **self.dotdot_attr)
1398 text = HTML.span(c=text, **self.dotdot_attr)
1392 nav_items.append(text)
1399 nav_items.append(text)
1393
1400
1394 # Create a link to the very last page (unless we are on the last
1401 # Create a link to the very last page (unless we are on the last
1395 # page or there would be no need to insert '..' spacers)
1402 # page or there would be no need to insert '..' spacers)
1396 if self.page != self.last_page and rightmost_page < self.last_page:
1403 if self.page != self.last_page and rightmost_page < self.last_page:
1397 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1404 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1398
1405
1399 ## prerender links
1406 ## prerender links
1400 #_page_link = url.current()
1407 #_page_link = url.current()
1401 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1408 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1402 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1409 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1403 return self.separator.join(nav_items)
1410 return self.separator.join(nav_items)
1404
1411
1405 def pager(self, format='~2~', page_param='page', partial_param='partial',
1412 def pager(self, format='~2~', page_param='page', partial_param='partial',
1406 show_if_single_page=False, separator=' ', onclick=None,
1413 show_if_single_page=False, separator=' ', onclick=None,
1407 symbol_first='<<', symbol_last='>>',
1414 symbol_first='<<', symbol_last='>>',
1408 symbol_previous='<', symbol_next='>',
1415 symbol_previous='<', symbol_next='>',
1409 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1416 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1410 curpage_attr={'class': 'pager_curpage'},
1417 curpage_attr={'class': 'pager_curpage'},
1411 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1418 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1412
1419
1413 self.curpage_attr = curpage_attr
1420 self.curpage_attr = curpage_attr
1414 self.separator = separator
1421 self.separator = separator
1415 self.pager_kwargs = kwargs
1422 self.pager_kwargs = kwargs
1416 self.page_param = page_param
1423 self.page_param = page_param
1417 self.partial_param = partial_param
1424 self.partial_param = partial_param
1418 self.onclick = onclick
1425 self.onclick = onclick
1419 self.link_attr = link_attr
1426 self.link_attr = link_attr
1420 self.dotdot_attr = dotdot_attr
1427 self.dotdot_attr = dotdot_attr
1421
1428
1422 # Don't show navigator if there is no more than one page
1429 # Don't show navigator if there is no more than one page
1423 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1430 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1424 return ''
1431 return ''
1425
1432
1426 from string import Template
1433 from string import Template
1427 # Replace ~...~ in token format by range of pages
1434 # Replace ~...~ in token format by range of pages
1428 result = re.sub(r'~(\d+)~', self._range, format)
1435 result = re.sub(r'~(\d+)~', self._range, format)
1429
1436
1430 # Interpolate '%' variables
1437 # Interpolate '%' variables
1431 result = Template(result).safe_substitute({
1438 result = Template(result).safe_substitute({
1432 'first_page': self.first_page,
1439 'first_page': self.first_page,
1433 'last_page': self.last_page,
1440 'last_page': self.last_page,
1434 'page': self.page,
1441 'page': self.page,
1435 'page_count': self.page_count,
1442 'page_count': self.page_count,
1436 'items_per_page': self.items_per_page,
1443 'items_per_page': self.items_per_page,
1437 'first_item': self.first_item,
1444 'first_item': self.first_item,
1438 'last_item': self.last_item,
1445 'last_item': self.last_item,
1439 'item_count': self.item_count,
1446 'item_count': self.item_count,
1440 'link_first': self.page > self.first_page and \
1447 'link_first': self.page > self.first_page and \
1441 self._pagerlink(self.first_page, symbol_first) or '',
1448 self._pagerlink(self.first_page, symbol_first) or '',
1442 'link_last': self.page < self.last_page and \
1449 'link_last': self.page < self.last_page and \
1443 self._pagerlink(self.last_page, symbol_last) or '',
1450 self._pagerlink(self.last_page, symbol_last) or '',
1444 'link_previous': self.previous_page and \
1451 'link_previous': self.previous_page and \
1445 self._pagerlink(self.previous_page, symbol_previous) \
1452 self._pagerlink(self.previous_page, symbol_previous) \
1446 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1453 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1447 'link_next': self.next_page and \
1454 'link_next': self.next_page and \
1448 self._pagerlink(self.next_page, symbol_next) \
1455 self._pagerlink(self.next_page, symbol_next) \
1449 or HTML.span(symbol_next, class_="pg-next disabled")
1456 or HTML.span(symbol_next, class_="pg-next disabled")
1450 })
1457 })
1451
1458
1452 return literal(result)
1459 return literal(result)
1453
1460
1454
1461
1455 #==============================================================================
1462 #==============================================================================
1456 # REPO PAGER, PAGER FOR REPOSITORY
1463 # REPO PAGER, PAGER FOR REPOSITORY
1457 #==============================================================================
1464 #==============================================================================
1458 class RepoPage(Page):
1465 class RepoPage(Page):
1459
1466
1460 def __init__(self, collection, page=1, items_per_page=20,
1467 def __init__(self, collection, page=1, items_per_page=20,
1461 item_count=None, url=None, **kwargs):
1468 item_count=None, url=None, **kwargs):
1462
1469
1463 """Create a "RepoPage" instance. special pager for paging
1470 """Create a "RepoPage" instance. special pager for paging
1464 repository
1471 repository
1465 """
1472 """
1466 self._url_generator = url
1473 self._url_generator = url
1467
1474
1468 # Safe the kwargs class-wide so they can be used in the pager() method
1475 # Safe the kwargs class-wide so they can be used in the pager() method
1469 self.kwargs = kwargs
1476 self.kwargs = kwargs
1470
1477
1471 # Save a reference to the collection
1478 # Save a reference to the collection
1472 self.original_collection = collection
1479 self.original_collection = collection
1473
1480
1474 self.collection = collection
1481 self.collection = collection
1475
1482
1476 # The self.page is the number of the current page.
1483 # The self.page is the number of the current page.
1477 # The first page has the number 1!
1484 # The first page has the number 1!
1478 try:
1485 try:
1479 self.page = int(page) # make it int() if we get it as a string
1486 self.page = int(page) # make it int() if we get it as a string
1480 except (ValueError, TypeError):
1487 except (ValueError, TypeError):
1481 self.page = 1
1488 self.page = 1
1482
1489
1483 self.items_per_page = items_per_page
1490 self.items_per_page = items_per_page
1484
1491
1485 # Unless the user tells us how many items the collections has
1492 # Unless the user tells us how many items the collections has
1486 # we calculate that ourselves.
1493 # we calculate that ourselves.
1487 if item_count is not None:
1494 if item_count is not None:
1488 self.item_count = item_count
1495 self.item_count = item_count
1489 else:
1496 else:
1490 self.item_count = len(self.collection)
1497 self.item_count = len(self.collection)
1491
1498
1492 # Compute the number of the first and last available page
1499 # Compute the number of the first and last available page
1493 if self.item_count > 0:
1500 if self.item_count > 0:
1494 self.first_page = 1
1501 self.first_page = 1
1495 self.page_count = int(math.ceil(float(self.item_count) /
1502 self.page_count = int(math.ceil(float(self.item_count) /
1496 self.items_per_page))
1503 self.items_per_page))
1497 self.last_page = self.first_page + self.page_count - 1
1504 self.last_page = self.first_page + self.page_count - 1
1498
1505
1499 # Make sure that the requested page number is the range of
1506 # Make sure that the requested page number is the range of
1500 # valid pages
1507 # valid pages
1501 if self.page > self.last_page:
1508 if self.page > self.last_page:
1502 self.page = self.last_page
1509 self.page = self.last_page
1503 elif self.page < self.first_page:
1510 elif self.page < self.first_page:
1504 self.page = self.first_page
1511 self.page = self.first_page
1505
1512
1506 # Note: the number of items on this page can be less than
1513 # Note: the number of items on this page can be less than
1507 # items_per_page if the last page is not full
1514 # items_per_page if the last page is not full
1508 self.first_item = max(0, (self.item_count) - (self.page *
1515 self.first_item = max(0, (self.item_count) - (self.page *
1509 items_per_page))
1516 items_per_page))
1510 self.last_item = ((self.item_count - 1) - items_per_page *
1517 self.last_item = ((self.item_count - 1) - items_per_page *
1511 (self.page - 1))
1518 (self.page - 1))
1512
1519
1513 self.items = list(self.collection[self.first_item:self.last_item + 1])
1520 self.items = list(self.collection[self.first_item:self.last_item + 1])
1514
1521
1515 # Links to previous and next page
1522 # Links to previous and next page
1516 if self.page > self.first_page:
1523 if self.page > self.first_page:
1517 self.previous_page = self.page - 1
1524 self.previous_page = self.page - 1
1518 else:
1525 else:
1519 self.previous_page = None
1526 self.previous_page = None
1520
1527
1521 if self.page < self.last_page:
1528 if self.page < self.last_page:
1522 self.next_page = self.page + 1
1529 self.next_page = self.page + 1
1523 else:
1530 else:
1524 self.next_page = None
1531 self.next_page = None
1525
1532
1526 # No items available
1533 # No items available
1527 else:
1534 else:
1528 self.first_page = None
1535 self.first_page = None
1529 self.page_count = 0
1536 self.page_count = 0
1530 self.last_page = None
1537 self.last_page = None
1531 self.first_item = None
1538 self.first_item = None
1532 self.last_item = None
1539 self.last_item = None
1533 self.previous_page = None
1540 self.previous_page = None
1534 self.next_page = None
1541 self.next_page = None
1535 self.items = []
1542 self.items = []
1536
1543
1537 # This is a subclass of the 'list' type. Initialise the list now.
1544 # This is a subclass of the 'list' type. Initialise the list now.
1538 list.__init__(self, reversed(self.items))
1545 list.__init__(self, reversed(self.items))
1539
1546
1540
1547
1541 def breadcrumb_repo_link(repo):
1548 def breadcrumb_repo_link(repo):
1542 """
1549 """
1543 Makes a breadcrumbs path link to repo
1550 Makes a breadcrumbs path link to repo
1544
1551
1545 ex::
1552 ex::
1546 group >> subgroup >> repo
1553 group >> subgroup >> repo
1547
1554
1548 :param repo: a Repository instance
1555 :param repo: a Repository instance
1549 """
1556 """
1550
1557
1551 path = [
1558 path = [
1552 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1559 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1553 for group in repo.groups_with_parents
1560 for group in repo.groups_with_parents
1554 ] + [
1561 ] + [
1555 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1562 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1556 ]
1563 ]
1557
1564
1558 return literal(' &raquo; '.join(path))
1565 return literal(' &raquo; '.join(path))
1559
1566
1560
1567
1561 def format_byte_size_binary(file_size):
1568 def format_byte_size_binary(file_size):
1562 """
1569 """
1563 Formats file/folder sizes to standard.
1570 Formats file/folder sizes to standard.
1564 """
1571 """
1565 if file_size is None:
1572 if file_size is None:
1566 file_size = 0
1573 file_size = 0
1567
1574
1568 formatted_size = format_byte_size(file_size, binary=True)
1575 formatted_size = format_byte_size(file_size, binary=True)
1569 return formatted_size
1576 return formatted_size
1570
1577
1571
1578
1572 def urlify_text(text_, safe=True):
1579 def urlify_text(text_, safe=True):
1573 """
1580 """
1574 Extrac urls from text and make html links out of them
1581 Extrac urls from text and make html links out of them
1575
1582
1576 :param text_:
1583 :param text_:
1577 """
1584 """
1578
1585
1579 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1586 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1580 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1587 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1581
1588
1582 def url_func(match_obj):
1589 def url_func(match_obj):
1583 url_full = match_obj.groups()[0]
1590 url_full = match_obj.groups()[0]
1584 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1591 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1585 _newtext = url_pat.sub(url_func, text_)
1592 _newtext = url_pat.sub(url_func, text_)
1586 if safe:
1593 if safe:
1587 return literal(_newtext)
1594 return literal(_newtext)
1588 return _newtext
1595 return _newtext
1589
1596
1590
1597
1591 def urlify_commits(text_, repository):
1598 def urlify_commits(text_, repository):
1592 """
1599 """
1593 Extract commit ids from text and make link from them
1600 Extract commit ids from text and make link from them
1594
1601
1595 :param text_:
1602 :param text_:
1596 :param repository: repo name to build the URL with
1603 :param repository: repo name to build the URL with
1597 """
1604 """
1598
1605
1599 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1606 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1600
1607
1601 def url_func(match_obj):
1608 def url_func(match_obj):
1602 commit_id = match_obj.groups()[1]
1609 commit_id = match_obj.groups()[1]
1603 pref = match_obj.groups()[0]
1610 pref = match_obj.groups()[0]
1604 suf = match_obj.groups()[2]
1611 suf = match_obj.groups()[2]
1605
1612
1606 tmpl = (
1613 tmpl = (
1607 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1614 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1608 '%(commit_id)s</a>%(suf)s'
1615 '%(commit_id)s</a>%(suf)s'
1609 )
1616 )
1610 return tmpl % {
1617 return tmpl % {
1611 'pref': pref,
1618 'pref': pref,
1612 'cls': 'revision-link',
1619 'cls': 'revision-link',
1613 'url': route_url('repo_commit', repo_name=repository,
1620 'url': route_url('repo_commit', repo_name=repository,
1614 commit_id=commit_id),
1621 commit_id=commit_id),
1615 'commit_id': commit_id,
1622 'commit_id': commit_id,
1616 'suf': suf
1623 'suf': suf
1617 }
1624 }
1618
1625
1619 newtext = URL_PAT.sub(url_func, text_)
1626 newtext = URL_PAT.sub(url_func, text_)
1620
1627
1621 return newtext
1628 return newtext
1622
1629
1623
1630
1624 def _process_url_func(match_obj, repo_name, uid, entry,
1631 def _process_url_func(match_obj, repo_name, uid, entry,
1625 return_raw_data=False, link_format='html'):
1632 return_raw_data=False, link_format='html'):
1626 pref = ''
1633 pref = ''
1627 if match_obj.group().startswith(' '):
1634 if match_obj.group().startswith(' '):
1628 pref = ' '
1635 pref = ' '
1629
1636
1630 issue_id = ''.join(match_obj.groups())
1637 issue_id = ''.join(match_obj.groups())
1631
1638
1632 if link_format == 'html':
1639 if link_format == 'html':
1633 tmpl = (
1640 tmpl = (
1634 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1641 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1635 '%(issue-prefix)s%(id-repr)s'
1642 '%(issue-prefix)s%(id-repr)s'
1636 '</a>')
1643 '</a>')
1637 elif link_format == 'rst':
1644 elif link_format == 'rst':
1638 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1645 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1639 elif link_format == 'markdown':
1646 elif link_format == 'markdown':
1640 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1647 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1641 else:
1648 else:
1642 raise ValueError('Bad link_format:{}'.format(link_format))
1649 raise ValueError('Bad link_format:{}'.format(link_format))
1643
1650
1644 (repo_name_cleaned,
1651 (repo_name_cleaned,
1645 parent_group_name) = RepoGroupModel().\
1652 parent_group_name) = RepoGroupModel().\
1646 _get_group_name_and_parent(repo_name)
1653 _get_group_name_and_parent(repo_name)
1647
1654
1648 # variables replacement
1655 # variables replacement
1649 named_vars = {
1656 named_vars = {
1650 'id': issue_id,
1657 'id': issue_id,
1651 'repo': repo_name,
1658 'repo': repo_name,
1652 'repo_name': repo_name_cleaned,
1659 'repo_name': repo_name_cleaned,
1653 'group_name': parent_group_name
1660 'group_name': parent_group_name
1654 }
1661 }
1655 # named regex variables
1662 # named regex variables
1656 named_vars.update(match_obj.groupdict())
1663 named_vars.update(match_obj.groupdict())
1657 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1664 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1658
1665
1659 data = {
1666 data = {
1660 'pref': pref,
1667 'pref': pref,
1661 'cls': 'issue-tracker-link',
1668 'cls': 'issue-tracker-link',
1662 'url': _url,
1669 'url': _url,
1663 'id-repr': issue_id,
1670 'id-repr': issue_id,
1664 'issue-prefix': entry['pref'],
1671 'issue-prefix': entry['pref'],
1665 'serv': entry['url'],
1672 'serv': entry['url'],
1666 }
1673 }
1667 if return_raw_data:
1674 if return_raw_data:
1668 return {
1675 return {
1669 'id': issue_id,
1676 'id': issue_id,
1670 'url': _url
1677 'url': _url
1671 }
1678 }
1672 return tmpl % data
1679 return tmpl % data
1673
1680
1674
1681
1675 def get_active_pattern_entries(repo_name):
1682 def get_active_pattern_entries(repo_name):
1676 repo = None
1683 repo = None
1677 if repo_name:
1684 if repo_name:
1678 # Retrieving repo_name to avoid invalid repo_name to explode on
1685 # Retrieving repo_name to avoid invalid repo_name to explode on
1679 # IssueTrackerSettingsModel but still passing invalid name further down
1686 # IssueTrackerSettingsModel but still passing invalid name further down
1680 repo = Repository.get_by_repo_name(repo_name, cache=True)
1687 repo = Repository.get_by_repo_name(repo_name, cache=True)
1681
1688
1682 settings_model = IssueTrackerSettingsModel(repo=repo)
1689 settings_model = IssueTrackerSettingsModel(repo=repo)
1683 active_entries = settings_model.get_settings(cache=True)
1690 active_entries = settings_model.get_settings(cache=True)
1684 return active_entries
1691 return active_entries
1685
1692
1686
1693
1687 def process_patterns(text_string, repo_name, link_format='html',
1694 def process_patterns(text_string, repo_name, link_format='html',
1688 active_entries=None):
1695 active_entries=None):
1689
1696
1690 allowed_formats = ['html', 'rst', 'markdown']
1697 allowed_formats = ['html', 'rst', 'markdown']
1691 if link_format not in allowed_formats:
1698 if link_format not in allowed_formats:
1692 raise ValueError('Link format can be only one of:{} got {}'.format(
1699 raise ValueError('Link format can be only one of:{} got {}'.format(
1693 allowed_formats, link_format))
1700 allowed_formats, link_format))
1694
1701
1695 active_entries = active_entries or get_active_pattern_entries(repo_name)
1702 active_entries = active_entries or get_active_pattern_entries(repo_name)
1696 issues_data = []
1703 issues_data = []
1697 newtext = text_string
1704 newtext = text_string
1698
1705
1699 for uid, entry in active_entries.items():
1706 for uid, entry in active_entries.items():
1700 log.debug('found issue tracker entry with uid %s' % (uid,))
1707 log.debug('found issue tracker entry with uid %s' % (uid,))
1701
1708
1702 if not (entry['pat'] and entry['url']):
1709 if not (entry['pat'] and entry['url']):
1703 log.debug('skipping due to missing data')
1710 log.debug('skipping due to missing data')
1704 continue
1711 continue
1705
1712
1706 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1713 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1707 % (uid, entry['pat'], entry['url'], entry['pref']))
1714 % (uid, entry['pat'], entry['url'], entry['pref']))
1708
1715
1709 try:
1716 try:
1710 pattern = re.compile(r'%s' % entry['pat'])
1717 pattern = re.compile(r'%s' % entry['pat'])
1711 except re.error:
1718 except re.error:
1712 log.exception(
1719 log.exception(
1713 'issue tracker pattern: `%s` failed to compile',
1720 'issue tracker pattern: `%s` failed to compile',
1714 entry['pat'])
1721 entry['pat'])
1715 continue
1722 continue
1716
1723
1717 data_func = partial(
1724 data_func = partial(
1718 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1725 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1719 return_raw_data=True)
1726 return_raw_data=True)
1720
1727
1721 for match_obj in pattern.finditer(text_string):
1728 for match_obj in pattern.finditer(text_string):
1722 issues_data.append(data_func(match_obj))
1729 issues_data.append(data_func(match_obj))
1723
1730
1724 url_func = partial(
1731 url_func = partial(
1725 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1732 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1726 link_format=link_format)
1733 link_format=link_format)
1727
1734
1728 newtext = pattern.sub(url_func, newtext)
1735 newtext = pattern.sub(url_func, newtext)
1729 log.debug('processed prefix:uid `%s`' % (uid,))
1736 log.debug('processed prefix:uid `%s`' % (uid,))
1730
1737
1731 return newtext, issues_data
1738 return newtext, issues_data
1732
1739
1733
1740
1734 def urlify_commit_message(commit_text, repository=None,
1741 def urlify_commit_message(commit_text, repository=None,
1735 active_pattern_entries=None):
1742 active_pattern_entries=None):
1736 """
1743 """
1737 Parses given text message and makes proper links.
1744 Parses given text message and makes proper links.
1738 issues are linked to given issue-server, and rest is a commit link
1745 issues are linked to given issue-server, and rest is a commit link
1739
1746
1740 :param commit_text:
1747 :param commit_text:
1741 :param repository:
1748 :param repository:
1742 """
1749 """
1743 def escaper(string):
1750 def escaper(string):
1744 return string.replace('<', '&lt;').replace('>', '&gt;')
1751 return string.replace('<', '&lt;').replace('>', '&gt;')
1745
1752
1746 newtext = escaper(commit_text)
1753 newtext = escaper(commit_text)
1747
1754
1748 # extract http/https links and make them real urls
1755 # extract http/https links and make them real urls
1749 newtext = urlify_text(newtext, safe=False)
1756 newtext = urlify_text(newtext, safe=False)
1750
1757
1751 # urlify commits - extract commit ids and make link out of them, if we have
1758 # urlify commits - extract commit ids and make link out of them, if we have
1752 # the scope of repository present.
1759 # the scope of repository present.
1753 if repository:
1760 if repository:
1754 newtext = urlify_commits(newtext, repository)
1761 newtext = urlify_commits(newtext, repository)
1755
1762
1756 # process issue tracker patterns
1763 # process issue tracker patterns
1757 newtext, issues = process_patterns(newtext, repository or '',
1764 newtext, issues = process_patterns(newtext, repository or '',
1758 active_entries=active_pattern_entries)
1765 active_entries=active_pattern_entries)
1759
1766
1760 return literal(newtext)
1767 return literal(newtext)
1761
1768
1762
1769
1763 def render_binary(repo_name, file_obj):
1770 def render_binary(repo_name, file_obj):
1764 """
1771 """
1765 Choose how to render a binary file
1772 Choose how to render a binary file
1766 """
1773 """
1767 filename = file_obj.name
1774 filename = file_obj.name
1768
1775
1769 # images
1776 # images
1770 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1777 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1771 if fnmatch.fnmatch(filename, pat=ext):
1778 if fnmatch.fnmatch(filename, pat=ext):
1772 alt = filename
1779 alt = filename
1773 src = route_path(
1780 src = route_path(
1774 'repo_file_raw', repo_name=repo_name,
1781 'repo_file_raw', repo_name=repo_name,
1775 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1782 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1776 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1783 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1777
1784
1778
1785
1779 def renderer_from_filename(filename, exclude=None):
1786 def renderer_from_filename(filename, exclude=None):
1780 """
1787 """
1781 choose a renderer based on filename, this works only for text based files
1788 choose a renderer based on filename, this works only for text based files
1782 """
1789 """
1783
1790
1784 # ipython
1791 # ipython
1785 for ext in ['*.ipynb']:
1792 for ext in ['*.ipynb']:
1786 if fnmatch.fnmatch(filename, pat=ext):
1793 if fnmatch.fnmatch(filename, pat=ext):
1787 return 'jupyter'
1794 return 'jupyter'
1788
1795
1789 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1796 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1790 if is_markup:
1797 if is_markup:
1791 return is_markup
1798 return is_markup
1792 return None
1799 return None
1793
1800
1794
1801
1795 def render(source, renderer='rst', mentions=False, relative_urls=None,
1802 def render(source, renderer='rst', mentions=False, relative_urls=None,
1796 repo_name=None):
1803 repo_name=None):
1797
1804
1798 def maybe_convert_relative_links(html_source):
1805 def maybe_convert_relative_links(html_source):
1799 if relative_urls:
1806 if relative_urls:
1800 return relative_links(html_source, relative_urls)
1807 return relative_links(html_source, relative_urls)
1801 return html_source
1808 return html_source
1802
1809
1803 if renderer == 'rst':
1810 if renderer == 'rst':
1804 if repo_name:
1811 if repo_name:
1805 # process patterns on comments if we pass in repo name
1812 # process patterns on comments if we pass in repo name
1806 source, issues = process_patterns(
1813 source, issues = process_patterns(
1807 source, repo_name, link_format='rst')
1814 source, repo_name, link_format='rst')
1808
1815
1809 return literal(
1816 return literal(
1810 '<div class="rst-block">%s</div>' %
1817 '<div class="rst-block">%s</div>' %
1811 maybe_convert_relative_links(
1818 maybe_convert_relative_links(
1812 MarkupRenderer.rst(source, mentions=mentions)))
1819 MarkupRenderer.rst(source, mentions=mentions)))
1813 elif renderer == 'markdown':
1820 elif renderer == 'markdown':
1814 if repo_name:
1821 if repo_name:
1815 # process patterns on comments if we pass in repo name
1822 # process patterns on comments if we pass in repo name
1816 source, issues = process_patterns(
1823 source, issues = process_patterns(
1817 source, repo_name, link_format='markdown')
1824 source, repo_name, link_format='markdown')
1818
1825
1819 return literal(
1826 return literal(
1820 '<div class="markdown-block">%s</div>' %
1827 '<div class="markdown-block">%s</div>' %
1821 maybe_convert_relative_links(
1828 maybe_convert_relative_links(
1822 MarkupRenderer.markdown(source, flavored=True,
1829 MarkupRenderer.markdown(source, flavored=True,
1823 mentions=mentions)))
1830 mentions=mentions)))
1824 elif renderer == 'jupyter':
1831 elif renderer == 'jupyter':
1825 return literal(
1832 return literal(
1826 '<div class="ipynb">%s</div>' %
1833 '<div class="ipynb">%s</div>' %
1827 maybe_convert_relative_links(
1834 maybe_convert_relative_links(
1828 MarkupRenderer.jupyter(source)))
1835 MarkupRenderer.jupyter(source)))
1829
1836
1830 # None means just show the file-source
1837 # None means just show the file-source
1831 return None
1838 return None
1832
1839
1833
1840
1834 def commit_status(repo, commit_id):
1841 def commit_status(repo, commit_id):
1835 return ChangesetStatusModel().get_status(repo, commit_id)
1842 return ChangesetStatusModel().get_status(repo, commit_id)
1836
1843
1837
1844
1838 def commit_status_lbl(commit_status):
1845 def commit_status_lbl(commit_status):
1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1846 return dict(ChangesetStatus.STATUSES).get(commit_status)
1840
1847
1841
1848
1842 def commit_time(repo_name, commit_id):
1849 def commit_time(repo_name, commit_id):
1843 repo = Repository.get_by_repo_name(repo_name)
1850 repo = Repository.get_by_repo_name(repo_name)
1844 commit = repo.get_commit(commit_id=commit_id)
1851 commit = repo.get_commit(commit_id=commit_id)
1845 return commit.date
1852 return commit.date
1846
1853
1847
1854
1848 def get_permission_name(key):
1855 def get_permission_name(key):
1849 return dict(Permission.PERMS).get(key)
1856 return dict(Permission.PERMS).get(key)
1850
1857
1851
1858
1852 def journal_filter_help(request):
1859 def journal_filter_help(request):
1853 _ = request.translate
1860 _ = request.translate
1854
1861
1855 return _(
1862 return _(
1856 'Example filter terms:\n' +
1863 'Example filter terms:\n' +
1857 ' repository:vcs\n' +
1864 ' repository:vcs\n' +
1858 ' username:marcin\n' +
1865 ' username:marcin\n' +
1859 ' username:(NOT marcin)\n' +
1866 ' username:(NOT marcin)\n' +
1860 ' action:*push*\n' +
1867 ' action:*push*\n' +
1861 ' ip:127.0.0.1\n' +
1868 ' ip:127.0.0.1\n' +
1862 ' date:20120101\n' +
1869 ' date:20120101\n' +
1863 ' date:[20120101100000 TO 20120102]\n' +
1870 ' date:[20120101100000 TO 20120102]\n' +
1864 '\n' +
1871 '\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1872 'Generate wildcards using \'*\' character:\n' +
1866 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1873 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1874 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1868 '\n' +
1875 '\n' +
1869 'Optional AND / OR operators in queries\n' +
1876 'Optional AND / OR operators in queries\n' +
1870 ' "repository:vcs OR repository:test"\n' +
1877 ' "repository:vcs OR repository:test"\n' +
1871 ' "username:test AND repository:test*"\n'
1878 ' "username:test AND repository:test*"\n'
1872 )
1879 )
1873
1880
1874
1881
1875 def search_filter_help(searcher, request):
1882 def search_filter_help(searcher, request):
1876 _ = request.translate
1883 _ = request.translate
1877
1884
1878 terms = ''
1885 terms = ''
1879 return _(
1886 return _(
1880 'Example filter terms for `{searcher}` search:\n' +
1887 'Example filter terms for `{searcher}` search:\n' +
1881 '{terms}\n' +
1888 '{terms}\n' +
1882 'Generate wildcards using \'*\' character:\n' +
1889 'Generate wildcards using \'*\' character:\n' +
1883 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1890 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1884 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1891 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1885 '\n' +
1892 '\n' +
1886 'Optional AND / OR operators in queries\n' +
1893 'Optional AND / OR operators in queries\n' +
1887 ' "repo_name:vcs OR repo_name:test"\n' +
1894 ' "repo_name:vcs OR repo_name:test"\n' +
1888 ' "owner:test AND repo_name:test*"\n' +
1895 ' "owner:test AND repo_name:test*"\n' +
1889 'More: {search_doc}'
1896 'More: {search_doc}'
1890 ).format(searcher=searcher.name,
1897 ).format(searcher=searcher.name,
1891 terms=terms, search_doc=searcher.query_lang_doc)
1898 terms=terms, search_doc=searcher.query_lang_doc)
1892
1899
1893
1900
1894 def not_mapped_error(repo_name):
1901 def not_mapped_error(repo_name):
1895 from rhodecode.translation import _
1902 from rhodecode.translation import _
1896 flash(_('%s repository is not mapped to db perhaps'
1903 flash(_('%s repository is not mapped to db perhaps'
1897 ' it was created or renamed from the filesystem'
1904 ' it was created or renamed from the filesystem'
1898 ' please run the application again'
1905 ' please run the application again'
1899 ' in order to rescan repositories') % repo_name, category='error')
1906 ' in order to rescan repositories') % repo_name, category='error')
1900
1907
1901
1908
1902 def ip_range(ip_addr):
1909 def ip_range(ip_addr):
1903 from rhodecode.model.db import UserIpMap
1910 from rhodecode.model.db import UserIpMap
1904 s, e = UserIpMap._get_ip_range(ip_addr)
1911 s, e = UserIpMap._get_ip_range(ip_addr)
1905 return '%s - %s' % (s, e)
1912 return '%s - %s' % (s, e)
1906
1913
1907
1914
1908 def form(url, method='post', needs_csrf_token=True, **attrs):
1915 def form(url, method='post', needs_csrf_token=True, **attrs):
1909 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1916 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1910 if method.lower() != 'get' and needs_csrf_token:
1917 if method.lower() != 'get' and needs_csrf_token:
1911 raise Exception(
1918 raise Exception(
1912 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1919 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1913 'CSRF token. If the endpoint does not require such token you can ' +
1920 'CSRF token. If the endpoint does not require such token you can ' +
1914 'explicitly set the parameter needs_csrf_token to false.')
1921 'explicitly set the parameter needs_csrf_token to false.')
1915
1922
1916 return wh_form(url, method=method, **attrs)
1923 return wh_form(url, method=method, **attrs)
1917
1924
1918
1925
1919 def secure_form(form_url, method="POST", multipart=False, **attrs):
1926 def secure_form(form_url, method="POST", multipart=False, **attrs):
1920 """Start a form tag that points the action to an url. This
1927 """Start a form tag that points the action to an url. This
1921 form tag will also include the hidden field containing
1928 form tag will also include the hidden field containing
1922 the auth token.
1929 the auth token.
1923
1930
1924 The url options should be given either as a string, or as a
1931 The url options should be given either as a string, or as a
1925 ``url()`` function. The method for the form defaults to POST.
1932 ``url()`` function. The method for the form defaults to POST.
1926
1933
1927 Options:
1934 Options:
1928
1935
1929 ``multipart``
1936 ``multipart``
1930 If set to True, the enctype is set to "multipart/form-data".
1937 If set to True, the enctype is set to "multipart/form-data".
1931 ``method``
1938 ``method``
1932 The method to use when submitting the form, usually either
1939 The method to use when submitting the form, usually either
1933 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1940 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1934 hidden input with name _method is added to simulate the verb
1941 hidden input with name _method is added to simulate the verb
1935 over POST.
1942 over POST.
1936
1943
1937 """
1944 """
1938 from webhelpers.pylonslib.secure_form import insecure_form
1945 from webhelpers.pylonslib.secure_form import insecure_form
1939
1946
1940 if 'request' in attrs:
1947 if 'request' in attrs:
1941 session = attrs['request'].session
1948 session = attrs['request'].session
1942 del attrs['request']
1949 del attrs['request']
1943 else:
1950 else:
1944 raise ValueError(
1951 raise ValueError(
1945 'Calling this form requires request= to be passed as argument')
1952 'Calling this form requires request= to be passed as argument')
1946
1953
1947 form = insecure_form(form_url, method, multipart, **attrs)
1954 form = insecure_form(form_url, method, multipart, **attrs)
1948 token = literal(
1955 token = literal(
1949 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1956 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1950 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1957 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1951
1958
1952 return literal("%s\n%s" % (form, token))
1959 return literal("%s\n%s" % (form, token))
1953
1960
1954
1961
1955 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1962 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1956 select_html = select(name, selected, options, **attrs)
1963 select_html = select(name, selected, options, **attrs)
1957 select2 = """
1964 select2 = """
1958 <script>
1965 <script>
1959 $(document).ready(function() {
1966 $(document).ready(function() {
1960 $('#%s').select2({
1967 $('#%s').select2({
1961 containerCssClass: 'drop-menu',
1968 containerCssClass: 'drop-menu',
1962 dropdownCssClass: 'drop-menu-dropdown',
1969 dropdownCssClass: 'drop-menu-dropdown',
1963 dropdownAutoWidth: true%s
1970 dropdownAutoWidth: true%s
1964 });
1971 });
1965 });
1972 });
1966 </script>
1973 </script>
1967 """
1974 """
1968 filter_option = """,
1975 filter_option = """,
1969 minimumResultsForSearch: -1
1976 minimumResultsForSearch: -1
1970 """
1977 """
1971 input_id = attrs.get('id') or name
1978 input_id = attrs.get('id') or name
1972 filter_enabled = "" if enable_filter else filter_option
1979 filter_enabled = "" if enable_filter else filter_option
1973 select_script = literal(select2 % (input_id, filter_enabled))
1980 select_script = literal(select2 % (input_id, filter_enabled))
1974
1981
1975 return literal(select_html+select_script)
1982 return literal(select_html+select_script)
1976
1983
1977
1984
1978 def get_visual_attr(tmpl_context_var, attr_name):
1985 def get_visual_attr(tmpl_context_var, attr_name):
1979 """
1986 """
1980 A safe way to get a variable from visual variable of template context
1987 A safe way to get a variable from visual variable of template context
1981
1988
1982 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1989 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1983 :param attr_name: name of the attribute we fetch from the c.visual
1990 :param attr_name: name of the attribute we fetch from the c.visual
1984 """
1991 """
1985 visual = getattr(tmpl_context_var, 'visual', None)
1992 visual = getattr(tmpl_context_var, 'visual', None)
1986 if not visual:
1993 if not visual:
1987 return
1994 return
1988 else:
1995 else:
1989 return getattr(visual, attr_name, None)
1996 return getattr(visual, attr_name, None)
1990
1997
1991
1998
1992 def get_last_path_part(file_node):
1999 def get_last_path_part(file_node):
1993 if not file_node.path:
2000 if not file_node.path:
1994 return u''
2001 return u''
1995
2002
1996 path = safe_unicode(file_node.path.split('/')[-1])
2003 path = safe_unicode(file_node.path.split('/')[-1])
1997 return u'../' + path
2004 return u'../' + path
1998
2005
1999
2006
2000 def route_url(*args, **kwargs):
2007 def route_url(*args, **kwargs):
2001 """
2008 """
2002 Wrapper around pyramids `route_url` (fully qualified url) function.
2009 Wrapper around pyramids `route_url` (fully qualified url) function.
2003 """
2010 """
2004 req = get_current_request()
2011 req = get_current_request()
2005 return req.route_url(*args, **kwargs)
2012 return req.route_url(*args, **kwargs)
2006
2013
2007
2014
2008 def route_path(*args, **kwargs):
2015 def route_path(*args, **kwargs):
2009 """
2016 """
2010 Wrapper around pyramids `route_path` function.
2017 Wrapper around pyramids `route_path` function.
2011 """
2018 """
2012 req = get_current_request()
2019 req = get_current_request()
2013 return req.route_path(*args, **kwargs)
2020 return req.route_path(*args, **kwargs)
2014
2021
2015
2022
2016 def route_path_or_none(*args, **kwargs):
2023 def route_path_or_none(*args, **kwargs):
2017 try:
2024 try:
2018 return route_path(*args, **kwargs)
2025 return route_path(*args, **kwargs)
2019 except KeyError:
2026 except KeyError:
2020 return None
2027 return None
2021
2028
2022
2029
2023 def current_route_path(request, **kw):
2030 def current_route_path(request, **kw):
2024 new_args = request.GET.mixed()
2031 new_args = request.GET.mixed()
2025 new_args.update(kw)
2032 new_args.update(kw)
2026 return request.current_route_path(_query=new_args)
2033 return request.current_route_path(_query=new_args)
2027
2034
2028
2035
2029 def api_call_example(method, args):
2036 def api_call_example(method, args):
2030 """
2037 """
2031 Generates an API call example via CURL
2038 Generates an API call example via CURL
2032 """
2039 """
2033 args_json = json.dumps(OrderedDict([
2040 args_json = json.dumps(OrderedDict([
2034 ('id', 1),
2041 ('id', 1),
2035 ('auth_token', 'SECRET'),
2042 ('auth_token', 'SECRET'),
2036 ('method', method),
2043 ('method', method),
2037 ('args', args)
2044 ('args', args)
2038 ]))
2045 ]))
2039 return literal(
2046 return literal(
2040 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2047 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2041 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2042 "and needs to be of `api calls` role."
2049 "and needs to be of `api calls` role."
2043 .format(
2050 .format(
2044 api_url=route_url('apiv2'),
2051 api_url=route_url('apiv2'),
2045 token_url=route_url('my_account_auth_tokens'),
2052 token_url=route_url('my_account_auth_tokens'),
2046 data=args_json))
2053 data=args_json))
2047
2054
2048
2055
2049 def notification_description(notification, request):
2056 def notification_description(notification, request):
2050 """
2057 """
2051 Generate notification human readable description based on notification type
2058 Generate notification human readable description based on notification type
2052 """
2059 """
2053 from rhodecode.model.notification import NotificationModel
2060 from rhodecode.model.notification import NotificationModel
2054 return NotificationModel().make_description(
2061 return NotificationModel().make_description(
2055 notification, translate=request.translate)
2062 notification, translate=request.translate)
2056
2063
2057
2064
2058 def go_import_header(request, db_repo=None):
2065 def go_import_header(request, db_repo=None):
2059 """
2066 """
2060 Creates a header for go-import functionality in Go Lang
2067 Creates a header for go-import functionality in Go Lang
2061 """
2068 """
2062
2069
2063 if not db_repo:
2070 if not db_repo:
2064 return
2071 return
2065 if 'go-get' not in request.GET:
2072 if 'go-get' not in request.GET:
2066 return
2073 return
2067
2074
2068 clone_url = db_repo.clone_url()
2075 clone_url = db_repo.clone_url()
2069 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2076 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2070 # we have a repo and go-get flag,
2077 # we have a repo and go-get flag,
2071 return literal('<meta name="go-import" content="{} {} {}">'.format(
2078 return literal('<meta name="go-import" content="{} {} {}">'.format(
2072 prefix, db_repo.repo_type, clone_url))
2079 prefix, db_repo.repo_type, clone_url))
2073
2080
2074
2081
2075 def reviewer_as_json(*args, **kwargs):
2082 def reviewer_as_json(*args, **kwargs):
2076 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2083 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2077 return _reviewer_as_json(*args, **kwargs)
2084 return _reviewer_as_json(*args, **kwargs)
@@ -1,775 +1,779 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 Utilities library for RhodeCode
22 Utilities library for RhodeCode
23 """
23 """
24
24
25 import datetime
25 import datetime
26 import decorator
26 import decorator
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30 import re
30 import re
31 import shutil
31 import shutil
32 import tempfile
32 import tempfile
33 import traceback
33 import traceback
34 import tarfile
34 import tarfile
35 import warnings
35 import warnings
36 import hashlib
36 import hashlib
37 from os.path import join as jn
37 from os.path import join as jn
38
38
39 import paste
39 import paste
40 import pkg_resources
40 import pkg_resources
41 from webhelpers.text import collapse, remove_formatting, strip_tags
41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 from mako import exceptions
42 from mako import exceptions
43 from pyramid.threadlocal import get_current_registry
43 from pyramid.threadlocal import get_current_registry
44 from pyramid.request import Request
44 from pyramid.request import Request
45
45
46 from rhodecode.lib.fakemod import create_module
46 from rhodecode.lib.fakemod import create_module
47 from rhodecode.lib.vcs.backends.base import Config
47 from rhodecode.lib.vcs.backends.base import Config
48 from rhodecode.lib.vcs.exceptions import VCSError
48 from rhodecode.lib.vcs.exceptions import VCSError
49 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
49 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 from rhodecode.lib.utils2 import (
50 from rhodecode.lib.utils2 import (
51 safe_str, safe_unicode, get_current_rhodecode_user, md5)
51 safe_str, safe_unicode, get_current_rhodecode_user, md5)
52 from rhodecode.model import meta
52 from rhodecode.model import meta
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
60 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61
61
62 # String which contains characters that are not allowed in slug names for
62 # String which contains characters that are not allowed in slug names for
63 # repositories or repository groups. It is properly escaped to use it in
63 # repositories or repository groups. It is properly escaped to use it in
64 # regular expressions.
64 # regular expressions.
65 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
65 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66
66
67 # Regex that matches forbidden characters in repo/group slugs.
67 # Regex that matches forbidden characters in repo/group slugs.
68 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
68 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
69
69
70 # Regex that matches allowed characters in repo/group slugs.
70 # Regex that matches allowed characters in repo/group slugs.
71 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
71 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
72
72
73 # Regex that matches whole repo/group slugs.
73 # Regex that matches whole repo/group slugs.
74 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
74 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
75
75
76 _license_cache = None
76 _license_cache = None
77
77
78
78
79 def repo_name_slug(value):
79 def repo_name_slug(value):
80 """
80 """
81 Return slug of name of repository
81 Return slug of name of repository
82 This function is called on each creation/modification
82 This function is called on each creation/modification
83 of repository to prevent bad names in repo
83 of repository to prevent bad names in repo
84 """
84 """
85 replacement_char = '-'
85 replacement_char = '-'
86
86
87 slug = remove_formatting(value)
87 slug = remove_formatting(value)
88 slug = SLUG_BAD_CHAR_RE.sub('', slug)
88 slug = SLUG_BAD_CHAR_RE.sub('', slug)
89 slug = re.sub('[\s]+', '-', slug)
89 slug = re.sub('[\s]+', '-', slug)
90 slug = collapse(slug, replacement_char)
90 slug = collapse(slug, replacement_char)
91 return slug
91 return slug
92
92
93
93
94 #==============================================================================
94 #==============================================================================
95 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
95 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
96 #==============================================================================
96 #==============================================================================
97 def get_repo_slug(request):
97 def get_repo_slug(request):
98 _repo = ''
98 _repo = ''
99
99
100 if hasattr(request, 'db_repo'):
100 if hasattr(request, 'db_repo'):
101 # if our requests has set db reference use it for name, this
101 # if our requests has set db reference use it for name, this
102 # translates the example.com/_<id> into proper repo names
102 # translates the example.com/_<id> into proper repo names
103 _repo = request.db_repo.repo_name
103 _repo = request.db_repo.repo_name
104 elif getattr(request, 'matchdict', None):
104 elif getattr(request, 'matchdict', None):
105 # pyramid
105 # pyramid
106 _repo = request.matchdict.get('repo_name')
106 _repo = request.matchdict.get('repo_name')
107
107
108 if _repo:
108 if _repo:
109 _repo = _repo.rstrip('/')
109 _repo = _repo.rstrip('/')
110 return _repo
110 return _repo
111
111
112
112
113 def get_repo_group_slug(request):
113 def get_repo_group_slug(request):
114 _group = ''
114 _group = ''
115 if hasattr(request, 'db_repo_group'):
115 if hasattr(request, 'db_repo_group'):
116 # if our requests has set db reference use it for name, this
116 # if our requests has set db reference use it for name, this
117 # translates the example.com/_<id> into proper repo group names
117 # translates the example.com/_<id> into proper repo group names
118 _group = request.db_repo_group.group_name
118 _group = request.db_repo_group.group_name
119 elif getattr(request, 'matchdict', None):
119 elif getattr(request, 'matchdict', None):
120 # pyramid
120 # pyramid
121 _group = request.matchdict.get('repo_group_name')
121 _group = request.matchdict.get('repo_group_name')
122
122
123
123
124 if _group:
124 if _group:
125 _group = _group.rstrip('/')
125 _group = _group.rstrip('/')
126 return _group
126 return _group
127
127
128
128
129 def get_user_group_slug(request):
129 def get_user_group_slug(request):
130 _user_group = ''
130 _user_group = ''
131
131
132 if hasattr(request, 'db_user_group'):
132 if hasattr(request, 'db_user_group'):
133 _user_group = request.db_user_group.users_group_name
133 _user_group = request.db_user_group.users_group_name
134 elif getattr(request, 'matchdict', None):
134 elif getattr(request, 'matchdict', None):
135 # pyramid
135 # pyramid
136 _user_group = request.matchdict.get('user_group_id')
136 _user_group = request.matchdict.get('user_group_id')
137
137 _user_group_name = request.matchdict.get('user_group_name')
138 try:
138 try:
139 _user_group = UserGroup.get(_user_group)
139 if _user_group:
140 _user_group = UserGroup.get(_user_group)
141 elif _user_group_name:
142 _user_group = UserGroup.get_by_group_name(_user_group_name)
143
140 if _user_group:
144 if _user_group:
141 _user_group = _user_group.users_group_name
145 _user_group = _user_group.users_group_name
142 except Exception:
146 except Exception:
143 log.exception('Failed to get user group by id')
147 log.exception('Failed to get user group by id and name')
144 # catch all failures here
148 # catch all failures here
145 return None
149 return None
146
150
147 return _user_group
151 return _user_group
148
152
149
153
150 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
154 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
151 """
155 """
152 Scans given path for repos and return (name,(type,path)) tuple
156 Scans given path for repos and return (name,(type,path)) tuple
153
157
154 :param path: path to scan for repositories
158 :param path: path to scan for repositories
155 :param recursive: recursive search and return names with subdirs in front
159 :param recursive: recursive search and return names with subdirs in front
156 """
160 """
157
161
158 # remove ending slash for better results
162 # remove ending slash for better results
159 path = path.rstrip(os.sep)
163 path = path.rstrip(os.sep)
160 log.debug('now scanning in %s location recursive:%s...', path, recursive)
164 log.debug('now scanning in %s location recursive:%s...', path, recursive)
161
165
162 def _get_repos(p):
166 def _get_repos(p):
163 dirpaths = _get_dirpaths(p)
167 dirpaths = _get_dirpaths(p)
164 if not _is_dir_writable(p):
168 if not _is_dir_writable(p):
165 log.warning('repo path without write access: %s', p)
169 log.warning('repo path without write access: %s', p)
166
170
167 for dirpath in dirpaths:
171 for dirpath in dirpaths:
168 if os.path.isfile(os.path.join(p, dirpath)):
172 if os.path.isfile(os.path.join(p, dirpath)):
169 continue
173 continue
170 cur_path = os.path.join(p, dirpath)
174 cur_path = os.path.join(p, dirpath)
171
175
172 # skip removed repos
176 # skip removed repos
173 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
177 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
174 continue
178 continue
175
179
176 #skip .<somethin> dirs
180 #skip .<somethin> dirs
177 if dirpath.startswith('.'):
181 if dirpath.startswith('.'):
178 continue
182 continue
179
183
180 try:
184 try:
181 scm_info = get_scm(cur_path)
185 scm_info = get_scm(cur_path)
182 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
186 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
183 except VCSError:
187 except VCSError:
184 if not recursive:
188 if not recursive:
185 continue
189 continue
186 #check if this dir containts other repos for recursive scan
190 #check if this dir containts other repos for recursive scan
187 rec_path = os.path.join(p, dirpath)
191 rec_path = os.path.join(p, dirpath)
188 if os.path.isdir(rec_path):
192 if os.path.isdir(rec_path):
189 for inner_scm in _get_repos(rec_path):
193 for inner_scm in _get_repos(rec_path):
190 yield inner_scm
194 yield inner_scm
191
195
192 return _get_repos(path)
196 return _get_repos(path)
193
197
194
198
195 def _get_dirpaths(p):
199 def _get_dirpaths(p):
196 try:
200 try:
197 # OS-independable way of checking if we have at least read-only
201 # OS-independable way of checking if we have at least read-only
198 # access or not.
202 # access or not.
199 dirpaths = os.listdir(p)
203 dirpaths = os.listdir(p)
200 except OSError:
204 except OSError:
201 log.warning('ignoring repo path without read access: %s', p)
205 log.warning('ignoring repo path without read access: %s', p)
202 return []
206 return []
203
207
204 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
208 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
205 # decode paths and suddenly returns unicode objects itself. The items it
209 # decode paths and suddenly returns unicode objects itself. The items it
206 # cannot decode are returned as strings and cause issues.
210 # cannot decode are returned as strings and cause issues.
207 #
211 #
208 # Those paths are ignored here until a solid solution for path handling has
212 # Those paths are ignored here until a solid solution for path handling has
209 # been built.
213 # been built.
210 expected_type = type(p)
214 expected_type = type(p)
211
215
212 def _has_correct_type(item):
216 def _has_correct_type(item):
213 if type(item) is not expected_type:
217 if type(item) is not expected_type:
214 log.error(
218 log.error(
215 u"Ignoring path %s since it cannot be decoded into unicode.",
219 u"Ignoring path %s since it cannot be decoded into unicode.",
216 # Using "repr" to make sure that we see the byte value in case
220 # Using "repr" to make sure that we see the byte value in case
217 # of support.
221 # of support.
218 repr(item))
222 repr(item))
219 return False
223 return False
220 return True
224 return True
221
225
222 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
226 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
223
227
224 return dirpaths
228 return dirpaths
225
229
226
230
227 def _is_dir_writable(path):
231 def _is_dir_writable(path):
228 """
232 """
229 Probe if `path` is writable.
233 Probe if `path` is writable.
230
234
231 Due to trouble on Cygwin / Windows, this is actually probing if it is
235 Due to trouble on Cygwin / Windows, this is actually probing if it is
232 possible to create a file inside of `path`, stat does not produce reliable
236 possible to create a file inside of `path`, stat does not produce reliable
233 results in this case.
237 results in this case.
234 """
238 """
235 try:
239 try:
236 with tempfile.TemporaryFile(dir=path):
240 with tempfile.TemporaryFile(dir=path):
237 pass
241 pass
238 except OSError:
242 except OSError:
239 return False
243 return False
240 return True
244 return True
241
245
242
246
243 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
247 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
244 """
248 """
245 Returns True if given path is a valid repository False otherwise.
249 Returns True if given path is a valid repository False otherwise.
246 If expect_scm param is given also, compare if given scm is the same
250 If expect_scm param is given also, compare if given scm is the same
247 as expected from scm parameter. If explicit_scm is given don't try to
251 as expected from scm parameter. If explicit_scm is given don't try to
248 detect the scm, just use the given one to check if repo is valid
252 detect the scm, just use the given one to check if repo is valid
249
253
250 :param repo_name:
254 :param repo_name:
251 :param base_path:
255 :param base_path:
252 :param expect_scm:
256 :param expect_scm:
253 :param explicit_scm:
257 :param explicit_scm:
254 :param config:
258 :param config:
255
259
256 :return True: if given path is a valid repository
260 :return True: if given path is a valid repository
257 """
261 """
258 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
262 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
259 log.debug('Checking if `%s` is a valid path for repository. '
263 log.debug('Checking if `%s` is a valid path for repository. '
260 'Explicit type: %s', repo_name, explicit_scm)
264 'Explicit type: %s', repo_name, explicit_scm)
261
265
262 try:
266 try:
263 if explicit_scm:
267 if explicit_scm:
264 detected_scms = [get_scm_backend(explicit_scm)(
268 detected_scms = [get_scm_backend(explicit_scm)(
265 full_path, config=config).alias]
269 full_path, config=config).alias]
266 else:
270 else:
267 detected_scms = get_scm(full_path)
271 detected_scms = get_scm(full_path)
268
272
269 if expect_scm:
273 if expect_scm:
270 return detected_scms[0] == expect_scm
274 return detected_scms[0] == expect_scm
271 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
275 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
272 return True
276 return True
273 except VCSError:
277 except VCSError:
274 log.debug('path: %s is not a valid repo !', full_path)
278 log.debug('path: %s is not a valid repo !', full_path)
275 return False
279 return False
276
280
277
281
278 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
282 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
279 """
283 """
280 Returns True if given path is a repository group, False otherwise
284 Returns True if given path is a repository group, False otherwise
281
285
282 :param repo_name:
286 :param repo_name:
283 :param base_path:
287 :param base_path:
284 """
288 """
285 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
289 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
286 log.debug('Checking if `%s` is a valid path for repository group',
290 log.debug('Checking if `%s` is a valid path for repository group',
287 repo_group_name)
291 repo_group_name)
288
292
289 # check if it's not a repo
293 # check if it's not a repo
290 if is_valid_repo(repo_group_name, base_path):
294 if is_valid_repo(repo_group_name, base_path):
291 log.debug('Repo called %s exist, it is not a valid '
295 log.debug('Repo called %s exist, it is not a valid '
292 'repo group' % repo_group_name)
296 'repo group' % repo_group_name)
293 return False
297 return False
294
298
295 try:
299 try:
296 # we need to check bare git repos at higher level
300 # we need to check bare git repos at higher level
297 # since we might match branches/hooks/info/objects or possible
301 # since we might match branches/hooks/info/objects or possible
298 # other things inside bare git repo
302 # other things inside bare git repo
299 scm_ = get_scm(os.path.dirname(full_path))
303 scm_ = get_scm(os.path.dirname(full_path))
300 log.debug('path: %s is a vcs object:%s, not valid '
304 log.debug('path: %s is a vcs object:%s, not valid '
301 'repo group' % (full_path, scm_))
305 'repo group' % (full_path, scm_))
302 return False
306 return False
303 except VCSError:
307 except VCSError:
304 pass
308 pass
305
309
306 # check if it's a valid path
310 # check if it's a valid path
307 if skip_path_check or os.path.isdir(full_path):
311 if skip_path_check or os.path.isdir(full_path):
308 log.debug('path: %s is a valid repo group !', full_path)
312 log.debug('path: %s is a valid repo group !', full_path)
309 return True
313 return True
310
314
311 log.debug('path: %s is not a valid repo group !', full_path)
315 log.debug('path: %s is not a valid repo group !', full_path)
312 return False
316 return False
313
317
314
318
315 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
319 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
316 while True:
320 while True:
317 ok = raw_input(prompt)
321 ok = raw_input(prompt)
318 if ok.lower() in ('y', 'ye', 'yes'):
322 if ok.lower() in ('y', 'ye', 'yes'):
319 return True
323 return True
320 if ok.lower() in ('n', 'no', 'nop', 'nope'):
324 if ok.lower() in ('n', 'no', 'nop', 'nope'):
321 return False
325 return False
322 retries = retries - 1
326 retries = retries - 1
323 if retries < 0:
327 if retries < 0:
324 raise IOError
328 raise IOError
325 print(complaint)
329 print(complaint)
326
330
327 # propagated from mercurial documentation
331 # propagated from mercurial documentation
328 ui_sections = [
332 ui_sections = [
329 'alias', 'auth',
333 'alias', 'auth',
330 'decode/encode', 'defaults',
334 'decode/encode', 'defaults',
331 'diff', 'email',
335 'diff', 'email',
332 'extensions', 'format',
336 'extensions', 'format',
333 'merge-patterns', 'merge-tools',
337 'merge-patterns', 'merge-tools',
334 'hooks', 'http_proxy',
338 'hooks', 'http_proxy',
335 'smtp', 'patch',
339 'smtp', 'patch',
336 'paths', 'profiling',
340 'paths', 'profiling',
337 'server', 'trusted',
341 'server', 'trusted',
338 'ui', 'web', ]
342 'ui', 'web', ]
339
343
340
344
341 def config_data_from_db(clear_session=True, repo=None):
345 def config_data_from_db(clear_session=True, repo=None):
342 """
346 """
343 Read the configuration data from the database and return configuration
347 Read the configuration data from the database and return configuration
344 tuples.
348 tuples.
345 """
349 """
346 from rhodecode.model.settings import VcsSettingsModel
350 from rhodecode.model.settings import VcsSettingsModel
347
351
348 config = []
352 config = []
349
353
350 sa = meta.Session()
354 sa = meta.Session()
351 settings_model = VcsSettingsModel(repo=repo, sa=sa)
355 settings_model = VcsSettingsModel(repo=repo, sa=sa)
352
356
353 ui_settings = settings_model.get_ui_settings()
357 ui_settings = settings_model.get_ui_settings()
354
358
355 for setting in ui_settings:
359 for setting in ui_settings:
356 if setting.active:
360 if setting.active:
357 log.debug(
361 log.debug(
358 'settings ui from db: [%s] %s=%s',
362 'settings ui from db: [%s] %s=%s',
359 setting.section, setting.key, setting.value)
363 setting.section, setting.key, setting.value)
360 config.append((
364 config.append((
361 safe_str(setting.section), safe_str(setting.key),
365 safe_str(setting.section), safe_str(setting.key),
362 safe_str(setting.value)))
366 safe_str(setting.value)))
363 if setting.key == 'push_ssl':
367 if setting.key == 'push_ssl':
364 # force set push_ssl requirement to False, rhodecode
368 # force set push_ssl requirement to False, rhodecode
365 # handles that
369 # handles that
366 config.append((
370 config.append((
367 safe_str(setting.section), safe_str(setting.key), False))
371 safe_str(setting.section), safe_str(setting.key), False))
368 if clear_session:
372 if clear_session:
369 meta.Session.remove()
373 meta.Session.remove()
370
374
371 # TODO: mikhail: probably it makes no sense to re-read hooks information.
375 # TODO: mikhail: probably it makes no sense to re-read hooks information.
372 # It's already there and activated/deactivated
376 # It's already there and activated/deactivated
373 skip_entries = []
377 skip_entries = []
374 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
378 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
375 if 'pull' not in enabled_hook_classes:
379 if 'pull' not in enabled_hook_classes:
376 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
380 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
377 if 'push' not in enabled_hook_classes:
381 if 'push' not in enabled_hook_classes:
378 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
382 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
379 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
383 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
380 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
384 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
381
385
382 config = [entry for entry in config if entry[:2] not in skip_entries]
386 config = [entry for entry in config if entry[:2] not in skip_entries]
383
387
384 return config
388 return config
385
389
386
390
387 def make_db_config(clear_session=True, repo=None):
391 def make_db_config(clear_session=True, repo=None):
388 """
392 """
389 Create a :class:`Config` instance based on the values in the database.
393 Create a :class:`Config` instance based on the values in the database.
390 """
394 """
391 config = Config()
395 config = Config()
392 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
396 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
393 for section, option, value in config_data:
397 for section, option, value in config_data:
394 config.set(section, option, value)
398 config.set(section, option, value)
395 return config
399 return config
396
400
397
401
398 def get_enabled_hook_classes(ui_settings):
402 def get_enabled_hook_classes(ui_settings):
399 """
403 """
400 Return the enabled hook classes.
404 Return the enabled hook classes.
401
405
402 :param ui_settings: List of ui_settings as returned
406 :param ui_settings: List of ui_settings as returned
403 by :meth:`VcsSettingsModel.get_ui_settings`
407 by :meth:`VcsSettingsModel.get_ui_settings`
404
408
405 :return: a list with the enabled hook classes. The order is not guaranteed.
409 :return: a list with the enabled hook classes. The order is not guaranteed.
406 :rtype: list
410 :rtype: list
407 """
411 """
408 enabled_hooks = []
412 enabled_hooks = []
409 active_hook_keys = [
413 active_hook_keys = [
410 key for section, key, value, active in ui_settings
414 key for section, key, value, active in ui_settings
411 if section == 'hooks' and active]
415 if section == 'hooks' and active]
412
416
413 hook_names = {
417 hook_names = {
414 RhodeCodeUi.HOOK_PUSH: 'push',
418 RhodeCodeUi.HOOK_PUSH: 'push',
415 RhodeCodeUi.HOOK_PULL: 'pull',
419 RhodeCodeUi.HOOK_PULL: 'pull',
416 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
420 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
417 }
421 }
418
422
419 for key in active_hook_keys:
423 for key in active_hook_keys:
420 hook = hook_names.get(key)
424 hook = hook_names.get(key)
421 if hook:
425 if hook:
422 enabled_hooks.append(hook)
426 enabled_hooks.append(hook)
423
427
424 return enabled_hooks
428 return enabled_hooks
425
429
426
430
427 def set_rhodecode_config(config):
431 def set_rhodecode_config(config):
428 """
432 """
429 Updates pyramid config with new settings from database
433 Updates pyramid config with new settings from database
430
434
431 :param config:
435 :param config:
432 """
436 """
433 from rhodecode.model.settings import SettingsModel
437 from rhodecode.model.settings import SettingsModel
434 app_settings = SettingsModel().get_all_settings()
438 app_settings = SettingsModel().get_all_settings()
435
439
436 for k, v in app_settings.items():
440 for k, v in app_settings.items():
437 config[k] = v
441 config[k] = v
438
442
439
443
440 def get_rhodecode_realm():
444 def get_rhodecode_realm():
441 """
445 """
442 Return the rhodecode realm from database.
446 Return the rhodecode realm from database.
443 """
447 """
444 from rhodecode.model.settings import SettingsModel
448 from rhodecode.model.settings import SettingsModel
445 realm = SettingsModel().get_setting_by_name('realm')
449 realm = SettingsModel().get_setting_by_name('realm')
446 return safe_str(realm.app_settings_value)
450 return safe_str(realm.app_settings_value)
447
451
448
452
449 def get_rhodecode_base_path():
453 def get_rhodecode_base_path():
450 """
454 """
451 Returns the base path. The base path is the filesystem path which points
455 Returns the base path. The base path is the filesystem path which points
452 to the repository store.
456 to the repository store.
453 """
457 """
454 from rhodecode.model.settings import SettingsModel
458 from rhodecode.model.settings import SettingsModel
455 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
459 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
456 return safe_str(paths_ui.ui_value)
460 return safe_str(paths_ui.ui_value)
457
461
458
462
459 def map_groups(path):
463 def map_groups(path):
460 """
464 """
461 Given a full path to a repository, create all nested groups that this
465 Given a full path to a repository, create all nested groups that this
462 repo is inside. This function creates parent-child relationships between
466 repo is inside. This function creates parent-child relationships between
463 groups and creates default perms for all new groups.
467 groups and creates default perms for all new groups.
464
468
465 :param paths: full path to repository
469 :param paths: full path to repository
466 """
470 """
467 from rhodecode.model.repo_group import RepoGroupModel
471 from rhodecode.model.repo_group import RepoGroupModel
468 sa = meta.Session()
472 sa = meta.Session()
469 groups = path.split(Repository.NAME_SEP)
473 groups = path.split(Repository.NAME_SEP)
470 parent = None
474 parent = None
471 group = None
475 group = None
472
476
473 # last element is repo in nested groups structure
477 # last element is repo in nested groups structure
474 groups = groups[:-1]
478 groups = groups[:-1]
475 rgm = RepoGroupModel(sa)
479 rgm = RepoGroupModel(sa)
476 owner = User.get_first_super_admin()
480 owner = User.get_first_super_admin()
477 for lvl, group_name in enumerate(groups):
481 for lvl, group_name in enumerate(groups):
478 group_name = '/'.join(groups[:lvl] + [group_name])
482 group_name = '/'.join(groups[:lvl] + [group_name])
479 group = RepoGroup.get_by_group_name(group_name)
483 group = RepoGroup.get_by_group_name(group_name)
480 desc = '%s group' % group_name
484 desc = '%s group' % group_name
481
485
482 # skip folders that are now removed repos
486 # skip folders that are now removed repos
483 if REMOVED_REPO_PAT.match(group_name):
487 if REMOVED_REPO_PAT.match(group_name):
484 break
488 break
485
489
486 if group is None:
490 if group is None:
487 log.debug('creating group level: %s group_name: %s',
491 log.debug('creating group level: %s group_name: %s',
488 lvl, group_name)
492 lvl, group_name)
489 group = RepoGroup(group_name, parent)
493 group = RepoGroup(group_name, parent)
490 group.group_description = desc
494 group.group_description = desc
491 group.user = owner
495 group.user = owner
492 sa.add(group)
496 sa.add(group)
493 perm_obj = rgm._create_default_perms(group)
497 perm_obj = rgm._create_default_perms(group)
494 sa.add(perm_obj)
498 sa.add(perm_obj)
495 sa.flush()
499 sa.flush()
496
500
497 parent = group
501 parent = group
498 return group
502 return group
499
503
500
504
501 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
505 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
502 """
506 """
503 maps all repos given in initial_repo_list, non existing repositories
507 maps all repos given in initial_repo_list, non existing repositories
504 are created, if remove_obsolete is True it also checks for db entries
508 are created, if remove_obsolete is True it also checks for db entries
505 that are not in initial_repo_list and removes them.
509 that are not in initial_repo_list and removes them.
506
510
507 :param initial_repo_list: list of repositories found by scanning methods
511 :param initial_repo_list: list of repositories found by scanning methods
508 :param remove_obsolete: check for obsolete entries in database
512 :param remove_obsolete: check for obsolete entries in database
509 """
513 """
510 from rhodecode.model.repo import RepoModel
514 from rhodecode.model.repo import RepoModel
511 from rhodecode.model.scm import ScmModel
515 from rhodecode.model.scm import ScmModel
512 from rhodecode.model.repo_group import RepoGroupModel
516 from rhodecode.model.repo_group import RepoGroupModel
513 from rhodecode.model.settings import SettingsModel
517 from rhodecode.model.settings import SettingsModel
514
518
515 sa = meta.Session()
519 sa = meta.Session()
516 repo_model = RepoModel()
520 repo_model = RepoModel()
517 user = User.get_first_super_admin()
521 user = User.get_first_super_admin()
518 added = []
522 added = []
519
523
520 # creation defaults
524 # creation defaults
521 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
525 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
522 enable_statistics = defs.get('repo_enable_statistics')
526 enable_statistics = defs.get('repo_enable_statistics')
523 enable_locking = defs.get('repo_enable_locking')
527 enable_locking = defs.get('repo_enable_locking')
524 enable_downloads = defs.get('repo_enable_downloads')
528 enable_downloads = defs.get('repo_enable_downloads')
525 private = defs.get('repo_private')
529 private = defs.get('repo_private')
526
530
527 for name, repo in initial_repo_list.items():
531 for name, repo in initial_repo_list.items():
528 group = map_groups(name)
532 group = map_groups(name)
529 unicode_name = safe_unicode(name)
533 unicode_name = safe_unicode(name)
530 db_repo = repo_model.get_by_repo_name(unicode_name)
534 db_repo = repo_model.get_by_repo_name(unicode_name)
531 # found repo that is on filesystem not in RhodeCode database
535 # found repo that is on filesystem not in RhodeCode database
532 if not db_repo:
536 if not db_repo:
533 log.info('repository %s not found, creating now', name)
537 log.info('repository %s not found, creating now', name)
534 added.append(name)
538 added.append(name)
535 desc = (repo.description
539 desc = (repo.description
536 if repo.description != 'unknown'
540 if repo.description != 'unknown'
537 else '%s repository' % name)
541 else '%s repository' % name)
538
542
539 db_repo = repo_model._create_repo(
543 db_repo = repo_model._create_repo(
540 repo_name=name,
544 repo_name=name,
541 repo_type=repo.alias,
545 repo_type=repo.alias,
542 description=desc,
546 description=desc,
543 repo_group=getattr(group, 'group_id', None),
547 repo_group=getattr(group, 'group_id', None),
544 owner=user,
548 owner=user,
545 enable_locking=enable_locking,
549 enable_locking=enable_locking,
546 enable_downloads=enable_downloads,
550 enable_downloads=enable_downloads,
547 enable_statistics=enable_statistics,
551 enable_statistics=enable_statistics,
548 private=private,
552 private=private,
549 state=Repository.STATE_CREATED
553 state=Repository.STATE_CREATED
550 )
554 )
551 sa.commit()
555 sa.commit()
552 # we added that repo just now, and make sure we updated server info
556 # we added that repo just now, and make sure we updated server info
553 if db_repo.repo_type == 'git':
557 if db_repo.repo_type == 'git':
554 git_repo = db_repo.scm_instance()
558 git_repo = db_repo.scm_instance()
555 # update repository server-info
559 # update repository server-info
556 log.debug('Running update server info')
560 log.debug('Running update server info')
557 git_repo._update_server_info()
561 git_repo._update_server_info()
558
562
559 db_repo.update_commit_cache()
563 db_repo.update_commit_cache()
560
564
561 config = db_repo._config
565 config = db_repo._config
562 config.set('extensions', 'largefiles', '')
566 config.set('extensions', 'largefiles', '')
563 ScmModel().install_hooks(
567 ScmModel().install_hooks(
564 db_repo.scm_instance(config=config),
568 db_repo.scm_instance(config=config),
565 repo_type=db_repo.repo_type)
569 repo_type=db_repo.repo_type)
566
570
567 removed = []
571 removed = []
568 if remove_obsolete:
572 if remove_obsolete:
569 # remove from database those repositories that are not in the filesystem
573 # remove from database those repositories that are not in the filesystem
570 for repo in sa.query(Repository).all():
574 for repo in sa.query(Repository).all():
571 if repo.repo_name not in initial_repo_list.keys():
575 if repo.repo_name not in initial_repo_list.keys():
572 log.debug("Removing non-existing repository found in db `%s`",
576 log.debug("Removing non-existing repository found in db `%s`",
573 repo.repo_name)
577 repo.repo_name)
574 try:
578 try:
575 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
579 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
576 sa.commit()
580 sa.commit()
577 removed.append(repo.repo_name)
581 removed.append(repo.repo_name)
578 except Exception:
582 except Exception:
579 # don't hold further removals on error
583 # don't hold further removals on error
580 log.error(traceback.format_exc())
584 log.error(traceback.format_exc())
581 sa.rollback()
585 sa.rollback()
582
586
583 def splitter(full_repo_name):
587 def splitter(full_repo_name):
584 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
588 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
585 gr_name = None
589 gr_name = None
586 if len(_parts) == 2:
590 if len(_parts) == 2:
587 gr_name = _parts[0]
591 gr_name = _parts[0]
588 return gr_name
592 return gr_name
589
593
590 initial_repo_group_list = [splitter(x) for x in
594 initial_repo_group_list = [splitter(x) for x in
591 initial_repo_list.keys() if splitter(x)]
595 initial_repo_list.keys() if splitter(x)]
592
596
593 # remove from database those repository groups that are not in the
597 # remove from database those repository groups that are not in the
594 # filesystem due to parent child relationships we need to delete them
598 # filesystem due to parent child relationships we need to delete them
595 # in a specific order of most nested first
599 # in a specific order of most nested first
596 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
600 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
597 nested_sort = lambda gr: len(gr.split('/'))
601 nested_sort = lambda gr: len(gr.split('/'))
598 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
602 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
599 if group_name not in initial_repo_group_list:
603 if group_name not in initial_repo_group_list:
600 repo_group = RepoGroup.get_by_group_name(group_name)
604 repo_group = RepoGroup.get_by_group_name(group_name)
601 if (repo_group.children.all() or
605 if (repo_group.children.all() or
602 not RepoGroupModel().check_exist_filesystem(
606 not RepoGroupModel().check_exist_filesystem(
603 group_name=group_name, exc_on_failure=False)):
607 group_name=group_name, exc_on_failure=False)):
604 continue
608 continue
605
609
606 log.info(
610 log.info(
607 'Removing non-existing repository group found in db `%s`',
611 'Removing non-existing repository group found in db `%s`',
608 group_name)
612 group_name)
609 try:
613 try:
610 RepoGroupModel(sa).delete(group_name, fs_remove=False)
614 RepoGroupModel(sa).delete(group_name, fs_remove=False)
611 sa.commit()
615 sa.commit()
612 removed.append(group_name)
616 removed.append(group_name)
613 except Exception:
617 except Exception:
614 # don't hold further removals on error
618 # don't hold further removals on error
615 log.exception(
619 log.exception(
616 'Unable to remove repository group `%s`',
620 'Unable to remove repository group `%s`',
617 group_name)
621 group_name)
618 sa.rollback()
622 sa.rollback()
619 raise
623 raise
620
624
621 return added, removed
625 return added, removed
622
626
623
627
624 def load_rcextensions(root_path):
628 def load_rcextensions(root_path):
625 import rhodecode
629 import rhodecode
626 from rhodecode.config import conf
630 from rhodecode.config import conf
627
631
628 path = os.path.join(root_path, 'rcextensions', '__init__.py')
632 path = os.path.join(root_path, 'rcextensions', '__init__.py')
629 if os.path.isfile(path):
633 if os.path.isfile(path):
630 rcext = create_module('rc', path)
634 rcext = create_module('rc', path)
631 EXT = rhodecode.EXTENSIONS = rcext
635 EXT = rhodecode.EXTENSIONS = rcext
632 log.debug('Found rcextensions now loading %s...', rcext)
636 log.debug('Found rcextensions now loading %s...', rcext)
633
637
634 # Additional mappings that are not present in the pygments lexers
638 # Additional mappings that are not present in the pygments lexers
635 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
639 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
636
640
637 # auto check if the module is not missing any data, set to default if is
641 # auto check if the module is not missing any data, set to default if is
638 # this will help autoupdate new feature of rcext module
642 # this will help autoupdate new feature of rcext module
639 #from rhodecode.config import rcextensions
643 #from rhodecode.config import rcextensions
640 #for k in dir(rcextensions):
644 #for k in dir(rcextensions):
641 # if not k.startswith('_') and not hasattr(EXT, k):
645 # if not k.startswith('_') and not hasattr(EXT, k):
642 # setattr(EXT, k, getattr(rcextensions, k))
646 # setattr(EXT, k, getattr(rcextensions, k))
643
647
644
648
645 def get_custom_lexer(extension):
649 def get_custom_lexer(extension):
646 """
650 """
647 returns a custom lexer if it is defined in rcextensions module, or None
651 returns a custom lexer if it is defined in rcextensions module, or None
648 if there's no custom lexer defined
652 if there's no custom lexer defined
649 """
653 """
650 import rhodecode
654 import rhodecode
651 from pygments import lexers
655 from pygments import lexers
652
656
653 # custom override made by RhodeCode
657 # custom override made by RhodeCode
654 if extension in ['mako']:
658 if extension in ['mako']:
655 return lexers.get_lexer_by_name('html+mako')
659 return lexers.get_lexer_by_name('html+mako')
656
660
657 # check if we didn't define this extension as other lexer
661 # check if we didn't define this extension as other lexer
658 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
662 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
659 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
663 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
660 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
664 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
661 return lexers.get_lexer_by_name(_lexer_name)
665 return lexers.get_lexer_by_name(_lexer_name)
662
666
663
667
664 #==============================================================================
668 #==============================================================================
665 # TEST FUNCTIONS AND CREATORS
669 # TEST FUNCTIONS AND CREATORS
666 #==============================================================================
670 #==============================================================================
667 def create_test_index(repo_location, config):
671 def create_test_index(repo_location, config):
668 """
672 """
669 Makes default test index.
673 Makes default test index.
670 """
674 """
671 import rc_testdata
675 import rc_testdata
672
676
673 rc_testdata.extract_search_index(
677 rc_testdata.extract_search_index(
674 'vcs_search_index', os.path.dirname(config['search.location']))
678 'vcs_search_index', os.path.dirname(config['search.location']))
675
679
676
680
677 def create_test_directory(test_path):
681 def create_test_directory(test_path):
678 """
682 """
679 Create test directory if it doesn't exist.
683 Create test directory if it doesn't exist.
680 """
684 """
681 if not os.path.isdir(test_path):
685 if not os.path.isdir(test_path):
682 log.debug('Creating testdir %s', test_path)
686 log.debug('Creating testdir %s', test_path)
683 os.makedirs(test_path)
687 os.makedirs(test_path)
684
688
685
689
686 def create_test_database(test_path, config):
690 def create_test_database(test_path, config):
687 """
691 """
688 Makes a fresh database.
692 Makes a fresh database.
689 """
693 """
690 from rhodecode.lib.db_manage import DbManage
694 from rhodecode.lib.db_manage import DbManage
691
695
692 # PART ONE create db
696 # PART ONE create db
693 dbconf = config['sqlalchemy.db1.url']
697 dbconf = config['sqlalchemy.db1.url']
694 log.debug('making test db %s', dbconf)
698 log.debug('making test db %s', dbconf)
695
699
696 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
700 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
697 tests=True, cli_args={'force_ask': True})
701 tests=True, cli_args={'force_ask': True})
698 dbmanage.create_tables(override=True)
702 dbmanage.create_tables(override=True)
699 dbmanage.set_db_version()
703 dbmanage.set_db_version()
700 # for tests dynamically set new root paths based on generated content
704 # for tests dynamically set new root paths based on generated content
701 dbmanage.create_settings(dbmanage.config_prompt(test_path))
705 dbmanage.create_settings(dbmanage.config_prompt(test_path))
702 dbmanage.create_default_user()
706 dbmanage.create_default_user()
703 dbmanage.create_test_admin_and_users()
707 dbmanage.create_test_admin_and_users()
704 dbmanage.create_permissions()
708 dbmanage.create_permissions()
705 dbmanage.populate_default_permissions()
709 dbmanage.populate_default_permissions()
706 Session().commit()
710 Session().commit()
707
711
708
712
709 def create_test_repositories(test_path, config):
713 def create_test_repositories(test_path, config):
710 """
714 """
711 Creates test repositories in the temporary directory. Repositories are
715 Creates test repositories in the temporary directory. Repositories are
712 extracted from archives within the rc_testdata package.
716 extracted from archives within the rc_testdata package.
713 """
717 """
714 import rc_testdata
718 import rc_testdata
715 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
719 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
716
720
717 log.debug('making test vcs repositories')
721 log.debug('making test vcs repositories')
718
722
719 idx_path = config['search.location']
723 idx_path = config['search.location']
720 data_path = config['cache_dir']
724 data_path = config['cache_dir']
721
725
722 # clean index and data
726 # clean index and data
723 if idx_path and os.path.exists(idx_path):
727 if idx_path and os.path.exists(idx_path):
724 log.debug('remove %s', idx_path)
728 log.debug('remove %s', idx_path)
725 shutil.rmtree(idx_path)
729 shutil.rmtree(idx_path)
726
730
727 if data_path and os.path.exists(data_path):
731 if data_path and os.path.exists(data_path):
728 log.debug('remove %s', data_path)
732 log.debug('remove %s', data_path)
729 shutil.rmtree(data_path)
733 shutil.rmtree(data_path)
730
734
731 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
735 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
732 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
736 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
733
737
734 # Note: Subversion is in the process of being integrated with the system,
738 # Note: Subversion is in the process of being integrated with the system,
735 # until we have a properly packed version of the test svn repository, this
739 # until we have a properly packed version of the test svn repository, this
736 # tries to copy over the repo from a package "rc_testdata"
740 # tries to copy over the repo from a package "rc_testdata"
737 svn_repo_path = rc_testdata.get_svn_repo_archive()
741 svn_repo_path = rc_testdata.get_svn_repo_archive()
738 with tarfile.open(svn_repo_path) as tar:
742 with tarfile.open(svn_repo_path) as tar:
739 tar.extractall(jn(test_path, SVN_REPO))
743 tar.extractall(jn(test_path, SVN_REPO))
740
744
741
745
742 def password_changed(auth_user, session):
746 def password_changed(auth_user, session):
743 # Never report password change in case of default user or anonymous user.
747 # Never report password change in case of default user or anonymous user.
744 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
748 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
745 return False
749 return False
746
750
747 password_hash = md5(auth_user.password) if auth_user.password else None
751 password_hash = md5(auth_user.password) if auth_user.password else None
748 rhodecode_user = session.get('rhodecode_user', {})
752 rhodecode_user = session.get('rhodecode_user', {})
749 session_password_hash = rhodecode_user.get('password', '')
753 session_password_hash = rhodecode_user.get('password', '')
750 return password_hash != session_password_hash
754 return password_hash != session_password_hash
751
755
752
756
753 def read_opensource_licenses():
757 def read_opensource_licenses():
754 global _license_cache
758 global _license_cache
755
759
756 if not _license_cache:
760 if not _license_cache:
757 licenses = pkg_resources.resource_string(
761 licenses = pkg_resources.resource_string(
758 'rhodecode', 'config/licenses.json')
762 'rhodecode', 'config/licenses.json')
759 _license_cache = json.loads(licenses)
763 _license_cache = json.loads(licenses)
760
764
761 return _license_cache
765 return _license_cache
762
766
763
767
764 def generate_platform_uuid():
768 def generate_platform_uuid():
765 """
769 """
766 Generates platform UUID based on it's name
770 Generates platform UUID based on it's name
767 """
771 """
768 import platform
772 import platform
769
773
770 try:
774 try:
771 uuid_list = [platform.platform()]
775 uuid_list = [platform.platform()]
772 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
776 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
773 except Exception as e:
777 except Exception as e:
774 log.error('Failed to generate host uuid: %s' % e)
778 log.error('Failed to generate host uuid: %s' % e)
775 return 'UNDEFINED'
779 return 'UNDEFINED'
@@ -1,146 +1,146 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-heading">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('Repository Group Permissions')}</h3>
5 <h3 class="panel-title">${_('Repository Group Permissions')}</h3>
6 </div>
6 </div>
7 <div class="panel-body">
7 <div class="panel-body">
8 ${h.secure_form(h.route_path('edit_repo_group_perms_update', repo_group_name=c.repo_group.group_name), request=request)}
8 ${h.secure_form(h.route_path('edit_repo_group_perms_update', repo_group_name=c.repo_group.group_name), request=request)}
9 <table id="permissions_manage" class="rctable permissions">
9 <table id="permissions_manage" class="rctable permissions">
10 <tr>
10 <tr>
11 <th class="td-radio">${_('None')}</th>
11 <th class="td-radio">${_('None')}</th>
12 <th class="td-radio">${_('Read')}</th>
12 <th class="td-radio">${_('Read')}</th>
13 <th class="td-radio">${_('Write')}</th>
13 <th class="td-radio">${_('Write')}</th>
14 <th class="td-radio">${_('Admin')}</th>
14 <th class="td-radio">${_('Admin')}</th>
15 <th class="td-owner">${_('User/User Group')}</th>
15 <th class="td-owner">${_('User/User Group')}</th>
16 <th></th>
16 <th></th>
17 </tr>
17 </tr>
18 ## USERS
18 ## USERS
19 %for _user in c.repo_group.permissions():
19 %for _user in c.repo_group.permissions():
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
21 <tr class="perm_admin_row">
21 <tr class="perm_admin_row">
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
26 <td class="td-user">
26 <td class="td-user">
27 ${base.gravatar(_user.email, 16)}
27 ${base.gravatar(_user.email, 16)}
28 ${h.link_to_user(_user.username)}
28 ${h.link_to_user(_user.username)}
29 %if getattr(_user, 'admin_row', None):
29 %if getattr(_user, 'admin_row', None):
30 (${_('super admin')})
30 (${_('super admin')})
31 %endif
31 %endif
32 %if getattr(_user, 'owner_row', None):
32 %if getattr(_user, 'owner_row', None):
33 (${_('owner')})
33 (${_('owner')})
34 %endif
34 %endif
35 </td>
35 </td>
36 <td></td>
36 <td></td>
37 </tr>
37 </tr>
38 %else:
38 %else:
39 <tr>
39 <tr>
40 ##forbid revoking permission from yourself, except if you're an super admin
40 ##forbid revoking permission from yourself, except if you're an super admin
41 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
41 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
42 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', checked=_user.permission=='group.none')}</td>
42 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', checked=_user.permission=='group.none')}</td>
43 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', checked=_user.permission=='group.read')}</td>
43 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', checked=_user.permission=='group.read')}</td>
44 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', checked=_user.permission=='group.write')}</td>
44 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', checked=_user.permission=='group.write')}</td>
45 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', checked=_user.permission=='group.admin')}</td>
45 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', checked=_user.permission=='group.admin')}</td>
46 <td class="td-user">
46 <td class="td-user">
47 ${base.gravatar(_user.email, 16)}
47 ${base.gravatar(_user.email, 16)}
48 <span class="user">
48 <span class="user">
49 % if _user.username == h.DEFAULT_USER:
49 % if _user.username == h.DEFAULT_USER:
50 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
50 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
51 % else:
51 % else:
52 ${h.link_to_user(_user.username)}
52 ${h.link_to_user(_user.username)}
53 % endif
53 % endif
54 </span>
54 </span>
55 </td>
55 </td>
56 <td class="td-action">
56 <td class="td-action">
57 %if _user.username != h.DEFAULT_USER:
57 %if _user.username != h.DEFAULT_USER:
58 <span class="btn btn-link btn-danger revoke_perm"
58 <span class="btn btn-link btn-danger revoke_perm"
59 member="${_user.user_id}" member_type="user">
59 member="${_user.user_id}" member_type="user">
60 <i class="icon-remove"></i> ${_('Revoke')}
60 <i class="icon-remove"></i> ${_('Revoke')}
61 </span>
61 </span>
62 %endif
62 %endif
63 </td>
63 </td>
64 %else:
64 %else:
65 ## special case for current user permissions, we make sure he cannot take his own permissions
65 ## special case for current user permissions, we make sure he cannot take his own permissions
66 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', disabled="disabled")}</td>
66 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', disabled="disabled")}</td>
67 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', disabled="disabled")}</td>
67 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', disabled="disabled")}</td>
68 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', disabled="disabled")}</td>
68 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', disabled="disabled")}</td>
69 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', disabled="disabled")}</td>
69 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', disabled="disabled")}</td>
70 <td class="td-user">
70 <td class="td-user">
71 ${base.gravatar(_user.email, 16)}
71 ${base.gravatar(_user.email, 16)}
72 <span class="user">
72 <span class="user">
73 % if _user.username == h.DEFAULT_USER:
73 % if _user.username == h.DEFAULT_USER:
74 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
74 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
75 % else:
75 % else:
76 ${h.link_to_user(_user.username)}
76 ${h.link_to_user(_user.username)}
77 % endif
77 % endif
78 <span class="user-perm-help-text">(${_('delegated admin')})</span>
78 <span class="user-perm-help-text">(${_('delegated admin')})</span>
79 </span>
79 </span>
80 </td>
80 </td>
81 <td></td>
81 <td></td>
82 %endif
82 %endif
83 </tr>
83 </tr>
84 %endif
84 %endif
85 %endfor
85 %endfor
86
86
87 ## USER GROUPS
87 ## USER GROUPS
88 %for _user_group in c.repo_group.permission_user_groups():
88 %for _user_group in c.repo_group.permission_user_groups():
89 <tr id="id${id(_user_group.users_group_name)}">
89 <tr id="id${id(_user_group.users_group_name)}">
90 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.none', checked=_user_group.permission=='group.none')}</td>
90 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.none', checked=_user_group.permission=='group.none')}</td>
91 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.read', checked=_user_group.permission=='group.read')}</td>
91 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.read', checked=_user_group.permission=='group.read')}</td>
92 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.write', checked=_user_group.permission=='group.write')}</td>
92 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.write', checked=_user_group.permission=='group.write')}</td>
93 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.admin', checked=_user_group.permission=='group.admin')}</td>
93 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.admin', checked=_user_group.permission=='group.admin')}</td>
94 <td class="td-componentname">
94 <td class="td-componentname">
95 <i class="icon-group" ></i>
95 <i class="icon-group" ></i>
96 %if h.HasPermissionAny('hg.admin')():
96 %if h.HasPermissionAny('hg.admin')():
97 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
97 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
98 ${_user_group.users_group_name}
98 ${_user_group.users_group_name}
99 </a>
99 </a>
100 %else:
100 %else:
101 ${_user_group.users_group_name}
101 ${h.link_to_group(_user_group.users_group_name)}
102 %endif
102 %endif
103 </td>
103 </td>
104 <td class="td-action">
104 <td class="td-action">
105 <span class="btn btn-link btn-danger revoke_perm"
105 <span class="btn btn-link btn-danger revoke_perm"
106 member="${_user_group.users_group_id}" member_type="user_group">
106 member="${_user_group.users_group_id}" member_type="user_group">
107 <i class="icon-remove"></i> ${_('Revoke')}
107 <i class="icon-remove"></i> ${_('Revoke')}
108 </span>
108 </span>
109 </td>
109 </td>
110 </tr>
110 </tr>
111 %endfor
111 %endfor
112
112
113 <tr class="new_members" id="add_perm_input"></tr>
113 <tr class="new_members" id="add_perm_input"></tr>
114 </table>
114 </table>
115 <div id="add_perm" class="link">
115 <div id="add_perm" class="link">
116 ${_('Add new')}
116 ${_('Add new')}
117 </div>
117 </div>
118 <div class="fields">
118 <div class="fields">
119 <div class="field">
119 <div class="field">
120 <div class="label label-radio">
120 <div class="label label-radio">
121 ${_('Apply to children')}:
121 ${_('Apply to children')}:
122 </div>
122 </div>
123 <div class="radios">
123 <div class="radios">
124 ${h.radio('recursive', 'none', label=_('None'), checked="checked")}
124 ${h.radio('recursive', 'none', label=_('None'), checked="checked")}
125 ${h.radio('recursive', 'groups', label=_('Repository Groups'))}
125 ${h.radio('recursive', 'groups', label=_('Repository Groups'))}
126 ${h.radio('recursive', 'repos', label=_('Repositories'))}
126 ${h.radio('recursive', 'repos', label=_('Repositories'))}
127 ${h.radio('recursive', 'all', label=_('Both'))}
127 ${h.radio('recursive', 'all', label=_('Both'))}
128 <span class="help-block">${_('Set or revoke permissions to selected types of children of this group, including non-private repositories and other groups if chosen.')}</span>
128 <span class="help-block">${_('Set or revoke permissions to selected types of children of this group, including non-private repositories and other groups if chosen.')}</span>
129 </div>
129 </div>
130 </div>
130 </div>
131 </div>
131 </div>
132 <div class="buttons">
132 <div class="buttons">
133 ${h.submit('save',_('Save'),class_="btn btn-primary")}
133 ${h.submit('save',_('Save'),class_="btn btn-primary")}
134 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
134 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
135 </div>
135 </div>
136 ${h.end_form()}
136 ${h.end_form()}
137 </div>
137 </div>
138 </div>
138 </div>
139 <script type="text/javascript">
139 <script type="text/javascript">
140 $('#add_perm').on('click', function(e){
140 $('#add_perm').on('click', function(e){
141 addNewPermInput($(this), 'group');
141 addNewPermInput($(this), 'group');
142 });
142 });
143 $('.revoke_perm').on('click', function(e){
143 $('.revoke_perm').on('click', function(e){
144 markRevokePermInput($(this), 'group');
144 markRevokePermInput($(this), 'group');
145 })
145 })
146 </script>
146 </script>
@@ -1,123 +1,123 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-heading">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('Repository Permissions')}</h3>
5 <h3 class="panel-title">${_('Repository Permissions')}</h3>
6 </div>
6 </div>
7 <div class="panel-body">
7 <div class="panel-body">
8 ${h.secure_form(h.route_path('edit_repo_perms', repo_name=c.repo_name), request=request)}
8 ${h.secure_form(h.route_path('edit_repo_perms', repo_name=c.repo_name), request=request)}
9 <table id="permissions_manage" class="rctable permissions">
9 <table id="permissions_manage" class="rctable permissions">
10 <tr>
10 <tr>
11 <th class="td-radio">${_('None')}</th>
11 <th class="td-radio">${_('None')}</th>
12 <th class="td-radio">${_('Read')}</th>
12 <th class="td-radio">${_('Read')}</th>
13 <th class="td-radio">${_('Write')}</th>
13 <th class="td-radio">${_('Write')}</th>
14 <th class="td-radio">${_('Admin')}</th>
14 <th class="td-radio">${_('Admin')}</th>
15 <th class="td-owner">${_('User/User Group')}</th>
15 <th class="td-owner">${_('User/User Group')}</th>
16 <th></th>
16 <th></th>
17 </tr>
17 </tr>
18 ## USERS
18 ## USERS
19 %for _user in c.rhodecode_db_repo.permissions():
19 %for _user in c.rhodecode_db_repo.permissions():
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
21 <tr class="perm_admin_row">
21 <tr class="perm_admin_row">
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
26 <td class="td-user">
26 <td class="td-user">
27 ${base.gravatar(_user.email, 16)}
27 ${base.gravatar(_user.email, 16)}
28 ${h.link_to_user(_user.username)}
28 ${h.link_to_user(_user.username)}
29 %if getattr(_user, 'admin_row', None):
29 %if getattr(_user, 'admin_row', None):
30 (${_('super admin')})
30 (${_('super admin')})
31 %endif
31 %endif
32 %if getattr(_user, 'owner_row', None):
32 %if getattr(_user, 'owner_row', None):
33 (${_('owner')})
33 (${_('owner')})
34 %endif
34 %endif
35 </td>
35 </td>
36 <td></td>
36 <td></td>
37 </tr>
37 </tr>
38 %elif _user.username == h.DEFAULT_USER and c.rhodecode_db_repo.private:
38 %elif _user.username == h.DEFAULT_USER and c.rhodecode_db_repo.private:
39 <tr>
39 <tr>
40 <td colspan="4">
40 <td colspan="4">
41 <span class="private_repo_msg">
41 <span class="private_repo_msg">
42 <strong title="${h.tooltip(_user.permission)}">${_('private repository')}</strong>
42 <strong title="${h.tooltip(_user.permission)}">${_('private repository')}</strong>
43 </span>
43 </span>
44 </td>
44 </td>
45 <td class="private_repo_msg">
45 <td class="private_repo_msg">
46 ${base.gravatar(h.DEFAULT_USER_EMAIL, 16)}
46 ${base.gravatar(h.DEFAULT_USER_EMAIL, 16)}
47 ${h.DEFAULT_USER} - ${_('only users/user groups explicitly added here will have access')}</td>
47 ${h.DEFAULT_USER} - ${_('only users/user groups explicitly added here will have access')}</td>
48 <td></td>
48 <td></td>
49 </tr>
49 </tr>
50 %else:
50 %else:
51 <tr>
51 <tr>
52 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.none', checked=_user.permission=='repository.none')}</td>
52 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.none', checked=_user.permission=='repository.none')}</td>
53 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.read', checked=_user.permission=='repository.read')}</td>
53 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.read', checked=_user.permission=='repository.read')}</td>
54 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.write', checked=_user.permission=='repository.write')}</td>
54 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.write', checked=_user.permission=='repository.write')}</td>
55 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.admin', checked=_user.permission=='repository.admin')}</td>
55 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.admin', checked=_user.permission=='repository.admin')}</td>
56 <td class="td-user">
56 <td class="td-user">
57 ${base.gravatar(_user.email, 16)}
57 ${base.gravatar(_user.email, 16)}
58 <span class="user">
58 <span class="user">
59 % if _user.username == h.DEFAULT_USER:
59 % if _user.username == h.DEFAULT_USER:
60 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
60 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
61 % else:
61 % else:
62 ${h.link_to_user(_user.username)}
62 ${h.link_to_user(_user.username)}
63 % endif
63 % endif
64 </span>
64 </span>
65 </td>
65 </td>
66 <td class="td-action">
66 <td class="td-action">
67 %if _user.username != h.DEFAULT_USER:
67 %if _user.username != h.DEFAULT_USER:
68 <span class="btn btn-link btn-danger revoke_perm"
68 <span class="btn btn-link btn-danger revoke_perm"
69 member="${_user.user_id}" member_type="user">
69 member="${_user.user_id}" member_type="user">
70 <i class="icon-remove"></i> ${_('Revoke')}
70 <i class="icon-remove"></i> ${_('Revoke')}
71 </span>
71 </span>
72 %endif
72 %endif
73 </td>
73 </td>
74 </tr>
74 </tr>
75 %endif
75 %endif
76 %endfor
76 %endfor
77
77
78 ## USER GROUPS
78 ## USER GROUPS
79 %for _user_group in c.rhodecode_db_repo.permission_user_groups():
79 %for _user_group in c.rhodecode_db_repo.permission_user_groups():
80 <tr>
80 <tr>
81 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.none', checked=_user_group.permission=='repository.none')}</td>
81 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.none', checked=_user_group.permission=='repository.none')}</td>
82 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.read', checked=_user_group.permission=='repository.read')}</td>
82 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.read', checked=_user_group.permission=='repository.read')}</td>
83 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.write', checked=_user_group.permission=='repository.write')}</td>
83 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.write', checked=_user_group.permission=='repository.write')}</td>
84 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.admin', checked=_user_group.permission=='repository.admin')}</td>
84 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.admin', checked=_user_group.permission=='repository.admin')}</td>
85 <td class="td-componentname">
85 <td class="td-componentname">
86 <i class="icon-group" ></i>
86 <i class="icon-group" ></i>
87 %if h.HasPermissionAny('hg.admin')():
87 %if h.HasPermissionAny('hg.admin')():
88 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
88 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
89 ${_user_group.users_group_name}
89 ${_user_group.users_group_name}
90 </a>
90 </a>
91 %else:
91 %else:
92 ${_user_group.users_group_name}
92 ${h.link_to_group(_user_group.users_group_name)}
93 %endif
93 %endif
94 </td>
94 </td>
95 <td class="td-action">
95 <td class="td-action">
96 <span class="btn btn-link btn-danger revoke_perm"
96 <span class="btn btn-link btn-danger revoke_perm"
97 member="${_user_group.users_group_id}" member_type="user_group">
97 member="${_user_group.users_group_id}" member_type="user_group">
98 <i class="icon-remove"></i> ${_('Revoke')}
98 <i class="icon-remove"></i> ${_('Revoke')}
99 </span>
99 </span>
100 </td>
100 </td>
101 </tr>
101 </tr>
102 %endfor
102 %endfor
103 <tr class="new_members" id="add_perm_input"></tr>
103 <tr class="new_members" id="add_perm_input"></tr>
104 </table>
104 </table>
105 <div id="add_perm" class="link">
105 <div id="add_perm" class="link">
106 ${_('Add new')}
106 ${_('Add new')}
107 </div>
107 </div>
108 <div class="buttons">
108 <div class="buttons">
109 ${h.submit('save',_('Save'),class_="btn btn-primary")}
109 ${h.submit('save',_('Save'),class_="btn btn-primary")}
110 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
110 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
111 </div>
111 </div>
112 ${h.end_form()}
112 ${h.end_form()}
113 </div>
113 </div>
114 </div>
114 </div>
115
115
116 <script type="text/javascript">
116 <script type="text/javascript">
117 $('#add_perm').on('click', function(e){
117 $('#add_perm').on('click', function(e){
118 addNewPermInput($(this), 'repository');
118 addNewPermInput($(this), 'repository');
119 });
119 });
120 $('.revoke_perm').on('click', function(e){
120 $('.revoke_perm').on('click', function(e){
121 markRevokePermInput($(this), 'repository');
121 markRevokePermInput($(this), 'repository');
122 });
122 });
123 </script>
123 </script>
@@ -1,134 +1,134 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-heading">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('User Group Permissions')}</h3>
5 <h3 class="panel-title">${_('User Group Permissions')}</h3>
6 </div>
6 </div>
7 <div class="panel-body">
7 <div class="panel-body">
8 ${h.secure_form(h.route_path('edit_user_group_perms_update', user_group_id=c.user_group.users_group_id), request=request)}
8 ${h.secure_form(h.route_path('edit_user_group_perms_update', user_group_id=c.user_group.users_group_id), request=request)}
9 <table id="permissions_manage" class="rctable permissions">
9 <table id="permissions_manage" class="rctable permissions">
10 <tr>
10 <tr>
11 <th class="td-radio">${_('None')}</th>
11 <th class="td-radio">${_('None')}</th>
12 <th class="td-radio">${_('Read')}</th>
12 <th class="td-radio">${_('Read')}</th>
13 <th class="td-radio">${_('Write')}</th>
13 <th class="td-radio">${_('Write')}</th>
14 <th class="td-radio">${_('Admin')}</th>
14 <th class="td-radio">${_('Admin')}</th>
15 <th>${_('User/User Group')}</th>
15 <th>${_('User/User Group')}</th>
16 <th></th>
16 <th></th>
17 </tr>
17 </tr>
18 ## USERS
18 ## USERS
19 %for _user in c.user_group.permissions():
19 %for _user in c.user_group.permissions():
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
20 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
21 <tr class="perm_admin_row">
21 <tr class="perm_admin_row">
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
22 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
26 <td class="td-user">
26 <td class="td-user">
27 ${base.gravatar(_user.email, 16)}
27 ${base.gravatar(_user.email, 16)}
28 <span class="user">
28 <span class="user">
29 ${h.link_to_user(_user.username)}
29 ${h.link_to_user(_user.username)}
30 %if getattr(_user, 'admin_row', None):
30 %if getattr(_user, 'admin_row', None):
31 (${_('super admin')})
31 (${_('super admin')})
32 %endif
32 %endif
33 %if getattr(_user, 'owner_row', None):
33 %if getattr(_user, 'owner_row', None):
34 (${_('owner')})
34 (${_('owner')})
35 %endif
35 %endif
36 </span>
36 </span>
37 </td>
37 </td>
38 <td></td>
38 <td></td>
39 </tr>
39 </tr>
40 %else:
40 %else:
41 ##forbid revoking permission from yourself, except if you're an super admin
41 ##forbid revoking permission from yourself, except if you're an super admin
42 <tr>
42 <tr>
43 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
43 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
44 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none')}</td>
44 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none')}</td>
45 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read')}</td>
45 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read')}</td>
46 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write')}</td>
46 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write')}</td>
47 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin')}</td>
47 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin')}</td>
48 <td class="td-user">
48 <td class="td-user">
49 ${base.gravatar(_user.email, 16)}
49 ${base.gravatar(_user.email, 16)}
50 <span class="user">
50 <span class="user">
51 % if _user.username == h.DEFAULT_USER:
51 % if _user.username == h.DEFAULT_USER:
52 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
52 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
53 % else:
53 % else:
54 ${h.link_to_user(_user.username)}
54 ${h.link_to_user(_user.username)}
55 % endif
55 % endif
56 </span>
56 </span>
57 </td>
57 </td>
58 <td class="td-action">
58 <td class="td-action">
59 %if _user.username != h.DEFAULT_USER:
59 %if _user.username != h.DEFAULT_USER:
60 <span class="btn btn-link btn-danger revoke_perm"
60 <span class="btn btn-link btn-danger revoke_perm"
61 member="${_user.user_id}" member_type="user">
61 member="${_user.user_id}" member_type="user">
62 <i class="icon-remove"></i> ${_('revoke')}
62 <i class="icon-remove"></i> ${_('revoke')}
63 </span>
63 </span>
64 %endif
64 %endif
65 </td>
65 </td>
66 %else:
66 %else:
67 ## special case for current user permissions, we make sure he cannot take his own permissions
67 ## special case for current user permissions, we make sure he cannot take his own permissions
68 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none', disabled="disabled")}</td>
68 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none', disabled="disabled")}</td>
69 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read', disabled="disabled")}</td>
69 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read', disabled="disabled")}</td>
70 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write', disabled="disabled")}</td>
70 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write', disabled="disabled")}</td>
71 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin', disabled="disabled")}</td>
71 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin', disabled="disabled")}</td>
72 <td class="td-user">
72 <td class="td-user">
73 ${base.gravatar(_user.email, 16)}
73 ${base.gravatar(_user.email, 16)}
74 <span class="user">
74 <span class="user">
75 % if _user.username == h.DEFAULT_USER:
75 % if _user.username == h.DEFAULT_USER:
76 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
76 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
77 % else:
77 % else:
78 ${h.link_to_user(_user.username)}
78 ${h.link_to_user(_user.username)}
79 % endif
79 % endif
80 <span class="user-perm-help-text">(${_('delegated admin')})</span>
80 <span class="user-perm-help-text">(${_('delegated admin')})</span>
81 </span>
81 </span>
82 </td>
82 </td>
83 <td></td>
83 <td></td>
84 %endif
84 %endif
85 </tr>
85 </tr>
86 %endif
86 %endif
87 %endfor
87 %endfor
88
88
89 ## USER GROUPS
89 ## USER GROUPS
90 %for _user_group in c.user_group.permission_user_groups():
90 %for _user_group in c.user_group.permission_user_groups():
91 <tr>
91 <tr>
92 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.none')}</td>
92 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.none')}</td>
93 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.read')}</td>
93 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.read')}</td>
94 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.write')}</td>
94 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.write')}</td>
95 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.admin')}</td>
95 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.admin')}</td>
96 <td class="td-user">
96 <td class="td-user">
97 <i class="icon-group" ></i>
97 <i class="icon-group" ></i>
98 %if h.HasPermissionAny('hg.admin')():
98 %if h.HasPermissionAny('hg.admin')():
99 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
99 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
100 ${_user_group.users_group_name}
100 ${_user_group.users_group_name}
101 </a>
101 </a>
102 %else:
102 %else:
103 ${_user_group.users_group_name}
103 ${h.link_to_group(_user_group.users_group_name)}
104 %endif
104 %endif
105 </td>
105 </td>
106 <td class="td-action">
106 <td class="td-action">
107 <span class="btn btn-link btn-danger revoke_perm"
107 <span class="btn btn-link btn-danger revoke_perm"
108 member="${_user_group.users_group_id}" member_type="user_group">
108 member="${_user_group.users_group_id}" member_type="user_group">
109 <i class="icon-remove"></i> ${_('revoke')}
109 <i class="icon-remove"></i> ${_('revoke')}
110 </span>
110 </span>
111 </td>
111 </td>
112 </tr>
112 </tr>
113 %endfor
113 %endfor
114 <tr class="new_members" id="add_perm_input"></tr>
114 <tr class="new_members" id="add_perm_input"></tr>
115 </table>
115 </table>
116 <div id="add_perm" class="link">
116 <div id="add_perm" class="link">
117 ${_('Add new')}
117 ${_('Add new')}
118 </div>
118 </div>
119 <div class="buttons">
119 <div class="buttons">
120 ${h.submit('save',_('Save'),class_="btn btn-primary")}
120 ${h.submit('save',_('Save'),class_="btn btn-primary")}
121 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
121 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
122 </div>
122 </div>
123 ${h.end_form()}
123 ${h.end_form()}
124 </div>
124 </div>
125 </div>
125 </div>
126
126
127 <script type="text/javascript">
127 <script type="text/javascript">
128 $('#add_perm').on('click', function(e){
128 $('#add_perm').on('click', function(e){
129 addNewPermInput($(this), 'usergroup');
129 addNewPermInput($(this), 'usergroup');
130 });
130 });
131 $('.revoke_perm').on('click', function(e){
131 $('.revoke_perm').on('click', function(e){
132 markRevokePermInput($(this), 'usergroup');
132 markRevokePermInput($(this), 'usergroup');
133 });
133 });
134 </script>
134 </script>
General Comments 0
You need to be logged in to leave comments. Login now