##// END OF EJS Templates
my-account-auth-tokens: moved into pyramid apps....
marcink -
r1505:0cb9b007 default
parent child Browse files
Show More
@@ -0,0 +1,39 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 from rhodecode.apps._base import ADMIN_PREFIX
23
24
25 def includeme(config):
26 config.add_route(
27 name='my_account_auth_tokens',
28 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
29 config.add_route(
30 name='my_account_auth_tokens_add',
31 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new',
32 )
33 config.add_route(
34 name='my_account_auth_tokens_delete',
35 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete',
36 )
37
38 # Scan module for configuration decorators.
39 config.scan()
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,111 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.model.db import User
25 from rhodecode.tests import (
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
27 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.utils import AssertResponse
30
31 fixture = Fixture()
32
33
34 def route_path(name, **kwargs):
35 return {
36 'my_account_auth_tokens':
37 ADMIN_PREFIX + '/my_account/auth_tokens',
38 'my_account_auth_tokens_add':
39 ADMIN_PREFIX + '/my_account/auth_tokens/new',
40 'my_account_auth_tokens_delete':
41 ADMIN_PREFIX + '/my_account/auth_tokens/delete',
42 }[name].format(**kwargs)
43
44
45 class TestMyAccountAuthTokens(TestController):
46
47 def test_my_account_auth_tokens(self):
48 usr = self.log_user('test_regular2', 'test12')
49 user = User.get(usr['user_id'])
50 response = self.app.get(route_path('my_account_auth_tokens'))
51 for token in user.auth_tokens:
52 response.mustcontain(token)
53 response.mustcontain('never')
54
55 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
56 user = user_util.create_user(password='qweqwe')
57 self.log_user(user.username, 'qweqwe')
58
59 self.app.post(
60 route_path('my_account_auth_tokens_add'),
61 {'description': 'desc', 'lifetime': -1}, status=403)
62
63 @pytest.mark.parametrize("desc, lifetime", [
64 ('forever', -1),
65 ('5mins', 60*5),
66 ('30days', 60*60*24*30),
67 ])
68 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
69 user = user_util.create_user(password='qweqwe')
70 user_id = user.user_id
71 self.log_user(user.username, 'qweqwe')
72
73 response = self.app.post(
74 route_path('my_account_auth_tokens_add'),
75 {'description': desc, 'lifetime': lifetime,
76 'csrf_token': self.csrf_token})
77 assert_session_flash(response, 'Auth token successfully created')
78
79 response = response.follow()
80 user = User.get(user_id)
81 for auth_token in user.auth_tokens:
82 response.mustcontain(auth_token)
83
84 def test_my_account_delete_auth_token(self, user_util):
85 user = user_util.create_user(password='qweqwe')
86 user_id = user.user_id
87 self.log_user(user.username, 'qweqwe')
88
89 user = User.get(user_id)
90 keys = user.extra_auth_tokens
91 assert 2 == len(keys)
92
93 response = self.app.post(
94 route_path('my_account_auth_tokens_add'),
95 {'description': 'desc', 'lifetime': -1,
96 'csrf_token': self.csrf_token})
97 assert_session_flash(response, 'Auth token successfully created')
98 response.follow()
99
100 user = User.get(user_id)
101 keys = user.extra_auth_tokens
102 assert 3 == len(keys)
103
104 response = self.app.post(
105 route_path('my_account_auth_tokens_delete'),
106 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
107 assert_session_flash(response, 'Auth token successfully deleted')
108
109 user = User.get(user_id)
110 keys = user.extra_auth_tokens
111 assert 2 == len(keys)
@@ -0,0 +1,111 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
28 from rhodecode.lib.utils2 import safe_int
29 from rhodecode.lib import helpers as h
30 from rhodecode.model.auth_token import AuthTokenModel
31 from rhodecode.model.meta import Session
32
33 log = logging.getLogger(__name__)
34
35
36 class MyAccountView(BaseAppView):
37
38 def load_default_context(self):
39 c = self._get_local_tmpl_context()
40
41 c.auth_user = self.request.user
42 c.user = c.auth_user.get_instance()
43
44 self._register_global_c(c)
45 return c
46
47 @LoginRequired()
48 @NotAnonymous()
49 @view_config(
50 route_name='my_account_auth_tokens', request_method='GET',
51 renderer='rhodecode:templates/admin/my_account/my_account.mako')
52 def my_account_auth_tokens(self):
53 _ = self.request.translate
54
55 c = self.load_default_context()
56 c.active = 'auth_tokens'
57
58 show_expired = True
59
60 c.lifetime_values = [
61 (str(-1), _('forever')),
62 (str(5), _('5 minutes')),
63 (str(60), _('1 hour')),
64 (str(60 * 24), _('1 day')),
65 (str(60 * 24 * 30), _('1 month')),
66 ]
67 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68 c.role_values = [
69 (x, AuthTokenModel.cls._get_role_name(x))
70 for x in AuthTokenModel.cls.ROLES]
71 c.role_options = [(c.role_values, _("Role"))]
72 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
73 c.user.user_id, show_expired=show_expired)
74 return self._get_template_context(c)
75
76 @LoginRequired()
77 @NotAnonymous()
78 @CSRFRequired()
79 @view_config(
80 route_name='my_account_auth_tokens_add', request_method='POST')
81 def my_account_auth_tokens_add(self):
82 _ = self.request.translate
83 c = self.load_default_context()
84
85 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
86 description = self.request.POST.get('description')
87 role = self.request.POST.get('role')
88
89 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
90 Session().commit()
91 h.flash(_("Auth token successfully created"), category='success')
92
93 return HTTPFound(h.route_path('my_account_auth_tokens'))
94
95 @LoginRequired()
96 @NotAnonymous()
97 @CSRFRequired()
98 @view_config(
99 route_name='my_account_auth_tokens_delete', request_method='POST')
100 def my_account_auth_tokens_delete(self):
101 _ = self.request.translate
102 c = self.load_default_context()
103
104 del_auth_token = self.request.POST.get('del_auth_token')
105
106 if del_auth_token:
107 AuthTokenModel().delete(del_auth_token, c.user.user_id)
108 Session().commit()
109 h.flash(_("Auth token successfully deleted"), category='success')
110
111 return HTTPFound(h.route_path('my_account_auth_tokens'))
@@ -1,53 +1,64 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 from pylons import tmpl_context as c
23 23
24 24 from rhodecode.lib.utils2 import StrictAttributeDict
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 ADMIN_PREFIX = '/_admin'
30 STATIC_FILE_PREFIX = '/_static'
31
32
29 33 class TemplateArgs(StrictAttributeDict):
30 34 pass
31 35
32 36
33 37 class BaseAppView(object):
34 38
35 39 def __init__(self, context, request):
36 40 self.request = request
37 41 self.context = context
38 42 self.session = request.session
39 43 self._rhodecode_user = request.user
40 44
41 45 def _get_local_tmpl_context(self):
42 46 return TemplateArgs()
43 47
48 def _register_global_c(self, tmpl_args):
49 """
50 Registers attributes to pylons global `c`
51 """
52 # TODO(marcink): remove once pyramid migration is finished
53 for k, v in tmpl_args.items():
54 setattr(c, k, v)
55
44 56 def _get_template_context(self, tmpl_args):
45 57
46 for k, v in tmpl_args.items():
47 setattr(c, k, v)
58 self._register_global_c(tmpl_args)
48 59
49 60 return {
50 61 'defaults': {},
51 62 'errors': {},
52 63 }
53 64
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,501 +1,502 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 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 47 from rhodecode.lib.middleware import csrf
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.error_handling import (
50 50 PylonsErrorHandlingMiddleware)
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import (
56 56 scan_repositories_if_enabled, write_metadata_if_needed)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 # this is used to avoid avoid the route lookup overhead in routesmiddleware
63 63 # for certain routes which won't go to pylons to - eg. static files, debugger
64 64 # it is only needed for the pylons migration and can be removed once complete
65 65 class SkippableRoutesMiddleware(RoutesMiddleware):
66 66 """ Routes middleware that allows you to skip prefixes """
67 67
68 68 def __init__(self, *args, **kw):
69 69 self.skip_prefixes = kw.pop('skip_prefixes', [])
70 70 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
71 71
72 72 def __call__(self, environ, start_response):
73 73 for prefix in self.skip_prefixes:
74 74 if environ['PATH_INFO'].startswith(prefix):
75 75 # added to avoid the case when a missing /_static route falls
76 76 # through to pylons and causes an exception as pylons is
77 77 # expecting wsgiorg.routingargs to be set in the environ
78 78 # by RoutesMiddleware.
79 79 if 'wsgiorg.routing_args' not in environ:
80 80 environ['wsgiorg.routing_args'] = (None, {})
81 81 return self.app(environ, start_response)
82 82
83 83 return super(SkippableRoutesMiddleware, self).__call__(
84 84 environ, start_response)
85 85
86 86
87 87 def make_app(global_conf, static_files=True, **app_conf):
88 88 """Create a Pylons WSGI application and return it
89 89
90 90 ``global_conf``
91 91 The inherited configuration for this application. Normally from
92 92 the [DEFAULT] section of the Paste ini file.
93 93
94 94 ``app_conf``
95 95 The application's local configuration. Normally specified in
96 96 the [app:<name>] section of the Paste ini file (where <name>
97 97 defaults to main).
98 98
99 99 """
100 100 # Apply compatibility patches
101 101 patches.kombu_1_5_1_python_2_7_11()
102 102 patches.inspect_getargspec()
103 103
104 104 # Configure the Pylons environment
105 105 config = load_environment(global_conf, app_conf)
106 106
107 107 # The Pylons WSGI app
108 108 app = PylonsApp(config=config)
109 109 if rhodecode.is_test:
110 110 app = csrf.CSRFDetector(app)
111 111
112 112 expected_origin = config.get('expected_origin')
113 113 if expected_origin:
114 114 # The API can be accessed from other Origins.
115 115 app = csrf.OriginChecker(app, expected_origin,
116 116 skip_urls=[routes.util.url_for('api')])
117 117
118 118 # Establish the Registry for this application
119 119 app = RegistryManager(app)
120 120
121 121 app.config = config
122 122
123 123 return app
124 124
125 125
126 126 def make_pyramid_app(global_config, **settings):
127 127 """
128 128 Constructs the WSGI application based on Pyramid and wraps the Pylons based
129 129 application.
130 130
131 131 Specials:
132 132
133 133 * We migrate from Pylons to Pyramid. While doing this, we keep both
134 134 frameworks functional. This involves moving some WSGI middlewares around
135 135 and providing access to some data internals, so that the old code is
136 136 still functional.
137 137
138 138 * The application can also be integrated like a plugin via the call to
139 139 `includeme`. This is accompanied with the other utility functions which
140 140 are called. Changing this should be done with great care to not break
141 141 cases when these fragments are assembled from another place.
142 142
143 143 """
144 144 # The edition string should be available in pylons too, so we add it here
145 145 # before copying the settings.
146 146 settings.setdefault('rhodecode.edition', 'Community Edition')
147 147
148 148 # As long as our Pylons application does expect "unprepared" settings, make
149 149 # sure that we keep an unmodified copy. This avoids unintentional change of
150 150 # behavior in the old application.
151 151 settings_pylons = settings.copy()
152 152
153 153 sanitize_settings_and_apply_defaults(settings)
154 154 config = Configurator(settings=settings)
155 155 add_pylons_compat_data(config.registry, global_config, settings_pylons)
156 156
157 157 load_pyramid_environment(global_config, settings)
158 158
159 159 includeme_first(config)
160 160 includeme(config)
161 161 pyramid_app = config.make_wsgi_app()
162 162 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
163 163 pyramid_app.config = config
164 164
165 165 # creating the app uses a connection - return it after we are done
166 166 meta.Session.remove()
167 167
168 168 return pyramid_app
169 169
170 170
171 171 def make_not_found_view(config):
172 172 """
173 173 This creates the view which should be registered as not-found-view to
174 174 pyramid. Basically it contains of the old pylons app, converted to a view.
175 175 Additionally it is wrapped by some other middlewares.
176 176 """
177 177 settings = config.registry.settings
178 178 vcs_server_enabled = settings['vcs.server.enable']
179 179
180 180 # Make pylons app from unprepared settings.
181 181 pylons_app = make_app(
182 182 config.registry._pylons_compat_global_config,
183 183 **config.registry._pylons_compat_settings)
184 184 config.registry._pylons_compat_config = pylons_app.config
185 185
186 186 # Appenlight monitoring.
187 187 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
188 188 pylons_app, settings)
189 189
190 190 # The pylons app is executed inside of the pyramid 404 exception handler.
191 191 # Exceptions which are raised inside of it are not handled by pyramid
192 192 # again. Therefore we add a middleware that invokes the error handler in
193 193 # case of an exception or error response. This way we return proper error
194 194 # HTML pages in case of an error.
195 195 reraise = (settings.get('debugtoolbar.enabled', False) or
196 196 rhodecode.disable_error_handler)
197 197 pylons_app = PylonsErrorHandlingMiddleware(
198 198 pylons_app, error_handler, reraise)
199 199
200 200 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
201 201 # view to handle the request. Therefore it is wrapped around the pylons
202 202 # app. It has to be outside of the error handling otherwise error responses
203 203 # from the vcsserver are converted to HTML error pages. This confuses the
204 204 # command line tools and the user won't get a meaningful error message.
205 205 if vcs_server_enabled:
206 206 pylons_app = VCSMiddleware(
207 207 pylons_app, settings, appenlight_client, registry=config.registry)
208 208
209 209 # Convert WSGI app to pyramid view and return it.
210 210 return wsgiapp(pylons_app)
211 211
212 212
213 213 def add_pylons_compat_data(registry, global_config, settings):
214 214 """
215 215 Attach data to the registry to support the Pylons integration.
216 216 """
217 217 registry._pylons_compat_global_config = global_config
218 218 registry._pylons_compat_settings = settings
219 219
220 220
221 221 def error_handler(exception, request):
222 222 import rhodecode
223 223 from rhodecode.lib.utils2 import AttributeDict
224 224
225 225 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
226 226
227 227 base_response = HTTPInternalServerError()
228 228 # prefer original exception for the response since it may have headers set
229 229 if isinstance(exception, HTTPException):
230 230 base_response = exception
231 231
232 232 def is_http_error(response):
233 233 # error which should have traceback
234 234 return response.status_code > 499
235 235
236 236 if is_http_error(base_response):
237 237 log.exception(
238 238 'error occurred handling this request for path: %s', request.path)
239 239
240 240 c = AttributeDict()
241 241 c.error_message = base_response.status
242 242 c.error_explanation = base_response.explanation or str(base_response)
243 243 c.visual = AttributeDict()
244 244
245 245 c.visual.rhodecode_support_url = (
246 246 request.registry.settings.get('rhodecode_support_url') or
247 247 request.route_url('rhodecode_support')
248 248 )
249 249 c.redirect_time = 0
250 250 c.rhodecode_name = rhodecode_title
251 251 if not c.rhodecode_name:
252 252 c.rhodecode_name = 'Rhodecode'
253 253
254 254 c.causes = []
255 255 if hasattr(base_response, 'causes'):
256 256 c.causes = base_response.causes
257 257
258 258 response = render_to_response(
259 259 '/errors/error_document.mako', {'c': c}, request=request,
260 260 response=base_response)
261 261
262 262 return response
263 263
264 264
265 265 def includeme(config):
266 266 settings = config.registry.settings
267 267
268 268 # plugin information
269 269 config.registry.rhodecode_plugins = OrderedDict()
270 270
271 271 config.add_directive(
272 272 'register_rhodecode_plugin', register_rhodecode_plugin)
273 273
274 274 if asbool(settings.get('appenlight', 'false')):
275 275 config.include('appenlight_client.ext.pyramid_tween')
276 276
277 277 # Includes which are required. The application would fail without them.
278 278 config.include('pyramid_mako')
279 279 config.include('pyramid_beaker')
280 280
281 281 config.include('rhodecode.authentication')
282 282 config.include('rhodecode.integrations')
283 283
284 284 # apps
285 285 config.include('rhodecode.apps.admin')
286 286 config.include('rhodecode.apps.channelstream')
287 287 config.include('rhodecode.apps.login')
288 288 config.include('rhodecode.apps.user_profile')
289 config.include('rhodecode.apps.my_account')
289 290
290 291 config.include('rhodecode.tweens')
291 292 config.include('rhodecode.api')
292 293 config.include('rhodecode.svn_support')
293 294
294 295 config.add_route(
295 296 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
296 297
297 298 config.add_translation_dirs('rhodecode:i18n/')
298 299 settings['default_locale_name'] = settings.get('lang', 'en')
299 300
300 301 # Add subscribers.
301 302 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
302 303 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
303 304
304 305 # Set the authorization policy.
305 306 authz_policy = ACLAuthorizationPolicy()
306 307 config.set_authorization_policy(authz_policy)
307 308
308 309 # Set the default renderer for HTML templates to mako.
309 310 config.add_mako_renderer('.html')
310 311
311 312 # include RhodeCode plugins
312 313 includes = aslist(settings.get('rhodecode.includes', []))
313 314 for inc in includes:
314 315 config.include(inc)
315 316
316 317 # This is the glue which allows us to migrate in chunks. By registering the
317 318 # pylons based application as the "Not Found" view in Pyramid, we will
318 319 # fallback to the old application each time the new one does not yet know
319 320 # how to handle a request.
320 321 config.add_notfound_view(make_not_found_view(config))
321 322
322 323 if not settings.get('debugtoolbar.enabled', False):
323 324 # if no toolbar, then any exception gets caught and rendered
324 325 config.add_view(error_handler, context=Exception)
325 326
326 327 config.add_view(error_handler, context=HTTPError)
327 328
328 329
329 330 def includeme_first(config):
330 331 # redirect automatic browser favicon.ico requests to correct place
331 332 def favicon_redirect(context, request):
332 333 return HTTPFound(
333 334 request.static_path('rhodecode:public/images/favicon.ico'))
334 335
335 336 config.add_view(favicon_redirect, route_name='favicon')
336 337 config.add_route('favicon', '/favicon.ico')
337 338
338 339 def robots_redirect(context, request):
339 340 return HTTPFound(
340 341 request.static_path('rhodecode:public/robots.txt'))
341 342
342 343 config.add_view(robots_redirect, route_name='robots')
343 344 config.add_route('robots', '/robots.txt')
344 345
345 346 config.add_static_view(
346 347 '_static/deform', 'deform:static')
347 348 config.add_static_view(
348 349 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
349 350
350 351
351 352 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
352 353 """
353 354 Apply outer WSGI middlewares around the application.
354 355
355 356 Part of this has been moved up from the Pylons layer, so that the
356 357 data is also available if old Pylons code is hit through an already ported
357 358 view.
358 359 """
359 360 settings = config.registry.settings
360 361
361 362 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
362 363 pyramid_app = HttpsFixup(pyramid_app, settings)
363 364
364 365 # Add RoutesMiddleware to support the pylons compatibility tween during
365 366 # migration to pyramid.
366 367 pyramid_app = SkippableRoutesMiddleware(
367 368 pyramid_app, config.registry._pylons_compat_config['routes.map'],
368 369 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
369 370
370 371 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
371 372
372 373 if settings['gzip_responses']:
373 374 pyramid_app = make_gzip_middleware(
374 375 pyramid_app, settings, compress_level=1)
375 376
376 377 # this should be the outer most middleware in the wsgi stack since
377 378 # middleware like Routes make database calls
378 379 def pyramid_app_with_cleanup(environ, start_response):
379 380 try:
380 381 return pyramid_app(environ, start_response)
381 382 finally:
382 383 # Dispose current database session and rollback uncommitted
383 384 # transactions.
384 385 meta.Session.remove()
385 386
386 387 # In a single threaded mode server, on non sqlite db we should have
387 388 # '0 Current Checked out connections' at the end of a request,
388 389 # if not, then something, somewhere is leaving a connection open
389 390 pool = meta.Base.metadata.bind.engine.pool
390 391 log.debug('sa pool status: %s', pool.status())
391 392
392 393
393 394 return pyramid_app_with_cleanup
394 395
395 396
396 397 def sanitize_settings_and_apply_defaults(settings):
397 398 """
398 399 Applies settings defaults and does all type conversion.
399 400
400 401 We would move all settings parsing and preparation into this place, so that
401 402 we have only one place left which deals with this part. The remaining parts
402 403 of the application would start to rely fully on well prepared settings.
403 404
404 405 This piece would later be split up per topic to avoid a big fat monster
405 406 function.
406 407 """
407 408
408 409 # Pyramid's mako renderer has to search in the templates folder so that the
409 410 # old templates still work. Ported and new templates are expected to use
410 411 # real asset specifications for the includes.
411 412 mako_directories = settings.setdefault('mako.directories', [
412 413 # Base templates of the original Pylons application
413 414 'rhodecode:templates',
414 415 ])
415 416 log.debug(
416 417 "Using the following Mako template directories: %s",
417 418 mako_directories)
418 419
419 420 # Default includes, possible to change as a user
420 421 pyramid_includes = settings.setdefault('pyramid.includes', [
421 422 'rhodecode.lib.middleware.request_wrapper',
422 423 ])
423 424 log.debug(
424 425 "Using the following pyramid.includes: %s",
425 426 pyramid_includes)
426 427
427 428 # TODO: johbo: Re-think this, usually the call to config.include
428 429 # should allow to pass in a prefix.
429 430 settings.setdefault('rhodecode.api.url', '/_admin/api')
430 431
431 432 # Sanitize generic settings.
432 433 _list_setting(settings, 'default_encoding', 'UTF-8')
433 434 _bool_setting(settings, 'is_test', 'false')
434 435 _bool_setting(settings, 'gzip_responses', 'false')
435 436
436 437 # Call split out functions that sanitize settings for each topic.
437 438 _sanitize_appenlight_settings(settings)
438 439 _sanitize_vcs_settings(settings)
439 440
440 441 return settings
441 442
442 443
443 444 def _sanitize_appenlight_settings(settings):
444 445 _bool_setting(settings, 'appenlight', 'false')
445 446
446 447
447 448 def _sanitize_vcs_settings(settings):
448 449 """
449 450 Applies settings defaults and does type conversion for all VCS related
450 451 settings.
451 452 """
452 453 _string_setting(settings, 'vcs.svn.compatible_version', '')
453 454 _string_setting(settings, 'git_rev_filter', '--all')
454 455 _string_setting(settings, 'vcs.hooks.protocol', 'http')
455 456 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
456 457 _string_setting(settings, 'vcs.server', '')
457 458 _string_setting(settings, 'vcs.server.log_level', 'debug')
458 459 _string_setting(settings, 'vcs.server.protocol', 'http')
459 460 _bool_setting(settings, 'startup.import_repos', 'false')
460 461 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
461 462 _bool_setting(settings, 'vcs.server.enable', 'true')
462 463 _bool_setting(settings, 'vcs.start_server', 'false')
463 464 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
464 465 _int_setting(settings, 'vcs.connection_timeout', 3600)
465 466
466 467 # Support legacy values of vcs.scm_app_implementation. Legacy
467 468 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
468 469 # which is now mapped to 'http'.
469 470 scm_app_impl = settings['vcs.scm_app_implementation']
470 471 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
471 472 settings['vcs.scm_app_implementation'] = 'http'
472 473
473 474
474 475 def _int_setting(settings, name, default):
475 476 settings[name] = int(settings.get(name, default))
476 477
477 478
478 479 def _bool_setting(settings, name, default):
479 480 input = settings.get(name, default)
480 481 if isinstance(input, unicode):
481 482 input = input.encode('utf8')
482 483 settings[name] = asbool(input)
483 484
484 485
485 486 def _list_setting(settings, name, default):
486 487 raw_value = settings.get(name, default)
487 488
488 489 old_separator = ','
489 490 if old_separator in raw_value:
490 491 # If we get a comma separated list, pass it to our own function.
491 492 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
492 493 else:
493 494 # Otherwise we assume it uses pyramids space/newline separation.
494 495 settings[name] = aslist(raw_value)
495 496
496 497
497 498 def _string_setting(settings, name, default, lower=True):
498 499 value = settings.get(name, default)
499 500 if lower:
500 501 value = value.lower()
501 502 settings[name] = value
@@ -1,1169 +1,1163 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 295 m.connect('users', '/users',
296 296 action='index', conditions={'method': ['GET']})
297 297 m.connect('new_user', '/users/new',
298 298 action='new', conditions={'method': ['GET']})
299 299 m.connect('update_user', '/users/{user_id}',
300 300 action='update', conditions={'method': ['PUT']})
301 301 m.connect('delete_user', '/users/{user_id}',
302 302 action='delete', conditions={'method': ['DELETE']})
303 303 m.connect('edit_user', '/users/{user_id}/edit',
304 304 action='edit', conditions={'method': ['GET']}, jsroute=True)
305 305 m.connect('user', '/users/{user_id}',
306 306 action='show', conditions={'method': ['GET']})
307 307 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
308 308 action='reset_password', conditions={'method': ['POST']})
309 309 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
310 310 action='create_personal_repo_group', conditions={'method': ['POST']})
311 311
312 312 # EXTRAS USER ROUTES
313 313 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
314 314 action='edit_advanced', conditions={'method': ['GET']})
315 315 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
316 316 action='update_advanced', conditions={'method': ['PUT']})
317 317
318 318 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
319 319 action='edit_auth_tokens', conditions={'method': ['GET']})
320 320 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
321 321 action='add_auth_token', conditions={'method': ['PUT']})
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='delete_auth_token', conditions={'method': ['DELETE']})
324 324
325 325 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
326 326 action='edit_global_perms', conditions={'method': ['GET']})
327 327 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
328 328 action='update_global_perms', conditions={'method': ['PUT']})
329 329
330 330 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
331 331 action='edit_perms_summary', conditions={'method': ['GET']})
332 332
333 333 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
334 334 action='edit_emails', conditions={'method': ['GET']})
335 335 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
336 336 action='add_email', conditions={'method': ['PUT']})
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='delete_email', conditions={'method': ['DELETE']})
339 339
340 340 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
341 341 action='edit_ips', conditions={'method': ['GET']})
342 342 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
343 343 action='add_ip', conditions={'method': ['PUT']})
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='delete_ip', conditions={'method': ['DELETE']})
346 346
347 347 # ADMIN USER GROUPS REST ROUTES
348 348 with rmap.submapper(path_prefix=ADMIN_PREFIX,
349 349 controller='admin/user_groups') as m:
350 350 m.connect('users_groups', '/user_groups',
351 351 action='create', conditions={'method': ['POST']})
352 352 m.connect('users_groups', '/user_groups',
353 353 action='index', conditions={'method': ['GET']})
354 354 m.connect('new_users_group', '/user_groups/new',
355 355 action='new', conditions={'method': ['GET']})
356 356 m.connect('update_users_group', '/user_groups/{user_group_id}',
357 357 action='update', conditions={'method': ['PUT']})
358 358 m.connect('delete_users_group', '/user_groups/{user_group_id}',
359 359 action='delete', conditions={'method': ['DELETE']})
360 360 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
361 361 action='edit', conditions={'method': ['GET']},
362 362 function=check_user_group)
363 363
364 364 # EXTRAS USER GROUP ROUTES
365 365 m.connect('edit_user_group_global_perms',
366 366 '/user_groups/{user_group_id}/edit/global_permissions',
367 367 action='edit_global_perms', conditions={'method': ['GET']})
368 368 m.connect('edit_user_group_global_perms',
369 369 '/user_groups/{user_group_id}/edit/global_permissions',
370 370 action='update_global_perms', conditions={'method': ['PUT']})
371 371 m.connect('edit_user_group_perms_summary',
372 372 '/user_groups/{user_group_id}/edit/permissions_summary',
373 373 action='edit_perms_summary', conditions={'method': ['GET']})
374 374
375 375 m.connect('edit_user_group_perms',
376 376 '/user_groups/{user_group_id}/edit/permissions',
377 377 action='edit_perms', conditions={'method': ['GET']})
378 378 m.connect('edit_user_group_perms',
379 379 '/user_groups/{user_group_id}/edit/permissions',
380 380 action='update_perms', conditions={'method': ['PUT']})
381 381
382 382 m.connect('edit_user_group_advanced',
383 383 '/user_groups/{user_group_id}/edit/advanced',
384 384 action='edit_advanced', conditions={'method': ['GET']})
385 385
386 386 m.connect('edit_user_group_members',
387 387 '/user_groups/{user_group_id}/edit/members', jsroute=True,
388 388 action='user_group_members', conditions={'method': ['GET']})
389 389
390 390 # ADMIN PERMISSIONS ROUTES
391 391 with rmap.submapper(path_prefix=ADMIN_PREFIX,
392 392 controller='admin/permissions') as m:
393 393 m.connect('admin_permissions_application', '/permissions/application',
394 394 action='permission_application_update', conditions={'method': ['POST']})
395 395 m.connect('admin_permissions_application', '/permissions/application',
396 396 action='permission_application', conditions={'method': ['GET']})
397 397
398 398 m.connect('admin_permissions_global', '/permissions/global',
399 399 action='permission_global_update', conditions={'method': ['POST']})
400 400 m.connect('admin_permissions_global', '/permissions/global',
401 401 action='permission_global', conditions={'method': ['GET']})
402 402
403 403 m.connect('admin_permissions_object', '/permissions/object',
404 404 action='permission_objects_update', conditions={'method': ['POST']})
405 405 m.connect('admin_permissions_object', '/permissions/object',
406 406 action='permission_objects', conditions={'method': ['GET']})
407 407
408 408 m.connect('admin_permissions_ips', '/permissions/ips',
409 409 action='permission_ips', conditions={'method': ['POST']})
410 410 m.connect('admin_permissions_ips', '/permissions/ips',
411 411 action='permission_ips', conditions={'method': ['GET']})
412 412
413 413 m.connect('admin_permissions_overview', '/permissions/overview',
414 414 action='permission_perms', conditions={'method': ['GET']})
415 415
416 416 # ADMIN DEFAULTS REST ROUTES
417 417 with rmap.submapper(path_prefix=ADMIN_PREFIX,
418 418 controller='admin/defaults') as m:
419 419 m.connect('admin_defaults_repositories', '/defaults/repositories',
420 420 action='update_repository_defaults', conditions={'method': ['POST']})
421 421 m.connect('admin_defaults_repositories', '/defaults/repositories',
422 422 action='index', conditions={'method': ['GET']})
423 423
424 424 # ADMIN DEBUG STYLE ROUTES
425 425 if str2bool(config.get('debug_style')):
426 426 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
427 427 controller='debug_style') as m:
428 428 m.connect('debug_style_home', '',
429 429 action='index', conditions={'method': ['GET']})
430 430 m.connect('debug_style_template', '/t/{t_path}',
431 431 action='template', conditions={'method': ['GET']})
432 432
433 433 # ADMIN SETTINGS ROUTES
434 434 with rmap.submapper(path_prefix=ADMIN_PREFIX,
435 435 controller='admin/settings') as m:
436 436
437 437 # default
438 438 m.connect('admin_settings', '/settings',
439 439 action='settings_global_update',
440 440 conditions={'method': ['POST']})
441 441 m.connect('admin_settings', '/settings',
442 442 action='settings_global', conditions={'method': ['GET']})
443 443
444 444 m.connect('admin_settings_vcs', '/settings/vcs',
445 445 action='settings_vcs_update',
446 446 conditions={'method': ['POST']})
447 447 m.connect('admin_settings_vcs', '/settings/vcs',
448 448 action='settings_vcs',
449 449 conditions={'method': ['GET']})
450 450 m.connect('admin_settings_vcs', '/settings/vcs',
451 451 action='delete_svn_pattern',
452 452 conditions={'method': ['DELETE']})
453 453
454 454 m.connect('admin_settings_mapping', '/settings/mapping',
455 455 action='settings_mapping_update',
456 456 conditions={'method': ['POST']})
457 457 m.connect('admin_settings_mapping', '/settings/mapping',
458 458 action='settings_mapping', conditions={'method': ['GET']})
459 459
460 460 m.connect('admin_settings_global', '/settings/global',
461 461 action='settings_global_update',
462 462 conditions={'method': ['POST']})
463 463 m.connect('admin_settings_global', '/settings/global',
464 464 action='settings_global', conditions={'method': ['GET']})
465 465
466 466 m.connect('admin_settings_visual', '/settings/visual',
467 467 action='settings_visual_update',
468 468 conditions={'method': ['POST']})
469 469 m.connect('admin_settings_visual', '/settings/visual',
470 470 action='settings_visual', conditions={'method': ['GET']})
471 471
472 472 m.connect('admin_settings_issuetracker',
473 473 '/settings/issue-tracker', action='settings_issuetracker',
474 474 conditions={'method': ['GET']})
475 475 m.connect('admin_settings_issuetracker_save',
476 476 '/settings/issue-tracker/save',
477 477 action='settings_issuetracker_save',
478 478 conditions={'method': ['POST']})
479 479 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
480 480 action='settings_issuetracker_test',
481 481 conditions={'method': ['POST']})
482 482 m.connect('admin_issuetracker_delete',
483 483 '/settings/issue-tracker/delete',
484 484 action='settings_issuetracker_delete',
485 485 conditions={'method': ['DELETE']})
486 486
487 487 m.connect('admin_settings_email', '/settings/email',
488 488 action='settings_email_update',
489 489 conditions={'method': ['POST']})
490 490 m.connect('admin_settings_email', '/settings/email',
491 491 action='settings_email', conditions={'method': ['GET']})
492 492
493 493 m.connect('admin_settings_hooks', '/settings/hooks',
494 494 action='settings_hooks_update',
495 495 conditions={'method': ['POST', 'DELETE']})
496 496 m.connect('admin_settings_hooks', '/settings/hooks',
497 497 action='settings_hooks', conditions={'method': ['GET']})
498 498
499 499 m.connect('admin_settings_search', '/settings/search',
500 500 action='settings_search', conditions={'method': ['GET']})
501 501
502 502 m.connect('admin_settings_supervisor', '/settings/supervisor',
503 503 action='settings_supervisor', conditions={'method': ['GET']})
504 504 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
505 505 action='settings_supervisor_log', conditions={'method': ['GET']})
506 506
507 507 m.connect('admin_settings_labs', '/settings/labs',
508 508 action='settings_labs_update',
509 509 conditions={'method': ['POST']})
510 510 m.connect('admin_settings_labs', '/settings/labs',
511 511 action='settings_labs', conditions={'method': ['GET']})
512 512
513 513 # ADMIN MY ACCOUNT
514 514 with rmap.submapper(path_prefix=ADMIN_PREFIX,
515 515 controller='admin/my_account') as m:
516 516
517 517 m.connect('my_account', '/my_account',
518 518 action='my_account', conditions={'method': ['GET']})
519 519 m.connect('my_account_edit', '/my_account/edit',
520 520 action='my_account_edit', conditions={'method': ['GET']})
521 521 m.connect('my_account', '/my_account',
522 522 action='my_account_update', conditions={'method': ['POST']})
523 523
524 524 m.connect('my_account_password', '/my_account/password',
525 525 action='my_account_password', conditions={'method': ['GET', 'POST']})
526 526
527 527 m.connect('my_account_repos', '/my_account/repos',
528 528 action='my_account_repos', conditions={'method': ['GET']})
529 529
530 530 m.connect('my_account_watched', '/my_account/watched',
531 531 action='my_account_watched', conditions={'method': ['GET']})
532 532
533 533 m.connect('my_account_pullrequests', '/my_account/pull_requests',
534 534 action='my_account_pullrequests', conditions={'method': ['GET']})
535 535
536 536 m.connect('my_account_perms', '/my_account/perms',
537 537 action='my_account_perms', conditions={'method': ['GET']})
538 538
539 539 m.connect('my_account_emails', '/my_account/emails',
540 540 action='my_account_emails', conditions={'method': ['GET']})
541 541 m.connect('my_account_emails', '/my_account/emails',
542 542 action='my_account_emails_add', conditions={'method': ['POST']})
543 543 m.connect('my_account_emails', '/my_account/emails',
544 544 action='my_account_emails_delete', conditions={'method': ['DELETE']})
545 545
546 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
547 action='my_account_auth_tokens', conditions={'method': ['GET']})
548 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
549 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
551 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
552 546 m.connect('my_account_notifications', '/my_account/notifications',
553 547 action='my_notifications',
554 548 conditions={'method': ['GET']})
555 549 m.connect('my_account_notifications_toggle_visibility',
556 550 '/my_account/toggle_visibility',
557 551 action='my_notifications_toggle_visibility',
558 552 conditions={'method': ['POST']})
559 553 m.connect('my_account_notifications_test_channelstream',
560 554 '/my_account/test_channelstream',
561 555 action='my_account_notifications_test_channelstream',
562 556 conditions={'method': ['POST']})
563 557
564 558 # NOTIFICATION REST ROUTES
565 559 with rmap.submapper(path_prefix=ADMIN_PREFIX,
566 560 controller='admin/notifications') as m:
567 561 m.connect('notifications', '/notifications',
568 562 action='index', conditions={'method': ['GET']})
569 563 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
570 564 action='mark_all_read', conditions={'method': ['POST']})
571 565 m.connect('/notifications/{notification_id}',
572 566 action='update', conditions={'method': ['PUT']})
573 567 m.connect('/notifications/{notification_id}',
574 568 action='delete', conditions={'method': ['DELETE']})
575 569 m.connect('notification', '/notifications/{notification_id}',
576 570 action='show', conditions={'method': ['GET']})
577 571
578 572 # ADMIN GIST
579 573 with rmap.submapper(path_prefix=ADMIN_PREFIX,
580 574 controller='admin/gists') as m:
581 575 m.connect('gists', '/gists',
582 576 action='create', conditions={'method': ['POST']})
583 577 m.connect('gists', '/gists', jsroute=True,
584 578 action='index', conditions={'method': ['GET']})
585 579 m.connect('new_gist', '/gists/new', jsroute=True,
586 580 action='new', conditions={'method': ['GET']})
587 581
588 582 m.connect('/gists/{gist_id}',
589 583 action='delete', conditions={'method': ['DELETE']})
590 584 m.connect('edit_gist', '/gists/{gist_id}/edit',
591 585 action='edit_form', conditions={'method': ['GET']})
592 586 m.connect('edit_gist', '/gists/{gist_id}/edit',
593 587 action='edit', conditions={'method': ['POST']})
594 588 m.connect(
595 589 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
596 590 action='check_revision', conditions={'method': ['GET']})
597 591
598 592 m.connect('gist', '/gists/{gist_id}',
599 593 action='show', conditions={'method': ['GET']})
600 594 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
601 595 revision='tip',
602 596 action='show', conditions={'method': ['GET']})
603 597 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
604 598 revision='tip',
605 599 action='show', conditions={'method': ['GET']})
606 600 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
607 601 revision='tip',
608 602 action='show', conditions={'method': ['GET']},
609 603 requirements=URL_NAME_REQUIREMENTS)
610 604
611 605 # ADMIN MAIN PAGES
612 606 with rmap.submapper(path_prefix=ADMIN_PREFIX,
613 607 controller='admin/admin') as m:
614 608 m.connect('admin_home', '', action='index')
615 609 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
616 610 action='add_repo')
617 611 m.connect(
618 612 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
619 613 action='pull_requests')
620 614 m.connect(
621 615 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
622 616 action='pull_requests')
623 617 m.connect(
624 618 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
625 619 action='pull_requests')
626 620
627 621 # USER JOURNAL
628 622 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
629 623 controller='journal', action='index')
630 624 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
631 625 controller='journal', action='journal_rss')
632 626 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
633 627 controller='journal', action='journal_atom')
634 628
635 629 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
636 630 controller='journal', action='public_journal')
637 631
638 632 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
639 633 controller='journal', action='public_journal_rss')
640 634
641 635 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
642 636 controller='journal', action='public_journal_rss')
643 637
644 638 rmap.connect('public_journal_atom',
645 639 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
646 640 action='public_journal_atom')
647 641
648 642 rmap.connect('public_journal_atom_old',
649 643 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
650 644 action='public_journal_atom')
651 645
652 646 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
653 647 controller='journal', action='toggle_following', jsroute=True,
654 648 conditions={'method': ['POST']})
655 649
656 650 # FULL TEXT SEARCH
657 651 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
658 652 controller='search')
659 653 rmap.connect('search_repo_home', '/{repo_name}/search',
660 654 controller='search',
661 655 action='index',
662 656 conditions={'function': check_repo},
663 657 requirements=URL_NAME_REQUIREMENTS)
664 658
665 659 # FEEDS
666 660 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
667 661 controller='feed', action='rss',
668 662 conditions={'function': check_repo},
669 663 requirements=URL_NAME_REQUIREMENTS)
670 664
671 665 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
672 666 controller='feed', action='atom',
673 667 conditions={'function': check_repo},
674 668 requirements=URL_NAME_REQUIREMENTS)
675 669
676 670 #==========================================================================
677 671 # REPOSITORY ROUTES
678 672 #==========================================================================
679 673
680 674 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
681 675 controller='admin/repos', action='repo_creating',
682 676 requirements=URL_NAME_REQUIREMENTS)
683 677 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
684 678 controller='admin/repos', action='repo_check',
685 679 requirements=URL_NAME_REQUIREMENTS)
686 680
687 681 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
688 682 controller='summary', action='repo_stats',
689 683 conditions={'function': check_repo},
690 684 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
691 685
692 686 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
693 687 controller='summary', action='repo_refs_data',
694 688 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 689 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
696 690 controller='summary', action='repo_refs_changelog_data',
697 691 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
698 692 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
699 693 controller='summary', action='repo_default_reviewers_data',
700 694 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
701 695
702 696 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
703 697 controller='changeset', revision='tip',
704 698 conditions={'function': check_repo},
705 699 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
706 700 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
707 701 controller='changeset', revision='tip', action='changeset_children',
708 702 conditions={'function': check_repo},
709 703 requirements=URL_NAME_REQUIREMENTS)
710 704 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
711 705 controller='changeset', revision='tip', action='changeset_parents',
712 706 conditions={'function': check_repo},
713 707 requirements=URL_NAME_REQUIREMENTS)
714 708
715 709 # repo edit options
716 710 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
717 711 controller='admin/repos', action='edit',
718 712 conditions={'method': ['GET'], 'function': check_repo},
719 713 requirements=URL_NAME_REQUIREMENTS)
720 714
721 715 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
722 716 jsroute=True,
723 717 controller='admin/repos', action='edit_permissions',
724 718 conditions={'method': ['GET'], 'function': check_repo},
725 719 requirements=URL_NAME_REQUIREMENTS)
726 720 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
727 721 controller='admin/repos', action='edit_permissions_update',
728 722 conditions={'method': ['PUT'], 'function': check_repo},
729 723 requirements=URL_NAME_REQUIREMENTS)
730 724
731 725 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
732 726 controller='admin/repos', action='edit_fields',
733 727 conditions={'method': ['GET'], 'function': check_repo},
734 728 requirements=URL_NAME_REQUIREMENTS)
735 729 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
736 730 controller='admin/repos', action='create_repo_field',
737 731 conditions={'method': ['PUT'], 'function': check_repo},
738 732 requirements=URL_NAME_REQUIREMENTS)
739 733 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
740 734 controller='admin/repos', action='delete_repo_field',
741 735 conditions={'method': ['DELETE'], 'function': check_repo},
742 736 requirements=URL_NAME_REQUIREMENTS)
743 737
744 738 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
745 739 controller='admin/repos', action='edit_advanced',
746 740 conditions={'method': ['GET'], 'function': check_repo},
747 741 requirements=URL_NAME_REQUIREMENTS)
748 742
749 743 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
750 744 controller='admin/repos', action='edit_advanced_locking',
751 745 conditions={'method': ['PUT'], 'function': check_repo},
752 746 requirements=URL_NAME_REQUIREMENTS)
753 747 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
754 748 controller='admin/repos', action='toggle_locking',
755 749 conditions={'method': ['GET'], 'function': check_repo},
756 750 requirements=URL_NAME_REQUIREMENTS)
757 751
758 752 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
759 753 controller='admin/repos', action='edit_advanced_journal',
760 754 conditions={'method': ['PUT'], 'function': check_repo},
761 755 requirements=URL_NAME_REQUIREMENTS)
762 756
763 757 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
764 758 controller='admin/repos', action='edit_advanced_fork',
765 759 conditions={'method': ['PUT'], 'function': check_repo},
766 760 requirements=URL_NAME_REQUIREMENTS)
767 761
768 762 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
769 763 controller='admin/repos', action='edit_caches_form',
770 764 conditions={'method': ['GET'], 'function': check_repo},
771 765 requirements=URL_NAME_REQUIREMENTS)
772 766 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 767 controller='admin/repos', action='edit_caches',
774 768 conditions={'method': ['PUT'], 'function': check_repo},
775 769 requirements=URL_NAME_REQUIREMENTS)
776 770
777 771 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
778 772 controller='admin/repos', action='edit_remote_form',
779 773 conditions={'method': ['GET'], 'function': check_repo},
780 774 requirements=URL_NAME_REQUIREMENTS)
781 775 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 776 controller='admin/repos', action='edit_remote',
783 777 conditions={'method': ['PUT'], 'function': check_repo},
784 778 requirements=URL_NAME_REQUIREMENTS)
785 779
786 780 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
787 781 controller='admin/repos', action='edit_statistics_form',
788 782 conditions={'method': ['GET'], 'function': check_repo},
789 783 requirements=URL_NAME_REQUIREMENTS)
790 784 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 785 controller='admin/repos', action='edit_statistics',
792 786 conditions={'method': ['PUT'], 'function': check_repo},
793 787 requirements=URL_NAME_REQUIREMENTS)
794 788 rmap.connect('repo_settings_issuetracker',
795 789 '/{repo_name}/settings/issue-tracker',
796 790 controller='admin/repos', action='repo_issuetracker',
797 791 conditions={'method': ['GET'], 'function': check_repo},
798 792 requirements=URL_NAME_REQUIREMENTS)
799 793 rmap.connect('repo_issuetracker_test',
800 794 '/{repo_name}/settings/issue-tracker/test',
801 795 controller='admin/repos', action='repo_issuetracker_test',
802 796 conditions={'method': ['POST'], 'function': check_repo},
803 797 requirements=URL_NAME_REQUIREMENTS)
804 798 rmap.connect('repo_issuetracker_delete',
805 799 '/{repo_name}/settings/issue-tracker/delete',
806 800 controller='admin/repos', action='repo_issuetracker_delete',
807 801 conditions={'method': ['DELETE'], 'function': check_repo},
808 802 requirements=URL_NAME_REQUIREMENTS)
809 803 rmap.connect('repo_issuetracker_save',
810 804 '/{repo_name}/settings/issue-tracker/save',
811 805 controller='admin/repos', action='repo_issuetracker_save',
812 806 conditions={'method': ['POST'], 'function': check_repo},
813 807 requirements=URL_NAME_REQUIREMENTS)
814 808 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
815 809 controller='admin/repos', action='repo_settings_vcs_update',
816 810 conditions={'method': ['POST'], 'function': check_repo},
817 811 requirements=URL_NAME_REQUIREMENTS)
818 812 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 813 controller='admin/repos', action='repo_settings_vcs',
820 814 conditions={'method': ['GET'], 'function': check_repo},
821 815 requirements=URL_NAME_REQUIREMENTS)
822 816 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 817 controller='admin/repos', action='repo_delete_svn_pattern',
824 818 conditions={'method': ['DELETE'], 'function': check_repo},
825 819 requirements=URL_NAME_REQUIREMENTS)
826 820 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
827 821 controller='admin/repos', action='repo_settings_pullrequest',
828 822 conditions={'method': ['GET', 'POST'], 'function': check_repo},
829 823 requirements=URL_NAME_REQUIREMENTS)
830 824
831 825 # still working url for backward compat.
832 826 rmap.connect('raw_changeset_home_depraced',
833 827 '/{repo_name}/raw-changeset/{revision}',
834 828 controller='changeset', action='changeset_raw',
835 829 revision='tip', conditions={'function': check_repo},
836 830 requirements=URL_NAME_REQUIREMENTS)
837 831
838 832 # new URLs
839 833 rmap.connect('changeset_raw_home',
840 834 '/{repo_name}/changeset-diff/{revision}',
841 835 controller='changeset', action='changeset_raw',
842 836 revision='tip', conditions={'function': check_repo},
843 837 requirements=URL_NAME_REQUIREMENTS)
844 838
845 839 rmap.connect('changeset_patch_home',
846 840 '/{repo_name}/changeset-patch/{revision}',
847 841 controller='changeset', action='changeset_patch',
848 842 revision='tip', conditions={'function': check_repo},
849 843 requirements=URL_NAME_REQUIREMENTS)
850 844
851 845 rmap.connect('changeset_download_home',
852 846 '/{repo_name}/changeset-download/{revision}',
853 847 controller='changeset', action='changeset_download',
854 848 revision='tip', conditions={'function': check_repo},
855 849 requirements=URL_NAME_REQUIREMENTS)
856 850
857 851 rmap.connect('changeset_comment',
858 852 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
859 853 controller='changeset', revision='tip', action='comment',
860 854 conditions={'function': check_repo},
861 855 requirements=URL_NAME_REQUIREMENTS)
862 856
863 857 rmap.connect('changeset_comment_preview',
864 858 '/{repo_name}/changeset/comment/preview', jsroute=True,
865 859 controller='changeset', action='preview_comment',
866 860 conditions={'function': check_repo, 'method': ['POST']},
867 861 requirements=URL_NAME_REQUIREMENTS)
868 862
869 863 rmap.connect('changeset_comment_delete',
870 864 '/{repo_name}/changeset/comment/{comment_id}/delete',
871 865 controller='changeset', action='delete_comment',
872 866 conditions={'function': check_repo, 'method': ['DELETE']},
873 867 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
874 868
875 869 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
876 870 controller='changeset', action='changeset_info',
877 871 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878 872
879 873 rmap.connect('compare_home',
880 874 '/{repo_name}/compare',
881 875 controller='compare', action='index',
882 876 conditions={'function': check_repo},
883 877 requirements=URL_NAME_REQUIREMENTS)
884 878
885 879 rmap.connect('compare_url',
886 880 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
887 881 controller='compare', action='compare',
888 882 conditions={'function': check_repo},
889 883 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
890 884
891 885 rmap.connect('pullrequest_home',
892 886 '/{repo_name}/pull-request/new', controller='pullrequests',
893 887 action='index', conditions={'function': check_repo,
894 888 'method': ['GET']},
895 889 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
896 890
897 891 rmap.connect('pullrequest',
898 892 '/{repo_name}/pull-request/new', controller='pullrequests',
899 893 action='create', conditions={'function': check_repo,
900 894 'method': ['POST']},
901 895 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
902 896
903 897 rmap.connect('pullrequest_repo_refs',
904 898 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
905 899 controller='pullrequests',
906 900 action='get_repo_refs',
907 901 conditions={'function': check_repo, 'method': ['GET']},
908 902 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
909 903
910 904 rmap.connect('pullrequest_repo_destinations',
911 905 '/{repo_name}/pull-request/repo-destinations',
912 906 controller='pullrequests',
913 907 action='get_repo_destinations',
914 908 conditions={'function': check_repo, 'method': ['GET']},
915 909 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
916 910
917 911 rmap.connect('pullrequest_show',
918 912 '/{repo_name}/pull-request/{pull_request_id}',
919 913 controller='pullrequests',
920 914 action='show', conditions={'function': check_repo,
921 915 'method': ['GET']},
922 916 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
923 917
924 918 rmap.connect('pullrequest_update',
925 919 '/{repo_name}/pull-request/{pull_request_id}',
926 920 controller='pullrequests',
927 921 action='update', conditions={'function': check_repo,
928 922 'method': ['PUT']},
929 923 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
930 924
931 925 rmap.connect('pullrequest_merge',
932 926 '/{repo_name}/pull-request/{pull_request_id}',
933 927 controller='pullrequests',
934 928 action='merge', conditions={'function': check_repo,
935 929 'method': ['POST']},
936 930 requirements=URL_NAME_REQUIREMENTS)
937 931
938 932 rmap.connect('pullrequest_delete',
939 933 '/{repo_name}/pull-request/{pull_request_id}',
940 934 controller='pullrequests',
941 935 action='delete', conditions={'function': check_repo,
942 936 'method': ['DELETE']},
943 937 requirements=URL_NAME_REQUIREMENTS)
944 938
945 939 rmap.connect('pullrequest_show_all',
946 940 '/{repo_name}/pull-request',
947 941 controller='pullrequests',
948 942 action='show_all', conditions={'function': check_repo,
949 943 'method': ['GET']},
950 944 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
951 945
952 946 rmap.connect('pullrequest_comment',
953 947 '/{repo_name}/pull-request-comment/{pull_request_id}',
954 948 controller='pullrequests',
955 949 action='comment', conditions={'function': check_repo,
956 950 'method': ['POST']},
957 951 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
958 952
959 953 rmap.connect('pullrequest_comment_delete',
960 954 '/{repo_name}/pull-request-comment/{comment_id}/delete',
961 955 controller='pullrequests', action='delete_comment',
962 956 conditions={'function': check_repo, 'method': ['DELETE']},
963 957 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
964 958
965 959 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
966 960 controller='summary', conditions={'function': check_repo},
967 961 requirements=URL_NAME_REQUIREMENTS)
968 962
969 963 rmap.connect('branches_home', '/{repo_name}/branches',
970 964 controller='branches', conditions={'function': check_repo},
971 965 requirements=URL_NAME_REQUIREMENTS)
972 966
973 967 rmap.connect('tags_home', '/{repo_name}/tags',
974 968 controller='tags', conditions={'function': check_repo},
975 969 requirements=URL_NAME_REQUIREMENTS)
976 970
977 971 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
978 972 controller='bookmarks', conditions={'function': check_repo},
979 973 requirements=URL_NAME_REQUIREMENTS)
980 974
981 975 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
982 976 controller='changelog', conditions={'function': check_repo},
983 977 requirements=URL_NAME_REQUIREMENTS)
984 978
985 979 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
986 980 controller='changelog', action='changelog_summary',
987 981 conditions={'function': check_repo},
988 982 requirements=URL_NAME_REQUIREMENTS)
989 983
990 984 rmap.connect('changelog_file_home',
991 985 '/{repo_name}/changelog/{revision}/{f_path}',
992 986 controller='changelog', f_path=None,
993 987 conditions={'function': check_repo},
994 988 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
995 989
996 990 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
997 991 controller='changelog', action='changelog_elements',
998 992 conditions={'function': check_repo},
999 993 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1000 994
1001 995 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1002 996 controller='files', revision='tip', f_path='',
1003 997 conditions={'function': check_repo},
1004 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1005 999
1006 1000 rmap.connect('files_home_simple_catchrev',
1007 1001 '/{repo_name}/files/{revision}',
1008 1002 controller='files', revision='tip', f_path='',
1009 1003 conditions={'function': check_repo},
1010 1004 requirements=URL_NAME_REQUIREMENTS)
1011 1005
1012 1006 rmap.connect('files_home_simple_catchall',
1013 1007 '/{repo_name}/files',
1014 1008 controller='files', revision='tip', f_path='',
1015 1009 conditions={'function': check_repo},
1016 1010 requirements=URL_NAME_REQUIREMENTS)
1017 1011
1018 1012 rmap.connect('files_history_home',
1019 1013 '/{repo_name}/history/{revision}/{f_path}',
1020 1014 controller='files', action='history', revision='tip', f_path='',
1021 1015 conditions={'function': check_repo},
1022 1016 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1023 1017
1024 1018 rmap.connect('files_authors_home',
1025 1019 '/{repo_name}/authors/{revision}/{f_path}',
1026 1020 controller='files', action='authors', revision='tip', f_path='',
1027 1021 conditions={'function': check_repo},
1028 1022 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1029 1023
1030 1024 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1031 1025 controller='files', action='diff', f_path='',
1032 1026 conditions={'function': check_repo},
1033 1027 requirements=URL_NAME_REQUIREMENTS)
1034 1028
1035 1029 rmap.connect('files_diff_2way_home',
1036 1030 '/{repo_name}/diff-2way/{f_path}',
1037 1031 controller='files', action='diff_2way', f_path='',
1038 1032 conditions={'function': check_repo},
1039 1033 requirements=URL_NAME_REQUIREMENTS)
1040 1034
1041 1035 rmap.connect('files_rawfile_home',
1042 1036 '/{repo_name}/rawfile/{revision}/{f_path}',
1043 1037 controller='files', action='rawfile', revision='tip',
1044 1038 f_path='', conditions={'function': check_repo},
1045 1039 requirements=URL_NAME_REQUIREMENTS)
1046 1040
1047 1041 rmap.connect('files_raw_home',
1048 1042 '/{repo_name}/raw/{revision}/{f_path}',
1049 1043 controller='files', action='raw', revision='tip', f_path='',
1050 1044 conditions={'function': check_repo},
1051 1045 requirements=URL_NAME_REQUIREMENTS)
1052 1046
1053 1047 rmap.connect('files_render_home',
1054 1048 '/{repo_name}/render/{revision}/{f_path}',
1055 1049 controller='files', action='index', revision='tip', f_path='',
1056 1050 rendered=True, conditions={'function': check_repo},
1057 1051 requirements=URL_NAME_REQUIREMENTS)
1058 1052
1059 1053 rmap.connect('files_annotate_home',
1060 1054 '/{repo_name}/annotate/{revision}/{f_path}',
1061 1055 controller='files', action='index', revision='tip',
1062 1056 f_path='', annotate=True, conditions={'function': check_repo},
1063 1057 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1064 1058
1065 1059 rmap.connect('files_annotate_previous',
1066 1060 '/{repo_name}/annotate-previous/{revision}/{f_path}',
1067 1061 controller='files', action='annotate_previous', revision='tip',
1068 1062 f_path='', annotate=True, conditions={'function': check_repo},
1069 1063 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1070 1064
1071 1065 rmap.connect('files_edit',
1072 1066 '/{repo_name}/edit/{revision}/{f_path}',
1073 1067 controller='files', action='edit', revision='tip',
1074 1068 f_path='',
1075 1069 conditions={'function': check_repo, 'method': ['POST']},
1076 1070 requirements=URL_NAME_REQUIREMENTS)
1077 1071
1078 1072 rmap.connect('files_edit_home',
1079 1073 '/{repo_name}/edit/{revision}/{f_path}',
1080 1074 controller='files', action='edit_home', revision='tip',
1081 1075 f_path='', conditions={'function': check_repo},
1082 1076 requirements=URL_NAME_REQUIREMENTS)
1083 1077
1084 1078 rmap.connect('files_add',
1085 1079 '/{repo_name}/add/{revision}/{f_path}',
1086 1080 controller='files', action='add', revision='tip',
1087 1081 f_path='',
1088 1082 conditions={'function': check_repo, 'method': ['POST']},
1089 1083 requirements=URL_NAME_REQUIREMENTS)
1090 1084
1091 1085 rmap.connect('files_add_home',
1092 1086 '/{repo_name}/add/{revision}/{f_path}',
1093 1087 controller='files', action='add_home', revision='tip',
1094 1088 f_path='', conditions={'function': check_repo},
1095 1089 requirements=URL_NAME_REQUIREMENTS)
1096 1090
1097 1091 rmap.connect('files_delete',
1098 1092 '/{repo_name}/delete/{revision}/{f_path}',
1099 1093 controller='files', action='delete', revision='tip',
1100 1094 f_path='',
1101 1095 conditions={'function': check_repo, 'method': ['POST']},
1102 1096 requirements=URL_NAME_REQUIREMENTS)
1103 1097
1104 1098 rmap.connect('files_delete_home',
1105 1099 '/{repo_name}/delete/{revision}/{f_path}',
1106 1100 controller='files', action='delete_home', revision='tip',
1107 1101 f_path='', conditions={'function': check_repo},
1108 1102 requirements=URL_NAME_REQUIREMENTS)
1109 1103
1110 1104 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1111 1105 controller='files', action='archivefile',
1112 1106 conditions={'function': check_repo},
1113 1107 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1114 1108
1115 1109 rmap.connect('files_nodelist_home',
1116 1110 '/{repo_name}/nodelist/{revision}/{f_path}',
1117 1111 controller='files', action='nodelist',
1118 1112 conditions={'function': check_repo},
1119 1113 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1120 1114
1121 1115 rmap.connect('files_nodetree_full',
1122 1116 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1123 1117 controller='files', action='nodetree_full',
1124 1118 conditions={'function': check_repo},
1125 1119 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1126 1120
1127 1121 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1128 1122 controller='forks', action='fork_create',
1129 1123 conditions={'function': check_repo, 'method': ['POST']},
1130 1124 requirements=URL_NAME_REQUIREMENTS)
1131 1125
1132 1126 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1133 1127 controller='forks', action='fork',
1134 1128 conditions={'function': check_repo},
1135 1129 requirements=URL_NAME_REQUIREMENTS)
1136 1130
1137 1131 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1138 1132 controller='forks', action='forks',
1139 1133 conditions={'function': check_repo},
1140 1134 requirements=URL_NAME_REQUIREMENTS)
1141 1135
1142 1136 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1143 1137 controller='followers', action='followers',
1144 1138 conditions={'function': check_repo},
1145 1139 requirements=URL_NAME_REQUIREMENTS)
1146 1140
1147 1141 # must be here for proper group/repo catching pattern
1148 1142 _connect_with_slash(
1149 1143 rmap, 'repo_group_home', '/{group_name}',
1150 1144 controller='home', action='index_repo_group',
1151 1145 conditions={'function': check_group},
1152 1146 requirements=URL_NAME_REQUIREMENTS)
1153 1147
1154 1148 # catch all, at the end
1155 1149 _connect_with_slash(
1156 1150 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1157 1151 controller='summary', action='index',
1158 1152 conditions={'function': check_repo},
1159 1153 requirements=URL_NAME_REQUIREMENTS)
1160 1154
1161 1155 return rmap
1162 1156
1163 1157
1164 1158 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1165 1159 """
1166 1160 Connect a route with an optional trailing slash in `path`.
1167 1161 """
1168 1162 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1169 1163 mapper.connect(name, path, *args, **kwargs)
@@ -1,461 +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 from webob.exc import HTTPBadGateway
37 36
38 37 from rhodecode import forms
39 38 from rhodecode.lib import helpers as h
40 39 from rhodecode.lib import auth
41 40 from rhodecode.lib.auth import (
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
41 LoginRequired, NotAnonymous, AuthUser)
43 42 from rhodecode.lib.base import BaseController, render
44 43 from rhodecode.lib.utils import jsonify
45 44 from rhodecode.lib.utils2 import safe_int, md5, str2bool
46 45 from rhodecode.lib.ext_json import json
47 46 from rhodecode.lib.channelstream import channelstream_request, \
48 47 ChannelstreamException
49 48
50 49 from rhodecode.model.validation_schema.schemas import user_schema
51 50 from rhodecode.model.db import (
52 51 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 52 from rhodecode.model.forms import UserForm
54 53 from rhodecode.model.scm import RepoList
55 54 from rhodecode.model.user import UserModel
56 55 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.auth_token import AuthTokenModel
58 56 from rhodecode.model.meta import Session
59 57 from rhodecode.model.pull_request import PullRequestModel
60 58 from rhodecode.model.comment import CommentsModel
61 59
62 60 log = logging.getLogger(__name__)
63 61
64 62
65 63 class MyAccountController(BaseController):
66 64 """REST Controller styled on the Atom Publishing Protocol"""
67 65 # To properly map this controller, ensure your config/routing.py
68 66 # file has a resource setup:
69 67 # map.resource('setting', 'settings', controller='admin/settings',
70 68 # path_prefix='/admin', name_prefix='admin_')
71 69
72 70 @LoginRequired()
73 71 @NotAnonymous()
74 72 def __before__(self):
75 73 super(MyAccountController, self).__before__()
76 74
77 75 def __load_data(self):
78 76 c.user = User.get(c.rhodecode_user.user_id)
79 77 if c.user.username == User.DEFAULT_USER:
80 78 h.flash(_("You can't edit this user since it's"
81 79 " crucial for entire application"), category='warning')
82 80 return redirect(url('users'))
83 81
84 82 c.auth_user = AuthUser(
85 83 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
86 84
87 85 def _load_my_repos_data(self, watched=False):
88 86 if watched:
89 87 admin = False
90 88 follows_repos = Session().query(UserFollowing)\
91 89 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
92 90 .options(joinedload(UserFollowing.follows_repository))\
93 91 .all()
94 92 repo_list = [x.follows_repository for x in follows_repos]
95 93 else:
96 94 admin = True
97 95 repo_list = Repository.get_all_repos(
98 96 user_id=c.rhodecode_user.user_id)
99 97 repo_list = RepoList(repo_list, perm_set=[
100 98 'repository.read', 'repository.write', 'repository.admin'])
101 99
102 100 repos_data = RepoModel().get_repos_as_dict(
103 101 repo_list=repo_list, admin=admin)
104 102 # json used to render the grid
105 103 return json.dumps(repos_data)
106 104
107 105 @auth.CSRFRequired()
108 106 def my_account_update(self):
109 107 """
110 108 POST /_admin/my_account Updates info of my account
111 109 """
112 110 # url('my_account')
113 111 c.active = 'profile_edit'
114 112 self.__load_data()
115 113 c.perm_user = c.auth_user
116 114 c.extern_type = c.user.extern_type
117 115 c.extern_name = c.user.extern_name
118 116
119 117 defaults = c.user.get_dict()
120 118 update = False
121 119 _form = UserForm(edit=True,
122 120 old_data={'user_id': c.rhodecode_user.user_id,
123 121 'email': c.rhodecode_user.email})()
124 122 form_result = {}
125 123 try:
126 124 post_data = dict(request.POST)
127 125 post_data['new_password'] = ''
128 126 post_data['password_confirmation'] = ''
129 127 form_result = _form.to_python(post_data)
130 128 # skip updating those attrs for my account
131 129 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
132 130 'new_password', 'password_confirmation']
133 131 # TODO: plugin should define if username can be updated
134 132 if c.extern_type != "rhodecode":
135 133 # forbid updating username for external accounts
136 134 skip_attrs.append('username')
137 135
138 136 UserModel().update_user(
139 137 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
140 138 h.flash(_('Your account was updated successfully'),
141 139 category='success')
142 140 Session().commit()
143 141 update = True
144 142
145 143 except formencode.Invalid as errors:
146 144 return htmlfill.render(
147 145 render('admin/my_account/my_account.mako'),
148 146 defaults=errors.value,
149 147 errors=errors.error_dict or {},
150 148 prefix_error=False,
151 149 encoding="UTF-8",
152 150 force_defaults=False)
153 151 except Exception:
154 152 log.exception("Exception updating user")
155 153 h.flash(_('Error occurred during update of user %s')
156 154 % form_result.get('username'), category='error')
157 155
158 156 if update:
159 157 return redirect('my_account')
160 158
161 159 return htmlfill.render(
162 160 render('admin/my_account/my_account.mako'),
163 161 defaults=defaults,
164 162 encoding="UTF-8",
165 163 force_defaults=False
166 164 )
167 165
168 166 def my_account(self):
169 167 """
170 168 GET /_admin/my_account Displays info about my account
171 169 """
172 170 # url('my_account')
173 171 c.active = 'profile'
174 172 self.__load_data()
175 173
176 174 defaults = c.user.get_dict()
177 175 return htmlfill.render(
178 176 render('admin/my_account/my_account.mako'),
179 177 defaults=defaults, encoding="UTF-8", force_defaults=False)
180 178
181 179 def my_account_edit(self):
182 180 """
183 181 GET /_admin/my_account/edit Displays edit form of my account
184 182 """
185 183 c.active = 'profile_edit'
186 184 self.__load_data()
187 185 c.perm_user = c.auth_user
188 186 c.extern_type = c.user.extern_type
189 187 c.extern_name = c.user.extern_name
190 188
191 189 defaults = c.user.get_dict()
192 190 return htmlfill.render(
193 191 render('admin/my_account/my_account.mako'),
194 192 defaults=defaults,
195 193 encoding="UTF-8",
196 194 force_defaults=False
197 195 )
198 196
199 197 @auth.CSRFRequired(except_methods=['GET'])
200 198 def my_account_password(self):
201 199 c.active = 'password'
202 200 self.__load_data()
203 201 c.extern_type = c.user.extern_type
204 202
205 203 schema = user_schema.ChangePasswordSchema().bind(
206 204 username=c.rhodecode_user.username)
207 205
208 206 form = forms.Form(schema,
209 207 buttons=(forms.buttons.save, forms.buttons.reset))
210 208
211 209 if request.method == 'POST' and c.extern_type == 'rhodecode':
212 210 controls = request.POST.items()
213 211 try:
214 212 valid_data = form.validate(controls)
215 213 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
216 214 instance = c.rhodecode_user.get_instance()
217 215 instance.update_userdata(force_password_change=False)
218 216 Session().commit()
219 217 except forms.ValidationFailure as e:
220 218 request.session.flash(
221 219 _('Error occurred during update of user password'),
222 220 queue='error')
223 221 form = e
224 222 except Exception:
225 223 log.exception("Exception updating password")
226 224 request.session.flash(
227 225 _('Error occurred during update of user password'),
228 226 queue='error')
229 227 else:
230 228 session.setdefault('rhodecode_user', {}).update(
231 229 {'password': md5(instance.password)})
232 230 session.save()
233 231 request.session.flash(
234 232 _("Successfully updated password"), queue='success')
235 233 return redirect(url('my_account_password'))
236 234
237 235 c.form = form
238 236 return render('admin/my_account/my_account.mako')
239 237
240 238 def my_account_repos(self):
241 239 c.active = 'repos'
242 240 self.__load_data()
243 241
244 242 # json used to render the grid
245 243 c.data = self._load_my_repos_data()
246 244 return render('admin/my_account/my_account.mako')
247 245
248 246 def my_account_watched(self):
249 247 c.active = 'watched'
250 248 self.__load_data()
251 249
252 250 # json used to render the grid
253 251 c.data = self._load_my_repos_data(watched=True)
254 252 return render('admin/my_account/my_account.mako')
255 253
256 254 def my_account_perms(self):
257 255 c.active = 'perms'
258 256 self.__load_data()
259 257 c.perm_user = c.auth_user
260 258
261 259 return render('admin/my_account/my_account.mako')
262 260
263 261 def my_account_emails(self):
264 262 c.active = 'emails'
265 263 self.__load_data()
266 264
267 265 c.user_email_map = UserEmailMap.query()\
268 266 .filter(UserEmailMap.user == c.user).all()
269 267 return render('admin/my_account/my_account.mako')
270 268
271 269 @auth.CSRFRequired()
272 270 def my_account_emails_add(self):
273 271 email = request.POST.get('new_email')
274 272
275 273 try:
276 274 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
277 275 Session().commit()
278 276 h.flash(_("Added new email address `%s` for user account") % email,
279 277 category='success')
280 278 except formencode.Invalid as error:
281 279 msg = error.error_dict['email']
282 280 h.flash(msg, category='error')
283 281 except Exception:
284 282 log.exception("Exception in my_account_emails")
285 283 h.flash(_('An error occurred during email saving'),
286 284 category='error')
287 285 return redirect(url('my_account_emails'))
288 286
289 287 @auth.CSRFRequired()
290 288 def my_account_emails_delete(self):
291 289 email_id = request.POST.get('del_email_id')
292 290 user_model = UserModel()
293 291 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
294 292 Session().commit()
295 293 h.flash(_("Removed email address from user account"),
296 294 category='success')
297 295 return redirect(url('my_account_emails'))
298 296
299 297 def _extract_ordering(self, request):
300 298 column_index = safe_int(request.GET.get('order[0][column]'))
301 299 order_dir = request.GET.get('order[0][dir]', 'desc')
302 300 order_by = request.GET.get(
303 301 'columns[%s][data][sort]' % column_index, 'name_raw')
304 302 return order_by, order_dir
305 303
306 304 def _get_pull_requests_list(self, statuses):
307 305 start = safe_int(request.GET.get('start'), 0)
308 306 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
309 307 order_by, order_dir = self._extract_ordering(request)
310 308
311 309 pull_requests = PullRequestModel().get_im_participating_in(
312 310 user_id=c.rhodecode_user.user_id,
313 311 statuses=statuses,
314 312 offset=start, length=length, order_by=order_by,
315 313 order_dir=order_dir)
316 314
317 315 pull_requests_total_count = PullRequestModel().count_im_participating_in(
318 316 user_id=c.rhodecode_user.user_id, statuses=statuses)
319 317
320 318 from rhodecode.lib.utils import PartialRenderer
321 319 _render = PartialRenderer('data_table/_dt_elements.mako')
322 320 data = []
323 321 for pr in pull_requests:
324 322 repo_id = pr.target_repo_id
325 323 comments = CommentsModel().get_all_comments(
326 324 repo_id, pull_request=pr)
327 325 owned = pr.user_id == c.rhodecode_user.user_id
328 326 status = pr.calculated_review_status()
329 327
330 328 data.append({
331 329 'target_repo': _render('pullrequest_target_repo',
332 330 pr.target_repo.repo_name),
333 331 'name': _render('pullrequest_name',
334 332 pr.pull_request_id, pr.target_repo.repo_name,
335 333 short=True),
336 334 'name_raw': pr.pull_request_id,
337 335 'status': _render('pullrequest_status', status),
338 336 'title': _render(
339 337 'pullrequest_title', pr.title, pr.description),
340 338 'description': h.escape(pr.description),
341 339 'updated_on': _render('pullrequest_updated_on',
342 340 h.datetime_to_time(pr.updated_on)),
343 341 'updated_on_raw': h.datetime_to_time(pr.updated_on),
344 342 'created_on': _render('pullrequest_updated_on',
345 343 h.datetime_to_time(pr.created_on)),
346 344 'created_on_raw': h.datetime_to_time(pr.created_on),
347 345 'author': _render('pullrequest_author',
348 346 pr.author.full_contact, ),
349 347 'author_raw': pr.author.full_name,
350 348 'comments': _render('pullrequest_comments', len(comments)),
351 349 'comments_raw': len(comments),
352 350 'closed': pr.is_closed(),
353 351 'owned': owned
354 352 })
355 353 # json used to render the grid
356 354 data = ({
357 355 'data': data,
358 356 'recordsTotal': pull_requests_total_count,
359 357 'recordsFiltered': pull_requests_total_count,
360 358 })
361 359 return data
362 360
363 361 def my_account_pullrequests(self):
364 362 c.active = 'pullrequests'
365 363 self.__load_data()
366 364 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
367 365
368 366 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
369 367 if c.show_closed:
370 368 statuses += [PullRequest.STATUS_CLOSED]
371 369 data = self._get_pull_requests_list(statuses)
372 370 if not request.is_xhr:
373 371 c.data_participate = json.dumps(data['data'])
374 372 c.records_total_participate = data['recordsTotal']
375 373 return render('admin/my_account/my_account.mako')
376 374 else:
377 375 return json.dumps(data)
378 376
379 def my_account_auth_tokens(self):
380 c.active = 'auth_tokens'
381 self.__load_data()
382 show_expired = True
383 c.lifetime_values = [
384 (str(-1), _('forever')),
385 (str(5), _('5 minutes')),
386 (str(60), _('1 hour')),
387 (str(60 * 24), _('1 day')),
388 (str(60 * 24 * 30), _('1 month')),
389 ]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
392 for x in AuthTokenModel.cls.ROLES]
393 c.role_options = [(c.role_values, _("Role"))]
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
395 c.rhodecode_user.user_id, show_expired=show_expired)
396 return render('admin/my_account/my_account.mako')
397
398 @auth.CSRFRequired()
399 def my_account_auth_tokens_add(self):
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
401 description = request.POST.get('description')
402 role = request.POST.get('role')
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
404 role)
405 Session().commit()
406 h.flash(_("Auth token successfully created"), category='success')
407 return redirect(url('my_account_auth_tokens'))
408
409 @auth.CSRFRequired()
410 def my_account_auth_tokens_delete(self):
411 del_auth_token = request.POST.get('del_auth_token')
412
413 if del_auth_token:
414 AuthTokenModel().delete(del_auth_token, c.rhodecode_user.user_id)
415 Session().commit()
416 h.flash(_("Auth token successfully deleted"), category='success')
417
418 return redirect(url('my_account_auth_tokens'))
419
420 377 def my_notifications(self):
421 378 c.active = 'notifications'
422 379 return render('admin/my_account/my_account.mako')
423 380
424 381 @auth.CSRFRequired()
425 382 @jsonify
426 383 def my_notifications_toggle_visibility(self):
427 384 user = c.rhodecode_user.get_instance()
428 385 new_status = not user.user_data.get('notification_status', True)
429 386 user.update_userdata(notification_status=new_status)
430 387 Session().commit()
431 388 return user.user_data['notification_status']
432 389
433 390 @auth.CSRFRequired()
434 391 @jsonify
435 392 def my_account_notifications_test_channelstream(self):
436 393 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
437 394 c.rhodecode_user.username, datetime.datetime.now())
438 395 payload = {
439 396 'type': 'message',
440 397 'timestamp': datetime.datetime.utcnow(),
441 398 'user': 'system',
442 399 #'channel': 'broadcast',
443 400 'pm_users': [c.rhodecode_user.username],
444 401 'message': {
445 402 'message': message,
446 403 'level': 'info',
447 404 'topic': '/notifications'
448 405 }
449 406 }
450 407
451 408 registry = get_current_registry()
452 409 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
453 410 channelstream_config = rhodecode_plugins.get('channelstream', {})
454 411
455 412 try:
456 413 channelstream_request(channelstream_config, [payload], '/message')
457 414 except ChannelstreamException as e:
458 415 log.exception('Failed to send channelstream data')
459 416 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
460 417 return {"response": 'Channelstream data sent. '
461 418 'You should see a new live message now.'}
@@ -1,52 +1,52 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('My account')} ${c.rhodecode_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 ${_('My Account')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='my_account')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.breadcrumbs()}
23 23 </div>
24 24
25 25 <div class="sidebar-col-wrapper scw-small">
26 26 ##main
27 27 <div class="sidebar">
28 28 <ul class="nav nav-pills nav-stacked">
29 29 <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('Profile')}</a></li>
30 30 <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li>
31 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
31 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
32 32 ## TODO: Find a better integration of oauth views into navigation.
33 33 <% my_account_oauth_url = h.route_path_or_none('my_account_oauth') %>
34 34 % if my_account_oauth_url:
35 35 <li class="${'active' if c.active=='oauth' else ''}"><a href="${my_account_oauth_url}">${_('OAuth Identities')}</a></li>
36 36 % endif
37 37 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('Emails')}</a></li>
38 38 <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('Repositories')}</a></li>
39 39 <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li>
40 40 <li class="${'active' if c.active=='pullrequests' else ''}"><a href="${h.url('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
41 41 <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('my_account_perms')}">${_('Permissions')}</a></li>
42 42 <li class="${'active' if c.active=='my_notifications' else ''}"><a href="${h.url('my_account_notifications')}">${_('Live Notifications')}</a></li>
43 43 </ul>
44 44 </div>
45 45
46 46 <div class="main-content-full-width">
47 47 <%include file="/admin/my_account/my_account_${c.active}.mako"/>
48 48 </div>
49 49 </div>
50 50 </div>
51 51
52 52 </%def>
@@ -1,95 +1,95 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <p>
7 7 ${_('Each token can have a role. Token with a role can be used only in given context, '
8 8 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
9 9 ${_('Additionally scope for VCS type token can narrow the use to chosen repository.')}
10 10 </p>
11 11 <table class="rctable auth_tokens">
12 12 %if c.user_auth_tokens:
13 13 <tr>
14 14 <th>${_('Token')}</th>
15 15 <th>${_('Scope')}</th>
16 16 <th>${_('Description')}</th>
17 17 <th>${_('Role')}</th>
18 18 <th>${_('Expiration')}</th>
19 19 <th>${_('Action')}</th>
20 20 </tr>
21 21 %for auth_token in c.user_auth_tokens:
22 22 <tr class="${'expired' if auth_token.expired else ''}">
23 23 <td class="truncate-wrap td-authtoken">
24 24 <div class="user_auth_tokens truncate autoexpand">
25 25 <code>${auth_token.api_key}</code>
26 26 </div>
27 27 </td>
28 28 <td class="td">${auth_token.scope_humanized}</td>
29 29 <td class="td-wrap">${auth_token.description}</td>
30 30 <td class="td-tags">
31 31 <span class="tag disabled">${auth_token.role_humanized}</span>
32 32 </td>
33 33 <td class="td-exp">
34 34 %if auth_token.expires == -1:
35 35 ${_('never')}
36 36 %else:
37 37 %if auth_token.expired:
38 38 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
39 39 %else:
40 40 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
41 41 %endif
42 42 %endif
43 43 </td>
44 44 <td class="td-action">
45 ${h.secure_form(url('my_account_auth_tokens'),method='delete')}
45 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), method='post')}
46 46 ${h.hidden('del_auth_token',auth_token.api_key)}
47 47 <button class="btn btn-link btn-danger" type="submit"
48 48 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.api_key}');">
49 49 ${_('Delete')}
50 50 </button>
51 51 ${h.end_form()}
52 52 </td>
53 53 </tr>
54 54 %endfor
55 55 %else:
56 56 <tr><td><div class="ip">${_('No additional auth token specified')}</div></td></tr>
57 57 %endif
58 58 </table>
59 59
60 60 <div class="user_auth_tokens">
61 ${h.secure_form(url('my_account_auth_tokens'), method='post')}
61 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), method='post')}
62 62 <div class="form form-vertical">
63 63 <!-- fields -->
64 64 <div class="fields">
65 65 <div class="field">
66 66 <div class="label">
67 67 <label for="new_email">${_('New authentication token')}:</label>
68 68 </div>
69 69 <div class="input">
70 70 ${h.text('description', placeholder=_('Description'))}
71 71 ${h.select('lifetime', '', c.lifetime_options)}
72 72 ${h.select('role', '', c.role_options)}
73 73 </div>
74 74 </div>
75 75 <div class="buttons">
76 76 ${h.submit('save',_('Add'),class_="btn")}
77 77 ${h.reset('reset',_('Reset'),class_="btn")}
78 78 </div>
79 79 </div>
80 80 </div>
81 81 ${h.end_form()}
82 82 </div>
83 83 </div>
84 84 </div>
85 85 <script>
86 86 $(document).ready(function(){
87 87 var select2Options = {
88 88 'containerCssClass': "drop-menu",
89 89 'dropdownCssClass': "drop-menu-dropdown",
90 90 'dropdownAutoWidth': true
91 91 };
92 92 $("#lifetime").select2(select2Options);
93 93 $("#role").select2(select2Options);
94 94 });
95 95 </script>
@@ -1,383 +1,326 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.lib import helpers as h
24 24 from rhodecode.lib.auth import check_password
25 25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
29 29 assert_session_flash)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 class TestMyAccountController(TestController):
37 37 test_user_1 = 'testme'
38 38 test_user_1_password = '0jd83nHNS/d23n'
39 39 destroy_users = set()
40 40
41 41 @classmethod
42 42 def teardown_class(cls):
43 43 fixture.destroy_users(cls.destroy_users)
44 44
45 45 def test_my_account(self):
46 46 self.log_user()
47 47 response = self.app.get(url('my_account'))
48 48
49 49 response.mustcontain('test_admin')
50 50 response.mustcontain('href="/_admin/my_account/edit"')
51 51
52 52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
53 53 response = self.app.get(url('my_account'))
54 54 assert_response = AssertResponse(response)
55 55 element = assert_response.get_element('.logout #csrf_token')
56 56 assert element.value == csrf_token
57 57
58 58 def test_my_account_edit(self):
59 59 self.log_user()
60 60 response = self.app.get(url('my_account_edit'))
61 61
62 62 response.mustcontain('value="test_admin')
63 63
64 64 def test_my_account_my_repos(self):
65 65 self.log_user()
66 66 response = self.app.get(url('my_account_repos'))
67 67 repos = Repository.query().filter(
68 68 Repository.user == User.get_by_username(
69 69 TEST_USER_ADMIN_LOGIN)).all()
70 70 for repo in repos:
71 71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
72 72
73 73 def test_my_account_my_watched(self):
74 74 self.log_user()
75 75 response = self.app.get(url('my_account_watched'))
76 76
77 77 repos = UserFollowing.query().filter(
78 78 UserFollowing.user == User.get_by_username(
79 79 TEST_USER_ADMIN_LOGIN)).all()
80 80 for repo in repos:
81 81 response.mustcontain(
82 82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
83 83
84 84 @pytest.mark.backends("git", "hg")
85 85 def test_my_account_my_pullrequests(self, pr_util):
86 86 self.log_user()
87 87 response = self.app.get(url('my_account_pullrequests'))
88 88 response.mustcontain('There are currently no open pull '
89 89 'requests requiring your participation.')
90 90
91 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 92 response = self.app.get(url('my_account_pullrequests'))
93 93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
94 94 response.mustcontain('TestMyAccountPR')
95 95
96 96 def test_my_account_my_emails(self):
97 97 self.log_user()
98 98 response = self.app.get(url('my_account_emails'))
99 99 response.mustcontain('No additional emails specified')
100 100
101 101 def test_my_account_my_emails_add_existing_email(self):
102 102 self.log_user()
103 103 response = self.app.get(url('my_account_emails'))
104 104 response.mustcontain('No additional emails specified')
105 105 response = self.app.post(url('my_account_emails'),
106 106 {'new_email': TEST_USER_REGULAR_EMAIL,
107 107 'csrf_token': self.csrf_token})
108 108 assert_session_flash(response, 'This e-mail address is already taken')
109 109
110 110 def test_my_account_my_emails_add_mising_email_in_form(self):
111 111 self.log_user()
112 112 response = self.app.get(url('my_account_emails'))
113 113 response.mustcontain('No additional emails specified')
114 114 response = self.app.post(url('my_account_emails'),
115 115 {'csrf_token': self.csrf_token})
116 116 assert_session_flash(response, 'Please enter an email address')
117 117
118 118 def test_my_account_my_emails_add_remove(self):
119 119 self.log_user()
120 120 response = self.app.get(url('my_account_emails'))
121 121 response.mustcontain('No additional emails specified')
122 122
123 123 response = self.app.post(url('my_account_emails'),
124 124 {'new_email': 'foo@barz.com',
125 125 'csrf_token': self.csrf_token})
126 126
127 127 response = self.app.get(url('my_account_emails'))
128 128
129 129 from rhodecode.model.db import UserEmailMap
130 130 email_id = UserEmailMap.query().filter(
131 131 UserEmailMap.user == User.get_by_username(
132 132 TEST_USER_ADMIN_LOGIN)).filter(
133 133 UserEmailMap.email == 'foo@barz.com').one().email_id
134 134
135 135 response.mustcontain('foo@barz.com')
136 136 response.mustcontain('<input id="del_email_id" name="del_email_id" '
137 137 'type="hidden" value="%s" />' % email_id)
138 138
139 139 response = self.app.post(
140 140 url('my_account_emails'), {
141 141 'del_email_id': email_id, '_method': 'delete',
142 142 'csrf_token': self.csrf_token})
143 143 assert_session_flash(response, 'Removed email address from user account')
144 144 response = self.app.get(url('my_account_emails'))
145 145 response.mustcontain('No additional emails specified')
146 146
147 147 @pytest.mark.parametrize(
148 148 "name, attrs", [
149 149 ('firstname', {'firstname': 'new_username'}),
150 150 ('lastname', {'lastname': 'new_username'}),
151 151 ('admin', {'admin': True}),
152 152 ('admin', {'admin': False}),
153 153 ('extern_type', {'extern_type': 'ldap'}),
154 154 ('extern_type', {'extern_type': None}),
155 155 # ('extern_name', {'extern_name': 'test'}),
156 156 # ('extern_name', {'extern_name': None}),
157 157 ('active', {'active': False}),
158 158 ('active', {'active': True}),
159 159 ('email', {'email': 'some@email.com'}),
160 160 ])
161 161 def test_my_account_update(self, name, attrs):
162 162 usr = fixture.create_user(self.test_user_1,
163 163 password=self.test_user_1_password,
164 164 email='testme@rhodecode.org',
165 165 extern_type='rhodecode',
166 166 extern_name=self.test_user_1,
167 167 skip_if_exists=True)
168 168 self.destroy_users.add(self.test_user_1)
169 169
170 170 params = usr.get_api_data() # current user data
171 171 user_id = usr.user_id
172 172 self.log_user(
173 173 username=self.test_user_1, password=self.test_user_1_password)
174 174
175 175 params.update({'password_confirmation': ''})
176 176 params.update({'new_password': ''})
177 177 params.update({'extern_type': 'rhodecode'})
178 178 params.update({'extern_name': self.test_user_1})
179 179 params.update({'csrf_token': self.csrf_token})
180 180
181 181 params.update(attrs)
182 182 # my account page cannot set language param yet, only for admins
183 183 del params['language']
184 184 response = self.app.post(url('my_account'), params)
185 185
186 186 assert_session_flash(
187 187 response, 'Your account was updated successfully')
188 188
189 189 del params['csrf_token']
190 190
191 191 updated_user = User.get_by_username(self.test_user_1)
192 192 updated_params = updated_user.get_api_data()
193 193 updated_params.update({'password_confirmation': ''})
194 194 updated_params.update({'new_password': ''})
195 195
196 196 params['last_login'] = updated_params['last_login']
197 197 # my account page cannot set language param yet, only for admins
198 198 # but we get this info from API anyway
199 199 params['language'] = updated_params['language']
200 200
201 201 if name == 'email':
202 202 params['emails'] = [attrs['email']]
203 203 if name == 'extern_type':
204 204 # cannot update this via form, expected value is original one
205 205 params['extern_type'] = "rhodecode"
206 206 if name == 'extern_name':
207 207 # cannot update this via form, expected value is original one
208 208 params['extern_name'] = str(user_id)
209 209 if name == 'active':
210 210 # my account cannot deactivate account
211 211 params['active'] = True
212 212 if name == 'admin':
213 213 # my account cannot make you an admin !
214 214 params['admin'] = False
215 215
216 216 assert params == updated_params
217 217
218 218 def test_my_account_update_err_email_exists(self):
219 219 self.log_user()
220 220
221 221 new_email = 'test_regular@mail.com' # already exisitn email
222 222 response = self.app.post(url('my_account'),
223 223 params={
224 224 'username': 'test_admin',
225 225 'new_password': 'test12',
226 226 'password_confirmation': 'test122',
227 227 'firstname': 'NewName',
228 228 'lastname': 'NewLastname',
229 229 'email': new_email,
230 230 'csrf_token': self.csrf_token,
231 231 })
232 232
233 233 response.mustcontain('This e-mail address is already taken')
234 234
235 235 def test_my_account_update_err(self):
236 236 self.log_user('test_regular2', 'test12')
237 237
238 238 new_email = 'newmail.pl'
239 239 response = self.app.post(url('my_account'),
240 240 params={
241 241 'username': 'test_admin',
242 242 'new_password': 'test12',
243 243 'password_confirmation': 'test122',
244 244 'firstname': 'NewName',
245 245 'lastname': 'NewLastname',
246 246 'email': new_email,
247 247 'csrf_token': self.csrf_token,
248 248 })
249 249
250 250 response.mustcontain('An email address must contain a single @')
251 251 from rhodecode.model import validators
252 252 msg = validators.ValidUsername(
253 253 edit=False, old_data={})._messages['username_exists']
254 254 msg = h.html_escape(msg % {'username': 'test_admin'})
255 255 response.mustcontain(u"%s" % msg)
256 256
257 def test_my_account_auth_tokens(self):
258 usr = self.log_user('test_regular2', 'test12')
259 user = User.get(usr['user_id'])
260 response = self.app.get(url('my_account_auth_tokens'))
261 for token in user.auth_tokens:
262 response.mustcontain(token)
263 response.mustcontain('never')
264
265 @pytest.mark.parametrize("desc, lifetime", [
266 ('forever', -1),
267 ('5mins', 60*5),
268 ('30days', 60*60*24*30),
269 ])
270 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
271 user = user_util.create_user(password='qweqwe')
272 user_id = user.user_id
273 self.log_user(user.username, 'qweqwe')
274
275 response = self.app.post(url('my_account_auth_tokens'),
276 {'description': desc, 'lifetime': lifetime,
277 'csrf_token': self.csrf_token})
278 assert_session_flash(response, 'Auth token successfully created')
279
280 response = response.follow()
281 user = User.get(user_id)
282 for auth_token in user.auth_tokens:
283 response.mustcontain(auth_token)
284
285 def test_my_account_remove_auth_token(self, user_util):
286 user = user_util.create_user(password='qweqwe')
287 user_id = user.user_id
288 self.log_user(user.username, 'qweqwe')
289
290 user = User.get(user_id)
291 keys = user.extra_auth_tokens
292 assert 2 == len(keys)
293
294 response = self.app.post(url('my_account_auth_tokens'),
295 {'description': 'desc', 'lifetime': -1,
296 'csrf_token': self.csrf_token})
297 assert_session_flash(response, 'Auth token successfully created')
298 response.follow()
299
300 user = User.get(user_id)
301 keys = user.extra_auth_tokens
302 assert 3 == len(keys)
303
304 response = self.app.post(
305 url('my_account_auth_tokens'),
306 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
307 'csrf_token': self.csrf_token})
308 assert_session_flash(response, 'Auth token successfully deleted')
309
310 user = User.get(user_id)
311 keys = user.extra_auth_tokens
312 assert 2 == len(keys)
313
314 257 def test_valid_change_password(self, user_util):
315 258 new_password = 'my_new_valid_password'
316 259 user = user_util.create_user(password=self.test_user_1_password)
317 260 session = self.log_user(user.username, self.test_user_1_password)
318 261 form_data = [
319 262 ('current_password', self.test_user_1_password),
320 263 ('__start__', 'new_password:mapping'),
321 264 ('new_password', new_password),
322 265 ('new_password-confirm', new_password),
323 266 ('__end__', 'new_password:mapping'),
324 267 ('csrf_token', self.csrf_token),
325 268 ]
326 269 response = self.app.post(url('my_account_password'), form_data).follow()
327 270 assert 'Successfully updated password' in response
328 271
329 272 # check_password depends on user being in session
330 273 Session().add(user)
331 274 try:
332 275 assert check_password(new_password, user.password)
333 276 finally:
334 277 Session().expunge(user)
335 278
336 279 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
337 280 ('', 'abcdef123', 'abcdef123'),
338 281 ('wrong_pw', 'abcdef123', 'abcdef123'),
339 282 (test_user_1_password, test_user_1_password, test_user_1_password),
340 283 (test_user_1_password, '', ''),
341 284 (test_user_1_password, 'abcdef123', ''),
342 285 (test_user_1_password, '', 'abcdef123'),
343 286 (test_user_1_password, 'not_the', 'same_pw'),
344 287 (test_user_1_password, 'short', 'short'),
345 288 ])
346 289 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
347 290 user_util):
348 291 user = user_util.create_user(password=self.test_user_1_password)
349 292 session = self.log_user(user.username, self.test_user_1_password)
350 293 old_password_hash = session['password']
351 294 form_data = [
352 295 ('current_password', current_pw),
353 296 ('__start__', 'new_password:mapping'),
354 297 ('new_password', new_pw),
355 298 ('new_password-confirm', confirm_pw),
356 299 ('__end__', 'new_password:mapping'),
357 300 ('csrf_token', self.csrf_token),
358 301 ]
359 302 response = self.app.post(url('my_account_password'), form_data)
360 303 assert 'Error occurred' in response
361 304
362 305 def test_password_is_updated_in_session_on_password_change(self, user_util):
363 306 old_password = 'abcdef123'
364 307 new_password = 'abcdef124'
365 308
366 309 user = user_util.create_user(password=old_password)
367 310 session = self.log_user(user.username, old_password)
368 311 old_password_hash = session['password']
369 312
370 313 form_data = [
371 314 ('current_password', old_password),
372 315 ('__start__', 'new_password:mapping'),
373 316 ('new_password', new_password),
374 317 ('new_password-confirm', new_password),
375 318 ('__end__', 'new_password:mapping'),
376 319 ('csrf_token', self.csrf_token),
377 320 ]
378 321 self.app.post(url('my_account_password'), form_data)
379 322
380 323 response = self.app.get(url('home'))
381 324 new_password_hash = response.session['rhodecode_user']['password']
382 325
383 326 assert old_password_hash != new_password_hash
General Comments 0
You need to be logged in to leave comments. Login now