##// END OF EJS Templates
admin-users: moved grid browsing to pyramid....
marcink -
r1520:67ca1dd5 default
parent child Browse files
Show More
@@ -1,68 +1,77 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def includeme(config):
28 28 settings = config.get_settings()
29 29
30 30 # Create admin navigation registry and add it to the pyramid registry.
31 31 labs_active = str2bool(settings.get('labs_settings_active', False))
32 32 navigation_registry = NavigationRegistry(labs_active=labs_active)
33 33 config.registry.registerUtility(navigation_registry)
34 34
35 35 config.add_route(
36 36 name='admin_settings_open_source',
37 37 pattern=ADMIN_PREFIX + '/settings/open_source')
38 38 config.add_route(
39 39 name='admin_settings_vcs_svn_generate_cfg',
40 40 pattern=ADMIN_PREFIX + '/settings/vcs/svn_generate_cfg')
41 41
42 42 config.add_route(
43 43 name='admin_settings_system',
44 44 pattern=ADMIN_PREFIX + '/settings/system')
45 45 config.add_route(
46 46 name='admin_settings_system_update',
47 47 pattern=ADMIN_PREFIX + '/settings/system/updates')
48 48
49 49 config.add_route(
50 50 name='admin_settings_sessions',
51 51 pattern=ADMIN_PREFIX + '/settings/sessions')
52 52 config.add_route(
53 53 name='admin_settings_sessions_cleanup',
54 54 pattern=ADMIN_PREFIX + '/settings/sessions/cleanup')
55 55
56 # users admin
57 config.add_route(
58 name='users',
59 pattern=ADMIN_PREFIX + '/users')
60
61 config.add_route(
62 name='users_data',
63 pattern=ADMIN_PREFIX + '/users_data')
64
56 65 # user auth tokens
57 66 config.add_route(
58 67 name='edit_user_auth_tokens',
59 68 pattern=ADMIN_PREFIX + '/users/{user_id:\d+}/edit/auth_tokens')
60 69 config.add_route(
61 70 name='edit_user_auth_tokens_add',
62 71 pattern=ADMIN_PREFIX + '/users/{user_id:\d+}/edit/auth_tokens/new')
63 72 config.add_route(
64 73 name='edit_user_auth_tokens_delete',
65 74 pattern=ADMIN_PREFIX + '/users/{user_id:\d+}/edit/auth_tokens/delete')
66 75
67 76 # Scan module for configuration decorators.
68 77 config.scan()
@@ -1,114 +1,142 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import User, UserApiKeys
24 24
25 from rhodecode.apps._base import ADMIN_PREFIX
26 25 from rhodecode.tests import (
27 26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 27 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.utils import AssertResponse
30 28
31 29 fixture = Fixture()
32 30
33 31
32 def route_path(name, params=None, **kwargs):
33 import urllib
34 from rhodecode.apps._base import ADMIN_PREFIX
34 35
35 def route_path(name, **kwargs):
36 return {
36 base_url = {
37 37 'users':
38 38 ADMIN_PREFIX + '/users',
39 39 'users_data':
40 40 ADMIN_PREFIX + '/users_data',
41 41 'edit_user_auth_tokens':
42 42 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
43 43 'edit_user_auth_tokens_add':
44 44 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
45 45 'edit_user_auth_tokens_delete':
46 46 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
47 47 }[name].format(**kwargs)
48 48
49 if params:
50 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 return base_url
52
49 53
50 54 class TestAdminUsersView(TestController):
51 55
56 def test_show_users(self):
57 self.log_user()
58 self.app.get(route_path('users'))
59
60 def test_show_users_data(self, xhr_header):
61 self.log_user()
62 response = self.app.get(route_path(
63 'users_data'), extra_environ=xhr_header)
64
65 all_users = User.query().filter(
66 User.username != User.DEFAULT_USER).count()
67 assert response.json['recordsTotal'] == all_users
68
69 def test_show_users_data_filtered(self, xhr_header):
70 self.log_user()
71 response = self.app.get(route_path(
72 'users_data', params={'search[value]': 'empty_search'}),
73 extra_environ=xhr_header)
74
75 all_users = User.query().filter(
76 User.username != User.DEFAULT_USER).count()
77 assert response.json['recordsTotal'] == all_users
78 assert response.json['recordsFiltered'] == 0
79
52 80 def test_auth_tokens_default_user(self):
53 81 self.log_user()
54 82 user = User.get_default_user()
55 83 response = self.app.get(
56 84 route_path('edit_user_auth_tokens', user_id=user.user_id),
57 85 status=302)
58 86
59 87 def test_auth_tokens(self):
60 88 self.log_user()
61 89
62 90 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
63 91 response = self.app.get(
64 92 route_path('edit_user_auth_tokens', user_id=user.user_id))
65 93 for token in user.auth_tokens:
66 94 response.mustcontain(token)
67 95 response.mustcontain('never')
68 96
69 97 @pytest.mark.parametrize("desc, lifetime", [
70 98 ('forever', -1),
71 99 ('5mins', 60*5),
72 100 ('30days', 60*60*24*30),
73 101 ])
74 102 def test_add_auth_token(self, desc, lifetime, user_util):
75 103 self.log_user()
76 104 user = user_util.create_user()
77 105 user_id = user.user_id
78 106
79 107 response = self.app.post(
80 108 route_path('edit_user_auth_tokens_add', user_id=user_id),
81 109 {'description': desc, 'lifetime': lifetime,
82 110 'csrf_token': self.csrf_token})
83 111 assert_session_flash(response, 'Auth token successfully created')
84 112
85 113 response = response.follow()
86 114 user = User.get(user_id)
87 115 for auth_token in user.auth_tokens:
88 116 response.mustcontain(auth_token)
89 117
90 118 def test_delete_auth_token(self, user_util):
91 119 self.log_user()
92 120 user = user_util.create_user()
93 121 user_id = user.user_id
94 122 keys = user.extra_auth_tokens
95 123 assert 2 == len(keys)
96 124
97 125 response = self.app.post(
98 126 route_path('edit_user_auth_tokens_add', user_id=user_id),
99 127 {'description': 'desc', 'lifetime': -1,
100 128 'csrf_token': self.csrf_token})
101 129 assert_session_flash(response, 'Auth token successfully created')
102 130 response.follow()
103 131
104 132 # now delete our key
105 133 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
106 134 assert 3 == len(keys)
107 135
108 136 response = self.app.post(
109 137 route_path('edit_user_auth_tokens_delete', user_id=user_id),
110 138 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
111 139
112 140 assert_session_flash(response, 'Auth token successfully deleted')
113 141 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
114 142 assert 2 == len(keys)
@@ -1,141 +1,240 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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
23 23 from pyramid.httpexceptions import HTTPFound
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.utils import PartialRenderer
31 from rhodecode.lib.utils2 import safe_int
31 from rhodecode.lib.utils2 import safe_int, safe_unicode
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User
33 from rhodecode.model.db import User, or_
34 34 from rhodecode.model.meta import Session
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminUsersView(BaseAppView):
40 40 ALLOW_SCOPED_TOKENS = False
41 41 """
42 42 This view has alternative version inside EE, if modified please take a look
43 43 in there as well.
44 44 """
45 45
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context()
48 48 c.auth_user = self.request.user
49 49 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
50 50 self._register_global_c(c)
51 51 return c
52 52
53 53 def _redirect_for_default_user(self, username):
54 54 _ = self.request.translate
55 55 if username == User.DEFAULT_USER:
56 56 h.flash(_("You can't edit this user"), category='warning')
57 57 # TODO(marcink): redirect to 'users' admin panel once this
58 58 # is a pyramid view
59 59 raise HTTPFound('/')
60 60
61 def _extract_ordering(self, request):
62 column_index = safe_int(request.GET.get('order[0][column]'))
63 order_dir = request.GET.get(
64 'order[0][dir]', 'desc')
65 order_by = request.GET.get(
66 'columns[%s][data][sort]' % column_index, 'name_raw')
67
68 # translate datatable to DB columns
69 order_by = {
70 'first_name': 'name',
71 'last_name': 'lastname',
72 'last_activity': ''
73 }.get(order_by) or order_by
74
75 search_q = request.GET.get('search[value]')
76 return search_q, order_by, order_dir
77
78 def _extract_chunk(self, request):
79 start = safe_int(request.GET.get('start'), 0)
80 length = safe_int(request.GET.get('length'), 25)
81 draw = safe_int(request.GET.get('draw'))
82 return draw, start, length
83
84 @HasPermissionAllDecorator('hg.admin')
85 @view_config(
86 route_name='users', request_method='GET',
87 renderer='rhodecode:templates/admin/users/users.mako')
88 def users_list(self):
89 c = self.load_default_context()
90 return self._get_template_context(c)
91
92 @HasPermissionAllDecorator('hg.admin')
93 @view_config(
94 # renderer defined below
95 route_name='users_data', request_method='GET', renderer='json',
96 xhr=True)
97 def users_list_data(self):
98 draw, start, limit = self._extract_chunk(self.request)
99 search_q, order_by, order_dir = self._extract_ordering(self.request)
100
101 _render = PartialRenderer('data_table/_dt_elements.mako')
102
103 def user_actions(user_id, username):
104 return _render("user_actions", user_id, username)
105
106 users_data_total_count = User.query()\
107 .filter(User.username != User.DEFAULT_USER) \
108 .count()
109
110 # json generate
111 base_q = User.query().filter(User.username != User.DEFAULT_USER)
112
113 if search_q:
114 like_expression = u'{}%'.format(safe_unicode(search_q))
115 base_q = base_q.filter(or_(
116 User.username.ilike(like_expression),
117 User._email.ilike(like_expression),
118 User.name.ilike(like_expression),
119 User.lastname.ilike(like_expression),
120 ))
121
122 users_data_total_filtered_count = base_q.count()
123
124 sort_col = getattr(User, order_by, None)
125 if sort_col and order_dir == 'asc':
126 base_q = base_q.order_by(sort_col.asc())
127 elif sort_col:
128 base_q = base_q.order_by(sort_col.desc())
129
130 base_q = base_q.offset(start).limit(limit)
131 users_list = base_q.all()
132
133 users_data = []
134 for user in users_list:
135 users_data.append({
136 "username": h.gravatar_with_user(user.username),
137 "email": user.email,
138 "first_name": h.escape(user.name),
139 "last_name": h.escape(user.lastname),
140 "last_login": h.format_date(user.last_login),
141 "last_activity": h.format_date(
142 h.time_to_datetime(user.user_data.get('last_activity', 0))),
143 "active": h.bool2icon(user.active),
144 "active_raw": user.active,
145 "admin": h.bool2icon(user.admin),
146 "extern_type": user.extern_type,
147 "extern_name": user.extern_name,
148 "action": user_actions(user.user_id, user.username),
149 })
150
151 data = ({
152 'draw': draw,
153 'data': users_data,
154 'recordsTotal': users_data_total_count,
155 'recordsFiltered': users_data_total_filtered_count,
156 })
157
158 return data
159
61 160 @LoginRequired()
62 161 @HasPermissionAllDecorator('hg.admin')
63 162 @view_config(
64 163 route_name='edit_user_auth_tokens', request_method='GET',
65 164 renderer='rhodecode:templates/admin/users/user_edit.mako')
66 165 def auth_tokens(self):
67 166 _ = self.request.translate
68 167 c = self.load_default_context()
69 168
70 169 user_id = self.request.matchdict.get('user_id')
71 170 c.user = User.get_or_404(user_id, pyramid_exc=True)
72 171 self._redirect_for_default_user(c.user.username)
73 172
74 173 c.active = 'auth_tokens'
75 174
76 175 c.lifetime_values = [
77 176 (str(-1), _('forever')),
78 177 (str(5), _('5 minutes')),
79 178 (str(60), _('1 hour')),
80 179 (str(60 * 24), _('1 day')),
81 180 (str(60 * 24 * 30), _('1 month')),
82 181 ]
83 182 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
84 183 c.role_values = [
85 184 (x, AuthTokenModel.cls._get_role_name(x))
86 185 for x in AuthTokenModel.cls.ROLES]
87 186 c.role_options = [(c.role_values, _("Role"))]
88 187 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
89 188 c.user.user_id, show_expired=True)
90 189 return self._get_template_context(c)
91 190
92 191 def maybe_attach_token_scope(self, token):
93 192 # implemented in EE edition
94 193 pass
95 194
96 195 @LoginRequired()
97 196 @HasPermissionAllDecorator('hg.admin')
98 197 @CSRFRequired()
99 198 @view_config(
100 199 route_name='edit_user_auth_tokens_add', request_method='POST')
101 200 def auth_tokens_add(self):
102 201 _ = self.request.translate
103 202 c = self.load_default_context()
104 203
105 204 user_id = self.request.matchdict.get('user_id')
106 205 c.user = User.get_or_404(user_id, pyramid_exc=True)
107 206 self._redirect_for_default_user(c.user.username)
108 207
109 208 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
110 209 description = self.request.POST.get('description')
111 210 role = self.request.POST.get('role')
112 211
113 212 token = AuthTokenModel().create(
114 213 c.user.user_id, description, lifetime, role)
115 214 self.maybe_attach_token_scope(token)
116 215 Session().commit()
117 216
118 217 h.flash(_("Auth token successfully created"), category='success')
119 218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
120 219
121 220 @LoginRequired()
122 221 @HasPermissionAllDecorator('hg.admin')
123 222 @CSRFRequired()
124 223 @view_config(
125 224 route_name='edit_user_auth_tokens_delete', request_method='POST')
126 225 def auth_tokens_delete(self):
127 226 _ = self.request.translate
128 227 c = self.load_default_context()
129 228
130 229 user_id = self.request.matchdict.get('user_id')
131 230 c.user = User.get_or_404(user_id, pyramid_exc=True)
132 231 self._redirect_for_default_user(c.user.username)
133 232
134 233 del_auth_token = self.request.POST.get('del_auth_token')
135 234
136 235 if del_auth_token:
137 236 AuthTokenModel().delete(del_auth_token, c.user.user_id)
138 237 Session().commit()
139 238 h.flash(_("Auth token successfully deleted"), category='success')
140 239
141 240 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
@@ -1,1156 +1,1154 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('show_user', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 # TODO: johbo: Static links, to be replaced by our redirection mechanism
202 202 rmap.connect('rst_help',
203 203 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
204 204 _static=True)
205 205 rmap.connect('markdown_help',
206 206 'http://daringfireball.net/projects/markdown/syntax',
207 207 _static=True)
208 208 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
209 209 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
210 210 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
211 211 # TODO: anderson - making this a static link since redirect won't play
212 212 # nice with POST requests
213 213 rmap.connect('enterprise_license_convert_from_old',
214 214 'https://rhodecode.com/u/license-upgrade',
215 215 _static=True)
216 216
217 217 routing_links.connect_redirection_links(rmap)
218 218
219 219 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
220 220 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
221 221
222 222 # ADMIN REPOSITORY ROUTES
223 223 with rmap.submapper(path_prefix=ADMIN_PREFIX,
224 224 controller='admin/repos') as m:
225 225 m.connect('repos', '/repos',
226 226 action='create', conditions={'method': ['POST']})
227 227 m.connect('repos', '/repos',
228 228 action='index', conditions={'method': ['GET']})
229 229 m.connect('new_repo', '/create_repository', jsroute=True,
230 230 action='create_repository', conditions={'method': ['GET']})
231 231 m.connect('/repos/{repo_name}',
232 232 action='update', conditions={'method': ['PUT'],
233 233 'function': check_repo},
234 234 requirements=URL_NAME_REQUIREMENTS)
235 235 m.connect('delete_repo', '/repos/{repo_name}',
236 236 action='delete', conditions={'method': ['DELETE']},
237 237 requirements=URL_NAME_REQUIREMENTS)
238 238 m.connect('repo', '/repos/{repo_name}',
239 239 action='show', conditions={'method': ['GET'],
240 240 'function': check_repo},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242
243 243 # ADMIN REPOSITORY GROUPS ROUTES
244 244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 245 controller='admin/repo_groups') as m:
246 246 m.connect('repo_groups', '/repo_groups',
247 247 action='create', conditions={'method': ['POST']})
248 248 m.connect('repo_groups', '/repo_groups',
249 249 action='index', conditions={'method': ['GET']})
250 250 m.connect('new_repo_group', '/repo_groups/new',
251 251 action='new', conditions={'method': ['GET']})
252 252 m.connect('update_repo_group', '/repo_groups/{group_name}',
253 253 action='update', conditions={'method': ['PUT'],
254 254 'function': check_group},
255 255 requirements=URL_NAME_REQUIREMENTS)
256 256
257 257 # EXTRAS REPO GROUP ROUTES
258 258 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
259 259 action='edit',
260 260 conditions={'method': ['GET'], 'function': check_group},
261 261 requirements=URL_NAME_REQUIREMENTS)
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['PUT'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266
267 267 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
268 268 action='edit_repo_group_advanced',
269 269 conditions={'method': ['GET'], 'function': check_group},
270 270 requirements=URL_NAME_REQUIREMENTS)
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['PUT'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275
276 276 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
277 277 action='edit_repo_group_perms',
278 278 conditions={'method': ['GET'], 'function': check_group},
279 279 requirements=URL_NAME_REQUIREMENTS)
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='update_perms',
282 282 conditions={'method': ['PUT'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284
285 285 m.connect('delete_repo_group', '/repo_groups/{group_name}',
286 286 action='delete', conditions={'method': ['DELETE'],
287 287 'function': check_group},
288 288 requirements=URL_NAME_REQUIREMENTS)
289 289
290 290 # ADMIN USER ROUTES
291 291 with rmap.submapper(path_prefix=ADMIN_PREFIX,
292 292 controller='admin/users') as m:
293 293 m.connect('users', '/users',
294 294 action='create', conditions={'method': ['POST']})
295 m.connect('users', '/users',
296 action='index', conditions={'method': ['GET']})
297 295 m.connect('new_user', '/users/new',
298 296 action='new', conditions={'method': ['GET']})
299 297 m.connect('update_user', '/users/{user_id}',
300 298 action='update', conditions={'method': ['PUT']})
301 299 m.connect('delete_user', '/users/{user_id}',
302 300 action='delete', conditions={'method': ['DELETE']})
303 301 m.connect('edit_user', '/users/{user_id}/edit',
304 302 action='edit', conditions={'method': ['GET']}, jsroute=True)
305 303 m.connect('user', '/users/{user_id}',
306 304 action='show', conditions={'method': ['GET']})
307 305 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
308 306 action='reset_password', conditions={'method': ['POST']})
309 307 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
310 308 action='create_personal_repo_group', conditions={'method': ['POST']})
311 309
312 310 # EXTRAS USER ROUTES
313 311 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
314 312 action='edit_advanced', conditions={'method': ['GET']})
315 313 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
316 314 action='update_advanced', conditions={'method': ['PUT']})
317 315
318 316 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
319 317 action='edit_global_perms', conditions={'method': ['GET']})
320 318 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
321 319 action='update_global_perms', conditions={'method': ['PUT']})
322 320
323 321 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
324 322 action='edit_perms_summary', conditions={'method': ['GET']})
325 323
326 324 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
327 325 action='edit_emails', conditions={'method': ['GET']})
328 326 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
329 327 action='add_email', conditions={'method': ['PUT']})
330 328 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
331 329 action='delete_email', conditions={'method': ['DELETE']})
332 330
333 331 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
334 332 action='edit_ips', conditions={'method': ['GET']})
335 333 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
336 334 action='add_ip', conditions={'method': ['PUT']})
337 335 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
338 336 action='delete_ip', conditions={'method': ['DELETE']})
339 337
340 338 # ADMIN USER GROUPS REST ROUTES
341 339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
342 340 controller='admin/user_groups') as m:
343 341 m.connect('users_groups', '/user_groups',
344 342 action='create', conditions={'method': ['POST']})
345 343 m.connect('users_groups', '/user_groups',
346 344 action='index', conditions={'method': ['GET']})
347 345 m.connect('new_users_group', '/user_groups/new',
348 346 action='new', conditions={'method': ['GET']})
349 347 m.connect('update_users_group', '/user_groups/{user_group_id}',
350 348 action='update', conditions={'method': ['PUT']})
351 349 m.connect('delete_users_group', '/user_groups/{user_group_id}',
352 350 action='delete', conditions={'method': ['DELETE']})
353 351 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
354 352 action='edit', conditions={'method': ['GET']},
355 353 function=check_user_group)
356 354
357 355 # EXTRAS USER GROUP ROUTES
358 356 m.connect('edit_user_group_global_perms',
359 357 '/user_groups/{user_group_id}/edit/global_permissions',
360 358 action='edit_global_perms', conditions={'method': ['GET']})
361 359 m.connect('edit_user_group_global_perms',
362 360 '/user_groups/{user_group_id}/edit/global_permissions',
363 361 action='update_global_perms', conditions={'method': ['PUT']})
364 362 m.connect('edit_user_group_perms_summary',
365 363 '/user_groups/{user_group_id}/edit/permissions_summary',
366 364 action='edit_perms_summary', conditions={'method': ['GET']})
367 365
368 366 m.connect('edit_user_group_perms',
369 367 '/user_groups/{user_group_id}/edit/permissions',
370 368 action='edit_perms', conditions={'method': ['GET']})
371 369 m.connect('edit_user_group_perms',
372 370 '/user_groups/{user_group_id}/edit/permissions',
373 371 action='update_perms', conditions={'method': ['PUT']})
374 372
375 373 m.connect('edit_user_group_advanced',
376 374 '/user_groups/{user_group_id}/edit/advanced',
377 375 action='edit_advanced', conditions={'method': ['GET']})
378 376
379 377 m.connect('edit_user_group_members',
380 378 '/user_groups/{user_group_id}/edit/members', jsroute=True,
381 379 action='user_group_members', conditions={'method': ['GET']})
382 380
383 381 # ADMIN PERMISSIONS ROUTES
384 382 with rmap.submapper(path_prefix=ADMIN_PREFIX,
385 383 controller='admin/permissions') as m:
386 384 m.connect('admin_permissions_application', '/permissions/application',
387 385 action='permission_application_update', conditions={'method': ['POST']})
388 386 m.connect('admin_permissions_application', '/permissions/application',
389 387 action='permission_application', conditions={'method': ['GET']})
390 388
391 389 m.connect('admin_permissions_global', '/permissions/global',
392 390 action='permission_global_update', conditions={'method': ['POST']})
393 391 m.connect('admin_permissions_global', '/permissions/global',
394 392 action='permission_global', conditions={'method': ['GET']})
395 393
396 394 m.connect('admin_permissions_object', '/permissions/object',
397 395 action='permission_objects_update', conditions={'method': ['POST']})
398 396 m.connect('admin_permissions_object', '/permissions/object',
399 397 action='permission_objects', conditions={'method': ['GET']})
400 398
401 399 m.connect('admin_permissions_ips', '/permissions/ips',
402 400 action='permission_ips', conditions={'method': ['POST']})
403 401 m.connect('admin_permissions_ips', '/permissions/ips',
404 402 action='permission_ips', conditions={'method': ['GET']})
405 403
406 404 m.connect('admin_permissions_overview', '/permissions/overview',
407 405 action='permission_perms', conditions={'method': ['GET']})
408 406
409 407 # ADMIN DEFAULTS REST ROUTES
410 408 with rmap.submapper(path_prefix=ADMIN_PREFIX,
411 409 controller='admin/defaults') as m:
412 410 m.connect('admin_defaults_repositories', '/defaults/repositories',
413 411 action='update_repository_defaults', conditions={'method': ['POST']})
414 412 m.connect('admin_defaults_repositories', '/defaults/repositories',
415 413 action='index', conditions={'method': ['GET']})
416 414
417 415 # ADMIN DEBUG STYLE ROUTES
418 416 if str2bool(config.get('debug_style')):
419 417 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
420 418 controller='debug_style') as m:
421 419 m.connect('debug_style_home', '',
422 420 action='index', conditions={'method': ['GET']})
423 421 m.connect('debug_style_template', '/t/{t_path}',
424 422 action='template', conditions={'method': ['GET']})
425 423
426 424 # ADMIN SETTINGS ROUTES
427 425 with rmap.submapper(path_prefix=ADMIN_PREFIX,
428 426 controller='admin/settings') as m:
429 427
430 428 # default
431 429 m.connect('admin_settings', '/settings',
432 430 action='settings_global_update',
433 431 conditions={'method': ['POST']})
434 432 m.connect('admin_settings', '/settings',
435 433 action='settings_global', conditions={'method': ['GET']})
436 434
437 435 m.connect('admin_settings_vcs', '/settings/vcs',
438 436 action='settings_vcs_update',
439 437 conditions={'method': ['POST']})
440 438 m.connect('admin_settings_vcs', '/settings/vcs',
441 439 action='settings_vcs',
442 440 conditions={'method': ['GET']})
443 441 m.connect('admin_settings_vcs', '/settings/vcs',
444 442 action='delete_svn_pattern',
445 443 conditions={'method': ['DELETE']})
446 444
447 445 m.connect('admin_settings_mapping', '/settings/mapping',
448 446 action='settings_mapping_update',
449 447 conditions={'method': ['POST']})
450 448 m.connect('admin_settings_mapping', '/settings/mapping',
451 449 action='settings_mapping', conditions={'method': ['GET']})
452 450
453 451 m.connect('admin_settings_global', '/settings/global',
454 452 action='settings_global_update',
455 453 conditions={'method': ['POST']})
456 454 m.connect('admin_settings_global', '/settings/global',
457 455 action='settings_global', conditions={'method': ['GET']})
458 456
459 457 m.connect('admin_settings_visual', '/settings/visual',
460 458 action='settings_visual_update',
461 459 conditions={'method': ['POST']})
462 460 m.connect('admin_settings_visual', '/settings/visual',
463 461 action='settings_visual', conditions={'method': ['GET']})
464 462
465 463 m.connect('admin_settings_issuetracker',
466 464 '/settings/issue-tracker', action='settings_issuetracker',
467 465 conditions={'method': ['GET']})
468 466 m.connect('admin_settings_issuetracker_save',
469 467 '/settings/issue-tracker/save',
470 468 action='settings_issuetracker_save',
471 469 conditions={'method': ['POST']})
472 470 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
473 471 action='settings_issuetracker_test',
474 472 conditions={'method': ['POST']})
475 473 m.connect('admin_issuetracker_delete',
476 474 '/settings/issue-tracker/delete',
477 475 action='settings_issuetracker_delete',
478 476 conditions={'method': ['DELETE']})
479 477
480 478 m.connect('admin_settings_email', '/settings/email',
481 479 action='settings_email_update',
482 480 conditions={'method': ['POST']})
483 481 m.connect('admin_settings_email', '/settings/email',
484 482 action='settings_email', conditions={'method': ['GET']})
485 483
486 484 m.connect('admin_settings_hooks', '/settings/hooks',
487 485 action='settings_hooks_update',
488 486 conditions={'method': ['POST', 'DELETE']})
489 487 m.connect('admin_settings_hooks', '/settings/hooks',
490 488 action='settings_hooks', conditions={'method': ['GET']})
491 489
492 490 m.connect('admin_settings_search', '/settings/search',
493 491 action='settings_search', conditions={'method': ['GET']})
494 492
495 493 m.connect('admin_settings_supervisor', '/settings/supervisor',
496 494 action='settings_supervisor', conditions={'method': ['GET']})
497 495 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
498 496 action='settings_supervisor_log', conditions={'method': ['GET']})
499 497
500 498 m.connect('admin_settings_labs', '/settings/labs',
501 499 action='settings_labs_update',
502 500 conditions={'method': ['POST']})
503 501 m.connect('admin_settings_labs', '/settings/labs',
504 502 action='settings_labs', conditions={'method': ['GET']})
505 503
506 504 # ADMIN MY ACCOUNT
507 505 with rmap.submapper(path_prefix=ADMIN_PREFIX,
508 506 controller='admin/my_account') as m:
509 507
510 508 m.connect('my_account', '/my_account',
511 509 action='my_account', conditions={'method': ['GET']})
512 510 m.connect('my_account_edit', '/my_account/edit',
513 511 action='my_account_edit', conditions={'method': ['GET']})
514 512 m.connect('my_account', '/my_account',
515 513 action='my_account_update', conditions={'method': ['POST']})
516 514
517 515 m.connect('my_account_password', '/my_account/password',
518 516 action='my_account_password', conditions={'method': ['GET', 'POST']})
519 517
520 518 m.connect('my_account_repos', '/my_account/repos',
521 519 action='my_account_repos', conditions={'method': ['GET']})
522 520
523 521 m.connect('my_account_watched', '/my_account/watched',
524 522 action='my_account_watched', conditions={'method': ['GET']})
525 523
526 524 m.connect('my_account_pullrequests', '/my_account/pull_requests',
527 525 action='my_account_pullrequests', conditions={'method': ['GET']})
528 526
529 527 m.connect('my_account_perms', '/my_account/perms',
530 528 action='my_account_perms', conditions={'method': ['GET']})
531 529
532 530 m.connect('my_account_emails', '/my_account/emails',
533 531 action='my_account_emails', conditions={'method': ['GET']})
534 532 m.connect('my_account_emails', '/my_account/emails',
535 533 action='my_account_emails_add', conditions={'method': ['POST']})
536 534 m.connect('my_account_emails', '/my_account/emails',
537 535 action='my_account_emails_delete', conditions={'method': ['DELETE']})
538 536
539 537 m.connect('my_account_notifications', '/my_account/notifications',
540 538 action='my_notifications',
541 539 conditions={'method': ['GET']})
542 540 m.connect('my_account_notifications_toggle_visibility',
543 541 '/my_account/toggle_visibility',
544 542 action='my_notifications_toggle_visibility',
545 543 conditions={'method': ['POST']})
546 544 m.connect('my_account_notifications_test_channelstream',
547 545 '/my_account/test_channelstream',
548 546 action='my_account_notifications_test_channelstream',
549 547 conditions={'method': ['POST']})
550 548
551 549 # NOTIFICATION REST ROUTES
552 550 with rmap.submapper(path_prefix=ADMIN_PREFIX,
553 551 controller='admin/notifications') as m:
554 552 m.connect('notifications', '/notifications',
555 553 action='index', conditions={'method': ['GET']})
556 554 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
557 555 action='mark_all_read', conditions={'method': ['POST']})
558 556 m.connect('/notifications/{notification_id}',
559 557 action='update', conditions={'method': ['PUT']})
560 558 m.connect('/notifications/{notification_id}',
561 559 action='delete', conditions={'method': ['DELETE']})
562 560 m.connect('notification', '/notifications/{notification_id}',
563 561 action='show', conditions={'method': ['GET']})
564 562
565 563 # ADMIN GIST
566 564 with rmap.submapper(path_prefix=ADMIN_PREFIX,
567 565 controller='admin/gists') as m:
568 566 m.connect('gists', '/gists',
569 567 action='create', conditions={'method': ['POST']})
570 568 m.connect('gists', '/gists', jsroute=True,
571 569 action='index', conditions={'method': ['GET']})
572 570 m.connect('new_gist', '/gists/new', jsroute=True,
573 571 action='new', conditions={'method': ['GET']})
574 572
575 573 m.connect('/gists/{gist_id}',
576 574 action='delete', conditions={'method': ['DELETE']})
577 575 m.connect('edit_gist', '/gists/{gist_id}/edit',
578 576 action='edit_form', conditions={'method': ['GET']})
579 577 m.connect('edit_gist', '/gists/{gist_id}/edit',
580 578 action='edit', conditions={'method': ['POST']})
581 579 m.connect(
582 580 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
583 581 action='check_revision', conditions={'method': ['GET']})
584 582
585 583 m.connect('gist', '/gists/{gist_id}',
586 584 action='show', conditions={'method': ['GET']})
587 585 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
588 586 revision='tip',
589 587 action='show', conditions={'method': ['GET']})
590 588 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
591 589 revision='tip',
592 590 action='show', conditions={'method': ['GET']})
593 591 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
594 592 revision='tip',
595 593 action='show', conditions={'method': ['GET']},
596 594 requirements=URL_NAME_REQUIREMENTS)
597 595
598 596 # ADMIN MAIN PAGES
599 597 with rmap.submapper(path_prefix=ADMIN_PREFIX,
600 598 controller='admin/admin') as m:
601 599 m.connect('admin_home', '', action='index')
602 600 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
603 601 action='add_repo')
604 602 m.connect(
605 603 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
606 604 action='pull_requests')
607 605 m.connect(
608 606 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
609 607 action='pull_requests')
610 608 m.connect(
611 609 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
612 610 action='pull_requests')
613 611
614 612 # USER JOURNAL
615 613 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
616 614 controller='journal', action='index')
617 615 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
618 616 controller='journal', action='journal_rss')
619 617 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
620 618 controller='journal', action='journal_atom')
621 619
622 620 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
623 621 controller='journal', action='public_journal')
624 622
625 623 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
626 624 controller='journal', action='public_journal_rss')
627 625
628 626 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
629 627 controller='journal', action='public_journal_rss')
630 628
631 629 rmap.connect('public_journal_atom',
632 630 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
633 631 action='public_journal_atom')
634 632
635 633 rmap.connect('public_journal_atom_old',
636 634 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
637 635 action='public_journal_atom')
638 636
639 637 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
640 638 controller='journal', action='toggle_following', jsroute=True,
641 639 conditions={'method': ['POST']})
642 640
643 641 # FULL TEXT SEARCH
644 642 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
645 643 controller='search')
646 644 rmap.connect('search_repo_home', '/{repo_name}/search',
647 645 controller='search',
648 646 action='index',
649 647 conditions={'function': check_repo},
650 648 requirements=URL_NAME_REQUIREMENTS)
651 649
652 650 # FEEDS
653 651 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
654 652 controller='feed', action='rss',
655 653 conditions={'function': check_repo},
656 654 requirements=URL_NAME_REQUIREMENTS)
657 655
658 656 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
659 657 controller='feed', action='atom',
660 658 conditions={'function': check_repo},
661 659 requirements=URL_NAME_REQUIREMENTS)
662 660
663 661 #==========================================================================
664 662 # REPOSITORY ROUTES
665 663 #==========================================================================
666 664
667 665 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
668 666 controller='admin/repos', action='repo_creating',
669 667 requirements=URL_NAME_REQUIREMENTS)
670 668 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
671 669 controller='admin/repos', action='repo_check',
672 670 requirements=URL_NAME_REQUIREMENTS)
673 671
674 672 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
675 673 controller='summary', action='repo_stats',
676 674 conditions={'function': check_repo},
677 675 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
678 676
679 677 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
680 678 controller='summary', action='repo_refs_data',
681 679 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
682 680 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
683 681 controller='summary', action='repo_refs_changelog_data',
684 682 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
685 683 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
686 684 controller='summary', action='repo_default_reviewers_data',
687 685 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
688 686
689 687 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
690 688 controller='changeset', revision='tip',
691 689 conditions={'function': check_repo},
692 690 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
693 691 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
694 692 controller='changeset', revision='tip', action='changeset_children',
695 693 conditions={'function': check_repo},
696 694 requirements=URL_NAME_REQUIREMENTS)
697 695 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
698 696 controller='changeset', revision='tip', action='changeset_parents',
699 697 conditions={'function': check_repo},
700 698 requirements=URL_NAME_REQUIREMENTS)
701 699
702 700 # repo edit options
703 701 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
704 702 controller='admin/repos', action='edit',
705 703 conditions={'method': ['GET'], 'function': check_repo},
706 704 requirements=URL_NAME_REQUIREMENTS)
707 705
708 706 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
709 707 jsroute=True,
710 708 controller='admin/repos', action='edit_permissions',
711 709 conditions={'method': ['GET'], 'function': check_repo},
712 710 requirements=URL_NAME_REQUIREMENTS)
713 711 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
714 712 controller='admin/repos', action='edit_permissions_update',
715 713 conditions={'method': ['PUT'], 'function': check_repo},
716 714 requirements=URL_NAME_REQUIREMENTS)
717 715
718 716 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
719 717 controller='admin/repos', action='edit_fields',
720 718 conditions={'method': ['GET'], 'function': check_repo},
721 719 requirements=URL_NAME_REQUIREMENTS)
722 720 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
723 721 controller='admin/repos', action='create_repo_field',
724 722 conditions={'method': ['PUT'], 'function': check_repo},
725 723 requirements=URL_NAME_REQUIREMENTS)
726 724 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
727 725 controller='admin/repos', action='delete_repo_field',
728 726 conditions={'method': ['DELETE'], 'function': check_repo},
729 727 requirements=URL_NAME_REQUIREMENTS)
730 728
731 729 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
732 730 controller='admin/repos', action='edit_advanced',
733 731 conditions={'method': ['GET'], 'function': check_repo},
734 732 requirements=URL_NAME_REQUIREMENTS)
735 733
736 734 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
737 735 controller='admin/repos', action='edit_advanced_locking',
738 736 conditions={'method': ['PUT'], 'function': check_repo},
739 737 requirements=URL_NAME_REQUIREMENTS)
740 738 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
741 739 controller='admin/repos', action='toggle_locking',
742 740 conditions={'method': ['GET'], 'function': check_repo},
743 741 requirements=URL_NAME_REQUIREMENTS)
744 742
745 743 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
746 744 controller='admin/repos', action='edit_advanced_journal',
747 745 conditions={'method': ['PUT'], 'function': check_repo},
748 746 requirements=URL_NAME_REQUIREMENTS)
749 747
750 748 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
751 749 controller='admin/repos', action='edit_advanced_fork',
752 750 conditions={'method': ['PUT'], 'function': check_repo},
753 751 requirements=URL_NAME_REQUIREMENTS)
754 752
755 753 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
756 754 controller='admin/repos', action='edit_caches_form',
757 755 conditions={'method': ['GET'], 'function': check_repo},
758 756 requirements=URL_NAME_REQUIREMENTS)
759 757 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
760 758 controller='admin/repos', action='edit_caches',
761 759 conditions={'method': ['PUT'], 'function': check_repo},
762 760 requirements=URL_NAME_REQUIREMENTS)
763 761
764 762 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
765 763 controller='admin/repos', action='edit_remote_form',
766 764 conditions={'method': ['GET'], 'function': check_repo},
767 765 requirements=URL_NAME_REQUIREMENTS)
768 766 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
769 767 controller='admin/repos', action='edit_remote',
770 768 conditions={'method': ['PUT'], 'function': check_repo},
771 769 requirements=URL_NAME_REQUIREMENTS)
772 770
773 771 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
774 772 controller='admin/repos', action='edit_statistics_form',
775 773 conditions={'method': ['GET'], 'function': check_repo},
776 774 requirements=URL_NAME_REQUIREMENTS)
777 775 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
778 776 controller='admin/repos', action='edit_statistics',
779 777 conditions={'method': ['PUT'], 'function': check_repo},
780 778 requirements=URL_NAME_REQUIREMENTS)
781 779 rmap.connect('repo_settings_issuetracker',
782 780 '/{repo_name}/settings/issue-tracker',
783 781 controller='admin/repos', action='repo_issuetracker',
784 782 conditions={'method': ['GET'], 'function': check_repo},
785 783 requirements=URL_NAME_REQUIREMENTS)
786 784 rmap.connect('repo_issuetracker_test',
787 785 '/{repo_name}/settings/issue-tracker/test',
788 786 controller='admin/repos', action='repo_issuetracker_test',
789 787 conditions={'method': ['POST'], 'function': check_repo},
790 788 requirements=URL_NAME_REQUIREMENTS)
791 789 rmap.connect('repo_issuetracker_delete',
792 790 '/{repo_name}/settings/issue-tracker/delete',
793 791 controller='admin/repos', action='repo_issuetracker_delete',
794 792 conditions={'method': ['DELETE'], 'function': check_repo},
795 793 requirements=URL_NAME_REQUIREMENTS)
796 794 rmap.connect('repo_issuetracker_save',
797 795 '/{repo_name}/settings/issue-tracker/save',
798 796 controller='admin/repos', action='repo_issuetracker_save',
799 797 conditions={'method': ['POST'], 'function': check_repo},
800 798 requirements=URL_NAME_REQUIREMENTS)
801 799 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
802 800 controller='admin/repos', action='repo_settings_vcs_update',
803 801 conditions={'method': ['POST'], 'function': check_repo},
804 802 requirements=URL_NAME_REQUIREMENTS)
805 803 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
806 804 controller='admin/repos', action='repo_settings_vcs',
807 805 conditions={'method': ['GET'], 'function': check_repo},
808 806 requirements=URL_NAME_REQUIREMENTS)
809 807 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
810 808 controller='admin/repos', action='repo_delete_svn_pattern',
811 809 conditions={'method': ['DELETE'], 'function': check_repo},
812 810 requirements=URL_NAME_REQUIREMENTS)
813 811 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
814 812 controller='admin/repos', action='repo_settings_pullrequest',
815 813 conditions={'method': ['GET', 'POST'], 'function': check_repo},
816 814 requirements=URL_NAME_REQUIREMENTS)
817 815
818 816 # still working url for backward compat.
819 817 rmap.connect('raw_changeset_home_depraced',
820 818 '/{repo_name}/raw-changeset/{revision}',
821 819 controller='changeset', action='changeset_raw',
822 820 revision='tip', conditions={'function': check_repo},
823 821 requirements=URL_NAME_REQUIREMENTS)
824 822
825 823 # new URLs
826 824 rmap.connect('changeset_raw_home',
827 825 '/{repo_name}/changeset-diff/{revision}',
828 826 controller='changeset', action='changeset_raw',
829 827 revision='tip', conditions={'function': check_repo},
830 828 requirements=URL_NAME_REQUIREMENTS)
831 829
832 830 rmap.connect('changeset_patch_home',
833 831 '/{repo_name}/changeset-patch/{revision}',
834 832 controller='changeset', action='changeset_patch',
835 833 revision='tip', conditions={'function': check_repo},
836 834 requirements=URL_NAME_REQUIREMENTS)
837 835
838 836 rmap.connect('changeset_download_home',
839 837 '/{repo_name}/changeset-download/{revision}',
840 838 controller='changeset', action='changeset_download',
841 839 revision='tip', conditions={'function': check_repo},
842 840 requirements=URL_NAME_REQUIREMENTS)
843 841
844 842 rmap.connect('changeset_comment',
845 843 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
846 844 controller='changeset', revision='tip', action='comment',
847 845 conditions={'function': check_repo},
848 846 requirements=URL_NAME_REQUIREMENTS)
849 847
850 848 rmap.connect('changeset_comment_preview',
851 849 '/{repo_name}/changeset/comment/preview', jsroute=True,
852 850 controller='changeset', action='preview_comment',
853 851 conditions={'function': check_repo, 'method': ['POST']},
854 852 requirements=URL_NAME_REQUIREMENTS)
855 853
856 854 rmap.connect('changeset_comment_delete',
857 855 '/{repo_name}/changeset/comment/{comment_id}/delete',
858 856 controller='changeset', action='delete_comment',
859 857 conditions={'function': check_repo, 'method': ['DELETE']},
860 858 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
861 859
862 860 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
863 861 controller='changeset', action='changeset_info',
864 862 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
865 863
866 864 rmap.connect('compare_home',
867 865 '/{repo_name}/compare',
868 866 controller='compare', action='index',
869 867 conditions={'function': check_repo},
870 868 requirements=URL_NAME_REQUIREMENTS)
871 869
872 870 rmap.connect('compare_url',
873 871 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
874 872 controller='compare', action='compare',
875 873 conditions={'function': check_repo},
876 874 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
877 875
878 876 rmap.connect('pullrequest_home',
879 877 '/{repo_name}/pull-request/new', controller='pullrequests',
880 878 action='index', conditions={'function': check_repo,
881 879 'method': ['GET']},
882 880 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
883 881
884 882 rmap.connect('pullrequest',
885 883 '/{repo_name}/pull-request/new', controller='pullrequests',
886 884 action='create', conditions={'function': check_repo,
887 885 'method': ['POST']},
888 886 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
889 887
890 888 rmap.connect('pullrequest_repo_refs',
891 889 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
892 890 controller='pullrequests',
893 891 action='get_repo_refs',
894 892 conditions={'function': check_repo, 'method': ['GET']},
895 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
896 894
897 895 rmap.connect('pullrequest_repo_destinations',
898 896 '/{repo_name}/pull-request/repo-destinations',
899 897 controller='pullrequests',
900 898 action='get_repo_destinations',
901 899 conditions={'function': check_repo, 'method': ['GET']},
902 900 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
903 901
904 902 rmap.connect('pullrequest_show',
905 903 '/{repo_name}/pull-request/{pull_request_id}',
906 904 controller='pullrequests',
907 905 action='show', conditions={'function': check_repo,
908 906 'method': ['GET']},
909 907 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
910 908
911 909 rmap.connect('pullrequest_update',
912 910 '/{repo_name}/pull-request/{pull_request_id}',
913 911 controller='pullrequests',
914 912 action='update', conditions={'function': check_repo,
915 913 'method': ['PUT']},
916 914 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
917 915
918 916 rmap.connect('pullrequest_merge',
919 917 '/{repo_name}/pull-request/{pull_request_id}',
920 918 controller='pullrequests',
921 919 action='merge', conditions={'function': check_repo,
922 920 'method': ['POST']},
923 921 requirements=URL_NAME_REQUIREMENTS)
924 922
925 923 rmap.connect('pullrequest_delete',
926 924 '/{repo_name}/pull-request/{pull_request_id}',
927 925 controller='pullrequests',
928 926 action='delete', conditions={'function': check_repo,
929 927 'method': ['DELETE']},
930 928 requirements=URL_NAME_REQUIREMENTS)
931 929
932 930 rmap.connect('pullrequest_show_all',
933 931 '/{repo_name}/pull-request',
934 932 controller='pullrequests',
935 933 action='show_all', conditions={'function': check_repo,
936 934 'method': ['GET']},
937 935 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
938 936
939 937 rmap.connect('pullrequest_comment',
940 938 '/{repo_name}/pull-request-comment/{pull_request_id}',
941 939 controller='pullrequests',
942 940 action='comment', conditions={'function': check_repo,
943 941 'method': ['POST']},
944 942 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
945 943
946 944 rmap.connect('pullrequest_comment_delete',
947 945 '/{repo_name}/pull-request-comment/{comment_id}/delete',
948 946 controller='pullrequests', action='delete_comment',
949 947 conditions={'function': check_repo, 'method': ['DELETE']},
950 948 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
951 949
952 950 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
953 951 controller='summary', conditions={'function': check_repo},
954 952 requirements=URL_NAME_REQUIREMENTS)
955 953
956 954 rmap.connect('branches_home', '/{repo_name}/branches',
957 955 controller='branches', conditions={'function': check_repo},
958 956 requirements=URL_NAME_REQUIREMENTS)
959 957
960 958 rmap.connect('tags_home', '/{repo_name}/tags',
961 959 controller='tags', conditions={'function': check_repo},
962 960 requirements=URL_NAME_REQUIREMENTS)
963 961
964 962 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
965 963 controller='bookmarks', conditions={'function': check_repo},
966 964 requirements=URL_NAME_REQUIREMENTS)
967 965
968 966 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
969 967 controller='changelog', conditions={'function': check_repo},
970 968 requirements=URL_NAME_REQUIREMENTS)
971 969
972 970 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
973 971 controller='changelog', action='changelog_summary',
974 972 conditions={'function': check_repo},
975 973 requirements=URL_NAME_REQUIREMENTS)
976 974
977 975 rmap.connect('changelog_file_home',
978 976 '/{repo_name}/changelog/{revision}/{f_path}',
979 977 controller='changelog', f_path=None,
980 978 conditions={'function': check_repo},
981 979 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
982 980
983 981 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
984 982 controller='changelog', action='changelog_elements',
985 983 conditions={'function': check_repo},
986 984 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
987 985
988 986 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
989 987 controller='files', revision='tip', f_path='',
990 988 conditions={'function': check_repo},
991 989 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
992 990
993 991 rmap.connect('files_home_simple_catchrev',
994 992 '/{repo_name}/files/{revision}',
995 993 controller='files', revision='tip', f_path='',
996 994 conditions={'function': check_repo},
997 995 requirements=URL_NAME_REQUIREMENTS)
998 996
999 997 rmap.connect('files_home_simple_catchall',
1000 998 '/{repo_name}/files',
1001 999 controller='files', revision='tip', f_path='',
1002 1000 conditions={'function': check_repo},
1003 1001 requirements=URL_NAME_REQUIREMENTS)
1004 1002
1005 1003 rmap.connect('files_history_home',
1006 1004 '/{repo_name}/history/{revision}/{f_path}',
1007 1005 controller='files', action='history', revision='tip', f_path='',
1008 1006 conditions={'function': check_repo},
1009 1007 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1010 1008
1011 1009 rmap.connect('files_authors_home',
1012 1010 '/{repo_name}/authors/{revision}/{f_path}',
1013 1011 controller='files', action='authors', revision='tip', f_path='',
1014 1012 conditions={'function': check_repo},
1015 1013 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1016 1014
1017 1015 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1018 1016 controller='files', action='diff', f_path='',
1019 1017 conditions={'function': check_repo},
1020 1018 requirements=URL_NAME_REQUIREMENTS)
1021 1019
1022 1020 rmap.connect('files_diff_2way_home',
1023 1021 '/{repo_name}/diff-2way/{f_path}',
1024 1022 controller='files', action='diff_2way', f_path='',
1025 1023 conditions={'function': check_repo},
1026 1024 requirements=URL_NAME_REQUIREMENTS)
1027 1025
1028 1026 rmap.connect('files_rawfile_home',
1029 1027 '/{repo_name}/rawfile/{revision}/{f_path}',
1030 1028 controller='files', action='rawfile', revision='tip',
1031 1029 f_path='', conditions={'function': check_repo},
1032 1030 requirements=URL_NAME_REQUIREMENTS)
1033 1031
1034 1032 rmap.connect('files_raw_home',
1035 1033 '/{repo_name}/raw/{revision}/{f_path}',
1036 1034 controller='files', action='raw', revision='tip', f_path='',
1037 1035 conditions={'function': check_repo},
1038 1036 requirements=URL_NAME_REQUIREMENTS)
1039 1037
1040 1038 rmap.connect('files_render_home',
1041 1039 '/{repo_name}/render/{revision}/{f_path}',
1042 1040 controller='files', action='index', revision='tip', f_path='',
1043 1041 rendered=True, conditions={'function': check_repo},
1044 1042 requirements=URL_NAME_REQUIREMENTS)
1045 1043
1046 1044 rmap.connect('files_annotate_home',
1047 1045 '/{repo_name}/annotate/{revision}/{f_path}',
1048 1046 controller='files', action='index', revision='tip',
1049 1047 f_path='', annotate=True, conditions={'function': check_repo},
1050 1048 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1051 1049
1052 1050 rmap.connect('files_annotate_previous',
1053 1051 '/{repo_name}/annotate-previous/{revision}/{f_path}',
1054 1052 controller='files', action='annotate_previous', revision='tip',
1055 1053 f_path='', annotate=True, conditions={'function': check_repo},
1056 1054 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1057 1055
1058 1056 rmap.connect('files_edit',
1059 1057 '/{repo_name}/edit/{revision}/{f_path}',
1060 1058 controller='files', action='edit', revision='tip',
1061 1059 f_path='',
1062 1060 conditions={'function': check_repo, 'method': ['POST']},
1063 1061 requirements=URL_NAME_REQUIREMENTS)
1064 1062
1065 1063 rmap.connect('files_edit_home',
1066 1064 '/{repo_name}/edit/{revision}/{f_path}',
1067 1065 controller='files', action='edit_home', revision='tip',
1068 1066 f_path='', conditions={'function': check_repo},
1069 1067 requirements=URL_NAME_REQUIREMENTS)
1070 1068
1071 1069 rmap.connect('files_add',
1072 1070 '/{repo_name}/add/{revision}/{f_path}',
1073 1071 controller='files', action='add', revision='tip',
1074 1072 f_path='',
1075 1073 conditions={'function': check_repo, 'method': ['POST']},
1076 1074 requirements=URL_NAME_REQUIREMENTS)
1077 1075
1078 1076 rmap.connect('files_add_home',
1079 1077 '/{repo_name}/add/{revision}/{f_path}',
1080 1078 controller='files', action='add_home', revision='tip',
1081 1079 f_path='', conditions={'function': check_repo},
1082 1080 requirements=URL_NAME_REQUIREMENTS)
1083 1081
1084 1082 rmap.connect('files_delete',
1085 1083 '/{repo_name}/delete/{revision}/{f_path}',
1086 1084 controller='files', action='delete', revision='tip',
1087 1085 f_path='',
1088 1086 conditions={'function': check_repo, 'method': ['POST']},
1089 1087 requirements=URL_NAME_REQUIREMENTS)
1090 1088
1091 1089 rmap.connect('files_delete_home',
1092 1090 '/{repo_name}/delete/{revision}/{f_path}',
1093 1091 controller='files', action='delete_home', revision='tip',
1094 1092 f_path='', conditions={'function': check_repo},
1095 1093 requirements=URL_NAME_REQUIREMENTS)
1096 1094
1097 1095 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1098 1096 controller='files', action='archivefile',
1099 1097 conditions={'function': check_repo},
1100 1098 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1101 1099
1102 1100 rmap.connect('files_nodelist_home',
1103 1101 '/{repo_name}/nodelist/{revision}/{f_path}',
1104 1102 controller='files', action='nodelist',
1105 1103 conditions={'function': check_repo},
1106 1104 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1107 1105
1108 1106 rmap.connect('files_nodetree_full',
1109 1107 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1110 1108 controller='files', action='nodetree_full',
1111 1109 conditions={'function': check_repo},
1112 1110 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1113 1111
1114 1112 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1115 1113 controller='forks', action='fork_create',
1116 1114 conditions={'function': check_repo, 'method': ['POST']},
1117 1115 requirements=URL_NAME_REQUIREMENTS)
1118 1116
1119 1117 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1120 1118 controller='forks', action='fork',
1121 1119 conditions={'function': check_repo},
1122 1120 requirements=URL_NAME_REQUIREMENTS)
1123 1121
1124 1122 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1125 1123 controller='forks', action='forks',
1126 1124 conditions={'function': check_repo},
1127 1125 requirements=URL_NAME_REQUIREMENTS)
1128 1126
1129 1127 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1130 1128 controller='followers', action='followers',
1131 1129 conditions={'function': check_repo},
1132 1130 requirements=URL_NAME_REQUIREMENTS)
1133 1131
1134 1132 # must be here for proper group/repo catching pattern
1135 1133 _connect_with_slash(
1136 1134 rmap, 'repo_group_home', '/{group_name}',
1137 1135 controller='home', action='index_repo_group',
1138 1136 conditions={'function': check_group},
1139 1137 requirements=URL_NAME_REQUIREMENTS)
1140 1138
1141 1139 # catch all, at the end
1142 1140 _connect_with_slash(
1143 1141 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1144 1142 controller='summary', action='index',
1145 1143 conditions={'function': check_repo},
1146 1144 requirements=URL_NAME_REQUIREMENTS)
1147 1145
1148 1146 return rmap
1149 1147
1150 1148
1151 1149 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1152 1150 """
1153 1151 Connect a route with an optional trailing slash in `path`.
1154 1152 """
1155 1153 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1156 1154 mapper.connect(name, path, *args, **kwargs)
@@ -1,418 +1,418 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27 import datetime
28 28
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pyramid.threadlocal import get_current_registry
32 32 from pylons import request, tmpl_context as c, url, session
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.orm import joinedload
36 36
37 37 from rhodecode import forms
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib import auth
40 40 from rhodecode.lib.auth import (
41 41 LoginRequired, NotAnonymous, AuthUser)
42 42 from rhodecode.lib.base import BaseController, render
43 43 from rhodecode.lib.utils import jsonify
44 44 from rhodecode.lib.utils2 import safe_int, md5, str2bool
45 45 from rhodecode.lib.ext_json import json
46 46 from rhodecode.lib.channelstream import channelstream_request, \
47 47 ChannelstreamException
48 48
49 49 from rhodecode.model.validation_schema.schemas import user_schema
50 50 from rhodecode.model.db import (
51 51 Repository, PullRequest, UserEmailMap, User, UserFollowing)
52 52 from rhodecode.model.forms import UserForm
53 53 from rhodecode.model.scm import RepoList
54 54 from rhodecode.model.user import UserModel
55 55 from rhodecode.model.repo import RepoModel
56 56 from rhodecode.model.meta import Session
57 57 from rhodecode.model.pull_request import PullRequestModel
58 58 from rhodecode.model.comment import CommentsModel
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class MyAccountController(BaseController):
64 64 """REST Controller styled on the Atom Publishing Protocol"""
65 65 # To properly map this controller, ensure your config/routing.py
66 66 # file has a resource setup:
67 67 # map.resource('setting', 'settings', controller='admin/settings',
68 68 # path_prefix='/admin', name_prefix='admin_')
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 def __before__(self):
73 73 super(MyAccountController, self).__before__()
74 74
75 75 def __load_data(self):
76 76 c.user = User.get(c.rhodecode_user.user_id)
77 77 if c.user.username == User.DEFAULT_USER:
78 78 h.flash(_("You can't edit this user since it's"
79 79 " crucial for entire application"), category='warning')
80 return redirect(url('users'))
80 return redirect(h.route_path('users'))
81 81
82 82 c.auth_user = AuthUser(
83 83 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
84 84
85 85 def _load_my_repos_data(self, watched=False):
86 86 if watched:
87 87 admin = False
88 88 follows_repos = Session().query(UserFollowing)\
89 89 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
90 90 .options(joinedload(UserFollowing.follows_repository))\
91 91 .all()
92 92 repo_list = [x.follows_repository for x in follows_repos]
93 93 else:
94 94 admin = True
95 95 repo_list = Repository.get_all_repos(
96 96 user_id=c.rhodecode_user.user_id)
97 97 repo_list = RepoList(repo_list, perm_set=[
98 98 'repository.read', 'repository.write', 'repository.admin'])
99 99
100 100 repos_data = RepoModel().get_repos_as_dict(
101 101 repo_list=repo_list, admin=admin)
102 102 # json used to render the grid
103 103 return json.dumps(repos_data)
104 104
105 105 @auth.CSRFRequired()
106 106 def my_account_update(self):
107 107 """
108 108 POST /_admin/my_account Updates info of my account
109 109 """
110 110 # url('my_account')
111 111 c.active = 'profile_edit'
112 112 self.__load_data()
113 113 c.perm_user = c.auth_user
114 114 c.extern_type = c.user.extern_type
115 115 c.extern_name = c.user.extern_name
116 116
117 117 defaults = c.user.get_dict()
118 118 update = False
119 119 _form = UserForm(edit=True,
120 120 old_data={'user_id': c.rhodecode_user.user_id,
121 121 'email': c.rhodecode_user.email})()
122 122 form_result = {}
123 123 try:
124 124 post_data = dict(request.POST)
125 125 post_data['new_password'] = ''
126 126 post_data['password_confirmation'] = ''
127 127 form_result = _form.to_python(post_data)
128 128 # skip updating those attrs for my account
129 129 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
130 130 'new_password', 'password_confirmation']
131 131 # TODO: plugin should define if username can be updated
132 132 if c.extern_type != "rhodecode":
133 133 # forbid updating username for external accounts
134 134 skip_attrs.append('username')
135 135
136 136 UserModel().update_user(
137 137 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
138 138 h.flash(_('Your account was updated successfully'),
139 139 category='success')
140 140 Session().commit()
141 141 update = True
142 142
143 143 except formencode.Invalid as errors:
144 144 return htmlfill.render(
145 145 render('admin/my_account/my_account.mako'),
146 146 defaults=errors.value,
147 147 errors=errors.error_dict or {},
148 148 prefix_error=False,
149 149 encoding="UTF-8",
150 150 force_defaults=False)
151 151 except Exception:
152 152 log.exception("Exception updating user")
153 153 h.flash(_('Error occurred during update of user %s')
154 154 % form_result.get('username'), category='error')
155 155
156 156 if update:
157 157 return redirect('my_account')
158 158
159 159 return htmlfill.render(
160 160 render('admin/my_account/my_account.mako'),
161 161 defaults=defaults,
162 162 encoding="UTF-8",
163 163 force_defaults=False
164 164 )
165 165
166 166 def my_account(self):
167 167 """
168 168 GET /_admin/my_account Displays info about my account
169 169 """
170 170 # url('my_account')
171 171 c.active = 'profile'
172 172 self.__load_data()
173 173
174 174 defaults = c.user.get_dict()
175 175 return htmlfill.render(
176 176 render('admin/my_account/my_account.mako'),
177 177 defaults=defaults, encoding="UTF-8", force_defaults=False)
178 178
179 179 def my_account_edit(self):
180 180 """
181 181 GET /_admin/my_account/edit Displays edit form of my account
182 182 """
183 183 c.active = 'profile_edit'
184 184 self.__load_data()
185 185 c.perm_user = c.auth_user
186 186 c.extern_type = c.user.extern_type
187 187 c.extern_name = c.user.extern_name
188 188
189 189 defaults = c.user.get_dict()
190 190 return htmlfill.render(
191 191 render('admin/my_account/my_account.mako'),
192 192 defaults=defaults,
193 193 encoding="UTF-8",
194 194 force_defaults=False
195 195 )
196 196
197 197 @auth.CSRFRequired(except_methods=['GET'])
198 198 def my_account_password(self):
199 199 c.active = 'password'
200 200 self.__load_data()
201 201 c.extern_type = c.user.extern_type
202 202
203 203 schema = user_schema.ChangePasswordSchema().bind(
204 204 username=c.rhodecode_user.username)
205 205
206 206 form = forms.Form(schema,
207 207 buttons=(forms.buttons.save, forms.buttons.reset))
208 208
209 209 if request.method == 'POST' and c.extern_type == 'rhodecode':
210 210 controls = request.POST.items()
211 211 try:
212 212 valid_data = form.validate(controls)
213 213 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
214 214 instance = c.rhodecode_user.get_instance()
215 215 instance.update_userdata(force_password_change=False)
216 216 Session().commit()
217 217 except forms.ValidationFailure as e:
218 218 request.session.flash(
219 219 _('Error occurred during update of user password'),
220 220 queue='error')
221 221 form = e
222 222 except Exception:
223 223 log.exception("Exception updating password")
224 224 request.session.flash(
225 225 _('Error occurred during update of user password'),
226 226 queue='error')
227 227 else:
228 228 session.setdefault('rhodecode_user', {}).update(
229 229 {'password': md5(instance.password)})
230 230 session.save()
231 231 request.session.flash(
232 232 _("Successfully updated password"), queue='success')
233 233 return redirect(url('my_account_password'))
234 234
235 235 c.form = form
236 236 return render('admin/my_account/my_account.mako')
237 237
238 238 def my_account_repos(self):
239 239 c.active = 'repos'
240 240 self.__load_data()
241 241
242 242 # json used to render the grid
243 243 c.data = self._load_my_repos_data()
244 244 return render('admin/my_account/my_account.mako')
245 245
246 246 def my_account_watched(self):
247 247 c.active = 'watched'
248 248 self.__load_data()
249 249
250 250 # json used to render the grid
251 251 c.data = self._load_my_repos_data(watched=True)
252 252 return render('admin/my_account/my_account.mako')
253 253
254 254 def my_account_perms(self):
255 255 c.active = 'perms'
256 256 self.__load_data()
257 257 c.perm_user = c.auth_user
258 258
259 259 return render('admin/my_account/my_account.mako')
260 260
261 261 def my_account_emails(self):
262 262 c.active = 'emails'
263 263 self.__load_data()
264 264
265 265 c.user_email_map = UserEmailMap.query()\
266 266 .filter(UserEmailMap.user == c.user).all()
267 267 return render('admin/my_account/my_account.mako')
268 268
269 269 @auth.CSRFRequired()
270 270 def my_account_emails_add(self):
271 271 email = request.POST.get('new_email')
272 272
273 273 try:
274 274 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
275 275 Session().commit()
276 276 h.flash(_("Added new email address `%s` for user account") % email,
277 277 category='success')
278 278 except formencode.Invalid as error:
279 279 msg = error.error_dict['email']
280 280 h.flash(msg, category='error')
281 281 except Exception:
282 282 log.exception("Exception in my_account_emails")
283 283 h.flash(_('An error occurred during email saving'),
284 284 category='error')
285 285 return redirect(url('my_account_emails'))
286 286
287 287 @auth.CSRFRequired()
288 288 def my_account_emails_delete(self):
289 289 email_id = request.POST.get('del_email_id')
290 290 user_model = UserModel()
291 291 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
292 292 Session().commit()
293 293 h.flash(_("Removed email address from user account"),
294 294 category='success')
295 295 return redirect(url('my_account_emails'))
296 296
297 297 def _extract_ordering(self, request):
298 298 column_index = safe_int(request.GET.get('order[0][column]'))
299 299 order_dir = request.GET.get('order[0][dir]', 'desc')
300 300 order_by = request.GET.get(
301 301 'columns[%s][data][sort]' % column_index, 'name_raw')
302 302 return order_by, order_dir
303 303
304 304 def _get_pull_requests_list(self, statuses):
305 305 start = safe_int(request.GET.get('start'), 0)
306 306 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
307 307 order_by, order_dir = self._extract_ordering(request)
308 308
309 309 pull_requests = PullRequestModel().get_im_participating_in(
310 310 user_id=c.rhodecode_user.user_id,
311 311 statuses=statuses,
312 312 offset=start, length=length, order_by=order_by,
313 313 order_dir=order_dir)
314 314
315 315 pull_requests_total_count = PullRequestModel().count_im_participating_in(
316 316 user_id=c.rhodecode_user.user_id, statuses=statuses)
317 317
318 318 from rhodecode.lib.utils import PartialRenderer
319 319 _render = PartialRenderer('data_table/_dt_elements.mako')
320 320 data = []
321 321 for pr in pull_requests:
322 322 repo_id = pr.target_repo_id
323 323 comments = CommentsModel().get_all_comments(
324 324 repo_id, pull_request=pr)
325 325 owned = pr.user_id == c.rhodecode_user.user_id
326 326 status = pr.calculated_review_status()
327 327
328 328 data.append({
329 329 'target_repo': _render('pullrequest_target_repo',
330 330 pr.target_repo.repo_name),
331 331 'name': _render('pullrequest_name',
332 332 pr.pull_request_id, pr.target_repo.repo_name,
333 333 short=True),
334 334 'name_raw': pr.pull_request_id,
335 335 'status': _render('pullrequest_status', status),
336 336 'title': _render(
337 337 'pullrequest_title', pr.title, pr.description),
338 338 'description': h.escape(pr.description),
339 339 'updated_on': _render('pullrequest_updated_on',
340 340 h.datetime_to_time(pr.updated_on)),
341 341 'updated_on_raw': h.datetime_to_time(pr.updated_on),
342 342 'created_on': _render('pullrequest_updated_on',
343 343 h.datetime_to_time(pr.created_on)),
344 344 'created_on_raw': h.datetime_to_time(pr.created_on),
345 345 'author': _render('pullrequest_author',
346 346 pr.author.full_contact, ),
347 347 'author_raw': pr.author.full_name,
348 348 'comments': _render('pullrequest_comments', len(comments)),
349 349 'comments_raw': len(comments),
350 350 'closed': pr.is_closed(),
351 351 'owned': owned
352 352 })
353 353 # json used to render the grid
354 354 data = ({
355 355 'data': data,
356 356 'recordsTotal': pull_requests_total_count,
357 357 'recordsFiltered': pull_requests_total_count,
358 358 })
359 359 return data
360 360
361 361 def my_account_pullrequests(self):
362 362 c.active = 'pullrequests'
363 363 self.__load_data()
364 364 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
365 365
366 366 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
367 367 if c.show_closed:
368 368 statuses += [PullRequest.STATUS_CLOSED]
369 369 data = self._get_pull_requests_list(statuses)
370 370 if not request.is_xhr:
371 371 c.data_participate = json.dumps(data['data'])
372 372 c.records_total_participate = data['recordsTotal']
373 373 return render('admin/my_account/my_account.mako')
374 374 else:
375 375 return json.dumps(data)
376 376
377 377 def my_notifications(self):
378 378 c.active = 'notifications'
379 379 return render('admin/my_account/my_account.mako')
380 380
381 381 @auth.CSRFRequired()
382 382 @jsonify
383 383 def my_notifications_toggle_visibility(self):
384 384 user = c.rhodecode_user.get_instance()
385 385 new_status = not user.user_data.get('notification_status', True)
386 386 user.update_userdata(notification_status=new_status)
387 387 Session().commit()
388 388 return user.user_data['notification_status']
389 389
390 390 @auth.CSRFRequired()
391 391 @jsonify
392 392 def my_account_notifications_test_channelstream(self):
393 393 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
394 394 c.rhodecode_user.username, datetime.datetime.now())
395 395 payload = {
396 396 'type': 'message',
397 397 'timestamp': datetime.datetime.utcnow(),
398 398 'user': 'system',
399 399 #'channel': 'broadcast',
400 400 'pm_users': [c.rhodecode_user.username],
401 401 'message': {
402 402 'message': message,
403 403 'level': 'info',
404 404 'topic': '/notifications'
405 405 }
406 406 }
407 407
408 408 registry = get_current_registry()
409 409 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
410 410 channelstream_config = rhodecode_plugins.get('channelstream', {})
411 411
412 412 try:
413 413 channelstream_request(channelstream_config, [payload], '/message')
414 414 except ChannelstreamException as e:
415 415 log.exception('Failed to send channelstream data')
416 416 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
417 417 return {"response": 'Channelstream data sent. '
418 418 'You should see a new live message now.'}
@@ -1,677 +1,630 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Users crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 from formencode import htmlfill
29 29 from pylons import request, tmpl_context as c, url, config
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.lib.exceptions import (
35 35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
36 36 UserOwnsUserGroupsException, UserCreationError)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import auth
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43
44 44 from rhodecode.model.db import (
45 45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
46 46 from rhodecode.model.forms import (
47 47 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.permission import PermissionModel
52 52 from rhodecode.lib.utils import action_logger
53 from rhodecode.lib.ext_json import json
54 53 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
55 54
56 55 log = logging.getLogger(__name__)
57 56
58 57
59 58 class UsersController(BaseController):
60 59 """REST Controller styled on the Atom Publishing Protocol"""
61 60
62 61 @LoginRequired()
63 62 def __before__(self):
64 63 super(UsersController, self).__before__()
65 64 c.available_permissions = config['available_permissions']
66 65 c.allowed_languages = [
67 66 ('en', 'English (en)'),
68 67 ('de', 'German (de)'),
69 68 ('fr', 'French (fr)'),
70 69 ('it', 'Italian (it)'),
71 70 ('ja', 'Japanese (ja)'),
72 71 ('pl', 'Polish (pl)'),
73 72 ('pt', 'Portuguese (pt)'),
74 73 ('ru', 'Russian (ru)'),
75 74 ('zh', 'Chinese (zh)'),
76 75 ]
77 76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
78 77
79 @HasPermissionAllDecorator('hg.admin')
80 def index(self):
81 """GET /users: All items in the collection"""
82 # url('users')
83
84 from rhodecode.lib.utils import PartialRenderer
85 _render = PartialRenderer('data_table/_dt_elements.mako')
86
87 def username(user_id, username):
88 return _render("user_name", user_id, username)
89
90 def user_actions(user_id, username):
91 return _render("user_actions", user_id, username)
92
93 # json generate
94 c.users_list = User.query()\
95 .filter(User.username != User.DEFAULT_USER) \
96 .all()
97
98 users_data = []
99 for user in c.users_list:
100 users_data.append({
101 "username": h.gravatar_with_user(user.username),
102 "username_raw": user.username,
103 "email": user.email,
104 "first_name": h.escape(user.name),
105 "last_name": h.escape(user.lastname),
106 "last_login": h.format_date(user.last_login),
107 "last_login_raw": datetime_to_time(user.last_login),
108 "last_activity": h.format_date(
109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
110 "last_activity_raw": user.user_data.get('last_activity', 0),
111 "active": h.bool2icon(user.active),
112 "active_raw": user.active,
113 "admin": h.bool2icon(user.admin),
114 "admin_raw": user.admin,
115 "extern_type": user.extern_type,
116 "extern_name": user.extern_name,
117 "action": user_actions(user.user_id, user.username),
118 })
119
120
121 c.data = json.dumps(users_data)
122 return render('admin/users/users.mako')
123
124 78 def _get_personal_repo_group_template_vars(self):
125 79 DummyUser = AttributeDict({
126 80 'username': '${username}',
127 81 'user_id': '${user_id}',
128 82 })
129 83 c.default_create_repo_group = RepoGroupModel() \
130 84 .get_default_create_personal_repo_group()
131 85 c.personal_repo_group_name = RepoGroupModel() \
132 86 .get_personal_group_name(DummyUser)
133 87
134 88 @HasPermissionAllDecorator('hg.admin')
135 89 @auth.CSRFRequired()
136 90 def create(self):
137 91 """POST /users: Create a new item"""
138 # url('users')
139 92 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
140 93 user_model = UserModel()
141 94 user_form = UserForm()()
142 95 try:
143 96 form_result = user_form.to_python(dict(request.POST))
144 97 user = user_model.create(form_result)
145 98 Session().flush()
146 99 username = form_result['username']
147 100 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
148 101 None, self.ip_addr, self.sa)
149 102
150 103 user_link = h.link_to(h.escape(username),
151 104 url('edit_user',
152 105 user_id=user.user_id))
153 106 h.flash(h.literal(_('Created user %(user_link)s')
154 107 % {'user_link': user_link}), category='success')
155 108 Session().commit()
156 109 except formencode.Invalid as errors:
157 110 self._get_personal_repo_group_template_vars()
158 111 return htmlfill.render(
159 112 render('admin/users/user_add.mako'),
160 113 defaults=errors.value,
161 114 errors=errors.error_dict or {},
162 115 prefix_error=False,
163 116 encoding="UTF-8",
164 117 force_defaults=False)
165 118 except UserCreationError as e:
166 119 h.flash(e, 'error')
167 120 except Exception:
168 121 log.exception("Exception creation of user")
169 122 h.flash(_('Error occurred during creation of user %s')
170 123 % request.POST.get('username'), category='error')
171 return redirect(url('users'))
124 return redirect(h.route_path('users'))
172 125
173 126 @HasPermissionAllDecorator('hg.admin')
174 127 def new(self):
175 128 """GET /users/new: Form to create a new item"""
176 129 # url('new_user')
177 130 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
178 131 self._get_personal_repo_group_template_vars()
179 132 return render('admin/users/user_add.mako')
180 133
181 134 @HasPermissionAllDecorator('hg.admin')
182 135 @auth.CSRFRequired()
183 136 def update(self, user_id):
184 137 """PUT /users/user_id: Update an existing item"""
185 138 # Forms posted to this method should contain a hidden field:
186 139 # <input type="hidden" name="_method" value="PUT" />
187 140 # Or using helpers:
188 141 # h.form(url('update_user', user_id=ID),
189 142 # method='put')
190 143 # url('user', user_id=ID)
191 144 user_id = safe_int(user_id)
192 145 c.user = User.get_or_404(user_id)
193 146 c.active = 'profile'
194 147 c.extern_type = c.user.extern_type
195 148 c.extern_name = c.user.extern_name
196 149 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
197 150 available_languages = [x[0] for x in c.allowed_languages]
198 151 _form = UserForm(edit=True, available_languages=available_languages,
199 152 old_data={'user_id': user_id,
200 153 'email': c.user.email})()
201 154 form_result = {}
202 155 try:
203 156 form_result = _form.to_python(dict(request.POST))
204 157 skip_attrs = ['extern_type', 'extern_name']
205 158 # TODO: plugin should define if username can be updated
206 159 if c.extern_type != "rhodecode":
207 160 # forbid updating username for external accounts
208 161 skip_attrs.append('username')
209 162
210 163 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
211 164 usr = form_result['username']
212 165 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
213 166 None, self.ip_addr, self.sa)
214 167 h.flash(_('User updated successfully'), category='success')
215 168 Session().commit()
216 169 except formencode.Invalid as errors:
217 170 defaults = errors.value
218 171 e = errors.error_dict or {}
219 172
220 173 return htmlfill.render(
221 174 render('admin/users/user_edit.mako'),
222 175 defaults=defaults,
223 176 errors=e,
224 177 prefix_error=False,
225 178 encoding="UTF-8",
226 179 force_defaults=False)
227 180 except UserCreationError as e:
228 181 h.flash(e, 'error')
229 182 except Exception:
230 183 log.exception("Exception updating user")
231 184 h.flash(_('Error occurred during update of user %s')
232 185 % form_result.get('username'), category='error')
233 186 return redirect(url('edit_user', user_id=user_id))
234 187
235 188 @HasPermissionAllDecorator('hg.admin')
236 189 @auth.CSRFRequired()
237 190 def delete(self, user_id):
238 191 """DELETE /users/user_id: Delete an existing item"""
239 192 # Forms posted to this method should contain a hidden field:
240 193 # <input type="hidden" name="_method" value="DELETE" />
241 194 # Or using helpers:
242 195 # h.form(url('delete_user', user_id=ID),
243 196 # method='delete')
244 197 # url('user', user_id=ID)
245 198 user_id = safe_int(user_id)
246 199 c.user = User.get_or_404(user_id)
247 200
248 201 _repos = c.user.repositories
249 202 _repo_groups = c.user.repository_groups
250 203 _user_groups = c.user.user_groups
251 204
252 205 handle_repos = None
253 206 handle_repo_groups = None
254 207 handle_user_groups = None
255 208 # dummy call for flash of handle
256 209 set_handle_flash_repos = lambda: None
257 210 set_handle_flash_repo_groups = lambda: None
258 211 set_handle_flash_user_groups = lambda: None
259 212
260 213 if _repos and request.POST.get('user_repos'):
261 214 do = request.POST['user_repos']
262 215 if do == 'detach':
263 216 handle_repos = 'detach'
264 217 set_handle_flash_repos = lambda: h.flash(
265 218 _('Detached %s repositories') % len(_repos),
266 219 category='success')
267 220 elif do == 'delete':
268 221 handle_repos = 'delete'
269 222 set_handle_flash_repos = lambda: h.flash(
270 223 _('Deleted %s repositories') % len(_repos),
271 224 category='success')
272 225
273 226 if _repo_groups and request.POST.get('user_repo_groups'):
274 227 do = request.POST['user_repo_groups']
275 228 if do == 'detach':
276 229 handle_repo_groups = 'detach'
277 230 set_handle_flash_repo_groups = lambda: h.flash(
278 231 _('Detached %s repository groups') % len(_repo_groups),
279 232 category='success')
280 233 elif do == 'delete':
281 234 handle_repo_groups = 'delete'
282 235 set_handle_flash_repo_groups = lambda: h.flash(
283 236 _('Deleted %s repository groups') % len(_repo_groups),
284 237 category='success')
285 238
286 239 if _user_groups and request.POST.get('user_user_groups'):
287 240 do = request.POST['user_user_groups']
288 241 if do == 'detach':
289 242 handle_user_groups = 'detach'
290 243 set_handle_flash_user_groups = lambda: h.flash(
291 244 _('Detached %s user groups') % len(_user_groups),
292 245 category='success')
293 246 elif do == 'delete':
294 247 handle_user_groups = 'delete'
295 248 set_handle_flash_user_groups = lambda: h.flash(
296 249 _('Deleted %s user groups') % len(_user_groups),
297 250 category='success')
298 251
299 252 try:
300 253 UserModel().delete(c.user, handle_repos=handle_repos,
301 254 handle_repo_groups=handle_repo_groups,
302 255 handle_user_groups=handle_user_groups)
303 256 Session().commit()
304 257 set_handle_flash_repos()
305 258 set_handle_flash_repo_groups()
306 259 set_handle_flash_user_groups()
307 260 h.flash(_('Successfully deleted user'), category='success')
308 261 except (UserOwnsReposException, UserOwnsRepoGroupsException,
309 262 UserOwnsUserGroupsException, DefaultUserException) as e:
310 263 h.flash(e, category='warning')
311 264 except Exception:
312 265 log.exception("Exception during deletion of user")
313 266 h.flash(_('An error occurred during deletion of user'),
314 267 category='error')
315 return redirect(url('users'))
268 return redirect(h.route_path('users'))
316 269
317 270 @HasPermissionAllDecorator('hg.admin')
318 271 @auth.CSRFRequired()
319 272 def reset_password(self, user_id):
320 273 """
321 274 toggle reset password flag for this user
322 275
323 276 :param user_id:
324 277 """
325 278 user_id = safe_int(user_id)
326 279 c.user = User.get_or_404(user_id)
327 280 try:
328 281 old_value = c.user.user_data.get('force_password_change')
329 282 c.user.update_userdata(force_password_change=not old_value)
330 283 Session().commit()
331 284 if old_value:
332 285 msg = _('Force password change disabled for user')
333 286 else:
334 287 msg = _('Force password change enabled for user')
335 288 h.flash(msg, category='success')
336 289 except Exception:
337 290 log.exception("Exception during password reset for user")
338 291 h.flash(_('An error occurred during password reset for user'),
339 292 category='error')
340 293
341 294 return redirect(url('edit_user_advanced', user_id=user_id))
342 295
343 296 @HasPermissionAllDecorator('hg.admin')
344 297 @auth.CSRFRequired()
345 298 def create_personal_repo_group(self, user_id):
346 299 """
347 300 Create personal repository group for this user
348 301
349 302 :param user_id:
350 303 """
351 304 from rhodecode.model.repo_group import RepoGroupModel
352 305
353 306 user_id = safe_int(user_id)
354 307 c.user = User.get_or_404(user_id)
355 308 personal_repo_group = RepoGroup.get_user_personal_repo_group(
356 309 c.user.user_id)
357 310 if personal_repo_group:
358 311 return redirect(url('edit_user_advanced', user_id=user_id))
359 312
360 313 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
361 314 c.user)
362 315 named_personal_group = RepoGroup.get_by_group_name(
363 316 personal_repo_group_name)
364 317 try:
365 318
366 319 if named_personal_group and named_personal_group.user_id == c.user.user_id:
367 320 # migrate the same named group, and mark it as personal
368 321 named_personal_group.personal = True
369 322 Session().add(named_personal_group)
370 323 Session().commit()
371 324 msg = _('Linked repository group `%s` as personal' % (
372 325 personal_repo_group_name,))
373 326 h.flash(msg, category='success')
374 327 elif not named_personal_group:
375 328 RepoGroupModel().create_personal_repo_group(c.user)
376 329
377 330 msg = _('Created repository group `%s`' % (
378 331 personal_repo_group_name,))
379 332 h.flash(msg, category='success')
380 333 else:
381 334 msg = _('Repository group `%s` is already taken' % (
382 335 personal_repo_group_name,))
383 336 h.flash(msg, category='warning')
384 337 except Exception:
385 338 log.exception("Exception during repository group creation")
386 339 msg = _(
387 340 'An error occurred during repository group creation for user')
388 341 h.flash(msg, category='error')
389 342 Session().rollback()
390 343
391 344 return redirect(url('edit_user_advanced', user_id=user_id))
392 345
393 346 @HasPermissionAllDecorator('hg.admin')
394 347 def show(self, user_id):
395 348 """GET /users/user_id: Show a specific item"""
396 349 # url('user', user_id=ID)
397 350 User.get_or_404(-1)
398 351
399 352 @HasPermissionAllDecorator('hg.admin')
400 353 def edit(self, user_id):
401 354 """GET /users/user_id/edit: Form to edit an existing item"""
402 355 # url('edit_user', user_id=ID)
403 356 user_id = safe_int(user_id)
404 357 c.user = User.get_or_404(user_id)
405 358 if c.user.username == User.DEFAULT_USER:
406 359 h.flash(_("You can't edit this user"), category='warning')
407 return redirect(url('users'))
360 return redirect(h.route_path('users'))
408 361
409 362 c.active = 'profile'
410 363 c.extern_type = c.user.extern_type
411 364 c.extern_name = c.user.extern_name
412 365 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
413 366
414 367 defaults = c.user.get_dict()
415 368 defaults.update({'language': c.user.user_data.get('language')})
416 369 return htmlfill.render(
417 370 render('admin/users/user_edit.mako'),
418 371 defaults=defaults,
419 372 encoding="UTF-8",
420 373 force_defaults=False)
421 374
422 375 @HasPermissionAllDecorator('hg.admin')
423 376 def edit_advanced(self, user_id):
424 377 user_id = safe_int(user_id)
425 378 user = c.user = User.get_or_404(user_id)
426 379 if user.username == User.DEFAULT_USER:
427 380 h.flash(_("You can't edit this user"), category='warning')
428 return redirect(url('users'))
381 return redirect(h.route_path('users'))
429 382
430 383 c.active = 'advanced'
431 384 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
432 385 c.personal_repo_group = c.perm_user.personal_repo_group
433 386 c.personal_repo_group_name = RepoGroupModel()\
434 387 .get_personal_group_name(user)
435 388 c.first_admin = User.get_first_super_admin()
436 389 defaults = user.get_dict()
437 390
438 391 # Interim workaround if the user participated on any pull requests as a
439 392 # reviewer.
440 393 has_review = bool(PullRequestReviewers.query().filter(
441 394 PullRequestReviewers.user_id == user_id).first())
442 395 c.can_delete_user = not has_review
443 396 c.can_delete_user_message = _(
444 397 'The user participates as reviewer in pull requests and '
445 398 'cannot be deleted. You can set the user to '
446 399 '"inactive" instead of deleting it.') if has_review else ''
447 400
448 401 return htmlfill.render(
449 402 render('admin/users/user_edit.mako'),
450 403 defaults=defaults,
451 404 encoding="UTF-8",
452 405 force_defaults=False)
453 406
454 407 @HasPermissionAllDecorator('hg.admin')
455 408 def edit_global_perms(self, user_id):
456 409 user_id = safe_int(user_id)
457 410 c.user = User.get_or_404(user_id)
458 411 if c.user.username == User.DEFAULT_USER:
459 412 h.flash(_("You can't edit this user"), category='warning')
460 return redirect(url('users'))
413 return redirect(h.route_path('users'))
461 414
462 415 c.active = 'global_perms'
463 416
464 417 c.default_user = User.get_default_user()
465 418 defaults = c.user.get_dict()
466 419 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
467 420 defaults.update(c.default_user.get_default_perms())
468 421 defaults.update(c.user.get_default_perms())
469 422
470 423 return htmlfill.render(
471 424 render('admin/users/user_edit.mako'),
472 425 defaults=defaults,
473 426 encoding="UTF-8",
474 427 force_defaults=False)
475 428
476 429 @HasPermissionAllDecorator('hg.admin')
477 430 @auth.CSRFRequired()
478 431 def update_global_perms(self, user_id):
479 432 """PUT /users_perm/user_id: Update an existing item"""
480 433 # url('user_perm', user_id=ID, method='put')
481 434 user_id = safe_int(user_id)
482 435 user = User.get_or_404(user_id)
483 436 c.active = 'global_perms'
484 437 try:
485 438 # first stage that verifies the checkbox
486 439 _form = UserIndividualPermissionsForm()
487 440 form_result = _form.to_python(dict(request.POST))
488 441 inherit_perms = form_result['inherit_default_permissions']
489 442 user.inherit_default_permissions = inherit_perms
490 443 Session().add(user)
491 444
492 445 if not inherit_perms:
493 446 # only update the individual ones if we un check the flag
494 447 _form = UserPermissionsForm(
495 448 [x[0] for x in c.repo_create_choices],
496 449 [x[0] for x in c.repo_create_on_write_choices],
497 450 [x[0] for x in c.repo_group_create_choices],
498 451 [x[0] for x in c.user_group_create_choices],
499 452 [x[0] for x in c.fork_choices],
500 453 [x[0] for x in c.inherit_default_permission_choices])()
501 454
502 455 form_result = _form.to_python(dict(request.POST))
503 456 form_result.update({'perm_user_id': user.user_id})
504 457
505 458 PermissionModel().update_user_permissions(form_result)
506 459
507 460 Session().commit()
508 461 h.flash(_('User global permissions updated successfully'),
509 462 category='success')
510 463
511 464 Session().commit()
512 465 except formencode.Invalid as errors:
513 466 defaults = errors.value
514 467 c.user = user
515 468 return htmlfill.render(
516 469 render('admin/users/user_edit.mako'),
517 470 defaults=defaults,
518 471 errors=errors.error_dict or {},
519 472 prefix_error=False,
520 473 encoding="UTF-8",
521 474 force_defaults=False)
522 475 except Exception:
523 476 log.exception("Exception during permissions saving")
524 477 h.flash(_('An error occurred during permissions saving'),
525 478 category='error')
526 479 return redirect(url('edit_user_global_perms', user_id=user_id))
527 480
528 481 @HasPermissionAllDecorator('hg.admin')
529 482 def edit_perms_summary(self, user_id):
530 483 user_id = safe_int(user_id)
531 484 c.user = User.get_or_404(user_id)
532 485 if c.user.username == User.DEFAULT_USER:
533 486 h.flash(_("You can't edit this user"), category='warning')
534 return redirect(url('users'))
487 return redirect(h.route_path('users'))
535 488
536 489 c.active = 'perms_summary'
537 490 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
538 491
539 492 return render('admin/users/user_edit.mako')
540 493
541 494 @HasPermissionAllDecorator('hg.admin')
542 495 def edit_emails(self, user_id):
543 496 user_id = safe_int(user_id)
544 497 c.user = User.get_or_404(user_id)
545 498 if c.user.username == User.DEFAULT_USER:
546 499 h.flash(_("You can't edit this user"), category='warning')
547 return redirect(url('users'))
500 return redirect(h.route_path('users'))
548 501
549 502 c.active = 'emails'
550 503 c.user_email_map = UserEmailMap.query() \
551 504 .filter(UserEmailMap.user == c.user).all()
552 505
553 506 defaults = c.user.get_dict()
554 507 return htmlfill.render(
555 508 render('admin/users/user_edit.mako'),
556 509 defaults=defaults,
557 510 encoding="UTF-8",
558 511 force_defaults=False)
559 512
560 513 @HasPermissionAllDecorator('hg.admin')
561 514 @auth.CSRFRequired()
562 515 def add_email(self, user_id):
563 516 """POST /user_emails:Add an existing item"""
564 517 # url('user_emails', user_id=ID, method='put')
565 518 user_id = safe_int(user_id)
566 519 c.user = User.get_or_404(user_id)
567 520
568 521 email = request.POST.get('new_email')
569 522 user_model = UserModel()
570 523
571 524 try:
572 525 user_model.add_extra_email(user_id, email)
573 526 Session().commit()
574 527 h.flash(_("Added new email address `%s` for user account") % email,
575 528 category='success')
576 529 except formencode.Invalid as error:
577 530 msg = error.error_dict['email']
578 531 h.flash(msg, category='error')
579 532 except Exception:
580 533 log.exception("Exception during email saving")
581 534 h.flash(_('An error occurred during email saving'),
582 535 category='error')
583 536 return redirect(url('edit_user_emails', user_id=user_id))
584 537
585 538 @HasPermissionAllDecorator('hg.admin')
586 539 @auth.CSRFRequired()
587 540 def delete_email(self, user_id):
588 541 """DELETE /user_emails_delete/user_id: Delete an existing item"""
589 542 # url('user_emails_delete', user_id=ID, method='delete')
590 543 user_id = safe_int(user_id)
591 544 c.user = User.get_or_404(user_id)
592 545 email_id = request.POST.get('del_email_id')
593 546 user_model = UserModel()
594 547 user_model.delete_extra_email(user_id, email_id)
595 548 Session().commit()
596 549 h.flash(_("Removed email address from user account"), category='success')
597 550 return redirect(url('edit_user_emails', user_id=user_id))
598 551
599 552 @HasPermissionAllDecorator('hg.admin')
600 553 def edit_ips(self, user_id):
601 554 user_id = safe_int(user_id)
602 555 c.user = User.get_or_404(user_id)
603 556 if c.user.username == User.DEFAULT_USER:
604 557 h.flash(_("You can't edit this user"), category='warning')
605 return redirect(url('users'))
558 return redirect(h.route_path('users'))
606 559
607 560 c.active = 'ips'
608 561 c.user_ip_map = UserIpMap.query() \
609 562 .filter(UserIpMap.user == c.user).all()
610 563
611 564 c.inherit_default_ips = c.user.inherit_default_permissions
612 565 c.default_user_ip_map = UserIpMap.query() \
613 566 .filter(UserIpMap.user == User.get_default_user()).all()
614 567
615 568 defaults = c.user.get_dict()
616 569 return htmlfill.render(
617 570 render('admin/users/user_edit.mako'),
618 571 defaults=defaults,
619 572 encoding="UTF-8",
620 573 force_defaults=False)
621 574
622 575 @HasPermissionAllDecorator('hg.admin')
623 576 @auth.CSRFRequired()
624 577 def add_ip(self, user_id):
625 578 """POST /user_ips:Add an existing item"""
626 579 # url('user_ips', user_id=ID, method='put')
627 580
628 581 user_id = safe_int(user_id)
629 582 c.user = User.get_or_404(user_id)
630 583 user_model = UserModel()
631 584 try:
632 585 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
633 586 except Exception as e:
634 587 ip_list = []
635 588 log.exception("Exception during ip saving")
636 589 h.flash(_('An error occurred during ip saving:%s' % (e,)),
637 590 category='error')
638 591
639 592 desc = request.POST.get('description')
640 593 added = []
641 594 for ip in ip_list:
642 595 try:
643 596 user_model.add_extra_ip(user_id, ip, desc)
644 597 Session().commit()
645 598 added.append(ip)
646 599 except formencode.Invalid as error:
647 600 msg = error.error_dict['ip']
648 601 h.flash(msg, category='error')
649 602 except Exception:
650 603 log.exception("Exception during ip saving")
651 604 h.flash(_('An error occurred during ip saving'),
652 605 category='error')
653 606 if added:
654 607 h.flash(
655 608 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
656 609 category='success')
657 610 if 'default_user' in request.POST:
658 611 return redirect(url('admin_permissions_ips'))
659 612 return redirect(url('edit_user_ips', user_id=user_id))
660 613
661 614 @HasPermissionAllDecorator('hg.admin')
662 615 @auth.CSRFRequired()
663 616 def delete_ip(self, user_id):
664 617 """DELETE /user_ips_delete/user_id: Delete an existing item"""
665 618 # url('user_ips_delete', user_id=ID, method='delete')
666 619 user_id = safe_int(user_id)
667 620 c.user = User.get_or_404(user_id)
668 621
669 622 ip_id = request.POST.get('del_ip_id')
670 623 user_model = UserModel()
671 624 user_model.delete_extra_ip(user_id, ip_id)
672 625 Session().commit()
673 626 h.flash(_("Removed ip address from user whitelist"), category='success')
674 627
675 628 if 'default_user' in request.POST:
676 629 return redirect(url('admin_permissions_ips'))
677 630 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,147 +1,147 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Add user')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10 <%def name="breadcrumbs_links()">
11 11 ${h.link_to(_('Admin'),h.url('admin_home'))}
12 12 &raquo;
13 ${h.link_to(_('Users'),h.url('users'))}
13 ${h.link_to(_('Users'),h.route_path('users'))}
14 14 &raquo;
15 15 ${_('Add User')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='admin')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <!-- box / title -->
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 <!-- end box / title -->
29 29 ${h.secure_form(url('users'))}
30 30 <div class="form">
31 31 <!-- fields -->
32 32 <div class="fields">
33 33 <div class="field">
34 34 <div class="label">
35 35 <label for="username">${_('Username')}:</label>
36 36 </div>
37 37 <div class="input">
38 38 ${h.text('username', class_='medium')}
39 39 </div>
40 40 </div>
41 41
42 42 <div class="field">
43 43 <div class="label">
44 44 <label for="password">${_('Password')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 ${h.password('password', class_='medium')}
48 48 </div>
49 49 </div>
50 50
51 51 <div class="field">
52 52 <div class="label">
53 53 <label for="password_confirmation">${_('Password confirmation')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 ${h.password('password_confirmation',autocomplete="off", class_='medium')}
57 57 <div class="info-block">
58 58 <a id="generate_password" href="#">
59 59 <i class="icon-lock"></i> ${_('Generate password')}
60 60 </a>
61 61 <span id="generate_password_preview"></span>
62 62 </div>
63 63 </div>
64 64 </div>
65 65
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="firstname">${_('First Name')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.text('firstname', class_='medium')}
72 72 </div>
73 73 </div>
74 74
75 75 <div class="field">
76 76 <div class="label">
77 77 <label for="lastname">${_('Last Name')}:</label>
78 78 </div>
79 79 <div class="input">
80 80 ${h.text('lastname', class_='medium')}
81 81 </div>
82 82 </div>
83 83
84 84 <div class="field">
85 85 <div class="label">
86 86 <label for="email">${_('Email')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 ${h.text('email', class_='medium')}
90 90 ${h.hidden('extern_name', c.default_extern_type)}
91 91 ${h.hidden('extern_type', c.default_extern_type)}
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-checkbox">
97 97 <label for="active">${_('Active')}:</label>
98 98 </div>
99 99 <div class="checkboxes">
100 100 ${h.checkbox('active',value=True,checked='checked')}
101 101 </div>
102 102 </div>
103 103
104 104 <div class="field">
105 105 <div class="label label-checkbox">
106 106 <label for="password_change">${_('Password change')}:</label>
107 107 </div>
108 108 <div class="checkboxes">
109 109 ${h.checkbox('password_change',value=True)}
110 110 <span class="help-block">${_('Force user to change his password on the next login')}</span>
111 111 </div>
112 112 </div>
113 113
114 114 <div class="field">
115 115 <div class="label label-checkbox">
116 116 <label for="create_repo_group">${_('Add personal repository group')}:</label>
117 117 </div>
118 118 <div class="checkboxes">
119 119 ${h.checkbox('create_repo_group',value=True, checked=c.default_create_repo_group)}
120 120 <span class="help-block">
121 121 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}<br/>
122 122 ${_('User will be automatically set as this group owner.')}
123 123 </span>
124 124 </div>
125 125 </div>
126 126
127 127 <div class="buttons">
128 128 ${h.submit('save',_('Save'),class_="btn")}
129 129 </div>
130 130 </div>
131 131 </div>
132 132 ${h.end_form()}
133 133 </div>
134 134 <script>
135 135 $(document).ready(function(){
136 136 $('#username').focus();
137 137
138 138 $('#generate_password').on('click', function(e){
139 139 var tmpl = "(${_('generated password:')} {0})";
140 140 var new_passwd = generatePassword(12);
141 141 $('#generate_password_preview').html(tmpl.format(new_passwd));
142 142 $('#password').val(new_passwd);
143 143 $('#password_confirmation').val(new_passwd);
144 144 })
145 145 })
146 146 </script>
147 147 </%def>
@@ -1,49 +1,49 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s user settings') % c.user.username}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.link_to(_('Admin'),h.url('admin_home'))}
13 13 &raquo;
14 ${h.link_to(_('Users'),h.url('users'))}
14 ${h.link_to(_('Users'),h.route_path('users'))}
15 15 &raquo;
16 16 ${c.user.username}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_nav()">
20 20 ${self.menu_items(active='admin')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <div class="box user_settings">
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28
29 29 ##main
30 30 <div class="sidebar-col-wrapper">
31 31 <div class="sidebar">
32 32 <ul class="nav nav-pills nav-stacked">
33 33 <li class="${'active' if c.active=='profile' else ''}"><a href="${h.url('edit_user', user_id=c.user.user_id)}">${_('User Profile')}</a></li>
34 34 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('edit_user_auth_tokens', user_id=c.user.user_id)}">${_('Auth tokens')}</a></li>
35 35 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_advanced', user_id=c.user.user_id)}">${_('Advanced')}</a></li>
36 36 <li class="${'active' if c.active=='global_perms' else ''}"><a href="${h.url('edit_user_global_perms', user_id=c.user.user_id)}">${_('Global permissions')}</a></li>
37 37 <li class="${'active' if c.active=='perms_summary' else ''}"><a href="${h.url('edit_user_perms_summary', user_id=c.user.user_id)}">${_('Permissions summary')}</a></li>
38 38 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('edit_user_emails', user_id=c.user.user_id)}">${_('Emails')}</a></li>
39 39 <li class="${'active' if c.active=='ips' else ''}"><a href="${h.url('edit_user_ips', user_id=c.user.user_id)}">${_('Ip Whitelist')}</a></li>
40 40 </ul>
41 41 </div>
42 42
43 43 <div class="main-content-full-width">
44 44 <%include file="/admin/users/user_edit_${c.active}.mako"/>
45 45 </div>
46 46 </div>
47 47 </div>
48 48
49 49 </%def>
@@ -1,142 +1,117 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Users administration')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
13 13 ${h.link_to(_('Admin'),h.url('admin_home'))} &raquo; <span id="user_count">0</span>
14 14 </%def>
15 15
16 16 <%def name="menu_bar_nav()">
17 17 ${self.menu_items(active='admin')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 <ul class="links">
26 26 <li>
27 27 <a href="${h.url('new_user')}" class="btn btn-small btn-success">${_(u'Add User')}</a>
28 28 </li>
29 29 </ul>
30 30 </div>
31 31
32 32 <div id="repos_list_wrap">
33 33 <table id="user_list_table" class="display"></table>
34 34 </div>
35 35 </div>
36 36
37 <script>
37 <script type="text/javascript">
38
38 39 $(document).ready(function() {
39 40
40 var get_datatable_count = function(){
41 var datatable = $('#user_list_table').dataTable();
42 var api = datatable.api();
43 var total = api.page.info().recordsDisplay;
44 var active = datatable.fnGetFilteredData();
45 var _text = _gettext("{0} active out of {1} users").format(active, total);
46 $('#user_count').text(_text);
47 };
48
49 // custom filter that filters by username OR email
50 $.fn.dataTable.ext.search.push(
51 function( settings, data, dataIndex ) {
52 var query = $('#q_filter').val();
53
54 var username = data[0];
55 var email = data[1];
56 var first_name = data[2];
57 var last_name = data[3];
41 var getDatatableCount = function(){
42 var table = $('#user_list_table').dataTable();
43 var page = table.api().page.info();
44 var active = page.recordsDisplay;
45 var total = page.recordsTotal;
58 46
59 var query_str = username + " " +
60 email + " " +
61 first_name + " " +
62 last_name;
63
64 if ((query_str).indexOf(query) !== -1) {
65 return true;
66 }
67 return false;
68 }
69 );
70 // filtered data plugin
71 $.fn.dataTableExt.oApi.fnGetFilteredData = function ( oSettings ) {
72 var res = [];
73 for ( var i=0, iLen=oSettings.fnRecordsDisplay() ; i<iLen ; i++ ) {
74 var record = oSettings.aoData[i]._aData;
75 if(record['active_raw']){
76 res.push(record);
77 }
78 }
79 return res.length;
47 var _text = _gettext("{0} out of {1} users").format(active, total);
48 $('#user_count').text(_text);
80 49 };
81 50
82 51 // user list
83 52 $('#user_list_table').DataTable({
84 data: ${c.data|n},
53 processing: false,
54 serverSide: true,
55 ajax: "${h.route_path('users_data')}",
85 56 dom: 'rtp',
86 57 pageLength: ${c.visual.admin_grid_items},
87 order: [[ 1, "asc" ]],
58 order: [[ 0, "asc" ]],
88 59 columns: [
89 60 { data: {"_": "username",
90 "sort": "username_raw"}, title: "${_('Username')}", className: "td-user" },
61 "sort": "username"}, title: "${_('Username')}", className: "td-user" },
91 62 { data: {"_": "email",
92 63 "sort": "email"}, title: "${_('Email')}", className: "td-email" },
93 64 { data: {"_": "first_name",
94 65 "sort": "first_name"}, title: "${_('First Name')}", className: "td-user" },
95 66 { data: {"_": "last_name",
96 67 "sort": "last_name"}, title: "${_('Last Name')}", className: "td-user" },
97 68 { data: {"_": "last_activity",
98 "sort": "last_activity_raw",
99 "type": Number}, title: "${_('Last activity')}", className: "td-time" },
69 "sort": "last_activity",
70 "type": Number}, title: "${_('Last activity')}", className: "td-time", orderable: false },
100 71 { data: {"_": "active",
101 "sort": "active_raw"}, title: "${_('Active')}", className: "td-active" },
72 "sort": "active"}, title: "${_('Active')}", className: "td-active" },
102 73 { data: {"_": "admin",
103 "sort": "admin_raw"}, title: "${_('Admin')}", className: "td-admin" },
74 "sort": "admin"}, title: "${_('Admin')}", className: "td-admin" },
104 75 { data: {"_": "extern_type",
105 76 "sort": "extern_type"}, title: "${_('Auth type')}", className: "td-type" },
106 77 { data: {"_": "action",
107 78 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
108 79 ],
109 80 language: {
110 81 paginate: DEFAULT_GRID_PAGINATION,
111 82 emptyTable: _gettext("No users available yet.")
112 83 },
113 "initComplete": function( settings, json ) {
114 get_datatable_count();
115 },
84
116 85 "createdRow": function ( row, data, index ) {
117 86 if (!data['active_raw']){
118 87 $(row).addClass('closed')
119 88 }
120 89 }
121 90 });
122 91
123 // update the counter when doing search
124 $('#user_list_table').on( 'search.dt', function (e,settings) {
125 get_datatable_count();
92 $('#user_list_table').on('xhr.dt', function(e, settings, json, xhr){
93 $('#user_list_table').css('opacity', 1);
94 });
95
96 $('#user_list_table').on('preXhr.dt', function(e, settings, data){
97 $('#user_list_table').css('opacity', 0.3);
126 98 });
127 99
128 // filter, filter both grids
129 $('#q_filter').on( 'keyup', function () {
130 var user_api = $('#user_list_table').dataTable().api();
131 user_api
132 .draw();
100 // refresh counters on draw
101 $('#user_list_table').on('draw.dt', function(){
102 getDatatableCount();
133 103 });
134 104
135 // refilter table if page load via back button
136 $("#q_filter").trigger('keyup');
105 // filter
106 $('#q_filter').on('keyup',
107 $.debounce(250, function() {
108 $('#user_list_table').DataTable().search(
109 $('#q_filter').val()
110 ).draw();
111 })
112 );
137 113
138 114 });
139
140 115 </script>
141 116
142 117 </%def>
@@ -1,601 +1,601 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.url('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.url('admin_home')}">${_('Admin journal')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 <li><a href="${h.url('users')}">${_('Users')}</a></li>
78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${title}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.author_string(email)}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.url('summary_home',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.url('summary_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.url('files_home', repo_name=c.repo_name, revision=c.rhodecode_db_repo.landing_rev[1])}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}">
232 232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 233 </li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink" href="#" class="dropdown"><div class="menulabel">${_('Options')} <div class="show_more"></div></div></a>
247 247 <ul class="submenu">
248 248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 249 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 250 %endif
251 251 %if c.rhodecode_db_repo.fork:
252 252 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
253 253 ${_('Compare fork')}</a></li>
254 254 %endif
255 255
256 256 <li><a href="${h.url('search_repo_home',repo_name=c.repo_name)}">${_('Search')}</a></li>
257 257
258 258 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
259 259 %if c.rhodecode_db_repo.locked[0]:
260 260 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
261 261 %else:
262 262 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
263 263 %endif
264 264 %endif
265 265 %if c.rhodecode_user.username != h.DEFAULT_USER:
266 266 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
267 267 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
268 268 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
269 269 %endif
270 270 %endif
271 271 </ul>
272 272 </li>
273 273 </ul>
274 274 </div>
275 275 <div class="clear"></div>
276 276 </div>
277 277 <!--- END CONTEXT BAR -->
278 278
279 279 </%def>
280 280
281 281 <%def name="usermenu(active=False)">
282 282 ## USER MENU
283 283 <li id="quick_login_li" class="${'active' if active else ''}">
284 284 <a id="quick_login_link" class="menulink childs">
285 285 ${gravatar(c.rhodecode_user.email, 20)}
286 286 <span class="user">
287 287 %if c.rhodecode_user.username != h.DEFAULT_USER:
288 288 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
289 289 %else:
290 290 <span>${_('Sign in')}</span>
291 291 %endif
292 292 </span>
293 293 </a>
294 294
295 295 <div class="user-menu submenu">
296 296 <div id="quick_login">
297 297 %if c.rhodecode_user.username == h.DEFAULT_USER:
298 298 <h4>${_('Sign in to your account')}</h4>
299 299 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
300 300 <div class="form form-vertical">
301 301 <div class="fields">
302 302 <div class="field">
303 303 <div class="label">
304 304 <label for="username">${_('Username')}:</label>
305 305 </div>
306 306 <div class="input">
307 307 ${h.text('username',class_='focus',tabindex=1)}
308 308 </div>
309 309
310 310 </div>
311 311 <div class="field">
312 312 <div class="label">
313 313 <label for="password">${_('Password')}:</label>
314 314 %if h.HasPermissionAny('hg.password_reset.enabled')():
315 315 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
316 316 %endif
317 317 </div>
318 318 <div class="input">
319 319 ${h.password('password',class_='focus',tabindex=2)}
320 320 </div>
321 321 </div>
322 322 <div class="buttons">
323 323 <div class="register">
324 324 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
325 325 ${h.link_to(_("Don't have an account ?"),h.route_path('register'))}
326 326 %endif
327 327 </div>
328 328 <div class="submit">
329 329 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
330 330 </div>
331 331 </div>
332 332 </div>
333 333 </div>
334 334 ${h.end_form()}
335 335 %else:
336 336 <div class="">
337 337 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
338 338 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
339 339 <div class="email">${c.rhodecode_user.email}</div>
340 340 </div>
341 341 <div class="">
342 342 <ol class="links">
343 343 <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li>
344 344 % if c.rhodecode_user.personal_repo_group:
345 345 <li>${h.link_to(_(u'My personal group'), h.url('repo_group_home', group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
346 346 % endif
347 347 <li class="logout">
348 348 ${h.secure_form(h.route_path('logout'))}
349 349 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
350 350 ${h.end_form()}
351 351 </li>
352 352 </ol>
353 353 </div>
354 354 %endif
355 355 </div>
356 356 </div>
357 357 %if c.rhodecode_user.username != h.DEFAULT_USER:
358 358 <div class="pill_container">
359 359 % if c.unread_notifications == 0:
360 360 <a class="menu_link_notifications empty" href="${h.url('notifications')}">${c.unread_notifications}</a>
361 361 % else:
362 362 <a class="menu_link_notifications" href="${h.url('notifications')}">${c.unread_notifications}</a>
363 363 % endif
364 364 </div>
365 365 % endif
366 366 </li>
367 367 </%def>
368 368
369 369 <%def name="menu_items(active=None)">
370 370 <%
371 371 def is_active(selected):
372 372 if selected == active:
373 373 return "active"
374 374 return ""
375 375 %>
376 376 <ul id="quick" class="main_nav navigation horizontal-list">
377 377 <!-- repo switcher -->
378 378 <li class="${is_active('repositories')} repo_switcher_li has_select2">
379 379 <input id="repo_switcher" name="repo_switcher" type="hidden">
380 380 </li>
381 381
382 382 ## ROOT MENU
383 383 %if c.rhodecode_user.username != h.DEFAULT_USER:
384 384 <li class="${is_active('journal')}">
385 385 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
386 386 <div class="menulabel">${_('Journal')}</div>
387 387 </a>
388 388 </li>
389 389 %else:
390 390 <li class="${is_active('journal')}">
391 391 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
392 392 <div class="menulabel">${_('Public journal')}</div>
393 393 </a>
394 394 </li>
395 395 %endif
396 396 <li class="${is_active('gists')}">
397 397 <a class="menulink childs" title="${_('Show Gists')}" href="${h.url('gists')}">
398 398 <div class="menulabel">${_('Gists')}</div>
399 399 </a>
400 400 </li>
401 401 <li class="${is_active('search')}">
402 402 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.url('search')}">
403 403 <div class="menulabel">${_('Search')}</div>
404 404 </a>
405 405 </li>
406 406 % if h.HasPermissionAll('hg.admin')('access admin main page'):
407 407 <li class="${is_active('admin')}">
408 408 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
409 409 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
410 410 </a>
411 411 ${admin_menu()}
412 412 </li>
413 413 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
414 414 <li class="${is_active('admin')}">
415 415 <a class="menulink childs" title="${_('Delegated Admin settings')}">
416 416 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
417 417 </a>
418 418 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
419 419 c.rhodecode_user.repository_groups_admin,
420 420 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
421 421 </li>
422 422 % endif
423 423 % if c.debug_style:
424 424 <li class="${is_active('debug_style')}">
425 425 <a class="menulink" title="${_('Style')}" href="${h.url('debug_style_home')}">
426 426 <div class="menulabel">${_('Style')}</div>
427 427 </a>
428 428 </li>
429 429 % endif
430 430 ## render extra user menu
431 431 ${usermenu(active=(active=='my_account'))}
432 432 </ul>
433 433
434 434 <script type="text/javascript">
435 435 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
436 436
437 437 /*format the look of items in the list*/
438 438 var format = function(state, escapeMarkup){
439 439 if (!state.id){
440 440 return state.text; // optgroup
441 441 }
442 442 var obj_dict = state.obj;
443 443 var tmpl = '';
444 444
445 445 if(obj_dict && state.type == 'repo'){
446 446 if(obj_dict['repo_type'] === 'hg'){
447 447 tmpl += '<i class="icon-hg"></i> ';
448 448 }
449 449 else if(obj_dict['repo_type'] === 'git'){
450 450 tmpl += '<i class="icon-git"></i> ';
451 451 }
452 452 else if(obj_dict['repo_type'] === 'svn'){
453 453 tmpl += '<i class="icon-svn"></i> ';
454 454 }
455 455 if(obj_dict['private']){
456 456 tmpl += '<i class="icon-lock" ></i> ';
457 457 }
458 458 else if(visual_show_public_icon){
459 459 tmpl += '<i class="icon-unlock-alt"></i> ';
460 460 }
461 461 }
462 462 if(obj_dict && state.type == 'commit') {
463 463 tmpl += '<i class="icon-tag"></i>';
464 464 }
465 465 if(obj_dict && state.type == 'group'){
466 466 tmpl += '<i class="icon-folder-close"></i> ';
467 467 }
468 468 tmpl += escapeMarkup(state.text);
469 469 return tmpl;
470 470 };
471 471
472 472 var formatResult = function(result, container, query, escapeMarkup) {
473 473 return format(result, escapeMarkup);
474 474 };
475 475
476 476 var formatSelection = function(data, container, escapeMarkup) {
477 477 return format(data, escapeMarkup);
478 478 };
479 479
480 480 $("#repo_switcher").select2({
481 481 cachedDataSource: {},
482 482 minimumInputLength: 2,
483 483 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
484 484 dropdownAutoWidth: true,
485 485 formatResult: formatResult,
486 486 formatSelection: formatSelection,
487 487 containerCssClass: "repo-switcher",
488 488 dropdownCssClass: "repo-switcher-dropdown",
489 489 escapeMarkup: function(m){
490 490 // don't escape our custom placeholder
491 491 if(m.substr(0,23) == '<div class="menulabel">'){
492 492 return m;
493 493 }
494 494
495 495 return Select2.util.escapeMarkup(m);
496 496 },
497 497 query: $.debounce(250, function(query){
498 498 self = this;
499 499 var cacheKey = query.term;
500 500 var cachedData = self.cachedDataSource[cacheKey];
501 501
502 502 if (cachedData) {
503 503 query.callback({results: cachedData.results});
504 504 } else {
505 505 $.ajax({
506 506 url: "${h.url('goto_switcher_data')}",
507 507 data: {'query': query.term},
508 508 dataType: 'json',
509 509 type: 'GET',
510 510 success: function(data) {
511 511 self.cachedDataSource[cacheKey] = data;
512 512 query.callback({results: data.results});
513 513 },
514 514 error: function(data, textStatus, errorThrown) {
515 515 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
516 516 }
517 517 })
518 518 }
519 519 })
520 520 });
521 521
522 522 $("#repo_switcher").on('select2-selecting', function(e){
523 523 e.preventDefault();
524 524 window.location = e.choice.url;
525 525 });
526 526
527 527 </script>
528 528 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
529 529 </%def>
530 530
531 531 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
532 532 <div class="modal-dialog">
533 533 <div class="modal-content">
534 534 <div class="modal-header">
535 535 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
536 536 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
537 537 </div>
538 538 <div class="modal-body">
539 539 <div class="block-left">
540 540 <table class="keyboard-mappings">
541 541 <tbody>
542 542 <tr>
543 543 <th></th>
544 544 <th>${_('Site-wide shortcuts')}</th>
545 545 </tr>
546 546 <%
547 547 elems = [
548 548 ('/', 'Open quick search box'),
549 549 ('g h', 'Goto home page'),
550 550 ('g g', 'Goto my private gists page'),
551 551 ('g G', 'Goto my public gists page'),
552 552 ('n r', 'New repository page'),
553 553 ('n g', 'New gist page'),
554 554 ]
555 555 %>
556 556 %for key, desc in elems:
557 557 <tr>
558 558 <td class="keys">
559 559 <span class="key tag">${key}</span>
560 560 </td>
561 561 <td>${desc}</td>
562 562 </tr>
563 563 %endfor
564 564 </tbody>
565 565 </table>
566 566 </div>
567 567 <div class="block-left">
568 568 <table class="keyboard-mappings">
569 569 <tbody>
570 570 <tr>
571 571 <th></th>
572 572 <th>${_('Repositories')}</th>
573 573 </tr>
574 574 <%
575 575 elems = [
576 576 ('g s', 'Goto summary page'),
577 577 ('g c', 'Goto changelog page'),
578 578 ('g f', 'Goto files page'),
579 579 ('g F', 'Goto files page with file search activated'),
580 580 ('g p', 'Goto pull requests page'),
581 581 ('g o', 'Goto repository settings'),
582 582 ('g O', 'Goto repository permissions settings'),
583 583 ]
584 584 %>
585 585 %for key, desc in elems:
586 586 <tr>
587 587 <td class="keys">
588 588 <span class="key tag">${key}</span>
589 589 </td>
590 590 <td>${desc}</td>
591 591 </tr>
592 592 %endfor
593 593 </tbody>
594 594 </table>
595 595 </div>
596 596 </div>
597 597 <div class="modal-footer">
598 598 </div>
599 599 </div><!-- /.modal-content -->
600 600 </div><!-- /.modal-dialog -->
601 601 </div><!-- /.modal -->
@@ -1,561 +1,575 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 from sqlalchemy.orm.exc import NoResultFound
23 23
24 24 from rhodecode.lib.auth import check_password
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model import validators
27 27 from rhodecode.model.db import User, UserIpMap, UserApiKeys
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.tests import (
31 31 TestController, url, link_to, TEST_USER_ADMIN_LOGIN,
32 32 TEST_USER_REGULAR_LOGIN, assert_session_flash)
33 33 from rhodecode.tests.fixture import Fixture
34 34 from rhodecode.tests.utils import AssertResponse
35 35
36 36 fixture = Fixture()
37 37
38 38
39 def route_path(name, params=None, **kwargs):
40 import urllib
41 from rhodecode.apps._base import ADMIN_PREFIX
42
43 base_url = {
44 'users_data':
45 ADMIN_PREFIX + '/users_data',
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
50 return base_url
51
52
39 53 class TestAdminUsersController(TestController):
40 54 test_user_1 = 'testme'
41 55 destroy_users = set()
42 56
43 57 @classmethod
44 58 def teardown_method(cls, method):
45 59 fixture.destroy_users(cls.destroy_users)
46 60
47 def test_create(self):
61 def test_create(self, xhr_header):
48 62 self.log_user()
49 63 username = 'newtestuser'
50 64 password = 'test12'
51 65 password_confirmation = password
52 66 name = 'name'
53 67 lastname = 'lastname'
54 68 email = 'mail@mail.com'
55 69
56 response = self.app.get(url('new_user'))
70 self.app.get(url('new_user'))
57 71
58 72 response = self.app.post(url('users'), params={
59 73 'username': username,
60 74 'password': password,
61 75 'password_confirmation': password_confirmation,
62 76 'firstname': name,
63 77 'active': True,
64 78 'lastname': lastname,
65 79 'extern_name': 'rhodecode',
66 80 'extern_type': 'rhodecode',
67 81 'email': email,
68 82 'csrf_token': self.csrf_token,
69 83 })
70 84 user_link = link_to(
71 85 username,
72 86 url('edit_user', user_id=User.get_by_username(username).user_id))
73 87 assert_session_flash(response, 'Created user %s' % (user_link,))
74 88 self.destroy_users.add(username)
75 89
76 90 new_user = User.query().filter(User.username == username).one()
77 91
78 92 assert new_user.username == username
79 93 assert check_password(password, new_user.password)
80 94 assert new_user.name == name
81 95 assert new_user.lastname == lastname
82 96 assert new_user.email == email
83 97
84 response.follow()
85 response = response.follow()
98 response = self.app.get(route_path('users_data'),
99 extra_environ=xhr_header)
86 100 response.mustcontain(username)
87 101
88 102 def test_create_err(self):
89 103 self.log_user()
90 104 username = 'new_user'
91 105 password = ''
92 106 name = 'name'
93 107 lastname = 'lastname'
94 108 email = 'errmail.com'
95 109
96 response = self.app.get(url('new_user'))
110 self.app.get(url('new_user'))
97 111
98 112 response = self.app.post(url('users'), params={
99 113 'username': username,
100 114 'password': password,
101 115 'name': name,
102 116 'active': False,
103 117 'lastname': lastname,
104 118 'email': email,
105 119 'csrf_token': self.csrf_token,
106 120 })
107 121
108 122 msg = validators.ValidUsername(
109 123 False, {})._messages['system_invalid_username']
110 124 msg = h.html_escape(msg % {'username': 'new_user'})
111 125 response.mustcontain('<span class="error-message">%s</span>' % msg)
112 126 response.mustcontain(
113 127 '<span class="error-message">Please enter a value</span>')
114 128 response.mustcontain(
115 129 '<span class="error-message">An email address must contain a'
116 130 ' single @</span>')
117 131
118 132 def get_user():
119 133 Session().query(User).filter(User.username == username).one()
120 134
121 135 with pytest.raises(NoResultFound):
122 136 get_user()
123 137
124 138 def test_new(self):
125 139 self.log_user()
126 140 self.app.get(url('new_user'))
127 141
128 142 @pytest.mark.parametrize("name, attrs", [
129 143 ('firstname', {'firstname': 'new_username'}),
130 144 ('lastname', {'lastname': 'new_username'}),
131 145 ('admin', {'admin': True}),
132 146 ('admin', {'admin': False}),
133 147 ('extern_type', {'extern_type': 'ldap'}),
134 148 ('extern_type', {'extern_type': None}),
135 149 ('extern_name', {'extern_name': 'test'}),
136 150 ('extern_name', {'extern_name': None}),
137 151 ('active', {'active': False}),
138 152 ('active', {'active': True}),
139 153 ('email', {'email': 'some@email.com'}),
140 154 ('language', {'language': 'de'}),
141 155 ('language', {'language': 'en'}),
142 156 # ('new_password', {'new_password': 'foobar123',
143 157 # 'password_confirmation': 'foobar123'})
144 158 ])
145 159 def test_update(self, name, attrs):
146 160 self.log_user()
147 161 usr = fixture.create_user(self.test_user_1, password='qweqwe',
148 162 email='testme@rhodecode.org',
149 163 extern_type='rhodecode',
150 164 extern_name=self.test_user_1,
151 165 skip_if_exists=True)
152 166 Session().commit()
153 167 self.destroy_users.add(self.test_user_1)
154 168 params = usr.get_api_data()
155 169 cur_lang = params['language'] or 'en'
156 170 params.update({
157 171 'password_confirmation': '',
158 172 'new_password': '',
159 173 'language': cur_lang,
160 174 '_method': 'put',
161 175 'csrf_token': self.csrf_token,
162 176 })
163 177 params.update({'new_password': ''})
164 178 params.update(attrs)
165 179 if name == 'email':
166 180 params['emails'] = [attrs['email']]
167 181 elif name == 'extern_type':
168 182 # cannot update this via form, expected value is original one
169 183 params['extern_type'] = "rhodecode"
170 184 elif name == 'extern_name':
171 185 # cannot update this via form, expected value is original one
172 186 params['extern_name'] = self.test_user_1
173 187 # special case since this user is not
174 188 # logged in yet his data is not filled
175 189 # so we use creation data
176 190
177 191 response = self.app.post(url('user', user_id=usr.user_id), params)
178 192 assert response.status_int == 302
179 193 assert_session_flash(response, 'User updated successfully')
180 194
181 195 updated_user = User.get_by_username(self.test_user_1)
182 196 updated_params = updated_user.get_api_data()
183 197 updated_params.update({'password_confirmation': ''})
184 198 updated_params.update({'new_password': ''})
185 199
186 200 del params['_method']
187 201 del params['csrf_token']
188 202 assert params == updated_params
189 203
190 204 def test_update_and_migrate_password(
191 205 self, autologin_user, real_crypto_backend):
192 206 from rhodecode.lib import auth
193 207
194 208 # create new user, with sha256 password
195 209 temp_user = 'test_admin_sha256'
196 210 user = fixture.create_user(temp_user)
197 211 user.password = auth._RhodeCodeCryptoSha256().hash_create(
198 212 b'test123')
199 213 Session().add(user)
200 214 Session().commit()
201 215 self.destroy_users.add('test_admin_sha256')
202 216
203 217 params = user.get_api_data()
204 218
205 219 params.update({
206 220 'password_confirmation': 'qweqwe123',
207 221 'new_password': 'qweqwe123',
208 222 'language': 'en',
209 223 '_method': 'put',
210 224 'csrf_token': autologin_user.csrf_token,
211 225 })
212 226
213 227 response = self.app.post(url('user', user_id=user.user_id), params)
214 228 assert response.status_int == 302
215 229 assert_session_flash(response, 'User updated successfully')
216 230
217 231 # new password should be bcrypted, after log-in and transfer
218 232 user = User.get_by_username(temp_user)
219 233 assert user.password.startswith('$')
220 234
221 235 updated_user = User.get_by_username(temp_user)
222 236 updated_params = updated_user.get_api_data()
223 237 updated_params.update({'password_confirmation': 'qweqwe123'})
224 238 updated_params.update({'new_password': 'qweqwe123'})
225 239
226 240 del params['_method']
227 241 del params['csrf_token']
228 242 assert params == updated_params
229 243
230 244 def test_delete(self):
231 245 self.log_user()
232 246 username = 'newtestuserdeleteme'
233 247
234 248 fixture.create_user(name=username)
235 249
236 250 new_user = Session().query(User)\
237 251 .filter(User.username == username).one()
238 252 response = self.app.post(url('user', user_id=new_user.user_id),
239 253 params={'_method': 'delete',
240 254 'csrf_token': self.csrf_token})
241 255
242 256 assert_session_flash(response, 'Successfully deleted user')
243 257
244 258 def test_delete_owner_of_repository(self):
245 259 self.log_user()
246 260 username = 'newtestuserdeleteme_repo_owner'
247 261 obj_name = 'test_repo'
248 262 usr = fixture.create_user(name=username)
249 263 self.destroy_users.add(username)
250 264 fixture.create_repo(obj_name, cur_user=usr.username)
251 265
252 266 new_user = Session().query(User)\
253 267 .filter(User.username == username).one()
254 268 response = self.app.post(url('user', user_id=new_user.user_id),
255 269 params={'_method': 'delete',
256 270 'csrf_token': self.csrf_token})
257 271
258 272 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
259 273 'Switch owners or remove those repositories:%s' % (username,
260 274 obj_name)
261 275 assert_session_flash(response, msg)
262 276 fixture.destroy_repo(obj_name)
263 277
264 278 def test_delete_owner_of_repository_detaching(self):
265 279 self.log_user()
266 280 username = 'newtestuserdeleteme_repo_owner_detach'
267 281 obj_name = 'test_repo'
268 282 usr = fixture.create_user(name=username)
269 283 self.destroy_users.add(username)
270 284 fixture.create_repo(obj_name, cur_user=usr.username)
271 285
272 286 new_user = Session().query(User)\
273 287 .filter(User.username == username).one()
274 288 response = self.app.post(url('user', user_id=new_user.user_id),
275 289 params={'_method': 'delete',
276 290 'user_repos': 'detach',
277 291 'csrf_token': self.csrf_token})
278 292
279 293 msg = 'Detached 1 repositories'
280 294 assert_session_flash(response, msg)
281 295 fixture.destroy_repo(obj_name)
282 296
283 297 def test_delete_owner_of_repository_deleting(self):
284 298 self.log_user()
285 299 username = 'newtestuserdeleteme_repo_owner_delete'
286 300 obj_name = 'test_repo'
287 301 usr = fixture.create_user(name=username)
288 302 self.destroy_users.add(username)
289 303 fixture.create_repo(obj_name, cur_user=usr.username)
290 304
291 305 new_user = Session().query(User)\
292 306 .filter(User.username == username).one()
293 307 response = self.app.post(url('user', user_id=new_user.user_id),
294 308 params={'_method': 'delete',
295 309 'user_repos': 'delete',
296 310 'csrf_token': self.csrf_token})
297 311
298 312 msg = 'Deleted 1 repositories'
299 313 assert_session_flash(response, msg)
300 314
301 315 def test_delete_owner_of_repository_group(self):
302 316 self.log_user()
303 317 username = 'newtestuserdeleteme_repo_group_owner'
304 318 obj_name = 'test_group'
305 319 usr = fixture.create_user(name=username)
306 320 self.destroy_users.add(username)
307 321 fixture.create_repo_group(obj_name, cur_user=usr.username)
308 322
309 323 new_user = Session().query(User)\
310 324 .filter(User.username == username).one()
311 325 response = self.app.post(url('user', user_id=new_user.user_id),
312 326 params={'_method': 'delete',
313 327 'csrf_token': self.csrf_token})
314 328
315 329 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
316 330 'Switch owners or remove those repository groups:%s' % (username,
317 331 obj_name)
318 332 assert_session_flash(response, msg)
319 333 fixture.destroy_repo_group(obj_name)
320 334
321 335 def test_delete_owner_of_repository_group_detaching(self):
322 336 self.log_user()
323 337 username = 'newtestuserdeleteme_repo_group_owner_detach'
324 338 obj_name = 'test_group'
325 339 usr = fixture.create_user(name=username)
326 340 self.destroy_users.add(username)
327 341 fixture.create_repo_group(obj_name, cur_user=usr.username)
328 342
329 343 new_user = Session().query(User)\
330 344 .filter(User.username == username).one()
331 345 response = self.app.post(url('user', user_id=new_user.user_id),
332 346 params={'_method': 'delete',
333 347 'user_repo_groups': 'delete',
334 348 'csrf_token': self.csrf_token})
335 349
336 350 msg = 'Deleted 1 repository groups'
337 351 assert_session_flash(response, msg)
338 352
339 353 def test_delete_owner_of_repository_group_deleting(self):
340 354 self.log_user()
341 355 username = 'newtestuserdeleteme_repo_group_owner_delete'
342 356 obj_name = 'test_group'
343 357 usr = fixture.create_user(name=username)
344 358 self.destroy_users.add(username)
345 359 fixture.create_repo_group(obj_name, cur_user=usr.username)
346 360
347 361 new_user = Session().query(User)\
348 362 .filter(User.username == username).one()
349 363 response = self.app.post(url('user', user_id=new_user.user_id),
350 364 params={'_method': 'delete',
351 365 'user_repo_groups': 'detach',
352 366 'csrf_token': self.csrf_token})
353 367
354 368 msg = 'Detached 1 repository groups'
355 369 assert_session_flash(response, msg)
356 370 fixture.destroy_repo_group(obj_name)
357 371
358 372 def test_delete_owner_of_user_group(self):
359 373 self.log_user()
360 374 username = 'newtestuserdeleteme_user_group_owner'
361 375 obj_name = 'test_user_group'
362 376 usr = fixture.create_user(name=username)
363 377 self.destroy_users.add(username)
364 378 fixture.create_user_group(obj_name, cur_user=usr.username)
365 379
366 380 new_user = Session().query(User)\
367 381 .filter(User.username == username).one()
368 382 response = self.app.post(url('user', user_id=new_user.user_id),
369 383 params={'_method': 'delete',
370 384 'csrf_token': self.csrf_token})
371 385
372 386 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
373 387 'Switch owners or remove those user groups:%s' % (username,
374 388 obj_name)
375 389 assert_session_flash(response, msg)
376 390 fixture.destroy_user_group(obj_name)
377 391
378 392 def test_delete_owner_of_user_group_detaching(self):
379 393 self.log_user()
380 394 username = 'newtestuserdeleteme_user_group_owner_detaching'
381 395 obj_name = 'test_user_group'
382 396 usr = fixture.create_user(name=username)
383 397 self.destroy_users.add(username)
384 398 fixture.create_user_group(obj_name, cur_user=usr.username)
385 399
386 400 new_user = Session().query(User)\
387 401 .filter(User.username == username).one()
388 402 try:
389 403 response = self.app.post(url('user', user_id=new_user.user_id),
390 404 params={'_method': 'delete',
391 405 'user_user_groups': 'detach',
392 406 'csrf_token': self.csrf_token})
393 407
394 408 msg = 'Detached 1 user groups'
395 409 assert_session_flash(response, msg)
396 410 finally:
397 411 fixture.destroy_user_group(obj_name)
398 412
399 413 def test_delete_owner_of_user_group_deleting(self):
400 414 self.log_user()
401 415 username = 'newtestuserdeleteme_user_group_owner_deleting'
402 416 obj_name = 'test_user_group'
403 417 usr = fixture.create_user(name=username)
404 418 self.destroy_users.add(username)
405 419 fixture.create_user_group(obj_name, cur_user=usr.username)
406 420
407 421 new_user = Session().query(User)\
408 422 .filter(User.username == username).one()
409 423 response = self.app.post(url('user', user_id=new_user.user_id),
410 424 params={'_method': 'delete',
411 425 'user_user_groups': 'delete',
412 426 'csrf_token': self.csrf_token})
413 427
414 428 msg = 'Deleted 1 user groups'
415 429 assert_session_flash(response, msg)
416 430
417 431 def test_edit(self):
418 432 self.log_user()
419 433 user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
420 434 self.app.get(url('edit_user', user_id=user.user_id))
421 435
422 436 @pytest.mark.parametrize(
423 437 'repo_create, repo_create_write, user_group_create, repo_group_create,'
424 438 'fork_create, inherit_default_permissions, expect_error,'
425 439 'expect_form_error', [
426 440 ('hg.create.none', 'hg.create.write_on_repogroup.false',
427 441 'hg.usergroup.create.false', 'hg.repogroup.create.false',
428 442 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
429 443 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
430 444 'hg.usergroup.create.false', 'hg.repogroup.create.false',
431 445 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
432 446 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
433 447 'hg.usergroup.create.true', 'hg.repogroup.create.true',
434 448 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
435 449 False),
436 450 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
437 451 'hg.usergroup.create.true', 'hg.repogroup.create.true',
438 452 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
439 453 True),
440 454 ('', '', '', '', '', '', True, False),
441 455 ])
442 456 def test_global_perms_on_user(
443 457 self, repo_create, repo_create_write, user_group_create,
444 458 repo_group_create, fork_create, expect_error, expect_form_error,
445 459 inherit_default_permissions):
446 460 self.log_user()
447 461 user = fixture.create_user('dummy')
448 462 uid = user.user_id
449 463
450 464 # ENABLE REPO CREATE ON A GROUP
451 465 perm_params = {
452 466 'inherit_default_permissions': False,
453 467 'default_repo_create': repo_create,
454 468 'default_repo_create_on_write': repo_create_write,
455 469 'default_user_group_create': user_group_create,
456 470 'default_repo_group_create': repo_group_create,
457 471 'default_fork_create': fork_create,
458 472 'default_inherit_default_permissions': inherit_default_permissions,
459 473 '_method': 'put',
460 474 'csrf_token': self.csrf_token,
461 475 }
462 476 response = self.app.post(
463 477 url('edit_user_global_perms', user_id=uid),
464 478 params=perm_params)
465 479
466 480 if expect_form_error:
467 481 assert response.status_int == 200
468 482 response.mustcontain('Value must be one of')
469 483 else:
470 484 if expect_error:
471 485 msg = 'An error occurred during permissions saving'
472 486 else:
473 487 msg = 'User global permissions updated successfully'
474 488 ug = User.get(uid)
475 489 del perm_params['_method']
476 490 del perm_params['inherit_default_permissions']
477 491 del perm_params['csrf_token']
478 492 assert perm_params == ug.get_default_perms()
479 493 assert_session_flash(response, msg)
480 494 fixture.destroy_user(uid)
481 495
482 496 def test_global_permissions_initial_values(self, user_util):
483 497 self.log_user()
484 498 user = user_util.create_user()
485 499 uid = user.user_id
486 500 response = self.app.get(url('edit_user_global_perms', user_id=uid))
487 501 default_user = User.get_default_user()
488 502 default_permissions = default_user.get_default_perms()
489 503 assert_response = AssertResponse(response)
490 504 expected_permissions = (
491 505 'default_repo_create', 'default_repo_create_on_write',
492 506 'default_fork_create', 'default_repo_group_create',
493 507 'default_user_group_create', 'default_inherit_default_permissions')
494 508 for permission in expected_permissions:
495 509 css_selector = '[name={}][checked=checked]'.format(permission)
496 510 element = assert_response.get_element(css_selector)
497 511 assert element.value == default_permissions[permission]
498 512
499 513 def test_ips(self):
500 514 self.log_user()
501 515 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
502 516 response = self.app.get(url('edit_user_ips', user_id=user.user_id))
503 517 response.mustcontain('All IP addresses are allowed')
504 518
505 519 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
506 520 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
507 521 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
508 522 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
509 523 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
510 524 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
511 525 ('127_bad_ip', 'foobar', 'foobar', True),
512 526 ])
513 527 def test_add_ip(self, test_name, ip, ip_range, failure):
514 528 self.log_user()
515 529 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
516 530 user_id = user.user_id
517 531
518 532 response = self.app.post(url('edit_user_ips', user_id=user_id),
519 533 params={'new_ip': ip, '_method': 'put',
520 534 'csrf_token': self.csrf_token})
521 535
522 536 if failure:
523 537 assert_session_flash(
524 538 response, 'Please enter a valid IPv4 or IpV6 address')
525 539 response = self.app.get(url('edit_user_ips', user_id=user_id))
526 540 response.mustcontain(no=[ip])
527 541 response.mustcontain(no=[ip_range])
528 542
529 543 else:
530 544 response = self.app.get(url('edit_user_ips', user_id=user_id))
531 545 response.mustcontain(ip)
532 546 response.mustcontain(ip_range)
533 547
534 548 # cleanup
535 549 for del_ip in UserIpMap.query().filter(
536 550 UserIpMap.user_id == user_id).all():
537 551 Session().delete(del_ip)
538 552 Session().commit()
539 553
540 554 def test_delete_ip(self):
541 555 self.log_user()
542 556 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
543 557 user_id = user.user_id
544 558 ip = '127.0.0.1/32'
545 559 ip_range = '127.0.0.1 - 127.0.0.1'
546 560 new_ip = UserModel().add_extra_ip(user_id, ip)
547 561 Session().commit()
548 562 new_ip_id = new_ip.ip_id
549 563
550 564 response = self.app.get(url('edit_user_ips', user_id=user_id))
551 565 response.mustcontain(ip)
552 566 response.mustcontain(ip_range)
553 567
554 568 self.app.post(url('edit_user_ips', user_id=user_id),
555 569 params={'_method': 'delete', 'del_ip_id': new_ip_id,
556 570 'csrf_token': self.csrf_token})
557 571
558 572 response = self.app.get(url('edit_user_ips', user_id=user_id))
559 573 response.mustcontain('All IP addresses are allowed')
560 574 response.mustcontain(no=[ip])
561 575 response.mustcontain(no=[ip_range])
General Comments 0
You need to be logged in to leave comments. Login now