Show More
The requested changes are too big and content was truncated. Show full diff
@@ -1,55 +1,53 b'' | |||
|
1 | 1 | # top level files |
|
2 | 2 | include test.ini |
|
3 | 3 | include MANIFEST.in |
|
4 | 4 | include README.rst |
|
5 | 5 | include CHANGES.rst |
|
6 | 6 | include LICENSE.txt |
|
7 | 7 | |
|
8 | 8 | include rhodecode/VERSION |
|
9 | 9 | |
|
10 | 10 | # docs |
|
11 | 11 | recursive-include docs * |
|
12 | 12 | |
|
13 | 13 | # all config files |
|
14 | 14 | recursive-include configs * |
|
15 | 15 | |
|
16 | 16 | # translations |
|
17 | 17 | recursive-include rhodecode/i18n * |
|
18 | 18 | |
|
19 | 19 | # hook templates |
|
20 | 20 | recursive-include rhodecode/config/hook_templates * |
|
21 | 21 | |
|
22 | 22 | # non-python core stuff |
|
23 | 23 | recursive-include rhodecode *.cfg |
|
24 | 24 | recursive-include rhodecode *.json |
|
25 | 25 | recursive-include rhodecode *.ini_tmpl |
|
26 | 26 | recursive-include rhodecode *.sh |
|
27 | 27 | recursive-include rhodecode *.mako |
|
28 | 28 | |
|
29 | 29 | # 502 page |
|
30 | 30 | include rhodecode/public/502.html |
|
31 | 31 | |
|
32 | # 502 page | |
|
33 | include rhodecode/public/502.html | |
|
34 | 32 | |
|
35 | 33 | # images, css |
|
36 | 34 | include rhodecode/public/css/*.css |
|
37 | 35 | include rhodecode/public/images/*.* |
|
38 | 36 | |
|
39 | 37 | # sound files |
|
40 | 38 | include rhodecode/public/sounds/*.mp3 |
|
41 | 39 | include rhodecode/public/sounds/*.wav |
|
42 | 40 | |
|
43 | 41 | # fonts |
|
44 | 42 | recursive-include rhodecode/public/fonts/ProximaNova * |
|
45 | 43 | recursive-include rhodecode/public/fonts/RCIcons * |
|
46 | 44 | |
|
47 | 45 | # js |
|
48 | 46 | recursive-include rhodecode/public/js * |
|
49 | 47 | |
|
50 | 48 | # templates |
|
51 | 49 | recursive-include rhodecode/templates * |
|
52 | 50 | |
|
53 | 51 | # skip any tests files |
|
54 | 52 | recursive-exclude rhodecode/tests * |
|
55 | 53 |
@@ -1,82 +1,82 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 collections |
|
22 | 22 | import logging |
|
23 | 23 | |
|
24 | 24 | from pylons import tmpl_context as c |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | |
|
27 | 27 | from rhodecode.lib.auth import ( |
|
28 | 28 | LoginRequired, HasPermissionAllDecorator, CSRFRequired) |
|
29 | 29 | from rhodecode.lib.utils import read_opensource_licenses |
|
30 | 30 | from rhodecode.svn_support.utils import generate_mod_dav_svn_config |
|
31 | 31 | from rhodecode.translation import _ |
|
32 | 32 | |
|
33 | 33 | from .navigation import navigation_list |
|
34 | 34 | |
|
35 | 35 | |
|
36 | 36 | log = logging.getLogger(__name__) |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | class AdminSettingsView(object): |
|
40 | 40 | |
|
41 | 41 | def __init__(self, context, request): |
|
42 | 42 | self.request = request |
|
43 | 43 | self.context = context |
|
44 | 44 | self.session = request.session |
|
45 | 45 | self._rhodecode_user = request.user |
|
46 | 46 | |
|
47 | 47 | @LoginRequired() |
|
48 | 48 | @HasPermissionAllDecorator('hg.admin') |
|
49 | 49 | @view_config( |
|
50 | 50 | route_name='admin_settings_open_source', request_method='GET', |
|
51 |
renderer='rhodecode:templates/admin/settings/settings. |
|
|
51 | renderer='rhodecode:templates/admin/settings/settings.mako') | |
|
52 | 52 | def open_source_licenses(self): |
|
53 | 53 | c.active = 'open_source' |
|
54 | 54 | c.navlist = navigation_list(self.request) |
|
55 | 55 | c.opensource_licenses = collections.OrderedDict( |
|
56 | 56 | sorted(read_opensource_licenses().items(), key=lambda t: t[0])) |
|
57 | 57 | |
|
58 | 58 | return {} |
|
59 | 59 | |
|
60 | 60 | @LoginRequired() |
|
61 | 61 | @CSRFRequired() |
|
62 | 62 | @HasPermissionAllDecorator('hg.admin') |
|
63 | 63 | @view_config( |
|
64 | 64 | route_name='admin_settings_vcs_svn_generate_cfg', |
|
65 | 65 | request_method='POST', renderer='json') |
|
66 | 66 | def vcs_svn_generate_config(self): |
|
67 | 67 | try: |
|
68 | 68 | generate_mod_dav_svn_config(self.request.registry) |
|
69 | 69 | msg = { |
|
70 | 70 | 'message': _('Apache configuration for Subversion generated.'), |
|
71 | 71 | 'level': 'success', |
|
72 | 72 | } |
|
73 | 73 | except Exception: |
|
74 | 74 | log.exception( |
|
75 | 75 | 'Exception while generating the Apache configuration for Subversion.') |
|
76 | 76 | msg = { |
|
77 | 77 | 'message': _('Failed to generate the Apache configuration for Subversion.'), |
|
78 | 78 | 'level': 'error', |
|
79 | 79 | } |
|
80 | 80 | |
|
81 | 81 | data = {'message': msg} |
|
82 | 82 | return data |
@@ -1,284 +1,284 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | RhodeCode authentication plugin for Atlassian CROWD |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | |
|
26 | 26 | import colander |
|
27 | 27 | import base64 |
|
28 | 28 | import logging |
|
29 | 29 | import urllib2 |
|
30 | 30 | |
|
31 | 31 | from pylons.i18n.translation import lazy_ugettext as _ |
|
32 | 32 | from sqlalchemy.ext.hybrid import hybrid_property |
|
33 | 33 | |
|
34 | 34 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
35 | 35 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
36 | 36 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
37 | 37 | from rhodecode.lib.colander_utils import strip_whitespace |
|
38 | 38 | from rhodecode.lib.ext_json import json, formatted_json |
|
39 | 39 | from rhodecode.model.db import User |
|
40 | 40 | |
|
41 | 41 | log = logging.getLogger(__name__) |
|
42 | 42 | |
|
43 | 43 | |
|
44 | 44 | def plugin_factory(plugin_id, *args, **kwds): |
|
45 | 45 | """ |
|
46 | 46 | Factory function that is called during plugin discovery. |
|
47 | 47 | It returns the plugin instance. |
|
48 | 48 | """ |
|
49 | 49 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
50 | 50 | return plugin |
|
51 | 51 | |
|
52 | 52 | |
|
53 | 53 | class CrowdAuthnResource(AuthnPluginResourceBase): |
|
54 | 54 | pass |
|
55 | 55 | |
|
56 | 56 | |
|
57 | 57 | class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase): |
|
58 | 58 | host = colander.SchemaNode( |
|
59 | 59 | colander.String(), |
|
60 | 60 | default='127.0.0.1', |
|
61 | 61 | description=_('The FQDN or IP of the Atlassian CROWD Server'), |
|
62 | 62 | preparer=strip_whitespace, |
|
63 | 63 | title=_('Host'), |
|
64 | 64 | widget='string') |
|
65 | 65 | port = colander.SchemaNode( |
|
66 | 66 | colander.Int(), |
|
67 | 67 | default=8095, |
|
68 | 68 | description=_('The Port in use by the Atlassian CROWD Server'), |
|
69 | 69 | preparer=strip_whitespace, |
|
70 | 70 | title=_('Port'), |
|
71 | 71 | validator=colander.Range(min=0, max=65536), |
|
72 | 72 | widget='int') |
|
73 | 73 | app_name = colander.SchemaNode( |
|
74 | 74 | colander.String(), |
|
75 | 75 | default='', |
|
76 | 76 | description=_('The Application Name to authenticate to CROWD'), |
|
77 | 77 | preparer=strip_whitespace, |
|
78 | 78 | title=_('Application Name'), |
|
79 | 79 | widget='string') |
|
80 | 80 | app_password = colander.SchemaNode( |
|
81 | 81 | colander.String(), |
|
82 | 82 | default='', |
|
83 | 83 | description=_('The password to authenticate to CROWD'), |
|
84 | 84 | preparer=strip_whitespace, |
|
85 | 85 | title=_('Application Password'), |
|
86 | 86 | widget='password') |
|
87 | 87 | admin_groups = colander.SchemaNode( |
|
88 | 88 | colander.String(), |
|
89 | 89 | default='', |
|
90 | 90 | description=_('A comma separated list of group names that identify ' |
|
91 | 91 | 'users as RhodeCode Administrators'), |
|
92 | 92 | missing='', |
|
93 | 93 | preparer=strip_whitespace, |
|
94 | 94 | title=_('Admin Groups'), |
|
95 | 95 | widget='string') |
|
96 | 96 | |
|
97 | 97 | |
|
98 | 98 | class CrowdServer(object): |
|
99 | 99 | def __init__(self, *args, **kwargs): |
|
100 | 100 | """ |
|
101 | 101 | Create a new CrowdServer object that points to IP/Address 'host', |
|
102 | 102 | on the given port, and using the given method (https/http). user and |
|
103 | 103 | passwd can be set here or with set_credentials. If unspecified, |
|
104 | 104 | "version" defaults to "latest". |
|
105 | 105 | |
|
106 | 106 | example:: |
|
107 | 107 | |
|
108 | 108 | cserver = CrowdServer(host="127.0.0.1", |
|
109 | 109 | port="8095", |
|
110 | 110 | user="some_app", |
|
111 | 111 | passwd="some_passwd", |
|
112 | 112 | version="1") |
|
113 | 113 | """ |
|
114 | 114 | if not "port" in kwargs: |
|
115 | 115 | kwargs["port"] = "8095" |
|
116 | 116 | self._logger = kwargs.get("logger", logging.getLogger(__name__)) |
|
117 | 117 | self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"), |
|
118 | 118 | kwargs.get("host", "127.0.0.1"), |
|
119 | 119 | kwargs.get("port", "8095")) |
|
120 | 120 | self.set_credentials(kwargs.get("user", ""), |
|
121 | 121 | kwargs.get("passwd", "")) |
|
122 | 122 | self._version = kwargs.get("version", "latest") |
|
123 | 123 | self._url_list = None |
|
124 | 124 | self._appname = "crowd" |
|
125 | 125 | |
|
126 | 126 | def set_credentials(self, user, passwd): |
|
127 | 127 | self.user = user |
|
128 | 128 | self.passwd = passwd |
|
129 | 129 | self._make_opener() |
|
130 | 130 | |
|
131 | 131 | def _make_opener(self): |
|
132 | 132 | mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() |
|
133 | 133 | mgr.add_password(None, self._uri, self.user, self.passwd) |
|
134 | 134 | handler = urllib2.HTTPBasicAuthHandler(mgr) |
|
135 | 135 | self.opener = urllib2.build_opener(handler) |
|
136 | 136 | |
|
137 | 137 | def _request(self, url, body=None, headers=None, |
|
138 | 138 | method=None, noformat=False, |
|
139 | 139 | empty_response_ok=False): |
|
140 | 140 | _headers = {"Content-type": "application/json", |
|
141 | 141 | "Accept": "application/json"} |
|
142 | 142 | if self.user and self.passwd: |
|
143 | 143 | authstring = base64.b64encode("%s:%s" % (self.user, self.passwd)) |
|
144 | 144 | _headers["Authorization"] = "Basic %s" % authstring |
|
145 | 145 | if headers: |
|
146 | 146 | _headers.update(headers) |
|
147 | 147 | log.debug("Sent crowd: \n%s" |
|
148 | 148 | % (formatted_json({"url": url, "body": body, |
|
149 | 149 | "headers": _headers}))) |
|
150 | 150 | request = urllib2.Request(url, body, _headers) |
|
151 | 151 | if method: |
|
152 | 152 | request.get_method = lambda: method |
|
153 | 153 | |
|
154 | 154 | global msg |
|
155 | 155 | msg = "" |
|
156 | 156 | try: |
|
157 | 157 | rdoc = self.opener.open(request) |
|
158 | 158 | msg = "".join(rdoc.readlines()) |
|
159 | 159 | if not msg and empty_response_ok: |
|
160 | 160 | rval = {} |
|
161 | 161 | rval["status"] = True |
|
162 | 162 | rval["error"] = "Response body was empty" |
|
163 | 163 | elif not noformat: |
|
164 | 164 | rval = json.loads(msg) |
|
165 | 165 | rval["status"] = True |
|
166 | 166 | else: |
|
167 | 167 | rval = "".join(rdoc.readlines()) |
|
168 | 168 | except Exception as e: |
|
169 | 169 | if not noformat: |
|
170 | 170 | rval = {"status": False, |
|
171 | 171 | "body": body, |
|
172 | 172 | "error": str(e) + "\n" + msg} |
|
173 | 173 | else: |
|
174 | 174 | rval = None |
|
175 | 175 | return rval |
|
176 | 176 | |
|
177 | 177 | def user_auth(self, username, password): |
|
178 | 178 | """Authenticate a user against crowd. Returns brief information about |
|
179 | 179 | the user.""" |
|
180 | 180 | url = ("%s/rest/usermanagement/%s/authentication?username=%s" |
|
181 | 181 | % (self._uri, self._version, username)) |
|
182 | 182 | body = json.dumps({"value": password}) |
|
183 | 183 | return self._request(url, body) |
|
184 | 184 | |
|
185 | 185 | def user_groups(self, username): |
|
186 | 186 | """Retrieve a list of groups to which this user belongs.""" |
|
187 | 187 | url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s" |
|
188 | 188 | % (self._uri, self._version, username)) |
|
189 | 189 | return self._request(url) |
|
190 | 190 | |
|
191 | 191 | |
|
192 | 192 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): |
|
193 | 193 | |
|
194 | 194 | def includeme(self, config): |
|
195 | 195 | config.add_authn_plugin(self) |
|
196 | 196 | config.add_authn_resource(self.get_id(), CrowdAuthnResource(self)) |
|
197 | 197 | config.add_view( |
|
198 | 198 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
199 | 199 | attr='settings_get', |
|
200 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
200 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
201 | 201 | request_method='GET', |
|
202 | 202 | route_name='auth_home', |
|
203 | 203 | context=CrowdAuthnResource) |
|
204 | 204 | config.add_view( |
|
205 | 205 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
206 | 206 | attr='settings_post', |
|
207 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
207 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
208 | 208 | request_method='POST', |
|
209 | 209 | route_name='auth_home', |
|
210 | 210 | context=CrowdAuthnResource) |
|
211 | 211 | |
|
212 | 212 | def get_settings_schema(self): |
|
213 | 213 | return CrowdSettingsSchema() |
|
214 | 214 | |
|
215 | 215 | def get_display_name(self): |
|
216 | 216 | return _('CROWD') |
|
217 | 217 | |
|
218 | 218 | @hybrid_property |
|
219 | 219 | def name(self): |
|
220 | 220 | return "crowd" |
|
221 | 221 | |
|
222 | 222 | def use_fake_password(self): |
|
223 | 223 | return True |
|
224 | 224 | |
|
225 | 225 | def user_activation_state(self): |
|
226 | 226 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
227 | 227 | return 'hg.extern_activate.auto' in def_user_perms |
|
228 | 228 | |
|
229 | 229 | def auth(self, userobj, username, password, settings, **kwargs): |
|
230 | 230 | """ |
|
231 | 231 | Given a user object (which may be null), username, a plaintext password, |
|
232 | 232 | and a settings object (containing all the keys needed as listed in settings()), |
|
233 | 233 | authenticate this user's login attempt. |
|
234 | 234 | |
|
235 | 235 | Return None on failure. On success, return a dictionary of the form: |
|
236 | 236 | |
|
237 | 237 | see: RhodeCodeAuthPluginBase.auth_func_attrs |
|
238 | 238 | This is later validated for correctness |
|
239 | 239 | """ |
|
240 | 240 | if not username or not password: |
|
241 | 241 | log.debug('Empty username or password skipping...') |
|
242 | 242 | return None |
|
243 | 243 | |
|
244 | 244 | log.debug("Crowd settings: \n%s" % (formatted_json(settings))) |
|
245 | 245 | server = CrowdServer(**settings) |
|
246 | 246 | server.set_credentials(settings["app_name"], settings["app_password"]) |
|
247 | 247 | crowd_user = server.user_auth(username, password) |
|
248 | 248 | log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user))) |
|
249 | 249 | if not crowd_user["status"]: |
|
250 | 250 | return None |
|
251 | 251 | |
|
252 | 252 | res = server.user_groups(crowd_user["name"]) |
|
253 | 253 | log.debug("Crowd groups: \n%s" % (formatted_json(res))) |
|
254 | 254 | crowd_user["groups"] = [x["name"] for x in res["groups"]] |
|
255 | 255 | |
|
256 | 256 | # old attrs fetched from RhodeCode database |
|
257 | 257 | admin = getattr(userobj, 'admin', False) |
|
258 | 258 | active = getattr(userobj, 'active', True) |
|
259 | 259 | email = getattr(userobj, 'email', '') |
|
260 | 260 | username = getattr(userobj, 'username', username) |
|
261 | 261 | firstname = getattr(userobj, 'firstname', '') |
|
262 | 262 | lastname = getattr(userobj, 'lastname', '') |
|
263 | 263 | extern_type = getattr(userobj, 'extern_type', '') |
|
264 | 264 | |
|
265 | 265 | user_attrs = { |
|
266 | 266 | 'username': username, |
|
267 | 267 | 'firstname': crowd_user["first-name"] or firstname, |
|
268 | 268 | 'lastname': crowd_user["last-name"] or lastname, |
|
269 | 269 | 'groups': crowd_user["groups"], |
|
270 | 270 | 'email': crowd_user["email"] or email, |
|
271 | 271 | 'admin': admin, |
|
272 | 272 | 'active': active, |
|
273 | 273 | 'active_from_extern': crowd_user.get('active'), |
|
274 | 274 | 'extern_name': crowd_user["name"], |
|
275 | 275 | 'extern_type': extern_type, |
|
276 | 276 | } |
|
277 | 277 | |
|
278 | 278 | # set an admin if we're in admin_groups of crowd |
|
279 | 279 | for group in settings["admin_groups"]: |
|
280 | 280 | if group in user_attrs["groups"]: |
|
281 | 281 | user_attrs["admin"] = True |
|
282 | 282 | log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs))) |
|
283 | 283 | log.info('user %s authenticated correctly' % user_attrs['username']) |
|
284 | 284 | return user_attrs |
@@ -1,225 +1,225 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 colander |
|
22 | 22 | import logging |
|
23 | 23 | |
|
24 | 24 | from sqlalchemy.ext.hybrid import hybrid_property |
|
25 | 25 | |
|
26 | 26 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
27 | 27 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
28 | 28 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
29 | 29 | from rhodecode.lib.colander_utils import strip_whitespace |
|
30 | 30 | from rhodecode.lib.utils2 import str2bool, safe_unicode |
|
31 | 31 | from rhodecode.model.db import User |
|
32 | 32 | from rhodecode.translation import _ |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | def plugin_factory(plugin_id, *args, **kwds): |
|
39 | 39 | """ |
|
40 | 40 | Factory function that is called during plugin discovery. |
|
41 | 41 | It returns the plugin instance. |
|
42 | 42 | """ |
|
43 | 43 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
44 | 44 | return plugin |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class HeadersAuthnResource(AuthnPluginResourceBase): |
|
48 | 48 | pass |
|
49 | 49 | |
|
50 | 50 | |
|
51 | 51 | class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase): |
|
52 | 52 | header = colander.SchemaNode( |
|
53 | 53 | colander.String(), |
|
54 | 54 | default='REMOTE_USER', |
|
55 | 55 | description=_('Header to extract the user from'), |
|
56 | 56 | preparer=strip_whitespace, |
|
57 | 57 | title=_('Header'), |
|
58 | 58 | widget='string') |
|
59 | 59 | fallback_header = colander.SchemaNode( |
|
60 | 60 | colander.String(), |
|
61 | 61 | default='HTTP_X_FORWARDED_USER', |
|
62 | 62 | description=_('Header to extract the user from when main one fails'), |
|
63 | 63 | preparer=strip_whitespace, |
|
64 | 64 | title=_('Fallback header'), |
|
65 | 65 | widget='string') |
|
66 | 66 | clean_username = colander.SchemaNode( |
|
67 | 67 | colander.Boolean(), |
|
68 | 68 | default=True, |
|
69 | 69 | description=_('Perform cleaning of user, if passed user has @ in ' |
|
70 | 70 | 'username then first part before @ is taken. ' |
|
71 | 71 | 'If there\'s \\ in the username only the part after ' |
|
72 | 72 | ' \\ is taken'), |
|
73 | 73 | missing=False, |
|
74 | 74 | title=_('Clean username'), |
|
75 | 75 | widget='bool') |
|
76 | 76 | |
|
77 | 77 | |
|
78 | 78 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): |
|
79 | 79 | |
|
80 | 80 | def includeme(self, config): |
|
81 | 81 | config.add_authn_plugin(self) |
|
82 | 82 | config.add_authn_resource(self.get_id(), HeadersAuthnResource(self)) |
|
83 | 83 | config.add_view( |
|
84 | 84 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
85 | 85 | attr='settings_get', |
|
86 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
86 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
87 | 87 | request_method='GET', |
|
88 | 88 | route_name='auth_home', |
|
89 | 89 | context=HeadersAuthnResource) |
|
90 | 90 | config.add_view( |
|
91 | 91 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
92 | 92 | attr='settings_post', |
|
93 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
93 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
94 | 94 | request_method='POST', |
|
95 | 95 | route_name='auth_home', |
|
96 | 96 | context=HeadersAuthnResource) |
|
97 | 97 | |
|
98 | 98 | def get_display_name(self): |
|
99 | 99 | return _('Headers') |
|
100 | 100 | |
|
101 | 101 | def get_settings_schema(self): |
|
102 | 102 | return HeadersSettingsSchema() |
|
103 | 103 | |
|
104 | 104 | @hybrid_property |
|
105 | 105 | def name(self): |
|
106 | 106 | return 'headers' |
|
107 | 107 | |
|
108 | 108 | @property |
|
109 | 109 | def is_headers_auth(self): |
|
110 | 110 | return True |
|
111 | 111 | |
|
112 | 112 | def use_fake_password(self): |
|
113 | 113 | return True |
|
114 | 114 | |
|
115 | 115 | def user_activation_state(self): |
|
116 | 116 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
117 | 117 | return 'hg.extern_activate.auto' in def_user_perms |
|
118 | 118 | |
|
119 | 119 | def _clean_username(self, username): |
|
120 | 120 | # Removing realm and domain from username |
|
121 | 121 | username = username.split('@')[0] |
|
122 | 122 | username = username.rsplit('\\')[-1] |
|
123 | 123 | return username |
|
124 | 124 | |
|
125 | 125 | def _get_username(self, environ, settings): |
|
126 | 126 | username = None |
|
127 | 127 | environ = environ or {} |
|
128 | 128 | if not environ: |
|
129 | 129 | log.debug('got empty environ: %s' % environ) |
|
130 | 130 | |
|
131 | 131 | settings = settings or {} |
|
132 | 132 | if settings.get('header'): |
|
133 | 133 | header = settings.get('header') |
|
134 | 134 | username = environ.get(header) |
|
135 | 135 | log.debug('extracted %s:%s' % (header, username)) |
|
136 | 136 | |
|
137 | 137 | # fallback mode |
|
138 | 138 | if not username and settings.get('fallback_header'): |
|
139 | 139 | header = settings.get('fallback_header') |
|
140 | 140 | username = environ.get(header) |
|
141 | 141 | log.debug('extracted %s:%s' % (header, username)) |
|
142 | 142 | |
|
143 | 143 | if username and str2bool(settings.get('clean_username')): |
|
144 | 144 | log.debug('Received username `%s` from headers' % username) |
|
145 | 145 | username = self._clean_username(username) |
|
146 | 146 | log.debug('New cleanup user is:%s' % username) |
|
147 | 147 | return username |
|
148 | 148 | |
|
149 | 149 | def get_user(self, username=None, **kwargs): |
|
150 | 150 | """ |
|
151 | 151 | Helper method for user fetching in plugins, by default it's using |
|
152 | 152 | simple fetch by username, but this method can be custimized in plugins |
|
153 | 153 | eg. headers auth plugin to fetch user by environ params |
|
154 | 154 | :param username: username if given to fetch |
|
155 | 155 | :param kwargs: extra arguments needed for user fetching. |
|
156 | 156 | """ |
|
157 | 157 | environ = kwargs.get('environ') or {} |
|
158 | 158 | settings = kwargs.get('settings') or {} |
|
159 | 159 | username = self._get_username(environ, settings) |
|
160 | 160 | # we got the username, so use default method now |
|
161 | 161 | return super(RhodeCodeAuthPlugin, self).get_user(username) |
|
162 | 162 | |
|
163 | 163 | def auth(self, userobj, username, password, settings, **kwargs): |
|
164 | 164 | """ |
|
165 | 165 | Get's the headers_auth username (or email). It tries to get username |
|
166 | 166 | from REMOTE_USER if this plugin is enabled, if that fails |
|
167 | 167 | it tries to get username from HTTP_X_FORWARDED_USER if fallback header |
|
168 | 168 | is set. clean_username extracts the username from this data if it's |
|
169 | 169 | having @ in it. |
|
170 | 170 | Return None on failure. On success, return a dictionary of the form: |
|
171 | 171 | |
|
172 | 172 | see: RhodeCodeAuthPluginBase.auth_func_attrs |
|
173 | 173 | |
|
174 | 174 | :param userobj: |
|
175 | 175 | :param username: |
|
176 | 176 | :param password: |
|
177 | 177 | :param settings: |
|
178 | 178 | :param kwargs: |
|
179 | 179 | """ |
|
180 | 180 | environ = kwargs.get('environ') |
|
181 | 181 | if not environ: |
|
182 | 182 | log.debug('Empty environ data skipping...') |
|
183 | 183 | return None |
|
184 | 184 | |
|
185 | 185 | if not userobj: |
|
186 | 186 | userobj = self.get_user('', environ=environ, settings=settings) |
|
187 | 187 | |
|
188 | 188 | # we don't care passed username/password for headers auth plugins. |
|
189 | 189 | # only way to log in is using environ |
|
190 | 190 | username = None |
|
191 | 191 | if userobj: |
|
192 | 192 | username = getattr(userobj, 'username') |
|
193 | 193 | |
|
194 | 194 | if not username: |
|
195 | 195 | # we don't have any objects in DB user doesn't exist extract |
|
196 | 196 | # username from environ based on the settings |
|
197 | 197 | username = self._get_username(environ, settings) |
|
198 | 198 | |
|
199 | 199 | # if cannot fetch username, it's a no-go for this plugin to proceed |
|
200 | 200 | if not username: |
|
201 | 201 | return None |
|
202 | 202 | |
|
203 | 203 | # old attrs fetched from RhodeCode database |
|
204 | 204 | admin = getattr(userobj, 'admin', False) |
|
205 | 205 | active = getattr(userobj, 'active', True) |
|
206 | 206 | email = getattr(userobj, 'email', '') |
|
207 | 207 | firstname = getattr(userobj, 'firstname', '') |
|
208 | 208 | lastname = getattr(userobj, 'lastname', '') |
|
209 | 209 | extern_type = getattr(userobj, 'extern_type', '') |
|
210 | 210 | |
|
211 | 211 | user_attrs = { |
|
212 | 212 | 'username': username, |
|
213 | 213 | 'firstname': safe_unicode(firstname or username), |
|
214 | 214 | 'lastname': safe_unicode(lastname or ''), |
|
215 | 215 | 'groups': [], |
|
216 | 216 | 'email': email or '', |
|
217 | 217 | 'admin': admin or False, |
|
218 | 218 | 'active': active, |
|
219 | 219 | 'active_from_extern': True, |
|
220 | 220 | 'extern_name': username, |
|
221 | 221 | 'extern_type': extern_type, |
|
222 | 222 | } |
|
223 | 223 | |
|
224 | 224 | log.info('user `%s` authenticated correctly' % user_attrs['username']) |
|
225 | 225 | return user_attrs |
@@ -1,167 +1,167 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | RhodeCode authentication plugin for Jasig CAS |
|
23 | 23 | http://www.jasig.org/cas |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | import colander |
|
28 | 28 | import logging |
|
29 | 29 | import rhodecode |
|
30 | 30 | import urllib |
|
31 | 31 | import urllib2 |
|
32 | 32 | |
|
33 | 33 | from pylons.i18n.translation import lazy_ugettext as _ |
|
34 | 34 | from sqlalchemy.ext.hybrid import hybrid_property |
|
35 | 35 | |
|
36 | 36 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
37 | 37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
38 | 38 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
39 | 39 | from rhodecode.lib.colander_utils import strip_whitespace |
|
40 | 40 | from rhodecode.lib.utils2 import safe_unicode |
|
41 | 41 | from rhodecode.model.db import User |
|
42 | 42 | |
|
43 | 43 | log = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | def plugin_factory(plugin_id, *args, **kwds): |
|
47 | 47 | """ |
|
48 | 48 | Factory function that is called during plugin discovery. |
|
49 | 49 | It returns the plugin instance. |
|
50 | 50 | """ |
|
51 | 51 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
52 | 52 | return plugin |
|
53 | 53 | |
|
54 | 54 | |
|
55 | 55 | class JasigCasAuthnResource(AuthnPluginResourceBase): |
|
56 | 56 | pass |
|
57 | 57 | |
|
58 | 58 | |
|
59 | 59 | class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase): |
|
60 | 60 | service_url = colander.SchemaNode( |
|
61 | 61 | colander.String(), |
|
62 | 62 | default='https://domain.com/cas/v1/tickets', |
|
63 | 63 | description=_('The url of the Jasig CAS REST service'), |
|
64 | 64 | preparer=strip_whitespace, |
|
65 | 65 | title=_('URL'), |
|
66 | 66 | widget='string') |
|
67 | 67 | |
|
68 | 68 | |
|
69 | 69 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): |
|
70 | 70 | |
|
71 | 71 | def includeme(self, config): |
|
72 | 72 | config.add_authn_plugin(self) |
|
73 | 73 | config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self)) |
|
74 | 74 | config.add_view( |
|
75 | 75 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
76 | 76 | attr='settings_get', |
|
77 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
77 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
78 | 78 | request_method='GET', |
|
79 | 79 | route_name='auth_home', |
|
80 | 80 | context=JasigCasAuthnResource) |
|
81 | 81 | config.add_view( |
|
82 | 82 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
83 | 83 | attr='settings_post', |
|
84 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
84 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
85 | 85 | request_method='POST', |
|
86 | 86 | route_name='auth_home', |
|
87 | 87 | context=JasigCasAuthnResource) |
|
88 | 88 | |
|
89 | 89 | def get_settings_schema(self): |
|
90 | 90 | return JasigCasSettingsSchema() |
|
91 | 91 | |
|
92 | 92 | def get_display_name(self): |
|
93 | 93 | return _('Jasig-CAS') |
|
94 | 94 | |
|
95 | 95 | @hybrid_property |
|
96 | 96 | def name(self): |
|
97 | 97 | return "jasig-cas" |
|
98 | 98 | |
|
99 | 99 | @property |
|
100 | 100 | def is_headers_auth(self): |
|
101 | 101 | return True |
|
102 | 102 | |
|
103 | 103 | def use_fake_password(self): |
|
104 | 104 | return True |
|
105 | 105 | |
|
106 | 106 | def user_activation_state(self): |
|
107 | 107 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
108 | 108 | return 'hg.extern_activate.auto' in def_user_perms |
|
109 | 109 | |
|
110 | 110 | def auth(self, userobj, username, password, settings, **kwargs): |
|
111 | 111 | """ |
|
112 | 112 | Given a user object (which may be null), username, a plaintext password, |
|
113 | 113 | and a settings object (containing all the keys needed as listed in settings()), |
|
114 | 114 | authenticate this user's login attempt. |
|
115 | 115 | |
|
116 | 116 | Return None on failure. On success, return a dictionary of the form: |
|
117 | 117 | |
|
118 | 118 | see: RhodeCodeAuthPluginBase.auth_func_attrs |
|
119 | 119 | This is later validated for correctness |
|
120 | 120 | """ |
|
121 | 121 | if not username or not password: |
|
122 | 122 | log.debug('Empty username or password skipping...') |
|
123 | 123 | return None |
|
124 | 124 | |
|
125 | 125 | log.debug("Jasig CAS settings: %s", settings) |
|
126 | 126 | params = urllib.urlencode({'username': username, 'password': password}) |
|
127 | 127 | headers = {"Content-type": "application/x-www-form-urlencoded", |
|
128 | 128 | "Accept": "text/plain", |
|
129 | 129 | "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__} |
|
130 | 130 | url = settings["service_url"] |
|
131 | 131 | |
|
132 | 132 | log.debug("Sent Jasig CAS: \n%s", |
|
133 | 133 | {"url": url, "body": params, "headers": headers}) |
|
134 | 134 | request = urllib2.Request(url, params, headers) |
|
135 | 135 | try: |
|
136 | 136 | response = urllib2.urlopen(request) |
|
137 | 137 | except urllib2.HTTPError as e: |
|
138 | 138 | log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code) |
|
139 | 139 | return None |
|
140 | 140 | except urllib2.URLError as e: |
|
141 | 141 | log.debug("URLError when requesting Jasig CAS url: %s " % url) |
|
142 | 142 | return None |
|
143 | 143 | |
|
144 | 144 | # old attrs fetched from RhodeCode database |
|
145 | 145 | admin = getattr(userobj, 'admin', False) |
|
146 | 146 | active = getattr(userobj, 'active', True) |
|
147 | 147 | email = getattr(userobj, 'email', '') |
|
148 | 148 | username = getattr(userobj, 'username', username) |
|
149 | 149 | firstname = getattr(userobj, 'firstname', '') |
|
150 | 150 | lastname = getattr(userobj, 'lastname', '') |
|
151 | 151 | extern_type = getattr(userobj, 'extern_type', '') |
|
152 | 152 | |
|
153 | 153 | user_attrs = { |
|
154 | 154 | 'username': username, |
|
155 | 155 | 'firstname': safe_unicode(firstname or username), |
|
156 | 156 | 'lastname': safe_unicode(lastname or ''), |
|
157 | 157 | 'groups': [], |
|
158 | 158 | 'email': email or '', |
|
159 | 159 | 'admin': admin or False, |
|
160 | 160 | 'active': active, |
|
161 | 161 | 'active_from_extern': True, |
|
162 | 162 | 'extern_name': username, |
|
163 | 163 | 'extern_type': extern_type, |
|
164 | 164 | } |
|
165 | 165 | |
|
166 | 166 | log.info('user %s authenticated correctly' % user_attrs['username']) |
|
167 | 167 | return user_attrs |
@@ -1,464 +1,464 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 | RhodeCode authentication plugin for LDAP |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | |
|
26 | 26 | import colander |
|
27 | 27 | import logging |
|
28 | 28 | import traceback |
|
29 | 29 | |
|
30 | 30 | from pylons.i18n.translation import lazy_ugettext as _ |
|
31 | 31 | from sqlalchemy.ext.hybrid import hybrid_property |
|
32 | 32 | |
|
33 | 33 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
34 | 34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
35 | 35 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
36 | 36 | from rhodecode.lib.colander_utils import strip_whitespace |
|
37 | 37 | from rhodecode.lib.exceptions import ( |
|
38 | 38 | LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError |
|
39 | 39 | ) |
|
40 | 40 | from rhodecode.lib.utils2 import safe_unicode, safe_str |
|
41 | 41 | from rhodecode.model.db import User |
|
42 | 42 | from rhodecode.model.validators import Missing |
|
43 | 43 | |
|
44 | 44 | log = logging.getLogger(__name__) |
|
45 | 45 | |
|
46 | 46 | try: |
|
47 | 47 | import ldap |
|
48 | 48 | except ImportError: |
|
49 | 49 | # means that python-ldap is not installed, we use Missing object to mark |
|
50 | 50 | # ldap lib is Missing |
|
51 | 51 | ldap = Missing |
|
52 | 52 | |
|
53 | 53 | |
|
54 | 54 | def plugin_factory(plugin_id, *args, **kwds): |
|
55 | 55 | """ |
|
56 | 56 | Factory function that is called during plugin discovery. |
|
57 | 57 | It returns the plugin instance. |
|
58 | 58 | """ |
|
59 | 59 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
60 | 60 | return plugin |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | class LdapAuthnResource(AuthnPluginResourceBase): |
|
64 | 64 | pass |
|
65 | 65 | |
|
66 | 66 | |
|
67 | 67 | class LdapSettingsSchema(AuthnPluginSettingsSchemaBase): |
|
68 | 68 | tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS'] |
|
69 | 69 | tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD'] |
|
70 | 70 | search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE'] |
|
71 | 71 | |
|
72 | 72 | host = colander.SchemaNode( |
|
73 | 73 | colander.String(), |
|
74 | 74 | default='', |
|
75 | 75 | description=_('Host of the LDAP Server'), |
|
76 | 76 | preparer=strip_whitespace, |
|
77 | 77 | title=_('LDAP Host'), |
|
78 | 78 | widget='string') |
|
79 | 79 | port = colander.SchemaNode( |
|
80 | 80 | colander.Int(), |
|
81 | 81 | default=389, |
|
82 | 82 | description=_('Port that the LDAP server is listening on'), |
|
83 | 83 | preparer=strip_whitespace, |
|
84 | 84 | title=_('Port'), |
|
85 | 85 | validator=colander.Range(min=0, max=65536), |
|
86 | 86 | widget='int') |
|
87 | 87 | dn_user = colander.SchemaNode( |
|
88 | 88 | colander.String(), |
|
89 | 89 | default='', |
|
90 | 90 | description=_('User to connect to LDAP'), |
|
91 | 91 | missing='', |
|
92 | 92 | preparer=strip_whitespace, |
|
93 | 93 | title=_('Account'), |
|
94 | 94 | widget='string') |
|
95 | 95 | dn_pass = colander.SchemaNode( |
|
96 | 96 | colander.String(), |
|
97 | 97 | default='', |
|
98 | 98 | description=_('Password to connect to LDAP'), |
|
99 | 99 | missing='', |
|
100 | 100 | preparer=strip_whitespace, |
|
101 | 101 | title=_('Password'), |
|
102 | 102 | widget='password') |
|
103 | 103 | tls_kind = colander.SchemaNode( |
|
104 | 104 | colander.String(), |
|
105 | 105 | default=tls_kind_choices[0], |
|
106 | 106 | description=_('TLS Type'), |
|
107 | 107 | title=_('Connection Security'), |
|
108 | 108 | validator=colander.OneOf(tls_kind_choices), |
|
109 | 109 | widget='select') |
|
110 | 110 | tls_reqcert = colander.SchemaNode( |
|
111 | 111 | colander.String(), |
|
112 | 112 | default=tls_reqcert_choices[0], |
|
113 | 113 | description=_('Require Cert over TLS?'), |
|
114 | 114 | title=_('Certificate Checks'), |
|
115 | 115 | validator=colander.OneOf(tls_reqcert_choices), |
|
116 | 116 | widget='select') |
|
117 | 117 | base_dn = colander.SchemaNode( |
|
118 | 118 | colander.String(), |
|
119 | 119 | default='', |
|
120 | 120 | description=_('Base DN to search (e.g., dc=mydomain,dc=com)'), |
|
121 | 121 | missing='', |
|
122 | 122 | preparer=strip_whitespace, |
|
123 | 123 | title=_('Base DN'), |
|
124 | 124 | widget='string') |
|
125 | 125 | filter = colander.SchemaNode( |
|
126 | 126 | colander.String(), |
|
127 | 127 | default='', |
|
128 | 128 | description=_('Filter to narrow results (e.g., ou=Users, etc)'), |
|
129 | 129 | missing='', |
|
130 | 130 | preparer=strip_whitespace, |
|
131 | 131 | title=_('LDAP Search Filter'), |
|
132 | 132 | widget='string') |
|
133 | 133 | search_scope = colander.SchemaNode( |
|
134 | 134 | colander.String(), |
|
135 | 135 | default=search_scope_choices[0], |
|
136 | 136 | description=_('How deep to search LDAP'), |
|
137 | 137 | title=_('LDAP Search Scope'), |
|
138 | 138 | validator=colander.OneOf(search_scope_choices), |
|
139 | 139 | widget='select') |
|
140 | 140 | attr_login = colander.SchemaNode( |
|
141 | 141 | colander.String(), |
|
142 | 142 | default='', |
|
143 | 143 | description=_('LDAP Attribute to map to user name'), |
|
144 | 144 | preparer=strip_whitespace, |
|
145 | 145 | title=_('Login Attribute'), |
|
146 | 146 | missing_msg=_('The LDAP Login attribute of the CN must be specified'), |
|
147 | 147 | widget='string') |
|
148 | 148 | attr_firstname = colander.SchemaNode( |
|
149 | 149 | colander.String(), |
|
150 | 150 | default='', |
|
151 | 151 | description=_('LDAP Attribute to map to first name'), |
|
152 | 152 | missing='', |
|
153 | 153 | preparer=strip_whitespace, |
|
154 | 154 | title=_('First Name Attribute'), |
|
155 | 155 | widget='string') |
|
156 | 156 | attr_lastname = colander.SchemaNode( |
|
157 | 157 | colander.String(), |
|
158 | 158 | default='', |
|
159 | 159 | description=_('LDAP Attribute to map to last name'), |
|
160 | 160 | missing='', |
|
161 | 161 | preparer=strip_whitespace, |
|
162 | 162 | title=_('Last Name Attribute'), |
|
163 | 163 | widget='string') |
|
164 | 164 | attr_email = colander.SchemaNode( |
|
165 | 165 | colander.String(), |
|
166 | 166 | default='', |
|
167 | 167 | description=_('LDAP Attribute to map to email address'), |
|
168 | 168 | missing='', |
|
169 | 169 | preparer=strip_whitespace, |
|
170 | 170 | title=_('Email Attribute'), |
|
171 | 171 | widget='string') |
|
172 | 172 | |
|
173 | 173 | |
|
174 | 174 | class AuthLdap(object): |
|
175 | 175 | |
|
176 | 176 | def _build_servers(self): |
|
177 | 177 | return ', '.join( |
|
178 | 178 | ["{}://{}:{}".format( |
|
179 | 179 | self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT) |
|
180 | 180 | for host in self.SERVER_ADDRESSES]) |
|
181 | 181 | |
|
182 | 182 | def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', |
|
183 | 183 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, |
|
184 | 184 | search_scope='SUBTREE', attr_login='uid', |
|
185 | 185 | ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'): |
|
186 | 186 | if ldap == Missing: |
|
187 | 187 | raise LdapImportError("Missing or incompatible ldap library") |
|
188 | 188 | |
|
189 | 189 | self.debug = False |
|
190 | 190 | self.ldap_version = ldap_version |
|
191 | 191 | self.ldap_server_type = 'ldap' |
|
192 | 192 | |
|
193 | 193 | self.TLS_KIND = tls_kind |
|
194 | 194 | |
|
195 | 195 | if self.TLS_KIND == 'LDAPS': |
|
196 | 196 | port = port or 689 |
|
197 | 197 | self.ldap_server_type += 's' |
|
198 | 198 | |
|
199 | 199 | OPT_X_TLS_DEMAND = 2 |
|
200 | 200 | self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, |
|
201 | 201 | OPT_X_TLS_DEMAND) |
|
202 | 202 | # split server into list |
|
203 | 203 | self.SERVER_ADDRESSES = server.split(',') |
|
204 | 204 | self.LDAP_SERVER_PORT = port |
|
205 | 205 | |
|
206 | 206 | # USE FOR READ ONLY BIND TO LDAP SERVER |
|
207 | 207 | self.attr_login = attr_login |
|
208 | 208 | |
|
209 | 209 | self.LDAP_BIND_DN = safe_str(bind_dn) |
|
210 | 210 | self.LDAP_BIND_PASS = safe_str(bind_pass) |
|
211 | 211 | self.LDAP_SERVER = self._build_servers() |
|
212 | 212 | self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope) |
|
213 | 213 | self.BASE_DN = safe_str(base_dn) |
|
214 | 214 | self.LDAP_FILTER = safe_str(ldap_filter) |
|
215 | 215 | |
|
216 | 216 | def _get_ldap_server(self): |
|
217 | 217 | if self.debug: |
|
218 | 218 | ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255) |
|
219 | 219 | if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'): |
|
220 | 220 | ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, |
|
221 | 221 | '/etc/openldap/cacerts') |
|
222 | 222 | ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) |
|
223 | 223 | ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON) |
|
224 | 224 | ldap.set_option(ldap.OPT_TIMEOUT, 20) |
|
225 | 225 | ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) |
|
226 | 226 | ldap.set_option(ldap.OPT_TIMELIMIT, 15) |
|
227 | 227 | if self.TLS_KIND != 'PLAIN': |
|
228 | 228 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT) |
|
229 | 229 | server = ldap.initialize(self.LDAP_SERVER) |
|
230 | 230 | if self.ldap_version == 2: |
|
231 | 231 | server.protocol = ldap.VERSION2 |
|
232 | 232 | else: |
|
233 | 233 | server.protocol = ldap.VERSION3 |
|
234 | 234 | |
|
235 | 235 | if self.TLS_KIND == 'START_TLS': |
|
236 | 236 | server.start_tls_s() |
|
237 | 237 | |
|
238 | 238 | if self.LDAP_BIND_DN and self.LDAP_BIND_PASS: |
|
239 | 239 | log.debug('Trying simple_bind with password and given DN: %s', |
|
240 | 240 | self.LDAP_BIND_DN) |
|
241 | 241 | server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS) |
|
242 | 242 | |
|
243 | 243 | return server |
|
244 | 244 | |
|
245 | 245 | def get_uid(self, username): |
|
246 | 246 | from rhodecode.lib.helpers import chop_at |
|
247 | 247 | uid = username |
|
248 | 248 | for server_addr in self.SERVER_ADDRESSES: |
|
249 | 249 | uid = chop_at(username, "@%s" % server_addr) |
|
250 | 250 | return uid |
|
251 | 251 | |
|
252 | 252 | def fetch_attrs_from_simple_bind(self, server, dn, username, password): |
|
253 | 253 | try: |
|
254 | 254 | log.debug('Trying simple bind with %s', dn) |
|
255 | 255 | server.simple_bind_s(dn, safe_str(password)) |
|
256 | 256 | user = server.search_ext_s( |
|
257 | 257 | dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0] |
|
258 | 258 | _, attrs = user |
|
259 | 259 | return attrs |
|
260 | 260 | |
|
261 | 261 | except ldap.INVALID_CREDENTIALS: |
|
262 | 262 | log.debug( |
|
263 | 263 | "LDAP rejected password for user '%s': %s, org_exc:", |
|
264 | 264 | username, dn, exc_info=True) |
|
265 | 265 | |
|
266 | 266 | def authenticate_ldap(self, username, password): |
|
267 | 267 | """ |
|
268 | 268 | Authenticate a user via LDAP and return his/her LDAP properties. |
|
269 | 269 | |
|
270 | 270 | Raises AuthenticationError if the credentials are rejected, or |
|
271 | 271 | EnvironmentError if the LDAP server can't be reached. |
|
272 | 272 | |
|
273 | 273 | :param username: username |
|
274 | 274 | :param password: password |
|
275 | 275 | """ |
|
276 | 276 | |
|
277 | 277 | uid = self.get_uid(username) |
|
278 | 278 | |
|
279 | 279 | if not password: |
|
280 | 280 | msg = "Authenticating user %s with blank password not allowed" |
|
281 | 281 | log.warning(msg, username) |
|
282 | 282 | raise LdapPasswordError(msg) |
|
283 | 283 | if "," in username: |
|
284 | 284 | raise LdapUsernameError("invalid character in username: ,") |
|
285 | 285 | try: |
|
286 | 286 | server = self._get_ldap_server() |
|
287 | 287 | filter_ = '(&%s(%s=%s))' % ( |
|
288 | 288 | self.LDAP_FILTER, self.attr_login, username) |
|
289 | 289 | log.debug("Authenticating %r filter %s at %s", self.BASE_DN, |
|
290 | 290 | filter_, self.LDAP_SERVER) |
|
291 | 291 | lobjects = server.search_ext_s( |
|
292 | 292 | self.BASE_DN, self.SEARCH_SCOPE, filter_) |
|
293 | 293 | |
|
294 | 294 | if not lobjects: |
|
295 | 295 | raise ldap.NO_SUCH_OBJECT() |
|
296 | 296 | |
|
297 | 297 | for (dn, _attrs) in lobjects: |
|
298 | 298 | if dn is None: |
|
299 | 299 | continue |
|
300 | 300 | |
|
301 | 301 | user_attrs = self.fetch_attrs_from_simple_bind( |
|
302 | 302 | server, dn, username, password) |
|
303 | 303 | if user_attrs: |
|
304 | 304 | break |
|
305 | 305 | |
|
306 | 306 | else: |
|
307 | 307 | log.debug("No matching LDAP objects for authentication " |
|
308 | 308 | "of '%s' (%s)", uid, username) |
|
309 | 309 | raise LdapPasswordError('Failed to authenticate user ' |
|
310 | 310 | 'with given password') |
|
311 | 311 | |
|
312 | 312 | except ldap.NO_SUCH_OBJECT: |
|
313 | 313 | log.debug("LDAP says no such user '%s' (%s), org_exc:", |
|
314 | 314 | uid, username, exc_info=True) |
|
315 | 315 | raise LdapUsernameError() |
|
316 | 316 | except ldap.SERVER_DOWN: |
|
317 | 317 | org_exc = traceback.format_exc() |
|
318 | 318 | raise LdapConnectionError( |
|
319 | 319 | "LDAP can't access authentication " |
|
320 | 320 | "server, org_exc:%s" % org_exc) |
|
321 | 321 | |
|
322 | 322 | return dn, user_attrs |
|
323 | 323 | |
|
324 | 324 | |
|
325 | 325 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): |
|
326 | 326 | # used to define dynamic binding in the |
|
327 | 327 | DYNAMIC_BIND_VAR = '$login' |
|
328 | 328 | |
|
329 | 329 | def includeme(self, config): |
|
330 | 330 | config.add_authn_plugin(self) |
|
331 | 331 | config.add_authn_resource(self.get_id(), LdapAuthnResource(self)) |
|
332 | 332 | config.add_view( |
|
333 | 333 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
334 | 334 | attr='settings_get', |
|
335 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
335 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
336 | 336 | request_method='GET', |
|
337 | 337 | route_name='auth_home', |
|
338 | 338 | context=LdapAuthnResource) |
|
339 | 339 | config.add_view( |
|
340 | 340 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
341 | 341 | attr='settings_post', |
|
342 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
342 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
343 | 343 | request_method='POST', |
|
344 | 344 | route_name='auth_home', |
|
345 | 345 | context=LdapAuthnResource) |
|
346 | 346 | |
|
347 | 347 | def get_settings_schema(self): |
|
348 | 348 | return LdapSettingsSchema() |
|
349 | 349 | |
|
350 | 350 | def get_display_name(self): |
|
351 | 351 | return _('LDAP') |
|
352 | 352 | |
|
353 | 353 | @hybrid_property |
|
354 | 354 | def name(self): |
|
355 | 355 | return "ldap" |
|
356 | 356 | |
|
357 | 357 | def use_fake_password(self): |
|
358 | 358 | return True |
|
359 | 359 | |
|
360 | 360 | def user_activation_state(self): |
|
361 | 361 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
362 | 362 | return 'hg.extern_activate.auto' in def_user_perms |
|
363 | 363 | |
|
364 | 364 | def try_dynamic_binding(self, username, password, current_args): |
|
365 | 365 | """ |
|
366 | 366 | Detects marker inside our original bind, and uses dynamic auth if |
|
367 | 367 | present |
|
368 | 368 | """ |
|
369 | 369 | |
|
370 | 370 | org_bind = current_args['bind_dn'] |
|
371 | 371 | passwd = current_args['bind_pass'] |
|
372 | 372 | |
|
373 | 373 | def has_bind_marker(username): |
|
374 | 374 | if self.DYNAMIC_BIND_VAR in username: |
|
375 | 375 | return True |
|
376 | 376 | |
|
377 | 377 | # we only passed in user with "special" variable |
|
378 | 378 | if org_bind and has_bind_marker(org_bind) and not passwd: |
|
379 | 379 | log.debug('Using dynamic user/password binding for ldap ' |
|
380 | 380 | 'authentication. Replacing `%s` with username', |
|
381 | 381 | self.DYNAMIC_BIND_VAR) |
|
382 | 382 | current_args['bind_dn'] = org_bind.replace( |
|
383 | 383 | self.DYNAMIC_BIND_VAR, username) |
|
384 | 384 | current_args['bind_pass'] = password |
|
385 | 385 | |
|
386 | 386 | return current_args |
|
387 | 387 | |
|
388 | 388 | def auth(self, userobj, username, password, settings, **kwargs): |
|
389 | 389 | """ |
|
390 | 390 | Given a user object (which may be null), username, a plaintext password, |
|
391 | 391 | and a settings object (containing all the keys needed as listed in |
|
392 | 392 | settings()), authenticate this user's login attempt. |
|
393 | 393 | |
|
394 | 394 | Return None on failure. On success, return a dictionary of the form: |
|
395 | 395 | |
|
396 | 396 | see: RhodeCodeAuthPluginBase.auth_func_attrs |
|
397 | 397 | This is later validated for correctness |
|
398 | 398 | """ |
|
399 | 399 | |
|
400 | 400 | if not username or not password: |
|
401 | 401 | log.debug('Empty username or password skipping...') |
|
402 | 402 | return None |
|
403 | 403 | |
|
404 | 404 | ldap_args = { |
|
405 | 405 | 'server': settings.get('host', ''), |
|
406 | 406 | 'base_dn': settings.get('base_dn', ''), |
|
407 | 407 | 'port': settings.get('port'), |
|
408 | 408 | 'bind_dn': settings.get('dn_user'), |
|
409 | 409 | 'bind_pass': settings.get('dn_pass'), |
|
410 | 410 | 'tls_kind': settings.get('tls_kind'), |
|
411 | 411 | 'tls_reqcert': settings.get('tls_reqcert'), |
|
412 | 412 | 'search_scope': settings.get('search_scope'), |
|
413 | 413 | 'attr_login': settings.get('attr_login'), |
|
414 | 414 | 'ldap_version': 3, |
|
415 | 415 | 'ldap_filter': settings.get('filter'), |
|
416 | 416 | } |
|
417 | 417 | |
|
418 | 418 | ldap_attrs = self.try_dynamic_binding(username, password, ldap_args) |
|
419 | 419 | |
|
420 | 420 | log.debug('Checking for ldap authentication.') |
|
421 | 421 | |
|
422 | 422 | try: |
|
423 | 423 | aldap = AuthLdap(**ldap_args) |
|
424 | 424 | (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password) |
|
425 | 425 | log.debug('Got ldap DN response %s', user_dn) |
|
426 | 426 | |
|
427 | 427 | def get_ldap_attr(k): |
|
428 | 428 | return ldap_attrs.get(settings.get(k), [''])[0] |
|
429 | 429 | |
|
430 | 430 | # old attrs fetched from RhodeCode database |
|
431 | 431 | admin = getattr(userobj, 'admin', False) |
|
432 | 432 | active = getattr(userobj, 'active', True) |
|
433 | 433 | email = getattr(userobj, 'email', '') |
|
434 | 434 | username = getattr(userobj, 'username', username) |
|
435 | 435 | firstname = getattr(userobj, 'firstname', '') |
|
436 | 436 | lastname = getattr(userobj, 'lastname', '') |
|
437 | 437 | extern_type = getattr(userobj, 'extern_type', '') |
|
438 | 438 | |
|
439 | 439 | groups = [] |
|
440 | 440 | user_attrs = { |
|
441 | 441 | 'username': username, |
|
442 | 442 | 'firstname': safe_unicode( |
|
443 | 443 | get_ldap_attr('attr_firstname') or firstname), |
|
444 | 444 | 'lastname': safe_unicode( |
|
445 | 445 | get_ldap_attr('attr_lastname') or lastname), |
|
446 | 446 | 'groups': groups, |
|
447 | 447 | 'email': get_ldap_attr('attr_email') or email, |
|
448 | 448 | 'admin': admin, |
|
449 | 449 | 'active': active, |
|
450 | 450 | "active_from_extern": None, |
|
451 | 451 | 'extern_name': user_dn, |
|
452 | 452 | 'extern_type': extern_type, |
|
453 | 453 | } |
|
454 | 454 | log.debug('ldap user: %s', user_attrs) |
|
455 | 455 | log.info('user %s authenticated correctly', user_attrs['username']) |
|
456 | 456 | |
|
457 | 457 | return user_attrs |
|
458 | 458 | |
|
459 | 459 | except (LdapUsernameError, LdapPasswordError, LdapImportError): |
|
460 | 460 | log.exception("LDAP related exception") |
|
461 | 461 | return None |
|
462 | 462 | except (Exception,): |
|
463 | 463 | log.exception("Other exception") |
|
464 | 464 | return None |
@@ -1,160 +1,160 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | RhodeCode authentication library for PAM |
|
22 | 22 | """ |
|
23 | 23 | |
|
24 | 24 | import colander |
|
25 | 25 | import grp |
|
26 | 26 | import logging |
|
27 | 27 | import pam |
|
28 | 28 | import pwd |
|
29 | 29 | import re |
|
30 | 30 | import socket |
|
31 | 31 | |
|
32 | 32 | from pylons.i18n.translation import lazy_ugettext as _ |
|
33 | 33 | from sqlalchemy.ext.hybrid import hybrid_property |
|
34 | 34 | |
|
35 | 35 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
36 | 36 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
37 | 37 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
38 | 38 | from rhodecode.lib.colander_utils import strip_whitespace |
|
39 | 39 | |
|
40 | 40 | log = logging.getLogger(__name__) |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | def plugin_factory(plugin_id, *args, **kwds): |
|
44 | 44 | """ |
|
45 | 45 | Factory function that is called during plugin discovery. |
|
46 | 46 | It returns the plugin instance. |
|
47 | 47 | """ |
|
48 | 48 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
49 | 49 | return plugin |
|
50 | 50 | |
|
51 | 51 | |
|
52 | 52 | class PamAuthnResource(AuthnPluginResourceBase): |
|
53 | 53 | pass |
|
54 | 54 | |
|
55 | 55 | |
|
56 | 56 | class PamSettingsSchema(AuthnPluginSettingsSchemaBase): |
|
57 | 57 | service = colander.SchemaNode( |
|
58 | 58 | colander.String(), |
|
59 | 59 | default='login', |
|
60 | 60 | description=_('PAM service name to use for authentication.'), |
|
61 | 61 | preparer=strip_whitespace, |
|
62 | 62 | title=_('PAM service name'), |
|
63 | 63 | widget='string') |
|
64 | 64 | gecos = colander.SchemaNode( |
|
65 | 65 | colander.String(), |
|
66 | 66 | default='(?P<last_name>.+),\s*(?P<first_name>\w+)', |
|
67 | 67 | description=_('Regular expression for extracting user name/email etc. ' |
|
68 | 68 | 'from Unix userinfo.'), |
|
69 | 69 | preparer=strip_whitespace, |
|
70 | 70 | title=_('Gecos Regex'), |
|
71 | 71 | widget='string') |
|
72 | 72 | |
|
73 | 73 | |
|
74 | 74 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): |
|
75 | 75 | # PAM authentication can be slow. Repository operations involve a lot of |
|
76 | 76 | # auth calls. Little caching helps speedup push/pull operations significantly |
|
77 | 77 | AUTH_CACHE_TTL = 4 |
|
78 | 78 | |
|
79 | 79 | def includeme(self, config): |
|
80 | 80 | config.add_authn_plugin(self) |
|
81 | 81 | config.add_authn_resource(self.get_id(), PamAuthnResource(self)) |
|
82 | 82 | config.add_view( |
|
83 | 83 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
84 | 84 | attr='settings_get', |
|
85 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
85 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
86 | 86 | request_method='GET', |
|
87 | 87 | route_name='auth_home', |
|
88 | 88 | context=PamAuthnResource) |
|
89 | 89 | config.add_view( |
|
90 | 90 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
91 | 91 | attr='settings_post', |
|
92 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
92 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
93 | 93 | request_method='POST', |
|
94 | 94 | route_name='auth_home', |
|
95 | 95 | context=PamAuthnResource) |
|
96 | 96 | |
|
97 | 97 | def get_display_name(self): |
|
98 | 98 | return _('PAM') |
|
99 | 99 | |
|
100 | 100 | @hybrid_property |
|
101 | 101 | def name(self): |
|
102 | 102 | return "pam" |
|
103 | 103 | |
|
104 | 104 | def get_settings_schema(self): |
|
105 | 105 | return PamSettingsSchema() |
|
106 | 106 | |
|
107 | 107 | def use_fake_password(self): |
|
108 | 108 | return True |
|
109 | 109 | |
|
110 | 110 | def auth(self, userobj, username, password, settings, **kwargs): |
|
111 | 111 | if not username or not password: |
|
112 | 112 | log.debug('Empty username or password skipping...') |
|
113 | 113 | return None |
|
114 | 114 | |
|
115 | 115 | auth_result = pam.authenticate(username, password, settings["service"]) |
|
116 | 116 | |
|
117 | 117 | if not auth_result: |
|
118 | 118 | log.error("PAM was unable to authenticate user: %s" % (username, )) |
|
119 | 119 | return None |
|
120 | 120 | |
|
121 | 121 | log.debug('Got PAM response %s' % (auth_result, )) |
|
122 | 122 | |
|
123 | 123 | # old attrs fetched from RhodeCode database |
|
124 | 124 | default_email = "%s@%s" % (username, socket.gethostname()) |
|
125 | 125 | admin = getattr(userobj, 'admin', False) |
|
126 | 126 | active = getattr(userobj, 'active', True) |
|
127 | 127 | email = getattr(userobj, 'email', '') or default_email |
|
128 | 128 | username = getattr(userobj, 'username', username) |
|
129 | 129 | firstname = getattr(userobj, 'firstname', '') |
|
130 | 130 | lastname = getattr(userobj, 'lastname', '') |
|
131 | 131 | extern_type = getattr(userobj, 'extern_type', '') |
|
132 | 132 | |
|
133 | 133 | user_attrs = { |
|
134 | 134 | 'username': username, |
|
135 | 135 | 'firstname': firstname, |
|
136 | 136 | 'lastname': lastname, |
|
137 | 137 | 'groups': [g.gr_name for g in grp.getgrall() |
|
138 | 138 | if username in g.gr_mem], |
|
139 | 139 | 'email': email, |
|
140 | 140 | 'admin': admin, |
|
141 | 141 | 'active': active, |
|
142 | 142 | 'active_from_extern': None, |
|
143 | 143 | 'extern_name': username, |
|
144 | 144 | 'extern_type': extern_type, |
|
145 | 145 | } |
|
146 | 146 | |
|
147 | 147 | try: |
|
148 | 148 | user_data = pwd.getpwnam(username) |
|
149 | 149 | regex = settings["gecos"] |
|
150 | 150 | match = re.search(regex, user_data.pw_gecos) |
|
151 | 151 | if match: |
|
152 | 152 | user_attrs["firstname"] = match.group('first_name') |
|
153 | 153 | user_attrs["lastname"] = match.group('last_name') |
|
154 | 154 | except Exception: |
|
155 | 155 | log.warning("Cannot extract additional info for PAM user") |
|
156 | 156 | pass |
|
157 | 157 | |
|
158 | 158 | log.debug("pamuser: %s", user_attrs) |
|
159 | 159 | log.info('user %s authenticated correctly' % user_attrs['username']) |
|
160 | 160 | return user_attrs |
@@ -1,143 +1,143 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | RhodeCode authentication plugin for built in internal auth |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from pylons.i18n.translation import lazy_ugettext as _ |
|
28 | 28 | from sqlalchemy.ext.hybrid import hybrid_property |
|
29 | 29 | |
|
30 | 30 | from rhodecode.authentication.base import RhodeCodeAuthPluginBase |
|
31 | 31 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
32 | 32 | from rhodecode.lib.utils2 import safe_str |
|
33 | 33 | from rhodecode.model.db import User |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | def plugin_factory(plugin_id, *args, **kwds): |
|
39 | 39 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
40 | 40 | return plugin |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | class RhodecodeAuthnResource(AuthnPluginResourceBase): |
|
44 | 44 | pass |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): |
|
48 | 48 | |
|
49 | 49 | def includeme(self, config): |
|
50 | 50 | config.add_authn_plugin(self) |
|
51 | 51 | config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self)) |
|
52 | 52 | config.add_view( |
|
53 | 53 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
54 | 54 | attr='settings_get', |
|
55 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
55 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
56 | 56 | request_method='GET', |
|
57 | 57 | route_name='auth_home', |
|
58 | 58 | context=RhodecodeAuthnResource) |
|
59 | 59 | config.add_view( |
|
60 | 60 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
61 | 61 | attr='settings_post', |
|
62 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
62 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
63 | 63 | request_method='POST', |
|
64 | 64 | route_name='auth_home', |
|
65 | 65 | context=RhodecodeAuthnResource) |
|
66 | 66 | |
|
67 | 67 | def get_display_name(self): |
|
68 | 68 | return _('Rhodecode') |
|
69 | 69 | |
|
70 | 70 | @hybrid_property |
|
71 | 71 | def name(self): |
|
72 | 72 | return "rhodecode" |
|
73 | 73 | |
|
74 | 74 | def user_activation_state(self): |
|
75 | 75 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
76 | 76 | return 'hg.register.auto_activate' in def_user_perms |
|
77 | 77 | |
|
78 | 78 | def allows_authentication_from( |
|
79 | 79 | self, user, allows_non_existing_user=True, |
|
80 | 80 | allowed_auth_plugins=None, allowed_auth_sources=None): |
|
81 | 81 | """ |
|
82 | 82 | Custom method for this auth that doesn't accept non existing users. |
|
83 | 83 | We know that user exists in our database. |
|
84 | 84 | """ |
|
85 | 85 | allows_non_existing_user = False |
|
86 | 86 | return super(RhodeCodeAuthPlugin, self).allows_authentication_from( |
|
87 | 87 | user, allows_non_existing_user=allows_non_existing_user) |
|
88 | 88 | |
|
89 | 89 | def auth(self, userobj, username, password, settings, **kwargs): |
|
90 | 90 | if not userobj: |
|
91 | 91 | log.debug('userobj was:%s skipping' % (userobj, )) |
|
92 | 92 | return None |
|
93 | 93 | if userobj.extern_type != self.name: |
|
94 | 94 | log.warning( |
|
95 | 95 | "userobj:%s extern_type mismatch got:`%s` expected:`%s`" % |
|
96 | 96 | (userobj, userobj.extern_type, self.name)) |
|
97 | 97 | return None |
|
98 | 98 | |
|
99 | 99 | user_attrs = { |
|
100 | 100 | "username": userobj.username, |
|
101 | 101 | "firstname": userobj.firstname, |
|
102 | 102 | "lastname": userobj.lastname, |
|
103 | 103 | "groups": [], |
|
104 | 104 | "email": userobj.email, |
|
105 | 105 | "admin": userobj.admin, |
|
106 | 106 | "active": userobj.active, |
|
107 | 107 | "active_from_extern": userobj.active, |
|
108 | 108 | "extern_name": userobj.user_id, |
|
109 | 109 | "extern_type": userobj.extern_type, |
|
110 | 110 | } |
|
111 | 111 | |
|
112 | 112 | log.debug("User attributes:%s" % (user_attrs, )) |
|
113 | 113 | if userobj.active: |
|
114 | 114 | from rhodecode.lib import auth |
|
115 | 115 | crypto_backend = auth.crypto_backend() |
|
116 | 116 | password_encoded = safe_str(password) |
|
117 | 117 | password_match, new_hash = crypto_backend.hash_check_with_upgrade( |
|
118 | 118 | password_encoded, userobj.password) |
|
119 | 119 | |
|
120 | 120 | if password_match and new_hash: |
|
121 | 121 | log.debug('user %s properly authenticated, but ' |
|
122 | 122 | 'requires hash change to bcrypt', userobj) |
|
123 | 123 | # if password match, and we use OLD deprecated hash, |
|
124 | 124 | # we should migrate this user hash password to the new hash |
|
125 | 125 | # we store the new returned by hash_check_with_upgrade function |
|
126 | 126 | user_attrs['_hash_migrate'] = new_hash |
|
127 | 127 | |
|
128 | 128 | if userobj.username == User.DEFAULT_USER and userobj.active: |
|
129 | 129 | log.info( |
|
130 | 130 | 'user %s authenticated correctly as anonymous user', userobj) |
|
131 | 131 | return user_attrs |
|
132 | 132 | |
|
133 | 133 | elif userobj.username == username and password_match: |
|
134 | 134 | log.info('user %s authenticated correctly', userobj) |
|
135 | 135 | return user_attrs |
|
136 | 136 | log.info("user %s had a bad password when " |
|
137 | 137 | "authenticating on this plugin", userobj) |
|
138 | 138 | return None |
|
139 | 139 | else: |
|
140 | 140 | log.warning( |
|
141 | 141 | 'user `%s` failed to authenticate via %s, reason: account not ' |
|
142 | 142 | 'active.', username, self.name) |
|
143 | 143 | return None |
@@ -1,140 +1,140 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | RhodeCode authentication token plugin for built in internal auth |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from sqlalchemy.ext.hybrid import hybrid_property |
|
28 | 28 | |
|
29 | 29 | from rhodecode.translation import _ |
|
30 | 30 | from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE |
|
31 | 31 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
32 | 32 | from rhodecode.model.db import User, UserApiKeys |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | def plugin_factory(plugin_id, *args, **kwds): |
|
39 | 39 | plugin = RhodeCodeAuthPlugin(plugin_id) |
|
40 | 40 | return plugin |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | class RhodecodeAuthnResource(AuthnPluginResourceBase): |
|
44 | 44 | pass |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): |
|
48 | 48 | """ |
|
49 | 49 | Enables usage of authentication tokens for vcs operations. |
|
50 | 50 | """ |
|
51 | 51 | |
|
52 | 52 | def includeme(self, config): |
|
53 | 53 | config.add_authn_plugin(self) |
|
54 | 54 | config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self)) |
|
55 | 55 | config.add_view( |
|
56 | 56 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
57 | 57 | attr='settings_get', |
|
58 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
58 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
59 | 59 | request_method='GET', |
|
60 | 60 | route_name='auth_home', |
|
61 | 61 | context=RhodecodeAuthnResource) |
|
62 | 62 | config.add_view( |
|
63 | 63 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
64 | 64 | attr='settings_post', |
|
65 |
renderer='rhodecode:templates/admin/auth/plugin_settings. |
|
|
65 | renderer='rhodecode:templates/admin/auth/plugin_settings.mako', | |
|
66 | 66 | request_method='POST', |
|
67 | 67 | route_name='auth_home', |
|
68 | 68 | context=RhodecodeAuthnResource) |
|
69 | 69 | |
|
70 | 70 | def get_display_name(self): |
|
71 | 71 | return _('Rhodecode Token Auth') |
|
72 | 72 | |
|
73 | 73 | @hybrid_property |
|
74 | 74 | def name(self): |
|
75 | 75 | return "authtoken" |
|
76 | 76 | |
|
77 | 77 | def user_activation_state(self): |
|
78 | 78 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] |
|
79 | 79 | return 'hg.register.auto_activate' in def_user_perms |
|
80 | 80 | |
|
81 | 81 | def allows_authentication_from( |
|
82 | 82 | self, user, allows_non_existing_user=True, |
|
83 | 83 | allowed_auth_plugins=None, allowed_auth_sources=None): |
|
84 | 84 | """ |
|
85 | 85 | Custom method for this auth that doesn't accept empty users. And also |
|
86 | 86 | allows users from all other active plugins to use it and also |
|
87 | 87 | authenticate against it. But only via vcs mode |
|
88 | 88 | """ |
|
89 | 89 | from rhodecode.authentication.base import get_authn_registry |
|
90 | 90 | authn_registry = get_authn_registry() |
|
91 | 91 | |
|
92 | 92 | active_plugins = set( |
|
93 | 93 | [x.name for x in authn_registry.get_plugins_for_authentication()]) |
|
94 | 94 | active_plugins.discard(self.name) |
|
95 | 95 | |
|
96 | 96 | allowed_auth_plugins = [self.name] + list(active_plugins) |
|
97 | 97 | # only for vcs operations |
|
98 | 98 | allowed_auth_sources = [VCS_TYPE] |
|
99 | 99 | |
|
100 | 100 | return super(RhodeCodeAuthPlugin, self).allows_authentication_from( |
|
101 | 101 | user, allows_non_existing_user=False, |
|
102 | 102 | allowed_auth_plugins=allowed_auth_plugins, |
|
103 | 103 | allowed_auth_sources=allowed_auth_sources) |
|
104 | 104 | |
|
105 | 105 | def auth(self, userobj, username, password, settings, **kwargs): |
|
106 | 106 | if not userobj: |
|
107 | 107 | log.debug('userobj was:%s skipping' % (userobj, )) |
|
108 | 108 | return None |
|
109 | 109 | |
|
110 | 110 | user_attrs = { |
|
111 | 111 | "username": userobj.username, |
|
112 | 112 | "firstname": userobj.firstname, |
|
113 | 113 | "lastname": userobj.lastname, |
|
114 | 114 | "groups": [], |
|
115 | 115 | "email": userobj.email, |
|
116 | 116 | "admin": userobj.admin, |
|
117 | 117 | "active": userobj.active, |
|
118 | 118 | "active_from_extern": userobj.active, |
|
119 | 119 | "extern_name": userobj.user_id, |
|
120 | 120 | "extern_type": userobj.extern_type, |
|
121 | 121 | } |
|
122 | 122 | |
|
123 | 123 | log.debug('Authenticating user with args %s', user_attrs) |
|
124 | 124 | if userobj.active: |
|
125 | 125 | role = UserApiKeys.ROLE_VCS |
|
126 | 126 | active_tokens = [x.api_key for x in |
|
127 | 127 | User.extra_valid_auth_tokens(userobj, role=role)] |
|
128 | 128 | if userobj.username == username and password in active_tokens: |
|
129 | 129 | log.info( |
|
130 | 130 | 'user `%s` successfully authenticated via %s', |
|
131 | 131 | user_attrs['username'], self.name) |
|
132 | 132 | return user_attrs |
|
133 | 133 | log.error( |
|
134 | 134 | 'user `%s` failed to authenticate via %s, reason: bad or ' |
|
135 | 135 | 'inactive token.', username, self.name) |
|
136 | 136 | else: |
|
137 | 137 | log.warning( |
|
138 | 138 | 'user `%s` failed to authenticate via %s, reason: account not ' |
|
139 | 139 | 'active.', username, self.name) |
|
140 | 140 | return None |
@@ -1,192 +1,192 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 colander |
|
22 | 22 | import formencode.htmlfill |
|
23 | 23 | import logging |
|
24 | 24 | |
|
25 | 25 | from pyramid.httpexceptions import HTTPFound |
|
26 | 26 | from pyramid.renderers import render |
|
27 | 27 | from pyramid.response import Response |
|
28 | 28 | |
|
29 | 29 | from rhodecode.authentication.base import ( |
|
30 | 30 | get_auth_cache_manager, get_authn_registry) |
|
31 | 31 | from rhodecode.lib import auth |
|
32 | 32 | from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator |
|
33 | 33 | from rhodecode.model.forms import AuthSettingsForm |
|
34 | 34 | from rhodecode.model.meta import Session |
|
35 | 35 | from rhodecode.model.settings import SettingsModel |
|
36 | 36 | from rhodecode.translation import _ |
|
37 | 37 | |
|
38 | 38 | log = logging.getLogger(__name__) |
|
39 | 39 | |
|
40 | 40 | |
|
41 | 41 | class AuthnPluginViewBase(object): |
|
42 | 42 | |
|
43 | 43 | def __init__(self, context, request): |
|
44 | 44 | self.request = request |
|
45 | 45 | self.context = context |
|
46 | 46 | self.plugin = context.plugin |
|
47 | 47 | self._rhodecode_user = request.user |
|
48 | 48 | |
|
49 | 49 | @LoginRequired() |
|
50 | 50 | @HasPermissionAllDecorator('hg.admin') |
|
51 | 51 | def settings_get(self, defaults=None, errors=None): |
|
52 | 52 | """ |
|
53 | 53 | View that displays the plugin settings as a form. |
|
54 | 54 | """ |
|
55 | 55 | defaults = defaults or {} |
|
56 | 56 | errors = errors or {} |
|
57 | 57 | schema = self.plugin.get_settings_schema() |
|
58 | 58 | |
|
59 | 59 | # Compute default values for the form. Priority is: |
|
60 | 60 | # 1. Passed to this method 2. DB value 3. Schema default |
|
61 | 61 | for node in schema: |
|
62 | 62 | if node.name not in defaults: |
|
63 | 63 | defaults[node.name] = self.plugin.get_setting_by_name( |
|
64 | 64 | node.name, node.default) |
|
65 | 65 | |
|
66 | 66 | template_context = { |
|
67 | 67 | 'defaults': defaults, |
|
68 | 68 | 'errors': errors, |
|
69 | 69 | 'plugin': self.context.plugin, |
|
70 | 70 | 'resource': self.context, |
|
71 | 71 | } |
|
72 | 72 | |
|
73 | 73 | return template_context |
|
74 | 74 | |
|
75 | 75 | @LoginRequired() |
|
76 | 76 | @HasPermissionAllDecorator('hg.admin') |
|
77 | 77 | @auth.CSRFRequired() |
|
78 | 78 | def settings_post(self): |
|
79 | 79 | """ |
|
80 | 80 | View that validates and stores the plugin settings. |
|
81 | 81 | """ |
|
82 | 82 | schema = self.plugin.get_settings_schema() |
|
83 | 83 | data = self.request.params |
|
84 | 84 | |
|
85 | 85 | try: |
|
86 | 86 | valid_data = schema.deserialize(data) |
|
87 | 87 | except colander.Invalid as e: |
|
88 | 88 | # Display error message and display form again. |
|
89 | 89 | self.request.session.flash( |
|
90 | 90 | _('Errors exist when saving plugin settings. ' |
|
91 | 91 | 'Please check the form inputs.'), |
|
92 | 92 | queue='error') |
|
93 | 93 | defaults = {key: data[key] for key in data if key in schema} |
|
94 | 94 | return self.settings_get(errors=e.asdict(), defaults=defaults) |
|
95 | 95 | |
|
96 | 96 | # Store validated data. |
|
97 | 97 | for name, value in valid_data.items(): |
|
98 | 98 | self.plugin.create_or_update_setting(name, value) |
|
99 | 99 | Session().commit() |
|
100 | 100 | |
|
101 | 101 | # Display success message and redirect. |
|
102 | 102 | self.request.session.flash( |
|
103 | 103 | _('Auth settings updated successfully.'), |
|
104 | 104 | queue='success') |
|
105 | 105 | redirect_to = self.request.resource_path( |
|
106 | 106 | self.context, route_name='auth_home') |
|
107 | 107 | return HTTPFound(redirect_to) |
|
108 | 108 | |
|
109 | 109 | |
|
110 | 110 | # TODO: Ongoing migration in these views. |
|
111 | 111 | # - Maybe we should also use a colander schema for these views. |
|
112 | 112 | class AuthSettingsView(object): |
|
113 | 113 | def __init__(self, context, request): |
|
114 | 114 | self.context = context |
|
115 | 115 | self.request = request |
|
116 | 116 | |
|
117 | 117 | # TODO: Move this into a utility function. It is needed in all view |
|
118 | 118 | # classes during migration. Maybe a mixin? |
|
119 | 119 | |
|
120 | 120 | # Some of the decorators rely on this attribute to be present on the |
|
121 | 121 | # class of the decorated method. |
|
122 | 122 | self._rhodecode_user = request.user |
|
123 | 123 | |
|
124 | 124 | @LoginRequired() |
|
125 | 125 | @HasPermissionAllDecorator('hg.admin') |
|
126 | 126 | def index(self, defaults=None, errors=None, prefix_error=False): |
|
127 | 127 | defaults = defaults or {} |
|
128 | 128 | authn_registry = get_authn_registry(self.request.registry) |
|
129 | 129 | enabled_plugins = SettingsModel().get_auth_plugins() |
|
130 | 130 | |
|
131 | 131 | # Create template context and render it. |
|
132 | 132 | template_context = { |
|
133 | 133 | 'resource': self.context, |
|
134 | 134 | 'available_plugins': authn_registry.get_plugins(), |
|
135 | 135 | 'enabled_plugins': enabled_plugins, |
|
136 | 136 | } |
|
137 |
html = render('rhodecode:templates/admin/auth/auth_settings. |
|
|
137 | html = render('rhodecode:templates/admin/auth/auth_settings.mako', | |
|
138 | 138 | template_context, |
|
139 | 139 | request=self.request) |
|
140 | 140 | |
|
141 | 141 | # Create form default values and fill the form. |
|
142 | 142 | form_defaults = { |
|
143 | 143 | 'auth_plugins': ','.join(enabled_plugins) |
|
144 | 144 | } |
|
145 | 145 | form_defaults.update(defaults) |
|
146 | 146 | html = formencode.htmlfill.render( |
|
147 | 147 | html, |
|
148 | 148 | defaults=form_defaults, |
|
149 | 149 | errors=errors, |
|
150 | 150 | prefix_error=prefix_error, |
|
151 | 151 | encoding="UTF-8", |
|
152 | 152 | force_defaults=False) |
|
153 | 153 | |
|
154 | 154 | return Response(html) |
|
155 | 155 | |
|
156 | 156 | @LoginRequired() |
|
157 | 157 | @HasPermissionAllDecorator('hg.admin') |
|
158 | 158 | @auth.CSRFRequired() |
|
159 | 159 | def auth_settings(self): |
|
160 | 160 | try: |
|
161 | 161 | form = AuthSettingsForm()() |
|
162 | 162 | form_result = form.to_python(self.request.params) |
|
163 | 163 | plugins = ','.join(form_result['auth_plugins']) |
|
164 | 164 | setting = SettingsModel().create_or_update_setting( |
|
165 | 165 | 'auth_plugins', plugins) |
|
166 | 166 | Session().add(setting) |
|
167 | 167 | Session().commit() |
|
168 | 168 | |
|
169 | 169 | cache_manager = get_auth_cache_manager() |
|
170 | 170 | cache_manager.clear() |
|
171 | 171 | self.request.session.flash( |
|
172 | 172 | _('Auth settings updated successfully.'), |
|
173 | 173 | queue='success') |
|
174 | 174 | except formencode.Invalid as errors: |
|
175 | 175 | e = errors.error_dict or {} |
|
176 | 176 | self.request.session.flash( |
|
177 | 177 | _('Errors exist when saving plugin setting. ' |
|
178 | 178 | 'Please check the form inputs.'), |
|
179 | 179 | queue='error') |
|
180 | 180 | return self.index( |
|
181 | 181 | defaults=errors.value, |
|
182 | 182 | errors=e, |
|
183 | 183 | prefix_error=False) |
|
184 | 184 | except Exception: |
|
185 | 185 | log.exception('Exception in auth_settings') |
|
186 | 186 | self.request.session.flash( |
|
187 | 187 | _('Error occurred during update of auth settings.'), |
|
188 | 188 | queue='error') |
|
189 | 189 | |
|
190 | 190 | redirect_to = self.request.resource_path( |
|
191 | 191 | self.context, route_name='auth_home') |
|
192 | 192 | return HTTPFound(redirect_to) |
@@ -1,88 +1,88 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 os |
|
22 | 22 | |
|
23 | 23 | from pyramid.settings import asbool |
|
24 | 24 | |
|
25 | 25 | from rhodecode.config.routing import ADMIN_PREFIX |
|
26 | 26 | from rhodecode.lib.ext_json import json |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | def url_gen(request): |
|
30 | 30 | registry = request.registry |
|
31 | 31 | longpoll_url = registry.settings.get('channelstream.longpoll_url', '') |
|
32 | 32 | ws_url = registry.settings.get('channelstream.ws_url', '') |
|
33 | 33 | proxy_url = request.route_url('channelstream_proxy') |
|
34 | 34 | urls = { |
|
35 | 35 | 'connect': request.route_path('channelstream_connect'), |
|
36 | 36 | 'subscribe': request.route_path('channelstream_subscribe'), |
|
37 | 37 | 'longpoll': longpoll_url or proxy_url, |
|
38 | 38 | 'ws': ws_url or proxy_url.replace('http', 'ws') |
|
39 | 39 | } |
|
40 | 40 | return json.dumps(urls) |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | PLUGIN_DEFINITION = { |
|
44 | 44 | 'name': 'channelstream', |
|
45 | 45 | 'config': { |
|
46 | 46 | 'javascript': [], |
|
47 | 47 | 'css': [], |
|
48 | 48 | 'template_hooks': { |
|
49 |
'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init. |
|
|
49 | 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako' | |
|
50 | 50 | }, |
|
51 | 51 | 'url_gen': url_gen, |
|
52 | 52 | 'static': None, |
|
53 | 53 | 'enabled': False, |
|
54 | 54 | 'server': '', |
|
55 | 55 | 'secret': '' |
|
56 | 56 | } |
|
57 | 57 | } |
|
58 | 58 | |
|
59 | 59 | |
|
60 | 60 | def includeme(config): |
|
61 | 61 | settings = config.registry.settings |
|
62 | 62 | PLUGIN_DEFINITION['config']['enabled'] = asbool( |
|
63 | 63 | settings.get('channelstream.enabled')) |
|
64 | 64 | PLUGIN_DEFINITION['config']['server'] = settings.get( |
|
65 | 65 | 'channelstream.server', '') |
|
66 | 66 | PLUGIN_DEFINITION['config']['secret'] = settings.get( |
|
67 | 67 | 'channelstream.secret', '') |
|
68 | 68 | PLUGIN_DEFINITION['config']['history.location'] = settings.get( |
|
69 | 69 | 'channelstream.history.location', '') |
|
70 | 70 | config.register_rhodecode_plugin( |
|
71 | 71 | PLUGIN_DEFINITION['name'], |
|
72 | 72 | PLUGIN_DEFINITION['config'] |
|
73 | 73 | ) |
|
74 | 74 | # create plugin history location |
|
75 | 75 | history_dir = PLUGIN_DEFINITION['config']['history.location'] |
|
76 | 76 | if history_dir and not os.path.exists(history_dir): |
|
77 | 77 | os.makedirs(history_dir, 0750) |
|
78 | 78 | |
|
79 | 79 | config.add_route( |
|
80 | 80 | name='channelstream_connect', |
|
81 | 81 | pattern=ADMIN_PREFIX + '/channelstream/connect') |
|
82 | 82 | config.add_route( |
|
83 | 83 | name='channelstream_subscribe', |
|
84 | 84 | pattern=ADMIN_PREFIX + '/channelstream/subscribe') |
|
85 | 85 | config.add_route( |
|
86 | 86 | name='channelstream_proxy', |
|
87 | 87 | pattern=settings.get('channelstream.proxy_path') or '/_channelstream') |
|
88 | 88 | config.scan('rhodecode.channelstream') |
@@ -1,480 +1,480 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 | 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 scan_repositories_if_enabled |
|
56 | 56 | |
|
57 | 57 | |
|
58 | 58 | log = logging.getLogger(__name__) |
|
59 | 59 | |
|
60 | 60 | |
|
61 | 61 | # this is used to avoid avoid the route lookup overhead in routesmiddleware |
|
62 | 62 | # for certain routes which won't go to pylons to - eg. static files, debugger |
|
63 | 63 | # it is only needed for the pylons migration and can be removed once complete |
|
64 | 64 | class SkippableRoutesMiddleware(RoutesMiddleware): |
|
65 | 65 | """ Routes middleware that allows you to skip prefixes """ |
|
66 | 66 | |
|
67 | 67 | def __init__(self, *args, **kw): |
|
68 | 68 | self.skip_prefixes = kw.pop('skip_prefixes', []) |
|
69 | 69 | super(SkippableRoutesMiddleware, self).__init__(*args, **kw) |
|
70 | 70 | |
|
71 | 71 | def __call__(self, environ, start_response): |
|
72 | 72 | for prefix in self.skip_prefixes: |
|
73 | 73 | if environ['PATH_INFO'].startswith(prefix): |
|
74 | 74 | # added to avoid the case when a missing /_static route falls |
|
75 | 75 | # through to pylons and causes an exception as pylons is |
|
76 | 76 | # expecting wsgiorg.routingargs to be set in the environ |
|
77 | 77 | # by RoutesMiddleware. |
|
78 | 78 | if 'wsgiorg.routing_args' not in environ: |
|
79 | 79 | environ['wsgiorg.routing_args'] = (None, {}) |
|
80 | 80 | return self.app(environ, start_response) |
|
81 | 81 | |
|
82 | 82 | return super(SkippableRoutesMiddleware, self).__call__( |
|
83 | 83 | environ, start_response) |
|
84 | 84 | |
|
85 | 85 | |
|
86 | 86 | def make_app(global_conf, static_files=True, **app_conf): |
|
87 | 87 | """Create a Pylons WSGI application and return it |
|
88 | 88 | |
|
89 | 89 | ``global_conf`` |
|
90 | 90 | The inherited configuration for this application. Normally from |
|
91 | 91 | the [DEFAULT] section of the Paste ini file. |
|
92 | 92 | |
|
93 | 93 | ``app_conf`` |
|
94 | 94 | The application's local configuration. Normally specified in |
|
95 | 95 | the [app:<name>] section of the Paste ini file (where <name> |
|
96 | 96 | defaults to main). |
|
97 | 97 | |
|
98 | 98 | """ |
|
99 | 99 | # Apply compatibility patches |
|
100 | 100 | patches.kombu_1_5_1_python_2_7_11() |
|
101 | 101 | patches.inspect_getargspec() |
|
102 | 102 | |
|
103 | 103 | # Configure the Pylons environment |
|
104 | 104 | config = load_environment(global_conf, app_conf) |
|
105 | 105 | |
|
106 | 106 | # The Pylons WSGI app |
|
107 | 107 | app = PylonsApp(config=config) |
|
108 | 108 | if rhodecode.is_test: |
|
109 | 109 | app = csrf.CSRFDetector(app) |
|
110 | 110 | |
|
111 | 111 | expected_origin = config.get('expected_origin') |
|
112 | 112 | if expected_origin: |
|
113 | 113 | # The API can be accessed from other Origins. |
|
114 | 114 | app = csrf.OriginChecker(app, expected_origin, |
|
115 | 115 | skip_urls=[routes.util.url_for('api')]) |
|
116 | 116 | |
|
117 | 117 | # Establish the Registry for this application |
|
118 | 118 | app = RegistryManager(app) |
|
119 | 119 | |
|
120 | 120 | app.config = config |
|
121 | 121 | |
|
122 | 122 | return app |
|
123 | 123 | |
|
124 | 124 | |
|
125 | 125 | def make_pyramid_app(global_config, **settings): |
|
126 | 126 | """ |
|
127 | 127 | Constructs the WSGI application based on Pyramid and wraps the Pylons based |
|
128 | 128 | application. |
|
129 | 129 | |
|
130 | 130 | Specials: |
|
131 | 131 | |
|
132 | 132 | * We migrate from Pylons to Pyramid. While doing this, we keep both |
|
133 | 133 | frameworks functional. This involves moving some WSGI middlewares around |
|
134 | 134 | and providing access to some data internals, so that the old code is |
|
135 | 135 | still functional. |
|
136 | 136 | |
|
137 | 137 | * The application can also be integrated like a plugin via the call to |
|
138 | 138 | `includeme`. This is accompanied with the other utility functions which |
|
139 | 139 | are called. Changing this should be done with great care to not break |
|
140 | 140 | cases when these fragments are assembled from another place. |
|
141 | 141 | |
|
142 | 142 | """ |
|
143 | 143 | # The edition string should be available in pylons too, so we add it here |
|
144 | 144 | # before copying the settings. |
|
145 | 145 | settings.setdefault('rhodecode.edition', 'Community Edition') |
|
146 | 146 | |
|
147 | 147 | # As long as our Pylons application does expect "unprepared" settings, make |
|
148 | 148 | # sure that we keep an unmodified copy. This avoids unintentional change of |
|
149 | 149 | # behavior in the old application. |
|
150 | 150 | settings_pylons = settings.copy() |
|
151 | 151 | |
|
152 | 152 | sanitize_settings_and_apply_defaults(settings) |
|
153 | 153 | config = Configurator(settings=settings) |
|
154 | 154 | add_pylons_compat_data(config.registry, global_config, settings_pylons) |
|
155 | 155 | |
|
156 | 156 | load_pyramid_environment(global_config, settings) |
|
157 | 157 | |
|
158 | 158 | includeme_first(config) |
|
159 | 159 | includeme(config) |
|
160 | 160 | pyramid_app = config.make_wsgi_app() |
|
161 | 161 | pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config) |
|
162 | 162 | pyramid_app.config = config |
|
163 | 163 | |
|
164 | 164 | # creating the app uses a connection - return it after we are done |
|
165 | 165 | meta.Session.remove() |
|
166 | 166 | |
|
167 | 167 | return pyramid_app |
|
168 | 168 | |
|
169 | 169 | |
|
170 | 170 | def make_not_found_view(config): |
|
171 | 171 | """ |
|
172 | 172 | This creates the view which should be registered as not-found-view to |
|
173 | 173 | pyramid. Basically it contains of the old pylons app, converted to a view. |
|
174 | 174 | Additionally it is wrapped by some other middlewares. |
|
175 | 175 | """ |
|
176 | 176 | settings = config.registry.settings |
|
177 | 177 | vcs_server_enabled = settings['vcs.server.enable'] |
|
178 | 178 | |
|
179 | 179 | # Make pylons app from unprepared settings. |
|
180 | 180 | pylons_app = make_app( |
|
181 | 181 | config.registry._pylons_compat_global_config, |
|
182 | 182 | **config.registry._pylons_compat_settings) |
|
183 | 183 | config.registry._pylons_compat_config = pylons_app.config |
|
184 | 184 | |
|
185 | 185 | # Appenlight monitoring. |
|
186 | 186 | pylons_app, appenlight_client = wrap_in_appenlight_if_enabled( |
|
187 | 187 | pylons_app, settings) |
|
188 | 188 | |
|
189 | 189 | # The pylons app is executed inside of the pyramid 404 exception handler. |
|
190 | 190 | # Exceptions which are raised inside of it are not handled by pyramid |
|
191 | 191 | # again. Therefore we add a middleware that invokes the error handler in |
|
192 | 192 | # case of an exception or error response. This way we return proper error |
|
193 | 193 | # HTML pages in case of an error. |
|
194 | 194 | reraise = (settings.get('debugtoolbar.enabled', False) or |
|
195 | 195 | rhodecode.disable_error_handler) |
|
196 | 196 | pylons_app = PylonsErrorHandlingMiddleware( |
|
197 | 197 | pylons_app, error_handler, reraise) |
|
198 | 198 | |
|
199 | 199 | # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a |
|
200 | 200 | # view to handle the request. Therefore it is wrapped around the pylons |
|
201 | 201 | # app. It has to be outside of the error handling otherwise error responses |
|
202 | 202 | # from the vcsserver are converted to HTML error pages. This confuses the |
|
203 | 203 | # command line tools and the user won't get a meaningful error message. |
|
204 | 204 | if vcs_server_enabled: |
|
205 | 205 | pylons_app = VCSMiddleware( |
|
206 | 206 | pylons_app, settings, appenlight_client, registry=config.registry) |
|
207 | 207 | |
|
208 | 208 | # Convert WSGI app to pyramid view and return it. |
|
209 | 209 | return wsgiapp(pylons_app) |
|
210 | 210 | |
|
211 | 211 | |
|
212 | 212 | def add_pylons_compat_data(registry, global_config, settings): |
|
213 | 213 | """ |
|
214 | 214 | Attach data to the registry to support the Pylons integration. |
|
215 | 215 | """ |
|
216 | 216 | registry._pylons_compat_global_config = global_config |
|
217 | 217 | registry._pylons_compat_settings = settings |
|
218 | 218 | |
|
219 | 219 | |
|
220 | 220 | def error_handler(exception, request): |
|
221 | 221 | from rhodecode.model.settings import SettingsModel |
|
222 | 222 | from rhodecode.lib.utils2 import AttributeDict |
|
223 | 223 | |
|
224 | 224 | try: |
|
225 | 225 | rc_config = SettingsModel().get_all_settings() |
|
226 | 226 | except Exception: |
|
227 | 227 | log.exception('failed to fetch settings') |
|
228 | 228 | rc_config = {} |
|
229 | 229 | |
|
230 | 230 | base_response = HTTPInternalServerError() |
|
231 | 231 | # prefer original exception for the response since it may have headers set |
|
232 | 232 | if isinstance(exception, HTTPError): |
|
233 | 233 | base_response = exception |
|
234 | 234 | |
|
235 | 235 | c = AttributeDict() |
|
236 | 236 | c.error_message = base_response.status |
|
237 | 237 | c.error_explanation = base_response.explanation or str(base_response) |
|
238 | 238 | c.visual = AttributeDict() |
|
239 | 239 | |
|
240 | 240 | c.visual.rhodecode_support_url = ( |
|
241 | 241 | request.registry.settings.get('rhodecode_support_url') or |
|
242 | 242 | request.route_url('rhodecode_support') |
|
243 | 243 | ) |
|
244 | 244 | c.redirect_time = 0 |
|
245 | 245 | c.rhodecode_name = rc_config.get('rhodecode_title', '') |
|
246 | 246 | if not c.rhodecode_name: |
|
247 | 247 | c.rhodecode_name = 'Rhodecode' |
|
248 | 248 | |
|
249 | 249 | c.causes = [] |
|
250 | 250 | if hasattr(base_response, 'causes'): |
|
251 | 251 | c.causes = base_response.causes |
|
252 | 252 | |
|
253 | 253 | response = render_to_response( |
|
254 |
'/errors/error_document. |
|
|
254 | '/errors/error_document.mako', {'c': c}, request=request, | |
|
255 | 255 | response=base_response) |
|
256 | 256 | |
|
257 | 257 | return response |
|
258 | 258 | |
|
259 | 259 | |
|
260 | 260 | def includeme(config): |
|
261 | 261 | settings = config.registry.settings |
|
262 | 262 | |
|
263 | 263 | # plugin information |
|
264 | 264 | config.registry.rhodecode_plugins = OrderedDict() |
|
265 | 265 | |
|
266 | 266 | config.add_directive( |
|
267 | 267 | 'register_rhodecode_plugin', register_rhodecode_plugin) |
|
268 | 268 | |
|
269 | 269 | if asbool(settings.get('appenlight', 'false')): |
|
270 | 270 | config.include('appenlight_client.ext.pyramid_tween') |
|
271 | 271 | |
|
272 | 272 | # Includes which are required. The application would fail without them. |
|
273 | 273 | config.include('pyramid_mako') |
|
274 | 274 | config.include('pyramid_beaker') |
|
275 | 275 | config.include('rhodecode.channelstream') |
|
276 | 276 | config.include('rhodecode.admin') |
|
277 | 277 | config.include('rhodecode.authentication') |
|
278 | 278 | config.include('rhodecode.integrations') |
|
279 | 279 | config.include('rhodecode.login') |
|
280 | 280 | config.include('rhodecode.tweens') |
|
281 | 281 | config.include('rhodecode.api') |
|
282 | 282 | config.include('rhodecode.svn_support') |
|
283 | 283 | config.add_route( |
|
284 | 284 | 'rhodecode_support', 'https://rhodecode.com/help/', static=True) |
|
285 | 285 | |
|
286 | 286 | # Add subscribers. |
|
287 | 287 | config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated) |
|
288 | 288 | |
|
289 | 289 | # Set the authorization policy. |
|
290 | 290 | authz_policy = ACLAuthorizationPolicy() |
|
291 | 291 | config.set_authorization_policy(authz_policy) |
|
292 | 292 | |
|
293 | 293 | # Set the default renderer for HTML templates to mako. |
|
294 | 294 | config.add_mako_renderer('.html') |
|
295 | 295 | |
|
296 | 296 | # include RhodeCode plugins |
|
297 | 297 | includes = aslist(settings.get('rhodecode.includes', [])) |
|
298 | 298 | for inc in includes: |
|
299 | 299 | config.include(inc) |
|
300 | 300 | |
|
301 | 301 | # This is the glue which allows us to migrate in chunks. By registering the |
|
302 | 302 | # pylons based application as the "Not Found" view in Pyramid, we will |
|
303 | 303 | # fallback to the old application each time the new one does not yet know |
|
304 | 304 | # how to handle a request. |
|
305 | 305 | config.add_notfound_view(make_not_found_view(config)) |
|
306 | 306 | |
|
307 | 307 | if not settings.get('debugtoolbar.enabled', False): |
|
308 | 308 | # if no toolbar, then any exception gets caught and rendered |
|
309 | 309 | config.add_view(error_handler, context=Exception) |
|
310 | 310 | |
|
311 | 311 | config.add_view(error_handler, context=HTTPError) |
|
312 | 312 | |
|
313 | 313 | |
|
314 | 314 | def includeme_first(config): |
|
315 | 315 | # redirect automatic browser favicon.ico requests to correct place |
|
316 | 316 | def favicon_redirect(context, request): |
|
317 | 317 | return HTTPFound( |
|
318 | 318 | request.static_path('rhodecode:public/images/favicon.ico')) |
|
319 | 319 | |
|
320 | 320 | config.add_view(favicon_redirect, route_name='favicon') |
|
321 | 321 | config.add_route('favicon', '/favicon.ico') |
|
322 | 322 | |
|
323 | 323 | config.add_static_view( |
|
324 | 324 | '_static/deform', 'deform:static') |
|
325 | 325 | config.add_static_view( |
|
326 | 326 | '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24) |
|
327 | 327 | |
|
328 | 328 | |
|
329 | 329 | def wrap_app_in_wsgi_middlewares(pyramid_app, config): |
|
330 | 330 | """ |
|
331 | 331 | Apply outer WSGI middlewares around the application. |
|
332 | 332 | |
|
333 | 333 | Part of this has been moved up from the Pylons layer, so that the |
|
334 | 334 | data is also available if old Pylons code is hit through an already ported |
|
335 | 335 | view. |
|
336 | 336 | """ |
|
337 | 337 | settings = config.registry.settings |
|
338 | 338 | |
|
339 | 339 | # enable https redirects based on HTTP_X_URL_SCHEME set by proxy |
|
340 | 340 | pyramid_app = HttpsFixup(pyramid_app, settings) |
|
341 | 341 | |
|
342 | 342 | # Add RoutesMiddleware to support the pylons compatibility tween during |
|
343 | 343 | # migration to pyramid. |
|
344 | 344 | pyramid_app = SkippableRoutesMiddleware( |
|
345 | 345 | pyramid_app, config.registry._pylons_compat_config['routes.map'], |
|
346 | 346 | skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar')) |
|
347 | 347 | |
|
348 | 348 | pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings) |
|
349 | 349 | |
|
350 | 350 | if settings['gzip_responses']: |
|
351 | 351 | pyramid_app = make_gzip_middleware( |
|
352 | 352 | pyramid_app, settings, compress_level=1) |
|
353 | 353 | |
|
354 | 354 | |
|
355 | 355 | # this should be the outer most middleware in the wsgi stack since |
|
356 | 356 | # middleware like Routes make database calls |
|
357 | 357 | def pyramid_app_with_cleanup(environ, start_response): |
|
358 | 358 | try: |
|
359 | 359 | return pyramid_app(environ, start_response) |
|
360 | 360 | finally: |
|
361 | 361 | # Dispose current database session and rollback uncommitted |
|
362 | 362 | # transactions. |
|
363 | 363 | meta.Session.remove() |
|
364 | 364 | |
|
365 | 365 | # In a single threaded mode server, on non sqlite db we should have |
|
366 | 366 | # '0 Current Checked out connections' at the end of a request, |
|
367 | 367 | # if not, then something, somewhere is leaving a connection open |
|
368 | 368 | pool = meta.Base.metadata.bind.engine.pool |
|
369 | 369 | log.debug('sa pool status: %s', pool.status()) |
|
370 | 370 | |
|
371 | 371 | |
|
372 | 372 | return pyramid_app_with_cleanup |
|
373 | 373 | |
|
374 | 374 | |
|
375 | 375 | def sanitize_settings_and_apply_defaults(settings): |
|
376 | 376 | """ |
|
377 | 377 | Applies settings defaults and does all type conversion. |
|
378 | 378 | |
|
379 | 379 | We would move all settings parsing and preparation into this place, so that |
|
380 | 380 | we have only one place left which deals with this part. The remaining parts |
|
381 | 381 | of the application would start to rely fully on well prepared settings. |
|
382 | 382 | |
|
383 | 383 | This piece would later be split up per topic to avoid a big fat monster |
|
384 | 384 | function. |
|
385 | 385 | """ |
|
386 | 386 | |
|
387 | 387 | # Pyramid's mako renderer has to search in the templates folder so that the |
|
388 | 388 | # old templates still work. Ported and new templates are expected to use |
|
389 | 389 | # real asset specifications for the includes. |
|
390 | 390 | mako_directories = settings.setdefault('mako.directories', [ |
|
391 | 391 | # Base templates of the original Pylons application |
|
392 | 392 | 'rhodecode:templates', |
|
393 | 393 | ]) |
|
394 | 394 | log.debug( |
|
395 | 395 | "Using the following Mako template directories: %s", |
|
396 | 396 | mako_directories) |
|
397 | 397 | |
|
398 | 398 | # Default includes, possible to change as a user |
|
399 | 399 | pyramid_includes = settings.setdefault('pyramid.includes', [ |
|
400 | 400 | 'rhodecode.lib.middleware.request_wrapper', |
|
401 | 401 | ]) |
|
402 | 402 | log.debug( |
|
403 | 403 | "Using the following pyramid.includes: %s", |
|
404 | 404 | pyramid_includes) |
|
405 | 405 | |
|
406 | 406 | # TODO: johbo: Re-think this, usually the call to config.include |
|
407 | 407 | # should allow to pass in a prefix. |
|
408 | 408 | settings.setdefault('rhodecode.api.url', '/_admin/api') |
|
409 | 409 | |
|
410 | 410 | # Sanitize generic settings. |
|
411 | 411 | _list_setting(settings, 'default_encoding', 'UTF-8') |
|
412 | 412 | _bool_setting(settings, 'is_test', 'false') |
|
413 | 413 | _bool_setting(settings, 'gzip_responses', 'false') |
|
414 | 414 | |
|
415 | 415 | # Call split out functions that sanitize settings for each topic. |
|
416 | 416 | _sanitize_appenlight_settings(settings) |
|
417 | 417 | _sanitize_vcs_settings(settings) |
|
418 | 418 | |
|
419 | 419 | return settings |
|
420 | 420 | |
|
421 | 421 | |
|
422 | 422 | def _sanitize_appenlight_settings(settings): |
|
423 | 423 | _bool_setting(settings, 'appenlight', 'false') |
|
424 | 424 | |
|
425 | 425 | |
|
426 | 426 | def _sanitize_vcs_settings(settings): |
|
427 | 427 | """ |
|
428 | 428 | Applies settings defaults and does type conversion for all VCS related |
|
429 | 429 | settings. |
|
430 | 430 | """ |
|
431 | 431 | _string_setting(settings, 'vcs.svn.compatible_version', '') |
|
432 | 432 | _string_setting(settings, 'git_rev_filter', '--all') |
|
433 | 433 | _string_setting(settings, 'vcs.hooks.protocol', 'http') |
|
434 | 434 | _string_setting(settings, 'vcs.scm_app_implementation', 'http') |
|
435 | 435 | _string_setting(settings, 'vcs.server', '') |
|
436 | 436 | _string_setting(settings, 'vcs.server.log_level', 'debug') |
|
437 | 437 | _string_setting(settings, 'vcs.server.protocol', 'http') |
|
438 | 438 | _bool_setting(settings, 'startup.import_repos', 'false') |
|
439 | 439 | _bool_setting(settings, 'vcs.hooks.direct_calls', 'false') |
|
440 | 440 | _bool_setting(settings, 'vcs.server.enable', 'true') |
|
441 | 441 | _bool_setting(settings, 'vcs.start_server', 'false') |
|
442 | 442 | _list_setting(settings, 'vcs.backends', 'hg, git, svn') |
|
443 | 443 | _int_setting(settings, 'vcs.connection_timeout', 3600) |
|
444 | 444 | |
|
445 | 445 | # Support legacy values of vcs.scm_app_implementation. Legacy |
|
446 | 446 | # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http' |
|
447 | 447 | # which is now mapped to 'http'. |
|
448 | 448 | scm_app_impl = settings['vcs.scm_app_implementation'] |
|
449 | 449 | if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http': |
|
450 | 450 | settings['vcs.scm_app_implementation'] = 'http' |
|
451 | 451 | |
|
452 | 452 | |
|
453 | 453 | def _int_setting(settings, name, default): |
|
454 | 454 | settings[name] = int(settings.get(name, default)) |
|
455 | 455 | |
|
456 | 456 | |
|
457 | 457 | def _bool_setting(settings, name, default): |
|
458 | 458 | input = settings.get(name, default) |
|
459 | 459 | if isinstance(input, unicode): |
|
460 | 460 | input = input.encode('utf8') |
|
461 | 461 | settings[name] = asbool(input) |
|
462 | 462 | |
|
463 | 463 | |
|
464 | 464 | def _list_setting(settings, name, default): |
|
465 | 465 | raw_value = settings.get(name, default) |
|
466 | 466 | |
|
467 | 467 | old_separator = ',' |
|
468 | 468 | if old_separator in raw_value: |
|
469 | 469 | # If we get a comma separated list, pass it to our own function. |
|
470 | 470 | settings[name] = rhodecode_aslist(raw_value, sep=old_separator) |
|
471 | 471 | else: |
|
472 | 472 | # Otherwise we assume it uses pyramids space/newline separation. |
|
473 | 473 | settings[name] = aslist(raw_value) |
|
474 | 474 | |
|
475 | 475 | |
|
476 | 476 | def _string_setting(settings, name, default, lower=True): |
|
477 | 477 | value = settings.get(name, default) |
|
478 | 478 | if lower: |
|
479 | 479 | value = value.lower() |
|
480 | 480 | settings[name] = value |
@@ -1,173 +1,173 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 | Controller for Admin panel of RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | |
|
28 | 28 | from pylons import request, tmpl_context as c, url |
|
29 | 29 | from pylons.controllers.util import redirect |
|
30 | 30 | from sqlalchemy.orm import joinedload |
|
31 | 31 | from whoosh.qparser.default import QueryParser, query |
|
32 | 32 | from whoosh.qparser.dateparse import DateParserPlugin |
|
33 | 33 | from whoosh.fields import (TEXT, Schema, DATETIME) |
|
34 | 34 | from sqlalchemy.sql.expression import or_, and_, func |
|
35 | 35 | |
|
36 | 36 | from rhodecode.model.db import UserLog, PullRequest |
|
37 | 37 | from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator |
|
38 | 38 | from rhodecode.lib.base import BaseController, render |
|
39 | 39 | from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix |
|
40 | 40 | from rhodecode.lib.helpers import Page |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | log = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh |
|
46 | 46 | # querylang to build sql queries and filter journals |
|
47 | 47 | JOURNAL_SCHEMA = Schema( |
|
48 | 48 | username=TEXT(), |
|
49 | 49 | date=DATETIME(), |
|
50 | 50 | action=TEXT(), |
|
51 | 51 | repository=TEXT(), |
|
52 | 52 | ip=TEXT(), |
|
53 | 53 | ) |
|
54 | 54 | |
|
55 | 55 | |
|
56 | 56 | def _journal_filter(user_log, search_term): |
|
57 | 57 | """ |
|
58 | 58 | Filters sqlalchemy user_log based on search_term with whoosh Query language |
|
59 | 59 | http://packages.python.org/Whoosh/querylang.html |
|
60 | 60 | |
|
61 | 61 | :param user_log: |
|
62 | 62 | :param search_term: |
|
63 | 63 | """ |
|
64 | 64 | log.debug('Initial search term: %r' % search_term) |
|
65 | 65 | qry = None |
|
66 | 66 | if search_term: |
|
67 | 67 | qp = QueryParser('repository', schema=JOURNAL_SCHEMA) |
|
68 | 68 | qp.add_plugin(DateParserPlugin()) |
|
69 | 69 | qry = qp.parse(unicode(search_term)) |
|
70 | 70 | log.debug('Filtering using parsed query %r' % qry) |
|
71 | 71 | |
|
72 | 72 | def wildcard_handler(col, wc_term): |
|
73 | 73 | if wc_term.startswith('*') and not wc_term.endswith('*'): |
|
74 | 74 | # postfix == endswith |
|
75 | 75 | wc_term = remove_prefix(wc_term, prefix='*') |
|
76 | 76 | return func.lower(col).endswith(wc_term) |
|
77 | 77 | elif wc_term.startswith('*') and wc_term.endswith('*'): |
|
78 | 78 | # wildcard == ilike |
|
79 | 79 | wc_term = remove_prefix(wc_term, prefix='*') |
|
80 | 80 | wc_term = remove_suffix(wc_term, suffix='*') |
|
81 | 81 | return func.lower(col).contains(wc_term) |
|
82 | 82 | |
|
83 | 83 | def get_filterion(field, val, term): |
|
84 | 84 | |
|
85 | 85 | if field == 'repository': |
|
86 | 86 | field = getattr(UserLog, 'repository_name') |
|
87 | 87 | elif field == 'ip': |
|
88 | 88 | field = getattr(UserLog, 'user_ip') |
|
89 | 89 | elif field == 'date': |
|
90 | 90 | field = getattr(UserLog, 'action_date') |
|
91 | 91 | elif field == 'username': |
|
92 | 92 | field = getattr(UserLog, 'username') |
|
93 | 93 | else: |
|
94 | 94 | field = getattr(UserLog, field) |
|
95 | 95 | log.debug('filter field: %s val=>%s' % (field, val)) |
|
96 | 96 | |
|
97 | 97 | # sql filtering |
|
98 | 98 | if isinstance(term, query.Wildcard): |
|
99 | 99 | return wildcard_handler(field, val) |
|
100 | 100 | elif isinstance(term, query.Prefix): |
|
101 | 101 | return func.lower(field).startswith(func.lower(val)) |
|
102 | 102 | elif isinstance(term, query.DateRange): |
|
103 | 103 | return and_(field >= val[0], field <= val[1]) |
|
104 | 104 | return func.lower(field) == func.lower(val) |
|
105 | 105 | |
|
106 | 106 | if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard, |
|
107 | 107 | query.DateRange)): |
|
108 | 108 | if not isinstance(qry, query.And): |
|
109 | 109 | qry = [qry] |
|
110 | 110 | for term in qry: |
|
111 | 111 | field = term.fieldname |
|
112 | 112 | val = (term.text if not isinstance(term, query.DateRange) |
|
113 | 113 | else [term.startdate, term.enddate]) |
|
114 | 114 | user_log = user_log.filter(get_filterion(field, val, term)) |
|
115 | 115 | elif isinstance(qry, query.Or): |
|
116 | 116 | filters = [] |
|
117 | 117 | for term in qry: |
|
118 | 118 | field = term.fieldname |
|
119 | 119 | val = (term.text if not isinstance(term, query.DateRange) |
|
120 | 120 | else [term.startdate, term.enddate]) |
|
121 | 121 | filters.append(get_filterion(field, val, term)) |
|
122 | 122 | user_log = user_log.filter(or_(*filters)) |
|
123 | 123 | |
|
124 | 124 | return user_log |
|
125 | 125 | |
|
126 | 126 | |
|
127 | 127 | class AdminController(BaseController): |
|
128 | 128 | |
|
129 | 129 | @LoginRequired() |
|
130 | 130 | def __before__(self): |
|
131 | 131 | super(AdminController, self).__before__() |
|
132 | 132 | |
|
133 | 133 | @HasPermissionAllDecorator('hg.admin') |
|
134 | 134 | def index(self): |
|
135 | 135 | users_log = UserLog.query()\ |
|
136 | 136 | .options(joinedload(UserLog.user))\ |
|
137 | 137 | .options(joinedload(UserLog.repository)) |
|
138 | 138 | |
|
139 | 139 | # FILTERING |
|
140 | 140 | c.search_term = request.GET.get('filter') |
|
141 | 141 | try: |
|
142 | 142 | users_log = _journal_filter(users_log, c.search_term) |
|
143 | 143 | except Exception: |
|
144 | 144 | # we want this to crash for now |
|
145 | 145 | raise |
|
146 | 146 | |
|
147 | 147 | users_log = users_log.order_by(UserLog.action_date.desc()) |
|
148 | 148 | |
|
149 | 149 | p = safe_int(request.GET.get('page', 1), 1) |
|
150 | 150 | |
|
151 | 151 | def url_generator(**kw): |
|
152 | 152 | return url.current(filter=c.search_term, **kw) |
|
153 | 153 | |
|
154 | 154 | c.users_log = Page(users_log, page=p, items_per_page=10, |
|
155 | 155 | url=url_generator) |
|
156 |
c.log_data = render('admin/admin_log. |
|
|
156 | c.log_data = render('admin/admin_log.mako') | |
|
157 | 157 | |
|
158 | 158 | if request.is_xhr: |
|
159 | 159 | return c.log_data |
|
160 |
return render('admin/admin. |
|
|
160 | return render('admin/admin.mako') | |
|
161 | 161 | |
|
162 | 162 | # global redirect doesn't need permissions |
|
163 | 163 | def pull_requests(self, pull_request_id): |
|
164 | 164 | """ |
|
165 | 165 | Global redirect for Pull Requests |
|
166 | 166 | |
|
167 | 167 | :param pull_request_id: id of pull requests in the system |
|
168 | 168 | """ |
|
169 | 169 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
170 | 170 | repo_name = pull_request.target_repo.repo_name |
|
171 | 171 | return redirect(url( |
|
172 | 172 | 'pullrequest_show', repo_name=repo_name, |
|
173 | 173 | pull_request_id=pull_request_id)) |
@@ -1,102 +1,102 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 | default settings controller for RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import formencode |
|
27 | 27 | from formencode import htmlfill |
|
28 | 28 | |
|
29 | 29 | from pylons import request, tmpl_context as c, url |
|
30 | 30 | from pylons.controllers.util import redirect |
|
31 | 31 | from pylons.i18n.translation import _ |
|
32 | 32 | |
|
33 | 33 | from rhodecode.lib import auth |
|
34 | 34 | from rhodecode.lib import helpers as h |
|
35 | 35 | from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator |
|
36 | 36 | from rhodecode.lib.base import BaseController, render |
|
37 | 37 | from rhodecode.model.forms import DefaultsForm |
|
38 | 38 | from rhodecode.model.meta import Session |
|
39 | 39 | from rhodecode import BACKENDS |
|
40 | 40 | from rhodecode.model.settings import SettingsModel |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | class DefaultsController(BaseController): |
|
46 | 46 | |
|
47 | 47 | @LoginRequired() |
|
48 | 48 | def __before__(self): |
|
49 | 49 | super(DefaultsController, self).__before__() |
|
50 | 50 | |
|
51 | 51 | @HasPermissionAllDecorator('hg.admin') |
|
52 | 52 | def index(self): |
|
53 | 53 | """GET /defaults: All items in the collection""" |
|
54 | 54 | # url('admin_defaults_repositories') |
|
55 | 55 | c.backends = BACKENDS.keys() |
|
56 | 56 | c.active = 'repositories' |
|
57 | 57 | defaults = SettingsModel().get_default_repo_settings() |
|
58 | 58 | |
|
59 | 59 | return htmlfill.render( |
|
60 |
render('admin/defaults/defaults. |
|
|
60 | render('admin/defaults/defaults.mako'), | |
|
61 | 61 | defaults=defaults, |
|
62 | 62 | encoding="UTF-8", |
|
63 | 63 | force_defaults=False |
|
64 | 64 | ) |
|
65 | 65 | |
|
66 | 66 | @HasPermissionAllDecorator('hg.admin') |
|
67 | 67 | @auth.CSRFRequired() |
|
68 | 68 | def update_repository_defaults(self): |
|
69 | 69 | """PUT /defaults/repositories: Update an existing item""" |
|
70 | 70 | # Forms posted to this method should contain a hidden field: |
|
71 | 71 | # Or using helpers: |
|
72 | 72 | # h.form(url('admin_defaults_repositories'), |
|
73 | 73 | # method='post') |
|
74 | 74 | # url('admin_defaults_repositories') |
|
75 | 75 | c.active = 'repositories' |
|
76 | 76 | _form = DefaultsForm()() |
|
77 | 77 | |
|
78 | 78 | try: |
|
79 | 79 | form_result = _form.to_python(dict(request.POST)) |
|
80 | 80 | for k, v in form_result.iteritems(): |
|
81 | 81 | setting = SettingsModel().create_or_update_setting(k, v) |
|
82 | 82 | Session().add(setting) |
|
83 | 83 | Session().commit() |
|
84 | 84 | h.flash(_('Default settings updated successfully'), |
|
85 | 85 | category='success') |
|
86 | 86 | |
|
87 | 87 | except formencode.Invalid as errors: |
|
88 | 88 | defaults = errors.value |
|
89 | 89 | |
|
90 | 90 | return htmlfill.render( |
|
91 |
render('admin/defaults/defaults. |
|
|
91 | render('admin/defaults/defaults.mako'), | |
|
92 | 92 | defaults=defaults, |
|
93 | 93 | errors=errors.error_dict or {}, |
|
94 | 94 | prefix_error=False, |
|
95 | 95 | encoding="UTF-8", |
|
96 | 96 | force_defaults=False) |
|
97 | 97 | except Exception: |
|
98 | 98 | log.exception('Exception in update action') |
|
99 | 99 | h.flash(_('Error occurred during update of default values'), |
|
100 | 100 | category='error') |
|
101 | 101 | |
|
102 | 102 | return redirect(url('admin_defaults_repositories')) |
@@ -1,365 +1,365 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 | gist controller for RhodeCode |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import time |
|
27 | 27 | import logging |
|
28 | 28 | |
|
29 | 29 | import formencode |
|
30 | 30 | import peppercorn |
|
31 | 31 | |
|
32 | 32 | from pylons import request, response, tmpl_context as c, url |
|
33 | 33 | from pylons.controllers.util import redirect |
|
34 | 34 | from pylons.i18n.translation import _ |
|
35 | 35 | from webob.exc import HTTPNotFound, HTTPForbidden |
|
36 | 36 | from sqlalchemy.sql.expression import or_ |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | from rhodecode.model.gist import GistModel |
|
40 | 40 | from rhodecode.model.meta import Session |
|
41 | 41 | from rhodecode.model.db import Gist, User |
|
42 | 42 | from rhodecode.lib import auth |
|
43 | 43 | from rhodecode.lib import helpers as h |
|
44 | 44 | from rhodecode.lib.base import BaseController, render |
|
45 | 45 | from rhodecode.lib.auth import LoginRequired, NotAnonymous |
|
46 | 46 | from rhodecode.lib.utils import jsonify |
|
47 | 47 | from rhodecode.lib.utils2 import time_to_datetime |
|
48 | 48 | from rhodecode.lib.ext_json import json |
|
49 | 49 | from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError |
|
50 | 50 | from rhodecode.model import validation_schema |
|
51 | 51 | from rhodecode.model.validation_schema.schemas import gist_schema |
|
52 | 52 | |
|
53 | 53 | |
|
54 | 54 | log = logging.getLogger(__name__) |
|
55 | 55 | |
|
56 | 56 | |
|
57 | 57 | class GistsController(BaseController): |
|
58 | 58 | """REST Controller styled on the Atom Publishing Protocol""" |
|
59 | 59 | |
|
60 | 60 | def __load_defaults(self, extra_values=None): |
|
61 | 61 | c.lifetime_values = [ |
|
62 | 62 | (-1, _('forever')), |
|
63 | 63 | (5, _('5 minutes')), |
|
64 | 64 | (60, _('1 hour')), |
|
65 | 65 | (60 * 24, _('1 day')), |
|
66 | 66 | (60 * 24 * 30, _('1 month')), |
|
67 | 67 | ] |
|
68 | 68 | if extra_values: |
|
69 | 69 | c.lifetime_values.append(extra_values) |
|
70 | 70 | c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] |
|
71 | 71 | c.acl_options = [ |
|
72 | 72 | (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")), |
|
73 | 73 | (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users")) |
|
74 | 74 | ] |
|
75 | 75 | |
|
76 | 76 | @LoginRequired() |
|
77 | 77 | def index(self): |
|
78 | 78 | """GET /admin/gists: All items in the collection""" |
|
79 | 79 | # url('gists') |
|
80 | 80 | not_default_user = c.rhodecode_user.username != User.DEFAULT_USER |
|
81 | 81 | c.show_private = request.GET.get('private') and not_default_user |
|
82 | 82 | c.show_public = request.GET.get('public') and not_default_user |
|
83 | 83 | c.show_all = request.GET.get('all') and c.rhodecode_user.admin |
|
84 | 84 | |
|
85 | 85 | gists = _gists = Gist().query()\ |
|
86 | 86 | .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\ |
|
87 | 87 | .order_by(Gist.created_on.desc()) |
|
88 | 88 | |
|
89 | 89 | c.active = 'public' |
|
90 | 90 | # MY private |
|
91 | 91 | if c.show_private and not c.show_public: |
|
92 | 92 | gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\ |
|
93 | 93 | .filter(Gist.gist_owner == c.rhodecode_user.user_id) |
|
94 | 94 | c.active = 'my_private' |
|
95 | 95 | # MY public |
|
96 | 96 | elif c.show_public and not c.show_private: |
|
97 | 97 | gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\ |
|
98 | 98 | .filter(Gist.gist_owner == c.rhodecode_user.user_id) |
|
99 | 99 | c.active = 'my_public' |
|
100 | 100 | # MY public+private |
|
101 | 101 | elif c.show_private and c.show_public: |
|
102 | 102 | gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC, |
|
103 | 103 | Gist.gist_type == Gist.GIST_PRIVATE))\ |
|
104 | 104 | .filter(Gist.gist_owner == c.rhodecode_user.user_id) |
|
105 | 105 | c.active = 'my_all' |
|
106 | 106 | # Show all by super-admin |
|
107 | 107 | elif c.show_all: |
|
108 | 108 | c.active = 'all' |
|
109 | 109 | gists = _gists |
|
110 | 110 | |
|
111 | 111 | # default show ALL public gists |
|
112 | 112 | if not c.show_public and not c.show_private and not c.show_all: |
|
113 | 113 | gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) |
|
114 | 114 | c.active = 'public' |
|
115 | 115 | |
|
116 | 116 | from rhodecode.lib.utils import PartialRenderer |
|
117 |
_render = PartialRenderer('data_table/_dt_elements. |
|
|
117 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
|
118 | 118 | |
|
119 | 119 | data = [] |
|
120 | 120 | |
|
121 | 121 | for gist in gists: |
|
122 | 122 | data.append({ |
|
123 | 123 | 'created_on': _render('gist_created', gist.created_on), |
|
124 | 124 | 'created_on_raw': gist.created_on, |
|
125 | 125 | 'type': _render('gist_type', gist.gist_type), |
|
126 | 126 | 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact), |
|
127 | 127 | 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires), |
|
128 | 128 | 'author_raw': h.escape(gist.owner.full_contact), |
|
129 | 129 | 'expires': _render('gist_expires', gist.gist_expires), |
|
130 | 130 | 'description': _render('gist_description', gist.gist_description) |
|
131 | 131 | }) |
|
132 | 132 | c.data = json.dumps(data) |
|
133 |
return render('admin/gists/index. |
|
|
133 | return render('admin/gists/index.mako') | |
|
134 | 134 | |
|
135 | 135 | @LoginRequired() |
|
136 | 136 | @NotAnonymous() |
|
137 | 137 | @auth.CSRFRequired() |
|
138 | 138 | def create(self): |
|
139 | 139 | """POST /admin/gists: Create a new item""" |
|
140 | 140 | # url('gists') |
|
141 | 141 | self.__load_defaults() |
|
142 | 142 | |
|
143 | 143 | data = dict(request.POST) |
|
144 | 144 | data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME |
|
145 | 145 | data['nodes'] = [{ |
|
146 | 146 | 'filename': data['filename'], |
|
147 | 147 | 'content': data.get('content'), |
|
148 | 148 | 'mimetype': data.get('mimetype') # None is autodetect |
|
149 | 149 | }] |
|
150 | 150 | |
|
151 | 151 | data['gist_type'] = ( |
|
152 | 152 | Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE) |
|
153 | 153 | data['gist_acl_level'] = ( |
|
154 | 154 | data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE) |
|
155 | 155 | |
|
156 | 156 | schema = gist_schema.GistSchema().bind( |
|
157 | 157 | lifetime_options=[x[0] for x in c.lifetime_values]) |
|
158 | 158 | |
|
159 | 159 | try: |
|
160 | 160 | |
|
161 | 161 | schema_data = schema.deserialize(data) |
|
162 | 162 | # convert to safer format with just KEYs so we sure no duplicates |
|
163 | 163 | schema_data['nodes'] = gist_schema.sequence_to_nodes( |
|
164 | 164 | schema_data['nodes']) |
|
165 | 165 | |
|
166 | 166 | gist = GistModel().create( |
|
167 | 167 | gist_id=schema_data['gistid'], # custom access id not real ID |
|
168 | 168 | description=schema_data['description'], |
|
169 | 169 | owner=c.rhodecode_user.user_id, |
|
170 | 170 | gist_mapping=schema_data['nodes'], |
|
171 | 171 | gist_type=schema_data['gist_type'], |
|
172 | 172 | lifetime=schema_data['lifetime'], |
|
173 | 173 | gist_acl_level=schema_data['gist_acl_level'] |
|
174 | 174 | ) |
|
175 | 175 | Session().commit() |
|
176 | 176 | new_gist_id = gist.gist_access_id |
|
177 | 177 | except validation_schema.Invalid as errors: |
|
178 | 178 | defaults = data |
|
179 | 179 | errors = errors.asdict() |
|
180 | 180 | |
|
181 | 181 | if 'nodes.0.content' in errors: |
|
182 | 182 | errors['content'] = errors['nodes.0.content'] |
|
183 | 183 | del errors['nodes.0.content'] |
|
184 | 184 | if 'nodes.0.filename' in errors: |
|
185 | 185 | errors['filename'] = errors['nodes.0.filename'] |
|
186 | 186 | del errors['nodes.0.filename'] |
|
187 | 187 | |
|
188 | 188 | return formencode.htmlfill.render( |
|
189 |
render('admin/gists/new. |
|
|
189 | render('admin/gists/new.mako'), | |
|
190 | 190 | defaults=defaults, |
|
191 | 191 | errors=errors, |
|
192 | 192 | prefix_error=False, |
|
193 | 193 | encoding="UTF-8", |
|
194 | 194 | force_defaults=False |
|
195 | 195 | ) |
|
196 | 196 | |
|
197 | 197 | except Exception: |
|
198 | 198 | log.exception("Exception while trying to create a gist") |
|
199 | 199 | h.flash(_('Error occurred during gist creation'), category='error') |
|
200 | 200 | return redirect(url('new_gist')) |
|
201 | 201 | return redirect(url('gist', gist_id=new_gist_id)) |
|
202 | 202 | |
|
203 | 203 | @LoginRequired() |
|
204 | 204 | @NotAnonymous() |
|
205 | 205 | def new(self, format='html'): |
|
206 | 206 | """GET /admin/gists/new: Form to create a new item""" |
|
207 | 207 | # url('new_gist') |
|
208 | 208 | self.__load_defaults() |
|
209 |
return render('admin/gists/new. |
|
|
209 | return render('admin/gists/new.mako') | |
|
210 | 210 | |
|
211 | 211 | @LoginRequired() |
|
212 | 212 | @NotAnonymous() |
|
213 | 213 | @auth.CSRFRequired() |
|
214 | 214 | def delete(self, gist_id): |
|
215 | 215 | """DELETE /admin/gists/gist_id: Delete an existing item""" |
|
216 | 216 | # Forms posted to this method should contain a hidden field: |
|
217 | 217 | # <input type="hidden" name="_method" value="DELETE" /> |
|
218 | 218 | # Or using helpers: |
|
219 | 219 | # h.form(url('gist', gist_id=ID), |
|
220 | 220 | # method='delete') |
|
221 | 221 | # url('gist', gist_id=ID) |
|
222 | 222 | c.gist = Gist.get_or_404(gist_id) |
|
223 | 223 | |
|
224 | 224 | owner = c.gist.gist_owner == c.rhodecode_user.user_id |
|
225 | 225 | if not (h.HasPermissionAny('hg.admin')() or owner): |
|
226 | 226 | raise HTTPForbidden() |
|
227 | 227 | |
|
228 | 228 | GistModel().delete(c.gist) |
|
229 | 229 | Session().commit() |
|
230 | 230 | h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success') |
|
231 | 231 | |
|
232 | 232 | return redirect(url('gists')) |
|
233 | 233 | |
|
234 | 234 | def _add_gist_to_context(self, gist_id): |
|
235 | 235 | c.gist = Gist.get_or_404(gist_id) |
|
236 | 236 | |
|
237 | 237 | # Check if this gist is expired |
|
238 | 238 | if c.gist.gist_expires != -1: |
|
239 | 239 | if time.time() > c.gist.gist_expires: |
|
240 | 240 | log.error( |
|
241 | 241 | 'Gist expired at %s', time_to_datetime(c.gist.gist_expires)) |
|
242 | 242 | raise HTTPNotFound() |
|
243 | 243 | |
|
244 | 244 | # check if this gist requires a login |
|
245 | 245 | is_default_user = c.rhodecode_user.username == User.DEFAULT_USER |
|
246 | 246 | if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user: |
|
247 | 247 | log.error("Anonymous user %s tried to access protected gist `%s`", |
|
248 | 248 | c.rhodecode_user, gist_id) |
|
249 | 249 | raise HTTPNotFound() |
|
250 | 250 | |
|
251 | 251 | @LoginRequired() |
|
252 | 252 | def show(self, gist_id, revision='tip', format='html', f_path=None): |
|
253 | 253 | """GET /admin/gists/gist_id: Show a specific item""" |
|
254 | 254 | # url('gist', gist_id=ID) |
|
255 | 255 | self._add_gist_to_context(gist_id) |
|
256 | 256 | c.render = not request.GET.get('no-render', False) |
|
257 | 257 | |
|
258 | 258 | try: |
|
259 | 259 | c.file_last_commit, c.files = GistModel().get_gist_files( |
|
260 | 260 | gist_id, revision=revision) |
|
261 | 261 | except VCSError: |
|
262 | 262 | log.exception("Exception in gist show") |
|
263 | 263 | raise HTTPNotFound() |
|
264 | 264 | if format == 'raw': |
|
265 | 265 | content = '\n\n'.join([f.content for f in c.files |
|
266 | 266 | if (f_path is None or f.path == f_path)]) |
|
267 | 267 | response.content_type = 'text/plain' |
|
268 | 268 | return content |
|
269 |
return render('admin/gists/show. |
|
|
269 | return render('admin/gists/show.mako') | |
|
270 | 270 | |
|
271 | 271 | @LoginRequired() |
|
272 | 272 | @NotAnonymous() |
|
273 | 273 | @auth.CSRFRequired() |
|
274 | 274 | def edit(self, gist_id): |
|
275 | 275 | self.__load_defaults() |
|
276 | 276 | self._add_gist_to_context(gist_id) |
|
277 | 277 | |
|
278 | 278 | owner = c.gist.gist_owner == c.rhodecode_user.user_id |
|
279 | 279 | if not (h.HasPermissionAny('hg.admin')() or owner): |
|
280 | 280 | raise HTTPForbidden() |
|
281 | 281 | |
|
282 | 282 | data = peppercorn.parse(request.POST.items()) |
|
283 | 283 | |
|
284 | 284 | schema = gist_schema.GistSchema() |
|
285 | 285 | schema = schema.bind( |
|
286 | 286 | # '0' is special value to leave lifetime untouched |
|
287 | 287 | lifetime_options=[x[0] for x in c.lifetime_values] + [0], |
|
288 | 288 | ) |
|
289 | 289 | |
|
290 | 290 | try: |
|
291 | 291 | schema_data = schema.deserialize(data) |
|
292 | 292 | # convert to safer format with just KEYs so we sure no duplicates |
|
293 | 293 | schema_data['nodes'] = gist_schema.sequence_to_nodes( |
|
294 | 294 | schema_data['nodes']) |
|
295 | 295 | |
|
296 | 296 | GistModel().update( |
|
297 | 297 | gist=c.gist, |
|
298 | 298 | description=schema_data['description'], |
|
299 | 299 | owner=c.gist.owner, |
|
300 | 300 | gist_mapping=schema_data['nodes'], |
|
301 | 301 | lifetime=schema_data['lifetime'], |
|
302 | 302 | gist_acl_level=schema_data['gist_acl_level'] |
|
303 | 303 | ) |
|
304 | 304 | |
|
305 | 305 | Session().commit() |
|
306 | 306 | h.flash(_('Successfully updated gist content'), category='success') |
|
307 | 307 | except NodeNotChangedError: |
|
308 | 308 | # raised if nothing was changed in repo itself. We anyway then |
|
309 | 309 | # store only DB stuff for gist |
|
310 | 310 | Session().commit() |
|
311 | 311 | h.flash(_('Successfully updated gist data'), category='success') |
|
312 | 312 | except validation_schema.Invalid as errors: |
|
313 | 313 | errors = errors.asdict() |
|
314 | 314 | h.flash(_('Error occurred during update of gist {}: {}').format( |
|
315 | 315 | gist_id, errors), category='error') |
|
316 | 316 | except Exception: |
|
317 | 317 | log.exception("Exception in gist edit") |
|
318 | 318 | h.flash(_('Error occurred during update of gist %s') % gist_id, |
|
319 | 319 | category='error') |
|
320 | 320 | |
|
321 | 321 | return redirect(url('gist', gist_id=gist_id)) |
|
322 | 322 | |
|
323 | 323 | @LoginRequired() |
|
324 | 324 | @NotAnonymous() |
|
325 | 325 | def edit_form(self, gist_id, format='html'): |
|
326 | 326 | """GET /admin/gists/gist_id/edit: Form to edit an existing item""" |
|
327 | 327 | # url('edit_gist', gist_id=ID) |
|
328 | 328 | self._add_gist_to_context(gist_id) |
|
329 | 329 | |
|
330 | 330 | owner = c.gist.gist_owner == c.rhodecode_user.user_id |
|
331 | 331 | if not (h.HasPermissionAny('hg.admin')() or owner): |
|
332 | 332 | raise HTTPForbidden() |
|
333 | 333 | |
|
334 | 334 | try: |
|
335 | 335 | c.file_last_commit, c.files = GistModel().get_gist_files(gist_id) |
|
336 | 336 | except VCSError: |
|
337 | 337 | log.exception("Exception in gist edit") |
|
338 | 338 | raise HTTPNotFound() |
|
339 | 339 | |
|
340 | 340 | if c.gist.gist_expires == -1: |
|
341 | 341 | expiry = _('never') |
|
342 | 342 | else: |
|
343 | 343 | # this cannot use timeago, since it's used in select2 as a value |
|
344 | 344 | expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) |
|
345 | 345 | self.__load_defaults( |
|
346 | 346 | extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry})) |
|
347 |
return render('admin/gists/edit. |
|
|
347 | return render('admin/gists/edit.mako') | |
|
348 | 348 | |
|
349 | 349 | @LoginRequired() |
|
350 | 350 | @NotAnonymous() |
|
351 | 351 | @jsonify |
|
352 | 352 | def check_revision(self, gist_id): |
|
353 | 353 | c.gist = Gist.get_or_404(gist_id) |
|
354 | 354 | last_rev = c.gist.scm_instance().get_commit() |
|
355 | 355 | success = True |
|
356 | 356 | revision = request.GET.get('revision') |
|
357 | 357 | |
|
358 | 358 | ##TODO: maybe move this to model ? |
|
359 | 359 | if revision != last_rev.raw_id: |
|
360 | 360 | log.error('Last revision %s is different then submitted %s' |
|
361 | 361 | % (revision, last_rev)) |
|
362 | 362 | # our gist has newer version than we |
|
363 | 363 | success = False |
|
364 | 364 | |
|
365 | 365 | return {'success': success} |
@@ -1,468 +1,468 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2013-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | |
|
22 | 22 | """ |
|
23 | 23 | my account controller for RhodeCode admin |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | import datetime |
|
28 | 28 | |
|
29 | 29 | import formencode |
|
30 | 30 | from formencode import htmlfill |
|
31 | 31 | from pyramid.threadlocal import get_current_registry |
|
32 | 32 | from pylons import request, tmpl_context as c, url, session |
|
33 | 33 | from pylons.controllers.util import redirect |
|
34 | 34 | from pylons.i18n.translation import _ |
|
35 | 35 | from sqlalchemy.orm import joinedload |
|
36 | 36 | from webob.exc import HTTPBadGateway |
|
37 | 37 | |
|
38 | 38 | from rhodecode import forms |
|
39 | 39 | from rhodecode.lib import helpers as h |
|
40 | 40 | from rhodecode.lib import auth |
|
41 | 41 | from rhodecode.lib.auth import ( |
|
42 | 42 | LoginRequired, NotAnonymous, AuthUser, generate_auth_token) |
|
43 | 43 | from rhodecode.lib.base import BaseController, render |
|
44 | 44 | from rhodecode.lib.utils import jsonify |
|
45 | 45 | from rhodecode.lib.utils2 import safe_int, md5, str2bool |
|
46 | 46 | from rhodecode.lib.ext_json import json |
|
47 | 47 | from rhodecode.lib.channelstream import channelstream_request, \ |
|
48 | 48 | ChannelstreamException |
|
49 | 49 | |
|
50 | 50 | from rhodecode.model.validation_schema.schemas import user_schema |
|
51 | 51 | from rhodecode.model.db import ( |
|
52 | 52 | Repository, PullRequest, UserEmailMap, User, UserFollowing) |
|
53 | 53 | from rhodecode.model.forms import UserForm |
|
54 | 54 | from rhodecode.model.scm import RepoList |
|
55 | 55 | from rhodecode.model.user import UserModel |
|
56 | 56 | from rhodecode.model.repo import RepoModel |
|
57 | 57 | from rhodecode.model.auth_token import AuthTokenModel |
|
58 | 58 | from rhodecode.model.meta import Session |
|
59 | 59 | from rhodecode.model.pull_request import PullRequestModel |
|
60 | 60 | from rhodecode.model.comment import ChangesetCommentsModel |
|
61 | 61 | |
|
62 | 62 | log = logging.getLogger(__name__) |
|
63 | 63 | |
|
64 | 64 | |
|
65 | 65 | class MyAccountController(BaseController): |
|
66 | 66 | """REST Controller styled on the Atom Publishing Protocol""" |
|
67 | 67 | # To properly map this controller, ensure your config/routing.py |
|
68 | 68 | # file has a resource setup: |
|
69 | 69 | # map.resource('setting', 'settings', controller='admin/settings', |
|
70 | 70 | # path_prefix='/admin', name_prefix='admin_') |
|
71 | 71 | |
|
72 | 72 | @LoginRequired() |
|
73 | 73 | @NotAnonymous() |
|
74 | 74 | def __before__(self): |
|
75 | 75 | super(MyAccountController, self).__before__() |
|
76 | 76 | |
|
77 | 77 | def __load_data(self): |
|
78 | 78 | c.user = User.get(c.rhodecode_user.user_id) |
|
79 | 79 | if c.user.username == User.DEFAULT_USER: |
|
80 | 80 | h.flash(_("You can't edit this user since it's" |
|
81 | 81 | " crucial for entire application"), category='warning') |
|
82 | 82 | return redirect(url('users')) |
|
83 | 83 | |
|
84 | 84 | c.auth_user = AuthUser( |
|
85 | 85 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) |
|
86 | 86 | |
|
87 | 87 | def _load_my_repos_data(self, watched=False): |
|
88 | 88 | if watched: |
|
89 | 89 | admin = False |
|
90 | 90 | follows_repos = Session().query(UserFollowing)\ |
|
91 | 91 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
92 | 92 | .options(joinedload(UserFollowing.follows_repository))\ |
|
93 | 93 | .all() |
|
94 | 94 | repo_list = [x.follows_repository for x in follows_repos] |
|
95 | 95 | else: |
|
96 | 96 | admin = True |
|
97 | 97 | repo_list = Repository.get_all_repos( |
|
98 | 98 | user_id=c.rhodecode_user.user_id) |
|
99 | 99 | repo_list = RepoList(repo_list, perm_set=[ |
|
100 | 100 | 'repository.read', 'repository.write', 'repository.admin']) |
|
101 | 101 | |
|
102 | 102 | repos_data = RepoModel().get_repos_as_dict( |
|
103 | 103 | repo_list=repo_list, admin=admin) |
|
104 | 104 | # json used to render the grid |
|
105 | 105 | return json.dumps(repos_data) |
|
106 | 106 | |
|
107 | 107 | @auth.CSRFRequired() |
|
108 | 108 | def my_account_update(self): |
|
109 | 109 | """ |
|
110 | 110 | POST /_admin/my_account Updates info of my account |
|
111 | 111 | """ |
|
112 | 112 | # url('my_account') |
|
113 | 113 | c.active = 'profile_edit' |
|
114 | 114 | self.__load_data() |
|
115 | 115 | c.perm_user = c.auth_user |
|
116 | 116 | c.extern_type = c.user.extern_type |
|
117 | 117 | c.extern_name = c.user.extern_name |
|
118 | 118 | |
|
119 | 119 | defaults = c.user.get_dict() |
|
120 | 120 | update = False |
|
121 | 121 | _form = UserForm(edit=True, |
|
122 | 122 | old_data={'user_id': c.rhodecode_user.user_id, |
|
123 | 123 | 'email': c.rhodecode_user.email})() |
|
124 | 124 | form_result = {} |
|
125 | 125 | try: |
|
126 | 126 | post_data = dict(request.POST) |
|
127 | 127 | post_data['new_password'] = '' |
|
128 | 128 | post_data['password_confirmation'] = '' |
|
129 | 129 | form_result = _form.to_python(post_data) |
|
130 | 130 | # skip updating those attrs for my account |
|
131 | 131 | skip_attrs = ['admin', 'active', 'extern_type', 'extern_name', |
|
132 | 132 | 'new_password', 'password_confirmation'] |
|
133 | 133 | # TODO: plugin should define if username can be updated |
|
134 | 134 | if c.extern_type != "rhodecode": |
|
135 | 135 | # forbid updating username for external accounts |
|
136 | 136 | skip_attrs.append('username') |
|
137 | 137 | |
|
138 | 138 | UserModel().update_user( |
|
139 | 139 | c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result) |
|
140 | 140 | h.flash(_('Your account was updated successfully'), |
|
141 | 141 | category='success') |
|
142 | 142 | Session().commit() |
|
143 | 143 | update = True |
|
144 | 144 | |
|
145 | 145 | except formencode.Invalid as errors: |
|
146 | 146 | return htmlfill.render( |
|
147 |
render('admin/my_account/my_account. |
|
|
147 | render('admin/my_account/my_account.mako'), | |
|
148 | 148 | defaults=errors.value, |
|
149 | 149 | errors=errors.error_dict or {}, |
|
150 | 150 | prefix_error=False, |
|
151 | 151 | encoding="UTF-8", |
|
152 | 152 | force_defaults=False) |
|
153 | 153 | except Exception: |
|
154 | 154 | log.exception("Exception updating user") |
|
155 | 155 | h.flash(_('Error occurred during update of user %s') |
|
156 | 156 | % form_result.get('username'), category='error') |
|
157 | 157 | |
|
158 | 158 | if update: |
|
159 | 159 | return redirect('my_account') |
|
160 | 160 | |
|
161 | 161 | return htmlfill.render( |
|
162 |
render('admin/my_account/my_account. |
|
|
162 | render('admin/my_account/my_account.mako'), | |
|
163 | 163 | defaults=defaults, |
|
164 | 164 | encoding="UTF-8", |
|
165 | 165 | force_defaults=False |
|
166 | 166 | ) |
|
167 | 167 | |
|
168 | 168 | def my_account(self): |
|
169 | 169 | """ |
|
170 | 170 | GET /_admin/my_account Displays info about my account |
|
171 | 171 | """ |
|
172 | 172 | # url('my_account') |
|
173 | 173 | c.active = 'profile' |
|
174 | 174 | self.__load_data() |
|
175 | 175 | |
|
176 | 176 | defaults = c.user.get_dict() |
|
177 | 177 | return htmlfill.render( |
|
178 |
render('admin/my_account/my_account. |
|
|
178 | render('admin/my_account/my_account.mako'), | |
|
179 | 179 | defaults=defaults, encoding="UTF-8", force_defaults=False) |
|
180 | 180 | |
|
181 | 181 | def my_account_edit(self): |
|
182 | 182 | """ |
|
183 | 183 | GET /_admin/my_account/edit Displays edit form of my account |
|
184 | 184 | """ |
|
185 | 185 | c.active = 'profile_edit' |
|
186 | 186 | self.__load_data() |
|
187 | 187 | c.perm_user = c.auth_user |
|
188 | 188 | c.extern_type = c.user.extern_type |
|
189 | 189 | c.extern_name = c.user.extern_name |
|
190 | 190 | |
|
191 | 191 | defaults = c.user.get_dict() |
|
192 | 192 | return htmlfill.render( |
|
193 |
render('admin/my_account/my_account. |
|
|
193 | render('admin/my_account/my_account.mako'), | |
|
194 | 194 | defaults=defaults, |
|
195 | 195 | encoding="UTF-8", |
|
196 | 196 | force_defaults=False |
|
197 | 197 | ) |
|
198 | 198 | |
|
199 | 199 | @auth.CSRFRequired(except_methods=['GET']) |
|
200 | 200 | def my_account_password(self): |
|
201 | 201 | c.active = 'password' |
|
202 | 202 | self.__load_data() |
|
203 | 203 | c.extern_type = c.user.extern_type |
|
204 | 204 | |
|
205 | 205 | schema = user_schema.ChangePasswordSchema().bind( |
|
206 | 206 | username=c.rhodecode_user.username) |
|
207 | 207 | |
|
208 | 208 | form = forms.Form(schema, |
|
209 | 209 | buttons=(forms.buttons.save, forms.buttons.reset)) |
|
210 | 210 | |
|
211 | 211 | if request.method == 'POST' and c.extern_type == 'rhodecode': |
|
212 | 212 | controls = request.POST.items() |
|
213 | 213 | try: |
|
214 | 214 | valid_data = form.validate(controls) |
|
215 | 215 | UserModel().update_user(c.rhodecode_user.user_id, **valid_data) |
|
216 | 216 | instance = c.rhodecode_user.get_instance() |
|
217 | 217 | instance.update_userdata(force_password_change=False) |
|
218 | 218 | Session().commit() |
|
219 | 219 | except forms.ValidationFailure as e: |
|
220 | 220 | request.session.flash( |
|
221 | 221 | _('Error occurred during update of user password'), |
|
222 | 222 | queue='error') |
|
223 | 223 | form = e |
|
224 | 224 | except Exception: |
|
225 | 225 | log.exception("Exception updating password") |
|
226 | 226 | request.session.flash( |
|
227 | 227 | _('Error occurred during update of user password'), |
|
228 | 228 | queue='error') |
|
229 | 229 | else: |
|
230 | 230 | session.setdefault('rhodecode_user', {}).update( |
|
231 | 231 | {'password': md5(instance.password)}) |
|
232 | 232 | session.save() |
|
233 | 233 | request.session.flash( |
|
234 | 234 | _("Successfully updated password"), queue='success') |
|
235 | 235 | return redirect(url('my_account_password')) |
|
236 | 236 | |
|
237 | 237 | c.form = form |
|
238 |
return render('admin/my_account/my_account. |
|
|
238 | return render('admin/my_account/my_account.mako') | |
|
239 | 239 | |
|
240 | 240 | def my_account_repos(self): |
|
241 | 241 | c.active = 'repos' |
|
242 | 242 | self.__load_data() |
|
243 | 243 | |
|
244 | 244 | # json used to render the grid |
|
245 | 245 | c.data = self._load_my_repos_data() |
|
246 |
return render('admin/my_account/my_account. |
|
|
246 | return render('admin/my_account/my_account.mako') | |
|
247 | 247 | |
|
248 | 248 | def my_account_watched(self): |
|
249 | 249 | c.active = 'watched' |
|
250 | 250 | self.__load_data() |
|
251 | 251 | |
|
252 | 252 | # json used to render the grid |
|
253 | 253 | c.data = self._load_my_repos_data(watched=True) |
|
254 |
return render('admin/my_account/my_account. |
|
|
254 | return render('admin/my_account/my_account.mako') | |
|
255 | 255 | |
|
256 | 256 | def my_account_perms(self): |
|
257 | 257 | c.active = 'perms' |
|
258 | 258 | self.__load_data() |
|
259 | 259 | c.perm_user = c.auth_user |
|
260 | 260 | |
|
261 |
return render('admin/my_account/my_account. |
|
|
261 | return render('admin/my_account/my_account.mako') | |
|
262 | 262 | |
|
263 | 263 | def my_account_emails(self): |
|
264 | 264 | c.active = 'emails' |
|
265 | 265 | self.__load_data() |
|
266 | 266 | |
|
267 | 267 | c.user_email_map = UserEmailMap.query()\ |
|
268 | 268 | .filter(UserEmailMap.user == c.user).all() |
|
269 |
return render('admin/my_account/my_account. |
|
|
269 | return render('admin/my_account/my_account.mako') | |
|
270 | 270 | |
|
271 | 271 | @auth.CSRFRequired() |
|
272 | 272 | def my_account_emails_add(self): |
|
273 | 273 | email = request.POST.get('new_email') |
|
274 | 274 | |
|
275 | 275 | try: |
|
276 | 276 | UserModel().add_extra_email(c.rhodecode_user.user_id, email) |
|
277 | 277 | Session().commit() |
|
278 | 278 | h.flash(_("Added new email address `%s` for user account") % email, |
|
279 | 279 | category='success') |
|
280 | 280 | except formencode.Invalid as error: |
|
281 | 281 | msg = error.error_dict['email'] |
|
282 | 282 | h.flash(msg, category='error') |
|
283 | 283 | except Exception: |
|
284 | 284 | log.exception("Exception in my_account_emails") |
|
285 | 285 | h.flash(_('An error occurred during email saving'), |
|
286 | 286 | category='error') |
|
287 | 287 | return redirect(url('my_account_emails')) |
|
288 | 288 | |
|
289 | 289 | @auth.CSRFRequired() |
|
290 | 290 | def my_account_emails_delete(self): |
|
291 | 291 | email_id = request.POST.get('del_email_id') |
|
292 | 292 | user_model = UserModel() |
|
293 | 293 | user_model.delete_extra_email(c.rhodecode_user.user_id, email_id) |
|
294 | 294 | Session().commit() |
|
295 | 295 | h.flash(_("Removed email address from user account"), |
|
296 | 296 | category='success') |
|
297 | 297 | return redirect(url('my_account_emails')) |
|
298 | 298 | |
|
299 | 299 | def _extract_ordering(self, request): |
|
300 | 300 | column_index = safe_int(request.GET.get('order[0][column]')) |
|
301 | 301 | order_dir = request.GET.get('order[0][dir]', 'desc') |
|
302 | 302 | order_by = request.GET.get( |
|
303 | 303 | 'columns[%s][data][sort]' % column_index, 'name_raw') |
|
304 | 304 | return order_by, order_dir |
|
305 | 305 | |
|
306 | 306 | def _get_pull_requests_list(self, statuses): |
|
307 | 307 | start = safe_int(request.GET.get('start'), 0) |
|
308 | 308 | length = safe_int(request.GET.get('length'), c.visual.dashboard_items) |
|
309 | 309 | order_by, order_dir = self._extract_ordering(request) |
|
310 | 310 | |
|
311 | 311 | pull_requests = PullRequestModel().get_im_participating_in( |
|
312 | 312 | user_id=c.rhodecode_user.user_id, |
|
313 | 313 | statuses=statuses, |
|
314 | 314 | offset=start, length=length, order_by=order_by, |
|
315 | 315 | order_dir=order_dir) |
|
316 | 316 | |
|
317 | 317 | pull_requests_total_count = PullRequestModel().count_im_participating_in( |
|
318 | 318 | user_id=c.rhodecode_user.user_id, statuses=statuses) |
|
319 | 319 | |
|
320 | 320 | from rhodecode.lib.utils import PartialRenderer |
|
321 |
_render = PartialRenderer('data_table/_dt_elements. |
|
|
321 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
|
322 | 322 | data = [] |
|
323 | 323 | for pr in pull_requests: |
|
324 | 324 | repo_id = pr.target_repo_id |
|
325 | 325 | comments = ChangesetCommentsModel().get_all_comments( |
|
326 | 326 | repo_id, pull_request=pr) |
|
327 | 327 | owned = pr.user_id == c.rhodecode_user.user_id |
|
328 | 328 | status = pr.calculated_review_status() |
|
329 | 329 | |
|
330 | 330 | data.append({ |
|
331 | 331 | 'target_repo': _render('pullrequest_target_repo', |
|
332 | 332 | pr.target_repo.repo_name), |
|
333 | 333 | 'name': _render('pullrequest_name', |
|
334 | 334 | pr.pull_request_id, pr.target_repo.repo_name, |
|
335 | 335 | short=True), |
|
336 | 336 | 'name_raw': pr.pull_request_id, |
|
337 | 337 | 'status': _render('pullrequest_status', status), |
|
338 | 338 | 'title': _render( |
|
339 | 339 | 'pullrequest_title', pr.title, pr.description), |
|
340 | 340 | 'description': h.escape(pr.description), |
|
341 | 341 | 'updated_on': _render('pullrequest_updated_on', |
|
342 | 342 | h.datetime_to_time(pr.updated_on)), |
|
343 | 343 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), |
|
344 | 344 | 'created_on': _render('pullrequest_updated_on', |
|
345 | 345 | h.datetime_to_time(pr.created_on)), |
|
346 | 346 | 'created_on_raw': h.datetime_to_time(pr.created_on), |
|
347 | 347 | 'author': _render('pullrequest_author', |
|
348 | 348 | pr.author.full_contact, ), |
|
349 | 349 | 'author_raw': pr.author.full_name, |
|
350 | 350 | 'comments': _render('pullrequest_comments', len(comments)), |
|
351 | 351 | 'comments_raw': len(comments), |
|
352 | 352 | 'closed': pr.is_closed(), |
|
353 | 353 | 'owned': owned |
|
354 | 354 | }) |
|
355 | 355 | # json used to render the grid |
|
356 | 356 | data = ({ |
|
357 | 357 | 'data': data, |
|
358 | 358 | 'recordsTotal': pull_requests_total_count, |
|
359 | 359 | 'recordsFiltered': pull_requests_total_count, |
|
360 | 360 | }) |
|
361 | 361 | return data |
|
362 | 362 | |
|
363 | 363 | def my_account_pullrequests(self): |
|
364 | 364 | c.active = 'pullrequests' |
|
365 | 365 | self.__load_data() |
|
366 | 366 | c.show_closed = str2bool(request.GET.get('pr_show_closed')) |
|
367 | 367 | |
|
368 | 368 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
369 | 369 | if c.show_closed: |
|
370 | 370 | statuses += [PullRequest.STATUS_CLOSED] |
|
371 | 371 | data = self._get_pull_requests_list(statuses) |
|
372 | 372 | if not request.is_xhr: |
|
373 | 373 | c.data_participate = json.dumps(data['data']) |
|
374 | 374 | c.records_total_participate = data['recordsTotal'] |
|
375 |
return render('admin/my_account/my_account. |
|
|
375 | return render('admin/my_account/my_account.mako') | |
|
376 | 376 | else: |
|
377 | 377 | return json.dumps(data) |
|
378 | 378 | |
|
379 | 379 | def my_account_auth_tokens(self): |
|
380 | 380 | c.active = 'auth_tokens' |
|
381 | 381 | self.__load_data() |
|
382 | 382 | show_expired = True |
|
383 | 383 | c.lifetime_values = [ |
|
384 | 384 | (str(-1), _('forever')), |
|
385 | 385 | (str(5), _('5 minutes')), |
|
386 | 386 | (str(60), _('1 hour')), |
|
387 | 387 | (str(60 * 24), _('1 day')), |
|
388 | 388 | (str(60 * 24 * 30), _('1 month')), |
|
389 | 389 | ] |
|
390 | 390 | c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] |
|
391 | 391 | c.role_values = [(x, AuthTokenModel.cls._get_role_name(x)) |
|
392 | 392 | for x in AuthTokenModel.cls.ROLES] |
|
393 | 393 | c.role_options = [(c.role_values, _("Role"))] |
|
394 | 394 | c.user_auth_tokens = AuthTokenModel().get_auth_tokens( |
|
395 | 395 | c.rhodecode_user.user_id, show_expired=show_expired) |
|
396 |
return render('admin/my_account/my_account. |
|
|
396 | return render('admin/my_account/my_account.mako') | |
|
397 | 397 | |
|
398 | 398 | @auth.CSRFRequired() |
|
399 | 399 | def my_account_auth_tokens_add(self): |
|
400 | 400 | lifetime = safe_int(request.POST.get('lifetime'), -1) |
|
401 | 401 | description = request.POST.get('description') |
|
402 | 402 | role = request.POST.get('role') |
|
403 | 403 | AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime, |
|
404 | 404 | role) |
|
405 | 405 | Session().commit() |
|
406 | 406 | h.flash(_("Auth token successfully created"), category='success') |
|
407 | 407 | return redirect(url('my_account_auth_tokens')) |
|
408 | 408 | |
|
409 | 409 | @auth.CSRFRequired() |
|
410 | 410 | def my_account_auth_tokens_delete(self): |
|
411 | 411 | auth_token = request.POST.get('del_auth_token') |
|
412 | 412 | user_id = c.rhodecode_user.user_id |
|
413 | 413 | if request.POST.get('del_auth_token_builtin'): |
|
414 | 414 | user = User.get(user_id) |
|
415 | 415 | if user: |
|
416 | 416 | user.api_key = generate_auth_token(user.username) |
|
417 | 417 | Session().add(user) |
|
418 | 418 | Session().commit() |
|
419 | 419 | h.flash(_("Auth token successfully reset"), category='success') |
|
420 | 420 | elif auth_token: |
|
421 | 421 | AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id) |
|
422 | 422 | Session().commit() |
|
423 | 423 | h.flash(_("Auth token successfully deleted"), category='success') |
|
424 | 424 | |
|
425 | 425 | return redirect(url('my_account_auth_tokens')) |
|
426 | 426 | |
|
427 | 427 | def my_notifications(self): |
|
428 | 428 | c.active = 'notifications' |
|
429 |
return render('admin/my_account/my_account. |
|
|
429 | return render('admin/my_account/my_account.mako') | |
|
430 | 430 | |
|
431 | 431 | @auth.CSRFRequired() |
|
432 | 432 | @jsonify |
|
433 | 433 | def my_notifications_toggle_visibility(self): |
|
434 | 434 | user = c.rhodecode_user.get_instance() |
|
435 | 435 | new_status = not user.user_data.get('notification_status', True) |
|
436 | 436 | user.update_userdata(notification_status=new_status) |
|
437 | 437 | Session().commit() |
|
438 | 438 | return user.user_data['notification_status'] |
|
439 | 439 | |
|
440 | 440 | @auth.CSRFRequired() |
|
441 | 441 | @jsonify |
|
442 | 442 | def my_account_notifications_test_channelstream(self): |
|
443 | 443 | message = 'Test message sent via Channelstream by user: {}, on {}'.format( |
|
444 | 444 | c.rhodecode_user.username, datetime.datetime.now()) |
|
445 | 445 | payload = { |
|
446 | 446 | 'type': 'message', |
|
447 | 447 | 'timestamp': datetime.datetime.utcnow(), |
|
448 | 448 | 'user': 'system', |
|
449 | 449 | #'channel': 'broadcast', |
|
450 | 450 | 'pm_users': [c.rhodecode_user.username], |
|
451 | 451 | 'message': { |
|
452 | 452 | 'message': message, |
|
453 | 453 | 'level': 'info', |
|
454 | 454 | 'topic': '/notifications' |
|
455 | 455 | } |
|
456 | 456 | } |
|
457 | 457 | |
|
458 | 458 | registry = get_current_registry() |
|
459 | 459 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) |
|
460 | 460 | channelstream_config = rhodecode_plugins.get('channelstream', {}) |
|
461 | 461 | |
|
462 | 462 | try: |
|
463 | 463 | channelstream_request(channelstream_config, [payload], '/message') |
|
464 | 464 | except ChannelstreamException as e: |
|
465 | 465 | log.exception('Failed to send channelstream data') |
|
466 | 466 | return {"response": 'ERROR: {}'.format(e.__class__.__name__)} |
|
467 | 467 | return {"response": 'Channelstream data sent. ' |
|
468 | 468 | 'You should see a new live message now.'} |
@@ -1,178 +1,178 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 | """ |
|
23 | 23 | notifications controller for RhodeCode |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | import traceback |
|
28 | 28 | |
|
29 | 29 | from pylons import request |
|
30 | 30 | from pylons import tmpl_context as c, url |
|
31 | 31 | from pylons.controllers.util import redirect, abort |
|
32 | 32 | import webhelpers.paginate |
|
33 | 33 | from webob.exc import HTTPBadRequest |
|
34 | 34 | |
|
35 | 35 | from rhodecode.lib import auth |
|
36 | 36 | from rhodecode.lib.auth import LoginRequired, NotAnonymous |
|
37 | 37 | from rhodecode.lib.base import BaseController, render |
|
38 | 38 | from rhodecode.lib import helpers as h |
|
39 | 39 | from rhodecode.lib.helpers import Page |
|
40 | 40 | from rhodecode.lib.utils2 import safe_int |
|
41 | 41 | from rhodecode.model.db import Notification |
|
42 | 42 | from rhodecode.model.notification import NotificationModel |
|
43 | 43 | from rhodecode.model.meta import Session |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | log = logging.getLogger(__name__) |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | class NotificationsController(BaseController): |
|
50 | 50 | """REST Controller styled on the Atom Publishing Protocol""" |
|
51 | 51 | # To properly map this controller, ensure your config/routing.py |
|
52 | 52 | # file has a resource setup: |
|
53 | 53 | # map.resource('notification', 'notifications', controller='_admin/notifications', |
|
54 | 54 | # path_prefix='/_admin', name_prefix='_admin_') |
|
55 | 55 | |
|
56 | 56 | @LoginRequired() |
|
57 | 57 | @NotAnonymous() |
|
58 | 58 | def __before__(self): |
|
59 | 59 | super(NotificationsController, self).__before__() |
|
60 | 60 | |
|
61 | 61 | def index(self): |
|
62 | 62 | """GET /_admin/notifications: All items in the collection""" |
|
63 | 63 | # url('notifications') |
|
64 | 64 | c.user = c.rhodecode_user |
|
65 | 65 | notif = NotificationModel().get_for_user(c.rhodecode_user.user_id, |
|
66 | 66 | filter_=request.GET.getall('type')) |
|
67 | 67 | |
|
68 | 68 | p = safe_int(request.GET.get('page', 1), 1) |
|
69 | 69 | notifications_url = webhelpers.paginate.PageURL( |
|
70 | 70 | url('notifications'), request.GET) |
|
71 | 71 | c.notifications = Page(notif, page=p, items_per_page=10, |
|
72 | 72 | url=notifications_url) |
|
73 | 73 | c.pull_request_type = Notification.TYPE_PULL_REQUEST |
|
74 | 74 | c.comment_type = [Notification.TYPE_CHANGESET_COMMENT, |
|
75 | 75 | Notification.TYPE_PULL_REQUEST_COMMENT] |
|
76 | 76 | |
|
77 | 77 | _current_filter = request.GET.getall('type') |
|
78 | 78 | c.current_filter = 'all' |
|
79 | 79 | if _current_filter == [c.pull_request_type]: |
|
80 | 80 | c.current_filter = 'pull_request' |
|
81 | 81 | elif _current_filter == c.comment_type: |
|
82 | 82 | c.current_filter = 'comment' |
|
83 | 83 | |
|
84 | 84 | if request.is_xhr: |
|
85 |
return render('admin/notifications/notifications_data. |
|
|
85 | return render('admin/notifications/notifications_data.mako') | |
|
86 | 86 | |
|
87 |
return render('admin/notifications/notifications. |
|
|
87 | return render('admin/notifications/notifications.mako') | |
|
88 | 88 | |
|
89 | 89 | |
|
90 | 90 | @auth.CSRFRequired() |
|
91 | 91 | def mark_all_read(self): |
|
92 | 92 | if request.is_xhr: |
|
93 | 93 | nm = NotificationModel() |
|
94 | 94 | # mark all read |
|
95 | 95 | nm.mark_all_read_for_user(c.rhodecode_user.user_id, |
|
96 | 96 | filter_=request.GET.getall('type')) |
|
97 | 97 | Session().commit() |
|
98 | 98 | c.user = c.rhodecode_user |
|
99 | 99 | notif = nm.get_for_user(c.rhodecode_user.user_id, |
|
100 | 100 | filter_=request.GET.getall('type')) |
|
101 | 101 | notifications_url = webhelpers.paginate.PageURL( |
|
102 | 102 | url('notifications'), request.GET) |
|
103 | 103 | c.notifications = Page(notif, page=1, items_per_page=10, |
|
104 | 104 | url=notifications_url) |
|
105 |
return render('admin/notifications/notifications_data. |
|
|
105 | return render('admin/notifications/notifications_data.mako') | |
|
106 | 106 | |
|
107 | 107 | def _has_permissions(self, notification): |
|
108 | 108 | def is_owner(): |
|
109 | 109 | user_id = c.rhodecode_user.user_id |
|
110 | 110 | for user_notification in notification.notifications_to_users: |
|
111 | 111 | if user_notification.user.user_id == user_id: |
|
112 | 112 | return True |
|
113 | 113 | return False |
|
114 | 114 | return h.HasPermissionAny('hg.admin')() or is_owner() |
|
115 | 115 | |
|
116 | 116 | @auth.CSRFRequired() |
|
117 | 117 | def update(self, notification_id): |
|
118 | 118 | """PUT /_admin/notifications/id: Update an existing item""" |
|
119 | 119 | # Forms posted to this method should contain a hidden field: |
|
120 | 120 | # <input type="hidden" name="_method" value="PUT" /> |
|
121 | 121 | # Or using helpers: |
|
122 | 122 | # h.form(url('notification', notification_id=ID), |
|
123 | 123 | # method='put') |
|
124 | 124 | # url('notification', notification_id=ID) |
|
125 | 125 | try: |
|
126 | 126 | no = Notification.get(notification_id) |
|
127 | 127 | if self._has_permissions(no): |
|
128 | 128 | # deletes only notification2user |
|
129 | 129 | NotificationModel().mark_read(c.rhodecode_user.user_id, no) |
|
130 | 130 | Session().commit() |
|
131 | 131 | return 'ok' |
|
132 | 132 | except Exception: |
|
133 | 133 | Session().rollback() |
|
134 | 134 | log.exception("Exception updating a notification item") |
|
135 | 135 | raise HTTPBadRequest() |
|
136 | 136 | |
|
137 | 137 | @auth.CSRFRequired() |
|
138 | 138 | def delete(self, notification_id): |
|
139 | 139 | """DELETE /_admin/notifications/id: Delete an existing item""" |
|
140 | 140 | # Forms posted to this method should contain a hidden field: |
|
141 | 141 | # <input type="hidden" name="_method" value="DELETE" /> |
|
142 | 142 | # Or using helpers: |
|
143 | 143 | # h.form(url('notification', notification_id=ID), |
|
144 | 144 | # method='delete') |
|
145 | 145 | # url('notification', notification_id=ID) |
|
146 | 146 | try: |
|
147 | 147 | no = Notification.get(notification_id) |
|
148 | 148 | if self._has_permissions(no): |
|
149 | 149 | # deletes only notification2user |
|
150 | 150 | NotificationModel().delete(c.rhodecode_user.user_id, no) |
|
151 | 151 | Session().commit() |
|
152 | 152 | return 'ok' |
|
153 | 153 | except Exception: |
|
154 | 154 | Session().rollback() |
|
155 | 155 | log.exception("Exception deleting a notification item") |
|
156 | 156 | raise HTTPBadRequest() |
|
157 | 157 | |
|
158 | 158 | def show(self, notification_id): |
|
159 | 159 | """GET /_admin/notifications/id: Show a specific item""" |
|
160 | 160 | # url('notification', notification_id=ID) |
|
161 | 161 | c.user = c.rhodecode_user |
|
162 | 162 | no = Notification.get(notification_id) |
|
163 | 163 | |
|
164 | 164 | if no and self._has_permissions(no): |
|
165 | 165 | unotification = NotificationModel()\ |
|
166 | 166 | .get_user_notification(c.user.user_id, no) |
|
167 | 167 | |
|
168 | 168 | # if this association to user is not valid, we don't want to show |
|
169 | 169 | # this message |
|
170 | 170 | if unotification: |
|
171 | 171 | if not unotification.read: |
|
172 | 172 | unotification.mark_as_read() |
|
173 | 173 | Session().commit() |
|
174 | 174 | c.notification = no |
|
175 | 175 | |
|
176 |
return render('admin/notifications/show_notification. |
|
|
176 | return render('admin/notifications/show_notification.mako') | |
|
177 | 177 | |
|
178 | 178 | return abort(403) |
@@ -1,249 +1,249 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 | """ |
|
23 | 23 | permissions controller for RhodeCode Enterprise |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | import logging |
|
28 | 28 | |
|
29 | 29 | import formencode |
|
30 | 30 | from formencode import htmlfill |
|
31 | 31 | from pylons import request, tmpl_context as c, url |
|
32 | 32 | from pylons.controllers.util import redirect |
|
33 | 33 | from pylons.i18n.translation import _ |
|
34 | 34 | |
|
35 | 35 | from rhodecode.lib import helpers as h |
|
36 | 36 | from rhodecode.lib import auth |
|
37 | 37 | from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator) |
|
38 | 38 | from rhodecode.lib.base import BaseController, render |
|
39 | 39 | from rhodecode.model.db import User, UserIpMap |
|
40 | 40 | from rhodecode.model.forms import ( |
|
41 | 41 | ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm) |
|
42 | 42 | from rhodecode.model.meta import Session |
|
43 | 43 | from rhodecode.model.permission import PermissionModel |
|
44 | 44 | from rhodecode.model.settings import SettingsModel |
|
45 | 45 | |
|
46 | 46 | log = logging.getLogger(__name__) |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | class PermissionsController(BaseController): |
|
50 | 50 | """REST Controller styled on the Atom Publishing Protocol""" |
|
51 | 51 | # To properly map this controller, ensure your config/routing.py |
|
52 | 52 | # file has a resource setup: |
|
53 | 53 | # map.resource('permission', 'permissions') |
|
54 | 54 | |
|
55 | 55 | @LoginRequired() |
|
56 | 56 | def __before__(self): |
|
57 | 57 | super(PermissionsController, self).__before__() |
|
58 | 58 | |
|
59 | 59 | def __load_data(self): |
|
60 | 60 | PermissionModel().set_global_permission_choices(c, gettext_translator=_) |
|
61 | 61 | |
|
62 | 62 | @HasPermissionAllDecorator('hg.admin') |
|
63 | 63 | def permission_application(self): |
|
64 | 64 | c.active = 'application' |
|
65 | 65 | self.__load_data() |
|
66 | 66 | |
|
67 | 67 | c.user = User.get_default_user() |
|
68 | 68 | |
|
69 | 69 | # TODO: johbo: The default user might be based on outdated state which |
|
70 | 70 | # has been loaded from the cache. A call to refresh() ensures that the |
|
71 | 71 | # latest state from the database is used. |
|
72 | 72 | Session().refresh(c.user) |
|
73 | 73 | |
|
74 | 74 | app_settings = SettingsModel().get_all_settings() |
|
75 | 75 | defaults = { |
|
76 | 76 | 'anonymous': c.user.active, |
|
77 | 77 | 'default_register_message': app_settings.get( |
|
78 | 78 | 'rhodecode_register_message') |
|
79 | 79 | } |
|
80 | 80 | defaults.update(c.user.get_default_perms()) |
|
81 | 81 | |
|
82 | 82 | return htmlfill.render( |
|
83 |
render('admin/permissions/permissions. |
|
|
83 | render('admin/permissions/permissions.mako'), | |
|
84 | 84 | defaults=defaults, |
|
85 | 85 | encoding="UTF-8", |
|
86 | 86 | force_defaults=False) |
|
87 | 87 | |
|
88 | 88 | @HasPermissionAllDecorator('hg.admin') |
|
89 | 89 | @auth.CSRFRequired() |
|
90 | 90 | def permission_application_update(self): |
|
91 | 91 | c.active = 'application' |
|
92 | 92 | self.__load_data() |
|
93 | 93 | _form = ApplicationPermissionsForm( |
|
94 | 94 | [x[0] for x in c.register_choices], |
|
95 | 95 | [x[0] for x in c.password_reset_choices], |
|
96 | 96 | [x[0] for x in c.extern_activate_choices])() |
|
97 | 97 | |
|
98 | 98 | try: |
|
99 | 99 | form_result = _form.to_python(dict(request.POST)) |
|
100 | 100 | form_result.update({'perm_user_name': User.DEFAULT_USER}) |
|
101 | 101 | PermissionModel().update_application_permissions(form_result) |
|
102 | 102 | |
|
103 | 103 | settings = [ |
|
104 | 104 | ('register_message', 'default_register_message'), |
|
105 | 105 | ] |
|
106 | 106 | for setting, form_key in settings: |
|
107 | 107 | sett = SettingsModel().create_or_update_setting( |
|
108 | 108 | setting, form_result[form_key]) |
|
109 | 109 | Session().add(sett) |
|
110 | 110 | |
|
111 | 111 | Session().commit() |
|
112 | 112 | h.flash(_('Application permissions updated successfully'), |
|
113 | 113 | category='success') |
|
114 | 114 | |
|
115 | 115 | except formencode.Invalid as errors: |
|
116 | 116 | defaults = errors.value |
|
117 | 117 | |
|
118 | 118 | return htmlfill.render( |
|
119 |
render('admin/permissions/permissions. |
|
|
119 | render('admin/permissions/permissions.mako'), | |
|
120 | 120 | defaults=defaults, |
|
121 | 121 | errors=errors.error_dict or {}, |
|
122 | 122 | prefix_error=False, |
|
123 | 123 | encoding="UTF-8", |
|
124 | 124 | force_defaults=False) |
|
125 | 125 | except Exception: |
|
126 | 126 | log.exception("Exception during update of permissions") |
|
127 | 127 | h.flash(_('Error occurred during update of permissions'), |
|
128 | 128 | category='error') |
|
129 | 129 | |
|
130 | 130 | return redirect(url('admin_permissions_application')) |
|
131 | 131 | |
|
132 | 132 | @HasPermissionAllDecorator('hg.admin') |
|
133 | 133 | def permission_objects(self): |
|
134 | 134 | c.active = 'objects' |
|
135 | 135 | self.__load_data() |
|
136 | 136 | c.user = User.get_default_user() |
|
137 | 137 | defaults = {} |
|
138 | 138 | defaults.update(c.user.get_default_perms()) |
|
139 | 139 | return htmlfill.render( |
|
140 |
render('admin/permissions/permissions. |
|
|
140 | render('admin/permissions/permissions.mako'), | |
|
141 | 141 | defaults=defaults, |
|
142 | 142 | encoding="UTF-8", |
|
143 | 143 | force_defaults=False) |
|
144 | 144 | |
|
145 | 145 | @HasPermissionAllDecorator('hg.admin') |
|
146 | 146 | @auth.CSRFRequired() |
|
147 | 147 | def permission_objects_update(self): |
|
148 | 148 | c.active = 'objects' |
|
149 | 149 | self.__load_data() |
|
150 | 150 | _form = ObjectPermissionsForm( |
|
151 | 151 | [x[0] for x in c.repo_perms_choices], |
|
152 | 152 | [x[0] for x in c.group_perms_choices], |
|
153 | 153 | [x[0] for x in c.user_group_perms_choices])() |
|
154 | 154 | |
|
155 | 155 | try: |
|
156 | 156 | form_result = _form.to_python(dict(request.POST)) |
|
157 | 157 | form_result.update({'perm_user_name': User.DEFAULT_USER}) |
|
158 | 158 | PermissionModel().update_object_permissions(form_result) |
|
159 | 159 | |
|
160 | 160 | Session().commit() |
|
161 | 161 | h.flash(_('Object permissions updated successfully'), |
|
162 | 162 | category='success') |
|
163 | 163 | |
|
164 | 164 | except formencode.Invalid as errors: |
|
165 | 165 | defaults = errors.value |
|
166 | 166 | |
|
167 | 167 | return htmlfill.render( |
|
168 |
render('admin/permissions/permissions. |
|
|
168 | render('admin/permissions/permissions.mako'), | |
|
169 | 169 | defaults=defaults, |
|
170 | 170 | errors=errors.error_dict or {}, |
|
171 | 171 | prefix_error=False, |
|
172 | 172 | encoding="UTF-8", |
|
173 | 173 | force_defaults=False) |
|
174 | 174 | except Exception: |
|
175 | 175 | log.exception("Exception during update of permissions") |
|
176 | 176 | h.flash(_('Error occurred during update of permissions'), |
|
177 | 177 | category='error') |
|
178 | 178 | |
|
179 | 179 | return redirect(url('admin_permissions_object')) |
|
180 | 180 | |
|
181 | 181 | @HasPermissionAllDecorator('hg.admin') |
|
182 | 182 | def permission_global(self): |
|
183 | 183 | c.active = 'global' |
|
184 | 184 | self.__load_data() |
|
185 | 185 | |
|
186 | 186 | c.user = User.get_default_user() |
|
187 | 187 | defaults = {} |
|
188 | 188 | defaults.update(c.user.get_default_perms()) |
|
189 | 189 | |
|
190 | 190 | return htmlfill.render( |
|
191 |
render('admin/permissions/permissions. |
|
|
191 | render('admin/permissions/permissions.mako'), | |
|
192 | 192 | defaults=defaults, |
|
193 | 193 | encoding="UTF-8", |
|
194 | 194 | force_defaults=False) |
|
195 | 195 | |
|
196 | 196 | @HasPermissionAllDecorator('hg.admin') |
|
197 | 197 | @auth.CSRFRequired() |
|
198 | 198 | def permission_global_update(self): |
|
199 | 199 | c.active = 'global' |
|
200 | 200 | self.__load_data() |
|
201 | 201 | _form = UserPermissionsForm( |
|
202 | 202 | [x[0] for x in c.repo_create_choices], |
|
203 | 203 | [x[0] for x in c.repo_create_on_write_choices], |
|
204 | 204 | [x[0] for x in c.repo_group_create_choices], |
|
205 | 205 | [x[0] for x in c.user_group_create_choices], |
|
206 | 206 | [x[0] for x in c.fork_choices], |
|
207 | 207 | [x[0] for x in c.inherit_default_permission_choices])() |
|
208 | 208 | |
|
209 | 209 | try: |
|
210 | 210 | form_result = _form.to_python(dict(request.POST)) |
|
211 | 211 | form_result.update({'perm_user_name': User.DEFAULT_USER}) |
|
212 | 212 | PermissionModel().update_user_permissions(form_result) |
|
213 | 213 | |
|
214 | 214 | Session().commit() |
|
215 | 215 | h.flash(_('Global permissions updated successfully'), |
|
216 | 216 | category='success') |
|
217 | 217 | |
|
218 | 218 | except formencode.Invalid as errors: |
|
219 | 219 | defaults = errors.value |
|
220 | 220 | |
|
221 | 221 | return htmlfill.render( |
|
222 |
render('admin/permissions/permissions. |
|
|
222 | render('admin/permissions/permissions.mako'), | |
|
223 | 223 | defaults=defaults, |
|
224 | 224 | errors=errors.error_dict or {}, |
|
225 | 225 | prefix_error=False, |
|
226 | 226 | encoding="UTF-8", |
|
227 | 227 | force_defaults=False) |
|
228 | 228 | except Exception: |
|
229 | 229 | log.exception("Exception during update of permissions") |
|
230 | 230 | h.flash(_('Error occurred during update of permissions'), |
|
231 | 231 | category='error') |
|
232 | 232 | |
|
233 | 233 | return redirect(url('admin_permissions_global')) |
|
234 | 234 | |
|
235 | 235 | @HasPermissionAllDecorator('hg.admin') |
|
236 | 236 | def permission_ips(self): |
|
237 | 237 | c.active = 'ips' |
|
238 | 238 | c.user = User.get_default_user() |
|
239 | 239 | c.user_ip_map = ( |
|
240 | 240 | UserIpMap.query().filter(UserIpMap.user == c.user).all()) |
|
241 | 241 | |
|
242 |
return render('admin/permissions/permissions. |
|
|
242 | return render('admin/permissions/permissions.mako') | |
|
243 | 243 | |
|
244 | 244 | @HasPermissionAllDecorator('hg.admin') |
|
245 | 245 | def permission_perms(self): |
|
246 | 246 | c.active = 'perms' |
|
247 | 247 | c.user = User.get_default_user() |
|
248 | 248 | c.perm_user = c.user.AuthUser |
|
249 |
return render('admin/permissions/permissions. |
|
|
249 | return render('admin/permissions/permissions.mako') |
@@ -1,406 +1,406 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 | """ |
|
23 | 23 | Repository groups controller for RhodeCode |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | import formencode |
|
28 | 28 | |
|
29 | 29 | from formencode import htmlfill |
|
30 | 30 | |
|
31 | 31 | from pylons import request, tmpl_context as c, url |
|
32 | 32 | from pylons.controllers.util import abort, redirect |
|
33 | 33 | from pylons.i18n.translation import _, ungettext |
|
34 | 34 | |
|
35 | 35 | from rhodecode.lib import auth |
|
36 | 36 | from rhodecode.lib import helpers as h |
|
37 | 37 | from rhodecode.lib.ext_json import json |
|
38 | 38 | from rhodecode.lib.auth import ( |
|
39 | 39 | LoginRequired, NotAnonymous, HasPermissionAll, |
|
40 | 40 | HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator) |
|
41 | 41 | from rhodecode.lib.base import BaseController, render |
|
42 | 42 | from rhodecode.model.db import RepoGroup, User |
|
43 | 43 | from rhodecode.model.scm import RepoGroupList |
|
44 | 44 | from rhodecode.model.repo_group import RepoGroupModel |
|
45 | 45 | from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm |
|
46 | 46 | from rhodecode.model.meta import Session |
|
47 | 47 | from rhodecode.lib.utils2 import safe_int |
|
48 | 48 | |
|
49 | 49 | |
|
50 | 50 | log = logging.getLogger(__name__) |
|
51 | 51 | |
|
52 | 52 | |
|
53 | 53 | class RepoGroupsController(BaseController): |
|
54 | 54 | """REST Controller styled on the Atom Publishing Protocol""" |
|
55 | 55 | |
|
56 | 56 | @LoginRequired() |
|
57 | 57 | def __before__(self): |
|
58 | 58 | super(RepoGroupsController, self).__before__() |
|
59 | 59 | |
|
60 | 60 | def __load_defaults(self, allow_empty_group=False, repo_group=None): |
|
61 | 61 | if self._can_create_repo_group(): |
|
62 | 62 | # we're global admin, we're ok and we can create TOP level groups |
|
63 | 63 | allow_empty_group = True |
|
64 | 64 | |
|
65 | 65 | # override the choices for this form, we need to filter choices |
|
66 | 66 | # and display only those we have ADMIN right |
|
67 | 67 | groups_with_admin_rights = RepoGroupList( |
|
68 | 68 | RepoGroup.query().all(), |
|
69 | 69 | perm_set=['group.admin']) |
|
70 | 70 | c.repo_groups = RepoGroup.groups_choices( |
|
71 | 71 | groups=groups_with_admin_rights, |
|
72 | 72 | show_empty_group=allow_empty_group) |
|
73 | 73 | |
|
74 | 74 | if repo_group: |
|
75 | 75 | # exclude filtered ids |
|
76 | 76 | exclude_group_ids = [repo_group.group_id] |
|
77 | 77 | c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids, |
|
78 | 78 | c.repo_groups) |
|
79 | 79 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
80 | 80 | parent_group = repo_group.parent_group |
|
81 | 81 | |
|
82 | 82 | add_parent_group = (parent_group and ( |
|
83 | 83 | unicode(parent_group.group_id) not in c.repo_groups_choices)) |
|
84 | 84 | if add_parent_group: |
|
85 | 85 | c.repo_groups_choices.append(unicode(parent_group.group_id)) |
|
86 | 86 | c.repo_groups.append(RepoGroup._generate_choice(parent_group)) |
|
87 | 87 | |
|
88 | 88 | def __load_data(self, group_id): |
|
89 | 89 | """ |
|
90 | 90 | Load defaults settings for edit, and update |
|
91 | 91 | |
|
92 | 92 | :param group_id: |
|
93 | 93 | """ |
|
94 | 94 | repo_group = RepoGroup.get_or_404(group_id) |
|
95 | 95 | data = repo_group.get_dict() |
|
96 | 96 | data['group_name'] = repo_group.name |
|
97 | 97 | |
|
98 | 98 | # fill owner |
|
99 | 99 | if repo_group.user: |
|
100 | 100 | data.update({'user': repo_group.user.username}) |
|
101 | 101 | else: |
|
102 | 102 | replacement_user = User.get_first_super_admin().username |
|
103 | 103 | data.update({'user': replacement_user}) |
|
104 | 104 | |
|
105 | 105 | # fill repository group users |
|
106 | 106 | for p in repo_group.repo_group_to_perm: |
|
107 | 107 | data.update({ |
|
108 | 108 | 'u_perm_%s' % p.user.user_id: p.permission.permission_name}) |
|
109 | 109 | |
|
110 | 110 | # fill repository group user groups |
|
111 | 111 | for p in repo_group.users_group_to_perm: |
|
112 | 112 | data.update({ |
|
113 | 113 | 'g_perm_%s' % p.users_group.users_group_id: |
|
114 | 114 | p.permission.permission_name}) |
|
115 | 115 | # html and form expects -1 as empty parent group |
|
116 | 116 | data['group_parent_id'] = data['group_parent_id'] or -1 |
|
117 | 117 | return data |
|
118 | 118 | |
|
119 | 119 | def _revoke_perms_on_yourself(self, form_result): |
|
120 | 120 | _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
121 | 121 | form_result['perm_updates']) |
|
122 | 122 | _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
123 | 123 | form_result['perm_additions']) |
|
124 | 124 | _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
125 | 125 | form_result['perm_deletions']) |
|
126 | 126 | admin_perm = 'group.admin' |
|
127 | 127 | if _updates and _updates[0][1] != admin_perm or \ |
|
128 | 128 | _additions and _additions[0][1] != admin_perm or \ |
|
129 | 129 | _deletions and _deletions[0][1] != admin_perm: |
|
130 | 130 | return True |
|
131 | 131 | return False |
|
132 | 132 | |
|
133 | 133 | def _can_create_repo_group(self, parent_group_id=None): |
|
134 | 134 | is_admin = HasPermissionAll('hg.admin')('group create controller') |
|
135 | 135 | create_repo_group = HasPermissionAll( |
|
136 | 136 | 'hg.repogroup.create.true')('group create controller') |
|
137 | 137 | if is_admin or (create_repo_group and not parent_group_id): |
|
138 | 138 | # we're global admin, or we have global repo group create |
|
139 | 139 | # permission |
|
140 | 140 | # we're ok and we can create TOP level groups |
|
141 | 141 | return True |
|
142 | 142 | elif parent_group_id: |
|
143 | 143 | # we check the permission if we can write to parent group |
|
144 | 144 | group = RepoGroup.get(parent_group_id) |
|
145 | 145 | group_name = group.group_name if group else None |
|
146 | 146 | if HasRepoGroupPermissionAll('group.admin')( |
|
147 | 147 | group_name, 'check if user is an admin of group'): |
|
148 | 148 | # we're an admin of passed in group, we're ok. |
|
149 | 149 | return True |
|
150 | 150 | else: |
|
151 | 151 | return False |
|
152 | 152 | return False |
|
153 | 153 | |
|
154 | 154 | @NotAnonymous() |
|
155 | 155 | def index(self): |
|
156 | 156 | """GET /repo_groups: All items in the collection""" |
|
157 | 157 | # url('repo_groups') |
|
158 | 158 | |
|
159 | 159 | repo_group_list = RepoGroup.get_all_repo_groups() |
|
160 | 160 | _perms = ['group.admin'] |
|
161 | 161 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) |
|
162 | 162 | repo_group_data = RepoGroupModel().get_repo_groups_as_dict( |
|
163 | 163 | repo_group_list=repo_group_list_acl, admin=True) |
|
164 | 164 | c.data = json.dumps(repo_group_data) |
|
165 |
return render('admin/repo_groups/repo_groups. |
|
|
165 | return render('admin/repo_groups/repo_groups.mako') | |
|
166 | 166 | |
|
167 | 167 | # perm checks inside |
|
168 | 168 | @NotAnonymous() |
|
169 | 169 | @auth.CSRFRequired() |
|
170 | 170 | def create(self): |
|
171 | 171 | """POST /repo_groups: Create a new item""" |
|
172 | 172 | # url('repo_groups') |
|
173 | 173 | |
|
174 | 174 | parent_group_id = safe_int(request.POST.get('group_parent_id')) |
|
175 | 175 | can_create = self._can_create_repo_group(parent_group_id) |
|
176 | 176 | |
|
177 | 177 | self.__load_defaults() |
|
178 | 178 | # permissions for can create group based on parent_id are checked |
|
179 | 179 | # here in the Form |
|
180 | 180 | available_groups = map(lambda k: unicode(k[0]), c.repo_groups) |
|
181 | 181 | repo_group_form = RepoGroupForm(available_groups=available_groups, |
|
182 | 182 | can_create_in_root=can_create)() |
|
183 | 183 | try: |
|
184 | 184 | owner = c.rhodecode_user |
|
185 | 185 | form_result = repo_group_form.to_python(dict(request.POST)) |
|
186 | 186 | RepoGroupModel().create( |
|
187 | 187 | group_name=form_result['group_name_full'], |
|
188 | 188 | group_description=form_result['group_description'], |
|
189 | 189 | owner=owner.user_id, |
|
190 | 190 | copy_permissions=form_result['group_copy_permissions'] |
|
191 | 191 | ) |
|
192 | 192 | Session().commit() |
|
193 | 193 | _new_group_name = form_result['group_name_full'] |
|
194 | 194 | repo_group_url = h.link_to( |
|
195 | 195 | _new_group_name, |
|
196 | 196 | h.url('repo_group_home', group_name=_new_group_name)) |
|
197 | 197 | h.flash(h.literal(_('Created repository group %s') |
|
198 | 198 | % repo_group_url), category='success') |
|
199 | 199 | # TODO: in futureaction_logger(, '', '', '', self.sa) |
|
200 | 200 | except formencode.Invalid as errors: |
|
201 | 201 | return htmlfill.render( |
|
202 |
render('admin/repo_groups/repo_group_add. |
|
|
202 | render('admin/repo_groups/repo_group_add.mako'), | |
|
203 | 203 | defaults=errors.value, |
|
204 | 204 | errors=errors.error_dict or {}, |
|
205 | 205 | prefix_error=False, |
|
206 | 206 | encoding="UTF-8", |
|
207 | 207 | force_defaults=False) |
|
208 | 208 | except Exception: |
|
209 | 209 | log.exception("Exception during creation of repository group") |
|
210 | 210 | h.flash(_('Error occurred during creation of repository group %s') |
|
211 | 211 | % request.POST.get('group_name'), category='error') |
|
212 | 212 | |
|
213 | 213 | # TODO: maybe we should get back to the main view, not the admin one |
|
214 | 214 | return redirect(url('repo_groups', parent_group=parent_group_id)) |
|
215 | 215 | |
|
216 | 216 | # perm checks inside |
|
217 | 217 | @NotAnonymous() |
|
218 | 218 | def new(self): |
|
219 | 219 | """GET /repo_groups/new: Form to create a new item""" |
|
220 | 220 | # url('new_repo_group') |
|
221 | 221 | # perm check for admin, create_group perm or admin of parent_group |
|
222 | 222 | parent_group_id = safe_int(request.GET.get('parent_group')) |
|
223 | 223 | if not self._can_create_repo_group(parent_group_id): |
|
224 | 224 | return abort(403) |
|
225 | 225 | |
|
226 | 226 | self.__load_defaults() |
|
227 |
return render('admin/repo_groups/repo_group_add. |
|
|
227 | return render('admin/repo_groups/repo_group_add.mako') | |
|
228 | 228 | |
|
229 | 229 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
230 | 230 | @auth.CSRFRequired() |
|
231 | 231 | def update(self, group_name): |
|
232 | 232 | """PUT /repo_groups/group_name: Update an existing item""" |
|
233 | 233 | # Forms posted to this method should contain a hidden field: |
|
234 | 234 | # <input type="hidden" name="_method" value="PUT" /> |
|
235 | 235 | # Or using helpers: |
|
236 | 236 | # h.form(url('repos_group', group_name=GROUP_NAME), method='put') |
|
237 | 237 | # url('repo_group_home', group_name=GROUP_NAME) |
|
238 | 238 | |
|
239 | 239 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
240 | 240 | can_create_in_root = self._can_create_repo_group() |
|
241 | 241 | show_root_location = can_create_in_root |
|
242 | 242 | if not c.repo_group.parent_group: |
|
243 | 243 | # this group don't have a parrent so we should show empty value |
|
244 | 244 | show_root_location = True |
|
245 | 245 | self.__load_defaults(allow_empty_group=show_root_location, |
|
246 | 246 | repo_group=c.repo_group) |
|
247 | 247 | |
|
248 | 248 | repo_group_form = RepoGroupForm( |
|
249 | 249 | edit=True, old_data=c.repo_group.get_dict(), |
|
250 | 250 | available_groups=c.repo_groups_choices, |
|
251 | 251 | can_create_in_root=can_create_in_root, allow_disabled=True)() |
|
252 | 252 | |
|
253 | 253 | try: |
|
254 | 254 | form_result = repo_group_form.to_python(dict(request.POST)) |
|
255 | 255 | gr_name = form_result['group_name'] |
|
256 | 256 | new_gr = RepoGroupModel().update(group_name, form_result) |
|
257 | 257 | Session().commit() |
|
258 | 258 | h.flash(_('Updated repository group %s') % (gr_name,), |
|
259 | 259 | category='success') |
|
260 | 260 | # we now have new name ! |
|
261 | 261 | group_name = new_gr.group_name |
|
262 | 262 | # TODO: in future action_logger(, '', '', '', self.sa) |
|
263 | 263 | except formencode.Invalid as errors: |
|
264 | 264 | c.active = 'settings' |
|
265 | 265 | return htmlfill.render( |
|
266 |
render('admin/repo_groups/repo_group_edit. |
|
|
266 | render('admin/repo_groups/repo_group_edit.mako'), | |
|
267 | 267 | defaults=errors.value, |
|
268 | 268 | errors=errors.error_dict or {}, |
|
269 | 269 | prefix_error=False, |
|
270 | 270 | encoding="UTF-8", |
|
271 | 271 | force_defaults=False) |
|
272 | 272 | except Exception: |
|
273 | 273 | log.exception("Exception during update or repository group") |
|
274 | 274 | h.flash(_('Error occurred during update of repository group %s') |
|
275 | 275 | % request.POST.get('group_name'), category='error') |
|
276 | 276 | |
|
277 | 277 | return redirect(url('edit_repo_group', group_name=group_name)) |
|
278 | 278 | |
|
279 | 279 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
280 | 280 | @auth.CSRFRequired() |
|
281 | 281 | def delete(self, group_name): |
|
282 | 282 | """DELETE /repo_groups/group_name: Delete an existing item""" |
|
283 | 283 | # Forms posted to this method should contain a hidden field: |
|
284 | 284 | # <input type="hidden" name="_method" value="DELETE" /> |
|
285 | 285 | # Or using helpers: |
|
286 | 286 | # h.form(url('repos_group', group_name=GROUP_NAME), method='delete') |
|
287 | 287 | # url('repo_group_home', group_name=GROUP_NAME) |
|
288 | 288 | |
|
289 | 289 | gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
290 | 290 | repos = gr.repositories.all() |
|
291 | 291 | if repos: |
|
292 | 292 | msg = ungettext( |
|
293 | 293 | 'This group contains %(num)d repository and cannot be deleted', |
|
294 | 294 | 'This group contains %(num)d repositories and cannot be' |
|
295 | 295 | ' deleted', |
|
296 | 296 | len(repos)) % {'num': len(repos)} |
|
297 | 297 | h.flash(msg, category='warning') |
|
298 | 298 | return redirect(url('repo_groups')) |
|
299 | 299 | |
|
300 | 300 | children = gr.children.all() |
|
301 | 301 | if children: |
|
302 | 302 | msg = ungettext( |
|
303 | 303 | 'This group contains %(num)d subgroup and cannot be deleted', |
|
304 | 304 | 'This group contains %(num)d subgroups and cannot be deleted', |
|
305 | 305 | len(children)) % {'num': len(children)} |
|
306 | 306 | h.flash(msg, category='warning') |
|
307 | 307 | return redirect(url('repo_groups')) |
|
308 | 308 | |
|
309 | 309 | try: |
|
310 | 310 | RepoGroupModel().delete(group_name) |
|
311 | 311 | Session().commit() |
|
312 | 312 | h.flash(_('Removed repository group %s') % group_name, |
|
313 | 313 | category='success') |
|
314 | 314 | # TODO: in future action_logger(, '', '', '', self.sa) |
|
315 | 315 | except Exception: |
|
316 | 316 | log.exception("Exception during deletion of repository group") |
|
317 | 317 | h.flash(_('Error occurred during deletion of repository group %s') |
|
318 | 318 | % group_name, category='error') |
|
319 | 319 | |
|
320 | 320 | return redirect(url('repo_groups')) |
|
321 | 321 | |
|
322 | 322 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
323 | 323 | def edit(self, group_name): |
|
324 | 324 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
325 | 325 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
326 | 326 | c.active = 'settings' |
|
327 | 327 | |
|
328 | 328 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
329 | 329 | # we can only allow moving empty group if it's already a top-level |
|
330 | 330 | # group, ie has no parents, or we're admin |
|
331 | 331 | can_create_in_root = self._can_create_repo_group() |
|
332 | 332 | show_root_location = can_create_in_root |
|
333 | 333 | if not c.repo_group.parent_group: |
|
334 | 334 | # this group don't have a parrent so we should show empty value |
|
335 | 335 | show_root_location = True |
|
336 | 336 | self.__load_defaults(allow_empty_group=show_root_location, |
|
337 | 337 | repo_group=c.repo_group) |
|
338 | 338 | defaults = self.__load_data(c.repo_group.group_id) |
|
339 | 339 | |
|
340 | 340 | return htmlfill.render( |
|
341 |
render('admin/repo_groups/repo_group_edit. |
|
|
341 | render('admin/repo_groups/repo_group_edit.mako'), | |
|
342 | 342 | defaults=defaults, |
|
343 | 343 | encoding="UTF-8", |
|
344 | 344 | force_defaults=False |
|
345 | 345 | ) |
|
346 | 346 | |
|
347 | 347 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
348 | 348 | def edit_repo_group_advanced(self, group_name): |
|
349 | 349 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
350 | 350 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
351 | 351 | c.active = 'advanced' |
|
352 | 352 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
353 | 353 | |
|
354 |
return render('admin/repo_groups/repo_group_edit. |
|
|
354 | return render('admin/repo_groups/repo_group_edit.mako') | |
|
355 | 355 | |
|
356 | 356 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
357 | 357 | def edit_repo_group_perms(self, group_name): |
|
358 | 358 | """GET /repo_groups/group_name/edit: Form to edit an existing item""" |
|
359 | 359 | # url('edit_repo_group', group_name=GROUP_NAME) |
|
360 | 360 | c.active = 'perms' |
|
361 | 361 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
362 | 362 | self.__load_defaults() |
|
363 | 363 | defaults = self.__load_data(c.repo_group.group_id) |
|
364 | 364 | |
|
365 | 365 | return htmlfill.render( |
|
366 |
render('admin/repo_groups/repo_group_edit. |
|
|
366 | render('admin/repo_groups/repo_group_edit.mako'), | |
|
367 | 367 | defaults=defaults, |
|
368 | 368 | encoding="UTF-8", |
|
369 | 369 | force_defaults=False |
|
370 | 370 | ) |
|
371 | 371 | |
|
372 | 372 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
373 | 373 | @auth.CSRFRequired() |
|
374 | 374 | def update_perms(self, group_name): |
|
375 | 375 | """ |
|
376 | 376 | Update permissions for given repository group |
|
377 | 377 | |
|
378 | 378 | :param group_name: |
|
379 | 379 | """ |
|
380 | 380 | |
|
381 | 381 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
382 | 382 | valid_recursive_choices = ['none', 'repos', 'groups', 'all'] |
|
383 | 383 | form = RepoGroupPermsForm(valid_recursive_choices)().to_python( |
|
384 | 384 | request.POST) |
|
385 | 385 | |
|
386 | 386 | if not c.rhodecode_user.is_admin: |
|
387 | 387 | if self._revoke_perms_on_yourself(form): |
|
388 | 388 | msg = _('Cannot change permission for yourself as admin') |
|
389 | 389 | h.flash(msg, category='warning') |
|
390 | 390 | return redirect( |
|
391 | 391 | url('edit_repo_group_perms', group_name=group_name)) |
|
392 | 392 | |
|
393 | 393 | # iterate over all members(if in recursive mode) of this groups and |
|
394 | 394 | # set the permissions ! |
|
395 | 395 | # this can be potentially heavy operation |
|
396 | 396 | RepoGroupModel().update_permissions( |
|
397 | 397 | c.repo_group, |
|
398 | 398 | form['perm_additions'], form['perm_updates'], |
|
399 | 399 | form['perm_deletions'], form['recursive']) |
|
400 | 400 | |
|
401 | 401 | # TODO: implement this |
|
402 | 402 | # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
403 | 403 | # repo_name, self.ip_addr, self.sa) |
|
404 | 404 | Session().commit() |
|
405 | 405 | h.flash(_('Repository Group permissions updated'), category='success') |
|
406 | 406 | return redirect(url('edit_repo_group_perms', group_name=group_name)) |
@@ -1,888 +1,888 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 | Repositories controller for RhodeCode |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | import traceback |
|
28 | 28 | |
|
29 | 29 | import formencode |
|
30 | 30 | from formencode import htmlfill |
|
31 | 31 | from pylons import request, tmpl_context as c, url |
|
32 | 32 | from pylons.controllers.util import redirect |
|
33 | 33 | from pylons.i18n.translation import _ |
|
34 | 34 | from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest |
|
35 | 35 | |
|
36 | 36 | import rhodecode |
|
37 | 37 | from rhodecode.lib import auth, helpers as h |
|
38 | 38 | from rhodecode.lib.auth import ( |
|
39 | 39 | LoginRequired, HasPermissionAllDecorator, |
|
40 | 40 | HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, |
|
41 | 41 | HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator) |
|
42 | 42 | from rhodecode.lib.base import BaseRepoController, render |
|
43 | 43 | from rhodecode.lib.ext_json import json |
|
44 | 44 | from rhodecode.lib.exceptions import AttachedForksError |
|
45 | 45 | from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify |
|
46 | 46 | from rhodecode.lib.utils2 import safe_int, str2bool |
|
47 | 47 | from rhodecode.lib.vcs import RepositoryError |
|
48 | 48 | from rhodecode.model.db import ( |
|
49 | 49 | User, Repository, UserFollowing, RepoGroup, RepositoryField) |
|
50 | 50 | from rhodecode.model.forms import ( |
|
51 | 51 | RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm, |
|
52 | 52 | IssueTrackerPatternsForm) |
|
53 | 53 | from rhodecode.model.meta import Session |
|
54 | 54 | from rhodecode.model.repo import RepoModel |
|
55 | 55 | from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList |
|
56 | 56 | from rhodecode.model.settings import ( |
|
57 | 57 | SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel, |
|
58 | 58 | SettingNotFound) |
|
59 | 59 | |
|
60 | 60 | log = logging.getLogger(__name__) |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | class ReposController(BaseRepoController): |
|
64 | 64 | """ |
|
65 | 65 | REST Controller styled on the Atom Publishing Protocol""" |
|
66 | 66 | # To properly map this controller, ensure your config/routing.py |
|
67 | 67 | # file has a resource setup: |
|
68 | 68 | # map.resource('repo', 'repos') |
|
69 | 69 | |
|
70 | 70 | @LoginRequired() |
|
71 | 71 | def __before__(self): |
|
72 | 72 | super(ReposController, self).__before__() |
|
73 | 73 | |
|
74 | 74 | def _load_repo(self, repo_name): |
|
75 | 75 | repo_obj = Repository.get_by_repo_name(repo_name) |
|
76 | 76 | |
|
77 | 77 | if repo_obj is None: |
|
78 | 78 | h.not_mapped_error(repo_name) |
|
79 | 79 | return redirect(url('repos')) |
|
80 | 80 | |
|
81 | 81 | return repo_obj |
|
82 | 82 | |
|
83 | 83 | def __load_defaults(self, repo=None): |
|
84 | 84 | acl_groups = RepoGroupList(RepoGroup.query().all(), |
|
85 | 85 | perm_set=['group.write', 'group.admin']) |
|
86 | 86 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
87 | 87 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
88 | 88 | |
|
89 | 89 | # in case someone no longer have a group.write access to a repository |
|
90 | 90 | # pre fill the list with this entry, we don't care if this is the same |
|
91 | 91 | # but it will allow saving repo data properly. |
|
92 | 92 | |
|
93 | 93 | repo_group = None |
|
94 | 94 | if repo: |
|
95 | 95 | repo_group = repo.group |
|
96 | 96 | if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices: |
|
97 | 97 | c.repo_groups_choices.append(unicode(repo_group.group_id)) |
|
98 | 98 | c.repo_groups.append(RepoGroup._generate_choice(repo_group)) |
|
99 | 99 | |
|
100 | 100 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
101 | 101 | c.landing_revs_choices = choices |
|
102 | 102 | |
|
103 | 103 | def __load_data(self, repo_name=None): |
|
104 | 104 | """ |
|
105 | 105 | Load defaults settings for edit, and update |
|
106 | 106 | |
|
107 | 107 | :param repo_name: |
|
108 | 108 | """ |
|
109 | 109 | c.repo_info = self._load_repo(repo_name) |
|
110 | 110 | self.__load_defaults(c.repo_info) |
|
111 | 111 | |
|
112 | 112 | # override defaults for exact repo info here git/hg etc |
|
113 | 113 | if not c.repository_requirements_missing: |
|
114 | 114 | choices, c.landing_revs = ScmModel().get_repo_landing_revs( |
|
115 | 115 | c.repo_info) |
|
116 | 116 | c.landing_revs_choices = choices |
|
117 | 117 | defaults = RepoModel()._get_defaults(repo_name) |
|
118 | 118 | |
|
119 | 119 | return defaults |
|
120 | 120 | |
|
121 | 121 | def _log_creation_exception(self, e, repo_name): |
|
122 | 122 | reason = None |
|
123 | 123 | if len(e.args) == 2: |
|
124 | 124 | reason = e.args[1] |
|
125 | 125 | |
|
126 | 126 | if reason == 'INVALID_CERTIFICATE': |
|
127 | 127 | log.exception( |
|
128 | 128 | 'Exception creating a repository: invalid certificate') |
|
129 | 129 | msg = (_('Error creating repository %s: invalid certificate') |
|
130 | 130 | % repo_name) |
|
131 | 131 | else: |
|
132 | 132 | log.exception("Exception creating a repository") |
|
133 | 133 | msg = (_('Error creating repository %s') |
|
134 | 134 | % repo_name) |
|
135 | 135 | |
|
136 | 136 | return msg |
|
137 | 137 | |
|
138 | 138 | @NotAnonymous() |
|
139 | 139 | def index(self, format='html'): |
|
140 | 140 | """GET /repos: All items in the collection""" |
|
141 | 141 | # url('repos') |
|
142 | 142 | |
|
143 | 143 | repo_list = Repository.get_all_repos() |
|
144 | 144 | c.repo_list = RepoList(repo_list, perm_set=['repository.admin']) |
|
145 | 145 | repos_data = RepoModel().get_repos_as_dict( |
|
146 | 146 | repo_list=c.repo_list, admin=True, super_user_actions=True) |
|
147 | 147 | # json used to render the grid |
|
148 | 148 | c.data = json.dumps(repos_data) |
|
149 | 149 | |
|
150 |
return render('admin/repos/repos. |
|
|
150 | return render('admin/repos/repos.mako') | |
|
151 | 151 | |
|
152 | 152 | # perms check inside |
|
153 | 153 | @NotAnonymous() |
|
154 | 154 | @auth.CSRFRequired() |
|
155 | 155 | def create(self): |
|
156 | 156 | """ |
|
157 | 157 | POST /repos: Create a new item""" |
|
158 | 158 | # url('repos') |
|
159 | 159 | |
|
160 | 160 | self.__load_defaults() |
|
161 | 161 | form_result = {} |
|
162 | 162 | task_id = None |
|
163 | 163 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
164 | 164 | try: |
|
165 | 165 | # CanWriteToGroup validators checks permissions of this POST |
|
166 | 166 | form_result = RepoForm(repo_groups=c.repo_groups_choices, |
|
167 | 167 | landing_revs=c.landing_revs_choices)()\ |
|
168 | 168 | .to_python(dict(request.POST)) |
|
169 | 169 | |
|
170 | 170 | # create is done sometimes async on celery, db transaction |
|
171 | 171 | # management is handled there. |
|
172 | 172 | task = RepoModel().create(form_result, c.rhodecode_user.user_id) |
|
173 | 173 | from celery.result import BaseAsyncResult |
|
174 | 174 | if isinstance(task, BaseAsyncResult): |
|
175 | 175 | task_id = task.task_id |
|
176 | 176 | except formencode.Invalid as errors: |
|
177 | 177 | return htmlfill.render( |
|
178 |
render('admin/repos/repo_add. |
|
|
178 | render('admin/repos/repo_add.mako'), | |
|
179 | 179 | defaults=errors.value, |
|
180 | 180 | errors=errors.error_dict or {}, |
|
181 | 181 | prefix_error=False, |
|
182 | 182 | encoding="UTF-8", |
|
183 | 183 | force_defaults=False) |
|
184 | 184 | |
|
185 | 185 | except Exception as e: |
|
186 | 186 | msg = self._log_creation_exception(e, form_result.get('repo_name')) |
|
187 | 187 | h.flash(msg, category='error') |
|
188 | 188 | return redirect(url('home')) |
|
189 | 189 | |
|
190 | 190 | return redirect(h.url('repo_creating_home', |
|
191 | 191 | repo_name=form_result['repo_name_full'], |
|
192 | 192 | task_id=task_id)) |
|
193 | 193 | |
|
194 | 194 | # perms check inside |
|
195 | 195 | @NotAnonymous() |
|
196 | 196 | def create_repository(self): |
|
197 | 197 | """GET /_admin/create_repository: Form to create a new item""" |
|
198 | 198 | new_repo = request.GET.get('repo', '') |
|
199 | 199 | parent_group = safe_int(request.GET.get('parent_group')) |
|
200 | 200 | _gr = RepoGroup.get(parent_group) |
|
201 | 201 | |
|
202 | 202 | if not HasPermissionAny('hg.admin', 'hg.create.repository')(): |
|
203 | 203 | # you're not super admin nor have global create permissions, |
|
204 | 204 | # but maybe you have at least write permission to a parent group ? |
|
205 | 205 | |
|
206 | 206 | gr_name = _gr.group_name if _gr else None |
|
207 | 207 | # create repositories with write permission on group is set to true |
|
208 | 208 | create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')() |
|
209 | 209 | group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name) |
|
210 | 210 | group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name) |
|
211 | 211 | if not (group_admin or (group_write and create_on_write)): |
|
212 | 212 | raise HTTPForbidden |
|
213 | 213 | |
|
214 | 214 | acl_groups = RepoGroupList(RepoGroup.query().all(), |
|
215 | 215 | perm_set=['group.write', 'group.admin']) |
|
216 | 216 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
217 | 217 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
218 | 218 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
219 | 219 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
220 | 220 | c.new_repo = repo_name_slug(new_repo) |
|
221 | 221 | |
|
222 | 222 | # apply the defaults from defaults page |
|
223 | 223 | defaults = SettingsModel().get_default_repo_settings(strip_prefix=True) |
|
224 | 224 | # set checkbox to autochecked |
|
225 | 225 | defaults['repo_copy_permissions'] = True |
|
226 | 226 | |
|
227 | 227 | parent_group_choice = '-1' |
|
228 | 228 | if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group: |
|
229 | 229 | parent_group_choice = c.rhodecode_user.personal_repo_group |
|
230 | 230 | |
|
231 | 231 | if parent_group and _gr: |
|
232 | 232 | if parent_group in [x[0] for x in c.repo_groups]: |
|
233 | 233 | parent_group_choice = unicode(parent_group) |
|
234 | 234 | |
|
235 | 235 | defaults.update({'repo_group': parent_group_choice}) |
|
236 | 236 | |
|
237 | 237 | return htmlfill.render( |
|
238 |
render('admin/repos/repo_add. |
|
|
238 | render('admin/repos/repo_add.mako'), | |
|
239 | 239 | defaults=defaults, |
|
240 | 240 | errors={}, |
|
241 | 241 | prefix_error=False, |
|
242 | 242 | encoding="UTF-8", |
|
243 | 243 | force_defaults=False |
|
244 | 244 | ) |
|
245 | 245 | |
|
246 | 246 | @NotAnonymous() |
|
247 | 247 | def repo_creating(self, repo_name): |
|
248 | 248 | c.repo = repo_name |
|
249 | 249 | c.task_id = request.GET.get('task_id') |
|
250 | 250 | if not c.repo: |
|
251 | 251 | raise HTTPNotFound() |
|
252 |
return render('admin/repos/repo_creating. |
|
|
252 | return render('admin/repos/repo_creating.mako') | |
|
253 | 253 | |
|
254 | 254 | @NotAnonymous() |
|
255 | 255 | @jsonify |
|
256 | 256 | def repo_check(self, repo_name): |
|
257 | 257 | c.repo = repo_name |
|
258 | 258 | task_id = request.GET.get('task_id') |
|
259 | 259 | |
|
260 | 260 | if task_id and task_id not in ['None']: |
|
261 | 261 | import rhodecode |
|
262 | 262 | from celery.result import AsyncResult |
|
263 | 263 | if rhodecode.CELERY_ENABLED: |
|
264 | 264 | task = AsyncResult(task_id) |
|
265 | 265 | if task.failed(): |
|
266 | 266 | msg = self._log_creation_exception(task.result, c.repo) |
|
267 | 267 | h.flash(msg, category='error') |
|
268 | 268 | return redirect(url('home'), code=501) |
|
269 | 269 | |
|
270 | 270 | repo = Repository.get_by_repo_name(repo_name) |
|
271 | 271 | if repo and repo.repo_state == Repository.STATE_CREATED: |
|
272 | 272 | if repo.clone_uri: |
|
273 | 273 | clone_uri = repo.clone_uri_hidden |
|
274 | 274 | h.flash(_('Created repository %s from %s') |
|
275 | 275 | % (repo.repo_name, clone_uri), category='success') |
|
276 | 276 | else: |
|
277 | 277 | repo_url = h.link_to(repo.repo_name, |
|
278 | 278 | h.url('summary_home', |
|
279 | 279 | repo_name=repo.repo_name)) |
|
280 | 280 | fork = repo.fork |
|
281 | 281 | if fork: |
|
282 | 282 | fork_name = fork.repo_name |
|
283 | 283 | h.flash(h.literal(_('Forked repository %s as %s') |
|
284 | 284 | % (fork_name, repo_url)), category='success') |
|
285 | 285 | else: |
|
286 | 286 | h.flash(h.literal(_('Created repository %s') % repo_url), |
|
287 | 287 | category='success') |
|
288 | 288 | return {'result': True} |
|
289 | 289 | return {'result': False} |
|
290 | 290 | |
|
291 | 291 | @HasRepoPermissionAllDecorator('repository.admin') |
|
292 | 292 | @auth.CSRFRequired() |
|
293 | 293 | def update(self, repo_name): |
|
294 | 294 | """ |
|
295 | 295 | PUT /repos/repo_name: Update an existing item""" |
|
296 | 296 | # Forms posted to this method should contain a hidden field: |
|
297 | 297 | # <input type="hidden" name="_method" value="PUT" /> |
|
298 | 298 | # Or using helpers: |
|
299 | 299 | # h.form(url('repo', repo_name=ID), |
|
300 | 300 | # method='put') |
|
301 | 301 | # url('repo', repo_name=ID) |
|
302 | 302 | |
|
303 | 303 | self.__load_data(repo_name) |
|
304 | 304 | c.active = 'settings' |
|
305 | 305 | c.repo_fields = RepositoryField.query()\ |
|
306 | 306 | .filter(RepositoryField.repository == c.repo_info).all() |
|
307 | 307 | |
|
308 | 308 | repo_model = RepoModel() |
|
309 | 309 | changed_name = repo_name |
|
310 | 310 | |
|
311 | 311 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
312 | 312 | # override the choices with extracted revisions ! |
|
313 | 313 | repo = Repository.get_by_repo_name(repo_name) |
|
314 | 314 | old_data = { |
|
315 | 315 | 'repo_name': repo_name, |
|
316 | 316 | 'repo_group': repo.group.get_dict() if repo.group else {}, |
|
317 | 317 | 'repo_type': repo.repo_type, |
|
318 | 318 | } |
|
319 | 319 | _form = RepoForm( |
|
320 | 320 | edit=True, old_data=old_data, repo_groups=c.repo_groups_choices, |
|
321 | 321 | landing_revs=c.landing_revs_choices, allow_disabled=True)() |
|
322 | 322 | |
|
323 | 323 | try: |
|
324 | 324 | form_result = _form.to_python(dict(request.POST)) |
|
325 | 325 | repo = repo_model.update(repo_name, **form_result) |
|
326 | 326 | ScmModel().mark_for_invalidation(repo_name) |
|
327 | 327 | h.flash(_('Repository %s updated successfully') % repo_name, |
|
328 | 328 | category='success') |
|
329 | 329 | changed_name = repo.repo_name |
|
330 | 330 | action_logger(c.rhodecode_user, 'admin_updated_repo', |
|
331 | 331 | changed_name, self.ip_addr, self.sa) |
|
332 | 332 | Session().commit() |
|
333 | 333 | except formencode.Invalid as errors: |
|
334 | 334 | defaults = self.__load_data(repo_name) |
|
335 | 335 | defaults.update(errors.value) |
|
336 | 336 | return htmlfill.render( |
|
337 |
render('admin/repos/repo_edit. |
|
|
337 | render('admin/repos/repo_edit.mako'), | |
|
338 | 338 | defaults=defaults, |
|
339 | 339 | errors=errors.error_dict or {}, |
|
340 | 340 | prefix_error=False, |
|
341 | 341 | encoding="UTF-8", |
|
342 | 342 | force_defaults=False) |
|
343 | 343 | |
|
344 | 344 | except Exception: |
|
345 | 345 | log.exception("Exception during update of repository") |
|
346 | 346 | h.flash(_('Error occurred during update of repository %s') \ |
|
347 | 347 | % repo_name, category='error') |
|
348 | 348 | return redirect(url('edit_repo', repo_name=changed_name)) |
|
349 | 349 | |
|
350 | 350 | @HasRepoPermissionAllDecorator('repository.admin') |
|
351 | 351 | @auth.CSRFRequired() |
|
352 | 352 | def delete(self, repo_name): |
|
353 | 353 | """ |
|
354 | 354 | DELETE /repos/repo_name: Delete an existing item""" |
|
355 | 355 | # Forms posted to this method should contain a hidden field: |
|
356 | 356 | # <input type="hidden" name="_method" value="DELETE" /> |
|
357 | 357 | # Or using helpers: |
|
358 | 358 | # h.form(url('repo', repo_name=ID), |
|
359 | 359 | # method='delete') |
|
360 | 360 | # url('repo', repo_name=ID) |
|
361 | 361 | |
|
362 | 362 | repo_model = RepoModel() |
|
363 | 363 | repo = repo_model.get_by_repo_name(repo_name) |
|
364 | 364 | if not repo: |
|
365 | 365 | h.not_mapped_error(repo_name) |
|
366 | 366 | return redirect(url('repos')) |
|
367 | 367 | try: |
|
368 | 368 | _forks = repo.forks.count() |
|
369 | 369 | handle_forks = None |
|
370 | 370 | if _forks and request.POST.get('forks'): |
|
371 | 371 | do = request.POST['forks'] |
|
372 | 372 | if do == 'detach_forks': |
|
373 | 373 | handle_forks = 'detach' |
|
374 | 374 | h.flash(_('Detached %s forks') % _forks, category='success') |
|
375 | 375 | elif do == 'delete_forks': |
|
376 | 376 | handle_forks = 'delete' |
|
377 | 377 | h.flash(_('Deleted %s forks') % _forks, category='success') |
|
378 | 378 | repo_model.delete(repo, forks=handle_forks) |
|
379 | 379 | action_logger(c.rhodecode_user, 'admin_deleted_repo', |
|
380 | 380 | repo_name, self.ip_addr, self.sa) |
|
381 | 381 | ScmModel().mark_for_invalidation(repo_name) |
|
382 | 382 | h.flash(_('Deleted repository %s') % repo_name, category='success') |
|
383 | 383 | Session().commit() |
|
384 | 384 | except AttachedForksError: |
|
385 | 385 | h.flash(_('Cannot delete %s it still contains attached forks') |
|
386 | 386 | % repo_name, category='warning') |
|
387 | 387 | |
|
388 | 388 | except Exception: |
|
389 | 389 | log.exception("Exception during deletion of repository") |
|
390 | 390 | h.flash(_('An error occurred during deletion of %s') % repo_name, |
|
391 | 391 | category='error') |
|
392 | 392 | |
|
393 | 393 | return redirect(url('repos')) |
|
394 | 394 | |
|
395 | 395 | @HasPermissionAllDecorator('hg.admin') |
|
396 | 396 | def show(self, repo_name, format='html'): |
|
397 | 397 | """GET /repos/repo_name: Show a specific item""" |
|
398 | 398 | # url('repo', repo_name=ID) |
|
399 | 399 | |
|
400 | 400 | @HasRepoPermissionAllDecorator('repository.admin') |
|
401 | 401 | def edit(self, repo_name): |
|
402 | 402 | """GET /repo_name/settings: Form to edit an existing item""" |
|
403 | 403 | # url('edit_repo', repo_name=ID) |
|
404 | 404 | defaults = self.__load_data(repo_name) |
|
405 | 405 | if 'clone_uri' in defaults: |
|
406 | 406 | del defaults['clone_uri'] |
|
407 | 407 | |
|
408 | 408 | c.repo_fields = RepositoryField.query()\ |
|
409 | 409 | .filter(RepositoryField.repository == c.repo_info).all() |
|
410 | 410 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
411 | 411 | c.active = 'settings' |
|
412 | 412 | return htmlfill.render( |
|
413 |
render('admin/repos/repo_edit. |
|
|
413 | render('admin/repos/repo_edit.mako'), | |
|
414 | 414 | defaults=defaults, |
|
415 | 415 | encoding="UTF-8", |
|
416 | 416 | force_defaults=False) |
|
417 | 417 | |
|
418 | 418 | @HasRepoPermissionAllDecorator('repository.admin') |
|
419 | 419 | def edit_permissions(self, repo_name): |
|
420 | 420 | """GET /repo_name/settings: Form to edit an existing item""" |
|
421 | 421 | # url('edit_repo', repo_name=ID) |
|
422 | 422 | c.repo_info = self._load_repo(repo_name) |
|
423 | 423 | c.active = 'permissions' |
|
424 | 424 | defaults = RepoModel()._get_defaults(repo_name) |
|
425 | 425 | |
|
426 | 426 | return htmlfill.render( |
|
427 |
render('admin/repos/repo_edit. |
|
|
427 | render('admin/repos/repo_edit.mako'), | |
|
428 | 428 | defaults=defaults, |
|
429 | 429 | encoding="UTF-8", |
|
430 | 430 | force_defaults=False) |
|
431 | 431 | |
|
432 | 432 | @HasRepoPermissionAllDecorator('repository.admin') |
|
433 | 433 | @auth.CSRFRequired() |
|
434 | 434 | def edit_permissions_update(self, repo_name): |
|
435 | 435 | form = RepoPermsForm()().to_python(request.POST) |
|
436 | 436 | RepoModel().update_permissions(repo_name, |
|
437 | 437 | form['perm_additions'], form['perm_updates'], form['perm_deletions']) |
|
438 | 438 | |
|
439 | 439 | #TODO: implement this |
|
440 | 440 | #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
441 | 441 | # repo_name, self.ip_addr, self.sa) |
|
442 | 442 | Session().commit() |
|
443 | 443 | h.flash(_('Repository permissions updated'), category='success') |
|
444 | 444 | return redirect(url('edit_repo_perms', repo_name=repo_name)) |
|
445 | 445 | |
|
446 | 446 | @HasRepoPermissionAllDecorator('repository.admin') |
|
447 | 447 | def edit_fields(self, repo_name): |
|
448 | 448 | """GET /repo_name/settings: Form to edit an existing item""" |
|
449 | 449 | # url('edit_repo', repo_name=ID) |
|
450 | 450 | c.repo_info = self._load_repo(repo_name) |
|
451 | 451 | c.repo_fields = RepositoryField.query()\ |
|
452 | 452 | .filter(RepositoryField.repository == c.repo_info).all() |
|
453 | 453 | c.active = 'fields' |
|
454 | 454 | if request.POST: |
|
455 | 455 | |
|
456 | 456 | return redirect(url('repo_edit_fields')) |
|
457 |
return render('admin/repos/repo_edit. |
|
|
457 | return render('admin/repos/repo_edit.mako') | |
|
458 | 458 | |
|
459 | 459 | @HasRepoPermissionAllDecorator('repository.admin') |
|
460 | 460 | @auth.CSRFRequired() |
|
461 | 461 | def create_repo_field(self, repo_name): |
|
462 | 462 | try: |
|
463 | 463 | form_result = RepoFieldForm()().to_python(dict(request.POST)) |
|
464 | 464 | RepoModel().add_repo_field( |
|
465 | 465 | repo_name, form_result['new_field_key'], |
|
466 | 466 | field_type=form_result['new_field_type'], |
|
467 | 467 | field_value=form_result['new_field_value'], |
|
468 | 468 | field_label=form_result['new_field_label'], |
|
469 | 469 | field_desc=form_result['new_field_desc']) |
|
470 | 470 | |
|
471 | 471 | Session().commit() |
|
472 | 472 | except Exception as e: |
|
473 | 473 | log.exception("Exception creating field") |
|
474 | 474 | msg = _('An error occurred during creation of field') |
|
475 | 475 | if isinstance(e, formencode.Invalid): |
|
476 | 476 | msg += ". " + e.msg |
|
477 | 477 | h.flash(msg, category='error') |
|
478 | 478 | return redirect(url('edit_repo_fields', repo_name=repo_name)) |
|
479 | 479 | |
|
480 | 480 | @HasRepoPermissionAllDecorator('repository.admin') |
|
481 | 481 | @auth.CSRFRequired() |
|
482 | 482 | def delete_repo_field(self, repo_name, field_id): |
|
483 | 483 | field = RepositoryField.get_or_404(field_id) |
|
484 | 484 | try: |
|
485 | 485 | RepoModel().delete_repo_field(repo_name, field.field_key) |
|
486 | 486 | Session().commit() |
|
487 | 487 | except Exception as e: |
|
488 | 488 | log.exception("Exception during removal of field") |
|
489 | 489 | msg = _('An error occurred during removal of field') |
|
490 | 490 | h.flash(msg, category='error') |
|
491 | 491 | return redirect(url('edit_repo_fields', repo_name=repo_name)) |
|
492 | 492 | |
|
493 | 493 | @HasRepoPermissionAllDecorator('repository.admin') |
|
494 | 494 | def edit_advanced(self, repo_name): |
|
495 | 495 | """GET /repo_name/settings: Form to edit an existing item""" |
|
496 | 496 | # url('edit_repo', repo_name=ID) |
|
497 | 497 | c.repo_info = self._load_repo(repo_name) |
|
498 | 498 | c.default_user_id = User.get_default_user().user_id |
|
499 | 499 | c.in_public_journal = UserFollowing.query()\ |
|
500 | 500 | .filter(UserFollowing.user_id == c.default_user_id)\ |
|
501 | 501 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() |
|
502 | 502 | |
|
503 | 503 | c.active = 'advanced' |
|
504 | 504 | c.has_origin_repo_read_perm = False |
|
505 | 505 | if c.repo_info.fork: |
|
506 | 506 | c.has_origin_repo_read_perm = h.HasRepoPermissionAny( |
|
507 | 507 | 'repository.write', 'repository.read', 'repository.admin')( |
|
508 | 508 | c.repo_info.fork.repo_name, 'repo set as fork page') |
|
509 | 509 | |
|
510 | 510 | if request.POST: |
|
511 | 511 | return redirect(url('repo_edit_advanced')) |
|
512 |
return render('admin/repos/repo_edit. |
|
|
512 | return render('admin/repos/repo_edit.mako') | |
|
513 | 513 | |
|
514 | 514 | @HasRepoPermissionAllDecorator('repository.admin') |
|
515 | 515 | @auth.CSRFRequired() |
|
516 | 516 | def edit_advanced_journal(self, repo_name): |
|
517 | 517 | """ |
|
518 | 518 | Set's this repository to be visible in public journal, |
|
519 | 519 | in other words assing default user to follow this repo |
|
520 | 520 | |
|
521 | 521 | :param repo_name: |
|
522 | 522 | """ |
|
523 | 523 | |
|
524 | 524 | try: |
|
525 | 525 | repo_id = Repository.get_by_repo_name(repo_name).repo_id |
|
526 | 526 | user_id = User.get_default_user().user_id |
|
527 | 527 | self.scm_model.toggle_following_repo(repo_id, user_id) |
|
528 | 528 | h.flash(_('Updated repository visibility in public journal'), |
|
529 | 529 | category='success') |
|
530 | 530 | Session().commit() |
|
531 | 531 | except Exception: |
|
532 | 532 | h.flash(_('An error occurred during setting this' |
|
533 | 533 | ' repository in public journal'), |
|
534 | 534 | category='error') |
|
535 | 535 | |
|
536 | 536 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
537 | 537 | |
|
538 | 538 | @HasRepoPermissionAllDecorator('repository.admin') |
|
539 | 539 | @auth.CSRFRequired() |
|
540 | 540 | def edit_advanced_fork(self, repo_name): |
|
541 | 541 | """ |
|
542 | 542 | Mark given repository as a fork of another |
|
543 | 543 | |
|
544 | 544 | :param repo_name: |
|
545 | 545 | """ |
|
546 | 546 | |
|
547 | 547 | new_fork_id = request.POST.get('id_fork_of') |
|
548 | 548 | try: |
|
549 | 549 | |
|
550 | 550 | if new_fork_id and not new_fork_id.isdigit(): |
|
551 | 551 | log.error('Given fork id %s is not an INT', new_fork_id) |
|
552 | 552 | |
|
553 | 553 | fork_id = safe_int(new_fork_id) |
|
554 | 554 | repo = ScmModel().mark_as_fork(repo_name, fork_id, |
|
555 | 555 | c.rhodecode_user.username) |
|
556 | 556 | fork = repo.fork.repo_name if repo.fork else _('Nothing') |
|
557 | 557 | Session().commit() |
|
558 | 558 | h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork), |
|
559 | 559 | category='success') |
|
560 | 560 | except RepositoryError as e: |
|
561 | 561 | log.exception("Repository Error occurred") |
|
562 | 562 | h.flash(str(e), category='error') |
|
563 | 563 | except Exception as e: |
|
564 | 564 | log.exception("Exception while editing fork") |
|
565 | 565 | h.flash(_('An error occurred during this operation'), |
|
566 | 566 | category='error') |
|
567 | 567 | |
|
568 | 568 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
569 | 569 | |
|
570 | 570 | @HasRepoPermissionAllDecorator('repository.admin') |
|
571 | 571 | @auth.CSRFRequired() |
|
572 | 572 | def edit_advanced_locking(self, repo_name): |
|
573 | 573 | """ |
|
574 | 574 | Unlock repository when it is locked ! |
|
575 | 575 | |
|
576 | 576 | :param repo_name: |
|
577 | 577 | """ |
|
578 | 578 | try: |
|
579 | 579 | repo = Repository.get_by_repo_name(repo_name) |
|
580 | 580 | if request.POST.get('set_lock'): |
|
581 | 581 | Repository.lock(repo, c.rhodecode_user.user_id, |
|
582 | 582 | lock_reason=Repository.LOCK_WEB) |
|
583 | 583 | h.flash(_('Locked repository'), category='success') |
|
584 | 584 | elif request.POST.get('set_unlock'): |
|
585 | 585 | Repository.unlock(repo) |
|
586 | 586 | h.flash(_('Unlocked repository'), category='success') |
|
587 | 587 | except Exception as e: |
|
588 | 588 | log.exception("Exception during unlocking") |
|
589 | 589 | h.flash(_('An error occurred during unlocking'), |
|
590 | 590 | category='error') |
|
591 | 591 | return redirect(url('edit_repo_advanced', repo_name=repo_name)) |
|
592 | 592 | |
|
593 | 593 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
594 | 594 | @auth.CSRFRequired() |
|
595 | 595 | def toggle_locking(self, repo_name): |
|
596 | 596 | """ |
|
597 | 597 | Toggle locking of repository by simple GET call to url |
|
598 | 598 | |
|
599 | 599 | :param repo_name: |
|
600 | 600 | """ |
|
601 | 601 | |
|
602 | 602 | try: |
|
603 | 603 | repo = Repository.get_by_repo_name(repo_name) |
|
604 | 604 | |
|
605 | 605 | if repo.enable_locking: |
|
606 | 606 | if repo.locked[0]: |
|
607 | 607 | Repository.unlock(repo) |
|
608 | 608 | action = _('Unlocked') |
|
609 | 609 | else: |
|
610 | 610 | Repository.lock(repo, c.rhodecode_user.user_id, |
|
611 | 611 | lock_reason=Repository.LOCK_WEB) |
|
612 | 612 | action = _('Locked') |
|
613 | 613 | |
|
614 | 614 | h.flash(_('Repository has been %s') % action, |
|
615 | 615 | category='success') |
|
616 | 616 | except Exception: |
|
617 | 617 | log.exception("Exception during unlocking") |
|
618 | 618 | h.flash(_('An error occurred during unlocking'), |
|
619 | 619 | category='error') |
|
620 | 620 | return redirect(url('summary_home', repo_name=repo_name)) |
|
621 | 621 | |
|
622 | 622 | @HasRepoPermissionAllDecorator('repository.admin') |
|
623 | 623 | @auth.CSRFRequired() |
|
624 | 624 | def edit_caches(self, repo_name): |
|
625 | 625 | """PUT /{repo_name}/settings/caches: invalidate the repo caches.""" |
|
626 | 626 | try: |
|
627 | 627 | ScmModel().mark_for_invalidation(repo_name, delete=True) |
|
628 | 628 | Session().commit() |
|
629 | 629 | h.flash(_('Cache invalidation successful'), |
|
630 | 630 | category='success') |
|
631 | 631 | except Exception: |
|
632 | 632 | log.exception("Exception during cache invalidation") |
|
633 | 633 | h.flash(_('An error occurred during cache invalidation'), |
|
634 | 634 | category='error') |
|
635 | 635 | |
|
636 | 636 | return redirect(url('edit_repo_caches', repo_name=c.repo_name)) |
|
637 | 637 | |
|
638 | 638 | @HasRepoPermissionAllDecorator('repository.admin') |
|
639 | 639 | def edit_caches_form(self, repo_name): |
|
640 | 640 | """GET /repo_name/settings: Form to edit an existing item""" |
|
641 | 641 | # url('edit_repo', repo_name=ID) |
|
642 | 642 | c.repo_info = self._load_repo(repo_name) |
|
643 | 643 | c.active = 'caches' |
|
644 | 644 | |
|
645 |
return render('admin/repos/repo_edit. |
|
|
645 | return render('admin/repos/repo_edit.mako') | |
|
646 | 646 | |
|
647 | 647 | @HasRepoPermissionAllDecorator('repository.admin') |
|
648 | 648 | @auth.CSRFRequired() |
|
649 | 649 | def edit_remote(self, repo_name): |
|
650 | 650 | """PUT /{repo_name}/settings/remote: edit the repo remote.""" |
|
651 | 651 | try: |
|
652 | 652 | ScmModel().pull_changes(repo_name, c.rhodecode_user.username) |
|
653 | 653 | h.flash(_('Pulled from remote location'), category='success') |
|
654 | 654 | except Exception: |
|
655 | 655 | log.exception("Exception during pull from remote") |
|
656 | 656 | h.flash(_('An error occurred during pull from remote location'), |
|
657 | 657 | category='error') |
|
658 | 658 | return redirect(url('edit_repo_remote', repo_name=c.repo_name)) |
|
659 | 659 | |
|
660 | 660 | @HasRepoPermissionAllDecorator('repository.admin') |
|
661 | 661 | def edit_remote_form(self, repo_name): |
|
662 | 662 | """GET /repo_name/settings: Form to edit an existing item""" |
|
663 | 663 | # url('edit_repo', repo_name=ID) |
|
664 | 664 | c.repo_info = self._load_repo(repo_name) |
|
665 | 665 | c.active = 'remote' |
|
666 | 666 | |
|
667 |
return render('admin/repos/repo_edit. |
|
|
667 | return render('admin/repos/repo_edit.mako') | |
|
668 | 668 | |
|
669 | 669 | @HasRepoPermissionAllDecorator('repository.admin') |
|
670 | 670 | @auth.CSRFRequired() |
|
671 | 671 | def edit_statistics(self, repo_name): |
|
672 | 672 | """PUT /{repo_name}/settings/statistics: reset the repo statistics.""" |
|
673 | 673 | try: |
|
674 | 674 | RepoModel().delete_stats(repo_name) |
|
675 | 675 | Session().commit() |
|
676 | 676 | except Exception as e: |
|
677 | 677 | log.error(traceback.format_exc()) |
|
678 | 678 | h.flash(_('An error occurred during deletion of repository stats'), |
|
679 | 679 | category='error') |
|
680 | 680 | return redirect(url('edit_repo_statistics', repo_name=c.repo_name)) |
|
681 | 681 | |
|
682 | 682 | @HasRepoPermissionAllDecorator('repository.admin') |
|
683 | 683 | def edit_statistics_form(self, repo_name): |
|
684 | 684 | """GET /repo_name/settings: Form to edit an existing item""" |
|
685 | 685 | # url('edit_repo', repo_name=ID) |
|
686 | 686 | c.repo_info = self._load_repo(repo_name) |
|
687 | 687 | repo = c.repo_info.scm_instance() |
|
688 | 688 | |
|
689 | 689 | if c.repo_info.stats: |
|
690 | 690 | # this is on what revision we ended up so we add +1 for count |
|
691 | 691 | last_rev = c.repo_info.stats.stat_on_revision + 1 |
|
692 | 692 | else: |
|
693 | 693 | last_rev = 0 |
|
694 | 694 | c.stats_revision = last_rev |
|
695 | 695 | |
|
696 | 696 | c.repo_last_rev = repo.count() |
|
697 | 697 | |
|
698 | 698 | if last_rev == 0 or c.repo_last_rev == 0: |
|
699 | 699 | c.stats_percentage = 0 |
|
700 | 700 | else: |
|
701 | 701 | c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100) |
|
702 | 702 | |
|
703 | 703 | c.active = 'statistics' |
|
704 | 704 | |
|
705 |
return render('admin/repos/repo_edit. |
|
|
705 | return render('admin/repos/repo_edit.mako') | |
|
706 | 706 | |
|
707 | 707 | @HasRepoPermissionAllDecorator('repository.admin') |
|
708 | 708 | @auth.CSRFRequired() |
|
709 | 709 | def repo_issuetracker_test(self, repo_name): |
|
710 | 710 | if request.is_xhr: |
|
711 | 711 | return h.urlify_commit_message( |
|
712 | 712 | request.POST.get('test_text', ''), |
|
713 | 713 | repo_name) |
|
714 | 714 | else: |
|
715 | 715 | raise HTTPBadRequest() |
|
716 | 716 | |
|
717 | 717 | @HasRepoPermissionAllDecorator('repository.admin') |
|
718 | 718 | @auth.CSRFRequired() |
|
719 | 719 | def repo_issuetracker_delete(self, repo_name): |
|
720 | 720 | uid = request.POST.get('uid') |
|
721 | 721 | repo_settings = IssueTrackerSettingsModel(repo=repo_name) |
|
722 | 722 | try: |
|
723 | 723 | repo_settings.delete_entries(uid) |
|
724 | 724 | except Exception: |
|
725 | 725 | h.flash(_('Error occurred during deleting issue tracker entry'), |
|
726 | 726 | category='error') |
|
727 | 727 | else: |
|
728 | 728 | h.flash(_('Removed issue tracker entry'), category='success') |
|
729 | 729 | return redirect(url('repo_settings_issuetracker', |
|
730 | 730 | repo_name=repo_name)) |
|
731 | 731 | |
|
732 | 732 | def _update_patterns(self, form, repo_settings): |
|
733 | 733 | for uid in form['delete_patterns']: |
|
734 | 734 | repo_settings.delete_entries(uid) |
|
735 | 735 | |
|
736 | 736 | for pattern in form['patterns']: |
|
737 | 737 | for setting, value, type_ in pattern: |
|
738 | 738 | sett = repo_settings.create_or_update_setting( |
|
739 | 739 | setting, value, type_) |
|
740 | 740 | Session().add(sett) |
|
741 | 741 | |
|
742 | 742 | Session().commit() |
|
743 | 743 | |
|
744 | 744 | @HasRepoPermissionAllDecorator('repository.admin') |
|
745 | 745 | @auth.CSRFRequired() |
|
746 | 746 | def repo_issuetracker_save(self, repo_name): |
|
747 | 747 | # Save inheritance |
|
748 | 748 | repo_settings = IssueTrackerSettingsModel(repo=repo_name) |
|
749 | 749 | inherited = (request.POST.get('inherit_global_issuetracker') |
|
750 | 750 | == "inherited") |
|
751 | 751 | repo_settings.inherit_global_settings = inherited |
|
752 | 752 | Session().commit() |
|
753 | 753 | |
|
754 | 754 | form = IssueTrackerPatternsForm()().to_python(request.POST) |
|
755 | 755 | if form: |
|
756 | 756 | self._update_patterns(form, repo_settings) |
|
757 | 757 | |
|
758 | 758 | h.flash(_('Updated issue tracker entries'), category='success') |
|
759 | 759 | return redirect(url('repo_settings_issuetracker', |
|
760 | 760 | repo_name=repo_name)) |
|
761 | 761 | |
|
762 | 762 | @HasRepoPermissionAllDecorator('repository.admin') |
|
763 | 763 | def repo_issuetracker(self, repo_name): |
|
764 | 764 | """GET /admin/settings/issue-tracker: All items in the collection""" |
|
765 | 765 | c.active = 'issuetracker' |
|
766 | 766 | c.data = 'data' |
|
767 | 767 | c.repo_info = self._load_repo(repo_name) |
|
768 | 768 | |
|
769 | 769 | repo = Repository.get_by_repo_name(repo_name) |
|
770 | 770 | c.settings_model = IssueTrackerSettingsModel(repo=repo) |
|
771 | 771 | c.global_patterns = c.settings_model.get_global_settings() |
|
772 | 772 | c.repo_patterns = c.settings_model.get_repo_settings() |
|
773 | 773 | |
|
774 |
return render('admin/repos/repo_edit. |
|
|
774 | return render('admin/repos/repo_edit.mako') | |
|
775 | 775 | |
|
776 | 776 | @HasRepoPermissionAllDecorator('repository.admin') |
|
777 | 777 | def repo_settings_vcs(self, repo_name): |
|
778 | 778 | """GET /{repo_name}/settings/vcs/: All items in the collection""" |
|
779 | 779 | |
|
780 | 780 | model = VcsSettingsModel(repo=repo_name) |
|
781 | 781 | |
|
782 | 782 | c.active = 'vcs' |
|
783 | 783 | c.global_svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
784 | 784 | c.global_svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
785 | 785 | c.svn_branch_patterns = model.get_repo_svn_branch_patterns() |
|
786 | 786 | c.svn_tag_patterns = model.get_repo_svn_tag_patterns() |
|
787 | 787 | c.repo_info = self._load_repo(repo_name) |
|
788 | 788 | defaults = self._vcs_form_defaults(repo_name) |
|
789 | 789 | c.inherit_global_settings = defaults['inherit_global_settings'] |
|
790 | 790 | c.labs_active = str2bool( |
|
791 | 791 | rhodecode.CONFIG.get('labs_settings_active', 'true')) |
|
792 | 792 | |
|
793 | 793 | return htmlfill.render( |
|
794 |
render('admin/repos/repo_edit. |
|
|
794 | render('admin/repos/repo_edit.mako'), | |
|
795 | 795 | defaults=defaults, |
|
796 | 796 | encoding="UTF-8", |
|
797 | 797 | force_defaults=False) |
|
798 | 798 | |
|
799 | 799 | @HasRepoPermissionAllDecorator('repository.admin') |
|
800 | 800 | @auth.CSRFRequired() |
|
801 | 801 | def repo_settings_vcs_update(self, repo_name): |
|
802 | 802 | """POST /{repo_name}/settings/vcs/: All items in the collection""" |
|
803 | 803 | c.active = 'vcs' |
|
804 | 804 | |
|
805 | 805 | model = VcsSettingsModel(repo=repo_name) |
|
806 | 806 | c.global_svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
807 | 807 | c.global_svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
808 | 808 | c.svn_branch_patterns = model.get_repo_svn_branch_patterns() |
|
809 | 809 | c.svn_tag_patterns = model.get_repo_svn_tag_patterns() |
|
810 | 810 | c.repo_info = self._load_repo(repo_name) |
|
811 | 811 | defaults = self._vcs_form_defaults(repo_name) |
|
812 | 812 | c.inherit_global_settings = defaults['inherit_global_settings'] |
|
813 | 813 | |
|
814 | 814 | application_form = RepoVcsSettingsForm(repo_name)() |
|
815 | 815 | try: |
|
816 | 816 | form_result = application_form.to_python(dict(request.POST)) |
|
817 | 817 | except formencode.Invalid as errors: |
|
818 | 818 | h.flash( |
|
819 | 819 | _("Some form inputs contain invalid data."), |
|
820 | 820 | category='error') |
|
821 | 821 | return htmlfill.render( |
|
822 |
render('admin/repos/repo_edit. |
|
|
822 | render('admin/repos/repo_edit.mako'), | |
|
823 | 823 | defaults=errors.value, |
|
824 | 824 | errors=errors.error_dict or {}, |
|
825 | 825 | prefix_error=False, |
|
826 | 826 | encoding="UTF-8", |
|
827 | 827 | force_defaults=False |
|
828 | 828 | ) |
|
829 | 829 | |
|
830 | 830 | try: |
|
831 | 831 | inherit_global_settings = form_result['inherit_global_settings'] |
|
832 | 832 | model.create_or_update_repo_settings( |
|
833 | 833 | form_result, inherit_global_settings=inherit_global_settings) |
|
834 | 834 | except Exception: |
|
835 | 835 | log.exception("Exception while updating settings") |
|
836 | 836 | h.flash( |
|
837 | 837 | _('Error occurred during updating repository VCS settings'), |
|
838 | 838 | category='error') |
|
839 | 839 | else: |
|
840 | 840 | Session().commit() |
|
841 | 841 | h.flash(_('Updated VCS settings'), category='success') |
|
842 | 842 | return redirect(url('repo_vcs_settings', repo_name=repo_name)) |
|
843 | 843 | |
|
844 | 844 | return htmlfill.render( |
|
845 |
render('admin/repos/repo_edit. |
|
|
845 | render('admin/repos/repo_edit.mako'), | |
|
846 | 846 | defaults=self._vcs_form_defaults(repo_name), |
|
847 | 847 | encoding="UTF-8", |
|
848 | 848 | force_defaults=False) |
|
849 | 849 | |
|
850 | 850 | @HasRepoPermissionAllDecorator('repository.admin') |
|
851 | 851 | @auth.CSRFRequired() |
|
852 | 852 | @jsonify |
|
853 | 853 | def repo_delete_svn_pattern(self, repo_name): |
|
854 | 854 | if not request.is_xhr: |
|
855 | 855 | return False |
|
856 | 856 | |
|
857 | 857 | delete_pattern_id = request.POST.get('delete_svn_pattern') |
|
858 | 858 | model = VcsSettingsModel(repo=repo_name) |
|
859 | 859 | try: |
|
860 | 860 | model.delete_repo_svn_pattern(delete_pattern_id) |
|
861 | 861 | except SettingNotFound: |
|
862 | 862 | raise HTTPBadRequest() |
|
863 | 863 | |
|
864 | 864 | Session().commit() |
|
865 | 865 | return True |
|
866 | 866 | |
|
867 | 867 | def _vcs_form_defaults(self, repo_name): |
|
868 | 868 | model = VcsSettingsModel(repo=repo_name) |
|
869 | 869 | global_defaults = model.get_global_settings() |
|
870 | 870 | |
|
871 | 871 | repo_defaults = {} |
|
872 | 872 | repo_defaults.update(global_defaults) |
|
873 | 873 | repo_defaults.update(model.get_repo_settings()) |
|
874 | 874 | |
|
875 | 875 | global_defaults = { |
|
876 | 876 | '{}_inherited'.format(k): global_defaults[k] |
|
877 | 877 | for k in global_defaults} |
|
878 | 878 | |
|
879 | 879 | defaults = { |
|
880 | 880 | 'inherit_global_settings': model.inherit_global_settings |
|
881 | 881 | } |
|
882 | 882 | defaults.update(global_defaults) |
|
883 | 883 | defaults.update(repo_defaults) |
|
884 | 884 | defaults.update({ |
|
885 | 885 | 'new_svn_branch': '', |
|
886 | 886 | 'new_svn_tag': '', |
|
887 | 887 | }) |
|
888 | 888 | return defaults |
@@ -1,842 +1,842 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 | """ |
|
23 | 23 | settings controller for rhodecode admin |
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import collections |
|
27 | 27 | import logging |
|
28 | 28 | import urllib2 |
|
29 | 29 | |
|
30 | 30 | import datetime |
|
31 | 31 | import formencode |
|
32 | 32 | from formencode import htmlfill |
|
33 | 33 | import packaging.version |
|
34 | 34 | from pylons import request, tmpl_context as c, url, config |
|
35 | 35 | from pylons.controllers.util import redirect |
|
36 | 36 | from pylons.i18n.translation import _, lazy_ugettext |
|
37 | 37 | from pyramid.threadlocal import get_current_registry |
|
38 | 38 | from webob.exc import HTTPBadRequest |
|
39 | 39 | |
|
40 | 40 | import rhodecode |
|
41 | 41 | from rhodecode.admin.navigation import navigation_list |
|
42 | 42 | from rhodecode.lib import auth |
|
43 | 43 | from rhodecode.lib import helpers as h |
|
44 | 44 | from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator |
|
45 | 45 | from rhodecode.lib.base import BaseController, render |
|
46 | 46 | from rhodecode.lib.celerylib import tasks, run_task |
|
47 | 47 | from rhodecode.lib.utils import repo2db_mapper |
|
48 | 48 | from rhodecode.lib.utils2 import ( |
|
49 | 49 | str2bool, safe_unicode, AttributeDict, safe_int) |
|
50 | 50 | from rhodecode.lib.compat import OrderedDict |
|
51 | 51 | from rhodecode.lib.ext_json import json |
|
52 | 52 | from rhodecode.lib.utils import jsonify |
|
53 | 53 | |
|
54 | 54 | from rhodecode.model.db import RhodeCodeUi, Repository |
|
55 | 55 | from rhodecode.model.forms import ApplicationSettingsForm, \ |
|
56 | 56 | ApplicationUiSettingsForm, ApplicationVisualisationForm, \ |
|
57 | 57 | LabsSettingsForm, IssueTrackerPatternsForm |
|
58 | 58 | from rhodecode.model.repo_group import RepoGroupModel |
|
59 | 59 | |
|
60 | 60 | from rhodecode.model.scm import ScmModel |
|
61 | 61 | from rhodecode.model.notification import EmailNotificationModel |
|
62 | 62 | from rhodecode.model.meta import Session |
|
63 | 63 | from rhodecode.model.settings import ( |
|
64 | 64 | IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound, |
|
65 | 65 | SettingsModel) |
|
66 | 66 | |
|
67 | 67 | from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER |
|
68 | 68 | from rhodecode.svn_support.config_keys import generate_config |
|
69 | 69 | |
|
70 | 70 | |
|
71 | 71 | log = logging.getLogger(__name__) |
|
72 | 72 | |
|
73 | 73 | |
|
74 | 74 | class SettingsController(BaseController): |
|
75 | 75 | """REST Controller styled on the Atom Publishing Protocol""" |
|
76 | 76 | # To properly map this controller, ensure your config/routing.py |
|
77 | 77 | # file has a resource setup: |
|
78 | 78 | # map.resource('setting', 'settings', controller='admin/settings', |
|
79 | 79 | # path_prefix='/admin', name_prefix='admin_') |
|
80 | 80 | |
|
81 | 81 | @LoginRequired() |
|
82 | 82 | def __before__(self): |
|
83 | 83 | super(SettingsController, self).__before__() |
|
84 | 84 | c.labs_active = str2bool( |
|
85 | 85 | rhodecode.CONFIG.get('labs_settings_active', 'true')) |
|
86 | 86 | c.navlist = navigation_list(request) |
|
87 | 87 | |
|
88 | 88 | def _get_hg_ui_settings(self): |
|
89 | 89 | ret = RhodeCodeUi.query().all() |
|
90 | 90 | |
|
91 | 91 | if not ret: |
|
92 | 92 | raise Exception('Could not get application ui settings !') |
|
93 | 93 | settings = {} |
|
94 | 94 | for each in ret: |
|
95 | 95 | k = each.ui_key |
|
96 | 96 | v = each.ui_value |
|
97 | 97 | if k == '/': |
|
98 | 98 | k = 'root_path' |
|
99 | 99 | |
|
100 | 100 | if k in ['push_ssl', 'publish']: |
|
101 | 101 | v = str2bool(v) |
|
102 | 102 | |
|
103 | 103 | if k.find('.') != -1: |
|
104 | 104 | k = k.replace('.', '_') |
|
105 | 105 | |
|
106 | 106 | if each.ui_section in ['hooks', 'extensions']: |
|
107 | 107 | v = each.ui_active |
|
108 | 108 | |
|
109 | 109 | settings[each.ui_section + '_' + k] = v |
|
110 | 110 | return settings |
|
111 | 111 | |
|
112 | 112 | @HasPermissionAllDecorator('hg.admin') |
|
113 | 113 | @auth.CSRFRequired() |
|
114 | 114 | @jsonify |
|
115 | 115 | def delete_svn_pattern(self): |
|
116 | 116 | if not request.is_xhr: |
|
117 | 117 | raise HTTPBadRequest() |
|
118 | 118 | |
|
119 | 119 | delete_pattern_id = request.POST.get('delete_svn_pattern') |
|
120 | 120 | model = VcsSettingsModel() |
|
121 | 121 | try: |
|
122 | 122 | model.delete_global_svn_pattern(delete_pattern_id) |
|
123 | 123 | except SettingNotFound: |
|
124 | 124 | raise HTTPBadRequest() |
|
125 | 125 | |
|
126 | 126 | Session().commit() |
|
127 | 127 | return True |
|
128 | 128 | |
|
129 | 129 | @HasPermissionAllDecorator('hg.admin') |
|
130 | 130 | @auth.CSRFRequired() |
|
131 | 131 | def settings_vcs_update(self): |
|
132 | 132 | """POST /admin/settings: All items in the collection""" |
|
133 | 133 | # url('admin_settings_vcs') |
|
134 | 134 | c.active = 'vcs' |
|
135 | 135 | |
|
136 | 136 | model = VcsSettingsModel() |
|
137 | 137 | c.svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
138 | 138 | c.svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
139 | 139 | |
|
140 | 140 | # TODO: Replace with request.registry after migrating to pyramid. |
|
141 | 141 | pyramid_settings = get_current_registry().settings |
|
142 | 142 | c.svn_proxy_generate_config = pyramid_settings[generate_config] |
|
143 | 143 | |
|
144 | 144 | application_form = ApplicationUiSettingsForm()() |
|
145 | 145 | |
|
146 | 146 | try: |
|
147 | 147 | form_result = application_form.to_python(dict(request.POST)) |
|
148 | 148 | except formencode.Invalid as errors: |
|
149 | 149 | h.flash( |
|
150 | 150 | _("Some form inputs contain invalid data."), |
|
151 | 151 | category='error') |
|
152 | 152 | return htmlfill.render( |
|
153 |
render('admin/settings/settings. |
|
|
153 | render('admin/settings/settings.mako'), | |
|
154 | 154 | defaults=errors.value, |
|
155 | 155 | errors=errors.error_dict or {}, |
|
156 | 156 | prefix_error=False, |
|
157 | 157 | encoding="UTF-8", |
|
158 | 158 | force_defaults=False |
|
159 | 159 | ) |
|
160 | 160 | |
|
161 | 161 | try: |
|
162 | 162 | if c.visual.allow_repo_location_change: |
|
163 | 163 | model.update_global_path_setting( |
|
164 | 164 | form_result['paths_root_path']) |
|
165 | 165 | |
|
166 | 166 | model.update_global_ssl_setting(form_result['web_push_ssl']) |
|
167 | 167 | model.update_global_hook_settings(form_result) |
|
168 | 168 | |
|
169 | 169 | model.create_or_update_global_svn_settings(form_result) |
|
170 | 170 | model.create_or_update_global_hg_settings(form_result) |
|
171 | 171 | model.create_or_update_global_pr_settings(form_result) |
|
172 | 172 | except Exception: |
|
173 | 173 | log.exception("Exception while updating settings") |
|
174 | 174 | h.flash(_('Error occurred during updating ' |
|
175 | 175 | 'application settings'), category='error') |
|
176 | 176 | else: |
|
177 | 177 | Session().commit() |
|
178 | 178 | h.flash(_('Updated VCS settings'), category='success') |
|
179 | 179 | return redirect(url('admin_settings_vcs')) |
|
180 | 180 | |
|
181 | 181 | return htmlfill.render( |
|
182 |
render('admin/settings/settings. |
|
|
182 | render('admin/settings/settings.mako'), | |
|
183 | 183 | defaults=self._form_defaults(), |
|
184 | 184 | encoding="UTF-8", |
|
185 | 185 | force_defaults=False) |
|
186 | 186 | |
|
187 | 187 | @HasPermissionAllDecorator('hg.admin') |
|
188 | 188 | def settings_vcs(self): |
|
189 | 189 | """GET /admin/settings: All items in the collection""" |
|
190 | 190 | # url('admin_settings_vcs') |
|
191 | 191 | c.active = 'vcs' |
|
192 | 192 | model = VcsSettingsModel() |
|
193 | 193 | c.svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
194 | 194 | c.svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
195 | 195 | |
|
196 | 196 | # TODO: Replace with request.registry after migrating to pyramid. |
|
197 | 197 | pyramid_settings = get_current_registry().settings |
|
198 | 198 | c.svn_proxy_generate_config = pyramid_settings[generate_config] |
|
199 | 199 | |
|
200 | 200 | return htmlfill.render( |
|
201 |
render('admin/settings/settings. |
|
|
201 | render('admin/settings/settings.mako'), | |
|
202 | 202 | defaults=self._form_defaults(), |
|
203 | 203 | encoding="UTF-8", |
|
204 | 204 | force_defaults=False) |
|
205 | 205 | |
|
206 | 206 | @HasPermissionAllDecorator('hg.admin') |
|
207 | 207 | @auth.CSRFRequired() |
|
208 | 208 | def settings_mapping_update(self): |
|
209 | 209 | """POST /admin/settings/mapping: All items in the collection""" |
|
210 | 210 | # url('admin_settings_mapping') |
|
211 | 211 | c.active = 'mapping' |
|
212 | 212 | rm_obsolete = request.POST.get('destroy', False) |
|
213 | 213 | invalidate_cache = request.POST.get('invalidate', False) |
|
214 | 214 | log.debug( |
|
215 | 215 | 'rescanning repo location with destroy obsolete=%s', rm_obsolete) |
|
216 | 216 | |
|
217 | 217 | if invalidate_cache: |
|
218 | 218 | log.debug('invalidating all repositories cache') |
|
219 | 219 | for repo in Repository.get_all(): |
|
220 | 220 | ScmModel().mark_for_invalidation(repo.repo_name, delete=True) |
|
221 | 221 | |
|
222 | 222 | filesystem_repos = ScmModel().repo_scan() |
|
223 | 223 | added, removed = repo2db_mapper(filesystem_repos, rm_obsolete) |
|
224 | 224 | _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-' |
|
225 | 225 | h.flash(_('Repositories successfully ' |
|
226 | 226 | 'rescanned added: %s ; removed: %s') % |
|
227 | 227 | (_repr(added), _repr(removed)), |
|
228 | 228 | category='success') |
|
229 | 229 | return redirect(url('admin_settings_mapping')) |
|
230 | 230 | |
|
231 | 231 | @HasPermissionAllDecorator('hg.admin') |
|
232 | 232 | def settings_mapping(self): |
|
233 | 233 | """GET /admin/settings/mapping: All items in the collection""" |
|
234 | 234 | # url('admin_settings_mapping') |
|
235 | 235 | c.active = 'mapping' |
|
236 | 236 | |
|
237 | 237 | return htmlfill.render( |
|
238 |
render('admin/settings/settings. |
|
|
238 | render('admin/settings/settings.mako'), | |
|
239 | 239 | defaults=self._form_defaults(), |
|
240 | 240 | encoding="UTF-8", |
|
241 | 241 | force_defaults=False) |
|
242 | 242 | |
|
243 | 243 | @HasPermissionAllDecorator('hg.admin') |
|
244 | 244 | @auth.CSRFRequired() |
|
245 | 245 | def settings_global_update(self): |
|
246 | 246 | """POST /admin/settings/global: All items in the collection""" |
|
247 | 247 | # url('admin_settings_global') |
|
248 | 248 | c.active = 'global' |
|
249 | 249 | c.personal_repo_group_default_pattern = RepoGroupModel()\ |
|
250 | 250 | .get_personal_group_name_pattern() |
|
251 | 251 | application_form = ApplicationSettingsForm()() |
|
252 | 252 | try: |
|
253 | 253 | form_result = application_form.to_python(dict(request.POST)) |
|
254 | 254 | except formencode.Invalid as errors: |
|
255 | 255 | return htmlfill.render( |
|
256 |
render('admin/settings/settings. |
|
|
256 | render('admin/settings/settings.mako'), | |
|
257 | 257 | defaults=errors.value, |
|
258 | 258 | errors=errors.error_dict or {}, |
|
259 | 259 | prefix_error=False, |
|
260 | 260 | encoding="UTF-8", |
|
261 | 261 | force_defaults=False) |
|
262 | 262 | |
|
263 | 263 | try: |
|
264 | 264 | settings = [ |
|
265 | 265 | ('title', 'rhodecode_title', 'unicode'), |
|
266 | 266 | ('realm', 'rhodecode_realm', 'unicode'), |
|
267 | 267 | ('pre_code', 'rhodecode_pre_code', 'unicode'), |
|
268 | 268 | ('post_code', 'rhodecode_post_code', 'unicode'), |
|
269 | 269 | ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'), |
|
270 | 270 | ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'), |
|
271 | 271 | ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'), |
|
272 | 272 | ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'), |
|
273 | 273 | ] |
|
274 | 274 | for setting, form_key, type_ in settings: |
|
275 | 275 | sett = SettingsModel().create_or_update_setting( |
|
276 | 276 | setting, form_result[form_key], type_) |
|
277 | 277 | Session().add(sett) |
|
278 | 278 | |
|
279 | 279 | Session().commit() |
|
280 | 280 | SettingsModel().invalidate_settings_cache() |
|
281 | 281 | h.flash(_('Updated application settings'), category='success') |
|
282 | 282 | except Exception: |
|
283 | 283 | log.exception("Exception while updating application settings") |
|
284 | 284 | h.flash( |
|
285 | 285 | _('Error occurred during updating application settings'), |
|
286 | 286 | category='error') |
|
287 | 287 | |
|
288 | 288 | return redirect(url('admin_settings_global')) |
|
289 | 289 | |
|
290 | 290 | @HasPermissionAllDecorator('hg.admin') |
|
291 | 291 | def settings_global(self): |
|
292 | 292 | """GET /admin/settings/global: All items in the collection""" |
|
293 | 293 | # url('admin_settings_global') |
|
294 | 294 | c.active = 'global' |
|
295 | 295 | c.personal_repo_group_default_pattern = RepoGroupModel()\ |
|
296 | 296 | .get_personal_group_name_pattern() |
|
297 | 297 | |
|
298 | 298 | return htmlfill.render( |
|
299 |
render('admin/settings/settings. |
|
|
299 | render('admin/settings/settings.mako'), | |
|
300 | 300 | defaults=self._form_defaults(), |
|
301 | 301 | encoding="UTF-8", |
|
302 | 302 | force_defaults=False) |
|
303 | 303 | |
|
304 | 304 | @HasPermissionAllDecorator('hg.admin') |
|
305 | 305 | @auth.CSRFRequired() |
|
306 | 306 | def settings_visual_update(self): |
|
307 | 307 | """POST /admin/settings/visual: All items in the collection""" |
|
308 | 308 | # url('admin_settings_visual') |
|
309 | 309 | c.active = 'visual' |
|
310 | 310 | application_form = ApplicationVisualisationForm()() |
|
311 | 311 | try: |
|
312 | 312 | form_result = application_form.to_python(dict(request.POST)) |
|
313 | 313 | except formencode.Invalid as errors: |
|
314 | 314 | return htmlfill.render( |
|
315 |
render('admin/settings/settings. |
|
|
315 | render('admin/settings/settings.mako'), | |
|
316 | 316 | defaults=errors.value, |
|
317 | 317 | errors=errors.error_dict or {}, |
|
318 | 318 | prefix_error=False, |
|
319 | 319 | encoding="UTF-8", |
|
320 | 320 | force_defaults=False |
|
321 | 321 | ) |
|
322 | 322 | |
|
323 | 323 | try: |
|
324 | 324 | settings = [ |
|
325 | 325 | ('show_public_icon', 'rhodecode_show_public_icon', 'bool'), |
|
326 | 326 | ('show_private_icon', 'rhodecode_show_private_icon', 'bool'), |
|
327 | 327 | ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'), |
|
328 | 328 | ('repository_fields', 'rhodecode_repository_fields', 'bool'), |
|
329 | 329 | ('dashboard_items', 'rhodecode_dashboard_items', 'int'), |
|
330 | 330 | ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'), |
|
331 | 331 | ('show_version', 'rhodecode_show_version', 'bool'), |
|
332 | 332 | ('use_gravatar', 'rhodecode_use_gravatar', 'bool'), |
|
333 | 333 | ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'), |
|
334 | 334 | ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'), |
|
335 | 335 | ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'), |
|
336 | 336 | ('support_url', 'rhodecode_support_url', 'unicode'), |
|
337 | 337 | ('show_revision_number', 'rhodecode_show_revision_number', 'bool'), |
|
338 | 338 | ('show_sha_length', 'rhodecode_show_sha_length', 'int'), |
|
339 | 339 | ] |
|
340 | 340 | for setting, form_key, type_ in settings: |
|
341 | 341 | sett = SettingsModel().create_or_update_setting( |
|
342 | 342 | setting, form_result[form_key], type_) |
|
343 | 343 | Session().add(sett) |
|
344 | 344 | |
|
345 | 345 | Session().commit() |
|
346 | 346 | SettingsModel().invalidate_settings_cache() |
|
347 | 347 | h.flash(_('Updated visualisation settings'), category='success') |
|
348 | 348 | except Exception: |
|
349 | 349 | log.exception("Exception updating visualization settings") |
|
350 | 350 | h.flash(_('Error occurred during updating ' |
|
351 | 351 | 'visualisation settings'), |
|
352 | 352 | category='error') |
|
353 | 353 | |
|
354 | 354 | return redirect(url('admin_settings_visual')) |
|
355 | 355 | |
|
356 | 356 | @HasPermissionAllDecorator('hg.admin') |
|
357 | 357 | def settings_visual(self): |
|
358 | 358 | """GET /admin/settings/visual: All items in the collection""" |
|
359 | 359 | # url('admin_settings_visual') |
|
360 | 360 | c.active = 'visual' |
|
361 | 361 | |
|
362 | 362 | return htmlfill.render( |
|
363 |
render('admin/settings/settings. |
|
|
363 | render('admin/settings/settings.mako'), | |
|
364 | 364 | defaults=self._form_defaults(), |
|
365 | 365 | encoding="UTF-8", |
|
366 | 366 | force_defaults=False) |
|
367 | 367 | |
|
368 | 368 | @HasPermissionAllDecorator('hg.admin') |
|
369 | 369 | @auth.CSRFRequired() |
|
370 | 370 | def settings_issuetracker_test(self): |
|
371 | 371 | if request.is_xhr: |
|
372 | 372 | return h.urlify_commit_message( |
|
373 | 373 | request.POST.get('test_text', ''), |
|
374 | 374 | 'repo_group/test_repo1') |
|
375 | 375 | else: |
|
376 | 376 | raise HTTPBadRequest() |
|
377 | 377 | |
|
378 | 378 | @HasPermissionAllDecorator('hg.admin') |
|
379 | 379 | @auth.CSRFRequired() |
|
380 | 380 | def settings_issuetracker_delete(self): |
|
381 | 381 | uid = request.POST.get('uid') |
|
382 | 382 | IssueTrackerSettingsModel().delete_entries(uid) |
|
383 | 383 | h.flash(_('Removed issue tracker entry'), category='success') |
|
384 | 384 | return redirect(url('admin_settings_issuetracker')) |
|
385 | 385 | |
|
386 | 386 | @HasPermissionAllDecorator('hg.admin') |
|
387 | 387 | def settings_issuetracker(self): |
|
388 | 388 | """GET /admin/settings/issue-tracker: All items in the collection""" |
|
389 | 389 | # url('admin_settings_issuetracker') |
|
390 | 390 | c.active = 'issuetracker' |
|
391 | 391 | defaults = SettingsModel().get_all_settings() |
|
392 | 392 | |
|
393 | 393 | entry_key = 'rhodecode_issuetracker_pat_' |
|
394 | 394 | |
|
395 | 395 | c.issuetracker_entries = {} |
|
396 | 396 | for k, v in defaults.items(): |
|
397 | 397 | if k.startswith(entry_key): |
|
398 | 398 | uid = k[len(entry_key):] |
|
399 | 399 | c.issuetracker_entries[uid] = None |
|
400 | 400 | |
|
401 | 401 | for uid in c.issuetracker_entries: |
|
402 | 402 | c.issuetracker_entries[uid] = AttributeDict({ |
|
403 | 403 | 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid), |
|
404 | 404 | 'url': defaults.get('rhodecode_issuetracker_url_' + uid), |
|
405 | 405 | 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid), |
|
406 | 406 | 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid), |
|
407 | 407 | }) |
|
408 | 408 | |
|
409 |
return render('admin/settings/settings. |
|
|
409 | return render('admin/settings/settings.mako') | |
|
410 | 410 | |
|
411 | 411 | @HasPermissionAllDecorator('hg.admin') |
|
412 | 412 | @auth.CSRFRequired() |
|
413 | 413 | def settings_issuetracker_save(self): |
|
414 | 414 | settings_model = IssueTrackerSettingsModel() |
|
415 | 415 | |
|
416 | 416 | form = IssueTrackerPatternsForm()().to_python(request.POST) |
|
417 | 417 | if form: |
|
418 | 418 | for uid in form.get('delete_patterns', []): |
|
419 | 419 | settings_model.delete_entries(uid) |
|
420 | 420 | |
|
421 | 421 | for pattern in form.get('patterns', []): |
|
422 | 422 | for setting, value, type_ in pattern: |
|
423 | 423 | sett = settings_model.create_or_update_setting( |
|
424 | 424 | setting, value, type_) |
|
425 | 425 | Session().add(sett) |
|
426 | 426 | |
|
427 | 427 | Session().commit() |
|
428 | 428 | |
|
429 | 429 | SettingsModel().invalidate_settings_cache() |
|
430 | 430 | h.flash(_('Updated issue tracker entries'), category='success') |
|
431 | 431 | return redirect(url('admin_settings_issuetracker')) |
|
432 | 432 | |
|
433 | 433 | @HasPermissionAllDecorator('hg.admin') |
|
434 | 434 | @auth.CSRFRequired() |
|
435 | 435 | def settings_email_update(self): |
|
436 | 436 | """POST /admin/settings/email: All items in the collection""" |
|
437 | 437 | # url('admin_settings_email') |
|
438 | 438 | c.active = 'email' |
|
439 | 439 | |
|
440 | 440 | test_email = request.POST.get('test_email') |
|
441 | 441 | |
|
442 | 442 | if not test_email: |
|
443 | 443 | h.flash(_('Please enter email address'), category='error') |
|
444 | 444 | return redirect(url('admin_settings_email')) |
|
445 | 445 | |
|
446 | 446 | email_kwargs = { |
|
447 | 447 | 'date': datetime.datetime.now(), |
|
448 | 448 | 'user': c.rhodecode_user, |
|
449 | 449 | 'rhodecode_version': c.rhodecode_version |
|
450 | 450 | } |
|
451 | 451 | |
|
452 | 452 | (subject, headers, email_body, |
|
453 | 453 | email_body_plaintext) = EmailNotificationModel().render_email( |
|
454 | 454 | EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs) |
|
455 | 455 | |
|
456 | 456 | recipients = [test_email] if test_email else None |
|
457 | 457 | |
|
458 | 458 | run_task(tasks.send_email, recipients, subject, |
|
459 | 459 | email_body_plaintext, email_body) |
|
460 | 460 | |
|
461 | 461 | h.flash(_('Send email task created'), category='success') |
|
462 | 462 | return redirect(url('admin_settings_email')) |
|
463 | 463 | |
|
464 | 464 | @HasPermissionAllDecorator('hg.admin') |
|
465 | 465 | def settings_email(self): |
|
466 | 466 | """GET /admin/settings/email: All items in the collection""" |
|
467 | 467 | # url('admin_settings_email') |
|
468 | 468 | c.active = 'email' |
|
469 | 469 | c.rhodecode_ini = rhodecode.CONFIG |
|
470 | 470 | |
|
471 | 471 | return htmlfill.render( |
|
472 |
render('admin/settings/settings. |
|
|
472 | render('admin/settings/settings.mako'), | |
|
473 | 473 | defaults=self._form_defaults(), |
|
474 | 474 | encoding="UTF-8", |
|
475 | 475 | force_defaults=False) |
|
476 | 476 | |
|
477 | 477 | @HasPermissionAllDecorator('hg.admin') |
|
478 | 478 | @auth.CSRFRequired() |
|
479 | 479 | def settings_hooks_update(self): |
|
480 | 480 | """POST or DELETE /admin/settings/hooks: All items in the collection""" |
|
481 | 481 | # url('admin_settings_hooks') |
|
482 | 482 | c.active = 'hooks' |
|
483 | 483 | if c.visual.allow_custom_hooks_settings: |
|
484 | 484 | ui_key = request.POST.get('new_hook_ui_key') |
|
485 | 485 | ui_value = request.POST.get('new_hook_ui_value') |
|
486 | 486 | |
|
487 | 487 | hook_id = request.POST.get('hook_id') |
|
488 | 488 | new_hook = False |
|
489 | 489 | |
|
490 | 490 | model = SettingsModel() |
|
491 | 491 | try: |
|
492 | 492 | if ui_value and ui_key: |
|
493 | 493 | model.create_or_update_hook(ui_key, ui_value) |
|
494 | 494 | h.flash(_('Added new hook'), category='success') |
|
495 | 495 | new_hook = True |
|
496 | 496 | elif hook_id: |
|
497 | 497 | RhodeCodeUi.delete(hook_id) |
|
498 | 498 | Session().commit() |
|
499 | 499 | |
|
500 | 500 | # check for edits |
|
501 | 501 | update = False |
|
502 | 502 | _d = request.POST.dict_of_lists() |
|
503 | 503 | for k, v in zip(_d.get('hook_ui_key', []), |
|
504 | 504 | _d.get('hook_ui_value_new', [])): |
|
505 | 505 | model.create_or_update_hook(k, v) |
|
506 | 506 | update = True |
|
507 | 507 | |
|
508 | 508 | if update and not new_hook: |
|
509 | 509 | h.flash(_('Updated hooks'), category='success') |
|
510 | 510 | Session().commit() |
|
511 | 511 | except Exception: |
|
512 | 512 | log.exception("Exception during hook creation") |
|
513 | 513 | h.flash(_('Error occurred during hook creation'), |
|
514 | 514 | category='error') |
|
515 | 515 | |
|
516 | 516 | return redirect(url('admin_settings_hooks')) |
|
517 | 517 | |
|
518 | 518 | @HasPermissionAllDecorator('hg.admin') |
|
519 | 519 | def settings_hooks(self): |
|
520 | 520 | """GET /admin/settings/hooks: All items in the collection""" |
|
521 | 521 | # url('admin_settings_hooks') |
|
522 | 522 | c.active = 'hooks' |
|
523 | 523 | |
|
524 | 524 | model = SettingsModel() |
|
525 | 525 | c.hooks = model.get_builtin_hooks() |
|
526 | 526 | c.custom_hooks = model.get_custom_hooks() |
|
527 | 527 | |
|
528 | 528 | return htmlfill.render( |
|
529 |
render('admin/settings/settings. |
|
|
529 | render('admin/settings/settings.mako'), | |
|
530 | 530 | defaults=self._form_defaults(), |
|
531 | 531 | encoding="UTF-8", |
|
532 | 532 | force_defaults=False) |
|
533 | 533 | |
|
534 | 534 | @HasPermissionAllDecorator('hg.admin') |
|
535 | 535 | def settings_search(self): |
|
536 | 536 | """GET /admin/settings/search: All items in the collection""" |
|
537 | 537 | # url('admin_settings_search') |
|
538 | 538 | c.active = 'search' |
|
539 | 539 | |
|
540 | 540 | from rhodecode.lib.index import searcher_from_config |
|
541 | 541 | searcher = searcher_from_config(config) |
|
542 | 542 | c.statistics = searcher.statistics() |
|
543 | 543 | |
|
544 |
return render('admin/settings/settings. |
|
|
544 | return render('admin/settings/settings.mako') | |
|
545 | 545 | |
|
546 | 546 | @HasPermissionAllDecorator('hg.admin') |
|
547 | 547 | def settings_system(self): |
|
548 | 548 | """GET /admin/settings/system: All items in the collection""" |
|
549 | 549 | # url('admin_settings_system') |
|
550 | 550 | snapshot = str2bool(request.GET.get('snapshot')) |
|
551 | 551 | defaults = self._form_defaults() |
|
552 | 552 | |
|
553 | 553 | c.active = 'system' |
|
554 | 554 | c.rhodecode_update_url = defaults.get('rhodecode_update_url') |
|
555 | 555 | server_info = ScmModel().get_server_info(request.environ) |
|
556 | 556 | |
|
557 | 557 | for key, val in server_info.iteritems(): |
|
558 | 558 | setattr(c, key, val) |
|
559 | 559 | |
|
560 | 560 | def val(name, subkey='human_value'): |
|
561 | 561 | return server_info[name][subkey] |
|
562 | 562 | |
|
563 | 563 | def state(name): |
|
564 | 564 | return server_info[name]['state'] |
|
565 | 565 | |
|
566 | 566 | def val2(name): |
|
567 | 567 | val = server_info[name]['human_value'] |
|
568 | 568 | state = server_info[name]['state'] |
|
569 | 569 | return val, state |
|
570 | 570 | |
|
571 | 571 | c.data_items = [ |
|
572 | 572 | # update info |
|
573 | 573 | (_('Update info'), h.literal( |
|
574 | 574 | '<span class="link" id="check_for_update" >%s.</span>' % ( |
|
575 | 575 | _('Check for updates')) + |
|
576 | 576 | '<br/> <span >%s.</span>' % (_('Note: please make sure this server can access `%s` for the update link to work') % c.rhodecode_update_url) |
|
577 | 577 | ), ''), |
|
578 | 578 | |
|
579 | 579 | # RhodeCode specific |
|
580 | 580 | (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')), |
|
581 | 581 | (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')), |
|
582 | 582 | (_('RhodeCode Server ID'), val('server')['server_id'], state('server')), |
|
583 | 583 | (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')), |
|
584 | 584 | ('', '', ''), # spacer |
|
585 | 585 | |
|
586 | 586 | # Database |
|
587 | 587 | (_('Database'), val('database')['url'], state('database')), |
|
588 | 588 | (_('Database version'), val('database')['version'], state('database')), |
|
589 | 589 | ('', '', ''), # spacer |
|
590 | 590 | |
|
591 | 591 | # Platform/Python |
|
592 | 592 | (_('Platform'), val('platform')['name'], state('platform')), |
|
593 | 593 | (_('Platform UUID'), val('platform')['uuid'], state('platform')), |
|
594 | 594 | (_('Python version'), val('python')['version'], state('python')), |
|
595 | 595 | (_('Python path'), val('python')['executable'], state('python')), |
|
596 | 596 | ('', '', ''), # spacer |
|
597 | 597 | |
|
598 | 598 | # Systems stats |
|
599 | 599 | (_('CPU'), val('cpu'), state('cpu')), |
|
600 | 600 | (_('Load'), val('load')['text'], state('load')), |
|
601 | 601 | (_('Memory'), val('memory')['text'], state('memory')), |
|
602 | 602 | (_('Uptime'), val('uptime')['text'], state('uptime')), |
|
603 | 603 | ('', '', ''), # spacer |
|
604 | 604 | |
|
605 | 605 | # Repo storage |
|
606 | 606 | (_('Storage location'), val('storage')['path'], state('storage')), |
|
607 | 607 | (_('Storage info'), val('storage')['text'], state('storage')), |
|
608 | 608 | (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')), |
|
609 | 609 | |
|
610 | 610 | (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')), |
|
611 | 611 | (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')), |
|
612 | 612 | |
|
613 | 613 | (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')), |
|
614 | 614 | (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')), |
|
615 | 615 | |
|
616 | 616 | (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')), |
|
617 | 617 | (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')), |
|
618 | 618 | |
|
619 | 619 | (_('Search info'), val('search')['text'], state('search')), |
|
620 | 620 | (_('Search location'), val('search')['location'], state('search')), |
|
621 | 621 | ('', '', ''), # spacer |
|
622 | 622 | |
|
623 | 623 | # VCS specific |
|
624 | 624 | (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')), |
|
625 | 625 | (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')), |
|
626 | 626 | (_('GIT'), val('git'), state('git')), |
|
627 | 627 | (_('HG'), val('hg'), state('hg')), |
|
628 | 628 | (_('SVN'), val('svn'), state('svn')), |
|
629 | 629 | |
|
630 | 630 | ] |
|
631 | 631 | |
|
632 | 632 | # TODO: marcink, figure out how to allow only selected users to do this |
|
633 | 633 | c.allowed_to_snapshot = c.rhodecode_user.admin |
|
634 | 634 | |
|
635 | 635 | if snapshot: |
|
636 | 636 | if c.allowed_to_snapshot: |
|
637 | 637 | c.data_items.pop(0) # remove server info |
|
638 |
return render('admin/settings/settings_system_snapshot. |
|
|
638 | return render('admin/settings/settings_system_snapshot.mako') | |
|
639 | 639 | else: |
|
640 | 640 | h.flash('You are not allowed to do this', category='warning') |
|
641 | 641 | |
|
642 | 642 | return htmlfill.render( |
|
643 |
render('admin/settings/settings. |
|
|
643 | render('admin/settings/settings.mako'), | |
|
644 | 644 | defaults=defaults, |
|
645 | 645 | encoding="UTF-8", |
|
646 | 646 | force_defaults=False) |
|
647 | 647 | |
|
648 | 648 | @staticmethod |
|
649 | 649 | def get_update_data(update_url): |
|
650 | 650 | """Return the JSON update data.""" |
|
651 | 651 | ver = rhodecode.__version__ |
|
652 | 652 | log.debug('Checking for upgrade on `%s` server', update_url) |
|
653 | 653 | opener = urllib2.build_opener() |
|
654 | 654 | opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)] |
|
655 | 655 | response = opener.open(update_url) |
|
656 | 656 | response_data = response.read() |
|
657 | 657 | data = json.loads(response_data) |
|
658 | 658 | |
|
659 | 659 | return data |
|
660 | 660 | |
|
661 | 661 | @HasPermissionAllDecorator('hg.admin') |
|
662 | 662 | def settings_system_update(self): |
|
663 | 663 | """GET /admin/settings/system/updates: All items in the collection""" |
|
664 | 664 | # url('admin_settings_system_update') |
|
665 | 665 | defaults = self._form_defaults() |
|
666 | 666 | update_url = defaults.get('rhodecode_update_url', '') |
|
667 | 667 | |
|
668 | 668 | _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s) |
|
669 | 669 | try: |
|
670 | 670 | data = self.get_update_data(update_url) |
|
671 | 671 | except urllib2.URLError as e: |
|
672 | 672 | log.exception("Exception contacting upgrade server") |
|
673 | 673 | return _err('Failed to contact upgrade server: %r' % e) |
|
674 | 674 | except ValueError as e: |
|
675 | 675 | log.exception("Bad data sent from update server") |
|
676 | 676 | return _err('Bad data sent from update server') |
|
677 | 677 | |
|
678 | 678 | latest = data['versions'][0] |
|
679 | 679 | |
|
680 | 680 | c.update_url = update_url |
|
681 | 681 | c.latest_data = latest |
|
682 | 682 | c.latest_ver = latest['version'] |
|
683 | 683 | c.cur_ver = rhodecode.__version__ |
|
684 | 684 | c.should_upgrade = False |
|
685 | 685 | |
|
686 | 686 | if (packaging.version.Version(c.latest_ver) > |
|
687 | 687 | packaging.version.Version(c.cur_ver)): |
|
688 | 688 | c.should_upgrade = True |
|
689 | 689 | c.important_notices = latest['general'] |
|
690 | 690 | |
|
691 |
return render('admin/settings/settings_system_update. |
|
|
691 | return render('admin/settings/settings_system_update.mako') | |
|
692 | 692 | |
|
693 | 693 | @HasPermissionAllDecorator('hg.admin') |
|
694 | 694 | def settings_supervisor(self): |
|
695 | 695 | c.rhodecode_ini = rhodecode.CONFIG |
|
696 | 696 | c.active = 'supervisor' |
|
697 | 697 | |
|
698 | 698 | c.supervisor_procs = OrderedDict([ |
|
699 | 699 | (SUPERVISOR_MASTER, {}), |
|
700 | 700 | ]) |
|
701 | 701 | |
|
702 | 702 | c.log_size = 10240 |
|
703 | 703 | supervisor = SupervisorModel() |
|
704 | 704 | |
|
705 | 705 | _connection = supervisor.get_connection( |
|
706 | 706 | c.rhodecode_ini.get('supervisor.uri')) |
|
707 | 707 | c.connection_error = None |
|
708 | 708 | try: |
|
709 | 709 | _connection.supervisor.getAllProcessInfo() |
|
710 | 710 | except Exception as e: |
|
711 | 711 | c.connection_error = str(e) |
|
712 | 712 | log.exception("Exception reading supervisor data") |
|
713 |
return render('admin/settings/settings. |
|
|
713 | return render('admin/settings/settings.mako') | |
|
714 | 714 | |
|
715 | 715 | groupid = c.rhodecode_ini.get('supervisor.group_id') |
|
716 | 716 | |
|
717 | 717 | # feed our group processes to the main |
|
718 | 718 | for proc in supervisor.get_group_processes(_connection, groupid): |
|
719 | 719 | c.supervisor_procs[proc['name']] = {} |
|
720 | 720 | |
|
721 | 721 | for k in c.supervisor_procs.keys(): |
|
722 | 722 | try: |
|
723 | 723 | # master process info |
|
724 | 724 | if k == SUPERVISOR_MASTER: |
|
725 | 725 | _data = supervisor.get_master_state(_connection) |
|
726 | 726 | _data['name'] = 'supervisor master' |
|
727 | 727 | _data['description'] = 'pid %s, id: %s, ver: %s' % ( |
|
728 | 728 | _data['pid'], _data['id'], _data['ver']) |
|
729 | 729 | c.supervisor_procs[k] = _data |
|
730 | 730 | else: |
|
731 | 731 | procid = groupid + ":" + k |
|
732 | 732 | c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid) |
|
733 | 733 | except Exception as e: |
|
734 | 734 | log.exception("Exception reading supervisor data") |
|
735 | 735 | c.supervisor_procs[k] = {'_rhodecode_error': str(e)} |
|
736 | 736 | |
|
737 |
return render('admin/settings/settings. |
|
|
737 | return render('admin/settings/settings.mako') | |
|
738 | 738 | |
|
739 | 739 | @HasPermissionAllDecorator('hg.admin') |
|
740 | 740 | def settings_supervisor_log(self, procid): |
|
741 | 741 | import rhodecode |
|
742 | 742 | c.rhodecode_ini = rhodecode.CONFIG |
|
743 | 743 | c.active = 'supervisor_tail' |
|
744 | 744 | |
|
745 | 745 | supervisor = SupervisorModel() |
|
746 | 746 | _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri')) |
|
747 | 747 | groupid = c.rhodecode_ini.get('supervisor.group_id') |
|
748 | 748 | procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid |
|
749 | 749 | |
|
750 | 750 | c.log_size = 10240 |
|
751 | 751 | offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1 |
|
752 | 752 | c.log = supervisor.read_process_log(_connection, procid, offset, 0) |
|
753 | 753 | |
|
754 |
return render('admin/settings/settings. |
|
|
754 | return render('admin/settings/settings.mako') | |
|
755 | 755 | |
|
756 | 756 | @HasPermissionAllDecorator('hg.admin') |
|
757 | 757 | @auth.CSRFRequired() |
|
758 | 758 | def settings_labs_update(self): |
|
759 | 759 | """POST /admin/settings/labs: All items in the collection""" |
|
760 | 760 | # url('admin_settings/labs', method={'POST'}) |
|
761 | 761 | c.active = 'labs' |
|
762 | 762 | |
|
763 | 763 | application_form = LabsSettingsForm()() |
|
764 | 764 | try: |
|
765 | 765 | form_result = application_form.to_python(dict(request.POST)) |
|
766 | 766 | except formencode.Invalid as errors: |
|
767 | 767 | h.flash( |
|
768 | 768 | _('Some form inputs contain invalid data.'), |
|
769 | 769 | category='error') |
|
770 | 770 | return htmlfill.render( |
|
771 |
render('admin/settings/settings. |
|
|
771 | render('admin/settings/settings.mako'), | |
|
772 | 772 | defaults=errors.value, |
|
773 | 773 | errors=errors.error_dict or {}, |
|
774 | 774 | prefix_error=False, |
|
775 | 775 | encoding='UTF-8', |
|
776 | 776 | force_defaults=False |
|
777 | 777 | ) |
|
778 | 778 | |
|
779 | 779 | try: |
|
780 | 780 | session = Session() |
|
781 | 781 | for setting in _LAB_SETTINGS: |
|
782 | 782 | setting_name = setting.key[len('rhodecode_'):] |
|
783 | 783 | sett = SettingsModel().create_or_update_setting( |
|
784 | 784 | setting_name, form_result[setting.key], setting.type) |
|
785 | 785 | session.add(sett) |
|
786 | 786 | |
|
787 | 787 | except Exception: |
|
788 | 788 | log.exception('Exception while updating lab settings') |
|
789 | 789 | h.flash(_('Error occurred during updating labs settings'), |
|
790 | 790 | category='error') |
|
791 | 791 | else: |
|
792 | 792 | Session().commit() |
|
793 | 793 | SettingsModel().invalidate_settings_cache() |
|
794 | 794 | h.flash(_('Updated Labs settings'), category='success') |
|
795 | 795 | return redirect(url('admin_settings_labs')) |
|
796 | 796 | |
|
797 | 797 | return htmlfill.render( |
|
798 |
render('admin/settings/settings. |
|
|
798 | render('admin/settings/settings.mako'), | |
|
799 | 799 | defaults=self._form_defaults(), |
|
800 | 800 | encoding='UTF-8', |
|
801 | 801 | force_defaults=False) |
|
802 | 802 | |
|
803 | 803 | @HasPermissionAllDecorator('hg.admin') |
|
804 | 804 | def settings_labs(self): |
|
805 | 805 | """GET /admin/settings/labs: All items in the collection""" |
|
806 | 806 | # url('admin_settings_labs') |
|
807 | 807 | if not c.labs_active: |
|
808 | 808 | redirect(url('admin_settings')) |
|
809 | 809 | |
|
810 | 810 | c.active = 'labs' |
|
811 | 811 | c.lab_settings = _LAB_SETTINGS |
|
812 | 812 | |
|
813 | 813 | return htmlfill.render( |
|
814 |
render('admin/settings/settings. |
|
|
814 | render('admin/settings/settings.mako'), | |
|
815 | 815 | defaults=self._form_defaults(), |
|
816 | 816 | encoding='UTF-8', |
|
817 | 817 | force_defaults=False) |
|
818 | 818 | |
|
819 | 819 | def _form_defaults(self): |
|
820 | 820 | defaults = SettingsModel().get_all_settings() |
|
821 | 821 | defaults.update(self._get_hg_ui_settings()) |
|
822 | 822 | defaults.update({ |
|
823 | 823 | 'new_svn_branch': '', |
|
824 | 824 | 'new_svn_tag': '', |
|
825 | 825 | }) |
|
826 | 826 | return defaults |
|
827 | 827 | |
|
828 | 828 | |
|
829 | 829 | # :param key: name of the setting including the 'rhodecode_' prefix |
|
830 | 830 | # :param type: the RhodeCodeSetting type to use. |
|
831 | 831 | # :param group: the i18ned group in which we should dispaly this setting |
|
832 | 832 | # :param label: the i18ned label we should display for this setting |
|
833 | 833 | # :param help: the i18ned help we should dispaly for this setting |
|
834 | 834 | LabSetting = collections.namedtuple( |
|
835 | 835 | 'LabSetting', ('key', 'type', 'group', 'label', 'help')) |
|
836 | 836 | |
|
837 | 837 | |
|
838 | 838 | # This list has to be kept in sync with the form |
|
839 | 839 | # rhodecode.model.forms.LabsSettingsForm. |
|
840 | 840 | _LAB_SETTINGS = [ |
|
841 | 841 | |
|
842 | 842 | ] |
@@ -1,487 +1,487 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-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 | User Groups crud controller for pylons |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import formencode |
|
27 | 27 | |
|
28 | 28 | import peppercorn |
|
29 | 29 | from formencode import htmlfill |
|
30 | 30 | from pylons import request, tmpl_context as c, url, config |
|
31 | 31 | from pylons.controllers.util import redirect |
|
32 | 32 | from pylons.i18n.translation import _ |
|
33 | 33 | |
|
34 | 34 | from sqlalchemy.orm import joinedload |
|
35 | 35 | |
|
36 | 36 | from rhodecode.lib import auth |
|
37 | 37 | from rhodecode.lib import helpers as h |
|
38 | 38 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ |
|
39 | 39 | RepoGroupAssignmentError |
|
40 | 40 | from rhodecode.lib.utils import jsonify, action_logger |
|
41 | 41 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
42 | 42 | from rhodecode.lib.auth import ( |
|
43 | 43 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, |
|
44 | 44 | HasPermissionAnyDecorator, XHRRequired) |
|
45 | 45 | from rhodecode.lib.base import BaseController, render |
|
46 | 46 | from rhodecode.model.permission import PermissionModel |
|
47 | 47 | from rhodecode.model.scm import UserGroupList |
|
48 | 48 | from rhodecode.model.user_group import UserGroupModel |
|
49 | 49 | from rhodecode.model.db import ( |
|
50 | 50 | User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm) |
|
51 | 51 | from rhodecode.model.forms import ( |
|
52 | 52 | UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm, |
|
53 | 53 | UserPermissionsForm) |
|
54 | 54 | from rhodecode.model.meta import Session |
|
55 | 55 | from rhodecode.lib.utils import action_logger |
|
56 | 56 | from rhodecode.lib.ext_json import json |
|
57 | 57 | |
|
58 | 58 | log = logging.getLogger(__name__) |
|
59 | 59 | |
|
60 | 60 | |
|
61 | 61 | class UserGroupsController(BaseController): |
|
62 | 62 | """REST Controller styled on the Atom Publishing Protocol""" |
|
63 | 63 | |
|
64 | 64 | @LoginRequired() |
|
65 | 65 | def __before__(self): |
|
66 | 66 | super(UserGroupsController, self).__before__() |
|
67 | 67 | c.available_permissions = config['available_permissions'] |
|
68 | 68 | PermissionModel().set_global_permission_choices(c, gettext_translator=_) |
|
69 | 69 | |
|
70 | 70 | def __load_data(self, user_group_id): |
|
71 | 71 | c.group_members_obj = [x.user for x in c.user_group.members] |
|
72 | 72 | c.group_members_obj.sort(key=lambda u: u.username.lower()) |
|
73 | 73 | c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] |
|
74 | 74 | |
|
75 | 75 | def __load_defaults(self, user_group_id): |
|
76 | 76 | """ |
|
77 | 77 | Load defaults settings for edit, and update |
|
78 | 78 | |
|
79 | 79 | :param user_group_id: |
|
80 | 80 | """ |
|
81 | 81 | user_group = UserGroup.get_or_404(user_group_id) |
|
82 | 82 | data = user_group.get_dict() |
|
83 | 83 | # fill owner |
|
84 | 84 | if user_group.user: |
|
85 | 85 | data.update({'user': user_group.user.username}) |
|
86 | 86 | else: |
|
87 | 87 | replacement_user = User.get_first_super_admin().username |
|
88 | 88 | data.update({'user': replacement_user}) |
|
89 | 89 | return data |
|
90 | 90 | |
|
91 | 91 | def _revoke_perms_on_yourself(self, form_result): |
|
92 | 92 | _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
93 | 93 | form_result['perm_updates']) |
|
94 | 94 | _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
95 | 95 | form_result['perm_additions']) |
|
96 | 96 | _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]), |
|
97 | 97 | form_result['perm_deletions']) |
|
98 | 98 | admin_perm = 'usergroup.admin' |
|
99 | 99 | if _updates and _updates[0][1] != admin_perm or \ |
|
100 | 100 | _additions and _additions[0][1] != admin_perm or \ |
|
101 | 101 | _deletions and _deletions[0][1] != admin_perm: |
|
102 | 102 | return True |
|
103 | 103 | return False |
|
104 | 104 | |
|
105 | 105 | # permission check inside |
|
106 | 106 | @NotAnonymous() |
|
107 | 107 | def index(self): |
|
108 | 108 | """GET /users_groups: All items in the collection""" |
|
109 | 109 | # url('users_groups') |
|
110 | 110 | |
|
111 | 111 | from rhodecode.lib.utils import PartialRenderer |
|
112 |
_render = PartialRenderer('data_table/_dt_elements. |
|
|
112 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
|
113 | 113 | |
|
114 | 114 | def user_group_name(user_group_id, user_group_name): |
|
115 | 115 | return _render("user_group_name", user_group_id, user_group_name) |
|
116 | 116 | |
|
117 | 117 | def user_group_actions(user_group_id, user_group_name): |
|
118 | 118 | return _render("user_group_actions", user_group_id, user_group_name) |
|
119 | 119 | |
|
120 | 120 | ## json generate |
|
121 | 121 | group_iter = UserGroupList(UserGroup.query().all(), |
|
122 | 122 | perm_set=['usergroup.admin']) |
|
123 | 123 | |
|
124 | 124 | user_groups_data = [] |
|
125 | 125 | for user_gr in group_iter: |
|
126 | 126 | user_groups_data.append({ |
|
127 | 127 | "group_name": user_group_name( |
|
128 | 128 | user_gr.users_group_id, h.escape(user_gr.users_group_name)), |
|
129 | 129 | "group_name_raw": user_gr.users_group_name, |
|
130 | 130 | "desc": h.escape(user_gr.user_group_description), |
|
131 | 131 | "members": len(user_gr.members), |
|
132 | 132 | "active": h.bool2icon(user_gr.users_group_active), |
|
133 | 133 | "owner": h.escape(h.link_to_user(user_gr.user.username)), |
|
134 | 134 | "action": user_group_actions( |
|
135 | 135 | user_gr.users_group_id, user_gr.users_group_name) |
|
136 | 136 | }) |
|
137 | 137 | |
|
138 | 138 | c.data = json.dumps(user_groups_data) |
|
139 |
return render('admin/user_groups/user_groups. |
|
|
139 | return render('admin/user_groups/user_groups.mako') | |
|
140 | 140 | |
|
141 | 141 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
142 | 142 | @auth.CSRFRequired() |
|
143 | 143 | def create(self): |
|
144 | 144 | """POST /users_groups: Create a new item""" |
|
145 | 145 | # url('users_groups') |
|
146 | 146 | |
|
147 | 147 | users_group_form = UserGroupForm()() |
|
148 | 148 | try: |
|
149 | 149 | form_result = users_group_form.to_python(dict(request.POST)) |
|
150 | 150 | user_group = UserGroupModel().create( |
|
151 | 151 | name=form_result['users_group_name'], |
|
152 | 152 | description=form_result['user_group_description'], |
|
153 | 153 | owner=c.rhodecode_user.user_id, |
|
154 | 154 | active=form_result['users_group_active']) |
|
155 | 155 | Session().flush() |
|
156 | 156 | |
|
157 | 157 | user_group_name = form_result['users_group_name'] |
|
158 | 158 | action_logger(c.rhodecode_user, |
|
159 | 159 | 'admin_created_users_group:%s' % user_group_name, |
|
160 | 160 | None, self.ip_addr, self.sa) |
|
161 | 161 | user_group_link = h.link_to(h.escape(user_group_name), |
|
162 | 162 | url('edit_users_group', |
|
163 | 163 | user_group_id=user_group.users_group_id)) |
|
164 | 164 | h.flash(h.literal(_('Created user group %(user_group_link)s') |
|
165 | 165 | % {'user_group_link': user_group_link}), |
|
166 | 166 | category='success') |
|
167 | 167 | Session().commit() |
|
168 | 168 | except formencode.Invalid as errors: |
|
169 | 169 | return htmlfill.render( |
|
170 |
render('admin/user_groups/user_group_add. |
|
|
170 | render('admin/user_groups/user_group_add.mako'), | |
|
171 | 171 | defaults=errors.value, |
|
172 | 172 | errors=errors.error_dict or {}, |
|
173 | 173 | prefix_error=False, |
|
174 | 174 | encoding="UTF-8", |
|
175 | 175 | force_defaults=False) |
|
176 | 176 | except Exception: |
|
177 | 177 | log.exception("Exception creating user group") |
|
178 | 178 | h.flash(_('Error occurred during creation of user group %s') \ |
|
179 | 179 | % request.POST.get('users_group_name'), category='error') |
|
180 | 180 | |
|
181 | 181 | return redirect( |
|
182 | 182 | url('edit_users_group', user_group_id=user_group.users_group_id)) |
|
183 | 183 | |
|
184 | 184 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
185 | 185 | def new(self): |
|
186 | 186 | """GET /user_groups/new: Form to create a new item""" |
|
187 | 187 | # url('new_users_group') |
|
188 |
return render('admin/user_groups/user_group_add. |
|
|
188 | return render('admin/user_groups/user_group_add.mako') | |
|
189 | 189 | |
|
190 | 190 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
191 | 191 | @auth.CSRFRequired() |
|
192 | 192 | def update(self, user_group_id): |
|
193 | 193 | """PUT /user_groups/user_group_id: Update an existing item""" |
|
194 | 194 | # Forms posted to this method should contain a hidden field: |
|
195 | 195 | # <input type="hidden" name="_method" value="PUT" /> |
|
196 | 196 | # Or using helpers: |
|
197 | 197 | # h.form(url('users_group', user_group_id=ID), |
|
198 | 198 | # method='put') |
|
199 | 199 | # url('users_group', user_group_id=ID) |
|
200 | 200 | |
|
201 | 201 | user_group_id = safe_int(user_group_id) |
|
202 | 202 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
203 | 203 | c.active = 'settings' |
|
204 | 204 | self.__load_data(user_group_id) |
|
205 | 205 | |
|
206 | 206 | users_group_form = UserGroupForm( |
|
207 | 207 | edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)() |
|
208 | 208 | |
|
209 | 209 | try: |
|
210 | 210 | form_result = users_group_form.to_python(request.POST) |
|
211 | 211 | pstruct = peppercorn.parse(request.POST.items()) |
|
212 | 212 | form_result['users_group_members'] = pstruct['user_group_members'] |
|
213 | 213 | |
|
214 | 214 | UserGroupModel().update(c.user_group, form_result) |
|
215 | 215 | updated_user_group = form_result['users_group_name'] |
|
216 | 216 | action_logger(c.rhodecode_user, |
|
217 | 217 | 'admin_updated_users_group:%s' % updated_user_group, |
|
218 | 218 | None, self.ip_addr, self.sa) |
|
219 | 219 | h.flash(_('Updated user group %s') % updated_user_group, |
|
220 | 220 | category='success') |
|
221 | 221 | Session().commit() |
|
222 | 222 | except formencode.Invalid as errors: |
|
223 | 223 | defaults = errors.value |
|
224 | 224 | e = errors.error_dict or {} |
|
225 | 225 | |
|
226 | 226 | return htmlfill.render( |
|
227 |
render('admin/user_groups/user_group_edit. |
|
|
227 | render('admin/user_groups/user_group_edit.mako'), | |
|
228 | 228 | defaults=defaults, |
|
229 | 229 | errors=e, |
|
230 | 230 | prefix_error=False, |
|
231 | 231 | encoding="UTF-8", |
|
232 | 232 | force_defaults=False) |
|
233 | 233 | except Exception: |
|
234 | 234 | log.exception("Exception during update of user group") |
|
235 | 235 | h.flash(_('Error occurred during update of user group %s') |
|
236 | 236 | % request.POST.get('users_group_name'), category='error') |
|
237 | 237 | |
|
238 | 238 | return redirect(url('edit_users_group', user_group_id=user_group_id)) |
|
239 | 239 | |
|
240 | 240 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
241 | 241 | @auth.CSRFRequired() |
|
242 | 242 | def delete(self, user_group_id): |
|
243 | 243 | """DELETE /user_groups/user_group_id: Delete an existing item""" |
|
244 | 244 | # Forms posted to this method should contain a hidden field: |
|
245 | 245 | # <input type="hidden" name="_method" value="DELETE" /> |
|
246 | 246 | # Or using helpers: |
|
247 | 247 | # h.form(url('users_group', user_group_id=ID), |
|
248 | 248 | # method='delete') |
|
249 | 249 | # url('users_group', user_group_id=ID) |
|
250 | 250 | user_group_id = safe_int(user_group_id) |
|
251 | 251 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
252 | 252 | force = str2bool(request.POST.get('force')) |
|
253 | 253 | |
|
254 | 254 | try: |
|
255 | 255 | UserGroupModel().delete(c.user_group, force=force) |
|
256 | 256 | Session().commit() |
|
257 | 257 | h.flash(_('Successfully deleted user group'), category='success') |
|
258 | 258 | except UserGroupAssignedException as e: |
|
259 | 259 | h.flash(str(e), category='error') |
|
260 | 260 | except Exception: |
|
261 | 261 | log.exception("Exception during deletion of user group") |
|
262 | 262 | h.flash(_('An error occurred during deletion of user group'), |
|
263 | 263 | category='error') |
|
264 | 264 | return redirect(url('users_groups')) |
|
265 | 265 | |
|
266 | 266 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
267 | 267 | def edit(self, user_group_id): |
|
268 | 268 | """GET /user_groups/user_group_id/edit: Form to edit an existing item""" |
|
269 | 269 | # url('edit_users_group', user_group_id=ID) |
|
270 | 270 | |
|
271 | 271 | user_group_id = safe_int(user_group_id) |
|
272 | 272 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
273 | 273 | c.active = 'settings' |
|
274 | 274 | self.__load_data(user_group_id) |
|
275 | 275 | |
|
276 | 276 | defaults = self.__load_defaults(user_group_id) |
|
277 | 277 | |
|
278 | 278 | return htmlfill.render( |
|
279 |
render('admin/user_groups/user_group_edit. |
|
|
279 | render('admin/user_groups/user_group_edit.mako'), | |
|
280 | 280 | defaults=defaults, |
|
281 | 281 | encoding="UTF-8", |
|
282 | 282 | force_defaults=False |
|
283 | 283 | ) |
|
284 | 284 | |
|
285 | 285 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
286 | 286 | def edit_perms(self, user_group_id): |
|
287 | 287 | user_group_id = safe_int(user_group_id) |
|
288 | 288 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
289 | 289 | c.active = 'perms' |
|
290 | 290 | |
|
291 | 291 | defaults = {} |
|
292 | 292 | # fill user group users |
|
293 | 293 | for p in c.user_group.user_user_group_to_perm: |
|
294 | 294 | defaults.update({'u_perm_%s' % p.user.user_id: |
|
295 | 295 | p.permission.permission_name}) |
|
296 | 296 | |
|
297 | 297 | for p in c.user_group.user_group_user_group_to_perm: |
|
298 | 298 | defaults.update({'g_perm_%s' % p.user_group.users_group_id: |
|
299 | 299 | p.permission.permission_name}) |
|
300 | 300 | |
|
301 | 301 | return htmlfill.render( |
|
302 |
render('admin/user_groups/user_group_edit. |
|
|
302 | render('admin/user_groups/user_group_edit.mako'), | |
|
303 | 303 | defaults=defaults, |
|
304 | 304 | encoding="UTF-8", |
|
305 | 305 | force_defaults=False |
|
306 | 306 | ) |
|
307 | 307 | |
|
308 | 308 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
309 | 309 | @auth.CSRFRequired() |
|
310 | 310 | def update_perms(self, user_group_id): |
|
311 | 311 | """ |
|
312 | 312 | grant permission for given usergroup |
|
313 | 313 | |
|
314 | 314 | :param user_group_id: |
|
315 | 315 | """ |
|
316 | 316 | user_group_id = safe_int(user_group_id) |
|
317 | 317 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
318 | 318 | form = UserGroupPermsForm()().to_python(request.POST) |
|
319 | 319 | |
|
320 | 320 | if not c.rhodecode_user.is_admin: |
|
321 | 321 | if self._revoke_perms_on_yourself(form): |
|
322 | 322 | msg = _('Cannot change permission for yourself as admin') |
|
323 | 323 | h.flash(msg, category='warning') |
|
324 | 324 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) |
|
325 | 325 | |
|
326 | 326 | try: |
|
327 | 327 | UserGroupModel().update_permissions(user_group_id, |
|
328 | 328 | form['perm_additions'], form['perm_updates'], form['perm_deletions']) |
|
329 | 329 | except RepoGroupAssignmentError: |
|
330 | 330 | h.flash(_('Target group cannot be the same'), category='error') |
|
331 | 331 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) |
|
332 | 332 | #TODO: implement this |
|
333 | 333 | #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', |
|
334 | 334 | # repo_name, self.ip_addr, self.sa) |
|
335 | 335 | Session().commit() |
|
336 | 336 | h.flash(_('User Group permissions updated'), category='success') |
|
337 | 337 | return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) |
|
338 | 338 | |
|
339 | 339 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
340 | 340 | def edit_perms_summary(self, user_group_id): |
|
341 | 341 | user_group_id = safe_int(user_group_id) |
|
342 | 342 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
343 | 343 | c.active = 'perms_summary' |
|
344 | 344 | permissions = { |
|
345 | 345 | 'repositories': {}, |
|
346 | 346 | 'repositories_groups': {}, |
|
347 | 347 | } |
|
348 | 348 | ugroup_repo_perms = UserGroupRepoToPerm.query()\ |
|
349 | 349 | .options(joinedload(UserGroupRepoToPerm.permission))\ |
|
350 | 350 | .options(joinedload(UserGroupRepoToPerm.repository))\ |
|
351 | 351 | .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\ |
|
352 | 352 | .all() |
|
353 | 353 | |
|
354 | 354 | for gr in ugroup_repo_perms: |
|
355 | 355 | permissions['repositories'][gr.repository.repo_name] \ |
|
356 | 356 | = gr.permission.permission_name |
|
357 | 357 | |
|
358 | 358 | ugroup_group_perms = UserGroupRepoGroupToPerm.query()\ |
|
359 | 359 | .options(joinedload(UserGroupRepoGroupToPerm.permission))\ |
|
360 | 360 | .options(joinedload(UserGroupRepoGroupToPerm.group))\ |
|
361 | 361 | .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\ |
|
362 | 362 | .all() |
|
363 | 363 | |
|
364 | 364 | for gr in ugroup_group_perms: |
|
365 | 365 | permissions['repositories_groups'][gr.group.group_name] \ |
|
366 | 366 | = gr.permission.permission_name |
|
367 | 367 | c.permissions = permissions |
|
368 |
return render('admin/user_groups/user_group_edit. |
|
|
368 | return render('admin/user_groups/user_group_edit.mako') | |
|
369 | 369 | |
|
370 | 370 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
371 | 371 | def edit_global_perms(self, user_group_id): |
|
372 | 372 | user_group_id = safe_int(user_group_id) |
|
373 | 373 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
374 | 374 | c.active = 'global_perms' |
|
375 | 375 | |
|
376 | 376 | c.default_user = User.get_default_user() |
|
377 | 377 | defaults = c.user_group.get_dict() |
|
378 | 378 | defaults.update(c.default_user.get_default_perms(suffix='_inherited')) |
|
379 | 379 | defaults.update(c.user_group.get_default_perms()) |
|
380 | 380 | |
|
381 | 381 | return htmlfill.render( |
|
382 |
render('admin/user_groups/user_group_edit. |
|
|
382 | render('admin/user_groups/user_group_edit.mako'), | |
|
383 | 383 | defaults=defaults, |
|
384 | 384 | encoding="UTF-8", |
|
385 | 385 | force_defaults=False |
|
386 | 386 | ) |
|
387 | 387 | |
|
388 | 388 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
389 | 389 | @auth.CSRFRequired() |
|
390 | 390 | def update_global_perms(self, user_group_id): |
|
391 | 391 | """PUT /users_perm/user_group_id: Update an existing item""" |
|
392 | 392 | # url('users_group_perm', user_group_id=ID, method='put') |
|
393 | 393 | user_group_id = safe_int(user_group_id) |
|
394 | 394 | user_group = UserGroup.get_or_404(user_group_id) |
|
395 | 395 | c.active = 'global_perms' |
|
396 | 396 | |
|
397 | 397 | try: |
|
398 | 398 | # first stage that verifies the checkbox |
|
399 | 399 | _form = UserIndividualPermissionsForm() |
|
400 | 400 | form_result = _form.to_python(dict(request.POST)) |
|
401 | 401 | inherit_perms = form_result['inherit_default_permissions'] |
|
402 | 402 | user_group.inherit_default_permissions = inherit_perms |
|
403 | 403 | Session().add(user_group) |
|
404 | 404 | |
|
405 | 405 | if not inherit_perms: |
|
406 | 406 | # only update the individual ones if we un check the flag |
|
407 | 407 | _form = UserPermissionsForm( |
|
408 | 408 | [x[0] for x in c.repo_create_choices], |
|
409 | 409 | [x[0] for x in c.repo_create_on_write_choices], |
|
410 | 410 | [x[0] for x in c.repo_group_create_choices], |
|
411 | 411 | [x[0] for x in c.user_group_create_choices], |
|
412 | 412 | [x[0] for x in c.fork_choices], |
|
413 | 413 | [x[0] for x in c.inherit_default_permission_choices])() |
|
414 | 414 | |
|
415 | 415 | form_result = _form.to_python(dict(request.POST)) |
|
416 | 416 | form_result.update({'perm_user_group_id': user_group.users_group_id}) |
|
417 | 417 | |
|
418 | 418 | PermissionModel().update_user_group_permissions(form_result) |
|
419 | 419 | |
|
420 | 420 | Session().commit() |
|
421 | 421 | h.flash(_('User Group global permissions updated successfully'), |
|
422 | 422 | category='success') |
|
423 | 423 | |
|
424 | 424 | except formencode.Invalid as errors: |
|
425 | 425 | defaults = errors.value |
|
426 | 426 | c.user_group = user_group |
|
427 | 427 | return htmlfill.render( |
|
428 |
render('admin/user_groups/user_group_edit. |
|
|
428 | render('admin/user_groups/user_group_edit.mako'), | |
|
429 | 429 | defaults=defaults, |
|
430 | 430 | errors=errors.error_dict or {}, |
|
431 | 431 | prefix_error=False, |
|
432 | 432 | encoding="UTF-8", |
|
433 | 433 | force_defaults=False) |
|
434 | 434 | |
|
435 | 435 | except Exception: |
|
436 | 436 | log.exception("Exception during permissions saving") |
|
437 | 437 | h.flash(_('An error occurred during permissions saving'), |
|
438 | 438 | category='error') |
|
439 | 439 | |
|
440 | 440 | return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id)) |
|
441 | 441 | |
|
442 | 442 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
443 | 443 | def edit_advanced(self, user_group_id): |
|
444 | 444 | user_group_id = safe_int(user_group_id) |
|
445 | 445 | c.user_group = UserGroup.get_or_404(user_group_id) |
|
446 | 446 | c.active = 'advanced' |
|
447 | 447 | c.group_members_obj = sorted( |
|
448 | 448 | (x.user for x in c.user_group.members), |
|
449 | 449 | key=lambda u: u.username.lower()) |
|
450 | 450 | |
|
451 | 451 | c.group_to_repos = sorted( |
|
452 | 452 | (x.repository for x in c.user_group.users_group_repo_to_perm), |
|
453 | 453 | key=lambda u: u.repo_name.lower()) |
|
454 | 454 | |
|
455 | 455 | c.group_to_repo_groups = sorted( |
|
456 | 456 | (x.group for x in c.user_group.users_group_repo_group_to_perm), |
|
457 | 457 | key=lambda u: u.group_name.lower()) |
|
458 | 458 | |
|
459 |
return render('admin/user_groups/user_group_edit. |
|
|
459 | return render('admin/user_groups/user_group_edit.mako') | |
|
460 | 460 | |
|
461 | 461 | @HasUserGroupPermissionAnyDecorator('usergroup.admin') |
|
462 | 462 | @XHRRequired() |
|
463 | 463 | @jsonify |
|
464 | 464 | def user_group_members(self, user_group_id): |
|
465 | 465 | user_group_id = safe_int(user_group_id) |
|
466 | 466 | user_group = UserGroup.get_or_404(user_group_id) |
|
467 | 467 | group_members_obj = sorted((x.user for x in user_group.members), |
|
468 | 468 | key=lambda u: u.username.lower()) |
|
469 | 469 | |
|
470 | 470 | group_members = [ |
|
471 | 471 | { |
|
472 | 472 | 'id': user.user_id, |
|
473 | 473 | 'first_name': user.name, |
|
474 | 474 | 'last_name': user.lastname, |
|
475 | 475 | 'username': user.username, |
|
476 | 476 | 'icon_link': h.gravatar_url(user.email, 30), |
|
477 | 477 | 'value_display': h.person(user.email), |
|
478 | 478 | 'value': user.username, |
|
479 | 479 | 'value_type': 'user', |
|
480 | 480 | 'active': user.active, |
|
481 | 481 | } |
|
482 | 482 | for user in group_members_obj |
|
483 | 483 | ] |
|
484 | 484 | |
|
485 | 485 | return { |
|
486 | 486 | 'members': group_members |
|
487 | 487 | } |
@@ -1,748 +1,748 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | Users crud controller for pylons |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import formencode |
|
27 | 27 | |
|
28 | 28 | from formencode import htmlfill |
|
29 | 29 | from pylons import request, tmpl_context as c, url, config |
|
30 | 30 | from pylons.controllers.util import redirect |
|
31 | 31 | from pylons.i18n.translation import _ |
|
32 | 32 | |
|
33 | 33 | from rhodecode.authentication.plugins import auth_rhodecode |
|
34 | 34 | from rhodecode.lib.exceptions import ( |
|
35 | 35 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, |
|
36 | 36 | UserOwnsUserGroupsException, UserCreationError) |
|
37 | 37 | from rhodecode.lib import helpers as h |
|
38 | 38 | from rhodecode.lib import auth |
|
39 | 39 | from rhodecode.lib.auth import ( |
|
40 | 40 | LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token) |
|
41 | 41 | from rhodecode.lib.base import BaseController, render |
|
42 | 42 | from rhodecode.model.auth_token import AuthTokenModel |
|
43 | 43 | |
|
44 | 44 | from rhodecode.model.db import ( |
|
45 | 45 | PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) |
|
46 | 46 | from rhodecode.model.forms import ( |
|
47 | 47 | UserForm, UserPermissionsForm, UserIndividualPermissionsForm) |
|
48 | 48 | from rhodecode.model.repo_group import RepoGroupModel |
|
49 | 49 | from rhodecode.model.user import UserModel |
|
50 | 50 | from rhodecode.model.meta import Session |
|
51 | 51 | from rhodecode.model.permission import PermissionModel |
|
52 | 52 | from rhodecode.lib.utils import action_logger |
|
53 | 53 | from rhodecode.lib.ext_json import json |
|
54 | 54 | from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict |
|
55 | 55 | |
|
56 | 56 | log = logging.getLogger(__name__) |
|
57 | 57 | |
|
58 | 58 | |
|
59 | 59 | class UsersController(BaseController): |
|
60 | 60 | """REST Controller styled on the Atom Publishing Protocol""" |
|
61 | 61 | |
|
62 | 62 | @LoginRequired() |
|
63 | 63 | def __before__(self): |
|
64 | 64 | super(UsersController, self).__before__() |
|
65 | 65 | c.available_permissions = config['available_permissions'] |
|
66 | 66 | c.allowed_languages = [ |
|
67 | 67 | ('en', 'English (en)'), |
|
68 | 68 | ('de', 'German (de)'), |
|
69 | 69 | ('fr', 'French (fr)'), |
|
70 | 70 | ('it', 'Italian (it)'), |
|
71 | 71 | ('ja', 'Japanese (ja)'), |
|
72 | 72 | ('pl', 'Polish (pl)'), |
|
73 | 73 | ('pt', 'Portuguese (pt)'), |
|
74 | 74 | ('ru', 'Russian (ru)'), |
|
75 | 75 | ('zh', 'Chinese (zh)'), |
|
76 | 76 | ] |
|
77 | 77 | PermissionModel().set_global_permission_choices(c, gettext_translator=_) |
|
78 | 78 | |
|
79 | 79 | @HasPermissionAllDecorator('hg.admin') |
|
80 | 80 | def index(self): |
|
81 | 81 | """GET /users: All items in the collection""" |
|
82 | 82 | # url('users') |
|
83 | 83 | |
|
84 | 84 | from rhodecode.lib.utils import PartialRenderer |
|
85 |
_render = PartialRenderer('data_table/_dt_elements. |
|
|
85 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
|
86 | 86 | |
|
87 | 87 | def username(user_id, username): |
|
88 | 88 | return _render("user_name", user_id, username) |
|
89 | 89 | |
|
90 | 90 | def user_actions(user_id, username): |
|
91 | 91 | return _render("user_actions", user_id, username) |
|
92 | 92 | |
|
93 | 93 | # json generate |
|
94 | 94 | c.users_list = User.query()\ |
|
95 | 95 | .filter(User.username != User.DEFAULT_USER) \ |
|
96 | 96 | .all() |
|
97 | 97 | |
|
98 | 98 | users_data = [] |
|
99 | 99 | for user in c.users_list: |
|
100 | 100 | users_data.append({ |
|
101 | 101 | "username": h.gravatar_with_user(user.username), |
|
102 | 102 | "username_raw": user.username, |
|
103 | 103 | "email": user.email, |
|
104 | 104 | "first_name": h.escape(user.name), |
|
105 | 105 | "last_name": h.escape(user.lastname), |
|
106 | 106 | "last_login": h.format_date(user.last_login), |
|
107 | 107 | "last_login_raw": datetime_to_time(user.last_login), |
|
108 | 108 | "last_activity": h.format_date( |
|
109 | 109 | h.time_to_datetime(user.user_data.get('last_activity', 0))), |
|
110 | 110 | "last_activity_raw": user.user_data.get('last_activity', 0), |
|
111 | 111 | "active": h.bool2icon(user.active), |
|
112 | 112 | "active_raw": user.active, |
|
113 | 113 | "admin": h.bool2icon(user.admin), |
|
114 | 114 | "admin_raw": user.admin, |
|
115 | 115 | "extern_type": user.extern_type, |
|
116 | 116 | "extern_name": user.extern_name, |
|
117 | 117 | "action": user_actions(user.user_id, user.username), |
|
118 | 118 | }) |
|
119 | 119 | |
|
120 | 120 | |
|
121 | 121 | c.data = json.dumps(users_data) |
|
122 |
return render('admin/users/users. |
|
|
122 | return render('admin/users/users.mako') | |
|
123 | 123 | |
|
124 | 124 | def _get_personal_repo_group_template_vars(self): |
|
125 | 125 | DummyUser = AttributeDict({ |
|
126 | 126 | 'username': '${username}', |
|
127 | 127 | 'user_id': '${user_id}', |
|
128 | 128 | }) |
|
129 | 129 | c.default_create_repo_group = RepoGroupModel() \ |
|
130 | 130 | .get_default_create_personal_repo_group() |
|
131 | 131 | c.personal_repo_group_name = RepoGroupModel() \ |
|
132 | 132 | .get_personal_group_name(DummyUser) |
|
133 | 133 | |
|
134 | 134 | @HasPermissionAllDecorator('hg.admin') |
|
135 | 135 | @auth.CSRFRequired() |
|
136 | 136 | def create(self): |
|
137 | 137 | """POST /users: Create a new item""" |
|
138 | 138 | # url('users') |
|
139 | 139 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name |
|
140 | 140 | user_model = UserModel() |
|
141 | 141 | user_form = UserForm()() |
|
142 | 142 | try: |
|
143 | 143 | form_result = user_form.to_python(dict(request.POST)) |
|
144 | 144 | user = user_model.create(form_result) |
|
145 | 145 | Session().flush() |
|
146 | 146 | username = form_result['username'] |
|
147 | 147 | action_logger(c.rhodecode_user, 'admin_created_user:%s' % username, |
|
148 | 148 | None, self.ip_addr, self.sa) |
|
149 | 149 | |
|
150 | 150 | user_link = h.link_to(h.escape(username), |
|
151 | 151 | url('edit_user', |
|
152 | 152 | user_id=user.user_id)) |
|
153 | 153 | h.flash(h.literal(_('Created user %(user_link)s') |
|
154 | 154 | % {'user_link': user_link}), category='success') |
|
155 | 155 | Session().commit() |
|
156 | 156 | except formencode.Invalid as errors: |
|
157 | 157 | self._get_personal_repo_group_template_vars() |
|
158 | 158 | return htmlfill.render( |
|
159 |
render('admin/users/user_add. |
|
|
159 | render('admin/users/user_add.mako'), | |
|
160 | 160 | defaults=errors.value, |
|
161 | 161 | errors=errors.error_dict or {}, |
|
162 | 162 | prefix_error=False, |
|
163 | 163 | encoding="UTF-8", |
|
164 | 164 | force_defaults=False) |
|
165 | 165 | except UserCreationError as e: |
|
166 | 166 | h.flash(e, 'error') |
|
167 | 167 | except Exception: |
|
168 | 168 | log.exception("Exception creation of user") |
|
169 | 169 | h.flash(_('Error occurred during creation of user %s') |
|
170 | 170 | % request.POST.get('username'), category='error') |
|
171 | 171 | return redirect(url('users')) |
|
172 | 172 | |
|
173 | 173 | @HasPermissionAllDecorator('hg.admin') |
|
174 | 174 | def new(self): |
|
175 | 175 | """GET /users/new: Form to create a new item""" |
|
176 | 176 | # url('new_user') |
|
177 | 177 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name |
|
178 | 178 | self._get_personal_repo_group_template_vars() |
|
179 |
return render('admin/users/user_add. |
|
|
179 | return render('admin/users/user_add.mako') | |
|
180 | 180 | |
|
181 | 181 | @HasPermissionAllDecorator('hg.admin') |
|
182 | 182 | @auth.CSRFRequired() |
|
183 | 183 | def update(self, user_id): |
|
184 | 184 | """PUT /users/user_id: Update an existing item""" |
|
185 | 185 | # Forms posted to this method should contain a hidden field: |
|
186 | 186 | # <input type="hidden" name="_method" value="PUT" /> |
|
187 | 187 | # Or using helpers: |
|
188 | 188 | # h.form(url('update_user', user_id=ID), |
|
189 | 189 | # method='put') |
|
190 | 190 | # url('user', user_id=ID) |
|
191 | 191 | user_id = safe_int(user_id) |
|
192 | 192 | c.user = User.get_or_404(user_id) |
|
193 | 193 | c.active = 'profile' |
|
194 | 194 | c.extern_type = c.user.extern_type |
|
195 | 195 | c.extern_name = c.user.extern_name |
|
196 | 196 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
197 | 197 | available_languages = [x[0] for x in c.allowed_languages] |
|
198 | 198 | _form = UserForm(edit=True, available_languages=available_languages, |
|
199 | 199 | old_data={'user_id': user_id, |
|
200 | 200 | 'email': c.user.email})() |
|
201 | 201 | form_result = {} |
|
202 | 202 | try: |
|
203 | 203 | form_result = _form.to_python(dict(request.POST)) |
|
204 | 204 | skip_attrs = ['extern_type', 'extern_name'] |
|
205 | 205 | # TODO: plugin should define if username can be updated |
|
206 | 206 | if c.extern_type != "rhodecode": |
|
207 | 207 | # forbid updating username for external accounts |
|
208 | 208 | skip_attrs.append('username') |
|
209 | 209 | |
|
210 | 210 | UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result) |
|
211 | 211 | usr = form_result['username'] |
|
212 | 212 | action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr, |
|
213 | 213 | None, self.ip_addr, self.sa) |
|
214 | 214 | h.flash(_('User updated successfully'), category='success') |
|
215 | 215 | Session().commit() |
|
216 | 216 | except formencode.Invalid as errors: |
|
217 | 217 | defaults = errors.value |
|
218 | 218 | e = errors.error_dict or {} |
|
219 | 219 | |
|
220 | 220 | return htmlfill.render( |
|
221 |
render('admin/users/user_edit. |
|
|
221 | render('admin/users/user_edit.mako'), | |
|
222 | 222 | defaults=defaults, |
|
223 | 223 | errors=e, |
|
224 | 224 | prefix_error=False, |
|
225 | 225 | encoding="UTF-8", |
|
226 | 226 | force_defaults=False) |
|
227 | 227 | except UserCreationError as e: |
|
228 | 228 | h.flash(e, 'error') |
|
229 | 229 | except Exception: |
|
230 | 230 | log.exception("Exception updating user") |
|
231 | 231 | h.flash(_('Error occurred during update of user %s') |
|
232 | 232 | % form_result.get('username'), category='error') |
|
233 | 233 | return redirect(url('edit_user', user_id=user_id)) |
|
234 | 234 | |
|
235 | 235 | @HasPermissionAllDecorator('hg.admin') |
|
236 | 236 | @auth.CSRFRequired() |
|
237 | 237 | def delete(self, user_id): |
|
238 | 238 | """DELETE /users/user_id: Delete an existing item""" |
|
239 | 239 | # Forms posted to this method should contain a hidden field: |
|
240 | 240 | # <input type="hidden" name="_method" value="DELETE" /> |
|
241 | 241 | # Or using helpers: |
|
242 | 242 | # h.form(url('delete_user', user_id=ID), |
|
243 | 243 | # method='delete') |
|
244 | 244 | # url('user', user_id=ID) |
|
245 | 245 | user_id = safe_int(user_id) |
|
246 | 246 | c.user = User.get_or_404(user_id) |
|
247 | 247 | |
|
248 | 248 | _repos = c.user.repositories |
|
249 | 249 | _repo_groups = c.user.repository_groups |
|
250 | 250 | _user_groups = c.user.user_groups |
|
251 | 251 | |
|
252 | 252 | handle_repos = None |
|
253 | 253 | handle_repo_groups = None |
|
254 | 254 | handle_user_groups = None |
|
255 | 255 | # dummy call for flash of handle |
|
256 | 256 | set_handle_flash_repos = lambda: None |
|
257 | 257 | set_handle_flash_repo_groups = lambda: None |
|
258 | 258 | set_handle_flash_user_groups = lambda: None |
|
259 | 259 | |
|
260 | 260 | if _repos and request.POST.get('user_repos'): |
|
261 | 261 | do = request.POST['user_repos'] |
|
262 | 262 | if do == 'detach': |
|
263 | 263 | handle_repos = 'detach' |
|
264 | 264 | set_handle_flash_repos = lambda: h.flash( |
|
265 | 265 | _('Detached %s repositories') % len(_repos), |
|
266 | 266 | category='success') |
|
267 | 267 | elif do == 'delete': |
|
268 | 268 | handle_repos = 'delete' |
|
269 | 269 | set_handle_flash_repos = lambda: h.flash( |
|
270 | 270 | _('Deleted %s repositories') % len(_repos), |
|
271 | 271 | category='success') |
|
272 | 272 | |
|
273 | 273 | if _repo_groups and request.POST.get('user_repo_groups'): |
|
274 | 274 | do = request.POST['user_repo_groups'] |
|
275 | 275 | if do == 'detach': |
|
276 | 276 | handle_repo_groups = 'detach' |
|
277 | 277 | set_handle_flash_repo_groups = lambda: h.flash( |
|
278 | 278 | _('Detached %s repository groups') % len(_repo_groups), |
|
279 | 279 | category='success') |
|
280 | 280 | elif do == 'delete': |
|
281 | 281 | handle_repo_groups = 'delete' |
|
282 | 282 | set_handle_flash_repo_groups = lambda: h.flash( |
|
283 | 283 | _('Deleted %s repository groups') % len(_repo_groups), |
|
284 | 284 | category='success') |
|
285 | 285 | |
|
286 | 286 | if _user_groups and request.POST.get('user_user_groups'): |
|
287 | 287 | do = request.POST['user_user_groups'] |
|
288 | 288 | if do == 'detach': |
|
289 | 289 | handle_user_groups = 'detach' |
|
290 | 290 | set_handle_flash_user_groups = lambda: h.flash( |
|
291 | 291 | _('Detached %s user groups') % len(_user_groups), |
|
292 | 292 | category='success') |
|
293 | 293 | elif do == 'delete': |
|
294 | 294 | handle_user_groups = 'delete' |
|
295 | 295 | set_handle_flash_user_groups = lambda: h.flash( |
|
296 | 296 | _('Deleted %s user groups') % len(_user_groups), |
|
297 | 297 | category='success') |
|
298 | 298 | |
|
299 | 299 | try: |
|
300 | 300 | UserModel().delete(c.user, handle_repos=handle_repos, |
|
301 | 301 | handle_repo_groups=handle_repo_groups, |
|
302 | 302 | handle_user_groups=handle_user_groups) |
|
303 | 303 | Session().commit() |
|
304 | 304 | set_handle_flash_repos() |
|
305 | 305 | set_handle_flash_repo_groups() |
|
306 | 306 | set_handle_flash_user_groups() |
|
307 | 307 | h.flash(_('Successfully deleted user'), category='success') |
|
308 | 308 | except (UserOwnsReposException, UserOwnsRepoGroupsException, |
|
309 | 309 | UserOwnsUserGroupsException, DefaultUserException) as e: |
|
310 | 310 | h.flash(e, category='warning') |
|
311 | 311 | except Exception: |
|
312 | 312 | log.exception("Exception during deletion of user") |
|
313 | 313 | h.flash(_('An error occurred during deletion of user'), |
|
314 | 314 | category='error') |
|
315 | 315 | return redirect(url('users')) |
|
316 | 316 | |
|
317 | 317 | @HasPermissionAllDecorator('hg.admin') |
|
318 | 318 | @auth.CSRFRequired() |
|
319 | 319 | def reset_password(self, user_id): |
|
320 | 320 | """ |
|
321 | 321 | toggle reset password flag for this user |
|
322 | 322 | |
|
323 | 323 | :param user_id: |
|
324 | 324 | """ |
|
325 | 325 | user_id = safe_int(user_id) |
|
326 | 326 | c.user = User.get_or_404(user_id) |
|
327 | 327 | try: |
|
328 | 328 | old_value = c.user.user_data.get('force_password_change') |
|
329 | 329 | c.user.update_userdata(force_password_change=not old_value) |
|
330 | 330 | Session().commit() |
|
331 | 331 | if old_value: |
|
332 | 332 | msg = _('Force password change disabled for user') |
|
333 | 333 | else: |
|
334 | 334 | msg = _('Force password change enabled for user') |
|
335 | 335 | h.flash(msg, category='success') |
|
336 | 336 | except Exception: |
|
337 | 337 | log.exception("Exception during password reset for user") |
|
338 | 338 | h.flash(_('An error occurred during password reset for user'), |
|
339 | 339 | category='error') |
|
340 | 340 | |
|
341 | 341 | return redirect(url('edit_user_advanced', user_id=user_id)) |
|
342 | 342 | |
|
343 | 343 | @HasPermissionAllDecorator('hg.admin') |
|
344 | 344 | @auth.CSRFRequired() |
|
345 | 345 | def create_personal_repo_group(self, user_id): |
|
346 | 346 | """ |
|
347 | 347 | Create personal repository group for this user |
|
348 | 348 | |
|
349 | 349 | :param user_id: |
|
350 | 350 | """ |
|
351 | 351 | from rhodecode.model.repo_group import RepoGroupModel |
|
352 | 352 | |
|
353 | 353 | user_id = safe_int(user_id) |
|
354 | 354 | c.user = User.get_or_404(user_id) |
|
355 | 355 | personal_repo_group = RepoGroup.get_user_personal_repo_group( |
|
356 | 356 | c.user.user_id) |
|
357 | 357 | if personal_repo_group: |
|
358 | 358 | return redirect(url('edit_user_advanced', user_id=user_id)) |
|
359 | 359 | |
|
360 | 360 | personal_repo_group_name = RepoGroupModel().get_personal_group_name( |
|
361 | 361 | c.user) |
|
362 | 362 | named_personal_group = RepoGroup.get_by_group_name( |
|
363 | 363 | personal_repo_group_name) |
|
364 | 364 | try: |
|
365 | 365 | |
|
366 | 366 | if named_personal_group and named_personal_group.user_id == c.user.user_id: |
|
367 | 367 | # migrate the same named group, and mark it as personal |
|
368 | 368 | named_personal_group.personal = True |
|
369 | 369 | Session().add(named_personal_group) |
|
370 | 370 | Session().commit() |
|
371 | 371 | msg = _('Linked repository group `%s` as personal' % ( |
|
372 | 372 | personal_repo_group_name,)) |
|
373 | 373 | h.flash(msg, category='success') |
|
374 | 374 | elif not named_personal_group: |
|
375 | 375 | RepoGroupModel().create_personal_repo_group(c.user) |
|
376 | 376 | |
|
377 | 377 | msg = _('Created repository group `%s`' % ( |
|
378 | 378 | personal_repo_group_name,)) |
|
379 | 379 | h.flash(msg, category='success') |
|
380 | 380 | else: |
|
381 | 381 | msg = _('Repository group `%s` is already taken' % ( |
|
382 | 382 | personal_repo_group_name,)) |
|
383 | 383 | h.flash(msg, category='warning') |
|
384 | 384 | except Exception: |
|
385 | 385 | log.exception("Exception during repository group creation") |
|
386 | 386 | msg = _( |
|
387 | 387 | 'An error occurred during repository group creation for user') |
|
388 | 388 | h.flash(msg, category='error') |
|
389 | 389 | Session().rollback() |
|
390 | 390 | |
|
391 | 391 | return redirect(url('edit_user_advanced', user_id=user_id)) |
|
392 | 392 | |
|
393 | 393 | @HasPermissionAllDecorator('hg.admin') |
|
394 | 394 | def show(self, user_id): |
|
395 | 395 | """GET /users/user_id: Show a specific item""" |
|
396 | 396 | # url('user', user_id=ID) |
|
397 | 397 | User.get_or_404(-1) |
|
398 | 398 | |
|
399 | 399 | @HasPermissionAllDecorator('hg.admin') |
|
400 | 400 | def edit(self, user_id): |
|
401 | 401 | """GET /users/user_id/edit: Form to edit an existing item""" |
|
402 | 402 | # url('edit_user', user_id=ID) |
|
403 | 403 | user_id = safe_int(user_id) |
|
404 | 404 | c.user = User.get_or_404(user_id) |
|
405 | 405 | if c.user.username == User.DEFAULT_USER: |
|
406 | 406 | h.flash(_("You can't edit this user"), category='warning') |
|
407 | 407 | return redirect(url('users')) |
|
408 | 408 | |
|
409 | 409 | c.active = 'profile' |
|
410 | 410 | c.extern_type = c.user.extern_type |
|
411 | 411 | c.extern_name = c.user.extern_name |
|
412 | 412 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
413 | 413 | |
|
414 | 414 | defaults = c.user.get_dict() |
|
415 | 415 | defaults.update({'language': c.user.user_data.get('language')}) |
|
416 | 416 | return htmlfill.render( |
|
417 |
render('admin/users/user_edit. |
|
|
417 | render('admin/users/user_edit.mako'), | |
|
418 | 418 | defaults=defaults, |
|
419 | 419 | encoding="UTF-8", |
|
420 | 420 | force_defaults=False) |
|
421 | 421 | |
|
422 | 422 | @HasPermissionAllDecorator('hg.admin') |
|
423 | 423 | def edit_advanced(self, user_id): |
|
424 | 424 | user_id = safe_int(user_id) |
|
425 | 425 | user = c.user = User.get_or_404(user_id) |
|
426 | 426 | if user.username == User.DEFAULT_USER: |
|
427 | 427 | h.flash(_("You can't edit this user"), category='warning') |
|
428 | 428 | return redirect(url('users')) |
|
429 | 429 | |
|
430 | 430 | c.active = 'advanced' |
|
431 | 431 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
432 | 432 | c.personal_repo_group = c.perm_user.personal_repo_group |
|
433 | 433 | c.personal_repo_group_name = RepoGroupModel()\ |
|
434 | 434 | .get_personal_group_name(user) |
|
435 | 435 | c.first_admin = User.get_first_super_admin() |
|
436 | 436 | defaults = user.get_dict() |
|
437 | 437 | |
|
438 | 438 | # Interim workaround if the user participated on any pull requests as a |
|
439 | 439 | # reviewer. |
|
440 | 440 | has_review = bool(PullRequestReviewers.query().filter( |
|
441 | 441 | PullRequestReviewers.user_id == user_id).first()) |
|
442 | 442 | c.can_delete_user = not has_review |
|
443 | 443 | c.can_delete_user_message = _( |
|
444 | 444 | 'The user participates as reviewer in pull requests and ' |
|
445 | 445 | 'cannot be deleted. You can set the user to ' |
|
446 | 446 | '"inactive" instead of deleting it.') if has_review else '' |
|
447 | 447 | |
|
448 | 448 | return htmlfill.render( |
|
449 |
render('admin/users/user_edit. |
|
|
449 | render('admin/users/user_edit.mako'), | |
|
450 | 450 | defaults=defaults, |
|
451 | 451 | encoding="UTF-8", |
|
452 | 452 | force_defaults=False) |
|
453 | 453 | |
|
454 | 454 | @HasPermissionAllDecorator('hg.admin') |
|
455 | 455 | def edit_auth_tokens(self, user_id): |
|
456 | 456 | user_id = safe_int(user_id) |
|
457 | 457 | c.user = User.get_or_404(user_id) |
|
458 | 458 | if c.user.username == User.DEFAULT_USER: |
|
459 | 459 | h.flash(_("You can't edit this user"), category='warning') |
|
460 | 460 | return redirect(url('users')) |
|
461 | 461 | |
|
462 | 462 | c.active = 'auth_tokens' |
|
463 | 463 | show_expired = True |
|
464 | 464 | c.lifetime_values = [ |
|
465 | 465 | (str(-1), _('forever')), |
|
466 | 466 | (str(5), _('5 minutes')), |
|
467 | 467 | (str(60), _('1 hour')), |
|
468 | 468 | (str(60 * 24), _('1 day')), |
|
469 | 469 | (str(60 * 24 * 30), _('1 month')), |
|
470 | 470 | ] |
|
471 | 471 | c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] |
|
472 | 472 | c.role_values = [(x, AuthTokenModel.cls._get_role_name(x)) |
|
473 | 473 | for x in AuthTokenModel.cls.ROLES] |
|
474 | 474 | c.role_options = [(c.role_values, _("Role"))] |
|
475 | 475 | c.user_auth_tokens = AuthTokenModel().get_auth_tokens( |
|
476 | 476 | c.user.user_id, show_expired=show_expired) |
|
477 | 477 | defaults = c.user.get_dict() |
|
478 | 478 | return htmlfill.render( |
|
479 |
render('admin/users/user_edit. |
|
|
479 | render('admin/users/user_edit.mako'), | |
|
480 | 480 | defaults=defaults, |
|
481 | 481 | encoding="UTF-8", |
|
482 | 482 | force_defaults=False) |
|
483 | 483 | |
|
484 | 484 | @HasPermissionAllDecorator('hg.admin') |
|
485 | 485 | @auth.CSRFRequired() |
|
486 | 486 | def add_auth_token(self, user_id): |
|
487 | 487 | user_id = safe_int(user_id) |
|
488 | 488 | c.user = User.get_or_404(user_id) |
|
489 | 489 | if c.user.username == User.DEFAULT_USER: |
|
490 | 490 | h.flash(_("You can't edit this user"), category='warning') |
|
491 | 491 | return redirect(url('users')) |
|
492 | 492 | |
|
493 | 493 | lifetime = safe_int(request.POST.get('lifetime'), -1) |
|
494 | 494 | description = request.POST.get('description') |
|
495 | 495 | role = request.POST.get('role') |
|
496 | 496 | AuthTokenModel().create(c.user.user_id, description, lifetime, role) |
|
497 | 497 | Session().commit() |
|
498 | 498 | h.flash(_("Auth token successfully created"), category='success') |
|
499 | 499 | return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id)) |
|
500 | 500 | |
|
501 | 501 | @HasPermissionAllDecorator('hg.admin') |
|
502 | 502 | @auth.CSRFRequired() |
|
503 | 503 | def delete_auth_token(self, user_id): |
|
504 | 504 | user_id = safe_int(user_id) |
|
505 | 505 | c.user = User.get_or_404(user_id) |
|
506 | 506 | if c.user.username == User.DEFAULT_USER: |
|
507 | 507 | h.flash(_("You can't edit this user"), category='warning') |
|
508 | 508 | return redirect(url('users')) |
|
509 | 509 | |
|
510 | 510 | auth_token = request.POST.get('del_auth_token') |
|
511 | 511 | if request.POST.get('del_auth_token_builtin'): |
|
512 | 512 | user = User.get(c.user.user_id) |
|
513 | 513 | if user: |
|
514 | 514 | user.api_key = generate_auth_token(user.username) |
|
515 | 515 | Session().add(user) |
|
516 | 516 | Session().commit() |
|
517 | 517 | h.flash(_("Auth token successfully reset"), category='success') |
|
518 | 518 | elif auth_token: |
|
519 | 519 | AuthTokenModel().delete(auth_token, c.user.user_id) |
|
520 | 520 | Session().commit() |
|
521 | 521 | h.flash(_("Auth token successfully deleted"), category='success') |
|
522 | 522 | |
|
523 | 523 | return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id)) |
|
524 | 524 | |
|
525 | 525 | @HasPermissionAllDecorator('hg.admin') |
|
526 | 526 | def edit_global_perms(self, user_id): |
|
527 | 527 | user_id = safe_int(user_id) |
|
528 | 528 | c.user = User.get_or_404(user_id) |
|
529 | 529 | if c.user.username == User.DEFAULT_USER: |
|
530 | 530 | h.flash(_("You can't edit this user"), category='warning') |
|
531 | 531 | return redirect(url('users')) |
|
532 | 532 | |
|
533 | 533 | c.active = 'global_perms' |
|
534 | 534 | |
|
535 | 535 | c.default_user = User.get_default_user() |
|
536 | 536 | defaults = c.user.get_dict() |
|
537 | 537 | defaults.update(c.default_user.get_default_perms(suffix='_inherited')) |
|
538 | 538 | defaults.update(c.default_user.get_default_perms()) |
|
539 | 539 | defaults.update(c.user.get_default_perms()) |
|
540 | 540 | |
|
541 | 541 | return htmlfill.render( |
|
542 |
render('admin/users/user_edit. |
|
|
542 | render('admin/users/user_edit.mako'), | |
|
543 | 543 | defaults=defaults, |
|
544 | 544 | encoding="UTF-8", |
|
545 | 545 | force_defaults=False) |
|
546 | 546 | |
|
547 | 547 | @HasPermissionAllDecorator('hg.admin') |
|
548 | 548 | @auth.CSRFRequired() |
|
549 | 549 | def update_global_perms(self, user_id): |
|
550 | 550 | """PUT /users_perm/user_id: Update an existing item""" |
|
551 | 551 | # url('user_perm', user_id=ID, method='put') |
|
552 | 552 | user_id = safe_int(user_id) |
|
553 | 553 | user = User.get_or_404(user_id) |
|
554 | 554 | c.active = 'global_perms' |
|
555 | 555 | try: |
|
556 | 556 | # first stage that verifies the checkbox |
|
557 | 557 | _form = UserIndividualPermissionsForm() |
|
558 | 558 | form_result = _form.to_python(dict(request.POST)) |
|
559 | 559 | inherit_perms = form_result['inherit_default_permissions'] |
|
560 | 560 | user.inherit_default_permissions = inherit_perms |
|
561 | 561 | Session().add(user) |
|
562 | 562 | |
|
563 | 563 | if not inherit_perms: |
|
564 | 564 | # only update the individual ones if we un check the flag |
|
565 | 565 | _form = UserPermissionsForm( |
|
566 | 566 | [x[0] for x in c.repo_create_choices], |
|
567 | 567 | [x[0] for x in c.repo_create_on_write_choices], |
|
568 | 568 | [x[0] for x in c.repo_group_create_choices], |
|
569 | 569 | [x[0] for x in c.user_group_create_choices], |
|
570 | 570 | [x[0] for x in c.fork_choices], |
|
571 | 571 | [x[0] for x in c.inherit_default_permission_choices])() |
|
572 | 572 | |
|
573 | 573 | form_result = _form.to_python(dict(request.POST)) |
|
574 | 574 | form_result.update({'perm_user_id': user.user_id}) |
|
575 | 575 | |
|
576 | 576 | PermissionModel().update_user_permissions(form_result) |
|
577 | 577 | |
|
578 | 578 | Session().commit() |
|
579 | 579 | h.flash(_('User global permissions updated successfully'), |
|
580 | 580 | category='success') |
|
581 | 581 | |
|
582 | 582 | Session().commit() |
|
583 | 583 | except formencode.Invalid as errors: |
|
584 | 584 | defaults = errors.value |
|
585 | 585 | c.user = user |
|
586 | 586 | return htmlfill.render( |
|
587 |
render('admin/users/user_edit. |
|
|
587 | render('admin/users/user_edit.mako'), | |
|
588 | 588 | defaults=defaults, |
|
589 | 589 | errors=errors.error_dict or {}, |
|
590 | 590 | prefix_error=False, |
|
591 | 591 | encoding="UTF-8", |
|
592 | 592 | force_defaults=False) |
|
593 | 593 | except Exception: |
|
594 | 594 | log.exception("Exception during permissions saving") |
|
595 | 595 | h.flash(_('An error occurred during permissions saving'), |
|
596 | 596 | category='error') |
|
597 | 597 | return redirect(url('edit_user_global_perms', user_id=user_id)) |
|
598 | 598 | |
|
599 | 599 | @HasPermissionAllDecorator('hg.admin') |
|
600 | 600 | def edit_perms_summary(self, user_id): |
|
601 | 601 | user_id = safe_int(user_id) |
|
602 | 602 | c.user = User.get_or_404(user_id) |
|
603 | 603 | if c.user.username == User.DEFAULT_USER: |
|
604 | 604 | h.flash(_("You can't edit this user"), category='warning') |
|
605 | 605 | return redirect(url('users')) |
|
606 | 606 | |
|
607 | 607 | c.active = 'perms_summary' |
|
608 | 608 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
609 | 609 | |
|
610 |
return render('admin/users/user_edit. |
|
|
610 | return render('admin/users/user_edit.mako') | |
|
611 | 611 | |
|
612 | 612 | @HasPermissionAllDecorator('hg.admin') |
|
613 | 613 | def edit_emails(self, user_id): |
|
614 | 614 | user_id = safe_int(user_id) |
|
615 | 615 | c.user = User.get_or_404(user_id) |
|
616 | 616 | if c.user.username == User.DEFAULT_USER: |
|
617 | 617 | h.flash(_("You can't edit this user"), category='warning') |
|
618 | 618 | return redirect(url('users')) |
|
619 | 619 | |
|
620 | 620 | c.active = 'emails' |
|
621 | 621 | c.user_email_map = UserEmailMap.query() \ |
|
622 | 622 | .filter(UserEmailMap.user == c.user).all() |
|
623 | 623 | |
|
624 | 624 | defaults = c.user.get_dict() |
|
625 | 625 | return htmlfill.render( |
|
626 |
render('admin/users/user_edit. |
|
|
626 | render('admin/users/user_edit.mako'), | |
|
627 | 627 | defaults=defaults, |
|
628 | 628 | encoding="UTF-8", |
|
629 | 629 | force_defaults=False) |
|
630 | 630 | |
|
631 | 631 | @HasPermissionAllDecorator('hg.admin') |
|
632 | 632 | @auth.CSRFRequired() |
|
633 | 633 | def add_email(self, user_id): |
|
634 | 634 | """POST /user_emails:Add an existing item""" |
|
635 | 635 | # url('user_emails', user_id=ID, method='put') |
|
636 | 636 | user_id = safe_int(user_id) |
|
637 | 637 | c.user = User.get_or_404(user_id) |
|
638 | 638 | |
|
639 | 639 | email = request.POST.get('new_email') |
|
640 | 640 | user_model = UserModel() |
|
641 | 641 | |
|
642 | 642 | try: |
|
643 | 643 | user_model.add_extra_email(user_id, email) |
|
644 | 644 | Session().commit() |
|
645 | 645 | h.flash(_("Added new email address `%s` for user account") % email, |
|
646 | 646 | category='success') |
|
647 | 647 | except formencode.Invalid as error: |
|
648 | 648 | msg = error.error_dict['email'] |
|
649 | 649 | h.flash(msg, category='error') |
|
650 | 650 | except Exception: |
|
651 | 651 | log.exception("Exception during email saving") |
|
652 | 652 | h.flash(_('An error occurred during email saving'), |
|
653 | 653 | category='error') |
|
654 | 654 | return redirect(url('edit_user_emails', user_id=user_id)) |
|
655 | 655 | |
|
656 | 656 | @HasPermissionAllDecorator('hg.admin') |
|
657 | 657 | @auth.CSRFRequired() |
|
658 | 658 | def delete_email(self, user_id): |
|
659 | 659 | """DELETE /user_emails_delete/user_id: Delete an existing item""" |
|
660 | 660 | # url('user_emails_delete', user_id=ID, method='delete') |
|
661 | 661 | user_id = safe_int(user_id) |
|
662 | 662 | c.user = User.get_or_404(user_id) |
|
663 | 663 | email_id = request.POST.get('del_email_id') |
|
664 | 664 | user_model = UserModel() |
|
665 | 665 | user_model.delete_extra_email(user_id, email_id) |
|
666 | 666 | Session().commit() |
|
667 | 667 | h.flash(_("Removed email address from user account"), category='success') |
|
668 | 668 | return redirect(url('edit_user_emails', user_id=user_id)) |
|
669 | 669 | |
|
670 | 670 | @HasPermissionAllDecorator('hg.admin') |
|
671 | 671 | def edit_ips(self, user_id): |
|
672 | 672 | user_id = safe_int(user_id) |
|
673 | 673 | c.user = User.get_or_404(user_id) |
|
674 | 674 | if c.user.username == User.DEFAULT_USER: |
|
675 | 675 | h.flash(_("You can't edit this user"), category='warning') |
|
676 | 676 | return redirect(url('users')) |
|
677 | 677 | |
|
678 | 678 | c.active = 'ips' |
|
679 | 679 | c.user_ip_map = UserIpMap.query() \ |
|
680 | 680 | .filter(UserIpMap.user == c.user).all() |
|
681 | 681 | |
|
682 | 682 | c.inherit_default_ips = c.user.inherit_default_permissions |
|
683 | 683 | c.default_user_ip_map = UserIpMap.query() \ |
|
684 | 684 | .filter(UserIpMap.user == User.get_default_user()).all() |
|
685 | 685 | |
|
686 | 686 | defaults = c.user.get_dict() |
|
687 | 687 | return htmlfill.render( |
|
688 |
render('admin/users/user_edit. |
|
|
688 | render('admin/users/user_edit.mako'), | |
|
689 | 689 | defaults=defaults, |
|
690 | 690 | encoding="UTF-8", |
|
691 | 691 | force_defaults=False) |
|
692 | 692 | |
|
693 | 693 | @HasPermissionAllDecorator('hg.admin') |
|
694 | 694 | @auth.CSRFRequired() |
|
695 | 695 | def add_ip(self, user_id): |
|
696 | 696 | """POST /user_ips:Add an existing item""" |
|
697 | 697 | # url('user_ips', user_id=ID, method='put') |
|
698 | 698 | |
|
699 | 699 | user_id = safe_int(user_id) |
|
700 | 700 | c.user = User.get_or_404(user_id) |
|
701 | 701 | user_model = UserModel() |
|
702 | 702 | try: |
|
703 | 703 | ip_list = user_model.parse_ip_range(request.POST.get('new_ip')) |
|
704 | 704 | except Exception as e: |
|
705 | 705 | ip_list = [] |
|
706 | 706 | log.exception("Exception during ip saving") |
|
707 | 707 | h.flash(_('An error occurred during ip saving:%s' % (e,)), |
|
708 | 708 | category='error') |
|
709 | 709 | |
|
710 | 710 | desc = request.POST.get('description') |
|
711 | 711 | added = [] |
|
712 | 712 | for ip in ip_list: |
|
713 | 713 | try: |
|
714 | 714 | user_model.add_extra_ip(user_id, ip, desc) |
|
715 | 715 | Session().commit() |
|
716 | 716 | added.append(ip) |
|
717 | 717 | except formencode.Invalid as error: |
|
718 | 718 | msg = error.error_dict['ip'] |
|
719 | 719 | h.flash(msg, category='error') |
|
720 | 720 | except Exception: |
|
721 | 721 | log.exception("Exception during ip saving") |
|
722 | 722 | h.flash(_('An error occurred during ip saving'), |
|
723 | 723 | category='error') |
|
724 | 724 | if added: |
|
725 | 725 | h.flash( |
|
726 | 726 | _("Added ips %s to user whitelist") % (', '.join(ip_list), ), |
|
727 | 727 | category='success') |
|
728 | 728 | if 'default_user' in request.POST: |
|
729 | 729 | return redirect(url('admin_permissions_ips')) |
|
730 | 730 | return redirect(url('edit_user_ips', user_id=user_id)) |
|
731 | 731 | |
|
732 | 732 | @HasPermissionAllDecorator('hg.admin') |
|
733 | 733 | @auth.CSRFRequired() |
|
734 | 734 | def delete_ip(self, user_id): |
|
735 | 735 | """DELETE /user_ips_delete/user_id: Delete an existing item""" |
|
736 | 736 | # url('user_ips_delete', user_id=ID, method='delete') |
|
737 | 737 | user_id = safe_int(user_id) |
|
738 | 738 | c.user = User.get_or_404(user_id) |
|
739 | 739 | |
|
740 | 740 | ip_id = request.POST.get('del_ip_id') |
|
741 | 741 | user_model = UserModel() |
|
742 | 742 | user_model.delete_extra_ip(user_id, ip_id) |
|
743 | 743 | Session().commit() |
|
744 | 744 | h.flash(_("Removed ip address from user whitelist"), category='success') |
|
745 | 745 | |
|
746 | 746 | if 'default_user' in request.POST: |
|
747 | 747 | return redirect(url('admin_permissions_ips')) |
|
748 | 748 | return redirect(url('edit_user_ips', user_id=user_id)) |
@@ -1,46 +1,46 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-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 | Bookmarks controller for rhodecode |
|
22 | 22 | """ |
|
23 | 23 | |
|
24 | 24 | import logging |
|
25 | 25 | |
|
26 | 26 | from pylons import tmpl_context as c |
|
27 | 27 | from webob.exc import HTTPNotFound |
|
28 | 28 | |
|
29 | 29 | from rhodecode.controllers.base_references import BaseReferencesController |
|
30 | 30 | from rhodecode.lib import helpers as h |
|
31 | 31 | |
|
32 | 32 | log = logging.getLogger(__name__) |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | class BookmarksController(BaseReferencesController): |
|
36 | 36 | |
|
37 |
partials_template = 'bookmarks/bookmarks_data. |
|
|
38 |
template = 'bookmarks/bookmarks. |
|
|
37 | partials_template = 'bookmarks/bookmarks_data.mako' | |
|
38 | template = 'bookmarks/bookmarks.mako' | |
|
39 | 39 | |
|
40 | 40 | def __before__(self): |
|
41 | 41 | super(BookmarksController, self).__before__() |
|
42 | 42 | if not h.is_hg(c.rhodecode_repo): |
|
43 | 43 | raise HTTPNotFound() |
|
44 | 44 | |
|
45 | 45 | def _get_reference_items(self, repo): |
|
46 | 46 | return repo.bookmarks.items() |
@@ -1,45 +1,45 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 | branches controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from pylons import tmpl_context as c |
|
28 | 28 | |
|
29 | 29 | from rhodecode.controllers.base_references import BaseReferencesController |
|
30 | 30 | |
|
31 | 31 | |
|
32 | 32 | log = logging.getLogger(__name__) |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | class BranchesController(BaseReferencesController): |
|
36 | 36 | |
|
37 |
partials_template = 'branches/branches_data. |
|
|
38 |
template = 'branches/branches. |
|
|
37 | partials_template = 'branches/branches_data.mako' | |
|
38 | template = 'branches/branches.mako' | |
|
39 | 39 | |
|
40 | 40 | def __before__(self): |
|
41 | 41 | super(BranchesController, self).__before__() |
|
42 | 42 | c.closed_branches = c.rhodecode_repo.branches_closed |
|
43 | 43 | |
|
44 | 44 | def _get_reference_items(self, repo): |
|
45 | 45 | return repo.branches_all.items() |
@@ -1,222 +1,222 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 | changelog controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from pylons import request, url, session, tmpl_context as c |
|
28 | 28 | from pylons.controllers.util import redirect |
|
29 | 29 | from pylons.i18n.translation import _ |
|
30 | 30 | from webob.exc import HTTPNotFound, HTTPBadRequest |
|
31 | 31 | |
|
32 | 32 | import rhodecode.lib.helpers as h |
|
33 | 33 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
34 | 34 | from rhodecode.lib.base import BaseRepoController, render |
|
35 | 35 | from rhodecode.lib.ext_json import json |
|
36 | 36 | from rhodecode.lib.graphmod import _colored, _dagwalker |
|
37 | 37 | from rhodecode.lib.helpers import RepoPage |
|
38 | 38 | from rhodecode.lib.utils2 import safe_int, safe_str |
|
39 | 39 | from rhodecode.lib.vcs.exceptions import ( |
|
40 | 40 | RepositoryError, CommitDoesNotExistError, |
|
41 | 41 | CommitError, NodeDoesNotExistError, EmptyRepositoryError) |
|
42 | 42 | |
|
43 | 43 | log = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | DEFAULT_CHANGELOG_SIZE = 20 |
|
46 | 46 | |
|
47 | 47 | |
|
48 | 48 | def _load_changelog_summary(): |
|
49 | 49 | p = safe_int(request.GET.get('page'), 1) |
|
50 | 50 | size = safe_int(request.GET.get('size'), 10) |
|
51 | 51 | |
|
52 | 52 | def url_generator(**kw): |
|
53 | 53 | return url('summary_home', |
|
54 | 54 | repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw) |
|
55 | 55 | |
|
56 | 56 | pre_load = ['author', 'branch', 'date', 'message'] |
|
57 | 57 | try: |
|
58 | 58 | collection = c.rhodecode_repo.get_commits(pre_load=pre_load) |
|
59 | 59 | except EmptyRepositoryError: |
|
60 | 60 | collection = c.rhodecode_repo |
|
61 | 61 | |
|
62 | 62 | c.repo_commits = RepoPage( |
|
63 | 63 | collection, page=p, items_per_page=size, url=url_generator) |
|
64 | 64 | page_ids = [x.raw_id for x in c.repo_commits] |
|
65 | 65 | c.comments = c.rhodecode_db_repo.get_comments(page_ids) |
|
66 | 66 | c.statuses = c.rhodecode_db_repo.statuses(page_ids) |
|
67 | 67 | |
|
68 | 68 | |
|
69 | 69 | class ChangelogController(BaseRepoController): |
|
70 | 70 | |
|
71 | 71 | def __before__(self): |
|
72 | 72 | super(ChangelogController, self).__before__() |
|
73 | 73 | c.affected_files_cut_off = 60 |
|
74 | 74 | |
|
75 | 75 | def __get_commit_or_redirect( |
|
76 | 76 | self, commit_id, repo, redirect_after=True, partial=False): |
|
77 | 77 | """ |
|
78 | 78 | This is a safe way to get a commit. If an error occurs it |
|
79 | 79 | redirects to a commit with a proper message. If partial is set |
|
80 | 80 | then it does not do redirect raise and throws an exception instead. |
|
81 | 81 | |
|
82 | 82 | :param commit_id: commit to fetch |
|
83 | 83 | :param repo: repo instance |
|
84 | 84 | """ |
|
85 | 85 | try: |
|
86 | 86 | return c.rhodecode_repo.get_commit(commit_id) |
|
87 | 87 | except EmptyRepositoryError: |
|
88 | 88 | if not redirect_after: |
|
89 | 89 | return None |
|
90 | 90 | h.flash(h.literal(_('There are no commits yet')), |
|
91 | 91 | category='warning') |
|
92 | 92 | redirect(url('changelog_home', repo_name=repo.repo_name)) |
|
93 | 93 | except RepositoryError as e: |
|
94 | 94 | msg = safe_str(e) |
|
95 | 95 | log.exception(msg) |
|
96 | 96 | h.flash(msg, category='warning') |
|
97 | 97 | if not partial: |
|
98 | 98 | redirect(h.url('changelog_home', repo_name=repo.repo_name)) |
|
99 | 99 | raise HTTPBadRequest() |
|
100 | 100 | |
|
101 | 101 | def _graph(self, repo, commits): |
|
102 | 102 | """ |
|
103 | 103 | Generates a DAG graph for repo |
|
104 | 104 | |
|
105 | 105 | :param repo: repo instance |
|
106 | 106 | :param commits: list of commits |
|
107 | 107 | """ |
|
108 | 108 | if not commits: |
|
109 | 109 | c.jsdata = json.dumps([]) |
|
110 | 110 | return |
|
111 | 111 | |
|
112 | 112 | dag = _dagwalker(repo, commits) |
|
113 | 113 | data = [['', vtx, edges] for vtx, edges in _colored(dag)] |
|
114 | 114 | c.jsdata = json.dumps(data) |
|
115 | 115 | |
|
116 | 116 | def _check_if_valid_branch(self, branch_name, repo_name, f_path): |
|
117 | 117 | if branch_name not in c.rhodecode_repo.branches_all: |
|
118 | 118 | h.flash('Branch {} is not found.'.format(branch_name), |
|
119 | 119 | category='warning') |
|
120 | 120 | redirect(url('changelog_file_home', repo_name=repo_name, |
|
121 | 121 | revision=branch_name, f_path=f_path or '')) |
|
122 | 122 | |
|
123 | 123 | @LoginRequired() |
|
124 | 124 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
125 | 125 | 'repository.admin') |
|
126 | 126 | def index(self, repo_name, revision=None, f_path=None): |
|
127 | 127 | commit_id = revision |
|
128 | 128 | limit = 100 |
|
129 | 129 | hist_limit = safe_int(request.GET.get('limit')) or None |
|
130 | 130 | if request.GET.get('size'): |
|
131 | 131 | c.size = safe_int(request.GET.get('size'), 1) |
|
132 | 132 | session['changelog_size'] = c.size |
|
133 | 133 | session.save() |
|
134 | 134 | else: |
|
135 | 135 | c.size = int(session.get('changelog_size', DEFAULT_CHANGELOG_SIZE)) |
|
136 | 136 | |
|
137 | 137 | # min size must be 1 and less than limit |
|
138 | 138 | c.size = max(c.size, 1) if c.size <= limit else limit |
|
139 | 139 | |
|
140 | 140 | p = safe_int(request.GET.get('page', 1), 1) |
|
141 | 141 | c.branch_name = branch_name = request.GET.get('branch', None) |
|
142 | 142 | c.book_name = book_name = request.GET.get('bookmark', None) |
|
143 | 143 | |
|
144 | 144 | c.selected_name = branch_name or book_name |
|
145 | 145 | if not commit_id and branch_name: |
|
146 | 146 | self._check_if_valid_branch(branch_name, repo_name, f_path) |
|
147 | 147 | |
|
148 | 148 | c.changelog_for_path = f_path |
|
149 | 149 | pre_load = ['author', 'branch', 'date', 'message', 'parents'] |
|
150 | 150 | try: |
|
151 | 151 | if f_path: |
|
152 | 152 | log.debug('generating changelog for path %s', f_path) |
|
153 | 153 | # get the history for the file ! |
|
154 | 154 | base_commit = c.rhodecode_repo.get_commit(revision) |
|
155 | 155 | try: |
|
156 | 156 | collection = base_commit.get_file_history( |
|
157 | 157 | f_path, limit=hist_limit, pre_load=pre_load) |
|
158 | 158 | if (collection |
|
159 | 159 | and request.environ.get('HTTP_X_PARTIAL_XHR')): |
|
160 | 160 | # for ajax call we remove first one since we're looking |
|
161 | 161 | # at it right now in the context of a file commit |
|
162 | 162 | collection.pop(0) |
|
163 | 163 | except (NodeDoesNotExistError, CommitError): |
|
164 | 164 | # this node is not present at tip! |
|
165 | 165 | try: |
|
166 | 166 | commit = self.__get_commit_or_redirect( |
|
167 | 167 | commit_id, repo_name) |
|
168 | 168 | collection = commit.get_file_history(f_path) |
|
169 | 169 | except RepositoryError as e: |
|
170 | 170 | h.flash(safe_str(e), category='warning') |
|
171 | 171 | redirect(h.url('changelog_home', repo_name=repo_name)) |
|
172 | 172 | collection = list(reversed(collection)) |
|
173 | 173 | else: |
|
174 | 174 | collection = c.rhodecode_repo.get_commits( |
|
175 | 175 | branch_name=branch_name, pre_load=pre_load) |
|
176 | 176 | |
|
177 | 177 | c.total_cs = len(collection) |
|
178 | 178 | c.showing_commits = min(c.size, c.total_cs) |
|
179 | 179 | c.pagination = RepoPage(collection, page=p, item_count=c.total_cs, |
|
180 | 180 | items_per_page=c.size, branch=branch_name) |
|
181 | 181 | page_commit_ids = [x.raw_id for x in c.pagination] |
|
182 | 182 | c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids) |
|
183 | 183 | c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids) |
|
184 | 184 | except EmptyRepositoryError as e: |
|
185 | 185 | h.flash(safe_str(e), category='warning') |
|
186 | 186 | return redirect(url('summary_home', repo_name=repo_name)) |
|
187 | 187 | except (RepositoryError, CommitDoesNotExistError, Exception) as e: |
|
188 | 188 | msg = safe_str(e) |
|
189 | 189 | log.exception(msg) |
|
190 | 190 | h.flash(msg, category='error') |
|
191 | 191 | return redirect(url('changelog_home', repo_name=repo_name)) |
|
192 | 192 | |
|
193 | 193 | if (request.environ.get('HTTP_X_PARTIAL_XHR') |
|
194 | 194 | or request.environ.get('HTTP_X_PJAX')): |
|
195 | 195 | # loading from ajax, we don't want the first result, it's popped |
|
196 |
return render('changelog/changelog_file_history. |
|
|
196 | return render('changelog/changelog_file_history.mako') | |
|
197 | 197 | |
|
198 | 198 | if f_path: |
|
199 | 199 | revs = [] |
|
200 | 200 | else: |
|
201 | 201 | revs = c.pagination |
|
202 | 202 | self._graph(c.rhodecode_repo, revs) |
|
203 | 203 | |
|
204 |
return render('changelog/changelog. |
|
|
204 | return render('changelog/changelog.mako') | |
|
205 | 205 | |
|
206 | 206 | @LoginRequired() |
|
207 | 207 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
208 | 208 | 'repository.admin') |
|
209 | 209 | def changelog_details(self, commit_id): |
|
210 | 210 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
211 | 211 | c.commit = c.rhodecode_repo.get_commit(commit_id=commit_id) |
|
212 |
return render('changelog/changelog_details. |
|
|
212 | return render('changelog/changelog_details.mako') | |
|
213 | 213 | raise HTTPNotFound() |
|
214 | 214 | |
|
215 | 215 | @LoginRequired() |
|
216 | 216 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
217 | 217 | 'repository.admin') |
|
218 | 218 | def changelog_summary(self, repo_name): |
|
219 | 219 | if request.environ.get('HTTP_X_PJAX'): |
|
220 | 220 | _load_changelog_summary() |
|
221 |
return render('changelog/changelog_summary_data. |
|
|
221 | return render('changelog/changelog_summary_data.mako') | |
|
222 | 222 | raise HTTPNotFound() |
@@ -1,468 +1,468 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 | commit controller for RhodeCode showing changes between commits |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from collections import defaultdict |
|
28 | 28 | from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound |
|
29 | 29 | |
|
30 | 30 | from pylons import tmpl_context as c, request, response |
|
31 | 31 | from pylons.i18n.translation import _ |
|
32 | 32 | from pylons.controllers.util import redirect |
|
33 | 33 | |
|
34 | 34 | from rhodecode.lib import auth |
|
35 | 35 | from rhodecode.lib import diffs, codeblocks |
|
36 | 36 | from rhodecode.lib.auth import ( |
|
37 | 37 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous) |
|
38 | 38 | from rhodecode.lib.base import BaseRepoController, render |
|
39 | 39 | from rhodecode.lib.compat import OrderedDict |
|
40 | 40 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError |
|
41 | 41 | import rhodecode.lib.helpers as h |
|
42 | 42 | from rhodecode.lib.utils import action_logger, jsonify |
|
43 | 43 | from rhodecode.lib.utils2 import safe_unicode |
|
44 | 44 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
45 | 45 | from rhodecode.lib.vcs.exceptions import ( |
|
46 | 46 | RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) |
|
47 | 47 | from rhodecode.model.db import ChangesetComment, ChangesetStatus |
|
48 | 48 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
49 | 49 | from rhodecode.model.comment import ChangesetCommentsModel |
|
50 | 50 | from rhodecode.model.meta import Session |
|
51 | 51 | from rhodecode.model.repo import RepoModel |
|
52 | 52 | |
|
53 | 53 | |
|
54 | 54 | log = logging.getLogger(__name__) |
|
55 | 55 | |
|
56 | 56 | |
|
57 | 57 | def _update_with_GET(params, GET): |
|
58 | 58 | for k in ['diff1', 'diff2', 'diff']: |
|
59 | 59 | params[k] += GET.getall(k) |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | def get_ignore_ws(fid, GET): |
|
63 | 63 | ig_ws_global = GET.get('ignorews') |
|
64 | 64 | ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid)) |
|
65 | 65 | if ig_ws: |
|
66 | 66 | try: |
|
67 | 67 | return int(ig_ws[0].split(':')[-1]) |
|
68 | 68 | except Exception: |
|
69 | 69 | pass |
|
70 | 70 | return ig_ws_global |
|
71 | 71 | |
|
72 | 72 | |
|
73 | 73 | def _ignorews_url(GET, fileid=None): |
|
74 | 74 | fileid = str(fileid) if fileid else None |
|
75 | 75 | params = defaultdict(list) |
|
76 | 76 | _update_with_GET(params, GET) |
|
77 | 77 | label = _('Show whitespace') |
|
78 | 78 | tooltiplbl = _('Show whitespace for all diffs') |
|
79 | 79 | ig_ws = get_ignore_ws(fileid, GET) |
|
80 | 80 | ln_ctx = get_line_ctx(fileid, GET) |
|
81 | 81 | |
|
82 | 82 | if ig_ws is None: |
|
83 | 83 | params['ignorews'] += [1] |
|
84 | 84 | label = _('Ignore whitespace') |
|
85 | 85 | tooltiplbl = _('Ignore whitespace for all diffs') |
|
86 | 86 | ctx_key = 'context' |
|
87 | 87 | ctx_val = ln_ctx |
|
88 | 88 | |
|
89 | 89 | # if we have passed in ln_ctx pass it along to our params |
|
90 | 90 | if ln_ctx: |
|
91 | 91 | params[ctx_key] += [ctx_val] |
|
92 | 92 | |
|
93 | 93 | if fileid: |
|
94 | 94 | params['anchor'] = 'a_' + fileid |
|
95 | 95 | return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip') |
|
96 | 96 | |
|
97 | 97 | |
|
98 | 98 | def get_line_ctx(fid, GET): |
|
99 | 99 | ln_ctx_global = GET.get('context') |
|
100 | 100 | if fid: |
|
101 | 101 | ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) |
|
102 | 102 | else: |
|
103 | 103 | _ln_ctx = filter(lambda k: k.startswith('C'), GET) |
|
104 | 104 | ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global |
|
105 | 105 | if ln_ctx: |
|
106 | 106 | ln_ctx = [ln_ctx] |
|
107 | 107 | |
|
108 | 108 | if ln_ctx: |
|
109 | 109 | retval = ln_ctx[0].split(':')[-1] |
|
110 | 110 | else: |
|
111 | 111 | retval = ln_ctx_global |
|
112 | 112 | |
|
113 | 113 | try: |
|
114 | 114 | return int(retval) |
|
115 | 115 | except Exception: |
|
116 | 116 | return 3 |
|
117 | 117 | |
|
118 | 118 | |
|
119 | 119 | def _context_url(GET, fileid=None): |
|
120 | 120 | """ |
|
121 | 121 | Generates a url for context lines. |
|
122 | 122 | |
|
123 | 123 | :param fileid: |
|
124 | 124 | """ |
|
125 | 125 | |
|
126 | 126 | fileid = str(fileid) if fileid else None |
|
127 | 127 | ig_ws = get_ignore_ws(fileid, GET) |
|
128 | 128 | ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2 |
|
129 | 129 | |
|
130 | 130 | params = defaultdict(list) |
|
131 | 131 | _update_with_GET(params, GET) |
|
132 | 132 | |
|
133 | 133 | if ln_ctx > 0: |
|
134 | 134 | params['context'] += [ln_ctx] |
|
135 | 135 | |
|
136 | 136 | if ig_ws: |
|
137 | 137 | ig_ws_key = 'ignorews' |
|
138 | 138 | ig_ws_val = 1 |
|
139 | 139 | params[ig_ws_key] += [ig_ws_val] |
|
140 | 140 | |
|
141 | 141 | lbl = _('Increase context') |
|
142 | 142 | tooltiplbl = _('Increase context for all diffs') |
|
143 | 143 | |
|
144 | 144 | if fileid: |
|
145 | 145 | params['anchor'] = 'a_' + fileid |
|
146 | 146 | return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip') |
|
147 | 147 | |
|
148 | 148 | |
|
149 | 149 | class ChangesetController(BaseRepoController): |
|
150 | 150 | |
|
151 | 151 | def __before__(self): |
|
152 | 152 | super(ChangesetController, self).__before__() |
|
153 | 153 | c.affected_files_cut_off = 60 |
|
154 | 154 | |
|
155 | 155 | def _index(self, commit_id_range, method): |
|
156 | 156 | c.ignorews_url = _ignorews_url |
|
157 | 157 | c.context_url = _context_url |
|
158 | 158 | c.fulldiff = fulldiff = request.GET.get('fulldiff') |
|
159 | 159 | |
|
160 | 160 | # fetch global flags of ignore ws or context lines |
|
161 | 161 | context_lcl = get_line_ctx('', request.GET) |
|
162 | 162 | ign_whitespace_lcl = get_ignore_ws('', request.GET) |
|
163 | 163 | |
|
164 | 164 | # diff_limit will cut off the whole diff if the limit is applied |
|
165 | 165 | # otherwise it will just hide the big files from the front-end |
|
166 | 166 | diff_limit = self.cut_off_limit_diff |
|
167 | 167 | file_limit = self.cut_off_limit_file |
|
168 | 168 | |
|
169 | 169 | # get ranges of commit ids if preset |
|
170 | 170 | commit_range = commit_id_range.split('...')[:2] |
|
171 | 171 | |
|
172 | 172 | try: |
|
173 | 173 | pre_load = ['affected_files', 'author', 'branch', 'date', |
|
174 | 174 | 'message', 'parents'] |
|
175 | 175 | |
|
176 | 176 | if len(commit_range) == 2: |
|
177 | 177 | commits = c.rhodecode_repo.get_commits( |
|
178 | 178 | start_id=commit_range[0], end_id=commit_range[1], |
|
179 | 179 | pre_load=pre_load) |
|
180 | 180 | commits = list(commits) |
|
181 | 181 | else: |
|
182 | 182 | commits = [c.rhodecode_repo.get_commit( |
|
183 | 183 | commit_id=commit_id_range, pre_load=pre_load)] |
|
184 | 184 | |
|
185 | 185 | c.commit_ranges = commits |
|
186 | 186 | if not c.commit_ranges: |
|
187 | 187 | raise RepositoryError( |
|
188 | 188 | 'The commit range returned an empty result') |
|
189 | 189 | except CommitDoesNotExistError: |
|
190 | 190 | msg = _('No such commit exists for this repository') |
|
191 | 191 | h.flash(msg, category='error') |
|
192 | 192 | raise HTTPNotFound() |
|
193 | 193 | except Exception: |
|
194 | 194 | log.exception("General failure") |
|
195 | 195 | raise HTTPNotFound() |
|
196 | 196 | |
|
197 | 197 | c.changes = OrderedDict() |
|
198 | 198 | c.lines_added = 0 |
|
199 | 199 | c.lines_deleted = 0 |
|
200 | 200 | |
|
201 | 201 | # auto collapse if we have more than limit |
|
202 | 202 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
203 | 203 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
204 | 204 | |
|
205 | 205 | c.commit_statuses = ChangesetStatus.STATUSES |
|
206 | 206 | c.inline_comments = [] |
|
207 | 207 | c.files = [] |
|
208 | 208 | |
|
209 | 209 | c.statuses = [] |
|
210 | 210 | c.comments = [] |
|
211 | 211 | if len(c.commit_ranges) == 1: |
|
212 | 212 | commit = c.commit_ranges[0] |
|
213 | 213 | c.comments = ChangesetCommentsModel().get_comments( |
|
214 | 214 | c.rhodecode_db_repo.repo_id, |
|
215 | 215 | revision=commit.raw_id) |
|
216 | 216 | c.statuses.append(ChangesetStatusModel().get_status( |
|
217 | 217 | c.rhodecode_db_repo.repo_id, commit.raw_id)) |
|
218 | 218 | # comments from PR |
|
219 | 219 | statuses = ChangesetStatusModel().get_statuses( |
|
220 | 220 | c.rhodecode_db_repo.repo_id, commit.raw_id, |
|
221 | 221 | with_revisions=True) |
|
222 | 222 | prs = set(st.pull_request for st in statuses |
|
223 | 223 | if st.pull_request is not None) |
|
224 | 224 | # from associated statuses, check the pull requests, and |
|
225 | 225 | # show comments from them |
|
226 | 226 | for pr in prs: |
|
227 | 227 | c.comments.extend(pr.comments) |
|
228 | 228 | |
|
229 | 229 | # Iterate over ranges (default commit view is always one commit) |
|
230 | 230 | for commit in c.commit_ranges: |
|
231 | 231 | c.changes[commit.raw_id] = [] |
|
232 | 232 | |
|
233 | 233 | commit2 = commit |
|
234 | 234 | commit1 = commit.parents[0] if commit.parents else EmptyCommit() |
|
235 | 235 | |
|
236 | 236 | _diff = c.rhodecode_repo.get_diff( |
|
237 | 237 | commit1, commit2, |
|
238 | 238 | ignore_whitespace=ign_whitespace_lcl, context=context_lcl) |
|
239 | 239 | diff_processor = diffs.DiffProcessor( |
|
240 | 240 | _diff, format='newdiff', diff_limit=diff_limit, |
|
241 | 241 | file_limit=file_limit, show_full_diff=fulldiff) |
|
242 | 242 | |
|
243 | 243 | commit_changes = OrderedDict() |
|
244 | 244 | if method == 'show': |
|
245 | 245 | _parsed = diff_processor.prepare() |
|
246 | 246 | c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer) |
|
247 | 247 | |
|
248 | 248 | _parsed = diff_processor.prepare() |
|
249 | 249 | |
|
250 | 250 | def _node_getter(commit): |
|
251 | 251 | def get_node(fname): |
|
252 | 252 | try: |
|
253 | 253 | return commit.get_node(fname) |
|
254 | 254 | except NodeDoesNotExistError: |
|
255 | 255 | return None |
|
256 | 256 | return get_node |
|
257 | 257 | |
|
258 | 258 | inline_comments = ChangesetCommentsModel().get_inline_comments( |
|
259 | 259 | c.rhodecode_db_repo.repo_id, revision=commit.raw_id) |
|
260 | 260 | c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count( |
|
261 | 261 | inline_comments) |
|
262 | 262 | |
|
263 | 263 | diffset = codeblocks.DiffSet( |
|
264 | 264 | repo_name=c.repo_name, |
|
265 | 265 | source_node_getter=_node_getter(commit1), |
|
266 | 266 | target_node_getter=_node_getter(commit2), |
|
267 | 267 | comments=inline_comments |
|
268 | 268 | ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id) |
|
269 | 269 | c.changes[commit.raw_id] = diffset |
|
270 | 270 | else: |
|
271 | 271 | # downloads/raw we only need RAW diff nothing else |
|
272 | 272 | diff = diff_processor.as_raw() |
|
273 | 273 | c.changes[commit.raw_id] = [None, None, None, None, diff, None, None] |
|
274 | 274 | |
|
275 | 275 | # sort comments by how they were generated |
|
276 | 276 | c.comments = sorted(c.comments, key=lambda x: x.comment_id) |
|
277 | 277 | |
|
278 | 278 | |
|
279 | 279 | if len(c.commit_ranges) == 1: |
|
280 | 280 | c.commit = c.commit_ranges[0] |
|
281 | 281 | c.parent_tmpl = ''.join( |
|
282 | 282 | '# Parent %s\n' % x.raw_id for x in c.commit.parents) |
|
283 | 283 | if method == 'download': |
|
284 | 284 | response.content_type = 'text/plain' |
|
285 | 285 | response.content_disposition = ( |
|
286 | 286 | 'attachment; filename=%s.diff' % commit_id_range[:12]) |
|
287 | 287 | return diff |
|
288 | 288 | elif method == 'patch': |
|
289 | 289 | response.content_type = 'text/plain' |
|
290 | 290 | c.diff = safe_unicode(diff) |
|
291 |
return render('changeset/patch_changeset. |
|
|
291 | return render('changeset/patch_changeset.mako') | |
|
292 | 292 | elif method == 'raw': |
|
293 | 293 | response.content_type = 'text/plain' |
|
294 | 294 | return diff |
|
295 | 295 | elif method == 'show': |
|
296 | 296 | if len(c.commit_ranges) == 1: |
|
297 |
return render('changeset/changeset. |
|
|
297 | return render('changeset/changeset.mako') | |
|
298 | 298 | else: |
|
299 | 299 | c.ancestor = None |
|
300 | 300 | c.target_repo = c.rhodecode_db_repo |
|
301 |
return render('changeset/changeset_range. |
|
|
301 | return render('changeset/changeset_range.mako') | |
|
302 | 302 | |
|
303 | 303 | @LoginRequired() |
|
304 | 304 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
305 | 305 | 'repository.admin') |
|
306 | 306 | def index(self, revision, method='show'): |
|
307 | 307 | return self._index(revision, method=method) |
|
308 | 308 | |
|
309 | 309 | @LoginRequired() |
|
310 | 310 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
311 | 311 | 'repository.admin') |
|
312 | 312 | def changeset_raw(self, revision): |
|
313 | 313 | return self._index(revision, method='raw') |
|
314 | 314 | |
|
315 | 315 | @LoginRequired() |
|
316 | 316 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
317 | 317 | 'repository.admin') |
|
318 | 318 | def changeset_patch(self, revision): |
|
319 | 319 | return self._index(revision, method='patch') |
|
320 | 320 | |
|
321 | 321 | @LoginRequired() |
|
322 | 322 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
323 | 323 | 'repository.admin') |
|
324 | 324 | def changeset_download(self, revision): |
|
325 | 325 | return self._index(revision, method='download') |
|
326 | 326 | |
|
327 | 327 | @LoginRequired() |
|
328 | 328 | @NotAnonymous() |
|
329 | 329 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
330 | 330 | 'repository.admin') |
|
331 | 331 | @auth.CSRFRequired() |
|
332 | 332 | @jsonify |
|
333 | 333 | def comment(self, repo_name, revision): |
|
334 | 334 | commit_id = revision |
|
335 | 335 | status = request.POST.get('changeset_status', None) |
|
336 | 336 | text = request.POST.get('text') |
|
337 | 337 | if status: |
|
338 | 338 | text = text or (_('Status change %(transition_icon)s %(status)s') |
|
339 | 339 | % {'transition_icon': '>', |
|
340 | 340 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
341 | 341 | |
|
342 | 342 | multi_commit_ids = filter( |
|
343 | 343 | lambda s: s not in ['', None], |
|
344 | 344 | request.POST.get('commit_ids', '').split(','),) |
|
345 | 345 | |
|
346 | 346 | commit_ids = multi_commit_ids or [commit_id] |
|
347 | 347 | comment = None |
|
348 | 348 | for current_id in filter(None, commit_ids): |
|
349 | 349 | c.co = comment = ChangesetCommentsModel().create( |
|
350 | 350 | text=text, |
|
351 | 351 | repo=c.rhodecode_db_repo.repo_id, |
|
352 | 352 | user=c.rhodecode_user.user_id, |
|
353 | 353 | revision=current_id, |
|
354 | 354 | f_path=request.POST.get('f_path'), |
|
355 | 355 | line_no=request.POST.get('line'), |
|
356 | 356 | status_change=(ChangesetStatus.get_status_lbl(status) |
|
357 | 357 | if status else None), |
|
358 | 358 | status_change_type=status |
|
359 | 359 | ) |
|
360 | 360 | # get status if set ! |
|
361 | 361 | if status: |
|
362 | 362 | # if latest status was from pull request and it's closed |
|
363 | 363 | # disallow changing status ! |
|
364 | 364 | # dont_allow_on_closed_pull_request = True ! |
|
365 | 365 | |
|
366 | 366 | try: |
|
367 | 367 | ChangesetStatusModel().set_status( |
|
368 | 368 | c.rhodecode_db_repo.repo_id, |
|
369 | 369 | status, |
|
370 | 370 | c.rhodecode_user.user_id, |
|
371 | 371 | comment, |
|
372 | 372 | revision=current_id, |
|
373 | 373 | dont_allow_on_closed_pull_request=True |
|
374 | 374 | ) |
|
375 | 375 | except StatusChangeOnClosedPullRequestError: |
|
376 | 376 | msg = _('Changing the status of a commit associated with ' |
|
377 | 377 | 'a closed pull request is not allowed') |
|
378 | 378 | log.exception(msg) |
|
379 | 379 | h.flash(msg, category='warning') |
|
380 | 380 | return redirect(h.url( |
|
381 | 381 | 'changeset_home', repo_name=repo_name, |
|
382 | 382 | revision=current_id)) |
|
383 | 383 | |
|
384 | 384 | # finalize, commit and redirect |
|
385 | 385 | Session().commit() |
|
386 | 386 | |
|
387 | 387 | data = { |
|
388 | 388 | 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), |
|
389 | 389 | } |
|
390 | 390 | if comment: |
|
391 | 391 | data.update(comment.get_dict()) |
|
392 | 392 | data.update({'rendered_text': |
|
393 |
render('changeset/changeset_comment_block. |
|
|
393 | render('changeset/changeset_comment_block.mako')}) | |
|
394 | 394 | |
|
395 | 395 | return data |
|
396 | 396 | |
|
397 | 397 | @LoginRequired() |
|
398 | 398 | @NotAnonymous() |
|
399 | 399 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
400 | 400 | 'repository.admin') |
|
401 | 401 | @auth.CSRFRequired() |
|
402 | 402 | def preview_comment(self): |
|
403 | 403 | # Technically a CSRF token is not needed as no state changes with this |
|
404 | 404 | # call. However, as this is a POST is better to have it, so automated |
|
405 | 405 | # tools don't flag it as potential CSRF. |
|
406 | 406 | # Post is required because the payload could be bigger than the maximum |
|
407 | 407 | # allowed by GET. |
|
408 | 408 | if not request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
409 | 409 | raise HTTPBadRequest() |
|
410 | 410 | text = request.POST.get('text') |
|
411 | 411 | renderer = request.POST.get('renderer') or 'rst' |
|
412 | 412 | if text: |
|
413 | 413 | return h.render(text, renderer=renderer, mentions=True) |
|
414 | 414 | return '' |
|
415 | 415 | |
|
416 | 416 | @LoginRequired() |
|
417 | 417 | @NotAnonymous() |
|
418 | 418 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
419 | 419 | 'repository.admin') |
|
420 | 420 | @auth.CSRFRequired() |
|
421 | 421 | @jsonify |
|
422 | 422 | def delete_comment(self, repo_name, comment_id): |
|
423 | 423 | comment = ChangesetComment.get(comment_id) |
|
424 | 424 | owner = (comment.author.user_id == c.rhodecode_user.user_id) |
|
425 | 425 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) |
|
426 | 426 | if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner: |
|
427 | 427 | ChangesetCommentsModel().delete(comment=comment) |
|
428 | 428 | Session().commit() |
|
429 | 429 | return True |
|
430 | 430 | else: |
|
431 | 431 | raise HTTPForbidden() |
|
432 | 432 | |
|
433 | 433 | @LoginRequired() |
|
434 | 434 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
435 | 435 | 'repository.admin') |
|
436 | 436 | @jsonify |
|
437 | 437 | def changeset_info(self, repo_name, revision): |
|
438 | 438 | if request.is_xhr: |
|
439 | 439 | try: |
|
440 | 440 | return c.rhodecode_repo.get_commit(commit_id=revision) |
|
441 | 441 | except CommitDoesNotExistError as e: |
|
442 | 442 | return EmptyCommit(message=str(e)) |
|
443 | 443 | else: |
|
444 | 444 | raise HTTPBadRequest() |
|
445 | 445 | |
|
446 | 446 | @LoginRequired() |
|
447 | 447 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
448 | 448 | 'repository.admin') |
|
449 | 449 | @jsonify |
|
450 | 450 | def changeset_children(self, repo_name, revision): |
|
451 | 451 | if request.is_xhr: |
|
452 | 452 | commit = c.rhodecode_repo.get_commit(commit_id=revision) |
|
453 | 453 | result = {"results": commit.children} |
|
454 | 454 | return result |
|
455 | 455 | else: |
|
456 | 456 | raise HTTPBadRequest() |
|
457 | 457 | |
|
458 | 458 | @LoginRequired() |
|
459 | 459 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
460 | 460 | 'repository.admin') |
|
461 | 461 | @jsonify |
|
462 | 462 | def changeset_parents(self, repo_name, revision): |
|
463 | 463 | if request.is_xhr: |
|
464 | 464 | commit = c.rhodecode_repo.get_commit(commit_id=revision) |
|
465 | 465 | result = {"results": commit.parents} |
|
466 | 466 | return result |
|
467 | 467 | else: |
|
468 | 468 | raise HTTPBadRequest() |
@@ -1,282 +1,282 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | Compare controller for showing differences between two commits/refs/tags etc. |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from webob.exc import HTTPBadRequest |
|
28 | 28 | from pylons import request, tmpl_context as c, url |
|
29 | 29 | from pylons.controllers.util import redirect |
|
30 | 30 | from pylons.i18n.translation import _ |
|
31 | 31 | |
|
32 | 32 | from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name |
|
33 | 33 | from rhodecode.lib import helpers as h |
|
34 | 34 | from rhodecode.lib import diffs, codeblocks |
|
35 | 35 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
36 | 36 | from rhodecode.lib.base import BaseRepoController, render |
|
37 | 37 | from rhodecode.lib.utils import safe_str |
|
38 | 38 | from rhodecode.lib.utils2 import safe_unicode, str2bool |
|
39 | 39 | from rhodecode.lib.vcs.exceptions import ( |
|
40 | 40 | EmptyRepositoryError, RepositoryError, RepositoryRequirementError, |
|
41 | 41 | NodeDoesNotExistError) |
|
42 | 42 | from rhodecode.model.db import Repository, ChangesetStatus |
|
43 | 43 | |
|
44 | 44 | log = logging.getLogger(__name__) |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class CompareController(BaseRepoController): |
|
48 | 48 | |
|
49 | 49 | def __before__(self): |
|
50 | 50 | super(CompareController, self).__before__() |
|
51 | 51 | |
|
52 | 52 | def _get_commit_or_redirect( |
|
53 | 53 | self, ref, ref_type, repo, redirect_after=True, partial=False): |
|
54 | 54 | """ |
|
55 | 55 | This is a safe way to get a commit. If an error occurs it |
|
56 | 56 | redirects to a commit with a proper message. If partial is set |
|
57 | 57 | then it does not do redirect raise and throws an exception instead. |
|
58 | 58 | """ |
|
59 | 59 | try: |
|
60 | 60 | return get_commit_from_ref_name(repo, safe_str(ref), ref_type) |
|
61 | 61 | except EmptyRepositoryError: |
|
62 | 62 | if not redirect_after: |
|
63 | 63 | return repo.scm_instance().EMPTY_COMMIT |
|
64 | 64 | h.flash(h.literal(_('There are no commits yet')), |
|
65 | 65 | category='warning') |
|
66 | 66 | redirect(url('summary_home', repo_name=repo.repo_name)) |
|
67 | 67 | |
|
68 | 68 | except RepositoryError as e: |
|
69 | 69 | msg = safe_str(e) |
|
70 | 70 | log.exception(msg) |
|
71 | 71 | h.flash(msg, category='warning') |
|
72 | 72 | if not partial: |
|
73 | 73 | redirect(h.url('summary_home', repo_name=repo.repo_name)) |
|
74 | 74 | raise HTTPBadRequest() |
|
75 | 75 | |
|
76 | 76 | @LoginRequired() |
|
77 | 77 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
78 | 78 | 'repository.admin') |
|
79 | 79 | def index(self, repo_name): |
|
80 | 80 | c.compare_home = True |
|
81 | 81 | c.commit_ranges = [] |
|
82 | 82 | c.collapse_all_commits = False |
|
83 | 83 | c.diffset = None |
|
84 | 84 | c.limited_diff = False |
|
85 | 85 | source_repo = c.rhodecode_db_repo.repo_name |
|
86 | 86 | target_repo = request.GET.get('target_repo', source_repo) |
|
87 | 87 | c.source_repo = Repository.get_by_repo_name(source_repo) |
|
88 | 88 | c.target_repo = Repository.get_by_repo_name(target_repo) |
|
89 | 89 | c.source_ref = c.target_ref = _('Select commit') |
|
90 | 90 | c.source_ref_type = "" |
|
91 | 91 | c.target_ref_type = "" |
|
92 | 92 | c.commit_statuses = ChangesetStatus.STATUSES |
|
93 | 93 | c.preview_mode = False |
|
94 | 94 | c.file_path = None |
|
95 |
return render('compare/compare_diff. |
|
|
95 | return render('compare/compare_diff.mako') | |
|
96 | 96 | |
|
97 | 97 | @LoginRequired() |
|
98 | 98 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
99 | 99 | 'repository.admin') |
|
100 | 100 | def compare(self, repo_name, source_ref_type, source_ref, |
|
101 | 101 | target_ref_type, target_ref): |
|
102 | 102 | # source_ref will be evaluated in source_repo |
|
103 | 103 | source_repo_name = c.rhodecode_db_repo.repo_name |
|
104 | 104 | source_path, source_id = parse_path_ref(source_ref) |
|
105 | 105 | |
|
106 | 106 | # target_ref will be evaluated in target_repo |
|
107 | 107 | target_repo_name = request.GET.get('target_repo', source_repo_name) |
|
108 | 108 | target_path, target_id = parse_path_ref( |
|
109 | 109 | target_ref, default_path=request.GET.get('f_path', '')) |
|
110 | 110 | |
|
111 | 111 | c.file_path = target_path |
|
112 | 112 | c.commit_statuses = ChangesetStatus.STATUSES |
|
113 | 113 | |
|
114 | 114 | # if merge is True |
|
115 | 115 | # Show what changes since the shared ancestor commit of target/source |
|
116 | 116 | # the source would get if it was merged with target. Only commits |
|
117 | 117 | # which are in target but not in source will be shown. |
|
118 | 118 | merge = str2bool(request.GET.get('merge')) |
|
119 | 119 | # if merge is False |
|
120 | 120 | # Show a raw diff of source/target refs even if no ancestor exists |
|
121 | 121 | |
|
122 | 122 | # c.fulldiff disables cut_off_limit |
|
123 | 123 | c.fulldiff = str2bool(request.GET.get('fulldiff')) |
|
124 | 124 | |
|
125 | 125 | # if partial, returns just compare_commits.html (commits log) |
|
126 | 126 | partial = request.is_xhr |
|
127 | 127 | |
|
128 | 128 | # swap url for compare_diff page |
|
129 | 129 | c.swap_url = h.url( |
|
130 | 130 | 'compare_url', |
|
131 | 131 | repo_name=target_repo_name, |
|
132 | 132 | source_ref_type=target_ref_type, |
|
133 | 133 | source_ref=target_ref, |
|
134 | 134 | target_repo=source_repo_name, |
|
135 | 135 | target_ref_type=source_ref_type, |
|
136 | 136 | target_ref=source_ref, |
|
137 | 137 | merge=merge and '1' or '', |
|
138 | 138 | f_path=target_path) |
|
139 | 139 | |
|
140 | 140 | source_repo = Repository.get_by_repo_name(source_repo_name) |
|
141 | 141 | target_repo = Repository.get_by_repo_name(target_repo_name) |
|
142 | 142 | |
|
143 | 143 | if source_repo is None: |
|
144 | 144 | msg = _('Could not find the original repo: %(repo)s') % { |
|
145 | 145 | 'repo': source_repo} |
|
146 | 146 | |
|
147 | 147 | log.error(msg) |
|
148 | 148 | h.flash(msg, category='error') |
|
149 | 149 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
150 | 150 | |
|
151 | 151 | if target_repo is None: |
|
152 | 152 | msg = _('Could not find the other repo: %(repo)s') % { |
|
153 | 153 | 'repo': target_repo_name} |
|
154 | 154 | log.error(msg) |
|
155 | 155 | h.flash(msg, category='error') |
|
156 | 156 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
157 | 157 | |
|
158 | 158 | source_scm = source_repo.scm_instance() |
|
159 | 159 | target_scm = target_repo.scm_instance() |
|
160 | 160 | |
|
161 | 161 | source_alias = source_scm.alias |
|
162 | 162 | target_alias = target_scm.alias |
|
163 | 163 | if source_alias != target_alias: |
|
164 | 164 | msg = _('The comparison of two different kinds of remote repos ' |
|
165 | 165 | 'is not available') |
|
166 | 166 | log.error(msg) |
|
167 | 167 | h.flash(msg, category='error') |
|
168 | 168 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
169 | 169 | |
|
170 | 170 | source_commit = self._get_commit_or_redirect( |
|
171 | 171 | ref=source_id, ref_type=source_ref_type, repo=source_repo, |
|
172 | 172 | partial=partial) |
|
173 | 173 | target_commit = self._get_commit_or_redirect( |
|
174 | 174 | ref=target_id, ref_type=target_ref_type, repo=target_repo, |
|
175 | 175 | partial=partial) |
|
176 | 176 | |
|
177 | 177 | c.compare_home = False |
|
178 | 178 | c.source_repo = source_repo |
|
179 | 179 | c.target_repo = target_repo |
|
180 | 180 | c.source_ref = source_ref |
|
181 | 181 | c.target_ref = target_ref |
|
182 | 182 | c.source_ref_type = source_ref_type |
|
183 | 183 | c.target_ref_type = target_ref_type |
|
184 | 184 | |
|
185 | 185 | pre_load = ["author", "branch", "date", "message"] |
|
186 | 186 | c.ancestor = None |
|
187 | 187 | |
|
188 | 188 | if c.file_path: |
|
189 | 189 | if source_commit == target_commit: |
|
190 | 190 | c.commit_ranges = [] |
|
191 | 191 | else: |
|
192 | 192 | c.commit_ranges = [target_commit] |
|
193 | 193 | else: |
|
194 | 194 | try: |
|
195 | 195 | c.commit_ranges = source_scm.compare( |
|
196 | 196 | source_commit.raw_id, target_commit.raw_id, |
|
197 | 197 | target_scm, merge, pre_load=pre_load) |
|
198 | 198 | if merge: |
|
199 | 199 | c.ancestor = source_scm.get_common_ancestor( |
|
200 | 200 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
201 | 201 | except RepositoryRequirementError: |
|
202 | 202 | msg = _('Could not compare repos with different ' |
|
203 | 203 | 'large file settings') |
|
204 | 204 | log.error(msg) |
|
205 | 205 | if partial: |
|
206 | 206 | return msg |
|
207 | 207 | h.flash(msg, category='error') |
|
208 | 208 | return redirect(url('compare_home', repo_name=c.repo_name)) |
|
209 | 209 | |
|
210 | 210 | c.statuses = c.rhodecode_db_repo.statuses( |
|
211 | 211 | [x.raw_id for x in c.commit_ranges]) |
|
212 | 212 | |
|
213 | 213 | # auto collapse if we have more than limit |
|
214 | 214 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
215 | 215 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
216 | 216 | |
|
217 | 217 | if partial: # for PR ajax commits loader |
|
218 | 218 | if not c.ancestor: |
|
219 | 219 | return '' # cannot merge if there is no ancestor |
|
220 |
return render('compare/compare_commits. |
|
|
220 | return render('compare/compare_commits.mako') | |
|
221 | 221 | |
|
222 | 222 | if c.ancestor: |
|
223 | 223 | # case we want a simple diff without incoming commits, |
|
224 | 224 | # previewing what will be merged. |
|
225 | 225 | # Make the diff on target repo (which is known to have target_ref) |
|
226 | 226 | log.debug('Using ancestor %s as source_ref instead of %s' |
|
227 | 227 | % (c.ancestor, source_ref)) |
|
228 | 228 | source_repo = target_repo |
|
229 | 229 | source_commit = target_repo.get_commit(commit_id=c.ancestor) |
|
230 | 230 | |
|
231 | 231 | # diff_limit will cut off the whole diff if the limit is applied |
|
232 | 232 | # otherwise it will just hide the big files from the front-end |
|
233 | 233 | diff_limit = self.cut_off_limit_diff |
|
234 | 234 | file_limit = self.cut_off_limit_file |
|
235 | 235 | |
|
236 | 236 | log.debug('calculating diff between ' |
|
237 | 237 | 'source_ref:%s and target_ref:%s for repo `%s`', |
|
238 | 238 | source_commit, target_commit, |
|
239 | 239 | safe_unicode(source_repo.scm_instance().path)) |
|
240 | 240 | |
|
241 | 241 | if source_commit.repository != target_commit.repository: |
|
242 | 242 | msg = _( |
|
243 | 243 | "Repositories unrelated. " |
|
244 | 244 | "Cannot compare commit %(commit1)s from repository %(repo1)s " |
|
245 | 245 | "with commit %(commit2)s from repository %(repo2)s.") % { |
|
246 | 246 | 'commit1': h.show_id(source_commit), |
|
247 | 247 | 'repo1': source_repo.repo_name, |
|
248 | 248 | 'commit2': h.show_id(target_commit), |
|
249 | 249 | 'repo2': target_repo.repo_name, |
|
250 | 250 | } |
|
251 | 251 | h.flash(msg, category='error') |
|
252 | 252 | raise HTTPBadRequest() |
|
253 | 253 | |
|
254 | 254 | txtdiff = source_repo.scm_instance().get_diff( |
|
255 | 255 | commit1=source_commit, commit2=target_commit, |
|
256 | 256 | path=target_path, path1=source_path) |
|
257 | 257 | |
|
258 | 258 | diff_processor = diffs.DiffProcessor( |
|
259 | 259 | txtdiff, format='newdiff', diff_limit=diff_limit, |
|
260 | 260 | file_limit=file_limit, show_full_diff=c.fulldiff) |
|
261 | 261 | _parsed = diff_processor.prepare() |
|
262 | 262 | |
|
263 | 263 | def _node_getter(commit): |
|
264 | 264 | """ Returns a function that returns a node for a commit or None """ |
|
265 | 265 | def get_node(fname): |
|
266 | 266 | try: |
|
267 | 267 | return commit.get_node(fname) |
|
268 | 268 | except NodeDoesNotExistError: |
|
269 | 269 | return None |
|
270 | 270 | return get_node |
|
271 | 271 | |
|
272 | 272 | c.diffset = codeblocks.DiffSet( |
|
273 | 273 | repo_name=source_repo.repo_name, |
|
274 | 274 | source_node_getter=_node_getter(source_commit), |
|
275 | 275 | target_node_getter=_node_getter(target_commit), |
|
276 | 276 | ).render_patchset(_parsed, source_ref, target_ref) |
|
277 | 277 | |
|
278 | 278 | c.preview_mode = merge |
|
279 | 279 | c.source_commit = source_commit |
|
280 | 280 | c.target_commit = target_commit |
|
281 | 281 | |
|
282 |
return render('compare/compare_diff. |
|
|
282 | return render('compare/compare_diff.mako') |
@@ -1,1061 +1,1061 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 | Files controller for RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import itertools |
|
26 | 26 | import logging |
|
27 | 27 | import os |
|
28 | 28 | import shutil |
|
29 | 29 | import tempfile |
|
30 | 30 | |
|
31 | 31 | from pylons import request, response, tmpl_context as c, url |
|
32 | 32 | from pylons.i18n.translation import _ |
|
33 | 33 | from pylons.controllers.util import redirect |
|
34 | 34 | from webob.exc import HTTPNotFound, HTTPBadRequest |
|
35 | 35 | |
|
36 | 36 | from rhodecode.controllers.utils import parse_path_ref |
|
37 | 37 | from rhodecode.lib import diffs, helpers as h, caches |
|
38 | 38 | from rhodecode.lib.compat import OrderedDict |
|
39 | 39 | from rhodecode.lib.codeblocks import ( |
|
40 | 40 | filenode_as_lines_tokens, filenode_as_annotated_lines_tokens) |
|
41 | 41 | from rhodecode.lib.utils import jsonify, action_logger |
|
42 | 42 | from rhodecode.lib.utils2 import ( |
|
43 | 43 | convert_line_endings, detect_mode, safe_str, str2bool) |
|
44 | 44 | from rhodecode.lib.auth import ( |
|
45 | 45 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired) |
|
46 | 46 | from rhodecode.lib.base import BaseRepoController, render |
|
47 | 47 | from rhodecode.lib.vcs import path as vcspath |
|
48 | 48 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
49 | 49 | from rhodecode.lib.vcs.conf import settings |
|
50 | 50 | from rhodecode.lib.vcs.exceptions import ( |
|
51 | 51 | RepositoryError, CommitDoesNotExistError, EmptyRepositoryError, |
|
52 | 52 | ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, |
|
53 | 53 | NodeDoesNotExistError, CommitError, NodeError) |
|
54 | 54 | from rhodecode.lib.vcs.nodes import FileNode |
|
55 | 55 | |
|
56 | 56 | from rhodecode.model.repo import RepoModel |
|
57 | 57 | from rhodecode.model.scm import ScmModel |
|
58 | 58 | from rhodecode.model.db import Repository |
|
59 | 59 | |
|
60 | 60 | from rhodecode.controllers.changeset import ( |
|
61 | 61 | _ignorews_url, _context_url, get_line_ctx, get_ignore_ws) |
|
62 | 62 | from rhodecode.lib.exceptions import NonRelativePathError |
|
63 | 63 | |
|
64 | 64 | log = logging.getLogger(__name__) |
|
65 | 65 | |
|
66 | 66 | |
|
67 | 67 | class FilesController(BaseRepoController): |
|
68 | 68 | |
|
69 | 69 | def __before__(self): |
|
70 | 70 | super(FilesController, self).__before__() |
|
71 | 71 | c.cut_off_limit = self.cut_off_limit_file |
|
72 | 72 | |
|
73 | 73 | def _get_default_encoding(self): |
|
74 | 74 | enc_list = getattr(c, 'default_encodings', []) |
|
75 | 75 | return enc_list[0] if enc_list else 'UTF-8' |
|
76 | 76 | |
|
77 | 77 | def __get_commit_or_redirect(self, commit_id, repo_name, |
|
78 | 78 | redirect_after=True): |
|
79 | 79 | """ |
|
80 | 80 | This is a safe way to get commit. If an error occurs it redirects to |
|
81 | 81 | tip with proper message |
|
82 | 82 | |
|
83 | 83 | :param commit_id: id of commit to fetch |
|
84 | 84 | :param repo_name: repo name to redirect after |
|
85 | 85 | :param redirect_after: toggle redirection |
|
86 | 86 | """ |
|
87 | 87 | try: |
|
88 | 88 | return c.rhodecode_repo.get_commit(commit_id) |
|
89 | 89 | except EmptyRepositoryError: |
|
90 | 90 | if not redirect_after: |
|
91 | 91 | return None |
|
92 | 92 | url_ = url('files_add_home', |
|
93 | 93 | repo_name=c.repo_name, |
|
94 | 94 | revision=0, f_path='', anchor='edit') |
|
95 | 95 | if h.HasRepoPermissionAny( |
|
96 | 96 | 'repository.write', 'repository.admin')(c.repo_name): |
|
97 | 97 | add_new = h.link_to( |
|
98 | 98 | _('Click here to add a new file.'), |
|
99 | 99 | url_, class_="alert-link") |
|
100 | 100 | else: |
|
101 | 101 | add_new = "" |
|
102 | 102 | h.flash(h.literal( |
|
103 | 103 | _('There are no files yet. %s') % add_new), category='warning') |
|
104 | 104 | redirect(h.url('summary_home', repo_name=repo_name)) |
|
105 | 105 | except (CommitDoesNotExistError, LookupError): |
|
106 | 106 | msg = _('No such commit exists for this repository') |
|
107 | 107 | h.flash(msg, category='error') |
|
108 | 108 | raise HTTPNotFound() |
|
109 | 109 | except RepositoryError as e: |
|
110 | 110 | h.flash(safe_str(e), category='error') |
|
111 | 111 | raise HTTPNotFound() |
|
112 | 112 | |
|
113 | 113 | def __get_filenode_or_redirect(self, repo_name, commit, path): |
|
114 | 114 | """ |
|
115 | 115 | Returns file_node, if error occurs or given path is directory, |
|
116 | 116 | it'll redirect to top level path |
|
117 | 117 | |
|
118 | 118 | :param repo_name: repo_name |
|
119 | 119 | :param commit: given commit |
|
120 | 120 | :param path: path to lookup |
|
121 | 121 | """ |
|
122 | 122 | try: |
|
123 | 123 | file_node = commit.get_node(path) |
|
124 | 124 | if file_node.is_dir(): |
|
125 | 125 | raise RepositoryError('The given path is a directory') |
|
126 | 126 | except CommitDoesNotExistError: |
|
127 | 127 | msg = _('No such commit exists for this repository') |
|
128 | 128 | log.exception(msg) |
|
129 | 129 | h.flash(msg, category='error') |
|
130 | 130 | raise HTTPNotFound() |
|
131 | 131 | except RepositoryError as e: |
|
132 | 132 | h.flash(safe_str(e), category='error') |
|
133 | 133 | raise HTTPNotFound() |
|
134 | 134 | |
|
135 | 135 | return file_node |
|
136 | 136 | |
|
137 | 137 | def __get_tree_cache_manager(self, repo_name, namespace_type): |
|
138 | 138 | _namespace = caches.get_repo_namespace_key(namespace_type, repo_name) |
|
139 | 139 | return caches.get_cache_manager('repo_cache_long', _namespace) |
|
140 | 140 | |
|
141 | 141 | def _get_tree_at_commit(self, repo_name, commit_id, f_path, |
|
142 | 142 | full_load=False, force=False): |
|
143 | 143 | def _cached_tree(): |
|
144 | 144 | log.debug('Generating cached file tree for %s, %s, %s', |
|
145 | 145 | repo_name, commit_id, f_path) |
|
146 | 146 | c.full_load = full_load |
|
147 |
return render('files/files_browser_tree. |
|
|
147 | return render('files/files_browser_tree.mako') | |
|
148 | 148 | |
|
149 | 149 | cache_manager = self.__get_tree_cache_manager( |
|
150 | 150 | repo_name, caches.FILE_TREE) |
|
151 | 151 | |
|
152 | 152 | cache_key = caches.compute_key_from_params( |
|
153 | 153 | repo_name, commit_id, f_path) |
|
154 | 154 | |
|
155 | 155 | if force: |
|
156 | 156 | # we want to force recompute of caches |
|
157 | 157 | cache_manager.remove_value(cache_key) |
|
158 | 158 | |
|
159 | 159 | return cache_manager.get(cache_key, createfunc=_cached_tree) |
|
160 | 160 | |
|
161 | 161 | def _get_nodelist_at_commit(self, repo_name, commit_id, f_path): |
|
162 | 162 | def _cached_nodes(): |
|
163 | 163 | log.debug('Generating cached nodelist for %s, %s, %s', |
|
164 | 164 | repo_name, commit_id, f_path) |
|
165 | 165 | _d, _f = ScmModel().get_nodes( |
|
166 | 166 | repo_name, commit_id, f_path, flat=False) |
|
167 | 167 | return _d + _f |
|
168 | 168 | |
|
169 | 169 | cache_manager = self.__get_tree_cache_manager( |
|
170 | 170 | repo_name, caches.FILE_SEARCH_TREE_META) |
|
171 | 171 | |
|
172 | 172 | cache_key = caches.compute_key_from_params( |
|
173 | 173 | repo_name, commit_id, f_path) |
|
174 | 174 | return cache_manager.get(cache_key, createfunc=_cached_nodes) |
|
175 | 175 | |
|
176 | 176 | @LoginRequired() |
|
177 | 177 | @HasRepoPermissionAnyDecorator( |
|
178 | 178 | 'repository.read', 'repository.write', 'repository.admin') |
|
179 | 179 | def index( |
|
180 | 180 | self, repo_name, revision, f_path, annotate=False, rendered=False): |
|
181 | 181 | commit_id = revision |
|
182 | 182 | |
|
183 | 183 | # redirect to given commit_id from form if given |
|
184 | 184 | get_commit_id = request.GET.get('at_rev', None) |
|
185 | 185 | if get_commit_id: |
|
186 | 186 | self.__get_commit_or_redirect(get_commit_id, repo_name) |
|
187 | 187 | |
|
188 | 188 | c.commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
189 | 189 | c.branch = request.GET.get('branch', None) |
|
190 | 190 | c.f_path = f_path |
|
191 | 191 | c.annotate = annotate |
|
192 | 192 | # default is false, but .rst/.md files later are autorendered, we can |
|
193 | 193 | # overwrite autorendering by setting this GET flag |
|
194 | 194 | c.renderer = rendered or not request.GET.get('no-render', False) |
|
195 | 195 | |
|
196 | 196 | # prev link |
|
197 | 197 | try: |
|
198 | 198 | prev_commit = c.commit.prev(c.branch) |
|
199 | 199 | c.prev_commit = prev_commit |
|
200 | 200 | c.url_prev = url('files_home', repo_name=c.repo_name, |
|
201 | 201 | revision=prev_commit.raw_id, f_path=f_path) |
|
202 | 202 | if c.branch: |
|
203 | 203 | c.url_prev += '?branch=%s' % c.branch |
|
204 | 204 | except (CommitDoesNotExistError, VCSError): |
|
205 | 205 | c.url_prev = '#' |
|
206 | 206 | c.prev_commit = EmptyCommit() |
|
207 | 207 | |
|
208 | 208 | # next link |
|
209 | 209 | try: |
|
210 | 210 | next_commit = c.commit.next(c.branch) |
|
211 | 211 | c.next_commit = next_commit |
|
212 | 212 | c.url_next = url('files_home', repo_name=c.repo_name, |
|
213 | 213 | revision=next_commit.raw_id, f_path=f_path) |
|
214 | 214 | if c.branch: |
|
215 | 215 | c.url_next += '?branch=%s' % c.branch |
|
216 | 216 | except (CommitDoesNotExistError, VCSError): |
|
217 | 217 | c.url_next = '#' |
|
218 | 218 | c.next_commit = EmptyCommit() |
|
219 | 219 | |
|
220 | 220 | # files or dirs |
|
221 | 221 | try: |
|
222 | 222 | c.file = c.commit.get_node(f_path) |
|
223 | 223 | c.file_author = True |
|
224 | 224 | c.file_tree = '' |
|
225 | 225 | if c.file.is_file(): |
|
226 | 226 | c.file_source_page = 'true' |
|
227 | 227 | c.file_last_commit = c.file.last_commit |
|
228 | 228 | if c.file.size < self.cut_off_limit_file: |
|
229 | 229 | if c.annotate: # annotation has precedence over renderer |
|
230 | 230 | c.annotated_lines = filenode_as_annotated_lines_tokens( |
|
231 | 231 | c.file |
|
232 | 232 | ) |
|
233 | 233 | else: |
|
234 | 234 | c.renderer = ( |
|
235 | 235 | c.renderer and h.renderer_from_filename(c.file.path) |
|
236 | 236 | ) |
|
237 | 237 | if not c.renderer: |
|
238 | 238 | c.lines = filenode_as_lines_tokens(c.file) |
|
239 | 239 | |
|
240 | 240 | c.on_branch_head = self._is_valid_head( |
|
241 | 241 | commit_id, c.rhodecode_repo) |
|
242 | 242 | c.branch_or_raw_id = c.commit.branch or c.commit.raw_id |
|
243 | 243 | |
|
244 | 244 | author = c.file_last_commit.author |
|
245 | 245 | c.authors = [(h.email(author), |
|
246 | 246 | h.person(author, 'username_or_name_or_email'))] |
|
247 | 247 | else: |
|
248 | 248 | c.file_source_page = 'false' |
|
249 | 249 | c.authors = [] |
|
250 | 250 | c.file_tree = self._get_tree_at_commit( |
|
251 | 251 | repo_name, c.commit.raw_id, f_path) |
|
252 | 252 | |
|
253 | 253 | except RepositoryError as e: |
|
254 | 254 | h.flash(safe_str(e), category='error') |
|
255 | 255 | raise HTTPNotFound() |
|
256 | 256 | |
|
257 | 257 | if request.environ.get('HTTP_X_PJAX'): |
|
258 |
return render('files/files_pjax. |
|
|
258 | return render('files/files_pjax.mako') | |
|
259 | 259 | |
|
260 |
return render('files/files. |
|
|
260 | return render('files/files.mako') | |
|
261 | 261 | |
|
262 | 262 | @LoginRequired() |
|
263 | 263 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
264 | 264 | 'repository.admin') |
|
265 | 265 | @jsonify |
|
266 | 266 | def history(self, repo_name, revision, f_path): |
|
267 | 267 | commit = self.__get_commit_or_redirect(revision, repo_name) |
|
268 | 268 | f_path = f_path |
|
269 | 269 | _file = commit.get_node(f_path) |
|
270 | 270 | if _file.is_file(): |
|
271 | 271 | file_history, _hist = self._get_node_history(commit, f_path) |
|
272 | 272 | |
|
273 | 273 | res = [] |
|
274 | 274 | for obj in file_history: |
|
275 | 275 | res.append({ |
|
276 | 276 | 'text': obj[1], |
|
277 | 277 | 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]] |
|
278 | 278 | }) |
|
279 | 279 | |
|
280 | 280 | data = { |
|
281 | 281 | 'more': False, |
|
282 | 282 | 'results': res |
|
283 | 283 | } |
|
284 | 284 | return data |
|
285 | 285 | |
|
286 | 286 | @LoginRequired() |
|
287 | 287 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
288 | 288 | 'repository.admin') |
|
289 | 289 | def authors(self, repo_name, revision, f_path): |
|
290 | 290 | commit = self.__get_commit_or_redirect(revision, repo_name) |
|
291 | 291 | file_node = commit.get_node(f_path) |
|
292 | 292 | if file_node.is_file(): |
|
293 | 293 | c.file_last_commit = file_node.last_commit |
|
294 | 294 | if request.GET.get('annotate') == '1': |
|
295 | 295 | # use _hist from annotation if annotation mode is on |
|
296 | 296 | commit_ids = set(x[1] for x in file_node.annotate) |
|
297 | 297 | _hist = ( |
|
298 | 298 | c.rhodecode_repo.get_commit(commit_id) |
|
299 | 299 | for commit_id in commit_ids) |
|
300 | 300 | else: |
|
301 | 301 | _f_history, _hist = self._get_node_history(commit, f_path) |
|
302 | 302 | c.file_author = False |
|
303 | 303 | c.authors = [] |
|
304 | 304 | for author in set(commit.author for commit in _hist): |
|
305 | 305 | c.authors.append(( |
|
306 | 306 | h.email(author), |
|
307 | 307 | h.person(author, 'username_or_name_or_email'))) |
|
308 |
return render('files/file_authors_box. |
|
|
308 | return render('files/file_authors_box.mako') | |
|
309 | 309 | |
|
310 | 310 | @LoginRequired() |
|
311 | 311 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
312 | 312 | 'repository.admin') |
|
313 | 313 | def rawfile(self, repo_name, revision, f_path): |
|
314 | 314 | """ |
|
315 | 315 | Action for download as raw |
|
316 | 316 | """ |
|
317 | 317 | commit = self.__get_commit_or_redirect(revision, repo_name) |
|
318 | 318 | file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path) |
|
319 | 319 | |
|
320 | 320 | response.content_disposition = 'attachment; filename=%s' % \ |
|
321 | 321 | safe_str(f_path.split(Repository.NAME_SEP)[-1]) |
|
322 | 322 | |
|
323 | 323 | response.content_type = file_node.mimetype |
|
324 | 324 | charset = self._get_default_encoding() |
|
325 | 325 | if charset: |
|
326 | 326 | response.charset = charset |
|
327 | 327 | |
|
328 | 328 | return file_node.content |
|
329 | 329 | |
|
330 | 330 | @LoginRequired() |
|
331 | 331 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
332 | 332 | 'repository.admin') |
|
333 | 333 | def raw(self, repo_name, revision, f_path): |
|
334 | 334 | """ |
|
335 | 335 | Action for show as raw, some mimetypes are "rendered", |
|
336 | 336 | those include images, icons. |
|
337 | 337 | """ |
|
338 | 338 | commit = self.__get_commit_or_redirect(revision, repo_name) |
|
339 | 339 | file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path) |
|
340 | 340 | |
|
341 | 341 | raw_mimetype_mapping = { |
|
342 | 342 | # map original mimetype to a mimetype used for "show as raw" |
|
343 | 343 | # you can also provide a content-disposition to override the |
|
344 | 344 | # default "attachment" disposition. |
|
345 | 345 | # orig_type: (new_type, new_dispo) |
|
346 | 346 | |
|
347 | 347 | # show images inline: |
|
348 | 348 | # Do not re-add SVG: it is unsafe and permits XSS attacks. One can |
|
349 | 349 | # for example render an SVG with javascript inside or even render |
|
350 | 350 | # HTML. |
|
351 | 351 | 'image/x-icon': ('image/x-icon', 'inline'), |
|
352 | 352 | 'image/png': ('image/png', 'inline'), |
|
353 | 353 | 'image/gif': ('image/gif', 'inline'), |
|
354 | 354 | 'image/jpeg': ('image/jpeg', 'inline'), |
|
355 | 355 | } |
|
356 | 356 | |
|
357 | 357 | mimetype = file_node.mimetype |
|
358 | 358 | try: |
|
359 | 359 | mimetype, dispo = raw_mimetype_mapping[mimetype] |
|
360 | 360 | except KeyError: |
|
361 | 361 | # we don't know anything special about this, handle it safely |
|
362 | 362 | if file_node.is_binary: |
|
363 | 363 | # do same as download raw for binary files |
|
364 | 364 | mimetype, dispo = 'application/octet-stream', 'attachment' |
|
365 | 365 | else: |
|
366 | 366 | # do not just use the original mimetype, but force text/plain, |
|
367 | 367 | # otherwise it would serve text/html and that might be unsafe. |
|
368 | 368 | # Note: underlying vcs library fakes text/plain mimetype if the |
|
369 | 369 | # mimetype can not be determined and it thinks it is not |
|
370 | 370 | # binary.This might lead to erroneous text display in some |
|
371 | 371 | # cases, but helps in other cases, like with text files |
|
372 | 372 | # without extension. |
|
373 | 373 | mimetype, dispo = 'text/plain', 'inline' |
|
374 | 374 | |
|
375 | 375 | if dispo == 'attachment': |
|
376 | 376 | dispo = 'attachment; filename=%s' % safe_str( |
|
377 | 377 | f_path.split(os.sep)[-1]) |
|
378 | 378 | |
|
379 | 379 | response.content_disposition = dispo |
|
380 | 380 | response.content_type = mimetype |
|
381 | 381 | charset = self._get_default_encoding() |
|
382 | 382 | if charset: |
|
383 | 383 | response.charset = charset |
|
384 | 384 | return file_node.content |
|
385 | 385 | |
|
386 | 386 | @CSRFRequired() |
|
387 | 387 | @LoginRequired() |
|
388 | 388 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
389 | 389 | def delete(self, repo_name, revision, f_path): |
|
390 | 390 | commit_id = revision |
|
391 | 391 | |
|
392 | 392 | repo = c.rhodecode_db_repo |
|
393 | 393 | if repo.enable_locking and repo.locked[0]: |
|
394 | 394 | h.flash(_('This repository has been locked by %s on %s') |
|
395 | 395 | % (h.person_by_id(repo.locked[0]), |
|
396 | 396 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
397 | 397 | 'warning') |
|
398 | 398 | return redirect(h.url('files_home', |
|
399 | 399 | repo_name=repo_name, revision='tip')) |
|
400 | 400 | |
|
401 | 401 | if not self._is_valid_head(commit_id, repo.scm_instance()): |
|
402 | 402 | h.flash(_('You can only delete files with revision ' |
|
403 | 403 | 'being a valid branch '), category='warning') |
|
404 | 404 | return redirect(h.url('files_home', |
|
405 | 405 | repo_name=repo_name, revision='tip', |
|
406 | 406 | f_path=f_path)) |
|
407 | 407 | |
|
408 | 408 | c.commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
409 | 409 | c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) |
|
410 | 410 | |
|
411 | 411 | c.default_message = _( |
|
412 | 412 | 'Deleted file %s via RhodeCode Enterprise') % (f_path) |
|
413 | 413 | c.f_path = f_path |
|
414 | 414 | node_path = f_path |
|
415 | 415 | author = c.rhodecode_user.full_contact |
|
416 | 416 | message = request.POST.get('message') or c.default_message |
|
417 | 417 | try: |
|
418 | 418 | nodes = { |
|
419 | 419 | node_path: { |
|
420 | 420 | 'content': '' |
|
421 | 421 | } |
|
422 | 422 | } |
|
423 | 423 | self.scm_model.delete_nodes( |
|
424 | 424 | user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo, |
|
425 | 425 | message=message, |
|
426 | 426 | nodes=nodes, |
|
427 | 427 | parent_commit=c.commit, |
|
428 | 428 | author=author, |
|
429 | 429 | ) |
|
430 | 430 | |
|
431 | 431 | h.flash(_('Successfully deleted file %s') % f_path, |
|
432 | 432 | category='success') |
|
433 | 433 | except Exception: |
|
434 | 434 | msg = _('Error occurred during commit') |
|
435 | 435 | log.exception(msg) |
|
436 | 436 | h.flash(msg, category='error') |
|
437 | 437 | return redirect(url('changeset_home', |
|
438 | 438 | repo_name=c.repo_name, revision='tip')) |
|
439 | 439 | |
|
440 | 440 | @LoginRequired() |
|
441 | 441 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
442 | 442 | def delete_home(self, repo_name, revision, f_path): |
|
443 | 443 | commit_id = revision |
|
444 | 444 | |
|
445 | 445 | repo = c.rhodecode_db_repo |
|
446 | 446 | if repo.enable_locking and repo.locked[0]: |
|
447 | 447 | h.flash(_('This repository has been locked by %s on %s') |
|
448 | 448 | % (h.person_by_id(repo.locked[0]), |
|
449 | 449 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
450 | 450 | 'warning') |
|
451 | 451 | return redirect(h.url('files_home', |
|
452 | 452 | repo_name=repo_name, revision='tip')) |
|
453 | 453 | |
|
454 | 454 | if not self._is_valid_head(commit_id, repo.scm_instance()): |
|
455 | 455 | h.flash(_('You can only delete files with revision ' |
|
456 | 456 | 'being a valid branch '), category='warning') |
|
457 | 457 | return redirect(h.url('files_home', |
|
458 | 458 | repo_name=repo_name, revision='tip', |
|
459 | 459 | f_path=f_path)) |
|
460 | 460 | |
|
461 | 461 | c.commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
462 | 462 | c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) |
|
463 | 463 | |
|
464 | 464 | c.default_message = _( |
|
465 | 465 | 'Deleted file %s via RhodeCode Enterprise') % (f_path) |
|
466 | 466 | c.f_path = f_path |
|
467 | 467 | |
|
468 |
return render('files/files_delete. |
|
|
468 | return render('files/files_delete.mako') | |
|
469 | 469 | |
|
470 | 470 | @CSRFRequired() |
|
471 | 471 | @LoginRequired() |
|
472 | 472 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
473 | 473 | def edit(self, repo_name, revision, f_path): |
|
474 | 474 | commit_id = revision |
|
475 | 475 | |
|
476 | 476 | repo = c.rhodecode_db_repo |
|
477 | 477 | if repo.enable_locking and repo.locked[0]: |
|
478 | 478 | h.flash(_('This repository has been locked by %s on %s') |
|
479 | 479 | % (h.person_by_id(repo.locked[0]), |
|
480 | 480 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
481 | 481 | 'warning') |
|
482 | 482 | return redirect(h.url('files_home', |
|
483 | 483 | repo_name=repo_name, revision='tip')) |
|
484 | 484 | |
|
485 | 485 | if not self._is_valid_head(commit_id, repo.scm_instance()): |
|
486 | 486 | h.flash(_('You can only edit files with revision ' |
|
487 | 487 | 'being a valid branch '), category='warning') |
|
488 | 488 | return redirect(h.url('files_home', |
|
489 | 489 | repo_name=repo_name, revision='tip', |
|
490 | 490 | f_path=f_path)) |
|
491 | 491 | |
|
492 | 492 | c.commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
493 | 493 | c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) |
|
494 | 494 | |
|
495 | 495 | if c.file.is_binary: |
|
496 | 496 | return redirect(url('files_home', repo_name=c.repo_name, |
|
497 | 497 | revision=c.commit.raw_id, f_path=f_path)) |
|
498 | 498 | c.default_message = _( |
|
499 | 499 | 'Edited file %s via RhodeCode Enterprise') % (f_path) |
|
500 | 500 | c.f_path = f_path |
|
501 | 501 | old_content = c.file.content |
|
502 | 502 | sl = old_content.splitlines(1) |
|
503 | 503 | first_line = sl[0] if sl else '' |
|
504 | 504 | |
|
505 | 505 | # modes: 0 - Unix, 1 - Mac, 2 - DOS |
|
506 | 506 | mode = detect_mode(first_line, 0) |
|
507 | 507 | content = convert_line_endings(request.POST.get('content', ''), mode) |
|
508 | 508 | |
|
509 | 509 | message = request.POST.get('message') or c.default_message |
|
510 | 510 | org_f_path = c.file.unicode_path |
|
511 | 511 | filename = request.POST['filename'] |
|
512 | 512 | org_filename = c.file.name |
|
513 | 513 | |
|
514 | 514 | if content == old_content and filename == org_filename: |
|
515 | 515 | h.flash(_('No changes'), category='warning') |
|
516 | 516 | return redirect(url('changeset_home', repo_name=c.repo_name, |
|
517 | 517 | revision='tip')) |
|
518 | 518 | try: |
|
519 | 519 | mapping = { |
|
520 | 520 | org_f_path: { |
|
521 | 521 | 'org_filename': org_f_path, |
|
522 | 522 | 'filename': os.path.join(c.file.dir_path, filename), |
|
523 | 523 | 'content': content, |
|
524 | 524 | 'lexer': '', |
|
525 | 525 | 'op': 'mod', |
|
526 | 526 | } |
|
527 | 527 | } |
|
528 | 528 | |
|
529 | 529 | ScmModel().update_nodes( |
|
530 | 530 | user=c.rhodecode_user.user_id, |
|
531 | 531 | repo=c.rhodecode_db_repo, |
|
532 | 532 | message=message, |
|
533 | 533 | nodes=mapping, |
|
534 | 534 | parent_commit=c.commit, |
|
535 | 535 | ) |
|
536 | 536 | |
|
537 | 537 | h.flash(_('Successfully committed to %s') % f_path, |
|
538 | 538 | category='success') |
|
539 | 539 | except Exception: |
|
540 | 540 | msg = _('Error occurred during commit') |
|
541 | 541 | log.exception(msg) |
|
542 | 542 | h.flash(msg, category='error') |
|
543 | 543 | return redirect(url('changeset_home', |
|
544 | 544 | repo_name=c.repo_name, revision='tip')) |
|
545 | 545 | |
|
546 | 546 | @LoginRequired() |
|
547 | 547 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
548 | 548 | def edit_home(self, repo_name, revision, f_path): |
|
549 | 549 | commit_id = revision |
|
550 | 550 | |
|
551 | 551 | repo = c.rhodecode_db_repo |
|
552 | 552 | if repo.enable_locking and repo.locked[0]: |
|
553 | 553 | h.flash(_('This repository has been locked by %s on %s') |
|
554 | 554 | % (h.person_by_id(repo.locked[0]), |
|
555 | 555 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
556 | 556 | 'warning') |
|
557 | 557 | return redirect(h.url('files_home', |
|
558 | 558 | repo_name=repo_name, revision='tip')) |
|
559 | 559 | |
|
560 | 560 | if not self._is_valid_head(commit_id, repo.scm_instance()): |
|
561 | 561 | h.flash(_('You can only edit files with revision ' |
|
562 | 562 | 'being a valid branch '), category='warning') |
|
563 | 563 | return redirect(h.url('files_home', |
|
564 | 564 | repo_name=repo_name, revision='tip', |
|
565 | 565 | f_path=f_path)) |
|
566 | 566 | |
|
567 | 567 | c.commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
568 | 568 | c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) |
|
569 | 569 | |
|
570 | 570 | if c.file.is_binary: |
|
571 | 571 | return redirect(url('files_home', repo_name=c.repo_name, |
|
572 | 572 | revision=c.commit.raw_id, f_path=f_path)) |
|
573 | 573 | c.default_message = _( |
|
574 | 574 | 'Edited file %s via RhodeCode Enterprise') % (f_path) |
|
575 | 575 | c.f_path = f_path |
|
576 | 576 | |
|
577 |
return render('files/files_edit. |
|
|
577 | return render('files/files_edit.mako') | |
|
578 | 578 | |
|
579 | 579 | def _is_valid_head(self, commit_id, repo): |
|
580 | 580 | # check if commit is a branch identifier- basically we cannot |
|
581 | 581 | # create multiple heads via file editing |
|
582 | 582 | valid_heads = repo.branches.keys() + repo.branches.values() |
|
583 | 583 | |
|
584 | 584 | if h.is_svn(repo) and not repo.is_empty(): |
|
585 | 585 | # Note: Subversion only has one head, we add it here in case there |
|
586 | 586 | # is no branch matched. |
|
587 | 587 | valid_heads.append(repo.get_commit(commit_idx=-1).raw_id) |
|
588 | 588 | |
|
589 | 589 | # check if commit is a branch name or branch hash |
|
590 | 590 | return commit_id in valid_heads |
|
591 | 591 | |
|
592 | 592 | @CSRFRequired() |
|
593 | 593 | @LoginRequired() |
|
594 | 594 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
595 | 595 | def add(self, repo_name, revision, f_path): |
|
596 | 596 | repo = Repository.get_by_repo_name(repo_name) |
|
597 | 597 | if repo.enable_locking and repo.locked[0]: |
|
598 | 598 | h.flash(_('This repository has been locked by %s on %s') |
|
599 | 599 | % (h.person_by_id(repo.locked[0]), |
|
600 | 600 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
601 | 601 | 'warning') |
|
602 | 602 | return redirect(h.url('files_home', |
|
603 | 603 | repo_name=repo_name, revision='tip')) |
|
604 | 604 | |
|
605 | 605 | r_post = request.POST |
|
606 | 606 | |
|
607 | 607 | c.commit = self.__get_commit_or_redirect( |
|
608 | 608 | revision, repo_name, redirect_after=False) |
|
609 | 609 | if c.commit is None: |
|
610 | 610 | c.commit = EmptyCommit(alias=c.rhodecode_repo.alias) |
|
611 | 611 | c.default_message = (_('Added file via RhodeCode Enterprise')) |
|
612 | 612 | c.f_path = f_path |
|
613 | 613 | unix_mode = 0 |
|
614 | 614 | content = convert_line_endings(r_post.get('content', ''), unix_mode) |
|
615 | 615 | |
|
616 | 616 | message = r_post.get('message') or c.default_message |
|
617 | 617 | filename = r_post.get('filename') |
|
618 | 618 | location = r_post.get('location', '') # dir location |
|
619 | 619 | file_obj = r_post.get('upload_file', None) |
|
620 | 620 | |
|
621 | 621 | if file_obj is not None and hasattr(file_obj, 'filename'): |
|
622 | 622 | filename = file_obj.filename |
|
623 | 623 | content = file_obj.file |
|
624 | 624 | |
|
625 | 625 | if hasattr(content, 'file'): |
|
626 | 626 | # non posix systems store real file under file attr |
|
627 | 627 | content = content.file |
|
628 | 628 | |
|
629 | 629 | # If there's no commit, redirect to repo summary |
|
630 | 630 | if type(c.commit) is EmptyCommit: |
|
631 | 631 | redirect_url = "summary_home" |
|
632 | 632 | else: |
|
633 | 633 | redirect_url = "changeset_home" |
|
634 | 634 | |
|
635 | 635 | if not filename: |
|
636 | 636 | h.flash(_('No filename'), category='warning') |
|
637 | 637 | return redirect(url(redirect_url, repo_name=c.repo_name, |
|
638 | 638 | revision='tip')) |
|
639 | 639 | |
|
640 | 640 | # extract the location from filename, |
|
641 | 641 | # allows using foo/bar.txt syntax to create subdirectories |
|
642 | 642 | subdir_loc = filename.rsplit('/', 1) |
|
643 | 643 | if len(subdir_loc) == 2: |
|
644 | 644 | location = os.path.join(location, subdir_loc[0]) |
|
645 | 645 | |
|
646 | 646 | # strip all crap out of file, just leave the basename |
|
647 | 647 | filename = os.path.basename(filename) |
|
648 | 648 | node_path = os.path.join(location, filename) |
|
649 | 649 | author = c.rhodecode_user.full_contact |
|
650 | 650 | |
|
651 | 651 | try: |
|
652 | 652 | nodes = { |
|
653 | 653 | node_path: { |
|
654 | 654 | 'content': content |
|
655 | 655 | } |
|
656 | 656 | } |
|
657 | 657 | self.scm_model.create_nodes( |
|
658 | 658 | user=c.rhodecode_user.user_id, |
|
659 | 659 | repo=c.rhodecode_db_repo, |
|
660 | 660 | message=message, |
|
661 | 661 | nodes=nodes, |
|
662 | 662 | parent_commit=c.commit, |
|
663 | 663 | author=author, |
|
664 | 664 | ) |
|
665 | 665 | |
|
666 | 666 | h.flash(_('Successfully committed to %s') % node_path, |
|
667 | 667 | category='success') |
|
668 | 668 | except NonRelativePathError as e: |
|
669 | 669 | h.flash(_( |
|
670 | 670 | 'The location specified must be a relative path and must not ' |
|
671 | 671 | 'contain .. in the path'), category='warning') |
|
672 | 672 | return redirect(url('changeset_home', repo_name=c.repo_name, |
|
673 | 673 | revision='tip')) |
|
674 | 674 | except (NodeError, NodeAlreadyExistsError) as e: |
|
675 | 675 | h.flash(_(e), category='error') |
|
676 | 676 | except Exception: |
|
677 | 677 | msg = _('Error occurred during commit') |
|
678 | 678 | log.exception(msg) |
|
679 | 679 | h.flash(msg, category='error') |
|
680 | 680 | return redirect(url('changeset_home', |
|
681 | 681 | repo_name=c.repo_name, revision='tip')) |
|
682 | 682 | |
|
683 | 683 | @LoginRequired() |
|
684 | 684 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
685 | 685 | def add_home(self, repo_name, revision, f_path): |
|
686 | 686 | |
|
687 | 687 | repo = Repository.get_by_repo_name(repo_name) |
|
688 | 688 | if repo.enable_locking and repo.locked[0]: |
|
689 | 689 | h.flash(_('This repository has been locked by %s on %s') |
|
690 | 690 | % (h.person_by_id(repo.locked[0]), |
|
691 | 691 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
692 | 692 | 'warning') |
|
693 | 693 | return redirect(h.url('files_home', |
|
694 | 694 | repo_name=repo_name, revision='tip')) |
|
695 | 695 | |
|
696 | 696 | c.commit = self.__get_commit_or_redirect( |
|
697 | 697 | revision, repo_name, redirect_after=False) |
|
698 | 698 | if c.commit is None: |
|
699 | 699 | c.commit = EmptyCommit(alias=c.rhodecode_repo.alias) |
|
700 | 700 | c.default_message = (_('Added file via RhodeCode Enterprise')) |
|
701 | 701 | c.f_path = f_path |
|
702 | 702 | |
|
703 |
return render('files/files_add. |
|
|
703 | return render('files/files_add.mako') | |
|
704 | 704 | |
|
705 | 705 | @LoginRequired() |
|
706 | 706 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
707 | 707 | 'repository.admin') |
|
708 | 708 | def archivefile(self, repo_name, fname): |
|
709 | 709 | fileformat = None |
|
710 | 710 | commit_id = None |
|
711 | 711 | ext = None |
|
712 | 712 | subrepos = request.GET.get('subrepos') == 'true' |
|
713 | 713 | |
|
714 | 714 | for a_type, ext_data in settings.ARCHIVE_SPECS.items(): |
|
715 | 715 | archive_spec = fname.split(ext_data[1]) |
|
716 | 716 | if len(archive_spec) == 2 and archive_spec[1] == '': |
|
717 | 717 | fileformat = a_type or ext_data[1] |
|
718 | 718 | commit_id = archive_spec[0] |
|
719 | 719 | ext = ext_data[1] |
|
720 | 720 | |
|
721 | 721 | dbrepo = RepoModel().get_by_repo_name(repo_name) |
|
722 | 722 | if not dbrepo.enable_downloads: |
|
723 | 723 | return _('Downloads disabled') |
|
724 | 724 | |
|
725 | 725 | try: |
|
726 | 726 | commit = c.rhodecode_repo.get_commit(commit_id) |
|
727 | 727 | content_type = settings.ARCHIVE_SPECS[fileformat][0] |
|
728 | 728 | except CommitDoesNotExistError: |
|
729 | 729 | return _('Unknown revision %s') % commit_id |
|
730 | 730 | except EmptyRepositoryError: |
|
731 | 731 | return _('Empty repository') |
|
732 | 732 | except KeyError: |
|
733 | 733 | return _('Unknown archive type') |
|
734 | 734 | |
|
735 | 735 | # archive cache |
|
736 | 736 | from rhodecode import CONFIG |
|
737 | 737 | |
|
738 | 738 | archive_name = '%s-%s%s%s' % ( |
|
739 | 739 | safe_str(repo_name.replace('/', '_')), |
|
740 | 740 | '-sub' if subrepos else '', |
|
741 | 741 | safe_str(commit.short_id), ext) |
|
742 | 742 | |
|
743 | 743 | use_cached_archive = False |
|
744 | 744 | archive_cache_enabled = CONFIG.get( |
|
745 | 745 | 'archive_cache_dir') and not request.GET.get('no_cache') |
|
746 | 746 | |
|
747 | 747 | if archive_cache_enabled: |
|
748 | 748 | # check if we it's ok to write |
|
749 | 749 | if not os.path.isdir(CONFIG['archive_cache_dir']): |
|
750 | 750 | os.makedirs(CONFIG['archive_cache_dir']) |
|
751 | 751 | cached_archive_path = os.path.join( |
|
752 | 752 | CONFIG['archive_cache_dir'], archive_name) |
|
753 | 753 | if os.path.isfile(cached_archive_path): |
|
754 | 754 | log.debug('Found cached archive in %s', cached_archive_path) |
|
755 | 755 | fd, archive = None, cached_archive_path |
|
756 | 756 | use_cached_archive = True |
|
757 | 757 | else: |
|
758 | 758 | log.debug('Archive %s is not yet cached', archive_name) |
|
759 | 759 | |
|
760 | 760 | if not use_cached_archive: |
|
761 | 761 | # generate new archive |
|
762 | 762 | fd, archive = tempfile.mkstemp() |
|
763 | 763 | log.debug('Creating new temp archive in %s' % (archive,)) |
|
764 | 764 | try: |
|
765 | 765 | commit.archive_repo(archive, kind=fileformat, subrepos=subrepos) |
|
766 | 766 | except ImproperArchiveTypeError: |
|
767 | 767 | return _('Unknown archive type') |
|
768 | 768 | if archive_cache_enabled: |
|
769 | 769 | # if we generated the archive and we have cache enabled |
|
770 | 770 | # let's use this for future |
|
771 | 771 | log.debug('Storing new archive in %s' % (cached_archive_path,)) |
|
772 | 772 | shutil.move(archive, cached_archive_path) |
|
773 | 773 | archive = cached_archive_path |
|
774 | 774 | |
|
775 | 775 | def get_chunked_archive(archive): |
|
776 | 776 | with open(archive, 'rb') as stream: |
|
777 | 777 | while True: |
|
778 | 778 | data = stream.read(16 * 1024) |
|
779 | 779 | if not data: |
|
780 | 780 | if fd: # fd means we used temporary file |
|
781 | 781 | os.close(fd) |
|
782 | 782 | if not archive_cache_enabled: |
|
783 | 783 | log.debug('Destroying temp archive %s', archive) |
|
784 | 784 | os.remove(archive) |
|
785 | 785 | break |
|
786 | 786 | yield data |
|
787 | 787 | |
|
788 | 788 | # store download action |
|
789 | 789 | action_logger(user=c.rhodecode_user, |
|
790 | 790 | action='user_downloaded_archive:%s' % archive_name, |
|
791 | 791 | repo=repo_name, ipaddr=self.ip_addr, commit=True) |
|
792 | 792 | response.content_disposition = str( |
|
793 | 793 | 'attachment; filename=%s' % archive_name) |
|
794 | 794 | response.content_type = str(content_type) |
|
795 | 795 | |
|
796 | 796 | return get_chunked_archive(archive) |
|
797 | 797 | |
|
798 | 798 | @LoginRequired() |
|
799 | 799 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
800 | 800 | 'repository.admin') |
|
801 | 801 | def diff(self, repo_name, f_path): |
|
802 | 802 | |
|
803 | 803 | c.action = request.GET.get('diff') |
|
804 | 804 | diff1 = request.GET.get('diff1', '') |
|
805 | 805 | diff2 = request.GET.get('diff2', '') |
|
806 | 806 | |
|
807 | 807 | path1, diff1 = parse_path_ref(diff1, default_path=f_path) |
|
808 | 808 | |
|
809 | 809 | ignore_whitespace = str2bool(request.GET.get('ignorews')) |
|
810 | 810 | line_context = request.GET.get('context', 3) |
|
811 | 811 | |
|
812 | 812 | if not any((diff1, diff2)): |
|
813 | 813 | h.flash( |
|
814 | 814 | 'Need query parameter "diff1" or "diff2" to generate a diff.', |
|
815 | 815 | category='error') |
|
816 | 816 | raise HTTPBadRequest() |
|
817 | 817 | |
|
818 | 818 | if c.action not in ['download', 'raw']: |
|
819 | 819 | # redirect to new view if we render diff |
|
820 | 820 | return redirect( |
|
821 | 821 | url('compare_url', repo_name=repo_name, |
|
822 | 822 | source_ref_type='rev', |
|
823 | 823 | source_ref=diff1, |
|
824 | 824 | target_repo=c.repo_name, |
|
825 | 825 | target_ref_type='rev', |
|
826 | 826 | target_ref=diff2, |
|
827 | 827 | f_path=f_path)) |
|
828 | 828 | |
|
829 | 829 | try: |
|
830 | 830 | node1 = self._get_file_node(diff1, path1) |
|
831 | 831 | node2 = self._get_file_node(diff2, f_path) |
|
832 | 832 | except (RepositoryError, NodeError): |
|
833 | 833 | log.exception("Exception while trying to get node from repository") |
|
834 | 834 | return redirect(url( |
|
835 | 835 | 'files_home', repo_name=c.repo_name, f_path=f_path)) |
|
836 | 836 | |
|
837 | 837 | if all(isinstance(node.commit, EmptyCommit) |
|
838 | 838 | for node in (node1, node2)): |
|
839 | 839 | raise HTTPNotFound |
|
840 | 840 | |
|
841 | 841 | c.commit_1 = node1.commit |
|
842 | 842 | c.commit_2 = node2.commit |
|
843 | 843 | |
|
844 | 844 | if c.action == 'download': |
|
845 | 845 | _diff = diffs.get_gitdiff(node1, node2, |
|
846 | 846 | ignore_whitespace=ignore_whitespace, |
|
847 | 847 | context=line_context) |
|
848 | 848 | diff = diffs.DiffProcessor(_diff, format='gitdiff') |
|
849 | 849 | |
|
850 | 850 | diff_name = '%s_vs_%s.diff' % (diff1, diff2) |
|
851 | 851 | response.content_type = 'text/plain' |
|
852 | 852 | response.content_disposition = ( |
|
853 | 853 | 'attachment; filename=%s' % (diff_name,) |
|
854 | 854 | ) |
|
855 | 855 | charset = self._get_default_encoding() |
|
856 | 856 | if charset: |
|
857 | 857 | response.charset = charset |
|
858 | 858 | return diff.as_raw() |
|
859 | 859 | |
|
860 | 860 | elif c.action == 'raw': |
|
861 | 861 | _diff = diffs.get_gitdiff(node1, node2, |
|
862 | 862 | ignore_whitespace=ignore_whitespace, |
|
863 | 863 | context=line_context) |
|
864 | 864 | diff = diffs.DiffProcessor(_diff, format='gitdiff') |
|
865 | 865 | response.content_type = 'text/plain' |
|
866 | 866 | charset = self._get_default_encoding() |
|
867 | 867 | if charset: |
|
868 | 868 | response.charset = charset |
|
869 | 869 | return diff.as_raw() |
|
870 | 870 | |
|
871 | 871 | else: |
|
872 | 872 | return redirect( |
|
873 | 873 | url('compare_url', repo_name=repo_name, |
|
874 | 874 | source_ref_type='rev', |
|
875 | 875 | source_ref=diff1, |
|
876 | 876 | target_repo=c.repo_name, |
|
877 | 877 | target_ref_type='rev', |
|
878 | 878 | target_ref=diff2, |
|
879 | 879 | f_path=f_path)) |
|
880 | 880 | |
|
881 | 881 | @LoginRequired() |
|
882 | 882 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
883 | 883 | 'repository.admin') |
|
884 | 884 | def diff_2way(self, repo_name, f_path): |
|
885 | 885 | """ |
|
886 | 886 | Kept only to make OLD links work |
|
887 | 887 | """ |
|
888 | 888 | diff1 = request.GET.get('diff1', '') |
|
889 | 889 | diff2 = request.GET.get('diff2', '') |
|
890 | 890 | |
|
891 | 891 | if not any((diff1, diff2)): |
|
892 | 892 | h.flash( |
|
893 | 893 | 'Need query parameter "diff1" or "diff2" to generate a diff.', |
|
894 | 894 | category='error') |
|
895 | 895 | raise HTTPBadRequest() |
|
896 | 896 | |
|
897 | 897 | return redirect( |
|
898 | 898 | url('compare_url', repo_name=repo_name, |
|
899 | 899 | source_ref_type='rev', |
|
900 | 900 | source_ref=diff1, |
|
901 | 901 | target_repo=c.repo_name, |
|
902 | 902 | target_ref_type='rev', |
|
903 | 903 | target_ref=diff2, |
|
904 | 904 | f_path=f_path, |
|
905 | 905 | diffmode='sideside')) |
|
906 | 906 | |
|
907 | 907 | def _get_file_node(self, commit_id, f_path): |
|
908 | 908 | if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]: |
|
909 | 909 | commit = c.rhodecode_repo.get_commit(commit_id=commit_id) |
|
910 | 910 | try: |
|
911 | 911 | node = commit.get_node(f_path) |
|
912 | 912 | if node.is_dir(): |
|
913 | 913 | raise NodeError('%s path is a %s not a file' |
|
914 | 914 | % (node, type(node))) |
|
915 | 915 | except NodeDoesNotExistError: |
|
916 | 916 | commit = EmptyCommit( |
|
917 | 917 | commit_id=commit_id, |
|
918 | 918 | idx=commit.idx, |
|
919 | 919 | repo=commit.repository, |
|
920 | 920 | alias=commit.repository.alias, |
|
921 | 921 | message=commit.message, |
|
922 | 922 | author=commit.author, |
|
923 | 923 | date=commit.date) |
|
924 | 924 | node = FileNode(f_path, '', commit=commit) |
|
925 | 925 | else: |
|
926 | 926 | commit = EmptyCommit( |
|
927 | 927 | repo=c.rhodecode_repo, |
|
928 | 928 | alias=c.rhodecode_repo.alias) |
|
929 | 929 | node = FileNode(f_path, '', commit=commit) |
|
930 | 930 | return node |
|
931 | 931 | |
|
932 | 932 | def _get_node_history(self, commit, f_path, commits=None): |
|
933 | 933 | """ |
|
934 | 934 | get commit history for given node |
|
935 | 935 | |
|
936 | 936 | :param commit: commit to calculate history |
|
937 | 937 | :param f_path: path for node to calculate history for |
|
938 | 938 | :param commits: if passed don't calculate history and take |
|
939 | 939 | commits defined in this list |
|
940 | 940 | """ |
|
941 | 941 | # calculate history based on tip |
|
942 | 942 | tip = c.rhodecode_repo.get_commit() |
|
943 | 943 | if commits is None: |
|
944 | 944 | pre_load = ["author", "branch"] |
|
945 | 945 | try: |
|
946 | 946 | commits = tip.get_file_history(f_path, pre_load=pre_load) |
|
947 | 947 | except (NodeDoesNotExistError, CommitError): |
|
948 | 948 | # this node is not present at tip! |
|
949 | 949 | commits = commit.get_file_history(f_path, pre_load=pre_load) |
|
950 | 950 | |
|
951 | 951 | history = [] |
|
952 | 952 | commits_group = ([], _("Changesets")) |
|
953 | 953 | for commit in commits: |
|
954 | 954 | branch = ' (%s)' % commit.branch if commit.branch else '' |
|
955 | 955 | n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch) |
|
956 | 956 | commits_group[0].append((commit.raw_id, n_desc,)) |
|
957 | 957 | history.append(commits_group) |
|
958 | 958 | |
|
959 | 959 | symbolic_reference = self._symbolic_reference |
|
960 | 960 | |
|
961 | 961 | if c.rhodecode_repo.alias == 'svn': |
|
962 | 962 | adjusted_f_path = self._adjust_file_path_for_svn( |
|
963 | 963 | f_path, c.rhodecode_repo) |
|
964 | 964 | if adjusted_f_path != f_path: |
|
965 | 965 | log.debug( |
|
966 | 966 | 'Recognized svn tag or branch in file "%s", using svn ' |
|
967 | 967 | 'specific symbolic references', f_path) |
|
968 | 968 | f_path = adjusted_f_path |
|
969 | 969 | symbolic_reference = self._symbolic_reference_svn |
|
970 | 970 | |
|
971 | 971 | branches = self._create_references( |
|
972 | 972 | c.rhodecode_repo.branches, symbolic_reference, f_path) |
|
973 | 973 | branches_group = (branches, _("Branches")) |
|
974 | 974 | |
|
975 | 975 | tags = self._create_references( |
|
976 | 976 | c.rhodecode_repo.tags, symbolic_reference, f_path) |
|
977 | 977 | tags_group = (tags, _("Tags")) |
|
978 | 978 | |
|
979 | 979 | history.append(branches_group) |
|
980 | 980 | history.append(tags_group) |
|
981 | 981 | |
|
982 | 982 | return history, commits |
|
983 | 983 | |
|
984 | 984 | def _adjust_file_path_for_svn(self, f_path, repo): |
|
985 | 985 | """ |
|
986 | 986 | Computes the relative path of `f_path`. |
|
987 | 987 | |
|
988 | 988 | This is mainly based on prefix matching of the recognized tags and |
|
989 | 989 | branches in the underlying repository. |
|
990 | 990 | """ |
|
991 | 991 | tags_and_branches = itertools.chain( |
|
992 | 992 | repo.branches.iterkeys(), |
|
993 | 993 | repo.tags.iterkeys()) |
|
994 | 994 | tags_and_branches = sorted(tags_and_branches, key=len, reverse=True) |
|
995 | 995 | |
|
996 | 996 | for name in tags_and_branches: |
|
997 | 997 | if f_path.startswith(name + '/'): |
|
998 | 998 | f_path = vcspath.relpath(f_path, name) |
|
999 | 999 | break |
|
1000 | 1000 | return f_path |
|
1001 | 1001 | |
|
1002 | 1002 | def _create_references( |
|
1003 | 1003 | self, branches_or_tags, symbolic_reference, f_path): |
|
1004 | 1004 | items = [] |
|
1005 | 1005 | for name, commit_id in branches_or_tags.items(): |
|
1006 | 1006 | sym_ref = symbolic_reference(commit_id, name, f_path) |
|
1007 | 1007 | items.append((sym_ref, name)) |
|
1008 | 1008 | return items |
|
1009 | 1009 | |
|
1010 | 1010 | def _symbolic_reference(self, commit_id, name, f_path): |
|
1011 | 1011 | return commit_id |
|
1012 | 1012 | |
|
1013 | 1013 | def _symbolic_reference_svn(self, commit_id, name, f_path): |
|
1014 | 1014 | new_f_path = vcspath.join(name, f_path) |
|
1015 | 1015 | return u'%s@%s' % (new_f_path, commit_id) |
|
1016 | 1016 | |
|
1017 | 1017 | @LoginRequired() |
|
1018 | 1018 | @XHRRequired() |
|
1019 | 1019 | @HasRepoPermissionAnyDecorator( |
|
1020 | 1020 | 'repository.read', 'repository.write', 'repository.admin') |
|
1021 | 1021 | @jsonify |
|
1022 | 1022 | def nodelist(self, repo_name, revision, f_path): |
|
1023 | 1023 | commit = self.__get_commit_or_redirect(revision, repo_name) |
|
1024 | 1024 | |
|
1025 | 1025 | metadata = self._get_nodelist_at_commit( |
|
1026 | 1026 | repo_name, commit.raw_id, f_path) |
|
1027 | 1027 | return {'nodes': metadata} |
|
1028 | 1028 | |
|
1029 | 1029 | @LoginRequired() |
|
1030 | 1030 | @XHRRequired() |
|
1031 | 1031 | @HasRepoPermissionAnyDecorator( |
|
1032 | 1032 | 'repository.read', 'repository.write', 'repository.admin') |
|
1033 | 1033 | def nodetree_full(self, repo_name, commit_id, f_path): |
|
1034 | 1034 | """ |
|
1035 | 1035 | Returns rendered html of file tree that contains commit date, |
|
1036 | 1036 | author, revision for the specified combination of |
|
1037 | 1037 | repo, commit_id and file path |
|
1038 | 1038 | |
|
1039 | 1039 | :param repo_name: name of the repository |
|
1040 | 1040 | :param commit_id: commit_id of file tree |
|
1041 | 1041 | :param f_path: file path of the requested directory |
|
1042 | 1042 | """ |
|
1043 | 1043 | |
|
1044 | 1044 | commit = self.__get_commit_or_redirect(commit_id, repo_name) |
|
1045 | 1045 | try: |
|
1046 | 1046 | dir_node = commit.get_node(f_path) |
|
1047 | 1047 | except RepositoryError as e: |
|
1048 | 1048 | return 'error {}'.format(safe_str(e)) |
|
1049 | 1049 | |
|
1050 | 1050 | if dir_node.is_file(): |
|
1051 | 1051 | return '' |
|
1052 | 1052 | |
|
1053 | 1053 | c.file = dir_node |
|
1054 | 1054 | c.commit = commit |
|
1055 | 1055 | |
|
1056 | 1056 | # using force=True here, make a little trick. We flush the cache and |
|
1057 | 1057 | # compute it using the same key as without full_load, so the fully |
|
1058 | 1058 | # loaded cached tree is now returned instead of partial |
|
1059 | 1059 | return self._get_tree_at_commit( |
|
1060 | 1060 | repo_name, commit.raw_id, dir_node.path, full_load=True, |
|
1061 | 1061 | force=True) |
@@ -1,58 +1,58 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-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 | Followers controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from pylons import tmpl_context as c, request |
|
28 | 28 | |
|
29 | 29 | from rhodecode.lib.helpers import Page |
|
30 | 30 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
31 | 31 | from rhodecode.lib.base import BaseRepoController, render |
|
32 | 32 | from rhodecode.model.db import UserFollowing |
|
33 | 33 | from rhodecode.lib.utils2 import safe_int |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | class FollowersController(BaseRepoController): |
|
39 | 39 | |
|
40 | 40 | def __before__(self): |
|
41 | 41 | super(FollowersController, self).__before__() |
|
42 | 42 | |
|
43 | 43 | @LoginRequired() |
|
44 | 44 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
45 | 45 | 'repository.admin') |
|
46 | 46 | def followers(self, repo_name): |
|
47 | 47 | p = safe_int(request.GET.get('page', 1), 1) |
|
48 | 48 | repo_id = c.rhodecode_db_repo.repo_id |
|
49 | 49 | d = UserFollowing.get_repo_followers(repo_id)\ |
|
50 | 50 | .order_by(UserFollowing.follows_from) |
|
51 | 51 | c.followers_pager = Page(d, page=p, items_per_page=20) |
|
52 | 52 | |
|
53 |
c.followers_data = render('/followers/followers_data. |
|
|
53 | c.followers_data = render('/followers/followers_data.mako') | |
|
54 | 54 | |
|
55 | 55 | if request.environ.get('HTTP_X_PJAX'): |
|
56 | 56 | return c.followers_data |
|
57 | 57 | |
|
58 |
return render('/followers/followers. |
|
|
58 | return render('/followers/followers.mako') |
@@ -1,196 +1,196 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-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 | forks controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import formencode |
|
26 | 26 | import logging |
|
27 | 27 | from formencode import htmlfill |
|
28 | 28 | |
|
29 | 29 | from pylons import tmpl_context as c, request, url |
|
30 | 30 | from pylons.controllers.util import redirect |
|
31 | 31 | from pylons.i18n.translation import _ |
|
32 | 32 | |
|
33 | 33 | import rhodecode.lib.helpers as h |
|
34 | 34 | |
|
35 | 35 | from rhodecode.lib import auth |
|
36 | 36 | from rhodecode.lib.helpers import Page |
|
37 | 37 | from rhodecode.lib.auth import ( |
|
38 | 38 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, |
|
39 | 39 | HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType) |
|
40 | 40 | from rhodecode.lib.base import BaseRepoController, render |
|
41 | 41 | from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User |
|
42 | 42 | from rhodecode.model.repo import RepoModel |
|
43 | 43 | from rhodecode.model.forms import RepoForkForm |
|
44 | 44 | from rhodecode.model.scm import ScmModel, RepoGroupList |
|
45 | 45 | from rhodecode.lib.utils2 import safe_int |
|
46 | 46 | |
|
47 | 47 | log = logging.getLogger(__name__) |
|
48 | 48 | |
|
49 | 49 | |
|
50 | 50 | class ForksController(BaseRepoController): |
|
51 | 51 | |
|
52 | 52 | def __before__(self): |
|
53 | 53 | super(ForksController, self).__before__() |
|
54 | 54 | |
|
55 | 55 | def __load_defaults(self): |
|
56 | 56 | acl_groups = RepoGroupList( |
|
57 | 57 | RepoGroup.query().all(), |
|
58 | 58 | perm_set=['group.write', 'group.admin']) |
|
59 | 59 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
60 | 60 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
61 | 61 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
62 | 62 | c.landing_revs_choices = choices |
|
63 | 63 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
64 | 64 | |
|
65 | 65 | def __load_data(self, repo_name=None): |
|
66 | 66 | """ |
|
67 | 67 | Load defaults settings for edit, and update |
|
68 | 68 | |
|
69 | 69 | :param repo_name: |
|
70 | 70 | """ |
|
71 | 71 | self.__load_defaults() |
|
72 | 72 | |
|
73 | 73 | c.repo_info = Repository.get_by_repo_name(repo_name) |
|
74 | 74 | repo = c.repo_info.scm_instance() |
|
75 | 75 | |
|
76 | 76 | if c.repo_info is None: |
|
77 | 77 | h.not_mapped_error(repo_name) |
|
78 | 78 | return redirect(url('repos')) |
|
79 | 79 | |
|
80 | 80 | c.default_user_id = User.get_default_user().user_id |
|
81 | 81 | c.in_public_journal = UserFollowing.query()\ |
|
82 | 82 | .filter(UserFollowing.user_id == c.default_user_id)\ |
|
83 | 83 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() |
|
84 | 84 | |
|
85 | 85 | if c.repo_info.stats: |
|
86 | 86 | last_rev = c.repo_info.stats.stat_on_revision+1 |
|
87 | 87 | else: |
|
88 | 88 | last_rev = 0 |
|
89 | 89 | c.stats_revision = last_rev |
|
90 | 90 | |
|
91 | 91 | c.repo_last_rev = repo.count() |
|
92 | 92 | |
|
93 | 93 | if last_rev == 0 or c.repo_last_rev == 0: |
|
94 | 94 | c.stats_percentage = 0 |
|
95 | 95 | else: |
|
96 | 96 | c.stats_percentage = '%.2f' % ((float((last_rev)) / |
|
97 | 97 | c.repo_last_rev) * 100) |
|
98 | 98 | |
|
99 | 99 | defaults = RepoModel()._get_defaults(repo_name) |
|
100 | 100 | # alter the description to indicate a fork |
|
101 | 101 | defaults['description'] = ('fork of repository: %s \n%s' |
|
102 | 102 | % (defaults['repo_name'], |
|
103 | 103 | defaults['description'])) |
|
104 | 104 | # add suffix to fork |
|
105 | 105 | defaults['repo_name'] = '%s-fork' % defaults['repo_name'] |
|
106 | 106 | |
|
107 | 107 | return defaults |
|
108 | 108 | |
|
109 | 109 | @LoginRequired() |
|
110 | 110 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
111 | 111 | 'repository.admin') |
|
112 | 112 | @HasAcceptedRepoType('git', 'hg') |
|
113 | 113 | def forks(self, repo_name): |
|
114 | 114 | p = safe_int(request.GET.get('page', 1), 1) |
|
115 | 115 | repo_id = c.rhodecode_db_repo.repo_id |
|
116 | 116 | d = [] |
|
117 | 117 | for r in Repository.get_repo_forks(repo_id): |
|
118 | 118 | if not HasRepoPermissionAny( |
|
119 | 119 | 'repository.read', 'repository.write', 'repository.admin' |
|
120 | 120 | )(r.repo_name, 'get forks check'): |
|
121 | 121 | continue |
|
122 | 122 | d.append(r) |
|
123 | 123 | c.forks_pager = Page(d, page=p, items_per_page=20) |
|
124 | 124 | |
|
125 |
c.forks_data = render('/forks/forks_data. |
|
|
125 | c.forks_data = render('/forks/forks_data.mako') | |
|
126 | 126 | |
|
127 | 127 | if request.environ.get('HTTP_X_PJAX'): |
|
128 | 128 | return c.forks_data |
|
129 | 129 | |
|
130 |
return render('/forks/forks. |
|
|
130 | return render('/forks/forks.mako') | |
|
131 | 131 | |
|
132 | 132 | @LoginRequired() |
|
133 | 133 | @NotAnonymous() |
|
134 | 134 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
135 | 135 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
136 | 136 | 'repository.admin') |
|
137 | 137 | @HasAcceptedRepoType('git', 'hg') |
|
138 | 138 | def fork(self, repo_name): |
|
139 | 139 | c.repo_info = Repository.get_by_repo_name(repo_name) |
|
140 | 140 | if not c.repo_info: |
|
141 | 141 | h.not_mapped_error(repo_name) |
|
142 | 142 | return redirect(url('home')) |
|
143 | 143 | |
|
144 | 144 | defaults = self.__load_data(repo_name) |
|
145 | 145 | |
|
146 | 146 | return htmlfill.render( |
|
147 |
render('forks/fork. |
|
|
147 | render('forks/fork.mako'), | |
|
148 | 148 | defaults=defaults, |
|
149 | 149 | encoding="UTF-8", |
|
150 | 150 | force_defaults=False |
|
151 | 151 | ) |
|
152 | 152 | |
|
153 | 153 | @LoginRequired() |
|
154 | 154 | @NotAnonymous() |
|
155 | 155 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
156 | 156 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
157 | 157 | 'repository.admin') |
|
158 | 158 | @HasAcceptedRepoType('git', 'hg') |
|
159 | 159 | @auth.CSRFRequired() |
|
160 | 160 | def fork_create(self, repo_name): |
|
161 | 161 | self.__load_defaults() |
|
162 | 162 | c.repo_info = Repository.get_by_repo_name(repo_name) |
|
163 | 163 | _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type}, |
|
164 | 164 | repo_groups=c.repo_groups_choices, |
|
165 | 165 | landing_revs=c.landing_revs_choices)() |
|
166 | 166 | form_result = {} |
|
167 | 167 | task_id = None |
|
168 | 168 | try: |
|
169 | 169 | form_result = _form.to_python(dict(request.POST)) |
|
170 | 170 | # create fork is done sometimes async on celery, db transaction |
|
171 | 171 | # management is handled there. |
|
172 | 172 | task = RepoModel().create_fork( |
|
173 | 173 | form_result, c.rhodecode_user.user_id) |
|
174 | 174 | from celery.result import BaseAsyncResult |
|
175 | 175 | if isinstance(task, BaseAsyncResult): |
|
176 | 176 | task_id = task.task_id |
|
177 | 177 | except formencode.Invalid as errors: |
|
178 | 178 | c.new_repo = errors.value['repo_name'] |
|
179 | 179 | return htmlfill.render( |
|
180 |
render('forks/fork. |
|
|
180 | render('forks/fork.mako'), | |
|
181 | 181 | defaults=errors.value, |
|
182 | 182 | errors=errors.error_dict or {}, |
|
183 | 183 | prefix_error=False, |
|
184 | 184 | encoding="UTF-8", |
|
185 | 185 | force_defaults=False) |
|
186 | 186 | except Exception: |
|
187 | 187 | log.exception( |
|
188 | 188 | u'Exception while trying to fork the repository %s', repo_name) |
|
189 | 189 | msg = ( |
|
190 | 190 | _('An error occurred during repository forking %s') % |
|
191 | 191 | (repo_name, )) |
|
192 | 192 | h.flash(msg, category='error') |
|
193 | 193 | |
|
194 | 194 | return redirect(h.url('repo_creating_home', |
|
195 | 195 | repo_name=form_result['repo_name_full'], |
|
196 | 196 | task_id=task_id)) |
@@ -1,288 +1,288 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 | Home controller for RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import time |
|
27 | 27 | import re |
|
28 | 28 | |
|
29 | 29 | from pylons import tmpl_context as c, request, url, config |
|
30 | 30 | from pylons.i18n.translation import _ |
|
31 | 31 | from sqlalchemy.sql import func |
|
32 | 32 | |
|
33 | 33 | from rhodecode.lib.auth import ( |
|
34 | 34 | LoginRequired, HasPermissionAllDecorator, AuthUser, |
|
35 | 35 | HasRepoGroupPermissionAnyDecorator, XHRRequired) |
|
36 | 36 | from rhodecode.lib.base import BaseController, render |
|
37 | 37 | from rhodecode.lib.index import searcher_from_config |
|
38 | 38 | from rhodecode.lib.ext_json import json |
|
39 | 39 | from rhodecode.lib.utils import jsonify |
|
40 | 40 | from rhodecode.lib.utils2 import safe_unicode, str2bool |
|
41 | 41 | from rhodecode.model.db import Repository, RepoGroup |
|
42 | 42 | from rhodecode.model.repo import RepoModel |
|
43 | 43 | from rhodecode.model.repo_group import RepoGroupModel |
|
44 | 44 | from rhodecode.model.scm import RepoList, RepoGroupList |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | log = logging.getLogger(__name__) |
|
48 | 48 | |
|
49 | 49 | |
|
50 | 50 | class HomeController(BaseController): |
|
51 | 51 | def __before__(self): |
|
52 | 52 | super(HomeController, self).__before__() |
|
53 | 53 | |
|
54 | 54 | def ping(self): |
|
55 | 55 | """ |
|
56 | 56 | Ping, doesn't require login, good for checking out the platform |
|
57 | 57 | """ |
|
58 | 58 | instance_id = getattr(c, 'rhodecode_instanceid', '') |
|
59 | 59 | return 'pong[%s] => %s' % (instance_id, self.ip_addr,) |
|
60 | 60 | |
|
61 | 61 | @LoginRequired() |
|
62 | 62 | @HasPermissionAllDecorator('hg.admin') |
|
63 | 63 | def error_test(self): |
|
64 | 64 | """ |
|
65 | 65 | Test exception handling and emails on errors |
|
66 | 66 | """ |
|
67 | 67 | class TestException(Exception): |
|
68 | 68 | pass |
|
69 | 69 | |
|
70 | 70 | msg = ('RhodeCode Enterprise %s test exception. Generation time: %s' |
|
71 | 71 | % (c.rhodecode_name, time.time())) |
|
72 | 72 | raise TestException(msg) |
|
73 | 73 | |
|
74 | 74 | def _get_groups_and_repos(self, repo_group_id=None): |
|
75 | 75 | # repo groups groups |
|
76 | 76 | repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) |
|
77 | 77 | _perms = ['group.read', 'group.write', 'group.admin'] |
|
78 | 78 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) |
|
79 | 79 | repo_group_data = RepoGroupModel().get_repo_groups_as_dict( |
|
80 | 80 | repo_group_list=repo_group_list_acl, admin=False) |
|
81 | 81 | |
|
82 | 82 | # repositories |
|
83 | 83 | repo_list = Repository.get_all_repos(group_id=repo_group_id) |
|
84 | 84 | _perms = ['repository.read', 'repository.write', 'repository.admin'] |
|
85 | 85 | repo_list_acl = RepoList(repo_list, perm_set=_perms) |
|
86 | 86 | repo_data = RepoModel().get_repos_as_dict( |
|
87 | 87 | repo_list=repo_list_acl, admin=False) |
|
88 | 88 | |
|
89 | 89 | return repo_data, repo_group_data |
|
90 | 90 | |
|
91 | 91 | @LoginRequired() |
|
92 | 92 | def index(self): |
|
93 | 93 | c.repo_group = None |
|
94 | 94 | |
|
95 | 95 | repo_data, repo_group_data = self._get_groups_and_repos() |
|
96 | 96 | # json used to render the grids |
|
97 | 97 | c.repos_data = json.dumps(repo_data) |
|
98 | 98 | c.repo_groups_data = json.dumps(repo_group_data) |
|
99 | 99 | |
|
100 |
return render('/index. |
|
|
100 | return render('/index.mako') | |
|
101 | 101 | |
|
102 | 102 | @LoginRequired() |
|
103 | 103 | @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write', |
|
104 | 104 | 'group.admin') |
|
105 | 105 | def index_repo_group(self, group_name): |
|
106 | 106 | """GET /repo_group_name: Show a specific item""" |
|
107 | 107 | c.repo_group = RepoGroupModel()._get_repo_group(group_name) |
|
108 | 108 | repo_data, repo_group_data = self._get_groups_and_repos( |
|
109 | 109 | c.repo_group.group_id) |
|
110 | 110 | |
|
111 | 111 | # json used to render the grids |
|
112 | 112 | c.repos_data = json.dumps(repo_data) |
|
113 | 113 | c.repo_groups_data = json.dumps(repo_group_data) |
|
114 | 114 | |
|
115 |
return render('index_repo_group. |
|
|
115 | return render('index_repo_group.mako') | |
|
116 | 116 | |
|
117 | 117 | def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): |
|
118 | 118 | query = Repository.query()\ |
|
119 | 119 | .order_by(func.length(Repository.repo_name))\ |
|
120 | 120 | .order_by(Repository.repo_name) |
|
121 | 121 | |
|
122 | 122 | if repo_type: |
|
123 | 123 | query = query.filter(Repository.repo_type == repo_type) |
|
124 | 124 | |
|
125 | 125 | if name_contains: |
|
126 | 126 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
127 | 127 | query = query.filter( |
|
128 | 128 | Repository.repo_name.ilike(ilike_expression)) |
|
129 | 129 | query = query.limit(limit) |
|
130 | 130 | |
|
131 | 131 | all_repos = query.all() |
|
132 | 132 | repo_iter = self.scm_model.get_repos(all_repos) |
|
133 | 133 | return [ |
|
134 | 134 | { |
|
135 | 135 | 'id': obj['name'], |
|
136 | 136 | 'text': obj['name'], |
|
137 | 137 | 'type': 'repo', |
|
138 | 138 | 'obj': obj['dbrepo'], |
|
139 | 139 | 'url': url('summary_home', repo_name=obj['name']) |
|
140 | 140 | } |
|
141 | 141 | for obj in repo_iter] |
|
142 | 142 | |
|
143 | 143 | def _get_repo_group_list(self, name_contains=None, limit=20): |
|
144 | 144 | query = RepoGroup.query()\ |
|
145 | 145 | .order_by(func.length(RepoGroup.group_name))\ |
|
146 | 146 | .order_by(RepoGroup.group_name) |
|
147 | 147 | |
|
148 | 148 | if name_contains: |
|
149 | 149 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
150 | 150 | query = query.filter( |
|
151 | 151 | RepoGroup.group_name.ilike(ilike_expression)) |
|
152 | 152 | query = query.limit(limit) |
|
153 | 153 | |
|
154 | 154 | all_groups = query.all() |
|
155 | 155 | repo_groups_iter = self.scm_model.get_repo_groups(all_groups) |
|
156 | 156 | return [ |
|
157 | 157 | { |
|
158 | 158 | 'id': obj.group_name, |
|
159 | 159 | 'text': obj.group_name, |
|
160 | 160 | 'type': 'group', |
|
161 | 161 | 'obj': {}, |
|
162 | 162 | 'url': url('repo_group_home', group_name=obj.group_name) |
|
163 | 163 | } |
|
164 | 164 | for obj in repo_groups_iter] |
|
165 | 165 | |
|
166 | 166 | def _get_hash_commit_list(self, hash_starts_with=None, limit=20): |
|
167 | 167 | if not hash_starts_with or len(hash_starts_with) < 3: |
|
168 | 168 | return [] |
|
169 | 169 | |
|
170 | 170 | commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) |
|
171 | 171 | |
|
172 | 172 | if len(commit_hashes) != 1: |
|
173 | 173 | return [] |
|
174 | 174 | |
|
175 | 175 | commit_hash_prefix = commit_hashes[0] |
|
176 | 176 | |
|
177 | 177 | auth_user = AuthUser( |
|
178 | 178 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) |
|
179 | 179 | searcher = searcher_from_config(config) |
|
180 | 180 | result = searcher.search( |
|
181 | 181 | 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user) |
|
182 | 182 | |
|
183 | 183 | return [ |
|
184 | 184 | { |
|
185 | 185 | 'id': entry['commit_id'], |
|
186 | 186 | 'text': entry['commit_id'], |
|
187 | 187 | 'type': 'commit', |
|
188 | 188 | 'obj': {'repo': entry['repository']}, |
|
189 | 189 | 'url': url('changeset_home', |
|
190 | 190 | repo_name=entry['repository'], revision=entry['commit_id']) |
|
191 | 191 | } |
|
192 | 192 | for entry in result['results']] |
|
193 | 193 | |
|
194 | 194 | @LoginRequired() |
|
195 | 195 | @XHRRequired() |
|
196 | 196 | @jsonify |
|
197 | 197 | def goto_switcher_data(self): |
|
198 | 198 | query = request.GET.get('query') |
|
199 | 199 | log.debug('generating goto switcher list, query %s', query) |
|
200 | 200 | |
|
201 | 201 | res = [] |
|
202 | 202 | repo_groups = self._get_repo_group_list(query) |
|
203 | 203 | if repo_groups: |
|
204 | 204 | res.append({ |
|
205 | 205 | 'text': _('Groups'), |
|
206 | 206 | 'children': repo_groups |
|
207 | 207 | }) |
|
208 | 208 | |
|
209 | 209 | repos = self._get_repo_list(query) |
|
210 | 210 | if repos: |
|
211 | 211 | res.append({ |
|
212 | 212 | 'text': _('Repositories'), |
|
213 | 213 | 'children': repos |
|
214 | 214 | }) |
|
215 | 215 | |
|
216 | 216 | commits = self._get_hash_commit_list(query) |
|
217 | 217 | if commits: |
|
218 | 218 | unique_repos = {} |
|
219 | 219 | for commit in commits: |
|
220 | 220 | unique_repos.setdefault(commit['obj']['repo'], [] |
|
221 | 221 | ).append(commit) |
|
222 | 222 | |
|
223 | 223 | for repo in unique_repos: |
|
224 | 224 | res.append({ |
|
225 | 225 | 'text': _('Commits in %(repo)s') % {'repo': repo}, |
|
226 | 226 | 'children': unique_repos[repo] |
|
227 | 227 | }) |
|
228 | 228 | |
|
229 | 229 | data = { |
|
230 | 230 | 'more': False, |
|
231 | 231 | 'results': res |
|
232 | 232 | } |
|
233 | 233 | return data |
|
234 | 234 | |
|
235 | 235 | @LoginRequired() |
|
236 | 236 | @XHRRequired() |
|
237 | 237 | @jsonify |
|
238 | 238 | def repo_list_data(self): |
|
239 | 239 | query = request.GET.get('query') |
|
240 | 240 | repo_type = request.GET.get('repo_type') |
|
241 | 241 | log.debug('generating repo list, query:%s', query) |
|
242 | 242 | |
|
243 | 243 | res = [] |
|
244 | 244 | repos = self._get_repo_list(query, repo_type=repo_type) |
|
245 | 245 | if repos: |
|
246 | 246 | res.append({ |
|
247 | 247 | 'text': _('Repositories'), |
|
248 | 248 | 'children': repos |
|
249 | 249 | }) |
|
250 | 250 | |
|
251 | 251 | data = { |
|
252 | 252 | 'more': False, |
|
253 | 253 | 'results': res |
|
254 | 254 | } |
|
255 | 255 | return data |
|
256 | 256 | |
|
257 | 257 | @LoginRequired() |
|
258 | 258 | @XHRRequired() |
|
259 | 259 | @jsonify |
|
260 | 260 | def user_autocomplete_data(self): |
|
261 | 261 | query = request.GET.get('query') |
|
262 | 262 | active = str2bool(request.GET.get('active') or True) |
|
263 | 263 | |
|
264 | 264 | repo_model = RepoModel() |
|
265 | 265 | _users = repo_model.get_users( |
|
266 | 266 | name_contains=query, only_active=active) |
|
267 | 267 | |
|
268 | 268 | if request.GET.get('user_groups'): |
|
269 | 269 | # extend with user groups |
|
270 | 270 | _user_groups = repo_model.get_user_groups( |
|
271 | 271 | name_contains=query, only_active=active) |
|
272 | 272 | _users = _users + _user_groups |
|
273 | 273 | |
|
274 | 274 | return {'suggestions': _users} |
|
275 | 275 | |
|
276 | 276 | @LoginRequired() |
|
277 | 277 | @XHRRequired() |
|
278 | 278 | @jsonify |
|
279 | 279 | def user_group_autocomplete_data(self): |
|
280 | 280 | query = request.GET.get('query') |
|
281 | 281 | active = str2bool(request.GET.get('active') or True) |
|
282 | 282 | |
|
283 | 283 | repo_model = RepoModel() |
|
284 | 284 | _user_groups = repo_model.get_user_groups( |
|
285 | 285 | name_contains=query, only_active=active) |
|
286 | 286 | _user_groups = _user_groups |
|
287 | 287 | |
|
288 | 288 | return {'suggestions': _user_groups} |
@@ -1,306 +1,306 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 | Journal / user event log controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | from itertools import groupby |
|
27 | 27 | |
|
28 | 28 | from sqlalchemy import or_ |
|
29 | 29 | from sqlalchemy.orm import joinedload |
|
30 | 30 | |
|
31 | 31 | from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed |
|
32 | 32 | |
|
33 | 33 | from webob.exc import HTTPBadRequest |
|
34 | 34 | from pylons import request, tmpl_context as c, response, url |
|
35 | 35 | from pylons.i18n.translation import _ |
|
36 | 36 | |
|
37 | 37 | from rhodecode.controllers.admin.admin import _journal_filter |
|
38 | 38 | from rhodecode.model.db import UserLog, UserFollowing, User |
|
39 | 39 | from rhodecode.model.meta import Session |
|
40 | 40 | import rhodecode.lib.helpers as h |
|
41 | 41 | from rhodecode.lib.helpers import Page |
|
42 | 42 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired |
|
43 | 43 | from rhodecode.lib.base import BaseController, render |
|
44 | 44 | from rhodecode.lib.utils2 import safe_int, AttributeDict |
|
45 | 45 | |
|
46 | 46 | log = logging.getLogger(__name__) |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | class JournalController(BaseController): |
|
50 | 50 | |
|
51 | 51 | def __before__(self): |
|
52 | 52 | super(JournalController, self).__before__() |
|
53 | 53 | self.language = 'en-us' |
|
54 | 54 | self.ttl = "5" |
|
55 | 55 | self.feed_nr = 20 |
|
56 | 56 | c.search_term = request.GET.get('filter') |
|
57 | 57 | |
|
58 | 58 | def _get_daily_aggregate(self, journal): |
|
59 | 59 | groups = [] |
|
60 | 60 | for k, g in groupby(journal, lambda x: x.action_as_day): |
|
61 | 61 | user_group = [] |
|
62 | 62 | #groupby username if it's a present value, else fallback to journal username |
|
63 | 63 | for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username): |
|
64 | 64 | l = list(g2) |
|
65 | 65 | user_group.append((l[0].user, l)) |
|
66 | 66 | |
|
67 | 67 | groups.append((k, user_group,)) |
|
68 | 68 | |
|
69 | 69 | return groups |
|
70 | 70 | |
|
71 | 71 | def _get_journal_data(self, following_repos): |
|
72 | 72 | repo_ids = [x.follows_repository.repo_id for x in following_repos |
|
73 | 73 | if x.follows_repository is not None] |
|
74 | 74 | user_ids = [x.follows_user.user_id for x in following_repos |
|
75 | 75 | if x.follows_user is not None] |
|
76 | 76 | |
|
77 | 77 | filtering_criterion = None |
|
78 | 78 | |
|
79 | 79 | if repo_ids and user_ids: |
|
80 | 80 | filtering_criterion = or_(UserLog.repository_id.in_(repo_ids), |
|
81 | 81 | UserLog.user_id.in_(user_ids)) |
|
82 | 82 | if repo_ids and not user_ids: |
|
83 | 83 | filtering_criterion = UserLog.repository_id.in_(repo_ids) |
|
84 | 84 | if not repo_ids and user_ids: |
|
85 | 85 | filtering_criterion = UserLog.user_id.in_(user_ids) |
|
86 | 86 | if filtering_criterion is not None: |
|
87 | 87 | journal = self.sa.query(UserLog)\ |
|
88 | 88 | .options(joinedload(UserLog.user))\ |
|
89 | 89 | .options(joinedload(UserLog.repository)) |
|
90 | 90 | #filter |
|
91 | 91 | try: |
|
92 | 92 | journal = _journal_filter(journal, c.search_term) |
|
93 | 93 | except Exception: |
|
94 | 94 | # we want this to crash for now |
|
95 | 95 | raise |
|
96 | 96 | journal = journal.filter(filtering_criterion)\ |
|
97 | 97 | .order_by(UserLog.action_date.desc()) |
|
98 | 98 | else: |
|
99 | 99 | journal = [] |
|
100 | 100 | |
|
101 | 101 | return journal |
|
102 | 102 | |
|
103 | 103 | def _atom_feed(self, repos, public=True): |
|
104 | 104 | journal = self._get_journal_data(repos) |
|
105 | 105 | if public: |
|
106 | 106 | _link = url('public_journal_atom', qualified=True) |
|
107 | 107 | _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'), |
|
108 | 108 | 'atom feed') |
|
109 | 109 | else: |
|
110 | 110 | _link = url('journal_atom', qualified=True) |
|
111 | 111 | _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed') |
|
112 | 112 | |
|
113 | 113 | feed = Atom1Feed(title=_desc, |
|
114 | 114 | link=_link, |
|
115 | 115 | description=_desc, |
|
116 | 116 | language=self.language, |
|
117 | 117 | ttl=self.ttl) |
|
118 | 118 | |
|
119 | 119 | for entry in journal[:self.feed_nr]: |
|
120 | 120 | user = entry.user |
|
121 | 121 | if user is None: |
|
122 | 122 | #fix deleted users |
|
123 | 123 | user = AttributeDict({'short_contact': entry.username, |
|
124 | 124 | 'email': '', |
|
125 | 125 | 'full_contact': ''}) |
|
126 | 126 | action, action_extra, ico = h.action_parser(entry, feed=True) |
|
127 | 127 | title = "%s - %s %s" % (user.short_contact, action(), |
|
128 | 128 | entry.repository.repo_name) |
|
129 | 129 | desc = action_extra() |
|
130 | 130 | _url = None |
|
131 | 131 | if entry.repository is not None: |
|
132 | 132 | _url = url('changelog_home', |
|
133 | 133 | repo_name=entry.repository.repo_name, |
|
134 | 134 | qualified=True) |
|
135 | 135 | |
|
136 | 136 | feed.add_item(title=title, |
|
137 | 137 | pubdate=entry.action_date, |
|
138 | 138 | link=_url or url('', qualified=True), |
|
139 | 139 | author_email=user.email, |
|
140 | 140 | author_name=user.full_contact, |
|
141 | 141 | description=desc) |
|
142 | 142 | |
|
143 | 143 | response.content_type = feed.mime_type |
|
144 | 144 | return feed.writeString('utf-8') |
|
145 | 145 | |
|
146 | 146 | def _rss_feed(self, repos, public=True): |
|
147 | 147 | journal = self._get_journal_data(repos) |
|
148 | 148 | if public: |
|
149 | 149 | _link = url('public_journal_atom', qualified=True) |
|
150 | 150 | _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'), |
|
151 | 151 | 'rss feed') |
|
152 | 152 | else: |
|
153 | 153 | _link = url('journal_atom', qualified=True) |
|
154 | 154 | _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed') |
|
155 | 155 | |
|
156 | 156 | feed = Rss201rev2Feed(title=_desc, |
|
157 | 157 | link=_link, |
|
158 | 158 | description=_desc, |
|
159 | 159 | language=self.language, |
|
160 | 160 | ttl=self.ttl) |
|
161 | 161 | |
|
162 | 162 | for entry in journal[:self.feed_nr]: |
|
163 | 163 | user = entry.user |
|
164 | 164 | if user is None: |
|
165 | 165 | #fix deleted users |
|
166 | 166 | user = AttributeDict({'short_contact': entry.username, |
|
167 | 167 | 'email': '', |
|
168 | 168 | 'full_contact': ''}) |
|
169 | 169 | action, action_extra, ico = h.action_parser(entry, feed=True) |
|
170 | 170 | title = "%s - %s %s" % (user.short_contact, action(), |
|
171 | 171 | entry.repository.repo_name) |
|
172 | 172 | desc = action_extra() |
|
173 | 173 | _url = None |
|
174 | 174 | if entry.repository is not None: |
|
175 | 175 | _url = url('changelog_home', |
|
176 | 176 | repo_name=entry.repository.repo_name, |
|
177 | 177 | qualified=True) |
|
178 | 178 | |
|
179 | 179 | feed.add_item(title=title, |
|
180 | 180 | pubdate=entry.action_date, |
|
181 | 181 | link=_url or url('', qualified=True), |
|
182 | 182 | author_email=user.email, |
|
183 | 183 | author_name=user.full_contact, |
|
184 | 184 | description=desc) |
|
185 | 185 | |
|
186 | 186 | response.content_type = feed.mime_type |
|
187 | 187 | return feed.writeString('utf-8') |
|
188 | 188 | |
|
189 | 189 | @LoginRequired() |
|
190 | 190 | @NotAnonymous() |
|
191 | 191 | def index(self): |
|
192 | 192 | # Return a rendered template |
|
193 | 193 | p = safe_int(request.GET.get('page', 1), 1) |
|
194 | 194 | c.user = User.get(c.rhodecode_user.user_id) |
|
195 | 195 | following = self.sa.query(UserFollowing)\ |
|
196 | 196 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
197 | 197 | .options(joinedload(UserFollowing.follows_repository))\ |
|
198 | 198 | .all() |
|
199 | 199 | |
|
200 | 200 | journal = self._get_journal_data(following) |
|
201 | 201 | |
|
202 | 202 | def url_generator(**kw): |
|
203 | 203 | return url.current(filter=c.search_term, **kw) |
|
204 | 204 | |
|
205 | 205 | c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator) |
|
206 | 206 | c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager) |
|
207 | 207 | |
|
208 |
c.journal_data = render('journal/journal_data. |
|
|
208 | c.journal_data = render('journal/journal_data.mako') | |
|
209 | 209 | if request.is_xhr: |
|
210 | 210 | return c.journal_data |
|
211 | 211 | |
|
212 |
return render('journal/journal. |
|
|
212 | return render('journal/journal.mako') | |
|
213 | 213 | |
|
214 | 214 | @LoginRequired(auth_token_access=True) |
|
215 | 215 | @NotAnonymous() |
|
216 | 216 | def journal_atom(self): |
|
217 | 217 | """ |
|
218 | 218 | Produce an atom-1.0 feed via feedgenerator module |
|
219 | 219 | """ |
|
220 | 220 | following = self.sa.query(UserFollowing)\ |
|
221 | 221 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
222 | 222 | .options(joinedload(UserFollowing.follows_repository))\ |
|
223 | 223 | .all() |
|
224 | 224 | return self._atom_feed(following, public=False) |
|
225 | 225 | |
|
226 | 226 | @LoginRequired(auth_token_access=True) |
|
227 | 227 | @NotAnonymous() |
|
228 | 228 | def journal_rss(self): |
|
229 | 229 | """ |
|
230 | 230 | Produce an rss feed via feedgenerator module |
|
231 | 231 | """ |
|
232 | 232 | following = self.sa.query(UserFollowing)\ |
|
233 | 233 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
234 | 234 | .options(joinedload(UserFollowing.follows_repository))\ |
|
235 | 235 | .all() |
|
236 | 236 | return self._rss_feed(following, public=False) |
|
237 | 237 | |
|
238 | 238 | @CSRFRequired() |
|
239 | 239 | @LoginRequired() |
|
240 | 240 | @NotAnonymous() |
|
241 | 241 | def toggle_following(self): |
|
242 | 242 | user_id = request.POST.get('follows_user_id') |
|
243 | 243 | if user_id: |
|
244 | 244 | try: |
|
245 | 245 | self.scm_model.toggle_following_user( |
|
246 | 246 | user_id, c.rhodecode_user.user_id) |
|
247 | 247 | Session().commit() |
|
248 | 248 | return 'ok' |
|
249 | 249 | except Exception: |
|
250 | 250 | raise HTTPBadRequest() |
|
251 | 251 | |
|
252 | 252 | repo_id = request.POST.get('follows_repo_id') |
|
253 | 253 | if repo_id: |
|
254 | 254 | try: |
|
255 | 255 | self.scm_model.toggle_following_repo( |
|
256 | 256 | repo_id, c.rhodecode_user.user_id) |
|
257 | 257 | Session().commit() |
|
258 | 258 | return 'ok' |
|
259 | 259 | except Exception: |
|
260 | 260 | raise HTTPBadRequest() |
|
261 | 261 | |
|
262 | 262 | |
|
263 | 263 | @LoginRequired() |
|
264 | 264 | def public_journal(self): |
|
265 | 265 | # Return a rendered template |
|
266 | 266 | p = safe_int(request.GET.get('page', 1), 1) |
|
267 | 267 | |
|
268 | 268 | c.following = self.sa.query(UserFollowing)\ |
|
269 | 269 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
270 | 270 | .options(joinedload(UserFollowing.follows_repository))\ |
|
271 | 271 | .all() |
|
272 | 272 | |
|
273 | 273 | journal = self._get_journal_data(c.following) |
|
274 | 274 | |
|
275 | 275 | c.journal_pager = Page(journal, page=p, items_per_page=20) |
|
276 | 276 | |
|
277 | 277 | c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager) |
|
278 | 278 | |
|
279 |
c.journal_data = render('journal/journal_data. |
|
|
279 | c.journal_data = render('journal/journal_data.mako') | |
|
280 | 280 | if request.is_xhr: |
|
281 | 281 | return c.journal_data |
|
282 |
return render('journal/public_journal. |
|
|
282 | return render('journal/public_journal.mako') | |
|
283 | 283 | |
|
284 | 284 | @LoginRequired(auth_token_access=True) |
|
285 | 285 | def public_journal_atom(self): |
|
286 | 286 | """ |
|
287 | 287 | Produce an atom-1.0 feed via feedgenerator module |
|
288 | 288 | """ |
|
289 | 289 | c.following = self.sa.query(UserFollowing)\ |
|
290 | 290 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
291 | 291 | .options(joinedload(UserFollowing.follows_repository))\ |
|
292 | 292 | .all() |
|
293 | 293 | |
|
294 | 294 | return self._atom_feed(c.following) |
|
295 | 295 | |
|
296 | 296 | @LoginRequired(auth_token_access=True) |
|
297 | 297 | def public_journal_rss(self): |
|
298 | 298 | """ |
|
299 | 299 | Produce an rss2 feed via feedgenerator module |
|
300 | 300 | """ |
|
301 | 301 | c.following = self.sa.query(UserFollowing)\ |
|
302 | 302 | .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ |
|
303 | 303 | .options(joinedload(UserFollowing.follows_repository))\ |
|
304 | 304 | .all() |
|
305 | 305 | |
|
306 | 306 | return self._rss_feed(c.following) |
@@ -1,1020 +1,1020 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | pull requests controller for rhodecode for initializing pull requests |
|
23 | 23 | """ |
|
24 | 24 | import types |
|
25 | 25 | |
|
26 | 26 | import peppercorn |
|
27 | 27 | import formencode |
|
28 | 28 | import logging |
|
29 | 29 | import collections |
|
30 | 30 | |
|
31 | 31 | from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest |
|
32 | 32 | from pylons import request, tmpl_context as c, url |
|
33 | 33 | from pylons.controllers.util import redirect |
|
34 | 34 | from pylons.i18n.translation import _ |
|
35 | 35 | from pyramid.threadlocal import get_current_registry |
|
36 | 36 | from sqlalchemy.sql import func |
|
37 | 37 | from sqlalchemy.sql.expression import or_ |
|
38 | 38 | |
|
39 | 39 | from rhodecode import events |
|
40 | 40 | from rhodecode.lib import auth, diffs, helpers as h, codeblocks |
|
41 | 41 | from rhodecode.lib.ext_json import json |
|
42 | 42 | from rhodecode.lib.base import ( |
|
43 | 43 | BaseRepoController, render, vcs_operation_context) |
|
44 | 44 | from rhodecode.lib.auth import ( |
|
45 | 45 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, |
|
46 | 46 | HasAcceptedRepoType, XHRRequired) |
|
47 | 47 | from rhodecode.lib.channelstream import channelstream_request |
|
48 | 48 | from rhodecode.lib.utils import jsonify |
|
49 | 49 | from rhodecode.lib.utils2 import ( |
|
50 | 50 | safe_int, safe_str, str2bool, safe_unicode) |
|
51 | 51 | from rhodecode.lib.vcs.backends.base import ( |
|
52 | 52 | EmptyCommit, UpdateFailureReason, EmptyRepository) |
|
53 | 53 | from rhodecode.lib.vcs.exceptions import ( |
|
54 | 54 | EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError, |
|
55 | 55 | NodeDoesNotExistError) |
|
56 | 56 | |
|
57 | 57 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
58 | 58 | from rhodecode.model.comment import ChangesetCommentsModel |
|
59 | 59 | from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment, |
|
60 | 60 | Repository, PullRequestVersion) |
|
61 | 61 | from rhodecode.model.forms import PullRequestForm |
|
62 | 62 | from rhodecode.model.meta import Session |
|
63 | 63 | from rhodecode.model.pull_request import PullRequestModel |
|
64 | 64 | |
|
65 | 65 | log = logging.getLogger(__name__) |
|
66 | 66 | |
|
67 | 67 | |
|
68 | 68 | class PullrequestsController(BaseRepoController): |
|
69 | 69 | def __before__(self): |
|
70 | 70 | super(PullrequestsController, self).__before__() |
|
71 | 71 | |
|
72 | 72 | def _load_compare_data(self, pull_request, inline_comments): |
|
73 | 73 | """ |
|
74 | 74 | Load context data needed for generating compare diff |
|
75 | 75 | |
|
76 | 76 | :param pull_request: object related to the request |
|
77 | 77 | :param enable_comments: flag to determine if comments are included |
|
78 | 78 | """ |
|
79 | 79 | source_repo = pull_request.source_repo |
|
80 | 80 | source_ref_id = pull_request.source_ref_parts.commit_id |
|
81 | 81 | |
|
82 | 82 | target_repo = pull_request.target_repo |
|
83 | 83 | target_ref_id = pull_request.target_ref_parts.commit_id |
|
84 | 84 | |
|
85 | 85 | # despite opening commits for bookmarks/branches/tags, we always |
|
86 | 86 | # convert this to rev to prevent changes after bookmark or branch change |
|
87 | 87 | c.source_ref_type = 'rev' |
|
88 | 88 | c.source_ref = source_ref_id |
|
89 | 89 | |
|
90 | 90 | c.target_ref_type = 'rev' |
|
91 | 91 | c.target_ref = target_ref_id |
|
92 | 92 | |
|
93 | 93 | c.source_repo = source_repo |
|
94 | 94 | c.target_repo = target_repo |
|
95 | 95 | |
|
96 | 96 | c.fulldiff = bool(request.GET.get('fulldiff')) |
|
97 | 97 | |
|
98 | 98 | # diff_limit is the old behavior, will cut off the whole diff |
|
99 | 99 | # if the limit is applied otherwise will just hide the |
|
100 | 100 | # big files from the front-end |
|
101 | 101 | diff_limit = self.cut_off_limit_diff |
|
102 | 102 | file_limit = self.cut_off_limit_file |
|
103 | 103 | |
|
104 | 104 | pre_load = ["author", "branch", "date", "message"] |
|
105 | 105 | |
|
106 | 106 | c.commit_ranges = [] |
|
107 | 107 | source_commit = EmptyCommit() |
|
108 | 108 | target_commit = EmptyCommit() |
|
109 | 109 | c.missing_requirements = False |
|
110 | 110 | try: |
|
111 | 111 | c.commit_ranges = [ |
|
112 | 112 | source_repo.get_commit(commit_id=rev, pre_load=pre_load) |
|
113 | 113 | for rev in pull_request.revisions] |
|
114 | 114 | |
|
115 | 115 | c.statuses = source_repo.statuses( |
|
116 | 116 | [x.raw_id for x in c.commit_ranges]) |
|
117 | 117 | |
|
118 | 118 | target_commit = source_repo.get_commit( |
|
119 | 119 | commit_id=safe_str(target_ref_id)) |
|
120 | 120 | source_commit = source_repo.get_commit( |
|
121 | 121 | commit_id=safe_str(source_ref_id)) |
|
122 | 122 | except RepositoryRequirementError: |
|
123 | 123 | c.missing_requirements = True |
|
124 | 124 | |
|
125 | 125 | # auto collapse if we have more than limit |
|
126 | 126 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
127 | 127 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
128 | 128 | |
|
129 | 129 | c.changes = {} |
|
130 | 130 | c.missing_commits = False |
|
131 | 131 | if (c.missing_requirements or |
|
132 | 132 | isinstance(source_commit, EmptyCommit) or |
|
133 | 133 | source_commit == target_commit): |
|
134 | 134 | _parsed = [] |
|
135 | 135 | c.missing_commits = True |
|
136 | 136 | else: |
|
137 | 137 | vcs_diff = PullRequestModel().get_diff(pull_request) |
|
138 | 138 | diff_processor = diffs.DiffProcessor( |
|
139 | 139 | vcs_diff, format='newdiff', diff_limit=diff_limit, |
|
140 | 140 | file_limit=file_limit, show_full_diff=c.fulldiff) |
|
141 | 141 | |
|
142 | 142 | _parsed = diff_processor.prepare() |
|
143 | 143 | c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer) |
|
144 | 144 | |
|
145 | 145 | included_files = {} |
|
146 | 146 | for f in _parsed: |
|
147 | 147 | included_files[f['filename']] = f['stats'] |
|
148 | 148 | |
|
149 | 149 | c.deleted_files = [fname for fname in inline_comments if |
|
150 | 150 | fname not in included_files] |
|
151 | 151 | |
|
152 | 152 | c.deleted_files_comments = collections.defaultdict(dict) |
|
153 | 153 | for fname, per_line_comments in inline_comments.items(): |
|
154 | 154 | if fname in c.deleted_files: |
|
155 | 155 | c.deleted_files_comments[fname]['stats'] = 0 |
|
156 | 156 | c.deleted_files_comments[fname]['comments'] = list() |
|
157 | 157 | for lno, comments in per_line_comments.items(): |
|
158 | 158 | c.deleted_files_comments[fname]['comments'].extend(comments) |
|
159 | 159 | |
|
160 | 160 | def _node_getter(commit): |
|
161 | 161 | def get_node(fname): |
|
162 | 162 | try: |
|
163 | 163 | return commit.get_node(fname) |
|
164 | 164 | except NodeDoesNotExistError: |
|
165 | 165 | return None |
|
166 | 166 | return get_node |
|
167 | 167 | |
|
168 | 168 | c.diffset = codeblocks.DiffSet( |
|
169 | 169 | repo_name=c.repo_name, |
|
170 | 170 | source_repo_name=c.source_repo.repo_name, |
|
171 | 171 | source_node_getter=_node_getter(target_commit), |
|
172 | 172 | target_node_getter=_node_getter(source_commit), |
|
173 | 173 | comments=inline_comments |
|
174 | 174 | ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id) |
|
175 | 175 | |
|
176 | 176 | def _extract_ordering(self, request): |
|
177 | 177 | column_index = safe_int(request.GET.get('order[0][column]')) |
|
178 | 178 | order_dir = request.GET.get('order[0][dir]', 'desc') |
|
179 | 179 | order_by = request.GET.get( |
|
180 | 180 | 'columns[%s][data][sort]' % column_index, 'name_raw') |
|
181 | 181 | return order_by, order_dir |
|
182 | 182 | |
|
183 | 183 | @LoginRequired() |
|
184 | 184 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
185 | 185 | 'repository.admin') |
|
186 | 186 | @HasAcceptedRepoType('git', 'hg') |
|
187 | 187 | def show_all(self, repo_name): |
|
188 | 188 | # filter types |
|
189 | 189 | c.active = 'open' |
|
190 | 190 | c.source = str2bool(request.GET.get('source')) |
|
191 | 191 | c.closed = str2bool(request.GET.get('closed')) |
|
192 | 192 | c.my = str2bool(request.GET.get('my')) |
|
193 | 193 | c.awaiting_review = str2bool(request.GET.get('awaiting_review')) |
|
194 | 194 | c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review')) |
|
195 | 195 | c.repo_name = repo_name |
|
196 | 196 | |
|
197 | 197 | opened_by = None |
|
198 | 198 | if c.my: |
|
199 | 199 | c.active = 'my' |
|
200 | 200 | opened_by = [c.rhodecode_user.user_id] |
|
201 | 201 | |
|
202 | 202 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
203 | 203 | if c.closed: |
|
204 | 204 | c.active = 'closed' |
|
205 | 205 | statuses = [PullRequest.STATUS_CLOSED] |
|
206 | 206 | |
|
207 | 207 | if c.awaiting_review and not c.source: |
|
208 | 208 | c.active = 'awaiting' |
|
209 | 209 | if c.source and not c.awaiting_review: |
|
210 | 210 | c.active = 'source' |
|
211 | 211 | if c.awaiting_my_review: |
|
212 | 212 | c.active = 'awaiting_my' |
|
213 | 213 | |
|
214 | 214 | data = self._get_pull_requests_list( |
|
215 | 215 | repo_name=repo_name, opened_by=opened_by, statuses=statuses) |
|
216 | 216 | if not request.is_xhr: |
|
217 | 217 | c.data = json.dumps(data['data']) |
|
218 | 218 | c.records_total = data['recordsTotal'] |
|
219 |
return render('/pullrequests/pullrequests. |
|
|
219 | return render('/pullrequests/pullrequests.mako') | |
|
220 | 220 | else: |
|
221 | 221 | return json.dumps(data) |
|
222 | 222 | |
|
223 | 223 | def _get_pull_requests_list(self, repo_name, opened_by, statuses): |
|
224 | 224 | # pagination |
|
225 | 225 | start = safe_int(request.GET.get('start'), 0) |
|
226 | 226 | length = safe_int(request.GET.get('length'), c.visual.dashboard_items) |
|
227 | 227 | order_by, order_dir = self._extract_ordering(request) |
|
228 | 228 | |
|
229 | 229 | if c.awaiting_review: |
|
230 | 230 | pull_requests = PullRequestModel().get_awaiting_review( |
|
231 | 231 | repo_name, source=c.source, opened_by=opened_by, |
|
232 | 232 | statuses=statuses, offset=start, length=length, |
|
233 | 233 | order_by=order_by, order_dir=order_dir) |
|
234 | 234 | pull_requests_total_count = PullRequestModel( |
|
235 | 235 | ).count_awaiting_review( |
|
236 | 236 | repo_name, source=c.source, statuses=statuses, |
|
237 | 237 | opened_by=opened_by) |
|
238 | 238 | elif c.awaiting_my_review: |
|
239 | 239 | pull_requests = PullRequestModel().get_awaiting_my_review( |
|
240 | 240 | repo_name, source=c.source, opened_by=opened_by, |
|
241 | 241 | user_id=c.rhodecode_user.user_id, statuses=statuses, |
|
242 | 242 | offset=start, length=length, order_by=order_by, |
|
243 | 243 | order_dir=order_dir) |
|
244 | 244 | pull_requests_total_count = PullRequestModel( |
|
245 | 245 | ).count_awaiting_my_review( |
|
246 | 246 | repo_name, source=c.source, user_id=c.rhodecode_user.user_id, |
|
247 | 247 | statuses=statuses, opened_by=opened_by) |
|
248 | 248 | else: |
|
249 | 249 | pull_requests = PullRequestModel().get_all( |
|
250 | 250 | repo_name, source=c.source, opened_by=opened_by, |
|
251 | 251 | statuses=statuses, offset=start, length=length, |
|
252 | 252 | order_by=order_by, order_dir=order_dir) |
|
253 | 253 | pull_requests_total_count = PullRequestModel().count_all( |
|
254 | 254 | repo_name, source=c.source, statuses=statuses, |
|
255 | 255 | opened_by=opened_by) |
|
256 | 256 | |
|
257 | 257 | from rhodecode.lib.utils import PartialRenderer |
|
258 |
_render = PartialRenderer('data_table/_dt_elements. |
|
|
258 | _render = PartialRenderer('data_table/_dt_elements.mako') | |
|
259 | 259 | data = [] |
|
260 | 260 | for pr in pull_requests: |
|
261 | 261 | comments = ChangesetCommentsModel().get_all_comments( |
|
262 | 262 | c.rhodecode_db_repo.repo_id, pull_request=pr) |
|
263 | 263 | |
|
264 | 264 | data.append({ |
|
265 | 265 | 'name': _render('pullrequest_name', |
|
266 | 266 | pr.pull_request_id, pr.target_repo.repo_name), |
|
267 | 267 | 'name_raw': pr.pull_request_id, |
|
268 | 268 | 'status': _render('pullrequest_status', |
|
269 | 269 | pr.calculated_review_status()), |
|
270 | 270 | 'title': _render( |
|
271 | 271 | 'pullrequest_title', pr.title, pr.description), |
|
272 | 272 | 'description': h.escape(pr.description), |
|
273 | 273 | 'updated_on': _render('pullrequest_updated_on', |
|
274 | 274 | h.datetime_to_time(pr.updated_on)), |
|
275 | 275 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), |
|
276 | 276 | 'created_on': _render('pullrequest_updated_on', |
|
277 | 277 | h.datetime_to_time(pr.created_on)), |
|
278 | 278 | 'created_on_raw': h.datetime_to_time(pr.created_on), |
|
279 | 279 | 'author': _render('pullrequest_author', |
|
280 | 280 | pr.author.full_contact, ), |
|
281 | 281 | 'author_raw': pr.author.full_name, |
|
282 | 282 | 'comments': _render('pullrequest_comments', len(comments)), |
|
283 | 283 | 'comments_raw': len(comments), |
|
284 | 284 | 'closed': pr.is_closed(), |
|
285 | 285 | }) |
|
286 | 286 | # json used to render the grid |
|
287 | 287 | data = ({ |
|
288 | 288 | 'data': data, |
|
289 | 289 | 'recordsTotal': pull_requests_total_count, |
|
290 | 290 | 'recordsFiltered': pull_requests_total_count, |
|
291 | 291 | }) |
|
292 | 292 | return data |
|
293 | 293 | |
|
294 | 294 | @LoginRequired() |
|
295 | 295 | @NotAnonymous() |
|
296 | 296 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
297 | 297 | 'repository.admin') |
|
298 | 298 | @HasAcceptedRepoType('git', 'hg') |
|
299 | 299 | def index(self): |
|
300 | 300 | source_repo = c.rhodecode_db_repo |
|
301 | 301 | |
|
302 | 302 | try: |
|
303 | 303 | source_repo.scm_instance().get_commit() |
|
304 | 304 | except EmptyRepositoryError: |
|
305 | 305 | h.flash(h.literal(_('There are no commits yet')), |
|
306 | 306 | category='warning') |
|
307 | 307 | redirect(url('summary_home', repo_name=source_repo.repo_name)) |
|
308 | 308 | |
|
309 | 309 | commit_id = request.GET.get('commit') |
|
310 | 310 | branch_ref = request.GET.get('branch') |
|
311 | 311 | bookmark_ref = request.GET.get('bookmark') |
|
312 | 312 | |
|
313 | 313 | try: |
|
314 | 314 | source_repo_data = PullRequestModel().generate_repo_data( |
|
315 | 315 | source_repo, commit_id=commit_id, |
|
316 | 316 | branch=branch_ref, bookmark=bookmark_ref) |
|
317 | 317 | except CommitDoesNotExistError as e: |
|
318 | 318 | log.exception(e) |
|
319 | 319 | h.flash(_('Commit does not exist'), 'error') |
|
320 | 320 | redirect(url('pullrequest_home', repo_name=source_repo.repo_name)) |
|
321 | 321 | |
|
322 | 322 | default_target_repo = source_repo |
|
323 | 323 | |
|
324 | 324 | if source_repo.parent: |
|
325 | 325 | parent_vcs_obj = source_repo.parent.scm_instance() |
|
326 | 326 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
327 | 327 | # change default if we have a parent repo |
|
328 | 328 | default_target_repo = source_repo.parent |
|
329 | 329 | |
|
330 | 330 | target_repo_data = PullRequestModel().generate_repo_data( |
|
331 | 331 | default_target_repo) |
|
332 | 332 | |
|
333 | 333 | selected_source_ref = source_repo_data['refs']['selected_ref'] |
|
334 | 334 | |
|
335 | 335 | title_source_ref = selected_source_ref.split(':', 2)[1] |
|
336 | 336 | c.default_title = PullRequestModel().generate_pullrequest_title( |
|
337 | 337 | source=source_repo.repo_name, |
|
338 | 338 | source_ref=title_source_ref, |
|
339 | 339 | target=default_target_repo.repo_name |
|
340 | 340 | ) |
|
341 | 341 | |
|
342 | 342 | c.default_repo_data = { |
|
343 | 343 | 'source_repo_name': source_repo.repo_name, |
|
344 | 344 | 'source_refs_json': json.dumps(source_repo_data), |
|
345 | 345 | 'target_repo_name': default_target_repo.repo_name, |
|
346 | 346 | 'target_refs_json': json.dumps(target_repo_data), |
|
347 | 347 | } |
|
348 | 348 | c.default_source_ref = selected_source_ref |
|
349 | 349 | |
|
350 |
return render('/pullrequests/pullrequest. |
|
|
350 | return render('/pullrequests/pullrequest.mako') | |
|
351 | 351 | |
|
352 | 352 | @LoginRequired() |
|
353 | 353 | @NotAnonymous() |
|
354 | 354 | @XHRRequired() |
|
355 | 355 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
356 | 356 | 'repository.admin') |
|
357 | 357 | @jsonify |
|
358 | 358 | def get_repo_refs(self, repo_name, target_repo_name): |
|
359 | 359 | repo = Repository.get_by_repo_name(target_repo_name) |
|
360 | 360 | if not repo: |
|
361 | 361 | raise HTTPNotFound |
|
362 | 362 | return PullRequestModel().generate_repo_data(repo) |
|
363 | 363 | |
|
364 | 364 | @LoginRequired() |
|
365 | 365 | @NotAnonymous() |
|
366 | 366 | @XHRRequired() |
|
367 | 367 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
368 | 368 | 'repository.admin') |
|
369 | 369 | @jsonify |
|
370 | 370 | def get_repo_destinations(self, repo_name): |
|
371 | 371 | repo = Repository.get_by_repo_name(repo_name) |
|
372 | 372 | if not repo: |
|
373 | 373 | raise HTTPNotFound |
|
374 | 374 | filter_query = request.GET.get('query') |
|
375 | 375 | |
|
376 | 376 | query = Repository.query() \ |
|
377 | 377 | .order_by(func.length(Repository.repo_name)) \ |
|
378 | 378 | .filter(or_( |
|
379 | 379 | Repository.repo_name == repo.repo_name, |
|
380 | 380 | Repository.fork_id == repo.repo_id)) |
|
381 | 381 | |
|
382 | 382 | if filter_query: |
|
383 | 383 | ilike_expression = u'%{}%'.format(safe_unicode(filter_query)) |
|
384 | 384 | query = query.filter( |
|
385 | 385 | Repository.repo_name.ilike(ilike_expression)) |
|
386 | 386 | |
|
387 | 387 | add_parent = False |
|
388 | 388 | if repo.parent: |
|
389 | 389 | if filter_query in repo.parent.repo_name: |
|
390 | 390 | parent_vcs_obj = repo.parent.scm_instance() |
|
391 | 391 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
392 | 392 | add_parent = True |
|
393 | 393 | |
|
394 | 394 | limit = 20 - 1 if add_parent else 20 |
|
395 | 395 | all_repos = query.limit(limit).all() |
|
396 | 396 | if add_parent: |
|
397 | 397 | all_repos += [repo.parent] |
|
398 | 398 | |
|
399 | 399 | repos = [] |
|
400 | 400 | for obj in self.scm_model.get_repos(all_repos): |
|
401 | 401 | repos.append({ |
|
402 | 402 | 'id': obj['name'], |
|
403 | 403 | 'text': obj['name'], |
|
404 | 404 | 'type': 'repo', |
|
405 | 405 | 'obj': obj['dbrepo'] |
|
406 | 406 | }) |
|
407 | 407 | |
|
408 | 408 | data = { |
|
409 | 409 | 'more': False, |
|
410 | 410 | 'results': [{ |
|
411 | 411 | 'text': _('Repositories'), |
|
412 | 412 | 'children': repos |
|
413 | 413 | }] if repos else [] |
|
414 | 414 | } |
|
415 | 415 | return data |
|
416 | 416 | |
|
417 | 417 | @LoginRequired() |
|
418 | 418 | @NotAnonymous() |
|
419 | 419 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
420 | 420 | 'repository.admin') |
|
421 | 421 | @HasAcceptedRepoType('git', 'hg') |
|
422 | 422 | @auth.CSRFRequired() |
|
423 | 423 | def create(self, repo_name): |
|
424 | 424 | repo = Repository.get_by_repo_name(repo_name) |
|
425 | 425 | if not repo: |
|
426 | 426 | raise HTTPNotFound |
|
427 | 427 | |
|
428 | 428 | controls = peppercorn.parse(request.POST.items()) |
|
429 | 429 | |
|
430 | 430 | try: |
|
431 | 431 | _form = PullRequestForm(repo.repo_id)().to_python(controls) |
|
432 | 432 | except formencode.Invalid as errors: |
|
433 | 433 | if errors.error_dict.get('revisions'): |
|
434 | 434 | msg = 'Revisions: %s' % errors.error_dict['revisions'] |
|
435 | 435 | elif errors.error_dict.get('pullrequest_title'): |
|
436 | 436 | msg = _('Pull request requires a title with min. 3 chars') |
|
437 | 437 | else: |
|
438 | 438 | msg = _('Error creating pull request: {}').format(errors) |
|
439 | 439 | log.exception(msg) |
|
440 | 440 | h.flash(msg, 'error') |
|
441 | 441 | |
|
442 | 442 | # would rather just go back to form ... |
|
443 | 443 | return redirect(url('pullrequest_home', repo_name=repo_name)) |
|
444 | 444 | |
|
445 | 445 | source_repo = _form['source_repo'] |
|
446 | 446 | source_ref = _form['source_ref'] |
|
447 | 447 | target_repo = _form['target_repo'] |
|
448 | 448 | target_ref = _form['target_ref'] |
|
449 | 449 | commit_ids = _form['revisions'][::-1] |
|
450 | 450 | reviewers = [ |
|
451 | 451 | (r['user_id'], r['reasons']) for r in _form['review_members']] |
|
452 | 452 | |
|
453 | 453 | # find the ancestor for this pr |
|
454 | 454 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) |
|
455 | 455 | target_db_repo = Repository.get_by_repo_name(_form['target_repo']) |
|
456 | 456 | |
|
457 | 457 | source_scm = source_db_repo.scm_instance() |
|
458 | 458 | target_scm = target_db_repo.scm_instance() |
|
459 | 459 | |
|
460 | 460 | source_commit = source_scm.get_commit(source_ref.split(':')[-1]) |
|
461 | 461 | target_commit = target_scm.get_commit(target_ref.split(':')[-1]) |
|
462 | 462 | |
|
463 | 463 | ancestor = source_scm.get_common_ancestor( |
|
464 | 464 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
465 | 465 | |
|
466 | 466 | target_ref_type, target_ref_name, __ = _form['target_ref'].split(':') |
|
467 | 467 | target_ref = ':'.join((target_ref_type, target_ref_name, ancestor)) |
|
468 | 468 | |
|
469 | 469 | pullrequest_title = _form['pullrequest_title'] |
|
470 | 470 | title_source_ref = source_ref.split(':', 2)[1] |
|
471 | 471 | if not pullrequest_title: |
|
472 | 472 | pullrequest_title = PullRequestModel().generate_pullrequest_title( |
|
473 | 473 | source=source_repo, |
|
474 | 474 | source_ref=title_source_ref, |
|
475 | 475 | target=target_repo |
|
476 | 476 | ) |
|
477 | 477 | |
|
478 | 478 | description = _form['pullrequest_desc'] |
|
479 | 479 | try: |
|
480 | 480 | pull_request = PullRequestModel().create( |
|
481 | 481 | c.rhodecode_user.user_id, source_repo, source_ref, target_repo, |
|
482 | 482 | target_ref, commit_ids, reviewers, pullrequest_title, |
|
483 | 483 | description |
|
484 | 484 | ) |
|
485 | 485 | Session().commit() |
|
486 | 486 | h.flash(_('Successfully opened new pull request'), |
|
487 | 487 | category='success') |
|
488 | 488 | except Exception as e: |
|
489 | 489 | msg = _('Error occurred during sending pull request') |
|
490 | 490 | log.exception(msg) |
|
491 | 491 | h.flash(msg, category='error') |
|
492 | 492 | return redirect(url('pullrequest_home', repo_name=repo_name)) |
|
493 | 493 | |
|
494 | 494 | return redirect(url('pullrequest_show', repo_name=target_repo, |
|
495 | 495 | pull_request_id=pull_request.pull_request_id)) |
|
496 | 496 | |
|
497 | 497 | @LoginRequired() |
|
498 | 498 | @NotAnonymous() |
|
499 | 499 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
500 | 500 | 'repository.admin') |
|
501 | 501 | @auth.CSRFRequired() |
|
502 | 502 | @jsonify |
|
503 | 503 | def update(self, repo_name, pull_request_id): |
|
504 | 504 | pull_request_id = safe_int(pull_request_id) |
|
505 | 505 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
506 | 506 | # only owner or admin can update it |
|
507 | 507 | allowed_to_update = PullRequestModel().check_user_update( |
|
508 | 508 | pull_request, c.rhodecode_user) |
|
509 | 509 | if allowed_to_update: |
|
510 | 510 | controls = peppercorn.parse(request.POST.items()) |
|
511 | 511 | |
|
512 | 512 | if 'review_members' in controls: |
|
513 | 513 | self._update_reviewers( |
|
514 | 514 | pull_request_id, controls['review_members']) |
|
515 | 515 | elif str2bool(request.POST.get('update_commits', 'false')): |
|
516 | 516 | self._update_commits(pull_request) |
|
517 | 517 | elif str2bool(request.POST.get('close_pull_request', 'false')): |
|
518 | 518 | self._reject_close(pull_request) |
|
519 | 519 | elif str2bool(request.POST.get('edit_pull_request', 'false')): |
|
520 | 520 | self._edit_pull_request(pull_request) |
|
521 | 521 | else: |
|
522 | 522 | raise HTTPBadRequest() |
|
523 | 523 | return True |
|
524 | 524 | raise HTTPForbidden() |
|
525 | 525 | |
|
526 | 526 | def _edit_pull_request(self, pull_request): |
|
527 | 527 | try: |
|
528 | 528 | PullRequestModel().edit( |
|
529 | 529 | pull_request, request.POST.get('title'), |
|
530 | 530 | request.POST.get('description')) |
|
531 | 531 | except ValueError: |
|
532 | 532 | msg = _(u'Cannot update closed pull requests.') |
|
533 | 533 | h.flash(msg, category='error') |
|
534 | 534 | return |
|
535 | 535 | else: |
|
536 | 536 | Session().commit() |
|
537 | 537 | |
|
538 | 538 | msg = _(u'Pull request title & description updated.') |
|
539 | 539 | h.flash(msg, category='success') |
|
540 | 540 | return |
|
541 | 541 | |
|
542 | 542 | def _update_commits(self, pull_request): |
|
543 | 543 | resp = PullRequestModel().update_commits(pull_request) |
|
544 | 544 | |
|
545 | 545 | if resp.executed: |
|
546 | 546 | msg = _( |
|
547 | 547 | u'Pull request updated to "{source_commit_id}" with ' |
|
548 | 548 | u'{count_added} added, {count_removed} removed commits.') |
|
549 | 549 | msg = msg.format( |
|
550 | 550 | source_commit_id=pull_request.source_ref_parts.commit_id, |
|
551 | 551 | count_added=len(resp.changes.added), |
|
552 | 552 | count_removed=len(resp.changes.removed)) |
|
553 | 553 | h.flash(msg, category='success') |
|
554 | 554 | |
|
555 | 555 | registry = get_current_registry() |
|
556 | 556 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) |
|
557 | 557 | channelstream_config = rhodecode_plugins.get('channelstream', {}) |
|
558 | 558 | if channelstream_config.get('enabled'): |
|
559 | 559 | message = msg + ( |
|
560 | 560 | ' - <a onclick="window.location.reload()">' |
|
561 | 561 | '<strong>{}</strong></a>'.format(_('Reload page'))) |
|
562 | 562 | channel = '/repo${}$/pr/{}'.format( |
|
563 | 563 | pull_request.target_repo.repo_name, |
|
564 | 564 | pull_request.pull_request_id |
|
565 | 565 | ) |
|
566 | 566 | payload = { |
|
567 | 567 | 'type': 'message', |
|
568 | 568 | 'user': 'system', |
|
569 | 569 | 'exclude_users': [request.user.username], |
|
570 | 570 | 'channel': channel, |
|
571 | 571 | 'message': { |
|
572 | 572 | 'message': message, |
|
573 | 573 | 'level': 'success', |
|
574 | 574 | 'topic': '/notifications' |
|
575 | 575 | } |
|
576 | 576 | } |
|
577 | 577 | channelstream_request( |
|
578 | 578 | channelstream_config, [payload], '/message', |
|
579 | 579 | raise_exc=False) |
|
580 | 580 | else: |
|
581 | 581 | msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason] |
|
582 | 582 | warning_reasons = [ |
|
583 | 583 | UpdateFailureReason.NO_CHANGE, |
|
584 | 584 | UpdateFailureReason.WRONG_REF_TPYE, |
|
585 | 585 | ] |
|
586 | 586 | category = 'warning' if resp.reason in warning_reasons else 'error' |
|
587 | 587 | h.flash(msg, category=category) |
|
588 | 588 | |
|
589 | 589 | @auth.CSRFRequired() |
|
590 | 590 | @LoginRequired() |
|
591 | 591 | @NotAnonymous() |
|
592 | 592 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
593 | 593 | 'repository.admin') |
|
594 | 594 | def merge(self, repo_name, pull_request_id): |
|
595 | 595 | """ |
|
596 | 596 | POST /{repo_name}/pull-request/{pull_request_id} |
|
597 | 597 | |
|
598 | 598 | Merge will perform a server-side merge of the specified |
|
599 | 599 | pull request, if the pull request is approved and mergeable. |
|
600 | 600 | After succesfull merging, the pull request is automatically |
|
601 | 601 | closed, with a relevant comment. |
|
602 | 602 | """ |
|
603 | 603 | pull_request_id = safe_int(pull_request_id) |
|
604 | 604 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
605 | 605 | user = c.rhodecode_user |
|
606 | 606 | |
|
607 | 607 | if self._meets_merge_pre_conditions(pull_request, user): |
|
608 | 608 | log.debug("Pre-conditions checked, trying to merge.") |
|
609 | 609 | extras = vcs_operation_context( |
|
610 | 610 | request.environ, repo_name=pull_request.target_repo.repo_name, |
|
611 | 611 | username=user.username, action='push', |
|
612 | 612 | scm=pull_request.target_repo.repo_type) |
|
613 | 613 | self._merge_pull_request(pull_request, user, extras) |
|
614 | 614 | |
|
615 | 615 | return redirect(url( |
|
616 | 616 | 'pullrequest_show', |
|
617 | 617 | repo_name=pull_request.target_repo.repo_name, |
|
618 | 618 | pull_request_id=pull_request.pull_request_id)) |
|
619 | 619 | |
|
620 | 620 | def _meets_merge_pre_conditions(self, pull_request, user): |
|
621 | 621 | if not PullRequestModel().check_user_merge(pull_request, user): |
|
622 | 622 | raise HTTPForbidden() |
|
623 | 623 | |
|
624 | 624 | merge_status, msg = PullRequestModel().merge_status(pull_request) |
|
625 | 625 | if not merge_status: |
|
626 | 626 | log.debug("Cannot merge, not mergeable.") |
|
627 | 627 | h.flash(msg, category='error') |
|
628 | 628 | return False |
|
629 | 629 | |
|
630 | 630 | if (pull_request.calculated_review_status() |
|
631 | 631 | is not ChangesetStatus.STATUS_APPROVED): |
|
632 | 632 | log.debug("Cannot merge, approval is pending.") |
|
633 | 633 | msg = _('Pull request reviewer approval is pending.') |
|
634 | 634 | h.flash(msg, category='error') |
|
635 | 635 | return False |
|
636 | 636 | return True |
|
637 | 637 | |
|
638 | 638 | def _merge_pull_request(self, pull_request, user, extras): |
|
639 | 639 | merge_resp = PullRequestModel().merge( |
|
640 | 640 | pull_request, user, extras=extras) |
|
641 | 641 | |
|
642 | 642 | if merge_resp.executed: |
|
643 | 643 | log.debug("The merge was successful, closing the pull request.") |
|
644 | 644 | PullRequestModel().close_pull_request( |
|
645 | 645 | pull_request.pull_request_id, user) |
|
646 | 646 | Session().commit() |
|
647 | 647 | msg = _('Pull request was successfully merged and closed.') |
|
648 | 648 | h.flash(msg, category='success') |
|
649 | 649 | else: |
|
650 | 650 | log.debug( |
|
651 | 651 | "The merge was not successful. Merge response: %s", |
|
652 | 652 | merge_resp) |
|
653 | 653 | msg = PullRequestModel().merge_status_message( |
|
654 | 654 | merge_resp.failure_reason) |
|
655 | 655 | h.flash(msg, category='error') |
|
656 | 656 | |
|
657 | 657 | def _update_reviewers(self, pull_request_id, review_members): |
|
658 | 658 | reviewers = [ |
|
659 | 659 | (int(r['user_id']), r['reasons']) for r in review_members] |
|
660 | 660 | PullRequestModel().update_reviewers(pull_request_id, reviewers) |
|
661 | 661 | Session().commit() |
|
662 | 662 | |
|
663 | 663 | def _reject_close(self, pull_request): |
|
664 | 664 | if pull_request.is_closed(): |
|
665 | 665 | raise HTTPForbidden() |
|
666 | 666 | |
|
667 | 667 | PullRequestModel().close_pull_request_with_comment( |
|
668 | 668 | pull_request, c.rhodecode_user, c.rhodecode_db_repo) |
|
669 | 669 | Session().commit() |
|
670 | 670 | |
|
671 | 671 | @LoginRequired() |
|
672 | 672 | @NotAnonymous() |
|
673 | 673 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
674 | 674 | 'repository.admin') |
|
675 | 675 | @auth.CSRFRequired() |
|
676 | 676 | @jsonify |
|
677 | 677 | def delete(self, repo_name, pull_request_id): |
|
678 | 678 | pull_request_id = safe_int(pull_request_id) |
|
679 | 679 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
680 | 680 | # only owner can delete it ! |
|
681 | 681 | if pull_request.author.user_id == c.rhodecode_user.user_id: |
|
682 | 682 | PullRequestModel().delete(pull_request) |
|
683 | 683 | Session().commit() |
|
684 | 684 | h.flash(_('Successfully deleted pull request'), |
|
685 | 685 | category='success') |
|
686 | 686 | return redirect(url('my_account_pullrequests')) |
|
687 | 687 | raise HTTPForbidden() |
|
688 | 688 | |
|
689 | 689 | def _get_pr_version(self, pull_request_id, version=None): |
|
690 | 690 | pull_request_id = safe_int(pull_request_id) |
|
691 | 691 | at_version = None |
|
692 | 692 | |
|
693 | 693 | if version and version == 'latest': |
|
694 | 694 | pull_request_ver = PullRequest.get(pull_request_id) |
|
695 | 695 | pull_request_obj = pull_request_ver |
|
696 | 696 | _org_pull_request_obj = pull_request_obj |
|
697 | 697 | at_version = 'latest' |
|
698 | 698 | elif version: |
|
699 | 699 | pull_request_ver = PullRequestVersion.get_or_404(version) |
|
700 | 700 | pull_request_obj = pull_request_ver |
|
701 | 701 | _org_pull_request_obj = pull_request_ver.pull_request |
|
702 | 702 | at_version = pull_request_ver.pull_request_version_id |
|
703 | 703 | else: |
|
704 | 704 | _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id) |
|
705 | 705 | |
|
706 | 706 | pull_request_display_obj = PullRequest.get_pr_display_object( |
|
707 | 707 | pull_request_obj, _org_pull_request_obj) |
|
708 | 708 | return _org_pull_request_obj, pull_request_obj, \ |
|
709 | 709 | pull_request_display_obj, at_version |
|
710 | 710 | |
|
711 | 711 | def _get_pr_version_changes(self, version, pull_request_latest): |
|
712 | 712 | """ |
|
713 | 713 | Generate changes commits, and diff data based on the current pr version |
|
714 | 714 | """ |
|
715 | 715 | |
|
716 | 716 | #TODO(marcink): save those changes as JSON metadata for chaching later. |
|
717 | 717 | |
|
718 | 718 | # fake the version to add the "initial" state object |
|
719 | 719 | pull_request_initial = PullRequest.get_pr_display_object( |
|
720 | 720 | pull_request_latest, pull_request_latest, |
|
721 | 721 | internal_methods=['get_commit', 'versions']) |
|
722 | 722 | pull_request_initial.revisions = [] |
|
723 | 723 | pull_request_initial.source_repo.get_commit = types.MethodType( |
|
724 | 724 | lambda *a, **k: EmptyCommit(), pull_request_initial) |
|
725 | 725 | pull_request_initial.source_repo.scm_instance = types.MethodType( |
|
726 | 726 | lambda *a, **k: EmptyRepository(), pull_request_initial) |
|
727 | 727 | |
|
728 | 728 | _changes_versions = [pull_request_latest] + \ |
|
729 | 729 | list(reversed(c.versions)) + \ |
|
730 | 730 | [pull_request_initial] |
|
731 | 731 | |
|
732 | 732 | if version == 'latest': |
|
733 | 733 | index = 0 |
|
734 | 734 | else: |
|
735 | 735 | for pos, prver in enumerate(_changes_versions): |
|
736 | 736 | ver = getattr(prver, 'pull_request_version_id', -1) |
|
737 | 737 | if ver == safe_int(version): |
|
738 | 738 | index = pos |
|
739 | 739 | break |
|
740 | 740 | else: |
|
741 | 741 | index = 0 |
|
742 | 742 | |
|
743 | 743 | cur_obj = _changes_versions[index] |
|
744 | 744 | prev_obj = _changes_versions[index + 1] |
|
745 | 745 | |
|
746 | 746 | old_commit_ids = set(prev_obj.revisions) |
|
747 | 747 | new_commit_ids = set(cur_obj.revisions) |
|
748 | 748 | |
|
749 | 749 | changes = PullRequestModel()._calculate_commit_id_changes( |
|
750 | 750 | old_commit_ids, new_commit_ids) |
|
751 | 751 | |
|
752 | 752 | old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs( |
|
753 | 753 | cur_obj, prev_obj) |
|
754 | 754 | file_changes = PullRequestModel()._calculate_file_changes( |
|
755 | 755 | old_diff_data, new_diff_data) |
|
756 | 756 | return changes, file_changes |
|
757 | 757 | |
|
758 | 758 | @LoginRequired() |
|
759 | 759 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
760 | 760 | 'repository.admin') |
|
761 | 761 | def show(self, repo_name, pull_request_id): |
|
762 | 762 | pull_request_id = safe_int(pull_request_id) |
|
763 | 763 | version = request.GET.get('version') |
|
764 | 764 | |
|
765 | 765 | (pull_request_latest, |
|
766 | 766 | pull_request_at_ver, |
|
767 | 767 | pull_request_display_obj, |
|
768 | 768 | at_version) = self._get_pr_version(pull_request_id, version=version) |
|
769 | 769 | |
|
770 | 770 | c.template_context['pull_request_data']['pull_request_id'] = \ |
|
771 | 771 | pull_request_id |
|
772 | 772 | |
|
773 | 773 | # pull_requests repo_name we opened it against |
|
774 | 774 | # ie. target_repo must match |
|
775 | 775 | if repo_name != pull_request_at_ver.target_repo.repo_name: |
|
776 | 776 | raise HTTPNotFound |
|
777 | 777 | |
|
778 | 778 | c.shadow_clone_url = PullRequestModel().get_shadow_clone_url( |
|
779 | 779 | pull_request_at_ver) |
|
780 | 780 | |
|
781 | 781 | pr_closed = pull_request_latest.is_closed() |
|
782 | 782 | if at_version and not at_version == 'latest': |
|
783 | 783 | c.allowed_to_change_status = False |
|
784 | 784 | c.allowed_to_update = False |
|
785 | 785 | c.allowed_to_merge = False |
|
786 | 786 | c.allowed_to_delete = False |
|
787 | 787 | c.allowed_to_comment = False |
|
788 | 788 | else: |
|
789 | 789 | c.allowed_to_change_status = PullRequestModel(). \ |
|
790 | 790 | check_user_change_status(pull_request_at_ver, c.rhodecode_user) |
|
791 | 791 | c.allowed_to_update = PullRequestModel().check_user_update( |
|
792 | 792 | pull_request_latest, c.rhodecode_user) and not pr_closed |
|
793 | 793 | c.allowed_to_merge = PullRequestModel().check_user_merge( |
|
794 | 794 | pull_request_latest, c.rhodecode_user) and not pr_closed |
|
795 | 795 | c.allowed_to_delete = PullRequestModel().check_user_delete( |
|
796 | 796 | pull_request_latest, c.rhodecode_user) and not pr_closed |
|
797 | 797 | c.allowed_to_comment = not pr_closed |
|
798 | 798 | |
|
799 | 799 | cc_model = ChangesetCommentsModel() |
|
800 | 800 | |
|
801 | 801 | c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses() |
|
802 | 802 | c.pull_request_review_status = pull_request_at_ver.calculated_review_status() |
|
803 | 803 | c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status( |
|
804 | 804 | pull_request_at_ver) |
|
805 | 805 | c.approval_msg = None |
|
806 | 806 | if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED: |
|
807 | 807 | c.approval_msg = _('Reviewer approval is pending.') |
|
808 | 808 | c.pr_merge_status = False |
|
809 | 809 | |
|
810 | 810 | # inline comments |
|
811 | 811 | inline_comments = cc_model.get_inline_comments( |
|
812 | 812 | c.rhodecode_db_repo.repo_id, pull_request=pull_request_id) |
|
813 | 813 | |
|
814 | 814 | _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count( |
|
815 | 815 | inline_comments, version=at_version, include_aggregates=True) |
|
816 | 816 | |
|
817 | 817 | c.at_version_num = at_version if at_version and at_version != 'latest' else None |
|
818 | 818 | is_outdated = lambda co: \ |
|
819 | 819 | not c.at_version_num \ |
|
820 | 820 | or co.pull_request_version_id <= c.at_version_num |
|
821 | 821 | |
|
822 | 822 | # inline_comments_until_version |
|
823 | 823 | if c.at_version_num: |
|
824 | 824 | # if we use version, then do not show later comments |
|
825 | 825 | # than current version |
|
826 | 826 | paths = collections.defaultdict(lambda: collections.defaultdict(list)) |
|
827 | 827 | for fname, per_line_comments in inline_comments.iteritems(): |
|
828 | 828 | for lno, comments in per_line_comments.iteritems(): |
|
829 | 829 | for co in comments: |
|
830 | 830 | if co.pull_request_version_id and is_outdated(co): |
|
831 | 831 | paths[co.f_path][co.line_no].append(co) |
|
832 | 832 | inline_comments = paths |
|
833 | 833 | |
|
834 | 834 | # outdated comments |
|
835 | 835 | c.outdated_cnt = 0 |
|
836 | 836 | if ChangesetCommentsModel.use_outdated_comments(pull_request_latest): |
|
837 | 837 | outdated_comments = cc_model.get_outdated_comments( |
|
838 | 838 | c.rhodecode_db_repo.repo_id, |
|
839 | 839 | pull_request=pull_request_at_ver) |
|
840 | 840 | |
|
841 | 841 | # Count outdated comments and check for deleted files |
|
842 | 842 | is_outdated = lambda co: \ |
|
843 | 843 | not c.at_version_num \ |
|
844 | 844 | or co.pull_request_version_id < c.at_version_num |
|
845 | 845 | for file_name, lines in outdated_comments.iteritems(): |
|
846 | 846 | for comments in lines.values(): |
|
847 | 847 | comments = [comm for comm in comments if is_outdated(comm)] |
|
848 | 848 | c.outdated_cnt += len(comments) |
|
849 | 849 | |
|
850 | 850 | # load compare data into template context |
|
851 | 851 | self._load_compare_data(pull_request_at_ver, inline_comments) |
|
852 | 852 | |
|
853 | 853 | # this is a hack to properly display links, when creating PR, the |
|
854 | 854 | # compare view and others uses different notation, and |
|
855 |
# compare_commits. |
|
|
855 | # compare_commits.mako renders links based on the target_repo. | |
|
856 | 856 | # We need to swap that here to generate it properly on the html side |
|
857 | 857 | c.target_repo = c.source_repo |
|
858 | 858 | |
|
859 | 859 | # general comments |
|
860 | 860 | c.comments = cc_model.get_comments( |
|
861 | 861 | c.rhodecode_db_repo.repo_id, pull_request=pull_request_id) |
|
862 | 862 | |
|
863 | 863 | if c.allowed_to_update: |
|
864 | 864 | force_close = ('forced_closed', _('Close Pull Request')) |
|
865 | 865 | statuses = ChangesetStatus.STATUSES + [force_close] |
|
866 | 866 | else: |
|
867 | 867 | statuses = ChangesetStatus.STATUSES |
|
868 | 868 | c.commit_statuses = statuses |
|
869 | 869 | |
|
870 | 870 | c.ancestor = None # TODO: add ancestor here |
|
871 | 871 | c.pull_request = pull_request_display_obj |
|
872 | 872 | c.pull_request_latest = pull_request_latest |
|
873 | 873 | c.at_version = at_version |
|
874 | 874 | |
|
875 | 875 | c.versions = pull_request_display_obj.versions() |
|
876 | 876 | c.changes = None |
|
877 | 877 | c.file_changes = None |
|
878 | 878 | |
|
879 | 879 | c.show_version_changes = 1 # control flag, not used yet |
|
880 | 880 | |
|
881 | 881 | if at_version and c.show_version_changes: |
|
882 | 882 | c.changes, c.file_changes = self._get_pr_version_changes( |
|
883 | 883 | version, pull_request_latest) |
|
884 | 884 | |
|
885 |
return render('/pullrequests/pullrequest_show. |
|
|
885 | return render('/pullrequests/pullrequest_show.mako') | |
|
886 | 886 | |
|
887 | 887 | @LoginRequired() |
|
888 | 888 | @NotAnonymous() |
|
889 | 889 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
890 | 890 | 'repository.admin') |
|
891 | 891 | @auth.CSRFRequired() |
|
892 | 892 | @jsonify |
|
893 | 893 | def comment(self, repo_name, pull_request_id): |
|
894 | 894 | pull_request_id = safe_int(pull_request_id) |
|
895 | 895 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
896 | 896 | if pull_request.is_closed(): |
|
897 | 897 | raise HTTPForbidden() |
|
898 | 898 | |
|
899 | 899 | # TODO: johbo: Re-think this bit, "approved_closed" does not exist |
|
900 | 900 | # as a changeset status, still we want to send it in one value. |
|
901 | 901 | status = request.POST.get('changeset_status', None) |
|
902 | 902 | text = request.POST.get('text') |
|
903 | 903 | if status and '_closed' in status: |
|
904 | 904 | close_pr = True |
|
905 | 905 | status = status.replace('_closed', '') |
|
906 | 906 | else: |
|
907 | 907 | close_pr = False |
|
908 | 908 | |
|
909 | 909 | forced = (status == 'forced') |
|
910 | 910 | if forced: |
|
911 | 911 | status = 'rejected' |
|
912 | 912 | |
|
913 | 913 | allowed_to_change_status = PullRequestModel().check_user_change_status( |
|
914 | 914 | pull_request, c.rhodecode_user) |
|
915 | 915 | |
|
916 | 916 | if status and allowed_to_change_status: |
|
917 | 917 | message = (_('Status change %(transition_icon)s %(status)s') |
|
918 | 918 | % {'transition_icon': '>', |
|
919 | 919 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
920 | 920 | if close_pr: |
|
921 | 921 | message = _('Closing with') + ' ' + message |
|
922 | 922 | text = text or message |
|
923 | 923 | comm = ChangesetCommentsModel().create( |
|
924 | 924 | text=text, |
|
925 | 925 | repo=c.rhodecode_db_repo.repo_id, |
|
926 | 926 | user=c.rhodecode_user.user_id, |
|
927 | 927 | pull_request=pull_request_id, |
|
928 | 928 | f_path=request.POST.get('f_path'), |
|
929 | 929 | line_no=request.POST.get('line'), |
|
930 | 930 | status_change=(ChangesetStatus.get_status_lbl(status) |
|
931 | 931 | if status and allowed_to_change_status else None), |
|
932 | 932 | status_change_type=(status |
|
933 | 933 | if status and allowed_to_change_status else None), |
|
934 | 934 | closing_pr=close_pr |
|
935 | 935 | ) |
|
936 | 936 | |
|
937 | 937 | if allowed_to_change_status: |
|
938 | 938 | old_calculated_status = pull_request.calculated_review_status() |
|
939 | 939 | # get status if set ! |
|
940 | 940 | if status: |
|
941 | 941 | ChangesetStatusModel().set_status( |
|
942 | 942 | c.rhodecode_db_repo.repo_id, |
|
943 | 943 | status, |
|
944 | 944 | c.rhodecode_user.user_id, |
|
945 | 945 | comm, |
|
946 | 946 | pull_request=pull_request_id |
|
947 | 947 | ) |
|
948 | 948 | |
|
949 | 949 | Session().flush() |
|
950 | 950 | events.trigger(events.PullRequestCommentEvent(pull_request, comm)) |
|
951 | 951 | # we now calculate the status of pull request, and based on that |
|
952 | 952 | # calculation we set the commits status |
|
953 | 953 | calculated_status = pull_request.calculated_review_status() |
|
954 | 954 | if old_calculated_status != calculated_status: |
|
955 | 955 | PullRequestModel()._trigger_pull_request_hook( |
|
956 | 956 | pull_request, c.rhodecode_user, 'review_status_change') |
|
957 | 957 | |
|
958 | 958 | calculated_status_lbl = ChangesetStatus.get_status_lbl( |
|
959 | 959 | calculated_status) |
|
960 | 960 | |
|
961 | 961 | if close_pr: |
|
962 | 962 | status_completed = ( |
|
963 | 963 | calculated_status in [ChangesetStatus.STATUS_APPROVED, |
|
964 | 964 | ChangesetStatus.STATUS_REJECTED]) |
|
965 | 965 | if forced or status_completed: |
|
966 | 966 | PullRequestModel().close_pull_request( |
|
967 | 967 | pull_request_id, c.rhodecode_user) |
|
968 | 968 | else: |
|
969 | 969 | h.flash(_('Closing pull request on other statuses than ' |
|
970 | 970 | 'rejected or approved is forbidden. ' |
|
971 | 971 | 'Calculated status from all reviewers ' |
|
972 | 972 | 'is currently: %s') % calculated_status_lbl, |
|
973 | 973 | category='warning') |
|
974 | 974 | |
|
975 | 975 | Session().commit() |
|
976 | 976 | |
|
977 | 977 | if not request.is_xhr: |
|
978 | 978 | return redirect(h.url('pullrequest_show', repo_name=repo_name, |
|
979 | 979 | pull_request_id=pull_request_id)) |
|
980 | 980 | |
|
981 | 981 | data = { |
|
982 | 982 | 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), |
|
983 | 983 | } |
|
984 | 984 | if comm: |
|
985 | 985 | c.co = comm |
|
986 | 986 | data.update(comm.get_dict()) |
|
987 | 987 | data.update({'rendered_text': |
|
988 |
render('changeset/changeset_comment_block. |
|
|
988 | render('changeset/changeset_comment_block.mako')}) | |
|
989 | 989 | |
|
990 | 990 | return data |
|
991 | 991 | |
|
992 | 992 | @LoginRequired() |
|
993 | 993 | @NotAnonymous() |
|
994 | 994 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
995 | 995 | 'repository.admin') |
|
996 | 996 | @auth.CSRFRequired() |
|
997 | 997 | @jsonify |
|
998 | 998 | def delete_comment(self, repo_name, comment_id): |
|
999 | 999 | return self._delete_comment(comment_id) |
|
1000 | 1000 | |
|
1001 | 1001 | def _delete_comment(self, comment_id): |
|
1002 | 1002 | comment_id = safe_int(comment_id) |
|
1003 | 1003 | co = ChangesetComment.get_or_404(comment_id) |
|
1004 | 1004 | if co.pull_request.is_closed(): |
|
1005 | 1005 | # don't allow deleting comments on closed pull request |
|
1006 | 1006 | raise HTTPForbidden() |
|
1007 | 1007 | |
|
1008 | 1008 | is_owner = co.author.user_id == c.rhodecode_user.user_id |
|
1009 | 1009 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) |
|
1010 | 1010 | if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner: |
|
1011 | 1011 | old_calculated_status = co.pull_request.calculated_review_status() |
|
1012 | 1012 | ChangesetCommentsModel().delete(comment=co) |
|
1013 | 1013 | Session().commit() |
|
1014 | 1014 | calculated_status = co.pull_request.calculated_review_status() |
|
1015 | 1015 | if old_calculated_status != calculated_status: |
|
1016 | 1016 | PullRequestModel()._trigger_pull_request_hook( |
|
1017 | 1017 | co.pull_request, c.rhodecode_user, 'review_status_change') |
|
1018 | 1018 | return True |
|
1019 | 1019 | else: |
|
1020 | 1020 | raise HTTPForbidden() |
@@ -1,111 +1,111 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 | Search controller for RhodeCode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import urllib |
|
27 | 27 | |
|
28 | 28 | from pylons import request, config, tmpl_context as c |
|
29 | 29 | |
|
30 | 30 | from webhelpers.util import update_params |
|
31 | 31 | |
|
32 | 32 | from rhodecode.lib.auth import LoginRequired, AuthUser |
|
33 | 33 | from rhodecode.lib.base import BaseRepoController, render |
|
34 | 34 | from rhodecode.lib.helpers import Page |
|
35 | 35 | from rhodecode.lib.utils2 import safe_str, safe_int |
|
36 | 36 | from rhodecode.lib.index import searcher_from_config |
|
37 | 37 | from rhodecode.model import validation_schema |
|
38 | 38 | from rhodecode.model.validation_schema.schemas import search_schema |
|
39 | 39 | |
|
40 | 40 | log = logging.getLogger(__name__) |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | class SearchController(BaseRepoController): |
|
44 | 44 | |
|
45 | 45 | @LoginRequired() |
|
46 | 46 | def index(self, repo_name=None): |
|
47 | 47 | |
|
48 | 48 | searcher = searcher_from_config(config) |
|
49 | 49 | formatted_results = [] |
|
50 | 50 | execution_time = '' |
|
51 | 51 | |
|
52 | 52 | schema = search_schema.SearchParamsSchema() |
|
53 | 53 | |
|
54 | 54 | search_params = {} |
|
55 | 55 | errors = [] |
|
56 | 56 | try: |
|
57 | 57 | search_params = schema.deserialize( |
|
58 | 58 | dict(search_query=request.GET.get('q'), |
|
59 | 59 | search_type=request.GET.get('type'), |
|
60 | 60 | search_sort=request.GET.get('sort'), |
|
61 | 61 | page_limit=request.GET.get('page_limit'), |
|
62 | 62 | requested_page=request.GET.get('page')) |
|
63 | 63 | ) |
|
64 | 64 | except validation_schema.Invalid as e: |
|
65 | 65 | errors = e.children |
|
66 | 66 | |
|
67 | 67 | def url_generator(**kw): |
|
68 | 68 | q = urllib.quote(safe_str(search_query)) |
|
69 | 69 | return update_params( |
|
70 | 70 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) |
|
71 | 71 | |
|
72 | 72 | search_query = search_params.get('search_query') |
|
73 | 73 | search_type = search_params.get('search_type') |
|
74 | 74 | search_sort = search_params.get('search_sort') |
|
75 | 75 | if search_params.get('search_query'): |
|
76 | 76 | page_limit = search_params['page_limit'] |
|
77 | 77 | requested_page = search_params['requested_page'] |
|
78 | 78 | |
|
79 | 79 | c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, |
|
80 | 80 | ip_addr=self.ip_addr) |
|
81 | 81 | |
|
82 | 82 | try: |
|
83 | 83 | search_result = searcher.search( |
|
84 | 84 | search_query, search_type, c.perm_user, repo_name, |
|
85 | 85 | requested_page, page_limit, search_sort) |
|
86 | 86 | |
|
87 | 87 | formatted_results = Page( |
|
88 | 88 | search_result['results'], page=requested_page, |
|
89 | 89 | item_count=search_result['count'], |
|
90 | 90 | items_per_page=page_limit, url=url_generator) |
|
91 | 91 | finally: |
|
92 | 92 | searcher.cleanup() |
|
93 | 93 | |
|
94 | 94 | if not search_result['error']: |
|
95 | 95 | execution_time = '%s results (%.3f seconds)' % ( |
|
96 | 96 | search_result['count'], |
|
97 | 97 | search_result['runtime']) |
|
98 | 98 | elif not errors: |
|
99 | 99 | node = schema['search_query'] |
|
100 | 100 | errors = [ |
|
101 | 101 | validation_schema.Invalid(node, search_result['error'])] |
|
102 | 102 | |
|
103 | 103 | c.sort = search_sort |
|
104 | 104 | c.url_generator = url_generator |
|
105 | 105 | c.errors = errors |
|
106 | 106 | c.formatted_results = formatted_results |
|
107 | 107 | c.runtime = execution_time |
|
108 | 108 | c.cur_query = search_query |
|
109 | 109 | c.search_type = search_type |
|
110 | 110 | # Return a rendered template |
|
111 |
return render('/search/search. |
|
|
111 | return render('/search/search.mako') |
@@ -1,318 +1,318 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 | Summary controller for RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | from string import lower |
|
27 | 27 | |
|
28 | 28 | from pylons import tmpl_context as c, request |
|
29 | 29 | from pylons.i18n.translation import _ |
|
30 | 30 | from beaker.cache import cache_region, region_invalidate |
|
31 | 31 | |
|
32 | 32 | from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP) |
|
33 | 33 | from rhodecode.controllers import utils |
|
34 | 34 | from rhodecode.controllers.changelog import _load_changelog_summary |
|
35 | 35 | from rhodecode.lib import caches, helpers as h |
|
36 | 36 | from rhodecode.lib.utils import jsonify |
|
37 | 37 | from rhodecode.lib.utils2 import safe_str |
|
38 | 38 | from rhodecode.lib.auth import ( |
|
39 | 39 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired) |
|
40 | 40 | from rhodecode.lib.base import BaseRepoController, render |
|
41 | 41 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
42 | 42 | from rhodecode.lib.ext_json import json |
|
43 | 43 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
44 | 44 | from rhodecode.lib.vcs.exceptions import ( |
|
45 | 45 | CommitError, EmptyRepositoryError, NodeDoesNotExistError) |
|
46 | 46 | from rhodecode.model.db import Statistics, CacheKey, User |
|
47 | 47 | from rhodecode.model.repo import ReadmeFinder |
|
48 | 48 | |
|
49 | 49 | |
|
50 | 50 | log = logging.getLogger(__name__) |
|
51 | 51 | |
|
52 | 52 | |
|
53 | 53 | class SummaryController(BaseRepoController): |
|
54 | 54 | |
|
55 | 55 | def __before__(self): |
|
56 | 56 | super(SummaryController, self).__before__() |
|
57 | 57 | |
|
58 | 58 | def __get_readme_data(self, db_repo): |
|
59 | 59 | repo_name = db_repo.repo_name |
|
60 | 60 | log.debug('Looking for README file') |
|
61 | 61 | default_renderer = c.visual.default_renderer |
|
62 | 62 | |
|
63 | 63 | @cache_region('long_term') |
|
64 | 64 | def _generate_readme(cache_key): |
|
65 | 65 | readme_data = None |
|
66 | 66 | readme_node = None |
|
67 | 67 | readme_filename = None |
|
68 | 68 | commit = self._get_landing_commit_or_none(db_repo) |
|
69 | 69 | if commit: |
|
70 | 70 | log.debug("Searching for a README file.") |
|
71 | 71 | readme_node = ReadmeFinder(default_renderer).search(commit) |
|
72 | 72 | if readme_node: |
|
73 | 73 | readme_data = self._render_readme_or_none(commit, readme_node) |
|
74 | 74 | readme_filename = readme_node.path |
|
75 | 75 | return readme_data, readme_filename |
|
76 | 76 | |
|
77 | 77 | invalidator_context = CacheKey.repo_context_cache( |
|
78 | 78 | _generate_readme, repo_name, CacheKey.CACHE_TYPE_README) |
|
79 | 79 | |
|
80 | 80 | with invalidator_context as context: |
|
81 | 81 | context.invalidate() |
|
82 | 82 | computed = context.compute() |
|
83 | 83 | |
|
84 | 84 | return computed |
|
85 | 85 | |
|
86 | 86 | def _get_landing_commit_or_none(self, db_repo): |
|
87 | 87 | log.debug("Getting the landing commit.") |
|
88 | 88 | try: |
|
89 | 89 | commit = db_repo.get_landing_commit() |
|
90 | 90 | if not isinstance(commit, EmptyCommit): |
|
91 | 91 | return commit |
|
92 | 92 | else: |
|
93 | 93 | log.debug("Repository is empty, no README to render.") |
|
94 | 94 | except CommitError: |
|
95 | 95 | log.exception( |
|
96 | 96 | "Problem getting commit when trying to render the README.") |
|
97 | 97 | |
|
98 | 98 | def _render_readme_or_none(self, commit, readme_node): |
|
99 | 99 | log.debug( |
|
100 | 100 | 'Found README file `%s` rendering...', readme_node.path) |
|
101 | 101 | renderer = MarkupRenderer() |
|
102 | 102 | try: |
|
103 | 103 | return renderer.render( |
|
104 | 104 | readme_node.content, filename=readme_node.path) |
|
105 | 105 | except Exception: |
|
106 | 106 | log.exception( |
|
107 | 107 | "Exception while trying to render the README") |
|
108 | 108 | |
|
109 | 109 | @LoginRequired() |
|
110 | 110 | @HasRepoPermissionAnyDecorator( |
|
111 | 111 | 'repository.read', 'repository.write', 'repository.admin') |
|
112 | 112 | def index(self, repo_name): |
|
113 | 113 | |
|
114 | 114 | # Prepare the clone URL |
|
115 | 115 | |
|
116 | 116 | username = '' |
|
117 | 117 | if c.rhodecode_user.username != User.DEFAULT_USER: |
|
118 | 118 | username = safe_str(c.rhodecode_user.username) |
|
119 | 119 | |
|
120 | 120 | _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl |
|
121 | 121 | if '{repo}' in _def_clone_uri: |
|
122 | 122 | _def_clone_uri_by_id = _def_clone_uri.replace( |
|
123 | 123 | '{repo}', '_{repoid}') |
|
124 | 124 | elif '{repoid}' in _def_clone_uri: |
|
125 | 125 | _def_clone_uri_by_id = _def_clone_uri.replace( |
|
126 | 126 | '_{repoid}', '{repo}') |
|
127 | 127 | |
|
128 | 128 | c.clone_repo_url = c.rhodecode_db_repo.clone_url( |
|
129 | 129 | user=username, uri_tmpl=_def_clone_uri) |
|
130 | 130 | c.clone_repo_url_id = c.rhodecode_db_repo.clone_url( |
|
131 | 131 | user=username, uri_tmpl=_def_clone_uri_by_id) |
|
132 | 132 | |
|
133 | 133 | # If enabled, get statistics data |
|
134 | 134 | |
|
135 | 135 | c.show_stats = bool(c.rhodecode_db_repo.enable_statistics) |
|
136 | 136 | |
|
137 | 137 | stats = self.sa.query(Statistics)\ |
|
138 | 138 | .filter(Statistics.repository == c.rhodecode_db_repo)\ |
|
139 | 139 | .scalar() |
|
140 | 140 | |
|
141 | 141 | c.stats_percentage = 0 |
|
142 | 142 | |
|
143 | 143 | if stats and stats.languages: |
|
144 | 144 | c.no_data = False is c.rhodecode_db_repo.enable_statistics |
|
145 | 145 | lang_stats_d = json.loads(stats.languages) |
|
146 | 146 | |
|
147 | 147 | # Sort first by decreasing count and second by the file extension, |
|
148 | 148 | # so we have a consistent output. |
|
149 | 149 | lang_stats_items = sorted(lang_stats_d.iteritems(), |
|
150 | 150 | key=lambda k: (-k[1], k[0]))[:10] |
|
151 | 151 | lang_stats = [(x, {"count": y, |
|
152 | 152 | "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) |
|
153 | 153 | for x, y in lang_stats_items] |
|
154 | 154 | |
|
155 | 155 | c.trending_languages = json.dumps(lang_stats) |
|
156 | 156 | else: |
|
157 | 157 | c.no_data = True |
|
158 | 158 | c.trending_languages = json.dumps({}) |
|
159 | 159 | |
|
160 | 160 | c.enable_downloads = c.rhodecode_db_repo.enable_downloads |
|
161 | 161 | c.repository_followers = self.scm_model.get_followers( |
|
162 | 162 | c.rhodecode_db_repo) |
|
163 | 163 | c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo) |
|
164 | 164 | c.repository_is_user_following = self.scm_model.is_following_repo( |
|
165 | 165 | c.repo_name, c.rhodecode_user.user_id) |
|
166 | 166 | |
|
167 | 167 | if c.repository_requirements_missing: |
|
168 |
return render('summary/missing_requirements. |
|
|
168 | return render('summary/missing_requirements.mako') | |
|
169 | 169 | |
|
170 | 170 | c.readme_data, c.readme_file = \ |
|
171 | 171 | self.__get_readme_data(c.rhodecode_db_repo) |
|
172 | 172 | |
|
173 | 173 | _load_changelog_summary() |
|
174 | 174 | |
|
175 | 175 | if request.is_xhr: |
|
176 |
return render('changelog/changelog_summary_data. |
|
|
176 | return render('changelog/changelog_summary_data.mako') | |
|
177 | 177 | |
|
178 |
return render('summary/summary. |
|
|
178 | return render('summary/summary.mako') | |
|
179 | 179 | |
|
180 | 180 | @LoginRequired() |
|
181 | 181 | @XHRRequired() |
|
182 | 182 | @HasRepoPermissionAnyDecorator( |
|
183 | 183 | 'repository.read', 'repository.write', 'repository.admin') |
|
184 | 184 | @jsonify |
|
185 | 185 | def repo_stats(self, repo_name, commit_id): |
|
186 | 186 | _namespace = caches.get_repo_namespace_key( |
|
187 | 187 | caches.SUMMARY_STATS, repo_name) |
|
188 | 188 | show_stats = bool(c.rhodecode_db_repo.enable_statistics) |
|
189 | 189 | cache_manager = caches.get_cache_manager('repo_cache_long', _namespace) |
|
190 | 190 | _cache_key = caches.compute_key_from_params( |
|
191 | 191 | repo_name, commit_id, show_stats) |
|
192 | 192 | |
|
193 | 193 | def compute_stats(): |
|
194 | 194 | code_stats = {} |
|
195 | 195 | size = 0 |
|
196 | 196 | try: |
|
197 | 197 | scm_instance = c.rhodecode_db_repo.scm_instance() |
|
198 | 198 | commit = scm_instance.get_commit(commit_id) |
|
199 | 199 | |
|
200 | 200 | for node in commit.get_filenodes_generator(): |
|
201 | 201 | size += node.size |
|
202 | 202 | if not show_stats: |
|
203 | 203 | continue |
|
204 | 204 | ext = lower(node.extension) |
|
205 | 205 | ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext) |
|
206 | 206 | if ext_info: |
|
207 | 207 | if ext in code_stats: |
|
208 | 208 | code_stats[ext]['count'] += 1 |
|
209 | 209 | else: |
|
210 | 210 | code_stats[ext] = {"count": 1, "desc": ext_info} |
|
211 | 211 | except EmptyRepositoryError: |
|
212 | 212 | pass |
|
213 | 213 | return {'size': h.format_byte_size_binary(size), |
|
214 | 214 | 'code_stats': code_stats} |
|
215 | 215 | |
|
216 | 216 | stats = cache_manager.get(_cache_key, createfunc=compute_stats) |
|
217 | 217 | return stats |
|
218 | 218 | |
|
219 | 219 | def _switcher_reference_data(self, repo_name, references, is_svn): |
|
220 | 220 | """Prepare reference data for given `references`""" |
|
221 | 221 | items = [] |
|
222 | 222 | for name, commit_id in references.items(): |
|
223 | 223 | use_commit_id = '/' in name or is_svn |
|
224 | 224 | items.append({ |
|
225 | 225 | 'name': name, |
|
226 | 226 | 'commit_id': commit_id, |
|
227 | 227 | 'files_url': h.url( |
|
228 | 228 | 'files_home', |
|
229 | 229 | repo_name=repo_name, |
|
230 | 230 | f_path=name if is_svn else '', |
|
231 | 231 | revision=commit_id if use_commit_id else name, |
|
232 | 232 | at=name) |
|
233 | 233 | }) |
|
234 | 234 | return items |
|
235 | 235 | |
|
236 | 236 | @LoginRequired() |
|
237 | 237 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
238 | 238 | 'repository.admin') |
|
239 | 239 | @jsonify |
|
240 | 240 | def repo_refs_data(self, repo_name): |
|
241 | 241 | repo = c.rhodecode_repo |
|
242 | 242 | refs_to_create = [ |
|
243 | 243 | (_("Branch"), repo.branches, 'branch'), |
|
244 | 244 | (_("Tag"), repo.tags, 'tag'), |
|
245 | 245 | (_("Bookmark"), repo.bookmarks, 'book'), |
|
246 | 246 | ] |
|
247 | 247 | res = self._create_reference_data(repo, repo_name, refs_to_create) |
|
248 | 248 | data = { |
|
249 | 249 | 'more': False, |
|
250 | 250 | 'results': res |
|
251 | 251 | } |
|
252 | 252 | return data |
|
253 | 253 | |
|
254 | 254 | @LoginRequired() |
|
255 | 255 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
256 | 256 | 'repository.admin') |
|
257 | 257 | @jsonify |
|
258 | 258 | def repo_default_reviewers_data(self, repo_name): |
|
259 | 259 | return { |
|
260 | 260 | 'reviewers': [utils.reviewer_as_json( |
|
261 | 261 | user=c.rhodecode_db_repo.user, reasons=None)] |
|
262 | 262 | } |
|
263 | 263 | |
|
264 | 264 | @jsonify |
|
265 | 265 | def repo_refs_changelog_data(self, repo_name): |
|
266 | 266 | repo = c.rhodecode_repo |
|
267 | 267 | |
|
268 | 268 | refs_to_create = [ |
|
269 | 269 | (_("Branches"), repo.branches, 'branch'), |
|
270 | 270 | (_("Closed branches"), repo.branches_closed, 'branch_closed'), |
|
271 | 271 | # TODO: enable when vcs can handle bookmarks filters |
|
272 | 272 | # (_("Bookmarks"), repo.bookmarks, "book"), |
|
273 | 273 | ] |
|
274 | 274 | res = self._create_reference_data(repo, repo_name, refs_to_create) |
|
275 | 275 | data = { |
|
276 | 276 | 'more': False, |
|
277 | 277 | 'results': res |
|
278 | 278 | } |
|
279 | 279 | return data |
|
280 | 280 | |
|
281 | 281 | def _create_reference_data(self, repo, full_repo_name, refs_to_create): |
|
282 | 282 | format_ref_id = utils.get_format_ref_id(repo) |
|
283 | 283 | |
|
284 | 284 | result = [] |
|
285 | 285 | for title, refs, ref_type in refs_to_create: |
|
286 | 286 | if refs: |
|
287 | 287 | result.append({ |
|
288 | 288 | 'text': title, |
|
289 | 289 | 'children': self._create_reference_items( |
|
290 | 290 | repo, full_repo_name, refs, ref_type, format_ref_id), |
|
291 | 291 | }) |
|
292 | 292 | return result |
|
293 | 293 | |
|
294 | 294 | def _create_reference_items(self, repo, full_repo_name, refs, ref_type, |
|
295 | 295 | format_ref_id): |
|
296 | 296 | result = [] |
|
297 | 297 | is_svn = h.is_svn(repo) |
|
298 | 298 | for ref_name, raw_id in refs.iteritems(): |
|
299 | 299 | files_url = self._create_files_url( |
|
300 | 300 | repo, full_repo_name, ref_name, raw_id, is_svn) |
|
301 | 301 | result.append({ |
|
302 | 302 | 'text': ref_name, |
|
303 | 303 | 'id': format_ref_id(ref_name, raw_id), |
|
304 | 304 | 'raw_id': raw_id, |
|
305 | 305 | 'type': ref_type, |
|
306 | 306 | 'files_url': files_url, |
|
307 | 307 | }) |
|
308 | 308 | return result |
|
309 | 309 | |
|
310 | 310 | def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, |
|
311 | 311 | is_svn): |
|
312 | 312 | use_commit_id = '/' in ref_name or is_svn |
|
313 | 313 | return h.url( |
|
314 | 314 | 'files_home', |
|
315 | 315 | repo_name=full_repo_name, |
|
316 | 316 | f_path=ref_name if is_svn else '', |
|
317 | 317 | revision=raw_id if use_commit_id else ref_name, |
|
318 | 318 | at=ref_name) |
@@ -1,38 +1,38 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 | Tags controller for rhodecode |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | |
|
27 | 27 | from rhodecode.controllers.base_references import BaseReferencesController |
|
28 | 28 | |
|
29 | 29 | log = logging.getLogger(__name__) |
|
30 | 30 | |
|
31 | 31 | |
|
32 | 32 | class TagsController(BaseReferencesController): |
|
33 | 33 | |
|
34 |
partials_template = 'tags/tags_data. |
|
|
35 |
template = 'tags/tags. |
|
|
34 | partials_template = 'tags/tags_data.mako' | |
|
35 | template = 'tags/tags.mako' | |
|
36 | 36 | |
|
37 | 37 | def _get_reference_items(self, repo): |
|
38 | 38 | return repo.tags.items() |
@@ -1,43 +1,43 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | Users profile controller |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | from pylons import tmpl_context as c |
|
26 | 26 | from webob.exc import HTTPNotFound |
|
27 | 27 | |
|
28 | 28 | from rhodecode.lib.auth import LoginRequired, NotAnonymous |
|
29 | 29 | from rhodecode.lib.base import BaseController, render |
|
30 | 30 | from rhodecode.model.db import User |
|
31 | 31 | from rhodecode.model.user import UserModel |
|
32 | 32 | |
|
33 | 33 | |
|
34 | 34 | class UsersController(BaseController): |
|
35 | 35 | @LoginRequired() |
|
36 | 36 | @NotAnonymous() |
|
37 | 37 | def user_profile(self, username): |
|
38 | 38 | c.user = UserModel().get_by_username(username) |
|
39 | 39 | if not c.user or c.user.username == User.DEFAULT_USER: |
|
40 | 40 | raise HTTPNotFound() |
|
41 | 41 | |
|
42 | 42 | c.active = 'user_profile' |
|
43 |
return render('users/user. |
|
|
43 | return render('users/user.mako') |
@@ -1,238 +1,238 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from rhodecode.model.db import Repository, Integration, RepoGroup |
|
24 | 24 | from rhodecode.config.routing import ( |
|
25 | 25 | ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS) |
|
26 | 26 | from rhodecode.integrations import integration_type_registry |
|
27 | 27 | |
|
28 | 28 | log = logging.getLogger(__name__) |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | def includeme(config): |
|
32 | 32 | |
|
33 | 33 | # global integrations |
|
34 | 34 | |
|
35 | 35 | config.add_route('global_integrations_new', |
|
36 | 36 | ADMIN_PREFIX + '/integrations/new') |
|
37 | 37 | config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', |
|
38 | 38 | attr='new_integration', |
|
39 |
renderer='rhodecode:templates/admin/integrations/new. |
|
|
39 | renderer='rhodecode:templates/admin/integrations/new.mako', | |
|
40 | 40 | request_method='GET', |
|
41 | 41 | route_name='global_integrations_new') |
|
42 | 42 | |
|
43 | 43 | config.add_route('global_integrations_home', |
|
44 | 44 | ADMIN_PREFIX + '/integrations') |
|
45 | 45 | config.add_route('global_integrations_list', |
|
46 | 46 | ADMIN_PREFIX + '/integrations/{integration}') |
|
47 | 47 | for route_name in ['global_integrations_home', 'global_integrations_list']: |
|
48 | 48 | config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', |
|
49 | 49 | attr='index', |
|
50 |
renderer='rhodecode:templates/admin/integrations/list. |
|
|
50 | renderer='rhodecode:templates/admin/integrations/list.mako', | |
|
51 | 51 | request_method='GET', |
|
52 | 52 | route_name=route_name) |
|
53 | 53 | |
|
54 | 54 | config.add_route('global_integrations_create', |
|
55 | 55 | ADMIN_PREFIX + '/integrations/{integration}/new', |
|
56 | 56 | custom_predicates=(valid_integration,)) |
|
57 | 57 | config.add_route('global_integrations_edit', |
|
58 | 58 | ADMIN_PREFIX + '/integrations/{integration}/{integration_id}', |
|
59 | 59 | custom_predicates=(valid_integration,)) |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | for route_name in ['global_integrations_create', 'global_integrations_edit']: |
|
63 | 63 | config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', |
|
64 | 64 | attr='settings_get', |
|
65 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
65 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
66 | 66 | request_method='GET', |
|
67 | 67 | route_name=route_name) |
|
68 | 68 | config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', |
|
69 | 69 | attr='settings_post', |
|
70 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
70 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
71 | 71 | request_method='POST', |
|
72 | 72 | route_name=route_name) |
|
73 | 73 | |
|
74 | 74 | |
|
75 | 75 | # repo group integrations |
|
76 | 76 | config.add_route('repo_group_integrations_home', |
|
77 | 77 | add_route_requirements( |
|
78 | 78 | '{repo_group_name}/settings/integrations', |
|
79 | 79 | URL_NAME_REQUIREMENTS |
|
80 | 80 | ), |
|
81 | 81 | custom_predicates=(valid_repo_group,) |
|
82 | 82 | ) |
|
83 | 83 | config.add_route('repo_group_integrations_list', |
|
84 | 84 | add_route_requirements( |
|
85 | 85 | '{repo_group_name}/settings/integrations/{integration}', |
|
86 | 86 | URL_NAME_REQUIREMENTS |
|
87 | 87 | ), |
|
88 | 88 | custom_predicates=(valid_repo_group, valid_integration)) |
|
89 | 89 | for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']: |
|
90 | 90 | config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', |
|
91 | 91 | attr='index', |
|
92 |
renderer='rhodecode:templates/admin/integrations/list. |
|
|
92 | renderer='rhodecode:templates/admin/integrations/list.mako', | |
|
93 | 93 | request_method='GET', |
|
94 | 94 | route_name=route_name) |
|
95 | 95 | |
|
96 | 96 | config.add_route('repo_group_integrations_new', |
|
97 | 97 | add_route_requirements( |
|
98 | 98 | '{repo_group_name}/settings/integrations/new', |
|
99 | 99 | URL_NAME_REQUIREMENTS |
|
100 | 100 | ), |
|
101 | 101 | custom_predicates=(valid_repo_group,)) |
|
102 | 102 | config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', |
|
103 | 103 | attr='new_integration', |
|
104 |
renderer='rhodecode:templates/admin/integrations/new. |
|
|
104 | renderer='rhodecode:templates/admin/integrations/new.mako', | |
|
105 | 105 | request_method='GET', |
|
106 | 106 | route_name='repo_group_integrations_new') |
|
107 | 107 | |
|
108 | 108 | config.add_route('repo_group_integrations_create', |
|
109 | 109 | add_route_requirements( |
|
110 | 110 | '{repo_group_name}/settings/integrations/{integration}/new', |
|
111 | 111 | URL_NAME_REQUIREMENTS |
|
112 | 112 | ), |
|
113 | 113 | custom_predicates=(valid_repo_group, valid_integration)) |
|
114 | 114 | config.add_route('repo_group_integrations_edit', |
|
115 | 115 | add_route_requirements( |
|
116 | 116 | '{repo_group_name}/settings/integrations/{integration}/{integration_id}', |
|
117 | 117 | URL_NAME_REQUIREMENTS |
|
118 | 118 | ), |
|
119 | 119 | custom_predicates=(valid_repo_group, valid_integration)) |
|
120 | 120 | for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']: |
|
121 | 121 | config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', |
|
122 | 122 | attr='settings_get', |
|
123 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
123 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
124 | 124 | request_method='GET', |
|
125 | 125 | route_name=route_name) |
|
126 | 126 | config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', |
|
127 | 127 | attr='settings_post', |
|
128 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
128 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
129 | 129 | request_method='POST', |
|
130 | 130 | route_name=route_name) |
|
131 | 131 | |
|
132 | 132 | |
|
133 | 133 | # repo integrations |
|
134 | 134 | config.add_route('repo_integrations_home', |
|
135 | 135 | add_route_requirements( |
|
136 | 136 | '{repo_name}/settings/integrations', |
|
137 | 137 | URL_NAME_REQUIREMENTS |
|
138 | 138 | ), |
|
139 | 139 | custom_predicates=(valid_repo,)) |
|
140 | 140 | config.add_route('repo_integrations_list', |
|
141 | 141 | add_route_requirements( |
|
142 | 142 | '{repo_name}/settings/integrations/{integration}', |
|
143 | 143 | URL_NAME_REQUIREMENTS |
|
144 | 144 | ), |
|
145 | 145 | custom_predicates=(valid_repo, valid_integration)) |
|
146 | 146 | for route_name in ['repo_integrations_home', 'repo_integrations_list']: |
|
147 | 147 | config.add_view('rhodecode.integrations.views.RepoIntegrationsView', |
|
148 | 148 | attr='index', |
|
149 | 149 | request_method='GET', |
|
150 |
renderer='rhodecode:templates/admin/integrations/list. |
|
|
150 | renderer='rhodecode:templates/admin/integrations/list.mako', | |
|
151 | 151 | route_name=route_name) |
|
152 | 152 | |
|
153 | 153 | config.add_route('repo_integrations_new', |
|
154 | 154 | add_route_requirements( |
|
155 | 155 | '{repo_name}/settings/integrations/new', |
|
156 | 156 | URL_NAME_REQUIREMENTS |
|
157 | 157 | ), |
|
158 | 158 | custom_predicates=(valid_repo,)) |
|
159 | 159 | config.add_view('rhodecode.integrations.views.RepoIntegrationsView', |
|
160 | 160 | attr='new_integration', |
|
161 |
renderer='rhodecode:templates/admin/integrations/new. |
|
|
161 | renderer='rhodecode:templates/admin/integrations/new.mako', | |
|
162 | 162 | request_method='GET', |
|
163 | 163 | route_name='repo_integrations_new') |
|
164 | 164 | |
|
165 | 165 | config.add_route('repo_integrations_create', |
|
166 | 166 | add_route_requirements( |
|
167 | 167 | '{repo_name}/settings/integrations/{integration}/new', |
|
168 | 168 | URL_NAME_REQUIREMENTS |
|
169 | 169 | ), |
|
170 | 170 | custom_predicates=(valid_repo, valid_integration)) |
|
171 | 171 | config.add_route('repo_integrations_edit', |
|
172 | 172 | add_route_requirements( |
|
173 | 173 | '{repo_name}/settings/integrations/{integration}/{integration_id}', |
|
174 | 174 | URL_NAME_REQUIREMENTS |
|
175 | 175 | ), |
|
176 | 176 | custom_predicates=(valid_repo, valid_integration)) |
|
177 | 177 | for route_name in ['repo_integrations_edit', 'repo_integrations_create']: |
|
178 | 178 | config.add_view('rhodecode.integrations.views.RepoIntegrationsView', |
|
179 | 179 | attr='settings_get', |
|
180 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
180 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
181 | 181 | request_method='GET', |
|
182 | 182 | route_name=route_name) |
|
183 | 183 | config.add_view('rhodecode.integrations.views.RepoIntegrationsView', |
|
184 | 184 | attr='settings_post', |
|
185 |
renderer='rhodecode:templates/admin/integrations/form. |
|
|
185 | renderer='rhodecode:templates/admin/integrations/form.mako', | |
|
186 | 186 | request_method='POST', |
|
187 | 187 | route_name=route_name) |
|
188 | 188 | |
|
189 | 189 | |
|
190 | 190 | def valid_repo(info, request): |
|
191 | 191 | repo = Repository.get_by_repo_name(info['match']['repo_name']) |
|
192 | 192 | if repo: |
|
193 | 193 | return True |
|
194 | 194 | |
|
195 | 195 | |
|
196 | 196 | def valid_repo_group(info, request): |
|
197 | 197 | repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name']) |
|
198 | 198 | if repo_group: |
|
199 | 199 | return True |
|
200 | 200 | return False |
|
201 | 201 | |
|
202 | 202 | |
|
203 | 203 | def valid_integration(info, request): |
|
204 | 204 | integration_type = info['match']['integration'] |
|
205 | 205 | integration_id = info['match'].get('integration_id') |
|
206 | 206 | repo_name = info['match'].get('repo_name') |
|
207 | 207 | repo_group_name = info['match'].get('repo_group_name') |
|
208 | 208 | |
|
209 | 209 | if integration_type not in integration_type_registry: |
|
210 | 210 | return False |
|
211 | 211 | |
|
212 | 212 | repo, repo_group = None, None |
|
213 | 213 | if repo_name: |
|
214 | 214 | repo = Repository.get_by_repo_name(repo_name) |
|
215 | 215 | if not repo: |
|
216 | 216 | return False |
|
217 | 217 | |
|
218 | 218 | if repo_group_name: |
|
219 | 219 | repo_group = RepoGroup.get_by_group_name(repo_group_name) |
|
220 | 220 | if not repo_group: |
|
221 | 221 | return False |
|
222 | 222 | |
|
223 | 223 | if repo_name and repo_group: |
|
224 | 224 | raise Exception('Either repo or repo_group can be set, not both') |
|
225 | 225 | |
|
226 | 226 | |
|
227 | 227 | if integration_id: |
|
228 | 228 | integration = Integration.get(integration_id) |
|
229 | 229 | if not integration: |
|
230 | 230 | return False |
|
231 | 231 | if integration.integration_type != integration_type: |
|
232 | 232 | return False |
|
233 | 233 | if repo and repo.repo_id != integration.repo_id: |
|
234 | 234 | return False |
|
235 | 235 | if repo_group and repo_group.group_id != integration.repo_group_id: |
|
236 | 236 | return False |
|
237 | 237 | |
|
238 | 238 | return True |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/admin.html to rhodecode/templates/admin/admin.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/admin_log.html to rhodecode/templates/admin/admin_log.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/auth/auth_settings.html to rhodecode/templates/admin/auth/auth_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/auth/plugin_settings.html to rhodecode/templates/admin/auth/plugin_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/defaults/defaults.html to rhodecode/templates/admin/defaults/defaults.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/defaults/defaults_repositories.html to rhodecode/templates/admin/defaults/defaults_repositories.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/gists/edit.html to rhodecode/templates/admin/gists/edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/gists/index.html to rhodecode/templates/admin/gists/index.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/gists/new.html to rhodecode/templates/admin/gists/new.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/gists/show.html to rhodecode/templates/admin/gists/show.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/integrations/base.html to rhodecode/templates/admin/integrations/base.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/integrations/form.html to rhodecode/templates/admin/integrations/form.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/integrations/list.html to rhodecode/templates/admin/integrations/list.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/integrations/new.html to rhodecode/templates/admin/integrations/new.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account.html to rhodecode/templates/admin/my_account/my_account.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_auth_tokens.html to rhodecode/templates/admin/my_account/my_account_auth_tokens.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_emails.html to rhodecode/templates/admin/my_account/my_account_emails.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_notifications.html to rhodecode/templates/admin/my_account/my_account_notifications.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_password.html to rhodecode/templates/admin/my_account/my_account_password.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_perms.html to rhodecode/templates/admin/my_account/my_account_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_profile.html to rhodecode/templates/admin/my_account/my_account_profile.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_profile_edit.html to rhodecode/templates/admin/my_account/my_account_profile_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_pullrequests.html to rhodecode/templates/admin/my_account/my_account_pullrequests.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_repos.html to rhodecode/templates/admin/my_account/my_account_repos.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/my_account/my_account_watched.html to rhodecode/templates/admin/my_account/my_account_watched.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/notifications/notifications.html to rhodecode/templates/admin/notifications/notifications.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/notifications/notifications_data.html to rhodecode/templates/admin/notifications/notifications_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/notifications/show_notification.html to rhodecode/templates/admin/notifications/show_notification.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions.html to rhodecode/templates/admin/permissions/permissions.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions_application.html to rhodecode/templates/admin/permissions/permissions_application.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions_global.html to rhodecode/templates/admin/permissions/permissions_global.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions_ips.html to rhodecode/templates/admin/permissions/permissions_ips.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions_objects.html to rhodecode/templates/admin/permissions/permissions_objects.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/permissions/permissions_perms.html to rhodecode/templates/admin/permissions/permissions_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_group_add.html to rhodecode/templates/admin/repo_groups/repo_group_add.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_group_edit.html to rhodecode/templates/admin/repo_groups/repo_group_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_group_edit_advanced.html to rhodecode/templates/admin/repo_groups/repo_group_edit_advanced.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_group_edit_perms.html to rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_group_edit_settings.html to rhodecode/templates/admin/repo_groups/repo_group_edit_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repo_groups/repo_groups.html to rhodecode/templates/admin/repo_groups/repo_groups.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_add.html to rhodecode/templates/admin/repos/repo_add.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_add_base.html to rhodecode/templates/admin/repos/repo_add_base.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_creating.html to rhodecode/templates/admin/repos/repo_creating.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit.html to rhodecode/templates/admin/repos/repo_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_advanced.html to rhodecode/templates/admin/repos/repo_edit_advanced.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_caches.html to rhodecode/templates/admin/repos/repo_edit_caches.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_fields.html to rhodecode/templates/admin/repos/repo_edit_fields.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_fork.html to rhodecode/templates/admin/repos/repo_edit_fork.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_issuetracker.html to rhodecode/templates/admin/repos/repo_edit_issuetracker.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_permissions.html to rhodecode/templates/admin/repos/repo_edit_permissions.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_remote.html to rhodecode/templates/admin/repos/repo_edit_remote.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_settings.html to rhodecode/templates/admin/repos/repo_edit_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_statistics.html to rhodecode/templates/admin/repos/repo_edit_statistics.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repo_edit_vcs.html to rhodecode/templates/admin/repos/repo_edit_vcs.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/repos/repos.html to rhodecode/templates/admin/repos/repos.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings.html to rhodecode/templates/admin/settings/settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_email.html to rhodecode/templates/admin/settings/settings_email.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_global.html to rhodecode/templates/admin/settings/settings_global.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_hooks.html to rhodecode/templates/admin/settings/settings_hooks.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_issuetracker.html to rhodecode/templates/admin/settings/settings_issuetracker.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_labs.html to rhodecode/templates/admin/settings/settings_labs.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_mapping.html to rhodecode/templates/admin/settings/settings_mapping.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_open_source.html to rhodecode/templates/admin/settings/settings_open_source.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_search.html to rhodecode/templates/admin/settings/settings_search.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_supervisor.html to rhodecode/templates/admin/settings/settings_supervisor.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_supervisor_tail.html to rhodecode/templates/admin/settings/settings_supervisor_tail.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_system.html to rhodecode/templates/admin/settings/settings_system.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_system_snapshot.html to rhodecode/templates/admin/settings/settings_system_snapshot.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_system_update.html to rhodecode/templates/admin/settings/settings_system_update.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_vcs.html to rhodecode/templates/admin/settings/settings_vcs.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/settings/settings_visual.html to rhodecode/templates/admin/settings/settings_visual.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_add.html to rhodecode/templates/admin/user_groups/user_group_add.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit.html to rhodecode/templates/admin/user_groups/user_group_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit_advanced.html to rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit_global_perms.html to rhodecode/templates/admin/user_groups/user_group_edit_global_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit_perms.html to rhodecode/templates/admin/user_groups/user_group_edit_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit_perms_summary.html to rhodecode/templates/admin/user_groups/user_group_edit_perms_summary.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_group_edit_settings.html to rhodecode/templates/admin/user_groups/user_group_edit_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/user_groups/user_groups.html to rhodecode/templates/admin/user_groups/user_groups.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_add.html to rhodecode/templates/admin/users/user_add.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit.html to rhodecode/templates/admin/users/user_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_advanced.html to rhodecode/templates/admin/users/user_edit_advanced.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_auth_tokens.html to rhodecode/templates/admin/users/user_edit_auth_tokens.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_emails.html to rhodecode/templates/admin/users/user_edit_emails.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_global_perms.html to rhodecode/templates/admin/users/user_edit_global_perms.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_ips.html to rhodecode/templates/admin/users/user_edit_ips.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_perms_summary.html to rhodecode/templates/admin/users/user_edit_perms_summary.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/user_edit_profile.html to rhodecode/templates/admin/users/user_edit_profile.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/admin/users/users.html to rhodecode/templates/admin/users/users.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/base.html to rhodecode/templates/base/base.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/default_perms_box.html to rhodecode/templates/base/default_perms_box.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/issue_tracker_settings.html to rhodecode/templates/base/issue_tracker_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/perms_summary.html to rhodecode/templates/base/perms_summary.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/plugins_base.html to rhodecode/templates/base/plugins_base.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/root.html to rhodecode/templates/base/root.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/base/vcs_settings.html to rhodecode/templates/base/vcs_settings.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/bookmarks/bookmarks.html to rhodecode/templates/bookmarks/bookmarks.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/bookmarks/bookmarks_data.html to rhodecode/templates/bookmarks/bookmarks_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/branches/branches.html to rhodecode/templates/branches/branches.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/branches/branches_data.html to rhodecode/templates/branches/branches_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changelog/changelog.html to rhodecode/templates/changelog/changelog.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changelog/changelog_details.html to rhodecode/templates/changelog/changelog_details.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changelog/changelog_file_history.html to rhodecode/templates/changelog/changelog_file_history.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changelog/changelog_summary_data.html to rhodecode/templates/changelog/changelog_summary_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/changeset.html to rhodecode/templates/changeset/changeset.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/changeset_comment_block.html to rhodecode/templates/changeset/changeset_comment_block.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/changeset_file_comment.html to rhodecode/templates/changeset/changeset_file_comment.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/changeset_range.html to rhodecode/templates/changeset/changeset_range.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/diff_block.html to rhodecode/templates/changeset/diff_block.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/changeset/patch_changeset.html to rhodecode/templates/changeset/patch_changeset.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/channelstream/plugin_init.html to rhodecode/templates/channelstream/plugin_init.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/codeblocks/diffs.html to rhodecode/templates/codeblocks/diffs.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/codeblocks/source.html to rhodecode/templates/codeblocks/source.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/compare/compare_commits.html to rhodecode/templates/compare/compare_commits.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/compare/compare_diff.html to rhodecode/templates/compare/compare_diff.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/data_table/_dt_elements.html to rhodecode/templates/data_table/_dt_elements.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/errors/error_document.html to rhodecode/templates/errors/error_document.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/base.html to rhodecode/templates/files/base.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/file_authors_box.html to rhodecode/templates/files/file_authors_box.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/file_tree_author_box.html to rhodecode/templates/files/file_tree_author_box.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/file_tree_detail.html to rhodecode/templates/files/file_tree_detail.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files.html to rhodecode/templates/files/files.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_add.html to rhodecode/templates/files/files_add.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_browser.html to rhodecode/templates/files/files_browser.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_browser_tree.html to rhodecode/templates/files/files_browser_tree.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_delete.html to rhodecode/templates/files/files_delete.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_detail.html to rhodecode/templates/files/files_detail.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_edit.html to rhodecode/templates/files/files_edit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_pjax.html to rhodecode/templates/files/files_pjax.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/files/files_source.html to rhodecode/templates/files/files_source.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/followers/followers.html to rhodecode/templates/followers/followers.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/followers/followers_data.html to rhodecode/templates/followers/followers_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/forks/fork.html to rhodecode/templates/forks/fork.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/forks/forks.html to rhodecode/templates/forks/forks.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/forks/forks_data.html to rhodecode/templates/forks/forks_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/index.html to rhodecode/templates/index.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/index_base.html to rhodecode/templates/index_base.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/index_repo_group.html to rhodecode/templates/index_repo_group.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/journal/journal.html to rhodecode/templates/journal/journal.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/journal/journal_data.html to rhodecode/templates/journal/journal_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/journal/public_journal.html to rhodecode/templates/journal/public_journal.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/login.html to rhodecode/templates/login.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/password_reset.html to rhodecode/templates/password_reset.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/pullrequests/pullrequest.html to rhodecode/templates/pullrequests/pullrequest.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/pullrequests/pullrequest_show.html to rhodecode/templates/pullrequests/pullrequest_show.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/pullrequests/pullrequests.html to rhodecode/templates/pullrequests/pullrequests.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/register.html to rhodecode/templates/register.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/search/search.html to rhodecode/templates/search/search.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/search/search_commit.html to rhodecode/templates/search/search_commit.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/search/search_content.html to rhodecode/templates/search/search_content.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/search/search_path.html to rhodecode/templates/search/search_path.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/search/search_repository.html to rhodecode/templates/search/search_repository.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/summary/base.html to rhodecode/templates/summary/base.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/summary/components.html to rhodecode/templates/summary/components.mako |
|
1 | NO CONTENT: file renamed from rhodecode/templates/summary/missing_requirements.html to rhodecode/templates/summary/missing_requirements.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/summary/summary.html to rhodecode/templates/summary/summary.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/tags/tags.html to rhodecode/templates/tags/tags.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/tags/tags_data.html to rhodecode/templates/tags/tags_data.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/users/user.html to rhodecode/templates/users/user.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/users/user_profile.html to rhodecode/templates/users/user_profile.mako | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file renamed from rhodecode/templates/widgets.html to rhodecode/templates/widgets.mako |
General Comments 0
You need to be logged in to leave comments.
Login now