##// END OF EJS Templates
templating: use .mako as extensions for template files.
marcink -
r1282:90601d74 default
parent child Browse files
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.html')
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html'
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.html', {'c': c}, request=request,
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.html')
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.html')
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.html'),
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.html'),
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.html')
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.html')
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.html'),
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.html')
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.html')
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.html')
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.html'),
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.html'),
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.html'),
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.html'),
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
85 return render('admin/notifications/notifications_data.mako')
86 86
87 return render('admin/notifications/notifications.html')
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.html')
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.html')
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html')
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.html')
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.html')
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.html'),
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.html')
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.html'),
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.html'),
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.html')
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.html'),
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.html')
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.html'),
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.html'),
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.html')
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.html'),
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.html'),
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.html'),
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html')
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.html'),
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.html'),
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.html')
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.html')
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.html'),
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.html')
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.html')
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.html')
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.html')
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.html'),
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.html'),
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.html'),
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.html')
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.html')
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.html'),
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.html')
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.html'),
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.html'),
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.html'),
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.html')
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.html'),
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.html'),
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.html')
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.html')
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.html')
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.html'),
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.html')
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html'),
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.html')
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.html'),
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.html'),
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.html'
38 template = 'bookmarks/bookmarks.html'
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.html'
38 template = 'branches/branches.html'
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')})
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.html')
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.html')
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.html')
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.html')
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.html')
258 return render('files/files_pjax.mako')
259 259
260 return render('files/files.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html'),
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.html'),
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html')
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.html renders links based on the target_repo.
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.html')
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.html')})
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.html')
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.html')
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.html')
176 return render('changelog/changelog_summary_data.mako')
177 177
178 return render('summary/summary.html')
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.html'
35 template = 'tags/tags.html'
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.html')
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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.html',
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