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