##// 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 # top level files
1 # top level files
2 include test.ini
2 include test.ini
3 include MANIFEST.in
3 include MANIFEST.in
4 include README.rst
4 include README.rst
5 include CHANGES.rst
5 include CHANGES.rst
6 include LICENSE.txt
6 include LICENSE.txt
7
7
8 include rhodecode/VERSION
8 include rhodecode/VERSION
9
9
10 # docs
10 # docs
11 recursive-include docs *
11 recursive-include docs *
12
12
13 # all config files
13 # all config files
14 recursive-include configs *
14 recursive-include configs *
15
15
16 # translations
16 # translations
17 recursive-include rhodecode/i18n *
17 recursive-include rhodecode/i18n *
18
18
19 # hook templates
19 # hook templates
20 recursive-include rhodecode/config/hook_templates *
20 recursive-include rhodecode/config/hook_templates *
21
21
22 # non-python core stuff
22 # non-python core stuff
23 recursive-include rhodecode *.cfg
23 recursive-include rhodecode *.cfg
24 recursive-include rhodecode *.json
24 recursive-include rhodecode *.json
25 recursive-include rhodecode *.ini_tmpl
25 recursive-include rhodecode *.ini_tmpl
26 recursive-include rhodecode *.sh
26 recursive-include rhodecode *.sh
27 recursive-include rhodecode *.mako
27 recursive-include rhodecode *.mako
28
28
29 # 502 page
29 # 502 page
30 include rhodecode/public/502.html
30 include rhodecode/public/502.html
31
31
32 # 502 page
33 include rhodecode/public/502.html
34
32
35 # images, css
33 # images, css
36 include rhodecode/public/css/*.css
34 include rhodecode/public/css/*.css
37 include rhodecode/public/images/*.*
35 include rhodecode/public/images/*.*
38
36
39 # sound files
37 # sound files
40 include rhodecode/public/sounds/*.mp3
38 include rhodecode/public/sounds/*.mp3
41 include rhodecode/public/sounds/*.wav
39 include rhodecode/public/sounds/*.wav
42
40
43 # fonts
41 # fonts
44 recursive-include rhodecode/public/fonts/ProximaNova *
42 recursive-include rhodecode/public/fonts/ProximaNova *
45 recursive-include rhodecode/public/fonts/RCIcons *
43 recursive-include rhodecode/public/fonts/RCIcons *
46
44
47 # js
45 # js
48 recursive-include rhodecode/public/js *
46 recursive-include rhodecode/public/js *
49
47
50 # templates
48 # templates
51 recursive-include rhodecode/templates *
49 recursive-include rhodecode/templates *
52
50
53 # skip any tests files
51 # skip any tests files
54 recursive-exclude rhodecode/tests *
52 recursive-exclude rhodecode/tests *
55
53
@@ -1,82 +1,82 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22 import logging
22 import logging
23
23
24 from pylons import tmpl_context as c
24 from pylons import tmpl_context as c
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 from rhodecode.lib.utils import read_opensource_licenses
29 from rhodecode.lib.utils import read_opensource_licenses
30 from rhodecode.svn_support.utils import generate_mod_dav_svn_config
30 from rhodecode.svn_support.utils import generate_mod_dav_svn_config
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32
32
33 from .navigation import navigation_list
33 from .navigation import navigation_list
34
34
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class AdminSettingsView(object):
39 class AdminSettingsView(object):
40
40
41 def __init__(self, context, request):
41 def __init__(self, context, request):
42 self.request = request
42 self.request = request
43 self.context = context
43 self.context = context
44 self.session = request.session
44 self.session = request.session
45 self._rhodecode_user = request.user
45 self._rhodecode_user = request.user
46
46
47 @LoginRequired()
47 @LoginRequired()
48 @HasPermissionAllDecorator('hg.admin')
48 @HasPermissionAllDecorator('hg.admin')
49 @view_config(
49 @view_config(
50 route_name='admin_settings_open_source', request_method='GET',
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 def open_source_licenses(self):
52 def open_source_licenses(self):
53 c.active = 'open_source'
53 c.active = 'open_source'
54 c.navlist = navigation_list(self.request)
54 c.navlist = navigation_list(self.request)
55 c.opensource_licenses = collections.OrderedDict(
55 c.opensource_licenses = collections.OrderedDict(
56 sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
56 sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
57
57
58 return {}
58 return {}
59
59
60 @LoginRequired()
60 @LoginRequired()
61 @CSRFRequired()
61 @CSRFRequired()
62 @HasPermissionAllDecorator('hg.admin')
62 @HasPermissionAllDecorator('hg.admin')
63 @view_config(
63 @view_config(
64 route_name='admin_settings_vcs_svn_generate_cfg',
64 route_name='admin_settings_vcs_svn_generate_cfg',
65 request_method='POST', renderer='json')
65 request_method='POST', renderer='json')
66 def vcs_svn_generate_config(self):
66 def vcs_svn_generate_config(self):
67 try:
67 try:
68 generate_mod_dav_svn_config(self.request.registry)
68 generate_mod_dav_svn_config(self.request.registry)
69 msg = {
69 msg = {
70 'message': _('Apache configuration for Subversion generated.'),
70 'message': _('Apache configuration for Subversion generated.'),
71 'level': 'success',
71 'level': 'success',
72 }
72 }
73 except Exception:
73 except Exception:
74 log.exception(
74 log.exception(
75 'Exception while generating the Apache configuration for Subversion.')
75 'Exception while generating the Apache configuration for Subversion.')
76 msg = {
76 msg = {
77 'message': _('Failed to generate the Apache configuration for Subversion.'),
77 'message': _('Failed to generate the Apache configuration for Subversion.'),
78 'level': 'error',
78 'level': 'error',
79 }
79 }
80
80
81 data = {'message': msg}
81 data = {'message': msg}
82 return data
82 return data
@@ -1,284 +1,284 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from pylons.i18n.translation import lazy_ugettext as _
31 from pylons.i18n.translation import lazy_ugettext as _
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from sqlalchemy.ext.hybrid import hybrid_property
33
33
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.lib.ext_json import json, formatted_json
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 def plugin_factory(plugin_id, *args, **kwds):
44 def plugin_factory(plugin_id, *args, **kwds):
45 """
45 """
46 Factory function that is called during plugin discovery.
46 Factory function that is called during plugin discovery.
47 It returns the plugin instance.
47 It returns the plugin instance.
48 """
48 """
49 plugin = RhodeCodeAuthPlugin(plugin_id)
49 plugin = RhodeCodeAuthPlugin(plugin_id)
50 return plugin
50 return plugin
51
51
52
52
53 class CrowdAuthnResource(AuthnPluginResourceBase):
53 class CrowdAuthnResource(AuthnPluginResourceBase):
54 pass
54 pass
55
55
56
56
57 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
58 host = colander.SchemaNode(
58 host = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 default='127.0.0.1',
60 default='127.0.0.1',
61 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 description=_('The FQDN or IP of the Atlassian CROWD Server'),
62 preparer=strip_whitespace,
62 preparer=strip_whitespace,
63 title=_('Host'),
63 title=_('Host'),
64 widget='string')
64 widget='string')
65 port = colander.SchemaNode(
65 port = colander.SchemaNode(
66 colander.Int(),
66 colander.Int(),
67 default=8095,
67 default=8095,
68 description=_('The Port in use by the Atlassian CROWD Server'),
68 description=_('The Port in use by the Atlassian CROWD Server'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Port'),
70 title=_('Port'),
71 validator=colander.Range(min=0, max=65536),
71 validator=colander.Range(min=0, max=65536),
72 widget='int')
72 widget='int')
73 app_name = colander.SchemaNode(
73 app_name = colander.SchemaNode(
74 colander.String(),
74 colander.String(),
75 default='',
75 default='',
76 description=_('The Application Name to authenticate to CROWD'),
76 description=_('The Application Name to authenticate to CROWD'),
77 preparer=strip_whitespace,
77 preparer=strip_whitespace,
78 title=_('Application Name'),
78 title=_('Application Name'),
79 widget='string')
79 widget='string')
80 app_password = colander.SchemaNode(
80 app_password = colander.SchemaNode(
81 colander.String(),
81 colander.String(),
82 default='',
82 default='',
83 description=_('The password to authenticate to CROWD'),
83 description=_('The password to authenticate to CROWD'),
84 preparer=strip_whitespace,
84 preparer=strip_whitespace,
85 title=_('Application Password'),
85 title=_('Application Password'),
86 widget='password')
86 widget='password')
87 admin_groups = colander.SchemaNode(
87 admin_groups = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 default='',
89 default='',
90 description=_('A comma separated list of group names that identify '
90 description=_('A comma separated list of group names that identify '
91 'users as RhodeCode Administrators'),
91 'users as RhodeCode Administrators'),
92 missing='',
92 missing='',
93 preparer=strip_whitespace,
93 preparer=strip_whitespace,
94 title=_('Admin Groups'),
94 title=_('Admin Groups'),
95 widget='string')
95 widget='string')
96
96
97
97
98 class CrowdServer(object):
98 class CrowdServer(object):
99 def __init__(self, *args, **kwargs):
99 def __init__(self, *args, **kwargs):
100 """
100 """
101 Create a new CrowdServer object that points to IP/Address 'host',
101 Create a new CrowdServer object that points to IP/Address 'host',
102 on the given port, and using the given method (https/http). user and
102 on the given port, and using the given method (https/http). user and
103 passwd can be set here or with set_credentials. If unspecified,
103 passwd can be set here or with set_credentials. If unspecified,
104 "version" defaults to "latest".
104 "version" defaults to "latest".
105
105
106 example::
106 example::
107
107
108 cserver = CrowdServer(host="127.0.0.1",
108 cserver = CrowdServer(host="127.0.0.1",
109 port="8095",
109 port="8095",
110 user="some_app",
110 user="some_app",
111 passwd="some_passwd",
111 passwd="some_passwd",
112 version="1")
112 version="1")
113 """
113 """
114 if not "port" in kwargs:
114 if not "port" in kwargs:
115 kwargs["port"] = "8095"
115 kwargs["port"] = "8095"
116 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._logger = kwargs.get("logger", logging.getLogger(__name__))
117 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
118 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("host", "127.0.0.1"),
119 kwargs.get("port", "8095"))
119 kwargs.get("port", "8095"))
120 self.set_credentials(kwargs.get("user", ""),
120 self.set_credentials(kwargs.get("user", ""),
121 kwargs.get("passwd", ""))
121 kwargs.get("passwd", ""))
122 self._version = kwargs.get("version", "latest")
122 self._version = kwargs.get("version", "latest")
123 self._url_list = None
123 self._url_list = None
124 self._appname = "crowd"
124 self._appname = "crowd"
125
125
126 def set_credentials(self, user, passwd):
126 def set_credentials(self, user, passwd):
127 self.user = user
127 self.user = user
128 self.passwd = passwd
128 self.passwd = passwd
129 self._make_opener()
129 self._make_opener()
130
130
131 def _make_opener(self):
131 def _make_opener(self):
132 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
133 mgr.add_password(None, self._uri, self.user, self.passwd)
133 mgr.add_password(None, self._uri, self.user, self.passwd)
134 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 handler = urllib2.HTTPBasicAuthHandler(mgr)
135 self.opener = urllib2.build_opener(handler)
135 self.opener = urllib2.build_opener(handler)
136
136
137 def _request(self, url, body=None, headers=None,
137 def _request(self, url, body=None, headers=None,
138 method=None, noformat=False,
138 method=None, noformat=False,
139 empty_response_ok=False):
139 empty_response_ok=False):
140 _headers = {"Content-type": "application/json",
140 _headers = {"Content-type": "application/json",
141 "Accept": "application/json"}
141 "Accept": "application/json"}
142 if self.user and self.passwd:
142 if self.user and self.passwd:
143 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
144 _headers["Authorization"] = "Basic %s" % authstring
144 _headers["Authorization"] = "Basic %s" % authstring
145 if headers:
145 if headers:
146 _headers.update(headers)
146 _headers.update(headers)
147 log.debug("Sent crowd: \n%s"
147 log.debug("Sent crowd: \n%s"
148 % (formatted_json({"url": url, "body": body,
148 % (formatted_json({"url": url, "body": body,
149 "headers": _headers})))
149 "headers": _headers})))
150 request = urllib2.Request(url, body, _headers)
150 request = urllib2.Request(url, body, _headers)
151 if method:
151 if method:
152 request.get_method = lambda: method
152 request.get_method = lambda: method
153
153
154 global msg
154 global msg
155 msg = ""
155 msg = ""
156 try:
156 try:
157 rdoc = self.opener.open(request)
157 rdoc = self.opener.open(request)
158 msg = "".join(rdoc.readlines())
158 msg = "".join(rdoc.readlines())
159 if not msg and empty_response_ok:
159 if not msg and empty_response_ok:
160 rval = {}
160 rval = {}
161 rval["status"] = True
161 rval["status"] = True
162 rval["error"] = "Response body was empty"
162 rval["error"] = "Response body was empty"
163 elif not noformat:
163 elif not noformat:
164 rval = json.loads(msg)
164 rval = json.loads(msg)
165 rval["status"] = True
165 rval["status"] = True
166 else:
166 else:
167 rval = "".join(rdoc.readlines())
167 rval = "".join(rdoc.readlines())
168 except Exception as e:
168 except Exception as e:
169 if not noformat:
169 if not noformat:
170 rval = {"status": False,
170 rval = {"status": False,
171 "body": body,
171 "body": body,
172 "error": str(e) + "\n" + msg}
172 "error": str(e) + "\n" + msg}
173 else:
173 else:
174 rval = None
174 rval = None
175 return rval
175 return rval
176
176
177 def user_auth(self, username, password):
177 def user_auth(self, username, password):
178 """Authenticate a user against crowd. Returns brief information about
178 """Authenticate a user against crowd. Returns brief information about
179 the user."""
179 the user."""
180 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
181 % (self._uri, self._version, username))
181 % (self._uri, self._version, username))
182 body = json.dumps({"value": password})
182 body = json.dumps({"value": password})
183 return self._request(url, body)
183 return self._request(url, body)
184
184
185 def user_groups(self, username):
185 def user_groups(self, username):
186 """Retrieve a list of groups to which this user belongs."""
186 """Retrieve a list of groups to which this user belongs."""
187 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
188 % (self._uri, self._version, username))
188 % (self._uri, self._version, username))
189 return self._request(url)
189 return self._request(url)
190
190
191
191
192 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @hybrid_property
218 @hybrid_property
219 def name(self):
219 def name(self):
220 return "crowd"
220 return "crowd"
221
221
222 def use_fake_password(self):
222 def use_fake_password(self):
223 return True
223 return True
224
224
225 def user_activation_state(self):
225 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser.permissions['global']
226 def_user_perms = User.get_default_user().AuthUser.permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
228
228
229 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
230 """
230 """
231 Given a user object (which may be null), username, a plaintext password,
231 Given a user object (which may be null), username, a plaintext password,
232 and a settings object (containing all the keys needed as listed in settings()),
232 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
233 authenticate this user's login attempt.
234
234
235 Return None on failure. On success, return a dictionary of the form:
235 Return None on failure. On success, return a dictionary of the form:
236
236
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
238 This is later validated for correctness
239 """
239 """
240 if not username or not password:
240 if not username or not password:
241 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
242 return None
242 return None
243
243
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 if not crowd_user["status"]:
249 if not crowd_user["status"]:
250 return None
250 return None
251
251
252 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
255
256 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
264
264
265 user_attrs = {
265 user_attrs = {
266 'username': username,
266 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
270 'email': crowd_user["email"] or email,
270 'email': crowd_user["email"] or email,
271 'admin': admin,
271 'admin': admin,
272 'active': active,
272 'active': active,
273 'active_from_extern': crowd_user.get('active'),
273 'active_from_extern': crowd_user.get('active'),
274 'extern_name': crowd_user["name"],
274 'extern_name': crowd_user["name"],
275 'extern_type': extern_type,
275 'extern_type': extern_type,
276 }
276 }
277
277
278 # set an admin if we're in admin_groups of crowd
278 # set an admin if we're in admin_groups of crowd
279 for group in settings["admin_groups"]:
279 for group in settings["admin_groups"]:
280 if group in user_attrs["groups"]:
280 if group in user_attrs["groups"]:
281 user_attrs["admin"] = True
281 user_attrs["admin"] = True
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.info('user %s authenticated correctly' % user_attrs['username'])
283 log.info('user %s authenticated correctly' % user_attrs['username'])
284 return user_attrs
284 return user_attrs
@@ -1,225 +1,225 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import logging
22 import logging
23
23
24 from sqlalchemy.ext.hybrid import hybrid_property
24 from sqlalchemy.ext.hybrid import hybrid_property
25
25
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.lib.colander_utils import strip_whitespace
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.translation import _
32 from rhodecode.translation import _
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwds):
38 def plugin_factory(plugin_id, *args, **kwds):
39 """
39 """
40 Factory function that is called during plugin discovery.
40 Factory function that is called during plugin discovery.
41 It returns the plugin instance.
41 It returns the plugin instance.
42 """
42 """
43 plugin = RhodeCodeAuthPlugin(plugin_id)
43 plugin = RhodeCodeAuthPlugin(plugin_id)
44 return plugin
44 return plugin
45
45
46
46
47 class HeadersAuthnResource(AuthnPluginResourceBase):
47 class HeadersAuthnResource(AuthnPluginResourceBase):
48 pass
48 pass
49
49
50
50
51 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
52 header = colander.SchemaNode(
52 header = colander.SchemaNode(
53 colander.String(),
53 colander.String(),
54 default='REMOTE_USER',
54 default='REMOTE_USER',
55 description=_('Header to extract the user from'),
55 description=_('Header to extract the user from'),
56 preparer=strip_whitespace,
56 preparer=strip_whitespace,
57 title=_('Header'),
57 title=_('Header'),
58 widget='string')
58 widget='string')
59 fallback_header = colander.SchemaNode(
59 fallback_header = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='HTTP_X_FORWARDED_USER',
61 default='HTTP_X_FORWARDED_USER',
62 description=_('Header to extract the user from when main one fails'),
62 description=_('Header to extract the user from when main one fails'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('Fallback header'),
64 title=_('Fallback header'),
65 widget='string')
65 widget='string')
66 clean_username = colander.SchemaNode(
66 clean_username = colander.SchemaNode(
67 colander.Boolean(),
67 colander.Boolean(),
68 default=True,
68 default=True,
69 description=_('Perform cleaning of user, if passed user has @ in '
69 description=_('Perform cleaning of user, if passed user has @ in '
70 'username then first part before @ is taken. '
70 'username then first part before @ is taken. '
71 'If there\'s \\ in the username only the part after '
71 'If there\'s \\ in the username only the part after '
72 ' \\ is taken'),
72 ' \\ is taken'),
73 missing=False,
73 missing=False,
74 title=_('Clean username'),
74 title=_('Clean username'),
75 widget='bool')
75 widget='bool')
76
76
77
77
78 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=HeadersAuthnResource)
89 context=HeadersAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=HeadersAuthnResource)
96 context=HeadersAuthnResource)
97
97
98 def get_display_name(self):
98 def get_display_name(self):
99 return _('Headers')
99 return _('Headers')
100
100
101 def get_settings_schema(self):
101 def get_settings_schema(self):
102 return HeadersSettingsSchema()
102 return HeadersSettingsSchema()
103
103
104 @hybrid_property
104 @hybrid_property
105 def name(self):
105 def name(self):
106 return 'headers'
106 return 'headers'
107
107
108 @property
108 @property
109 def is_headers_auth(self):
109 def is_headers_auth(self):
110 return True
110 return True
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def user_activation_state(self):
115 def user_activation_state(self):
116 def_user_perms = User.get_default_user().AuthUser.permissions['global']
116 def_user_perms = User.get_default_user().AuthUser.permissions['global']
117 return 'hg.extern_activate.auto' in def_user_perms
117 return 'hg.extern_activate.auto' in def_user_perms
118
118
119 def _clean_username(self, username):
119 def _clean_username(self, username):
120 # Removing realm and domain from username
120 # Removing realm and domain from username
121 username = username.split('@')[0]
121 username = username.split('@')[0]
122 username = username.rsplit('\\')[-1]
122 username = username.rsplit('\\')[-1]
123 return username
123 return username
124
124
125 def _get_username(self, environ, settings):
125 def _get_username(self, environ, settings):
126 username = None
126 username = None
127 environ = environ or {}
127 environ = environ or {}
128 if not environ:
128 if not environ:
129 log.debug('got empty environ: %s' % environ)
129 log.debug('got empty environ: %s' % environ)
130
130
131 settings = settings or {}
131 settings = settings or {}
132 if settings.get('header'):
132 if settings.get('header'):
133 header = settings.get('header')
133 header = settings.get('header')
134 username = environ.get(header)
134 username = environ.get(header)
135 log.debug('extracted %s:%s' % (header, username))
135 log.debug('extracted %s:%s' % (header, username))
136
136
137 # fallback mode
137 # fallback mode
138 if not username and settings.get('fallback_header'):
138 if not username and settings.get('fallback_header'):
139 header = settings.get('fallback_header')
139 header = settings.get('fallback_header')
140 username = environ.get(header)
140 username = environ.get(header)
141 log.debug('extracted %s:%s' % (header, username))
141 log.debug('extracted %s:%s' % (header, username))
142
142
143 if username and str2bool(settings.get('clean_username')):
143 if username and str2bool(settings.get('clean_username')):
144 log.debug('Received username `%s` from headers' % username)
144 log.debug('Received username `%s` from headers' % username)
145 username = self._clean_username(username)
145 username = self._clean_username(username)
146 log.debug('New cleanup user is:%s' % username)
146 log.debug('New cleanup user is:%s' % username)
147 return username
147 return username
148
148
149 def get_user(self, username=None, **kwargs):
149 def get_user(self, username=None, **kwargs):
150 """
150 """
151 Helper method for user fetching in plugins, by default it's using
151 Helper method for user fetching in plugins, by default it's using
152 simple fetch by username, but this method can be custimized in plugins
152 simple fetch by username, but this method can be custimized in plugins
153 eg. headers auth plugin to fetch user by environ params
153 eg. headers auth plugin to fetch user by environ params
154 :param username: username if given to fetch
154 :param username: username if given to fetch
155 :param kwargs: extra arguments needed for user fetching.
155 :param kwargs: extra arguments needed for user fetching.
156 """
156 """
157 environ = kwargs.get('environ') or {}
157 environ = kwargs.get('environ') or {}
158 settings = kwargs.get('settings') or {}
158 settings = kwargs.get('settings') or {}
159 username = self._get_username(environ, settings)
159 username = self._get_username(environ, settings)
160 # we got the username, so use default method now
160 # we got the username, so use default method now
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
162
162
163 def auth(self, userobj, username, password, settings, **kwargs):
163 def auth(self, userobj, username, password, settings, **kwargs):
164 """
164 """
165 Get's the headers_auth username (or email). It tries to get username
165 Get's the headers_auth username (or email). It tries to get username
166 from REMOTE_USER if this plugin is enabled, if that fails
166 from REMOTE_USER if this plugin is enabled, if that fails
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 is set. clean_username extracts the username from this data if it's
168 is set. clean_username extracts the username from this data if it's
169 having @ in it.
169 having @ in it.
170 Return None on failure. On success, return a dictionary of the form:
170 Return None on failure. On success, return a dictionary of the form:
171
171
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
173
173
174 :param userobj:
174 :param userobj:
175 :param username:
175 :param username:
176 :param password:
176 :param password:
177 :param settings:
177 :param settings:
178 :param kwargs:
178 :param kwargs:
179 """
179 """
180 environ = kwargs.get('environ')
180 environ = kwargs.get('environ')
181 if not environ:
181 if not environ:
182 log.debug('Empty environ data skipping...')
182 log.debug('Empty environ data skipping...')
183 return None
183 return None
184
184
185 if not userobj:
185 if not userobj:
186 userobj = self.get_user('', environ=environ, settings=settings)
186 userobj = self.get_user('', environ=environ, settings=settings)
187
187
188 # we don't care passed username/password for headers auth plugins.
188 # we don't care passed username/password for headers auth plugins.
189 # only way to log in is using environ
189 # only way to log in is using environ
190 username = None
190 username = None
191 if userobj:
191 if userobj:
192 username = getattr(userobj, 'username')
192 username = getattr(userobj, 'username')
193
193
194 if not username:
194 if not username:
195 # we don't have any objects in DB user doesn't exist extract
195 # we don't have any objects in DB user doesn't exist extract
196 # username from environ based on the settings
196 # username from environ based on the settings
197 username = self._get_username(environ, settings)
197 username = self._get_username(environ, settings)
198
198
199 # if cannot fetch username, it's a no-go for this plugin to proceed
199 # if cannot fetch username, it's a no-go for this plugin to proceed
200 if not username:
200 if not username:
201 return None
201 return None
202
202
203 # old attrs fetched from RhodeCode database
203 # old attrs fetched from RhodeCode database
204 admin = getattr(userobj, 'admin', False)
204 admin = getattr(userobj, 'admin', False)
205 active = getattr(userobj, 'active', True)
205 active = getattr(userobj, 'active', True)
206 email = getattr(userobj, 'email', '')
206 email = getattr(userobj, 'email', '')
207 firstname = getattr(userobj, 'firstname', '')
207 firstname = getattr(userobj, 'firstname', '')
208 lastname = getattr(userobj, 'lastname', '')
208 lastname = getattr(userobj, 'lastname', '')
209 extern_type = getattr(userobj, 'extern_type', '')
209 extern_type = getattr(userobj, 'extern_type', '')
210
210
211 user_attrs = {
211 user_attrs = {
212 'username': username,
212 'username': username,
213 'firstname': safe_unicode(firstname or username),
213 'firstname': safe_unicode(firstname or username),
214 'lastname': safe_unicode(lastname or ''),
214 'lastname': safe_unicode(lastname or ''),
215 'groups': [],
215 'groups': [],
216 'email': email or '',
216 'email': email or '',
217 'admin': admin or False,
217 'admin': admin or False,
218 'active': active,
218 'active': active,
219 'active_from_extern': True,
219 'active_from_extern': True,
220 'extern_name': username,
220 'extern_name': username,
221 'extern_type': extern_type,
221 'extern_type': extern_type,
222 }
222 }
223
223
224 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 log.info('user `%s` authenticated correctly' % user_attrs['username'])
225 return user_attrs
225 return user_attrs
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib
30 import urllib
31 import urllib2
31 import urllib2
32
32
33 from pylons.i18n.translation import lazy_ugettext as _
33 from pylons.i18n.translation import lazy_ugettext as _
34 from sqlalchemy.ext.hybrid import hybrid_property
34 from sqlalchemy.ext.hybrid import hybrid_property
35
35
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
39 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.colander_utils import strip_whitespace
40 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.lib.utils2 import safe_unicode
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def plugin_factory(plugin_id, *args, **kwds):
46 def plugin_factory(plugin_id, *args, **kwds):
47 """
47 """
48 Factory function that is called during plugin discovery.
48 Factory function that is called during plugin discovery.
49 It returns the plugin instance.
49 It returns the plugin instance.
50 """
50 """
51 plugin = RhodeCodeAuthPlugin(plugin_id)
51 plugin = RhodeCodeAuthPlugin(plugin_id)
52 return plugin
52 return plugin
53
53
54
54
55 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 class JasigCasAuthnResource(AuthnPluginResourceBase):
56 pass
56 pass
57
57
58
58
59 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
60 service_url = colander.SchemaNode(
60 service_url = colander.SchemaNode(
61 colander.String(),
61 colander.String(),
62 default='https://domain.com/cas/v1/tickets',
62 default='https://domain.com/cas/v1/tickets',
63 description=_('The url of the Jasig CAS REST service'),
63 description=_('The url of the Jasig CAS REST service'),
64 preparer=strip_whitespace,
64 preparer=strip_whitespace,
65 title=_('URL'),
65 title=_('URL'),
66 widget='string')
66 widget='string')
67
67
68
68
69 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
70
70
71 def includeme(self, config):
71 def includeme(self, config):
72 config.add_authn_plugin(self)
72 config.add_authn_plugin(self)
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
74 config.add_view(
74 config.add_view(
75 'rhodecode.authentication.views.AuthnPluginViewBase',
75 'rhodecode.authentication.views.AuthnPluginViewBase',
76 attr='settings_get',
76 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
78 request_method='GET',
78 request_method='GET',
79 route_name='auth_home',
79 route_name='auth_home',
80 context=JasigCasAuthnResource)
80 context=JasigCasAuthnResource)
81 config.add_view(
81 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_post',
83 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 request_method='POST',
85 request_method='POST',
86 route_name='auth_home',
86 route_name='auth_home',
87 context=JasigCasAuthnResource)
87 context=JasigCasAuthnResource)
88
88
89 def get_settings_schema(self):
89 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
90 return JasigCasSettingsSchema()
91
91
92 def get_display_name(self):
92 def get_display_name(self):
93 return _('Jasig-CAS')
93 return _('Jasig-CAS')
94
94
95 @hybrid_property
95 @hybrid_property
96 def name(self):
96 def name(self):
97 return "jasig-cas"
97 return "jasig-cas"
98
98
99 @property
99 @property
100 def is_headers_auth(self):
100 def is_headers_auth(self):
101 return True
101 return True
102
102
103 def use_fake_password(self):
103 def use_fake_password(self):
104 return True
104 return True
105
105
106 def user_activation_state(self):
106 def user_activation_state(self):
107 def_user_perms = User.get_default_user().AuthUser.permissions['global']
107 def_user_perms = User.get_default_user().AuthUser.permissions['global']
108 return 'hg.extern_activate.auto' in def_user_perms
108 return 'hg.extern_activate.auto' in def_user_perms
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 """
111 """
112 Given a user object (which may be null), username, a plaintext password,
112 Given a user object (which may be null), username, a plaintext password,
113 and a settings object (containing all the keys needed as listed in settings()),
113 and a settings object (containing all the keys needed as listed in settings()),
114 authenticate this user's login attempt.
114 authenticate this user's login attempt.
115
115
116 Return None on failure. On success, return a dictionary of the form:
116 Return None on failure. On success, return a dictionary of the form:
117
117
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
119 This is later validated for correctness
119 This is later validated for correctness
120 """
120 """
121 if not username or not password:
121 if not username or not password:
122 log.debug('Empty username or password skipping...')
122 log.debug('Empty username or password skipping...')
123 return None
123 return None
124
124
125 log.debug("Jasig CAS settings: %s", settings)
125 log.debug("Jasig CAS settings: %s", settings)
126 params = urllib.urlencode({'username': username, 'password': password})
126 params = urllib.urlencode({'username': username, 'password': password})
127 headers = {"Content-type": "application/x-www-form-urlencoded",
127 headers = {"Content-type": "application/x-www-form-urlencoded",
128 "Accept": "text/plain",
128 "Accept": "text/plain",
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
130 url = settings["service_url"]
130 url = settings["service_url"]
131
131
132 log.debug("Sent Jasig CAS: \n%s",
132 log.debug("Sent Jasig CAS: \n%s",
133 {"url": url, "body": params, "headers": headers})
133 {"url": url, "body": params, "headers": headers})
134 request = urllib2.Request(url, params, headers)
134 request = urllib2.Request(url, params, headers)
135 try:
135 try:
136 response = urllib2.urlopen(request)
136 response = urllib2.urlopen(request)
137 except urllib2.HTTPError as e:
137 except urllib2.HTTPError as e:
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
139 return None
139 return None
140 except urllib2.URLError as e:
140 except urllib2.URLError as e:
141 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 log.debug("URLError when requesting Jasig CAS url: %s " % url)
142 return None
142 return None
143
143
144 # old attrs fetched from RhodeCode database
144 # old attrs fetched from RhodeCode database
145 admin = getattr(userobj, 'admin', False)
145 admin = getattr(userobj, 'admin', False)
146 active = getattr(userobj, 'active', True)
146 active = getattr(userobj, 'active', True)
147 email = getattr(userobj, 'email', '')
147 email = getattr(userobj, 'email', '')
148 username = getattr(userobj, 'username', username)
148 username = getattr(userobj, 'username', username)
149 firstname = getattr(userobj, 'firstname', '')
149 firstname = getattr(userobj, 'firstname', '')
150 lastname = getattr(userobj, 'lastname', '')
150 lastname = getattr(userobj, 'lastname', '')
151 extern_type = getattr(userobj, 'extern_type', '')
151 extern_type = getattr(userobj, 'extern_type', '')
152
152
153 user_attrs = {
153 user_attrs = {
154 'username': username,
154 'username': username,
155 'firstname': safe_unicode(firstname or username),
155 'firstname': safe_unicode(firstname or username),
156 'lastname': safe_unicode(lastname or ''),
156 'lastname': safe_unicode(lastname or ''),
157 'groups': [],
157 'groups': [],
158 'email': email or '',
158 'email': email or '',
159 'admin': admin or False,
159 'admin': admin or False,
160 'active': active,
160 'active': active,
161 'active_from_extern': True,
161 'active_from_extern': True,
162 'extern_name': username,
162 'extern_name': username,
163 'extern_type': extern_type,
163 'extern_type': extern_type,
164 }
164 }
165
165
166 log.info('user %s authenticated correctly' % user_attrs['username'])
166 log.info('user %s authenticated correctly' % user_attrs['username'])
167 return user_attrs
167 return user_attrs
@@ -1,464 +1,464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from pylons.i18n.translation import lazy_ugettext as _
30 from pylons.i18n.translation import lazy_ugettext as _
31 from sqlalchemy.ext.hybrid import hybrid_property
31 from sqlalchemy.ext.hybrid import hybrid_property
32
32
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 )
39 )
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42 from rhodecode.model.validators import Missing
42 from rhodecode.model.validators import Missing
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46 try:
46 try:
47 import ldap
47 import ldap
48 except ImportError:
48 except ImportError:
49 # means that python-ldap is not installed, we use Missing object to mark
49 # means that python-ldap is not installed, we use Missing object to mark
50 # ldap lib is Missing
50 # ldap lib is Missing
51 ldap = Missing
51 ldap = Missing
52
52
53
53
54 def plugin_factory(plugin_id, *args, **kwds):
54 def plugin_factory(plugin_id, *args, **kwds):
55 """
55 """
56 Factory function that is called during plugin discovery.
56 Factory function that is called during plugin discovery.
57 It returns the plugin instance.
57 It returns the plugin instance.
58 """
58 """
59 plugin = RhodeCodeAuthPlugin(plugin_id)
59 plugin = RhodeCodeAuthPlugin(plugin_id)
60 return plugin
60 return plugin
61
61
62
62
63 class LdapAuthnResource(AuthnPluginResourceBase):
63 class LdapAuthnResource(AuthnPluginResourceBase):
64 pass
64 pass
65
65
66
66
67 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
68 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
69 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
70 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
71
71
72 host = colander.SchemaNode(
72 host = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('Host of the LDAP Server'),
75 description=_('Host of the LDAP Server'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('LDAP Host'),
77 title=_('LDAP Host'),
78 widget='string')
78 widget='string')
79 port = colander.SchemaNode(
79 port = colander.SchemaNode(
80 colander.Int(),
80 colander.Int(),
81 default=389,
81 default=389,
82 description=_('Port that the LDAP server is listening on'),
82 description=_('Port that the LDAP server is listening on'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Port'),
84 title=_('Port'),
85 validator=colander.Range(min=0, max=65536),
85 validator=colander.Range(min=0, max=65536),
86 widget='int')
86 widget='int')
87 dn_user = colander.SchemaNode(
87 dn_user = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 default='',
89 default='',
90 description=_('User to connect to LDAP'),
90 description=_('User to connect to LDAP'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Account'),
93 title=_('Account'),
94 widget='string')
94 widget='string')
95 dn_pass = colander.SchemaNode(
95 dn_pass = colander.SchemaNode(
96 colander.String(),
96 colander.String(),
97 default='',
97 default='',
98 description=_('Password to connect to LDAP'),
98 description=_('Password to connect to LDAP'),
99 missing='',
99 missing='',
100 preparer=strip_whitespace,
100 preparer=strip_whitespace,
101 title=_('Password'),
101 title=_('Password'),
102 widget='password')
102 widget='password')
103 tls_kind = colander.SchemaNode(
103 tls_kind = colander.SchemaNode(
104 colander.String(),
104 colander.String(),
105 default=tls_kind_choices[0],
105 default=tls_kind_choices[0],
106 description=_('TLS Type'),
106 description=_('TLS Type'),
107 title=_('Connection Security'),
107 title=_('Connection Security'),
108 validator=colander.OneOf(tls_kind_choices),
108 validator=colander.OneOf(tls_kind_choices),
109 widget='select')
109 widget='select')
110 tls_reqcert = colander.SchemaNode(
110 tls_reqcert = colander.SchemaNode(
111 colander.String(),
111 colander.String(),
112 default=tls_reqcert_choices[0],
112 default=tls_reqcert_choices[0],
113 description=_('Require Cert over TLS?'),
113 description=_('Require Cert over TLS?'),
114 title=_('Certificate Checks'),
114 title=_('Certificate Checks'),
115 validator=colander.OneOf(tls_reqcert_choices),
115 validator=colander.OneOf(tls_reqcert_choices),
116 widget='select')
116 widget='select')
117 base_dn = colander.SchemaNode(
117 base_dn = colander.SchemaNode(
118 colander.String(),
118 colander.String(),
119 default='',
119 default='',
120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
121 missing='',
121 missing='',
122 preparer=strip_whitespace,
122 preparer=strip_whitespace,
123 title=_('Base DN'),
123 title=_('Base DN'),
124 widget='string')
124 widget='string')
125 filter = colander.SchemaNode(
125 filter = colander.SchemaNode(
126 colander.String(),
126 colander.String(),
127 default='',
127 default='',
128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
129 missing='',
129 missing='',
130 preparer=strip_whitespace,
130 preparer=strip_whitespace,
131 title=_('LDAP Search Filter'),
131 title=_('LDAP Search Filter'),
132 widget='string')
132 widget='string')
133 search_scope = colander.SchemaNode(
133 search_scope = colander.SchemaNode(
134 colander.String(),
134 colander.String(),
135 default=search_scope_choices[0],
135 default=search_scope_choices[0],
136 description=_('How deep to search LDAP'),
136 description=_('How deep to search LDAP'),
137 title=_('LDAP Search Scope'),
137 title=_('LDAP Search Scope'),
138 validator=colander.OneOf(search_scope_choices),
138 validator=colander.OneOf(search_scope_choices),
139 widget='select')
139 widget='select')
140 attr_login = colander.SchemaNode(
140 attr_login = colander.SchemaNode(
141 colander.String(),
141 colander.String(),
142 default='',
142 default='',
143 description=_('LDAP Attribute to map to user name'),
143 description=_('LDAP Attribute to map to user name'),
144 preparer=strip_whitespace,
144 preparer=strip_whitespace,
145 title=_('Login Attribute'),
145 title=_('Login Attribute'),
146 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
146 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
147 widget='string')
147 widget='string')
148 attr_firstname = colander.SchemaNode(
148 attr_firstname = colander.SchemaNode(
149 colander.String(),
149 colander.String(),
150 default='',
150 default='',
151 description=_('LDAP Attribute to map to first name'),
151 description=_('LDAP Attribute to map to first name'),
152 missing='',
152 missing='',
153 preparer=strip_whitespace,
153 preparer=strip_whitespace,
154 title=_('First Name Attribute'),
154 title=_('First Name Attribute'),
155 widget='string')
155 widget='string')
156 attr_lastname = colander.SchemaNode(
156 attr_lastname = colander.SchemaNode(
157 colander.String(),
157 colander.String(),
158 default='',
158 default='',
159 description=_('LDAP Attribute to map to last name'),
159 description=_('LDAP Attribute to map to last name'),
160 missing='',
160 missing='',
161 preparer=strip_whitespace,
161 preparer=strip_whitespace,
162 title=_('Last Name Attribute'),
162 title=_('Last Name Attribute'),
163 widget='string')
163 widget='string')
164 attr_email = colander.SchemaNode(
164 attr_email = colander.SchemaNode(
165 colander.String(),
165 colander.String(),
166 default='',
166 default='',
167 description=_('LDAP Attribute to map to email address'),
167 description=_('LDAP Attribute to map to email address'),
168 missing='',
168 missing='',
169 preparer=strip_whitespace,
169 preparer=strip_whitespace,
170 title=_('Email Attribute'),
170 title=_('Email Attribute'),
171 widget='string')
171 widget='string')
172
172
173
173
174 class AuthLdap(object):
174 class AuthLdap(object):
175
175
176 def _build_servers(self):
176 def _build_servers(self):
177 return ', '.join(
177 return ', '.join(
178 ["{}://{}:{}".format(
178 ["{}://{}:{}".format(
179 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
179 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
180 for host in self.SERVER_ADDRESSES])
180 for host in self.SERVER_ADDRESSES])
181
181
182 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
182 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
183 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
183 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
184 search_scope='SUBTREE', attr_login='uid',
184 search_scope='SUBTREE', attr_login='uid',
185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
186 if ldap == Missing:
186 if ldap == Missing:
187 raise LdapImportError("Missing or incompatible ldap library")
187 raise LdapImportError("Missing or incompatible ldap library")
188
188
189 self.debug = False
189 self.debug = False
190 self.ldap_version = ldap_version
190 self.ldap_version = ldap_version
191 self.ldap_server_type = 'ldap'
191 self.ldap_server_type = 'ldap'
192
192
193 self.TLS_KIND = tls_kind
193 self.TLS_KIND = tls_kind
194
194
195 if self.TLS_KIND == 'LDAPS':
195 if self.TLS_KIND == 'LDAPS':
196 port = port or 689
196 port = port or 689
197 self.ldap_server_type += 's'
197 self.ldap_server_type += 's'
198
198
199 OPT_X_TLS_DEMAND = 2
199 OPT_X_TLS_DEMAND = 2
200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
201 OPT_X_TLS_DEMAND)
201 OPT_X_TLS_DEMAND)
202 # split server into list
202 # split server into list
203 self.SERVER_ADDRESSES = server.split(',')
203 self.SERVER_ADDRESSES = server.split(',')
204 self.LDAP_SERVER_PORT = port
204 self.LDAP_SERVER_PORT = port
205
205
206 # USE FOR READ ONLY BIND TO LDAP SERVER
206 # USE FOR READ ONLY BIND TO LDAP SERVER
207 self.attr_login = attr_login
207 self.attr_login = attr_login
208
208
209 self.LDAP_BIND_DN = safe_str(bind_dn)
209 self.LDAP_BIND_DN = safe_str(bind_dn)
210 self.LDAP_BIND_PASS = safe_str(bind_pass)
210 self.LDAP_BIND_PASS = safe_str(bind_pass)
211 self.LDAP_SERVER = self._build_servers()
211 self.LDAP_SERVER = self._build_servers()
212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
213 self.BASE_DN = safe_str(base_dn)
213 self.BASE_DN = safe_str(base_dn)
214 self.LDAP_FILTER = safe_str(ldap_filter)
214 self.LDAP_FILTER = safe_str(ldap_filter)
215
215
216 def _get_ldap_server(self):
216 def _get_ldap_server(self):
217 if self.debug:
217 if self.debug:
218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
221 '/etc/openldap/cacerts')
221 '/etc/openldap/cacerts')
222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
227 if self.TLS_KIND != 'PLAIN':
227 if self.TLS_KIND != 'PLAIN':
228 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
228 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
229 server = ldap.initialize(self.LDAP_SERVER)
229 server = ldap.initialize(self.LDAP_SERVER)
230 if self.ldap_version == 2:
230 if self.ldap_version == 2:
231 server.protocol = ldap.VERSION2
231 server.protocol = ldap.VERSION2
232 else:
232 else:
233 server.protocol = ldap.VERSION3
233 server.protocol = ldap.VERSION3
234
234
235 if self.TLS_KIND == 'START_TLS':
235 if self.TLS_KIND == 'START_TLS':
236 server.start_tls_s()
236 server.start_tls_s()
237
237
238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
239 log.debug('Trying simple_bind with password and given DN: %s',
239 log.debug('Trying simple_bind with password and given DN: %s',
240 self.LDAP_BIND_DN)
240 self.LDAP_BIND_DN)
241 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
241 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
242
242
243 return server
243 return server
244
244
245 def get_uid(self, username):
245 def get_uid(self, username):
246 from rhodecode.lib.helpers import chop_at
246 from rhodecode.lib.helpers import chop_at
247 uid = username
247 uid = username
248 for server_addr in self.SERVER_ADDRESSES:
248 for server_addr in self.SERVER_ADDRESSES:
249 uid = chop_at(username, "@%s" % server_addr)
249 uid = chop_at(username, "@%s" % server_addr)
250 return uid
250 return uid
251
251
252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
253 try:
253 try:
254 log.debug('Trying simple bind with %s', dn)
254 log.debug('Trying simple bind with %s', dn)
255 server.simple_bind_s(dn, safe_str(password))
255 server.simple_bind_s(dn, safe_str(password))
256 user = server.search_ext_s(
256 user = server.search_ext_s(
257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
258 _, attrs = user
258 _, attrs = user
259 return attrs
259 return attrs
260
260
261 except ldap.INVALID_CREDENTIALS:
261 except ldap.INVALID_CREDENTIALS:
262 log.debug(
262 log.debug(
263 "LDAP rejected password for user '%s': %s, org_exc:",
263 "LDAP rejected password for user '%s': %s, org_exc:",
264 username, dn, exc_info=True)
264 username, dn, exc_info=True)
265
265
266 def authenticate_ldap(self, username, password):
266 def authenticate_ldap(self, username, password):
267 """
267 """
268 Authenticate a user via LDAP and return his/her LDAP properties.
268 Authenticate a user via LDAP and return his/her LDAP properties.
269
269
270 Raises AuthenticationError if the credentials are rejected, or
270 Raises AuthenticationError if the credentials are rejected, or
271 EnvironmentError if the LDAP server can't be reached.
271 EnvironmentError if the LDAP server can't be reached.
272
272
273 :param username: username
273 :param username: username
274 :param password: password
274 :param password: password
275 """
275 """
276
276
277 uid = self.get_uid(username)
277 uid = self.get_uid(username)
278
278
279 if not password:
279 if not password:
280 msg = "Authenticating user %s with blank password not allowed"
280 msg = "Authenticating user %s with blank password not allowed"
281 log.warning(msg, username)
281 log.warning(msg, username)
282 raise LdapPasswordError(msg)
282 raise LdapPasswordError(msg)
283 if "," in username:
283 if "," in username:
284 raise LdapUsernameError("invalid character in username: ,")
284 raise LdapUsernameError("invalid character in username: ,")
285 try:
285 try:
286 server = self._get_ldap_server()
286 server = self._get_ldap_server()
287 filter_ = '(&%s(%s=%s))' % (
287 filter_ = '(&%s(%s=%s))' % (
288 self.LDAP_FILTER, self.attr_login, username)
288 self.LDAP_FILTER, self.attr_login, username)
289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
290 filter_, self.LDAP_SERVER)
290 filter_, self.LDAP_SERVER)
291 lobjects = server.search_ext_s(
291 lobjects = server.search_ext_s(
292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
293
293
294 if not lobjects:
294 if not lobjects:
295 raise ldap.NO_SUCH_OBJECT()
295 raise ldap.NO_SUCH_OBJECT()
296
296
297 for (dn, _attrs) in lobjects:
297 for (dn, _attrs) in lobjects:
298 if dn is None:
298 if dn is None:
299 continue
299 continue
300
300
301 user_attrs = self.fetch_attrs_from_simple_bind(
301 user_attrs = self.fetch_attrs_from_simple_bind(
302 server, dn, username, password)
302 server, dn, username, password)
303 if user_attrs:
303 if user_attrs:
304 break
304 break
305
305
306 else:
306 else:
307 log.debug("No matching LDAP objects for authentication "
307 log.debug("No matching LDAP objects for authentication "
308 "of '%s' (%s)", uid, username)
308 "of '%s' (%s)", uid, username)
309 raise LdapPasswordError('Failed to authenticate user '
309 raise LdapPasswordError('Failed to authenticate user '
310 'with given password')
310 'with given password')
311
311
312 except ldap.NO_SUCH_OBJECT:
312 except ldap.NO_SUCH_OBJECT:
313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
314 uid, username, exc_info=True)
314 uid, username, exc_info=True)
315 raise LdapUsernameError()
315 raise LdapUsernameError()
316 except ldap.SERVER_DOWN:
316 except ldap.SERVER_DOWN:
317 org_exc = traceback.format_exc()
317 org_exc = traceback.format_exc()
318 raise LdapConnectionError(
318 raise LdapConnectionError(
319 "LDAP can't access authentication "
319 "LDAP can't access authentication "
320 "server, org_exc:%s" % org_exc)
320 "server, org_exc:%s" % org_exc)
321
321
322 return dn, user_attrs
322 return dn, user_attrs
323
323
324
324
325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
326 # used to define dynamic binding in the
326 # used to define dynamic binding in the
327 DYNAMIC_BIND_VAR = '$login'
327 DYNAMIC_BIND_VAR = '$login'
328
328
329 def includeme(self, config):
329 def includeme(self, config):
330 config.add_authn_plugin(self)
330 config.add_authn_plugin(self)
331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
332 config.add_view(
332 config.add_view(
333 'rhodecode.authentication.views.AuthnPluginViewBase',
333 'rhodecode.authentication.views.AuthnPluginViewBase',
334 attr='settings_get',
334 attr='settings_get',
335 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
335 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
336 request_method='GET',
336 request_method='GET',
337 route_name='auth_home',
337 route_name='auth_home',
338 context=LdapAuthnResource)
338 context=LdapAuthnResource)
339 config.add_view(
339 config.add_view(
340 'rhodecode.authentication.views.AuthnPluginViewBase',
340 'rhodecode.authentication.views.AuthnPluginViewBase',
341 attr='settings_post',
341 attr='settings_post',
342 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
342 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
343 request_method='POST',
343 request_method='POST',
344 route_name='auth_home',
344 route_name='auth_home',
345 context=LdapAuthnResource)
345 context=LdapAuthnResource)
346
346
347 def get_settings_schema(self):
347 def get_settings_schema(self):
348 return LdapSettingsSchema()
348 return LdapSettingsSchema()
349
349
350 def get_display_name(self):
350 def get_display_name(self):
351 return _('LDAP')
351 return _('LDAP')
352
352
353 @hybrid_property
353 @hybrid_property
354 def name(self):
354 def name(self):
355 return "ldap"
355 return "ldap"
356
356
357 def use_fake_password(self):
357 def use_fake_password(self):
358 return True
358 return True
359
359
360 def user_activation_state(self):
360 def user_activation_state(self):
361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
362 return 'hg.extern_activate.auto' in def_user_perms
362 return 'hg.extern_activate.auto' in def_user_perms
363
363
364 def try_dynamic_binding(self, username, password, current_args):
364 def try_dynamic_binding(self, username, password, current_args):
365 """
365 """
366 Detects marker inside our original bind, and uses dynamic auth if
366 Detects marker inside our original bind, and uses dynamic auth if
367 present
367 present
368 """
368 """
369
369
370 org_bind = current_args['bind_dn']
370 org_bind = current_args['bind_dn']
371 passwd = current_args['bind_pass']
371 passwd = current_args['bind_pass']
372
372
373 def has_bind_marker(username):
373 def has_bind_marker(username):
374 if self.DYNAMIC_BIND_VAR in username:
374 if self.DYNAMIC_BIND_VAR in username:
375 return True
375 return True
376
376
377 # we only passed in user with "special" variable
377 # we only passed in user with "special" variable
378 if org_bind and has_bind_marker(org_bind) and not passwd:
378 if org_bind and has_bind_marker(org_bind) and not passwd:
379 log.debug('Using dynamic user/password binding for ldap '
379 log.debug('Using dynamic user/password binding for ldap '
380 'authentication. Replacing `%s` with username',
380 'authentication. Replacing `%s` with username',
381 self.DYNAMIC_BIND_VAR)
381 self.DYNAMIC_BIND_VAR)
382 current_args['bind_dn'] = org_bind.replace(
382 current_args['bind_dn'] = org_bind.replace(
383 self.DYNAMIC_BIND_VAR, username)
383 self.DYNAMIC_BIND_VAR, username)
384 current_args['bind_pass'] = password
384 current_args['bind_pass'] = password
385
385
386 return current_args
386 return current_args
387
387
388 def auth(self, userobj, username, password, settings, **kwargs):
388 def auth(self, userobj, username, password, settings, **kwargs):
389 """
389 """
390 Given a user object (which may be null), username, a plaintext password,
390 Given a user object (which may be null), username, a plaintext password,
391 and a settings object (containing all the keys needed as listed in
391 and a settings object (containing all the keys needed as listed in
392 settings()), authenticate this user's login attempt.
392 settings()), authenticate this user's login attempt.
393
393
394 Return None on failure. On success, return a dictionary of the form:
394 Return None on failure. On success, return a dictionary of the form:
395
395
396 see: RhodeCodeAuthPluginBase.auth_func_attrs
396 see: RhodeCodeAuthPluginBase.auth_func_attrs
397 This is later validated for correctness
397 This is later validated for correctness
398 """
398 """
399
399
400 if not username or not password:
400 if not username or not password:
401 log.debug('Empty username or password skipping...')
401 log.debug('Empty username or password skipping...')
402 return None
402 return None
403
403
404 ldap_args = {
404 ldap_args = {
405 'server': settings.get('host', ''),
405 'server': settings.get('host', ''),
406 'base_dn': settings.get('base_dn', ''),
406 'base_dn': settings.get('base_dn', ''),
407 'port': settings.get('port'),
407 'port': settings.get('port'),
408 'bind_dn': settings.get('dn_user'),
408 'bind_dn': settings.get('dn_user'),
409 'bind_pass': settings.get('dn_pass'),
409 'bind_pass': settings.get('dn_pass'),
410 'tls_kind': settings.get('tls_kind'),
410 'tls_kind': settings.get('tls_kind'),
411 'tls_reqcert': settings.get('tls_reqcert'),
411 'tls_reqcert': settings.get('tls_reqcert'),
412 'search_scope': settings.get('search_scope'),
412 'search_scope': settings.get('search_scope'),
413 'attr_login': settings.get('attr_login'),
413 'attr_login': settings.get('attr_login'),
414 'ldap_version': 3,
414 'ldap_version': 3,
415 'ldap_filter': settings.get('filter'),
415 'ldap_filter': settings.get('filter'),
416 }
416 }
417
417
418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
419
419
420 log.debug('Checking for ldap authentication.')
420 log.debug('Checking for ldap authentication.')
421
421
422 try:
422 try:
423 aldap = AuthLdap(**ldap_args)
423 aldap = AuthLdap(**ldap_args)
424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
425 log.debug('Got ldap DN response %s', user_dn)
425 log.debug('Got ldap DN response %s', user_dn)
426
426
427 def get_ldap_attr(k):
427 def get_ldap_attr(k):
428 return ldap_attrs.get(settings.get(k), [''])[0]
428 return ldap_attrs.get(settings.get(k), [''])[0]
429
429
430 # old attrs fetched from RhodeCode database
430 # old attrs fetched from RhodeCode database
431 admin = getattr(userobj, 'admin', False)
431 admin = getattr(userobj, 'admin', False)
432 active = getattr(userobj, 'active', True)
432 active = getattr(userobj, 'active', True)
433 email = getattr(userobj, 'email', '')
433 email = getattr(userobj, 'email', '')
434 username = getattr(userobj, 'username', username)
434 username = getattr(userobj, 'username', username)
435 firstname = getattr(userobj, 'firstname', '')
435 firstname = getattr(userobj, 'firstname', '')
436 lastname = getattr(userobj, 'lastname', '')
436 lastname = getattr(userobj, 'lastname', '')
437 extern_type = getattr(userobj, 'extern_type', '')
437 extern_type = getattr(userobj, 'extern_type', '')
438
438
439 groups = []
439 groups = []
440 user_attrs = {
440 user_attrs = {
441 'username': username,
441 'username': username,
442 'firstname': safe_unicode(
442 'firstname': safe_unicode(
443 get_ldap_attr('attr_firstname') or firstname),
443 get_ldap_attr('attr_firstname') or firstname),
444 'lastname': safe_unicode(
444 'lastname': safe_unicode(
445 get_ldap_attr('attr_lastname') or lastname),
445 get_ldap_attr('attr_lastname') or lastname),
446 'groups': groups,
446 'groups': groups,
447 'email': get_ldap_attr('attr_email') or email,
447 'email': get_ldap_attr('attr_email') or email,
448 'admin': admin,
448 'admin': admin,
449 'active': active,
449 'active': active,
450 "active_from_extern": None,
450 "active_from_extern": None,
451 'extern_name': user_dn,
451 'extern_name': user_dn,
452 'extern_type': extern_type,
452 'extern_type': extern_type,
453 }
453 }
454 log.debug('ldap user: %s', user_attrs)
454 log.debug('ldap user: %s', user_attrs)
455 log.info('user %s authenticated correctly', user_attrs['username'])
455 log.info('user %s authenticated correctly', user_attrs['username'])
456
456
457 return user_attrs
457 return user_attrs
458
458
459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
460 log.exception("LDAP related exception")
460 log.exception("LDAP related exception")
461 return None
461 return None
462 except (Exception,):
462 except (Exception,):
463 log.exception("Other exception")
463 log.exception("Other exception")
464 return None
464 return None
@@ -1,160 +1,160 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 """
20 """
21 RhodeCode authentication library for PAM
21 RhodeCode authentication library for PAM
22 """
22 """
23
23
24 import colander
24 import colander
25 import grp
25 import grp
26 import logging
26 import logging
27 import pam
27 import pam
28 import pwd
28 import pwd
29 import re
29 import re
30 import socket
30 import socket
31
31
32 from pylons.i18n.translation import lazy_ugettext as _
32 from pylons.i18n.translation import lazy_ugettext as _
33 from sqlalchemy.ext.hybrid import hybrid_property
33 from sqlalchemy.ext.hybrid import hybrid_property
34
34
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class PamAuthnResource(AuthnPluginResourceBase):
52 class PamAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 service = colander.SchemaNode(
57 service = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='login',
59 default='login',
60 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('PAM service name'),
62 title=_('PAM service name'),
63 widget='string')
63 widget='string')
64 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
68 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
71 widget='string')
71 widget='string')
72
72
73
73
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 # PAM authentication can be slow. Repository operations involve a lot of
75 # PAM authentication can be slow. Repository operations involve a lot of
76 # auth calls. Little caching helps speedup push/pull operations significantly
76 # auth calls. Little caching helps speedup push/pull operations significantly
77 AUTH_CACHE_TTL = 4
77 AUTH_CACHE_TTL = 4
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=PamAuthnResource)
88 context=PamAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=PamAuthnResource)
95 context=PamAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('PAM')
98 return _('PAM')
99
99
100 @hybrid_property
100 @hybrid_property
101 def name(self):
101 def name(self):
102 return "pam"
102 return "pam"
103
103
104 def get_settings_schema(self):
104 def get_settings_schema(self):
105 return PamSettingsSchema()
105 return PamSettingsSchema()
106
106
107 def use_fake_password(self):
107 def use_fake_password(self):
108 return True
108 return True
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 if not username or not password:
111 if not username or not password:
112 log.debug('Empty username or password skipping...')
112 log.debug('Empty username or password skipping...')
113 return None
113 return None
114
114
115 auth_result = pam.authenticate(username, password, settings["service"])
115 auth_result = pam.authenticate(username, password, settings["service"])
116
116
117 if not auth_result:
117 if not auth_result:
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
119 return None
119 return None
120
120
121 log.debug('Got PAM response %s' % (auth_result, ))
121 log.debug('Got PAM response %s' % (auth_result, ))
122
122
123 # old attrs fetched from RhodeCode database
123 # old attrs fetched from RhodeCode database
124 default_email = "%s@%s" % (username, socket.gethostname())
124 default_email = "%s@%s" % (username, socket.gethostname())
125 admin = getattr(userobj, 'admin', False)
125 admin = getattr(userobj, 'admin', False)
126 active = getattr(userobj, 'active', True)
126 active = getattr(userobj, 'active', True)
127 email = getattr(userobj, 'email', '') or default_email
127 email = getattr(userobj, 'email', '') or default_email
128 username = getattr(userobj, 'username', username)
128 username = getattr(userobj, 'username', username)
129 firstname = getattr(userobj, 'firstname', '')
129 firstname = getattr(userobj, 'firstname', '')
130 lastname = getattr(userobj, 'lastname', '')
130 lastname = getattr(userobj, 'lastname', '')
131 extern_type = getattr(userobj, 'extern_type', '')
131 extern_type = getattr(userobj, 'extern_type', '')
132
132
133 user_attrs = {
133 user_attrs = {
134 'username': username,
134 'username': username,
135 'firstname': firstname,
135 'firstname': firstname,
136 'lastname': lastname,
136 'lastname': lastname,
137 'groups': [g.gr_name for g in grp.getgrall()
137 'groups': [g.gr_name for g in grp.getgrall()
138 if username in g.gr_mem],
138 if username in g.gr_mem],
139 'email': email,
139 'email': email,
140 'admin': admin,
140 'admin': admin,
141 'active': active,
141 'active': active,
142 'active_from_extern': None,
142 'active_from_extern': None,
143 'extern_name': username,
143 'extern_name': username,
144 'extern_type': extern_type,
144 'extern_type': extern_type,
145 }
145 }
146
146
147 try:
147 try:
148 user_data = pwd.getpwnam(username)
148 user_data = pwd.getpwnam(username)
149 regex = settings["gecos"]
149 regex = settings["gecos"]
150 match = re.search(regex, user_data.pw_gecos)
150 match = re.search(regex, user_data.pw_gecos)
151 if match:
151 if match:
152 user_attrs["firstname"] = match.group('first_name')
152 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["lastname"] = match.group('last_name')
153 user_attrs["lastname"] = match.group('last_name')
154 except Exception:
154 except Exception:
155 log.warning("Cannot extract additional info for PAM user")
155 log.warning("Cannot extract additional info for PAM user")
156 pass
156 pass
157
157
158 log.debug("pamuser: %s", user_attrs)
158 log.debug("pamuser: %s", user_attrs)
159 log.info('user %s authenticated correctly' % user_attrs['username'])
159 log.info('user %s authenticated correctly' % user_attrs['username'])
160 return user_attrs
160 return user_attrs
@@ -1,143 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from pylons.i18n.translation import lazy_ugettext as _
27 from pylons.i18n.translation import lazy_ugettext as _
28 from sqlalchemy.ext.hybrid import hybrid_property
28 from sqlalchemy.ext.hybrid import hybrid_property
29
29
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.lib.utils2 import safe_str
33 from rhodecode.model.db import User
33 from rhodecode.model.db import User
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwds):
38 def plugin_factory(plugin_id, *args, **kwds):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
40 return plugin
41
41
42
42
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
44 pass
45
45
46
46
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48
48
49 def includeme(self, config):
49 def includeme(self, config):
50 config.add_authn_plugin(self)
50 config.add_authn_plugin(self)
51 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
52 config.add_view(
52 config.add_view(
53 'rhodecode.authentication.views.AuthnPluginViewBase',
53 'rhodecode.authentication.views.AuthnPluginViewBase',
54 attr='settings_get',
54 attr='settings_get',
55 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
55 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
56 request_method='GET',
56 request_method='GET',
57 route_name='auth_home',
57 route_name='auth_home',
58 context=RhodecodeAuthnResource)
58 context=RhodecodeAuthnResource)
59 config.add_view(
59 config.add_view(
60 'rhodecode.authentication.views.AuthnPluginViewBase',
60 'rhodecode.authentication.views.AuthnPluginViewBase',
61 attr='settings_post',
61 attr='settings_post',
62 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
62 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 request_method='POST',
63 request_method='POST',
64 route_name='auth_home',
64 route_name='auth_home',
65 context=RhodecodeAuthnResource)
65 context=RhodecodeAuthnResource)
66
66
67 def get_display_name(self):
67 def get_display_name(self):
68 return _('Rhodecode')
68 return _('Rhodecode')
69
69
70 @hybrid_property
70 @hybrid_property
71 def name(self):
71 def name(self):
72 return "rhodecode"
72 return "rhodecode"
73
73
74 def user_activation_state(self):
74 def user_activation_state(self):
75 def_user_perms = User.get_default_user().AuthUser.permissions['global']
75 def_user_perms = User.get_default_user().AuthUser.permissions['global']
76 return 'hg.register.auto_activate' in def_user_perms
76 return 'hg.register.auto_activate' in def_user_perms
77
77
78 def allows_authentication_from(
78 def allows_authentication_from(
79 self, user, allows_non_existing_user=True,
79 self, user, allows_non_existing_user=True,
80 allowed_auth_plugins=None, allowed_auth_sources=None):
80 allowed_auth_plugins=None, allowed_auth_sources=None):
81 """
81 """
82 Custom method for this auth that doesn't accept non existing users.
82 Custom method for this auth that doesn't accept non existing users.
83 We know that user exists in our database.
83 We know that user exists in our database.
84 """
84 """
85 allows_non_existing_user = False
85 allows_non_existing_user = False
86 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
87 user, allows_non_existing_user=allows_non_existing_user)
87 user, allows_non_existing_user=allows_non_existing_user)
88
88
89 def auth(self, userobj, username, password, settings, **kwargs):
89 def auth(self, userobj, username, password, settings, **kwargs):
90 if not userobj:
90 if not userobj:
91 log.debug('userobj was:%s skipping' % (userobj, ))
91 log.debug('userobj was:%s skipping' % (userobj, ))
92 return None
92 return None
93 if userobj.extern_type != self.name:
93 if userobj.extern_type != self.name:
94 log.warning(
94 log.warning(
95 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
95 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
96 (userobj, userobj.extern_type, self.name))
96 (userobj, userobj.extern_type, self.name))
97 return None
97 return None
98
98
99 user_attrs = {
99 user_attrs = {
100 "username": userobj.username,
100 "username": userobj.username,
101 "firstname": userobj.firstname,
101 "firstname": userobj.firstname,
102 "lastname": userobj.lastname,
102 "lastname": userobj.lastname,
103 "groups": [],
103 "groups": [],
104 "email": userobj.email,
104 "email": userobj.email,
105 "admin": userobj.admin,
105 "admin": userobj.admin,
106 "active": userobj.active,
106 "active": userobj.active,
107 "active_from_extern": userobj.active,
107 "active_from_extern": userobj.active,
108 "extern_name": userobj.user_id,
108 "extern_name": userobj.user_id,
109 "extern_type": userobj.extern_type,
109 "extern_type": userobj.extern_type,
110 }
110 }
111
111
112 log.debug("User attributes:%s" % (user_attrs, ))
112 log.debug("User attributes:%s" % (user_attrs, ))
113 if userobj.active:
113 if userobj.active:
114 from rhodecode.lib import auth
114 from rhodecode.lib import auth
115 crypto_backend = auth.crypto_backend()
115 crypto_backend = auth.crypto_backend()
116 password_encoded = safe_str(password)
116 password_encoded = safe_str(password)
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 password_encoded, userobj.password)
118 password_encoded, userobj.password)
119
119
120 if password_match and new_hash:
120 if password_match and new_hash:
121 log.debug('user %s properly authenticated, but '
121 log.debug('user %s properly authenticated, but '
122 'requires hash change to bcrypt', userobj)
122 'requires hash change to bcrypt', userobj)
123 # if password match, and we use OLD deprecated hash,
123 # if password match, and we use OLD deprecated hash,
124 # we should migrate this user hash password to the new hash
124 # we should migrate this user hash password to the new hash
125 # we store the new returned by hash_check_with_upgrade function
125 # we store the new returned by hash_check_with_upgrade function
126 user_attrs['_hash_migrate'] = new_hash
126 user_attrs['_hash_migrate'] = new_hash
127
127
128 if userobj.username == User.DEFAULT_USER and userobj.active:
128 if userobj.username == User.DEFAULT_USER and userobj.active:
129 log.info(
129 log.info(
130 'user %s authenticated correctly as anonymous user', userobj)
130 'user %s authenticated correctly as anonymous user', userobj)
131 return user_attrs
131 return user_attrs
132
132
133 elif userobj.username == username and password_match:
133 elif userobj.username == username and password_match:
134 log.info('user %s authenticated correctly', userobj)
134 log.info('user %s authenticated correctly', userobj)
135 return user_attrs
135 return user_attrs
136 log.info("user %s had a bad password when "
136 log.info("user %s had a bad password when "
137 "authenticating on this plugin", userobj)
137 "authenticating on this plugin", userobj)
138 return None
138 return None
139 else:
139 else:
140 log.warning(
140 log.warning(
141 'user `%s` failed to authenticate via %s, reason: account not '
141 'user `%s` failed to authenticate via %s, reason: account not '
142 'active.', username, self.name)
142 'active.', username, self.name)
143 return None
143 return None
@@ -1,140 +1,140 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from sqlalchemy.ext.hybrid import hybrid_property
27 from sqlalchemy.ext.hybrid import hybrid_property
28
28
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.model.db import User, UserApiKeys
32 from rhodecode.model.db import User, UserApiKeys
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwds):
38 def plugin_factory(plugin_id, *args, **kwds):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
40 return plugin
41
41
42
42
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
44 pass
45
45
46
46
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 """
48 """
49 Enables usage of authentication tokens for vcs operations.
49 Enables usage of authentication tokens for vcs operations.
50 """
50 """
51
51
52 def includeme(self, config):
52 def includeme(self, config):
53 config.add_authn_plugin(self)
53 config.add_authn_plugin(self)
54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
55 config.add_view(
55 config.add_view(
56 'rhodecode.authentication.views.AuthnPluginViewBase',
56 'rhodecode.authentication.views.AuthnPluginViewBase',
57 attr='settings_get',
57 attr='settings_get',
58 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
59 request_method='GET',
59 request_method='GET',
60 route_name='auth_home',
60 route_name='auth_home',
61 context=RhodecodeAuthnResource)
61 context=RhodecodeAuthnResource)
62 config.add_view(
62 config.add_view(
63 'rhodecode.authentication.views.AuthnPluginViewBase',
63 'rhodecode.authentication.views.AuthnPluginViewBase',
64 attr='settings_post',
64 attr='settings_post',
65 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
66 request_method='POST',
66 request_method='POST',
67 route_name='auth_home',
67 route_name='auth_home',
68 context=RhodecodeAuthnResource)
68 context=RhodecodeAuthnResource)
69
69
70 def get_display_name(self):
70 def get_display_name(self):
71 return _('Rhodecode Token Auth')
71 return _('Rhodecode Token Auth')
72
72
73 @hybrid_property
73 @hybrid_property
74 def name(self):
74 def name(self):
75 return "authtoken"
75 return "authtoken"
76
76
77 def user_activation_state(self):
77 def user_activation_state(self):
78 def_user_perms = User.get_default_user().AuthUser.permissions['global']
78 def_user_perms = User.get_default_user().AuthUser.permissions['global']
79 return 'hg.register.auto_activate' in def_user_perms
79 return 'hg.register.auto_activate' in def_user_perms
80
80
81 def allows_authentication_from(
81 def allows_authentication_from(
82 self, user, allows_non_existing_user=True,
82 self, user, allows_non_existing_user=True,
83 allowed_auth_plugins=None, allowed_auth_sources=None):
83 allowed_auth_plugins=None, allowed_auth_sources=None):
84 """
84 """
85 Custom method for this auth that doesn't accept empty users. And also
85 Custom method for this auth that doesn't accept empty users. And also
86 allows users from all other active plugins to use it and also
86 allows users from all other active plugins to use it and also
87 authenticate against it. But only via vcs mode
87 authenticate against it. But only via vcs mode
88 """
88 """
89 from rhodecode.authentication.base import get_authn_registry
89 from rhodecode.authentication.base import get_authn_registry
90 authn_registry = get_authn_registry()
90 authn_registry = get_authn_registry()
91
91
92 active_plugins = set(
92 active_plugins = set(
93 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 [x.name for x in authn_registry.get_plugins_for_authentication()])
94 active_plugins.discard(self.name)
94 active_plugins.discard(self.name)
95
95
96 allowed_auth_plugins = [self.name] + list(active_plugins)
96 allowed_auth_plugins = [self.name] + list(active_plugins)
97 # only for vcs operations
97 # only for vcs operations
98 allowed_auth_sources = [VCS_TYPE]
98 allowed_auth_sources = [VCS_TYPE]
99
99
100 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 user, allows_non_existing_user=False,
101 user, allows_non_existing_user=False,
102 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_plugins=allowed_auth_plugins,
103 allowed_auth_sources=allowed_auth_sources)
103 allowed_auth_sources=allowed_auth_sources)
104
104
105 def auth(self, userobj, username, password, settings, **kwargs):
105 def auth(self, userobj, username, password, settings, **kwargs):
106 if not userobj:
106 if not userobj:
107 log.debug('userobj was:%s skipping' % (userobj, ))
107 log.debug('userobj was:%s skipping' % (userobj, ))
108 return None
108 return None
109
109
110 user_attrs = {
110 user_attrs = {
111 "username": userobj.username,
111 "username": userobj.username,
112 "firstname": userobj.firstname,
112 "firstname": userobj.firstname,
113 "lastname": userobj.lastname,
113 "lastname": userobj.lastname,
114 "groups": [],
114 "groups": [],
115 "email": userobj.email,
115 "email": userobj.email,
116 "admin": userobj.admin,
116 "admin": userobj.admin,
117 "active": userobj.active,
117 "active": userobj.active,
118 "active_from_extern": userobj.active,
118 "active_from_extern": userobj.active,
119 "extern_name": userobj.user_id,
119 "extern_name": userobj.user_id,
120 "extern_type": userobj.extern_type,
120 "extern_type": userobj.extern_type,
121 }
121 }
122
122
123 log.debug('Authenticating user with args %s', user_attrs)
123 log.debug('Authenticating user with args %s', user_attrs)
124 if userobj.active:
124 if userobj.active:
125 role = UserApiKeys.ROLE_VCS
125 role = UserApiKeys.ROLE_VCS
126 active_tokens = [x.api_key for x in
126 active_tokens = [x.api_key for x in
127 User.extra_valid_auth_tokens(userobj, role=role)]
127 User.extra_valid_auth_tokens(userobj, role=role)]
128 if userobj.username == username and password in active_tokens:
128 if userobj.username == username and password in active_tokens:
129 log.info(
129 log.info(
130 'user `%s` successfully authenticated via %s',
130 'user `%s` successfully authenticated via %s',
131 user_attrs['username'], self.name)
131 user_attrs['username'], self.name)
132 return user_attrs
132 return user_attrs
133 log.error(
133 log.error(
134 'user `%s` failed to authenticate via %s, reason: bad or '
134 'user `%s` failed to authenticate via %s, reason: bad or '
135 'inactive token.', username, self.name)
135 'inactive token.', username, self.name)
136 else:
136 else:
137 log.warning(
137 log.warning(
138 'user `%s` failed to authenticate via %s, reason: account not '
138 'user `%s` failed to authenticate via %s, reason: account not '
139 'active.', username, self.name)
139 'active.', username, self.name)
140 return None
140 return None
@@ -1,192 +1,192 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import formencode.htmlfill
22 import formencode.htmlfill
23 import logging
23 import logging
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.authentication.base import (
29 from rhodecode.authentication.base import (
30 get_auth_cache_manager, get_authn_registry)
30 get_auth_cache_manager, get_authn_registry)
31 from rhodecode.lib import auth
31 from rhodecode.lib import auth
32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.model.forms import AuthSettingsForm
33 from rhodecode.model.forms import AuthSettingsForm
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.settings import SettingsModel
35 from rhodecode.model.settings import SettingsModel
36 from rhodecode.translation import _
36 from rhodecode.translation import _
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class AuthnPluginViewBase(object):
41 class AuthnPluginViewBase(object):
42
42
43 def __init__(self, context, request):
43 def __init__(self, context, request):
44 self.request = request
44 self.request = request
45 self.context = context
45 self.context = context
46 self.plugin = context.plugin
46 self.plugin = context.plugin
47 self._rhodecode_user = request.user
47 self._rhodecode_user = request.user
48
48
49 @LoginRequired()
49 @LoginRequired()
50 @HasPermissionAllDecorator('hg.admin')
50 @HasPermissionAllDecorator('hg.admin')
51 def settings_get(self, defaults=None, errors=None):
51 def settings_get(self, defaults=None, errors=None):
52 """
52 """
53 View that displays the plugin settings as a form.
53 View that displays the plugin settings as a form.
54 """
54 """
55 defaults = defaults or {}
55 defaults = defaults or {}
56 errors = errors or {}
56 errors = errors or {}
57 schema = self.plugin.get_settings_schema()
57 schema = self.plugin.get_settings_schema()
58
58
59 # Compute default values for the form. Priority is:
59 # Compute default values for the form. Priority is:
60 # 1. Passed to this method 2. DB value 3. Schema default
60 # 1. Passed to this method 2. DB value 3. Schema default
61 for node in schema:
61 for node in schema:
62 if node.name not in defaults:
62 if node.name not in defaults:
63 defaults[node.name] = self.plugin.get_setting_by_name(
63 defaults[node.name] = self.plugin.get_setting_by_name(
64 node.name, node.default)
64 node.name, node.default)
65
65
66 template_context = {
66 template_context = {
67 'defaults': defaults,
67 'defaults': defaults,
68 'errors': errors,
68 'errors': errors,
69 'plugin': self.context.plugin,
69 'plugin': self.context.plugin,
70 'resource': self.context,
70 'resource': self.context,
71 }
71 }
72
72
73 return template_context
73 return template_context
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @HasPermissionAllDecorator('hg.admin')
76 @HasPermissionAllDecorator('hg.admin')
77 @auth.CSRFRequired()
77 @auth.CSRFRequired()
78 def settings_post(self):
78 def settings_post(self):
79 """
79 """
80 View that validates and stores the plugin settings.
80 View that validates and stores the plugin settings.
81 """
81 """
82 schema = self.plugin.get_settings_schema()
82 schema = self.plugin.get_settings_schema()
83 data = self.request.params
83 data = self.request.params
84
84
85 try:
85 try:
86 valid_data = schema.deserialize(data)
86 valid_data = schema.deserialize(data)
87 except colander.Invalid as e:
87 except colander.Invalid as e:
88 # Display error message and display form again.
88 # Display error message and display form again.
89 self.request.session.flash(
89 self.request.session.flash(
90 _('Errors exist when saving plugin settings. '
90 _('Errors exist when saving plugin settings. '
91 'Please check the form inputs.'),
91 'Please check the form inputs.'),
92 queue='error')
92 queue='error')
93 defaults = {key: data[key] for key in data if key in schema}
93 defaults = {key: data[key] for key in data if key in schema}
94 return self.settings_get(errors=e.asdict(), defaults=defaults)
94 return self.settings_get(errors=e.asdict(), defaults=defaults)
95
95
96 # Store validated data.
96 # Store validated data.
97 for name, value in valid_data.items():
97 for name, value in valid_data.items():
98 self.plugin.create_or_update_setting(name, value)
98 self.plugin.create_or_update_setting(name, value)
99 Session().commit()
99 Session().commit()
100
100
101 # Display success message and redirect.
101 # Display success message and redirect.
102 self.request.session.flash(
102 self.request.session.flash(
103 _('Auth settings updated successfully.'),
103 _('Auth settings updated successfully.'),
104 queue='success')
104 queue='success')
105 redirect_to = self.request.resource_path(
105 redirect_to = self.request.resource_path(
106 self.context, route_name='auth_home')
106 self.context, route_name='auth_home')
107 return HTTPFound(redirect_to)
107 return HTTPFound(redirect_to)
108
108
109
109
110 # TODO: Ongoing migration in these views.
110 # TODO: Ongoing migration in these views.
111 # - Maybe we should also use a colander schema for these views.
111 # - Maybe we should also use a colander schema for these views.
112 class AuthSettingsView(object):
112 class AuthSettingsView(object):
113 def __init__(self, context, request):
113 def __init__(self, context, request):
114 self.context = context
114 self.context = context
115 self.request = request
115 self.request = request
116
116
117 # TODO: Move this into a utility function. It is needed in all view
117 # TODO: Move this into a utility function. It is needed in all view
118 # classes during migration. Maybe a mixin?
118 # classes during migration. Maybe a mixin?
119
119
120 # Some of the decorators rely on this attribute to be present on the
120 # Some of the decorators rely on this attribute to be present on the
121 # class of the decorated method.
121 # class of the decorated method.
122 self._rhodecode_user = request.user
122 self._rhodecode_user = request.user
123
123
124 @LoginRequired()
124 @LoginRequired()
125 @HasPermissionAllDecorator('hg.admin')
125 @HasPermissionAllDecorator('hg.admin')
126 def index(self, defaults=None, errors=None, prefix_error=False):
126 def index(self, defaults=None, errors=None, prefix_error=False):
127 defaults = defaults or {}
127 defaults = defaults or {}
128 authn_registry = get_authn_registry(self.request.registry)
128 authn_registry = get_authn_registry(self.request.registry)
129 enabled_plugins = SettingsModel().get_auth_plugins()
129 enabled_plugins = SettingsModel().get_auth_plugins()
130
130
131 # Create template context and render it.
131 # Create template context and render it.
132 template_context = {
132 template_context = {
133 'resource': self.context,
133 'resource': self.context,
134 'available_plugins': authn_registry.get_plugins(),
134 'available_plugins': authn_registry.get_plugins(),
135 'enabled_plugins': enabled_plugins,
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 template_context,
138 template_context,
139 request=self.request)
139 request=self.request)
140
140
141 # Create form default values and fill the form.
141 # Create form default values and fill the form.
142 form_defaults = {
142 form_defaults = {
143 'auth_plugins': ','.join(enabled_plugins)
143 'auth_plugins': ','.join(enabled_plugins)
144 }
144 }
145 form_defaults.update(defaults)
145 form_defaults.update(defaults)
146 html = formencode.htmlfill.render(
146 html = formencode.htmlfill.render(
147 html,
147 html,
148 defaults=form_defaults,
148 defaults=form_defaults,
149 errors=errors,
149 errors=errors,
150 prefix_error=prefix_error,
150 prefix_error=prefix_error,
151 encoding="UTF-8",
151 encoding="UTF-8",
152 force_defaults=False)
152 force_defaults=False)
153
153
154 return Response(html)
154 return Response(html)
155
155
156 @LoginRequired()
156 @LoginRequired()
157 @HasPermissionAllDecorator('hg.admin')
157 @HasPermissionAllDecorator('hg.admin')
158 @auth.CSRFRequired()
158 @auth.CSRFRequired()
159 def auth_settings(self):
159 def auth_settings(self):
160 try:
160 try:
161 form = AuthSettingsForm()()
161 form = AuthSettingsForm()()
162 form_result = form.to_python(self.request.params)
162 form_result = form.to_python(self.request.params)
163 plugins = ','.join(form_result['auth_plugins'])
163 plugins = ','.join(form_result['auth_plugins'])
164 setting = SettingsModel().create_or_update_setting(
164 setting = SettingsModel().create_or_update_setting(
165 'auth_plugins', plugins)
165 'auth_plugins', plugins)
166 Session().add(setting)
166 Session().add(setting)
167 Session().commit()
167 Session().commit()
168
168
169 cache_manager = get_auth_cache_manager()
169 cache_manager = get_auth_cache_manager()
170 cache_manager.clear()
170 cache_manager.clear()
171 self.request.session.flash(
171 self.request.session.flash(
172 _('Auth settings updated successfully.'),
172 _('Auth settings updated successfully.'),
173 queue='success')
173 queue='success')
174 except formencode.Invalid as errors:
174 except formencode.Invalid as errors:
175 e = errors.error_dict or {}
175 e = errors.error_dict or {}
176 self.request.session.flash(
176 self.request.session.flash(
177 _('Errors exist when saving plugin setting. '
177 _('Errors exist when saving plugin setting. '
178 'Please check the form inputs.'),
178 'Please check the form inputs.'),
179 queue='error')
179 queue='error')
180 return self.index(
180 return self.index(
181 defaults=errors.value,
181 defaults=errors.value,
182 errors=e,
182 errors=e,
183 prefix_error=False)
183 prefix_error=False)
184 except Exception:
184 except Exception:
185 log.exception('Exception in auth_settings')
185 log.exception('Exception in auth_settings')
186 self.request.session.flash(
186 self.request.session.flash(
187 _('Error occurred during update of auth settings.'),
187 _('Error occurred during update of auth settings.'),
188 queue='error')
188 queue='error')
189
189
190 redirect_to = self.request.resource_path(
190 redirect_to = self.request.resource_path(
191 self.context, route_name='auth_home')
191 self.context, route_name='auth_home')
192 return HTTPFound(redirect_to)
192 return HTTPFound(redirect_to)
@@ -1,88 +1,88 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22
22
23 from pyramid.settings import asbool
23 from pyramid.settings import asbool
24
24
25 from rhodecode.config.routing import ADMIN_PREFIX
25 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
27
27
28
28
29 def url_gen(request):
29 def url_gen(request):
30 registry = request.registry
30 registry = request.registry
31 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
31 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
32 ws_url = registry.settings.get('channelstream.ws_url', '')
32 ws_url = registry.settings.get('channelstream.ws_url', '')
33 proxy_url = request.route_url('channelstream_proxy')
33 proxy_url = request.route_url('channelstream_proxy')
34 urls = {
34 urls = {
35 'connect': request.route_path('channelstream_connect'),
35 'connect': request.route_path('channelstream_connect'),
36 'subscribe': request.route_path('channelstream_subscribe'),
36 'subscribe': request.route_path('channelstream_subscribe'),
37 'longpoll': longpoll_url or proxy_url,
37 'longpoll': longpoll_url or proxy_url,
38 'ws': ws_url or proxy_url.replace('http', 'ws')
38 'ws': ws_url or proxy_url.replace('http', 'ws')
39 }
39 }
40 return json.dumps(urls)
40 return json.dumps(urls)
41
41
42
42
43 PLUGIN_DEFINITION = {
43 PLUGIN_DEFINITION = {
44 'name': 'channelstream',
44 'name': 'channelstream',
45 'config': {
45 'config': {
46 'javascript': [],
46 'javascript': [],
47 'css': [],
47 'css': [],
48 'template_hooks': {
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 'url_gen': url_gen,
51 'url_gen': url_gen,
52 'static': None,
52 'static': None,
53 'enabled': False,
53 'enabled': False,
54 'server': '',
54 'server': '',
55 'secret': ''
55 'secret': ''
56 }
56 }
57 }
57 }
58
58
59
59
60 def includeme(config):
60 def includeme(config):
61 settings = config.registry.settings
61 settings = config.registry.settings
62 PLUGIN_DEFINITION['config']['enabled'] = asbool(
62 PLUGIN_DEFINITION['config']['enabled'] = asbool(
63 settings.get('channelstream.enabled'))
63 settings.get('channelstream.enabled'))
64 PLUGIN_DEFINITION['config']['server'] = settings.get(
64 PLUGIN_DEFINITION['config']['server'] = settings.get(
65 'channelstream.server', '')
65 'channelstream.server', '')
66 PLUGIN_DEFINITION['config']['secret'] = settings.get(
66 PLUGIN_DEFINITION['config']['secret'] = settings.get(
67 'channelstream.secret', '')
67 'channelstream.secret', '')
68 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
68 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
69 'channelstream.history.location', '')
69 'channelstream.history.location', '')
70 config.register_rhodecode_plugin(
70 config.register_rhodecode_plugin(
71 PLUGIN_DEFINITION['name'],
71 PLUGIN_DEFINITION['name'],
72 PLUGIN_DEFINITION['config']
72 PLUGIN_DEFINITION['config']
73 )
73 )
74 # create plugin history location
74 # create plugin history location
75 history_dir = PLUGIN_DEFINITION['config']['history.location']
75 history_dir = PLUGIN_DEFINITION['config']['history.location']
76 if history_dir and not os.path.exists(history_dir):
76 if history_dir and not os.path.exists(history_dir):
77 os.makedirs(history_dir, 0750)
77 os.makedirs(history_dir, 0750)
78
78
79 config.add_route(
79 config.add_route(
80 name='channelstream_connect',
80 name='channelstream_connect',
81 pattern=ADMIN_PREFIX + '/channelstream/connect')
81 pattern=ADMIN_PREFIX + '/channelstream/connect')
82 config.add_route(
82 config.add_route(
83 name='channelstream_subscribe',
83 name='channelstream_subscribe',
84 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
84 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
85 config.add_route(
85 config.add_route(
86 name='channelstream_proxy',
86 name='channelstream_proxy',
87 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
87 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
88 config.scan('rhodecode.channelstream')
88 config.scan('rhodecode.channelstream')
@@ -1,480 +1,480 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25 from collections import OrderedDict
25 from collections import OrderedDict
26
26
27 from paste.registry import RegistryManager
27 from paste.registry import RegistryManager
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 from pylons.wsgiapp import PylonsApp
29 from pylons.wsgiapp import PylonsApp
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.config import Configurator
31 from pyramid.config import Configurator
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPError, HTTPInternalServerError, HTTPFound)
35 HTTPError, HTTPInternalServerError, HTTPFound)
36 from pyramid.events import ApplicationCreated
36 from pyramid.events import ApplicationCreated
37 from pyramid.renderers import render_to_response
37 from pyramid.renderers import render_to_response
38 from routes.middleware import RoutesMiddleware
38 from routes.middleware import RoutesMiddleware
39 import routes.util
39 import routes.util
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.model import meta
42 from rhodecode.model import meta
43 from rhodecode.config import patches
43 from rhodecode.config import patches
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 from rhodecode.config.environment import (
45 from rhodecode.config.environment import (
46 load_environment, load_pyramid_environment)
46 load_environment, load_pyramid_environment)
47 from rhodecode.lib.middleware import csrf
47 from rhodecode.lib.middleware import csrf
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.error_handling import (
49 from rhodecode.lib.middleware.error_handling import (
50 PylonsErrorHandlingMiddleware)
50 PylonsErrorHandlingMiddleware)
51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 from rhodecode.lib.middleware.vcs import VCSMiddleware
52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 from rhodecode.subscribers import scan_repositories_if_enabled
55 from rhodecode.subscribers import scan_repositories_if_enabled
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 # this is used to avoid avoid the route lookup overhead in routesmiddleware
61 # this is used to avoid avoid the route lookup overhead in routesmiddleware
62 # for certain routes which won't go to pylons to - eg. static files, debugger
62 # for certain routes which won't go to pylons to - eg. static files, debugger
63 # it is only needed for the pylons migration and can be removed once complete
63 # it is only needed for the pylons migration and can be removed once complete
64 class SkippableRoutesMiddleware(RoutesMiddleware):
64 class SkippableRoutesMiddleware(RoutesMiddleware):
65 """ Routes middleware that allows you to skip prefixes """
65 """ Routes middleware that allows you to skip prefixes """
66
66
67 def __init__(self, *args, **kw):
67 def __init__(self, *args, **kw):
68 self.skip_prefixes = kw.pop('skip_prefixes', [])
68 self.skip_prefixes = kw.pop('skip_prefixes', [])
69 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
69 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
70
70
71 def __call__(self, environ, start_response):
71 def __call__(self, environ, start_response):
72 for prefix in self.skip_prefixes:
72 for prefix in self.skip_prefixes:
73 if environ['PATH_INFO'].startswith(prefix):
73 if environ['PATH_INFO'].startswith(prefix):
74 # added to avoid the case when a missing /_static route falls
74 # added to avoid the case when a missing /_static route falls
75 # through to pylons and causes an exception as pylons is
75 # through to pylons and causes an exception as pylons is
76 # expecting wsgiorg.routingargs to be set in the environ
76 # expecting wsgiorg.routingargs to be set in the environ
77 # by RoutesMiddleware.
77 # by RoutesMiddleware.
78 if 'wsgiorg.routing_args' not in environ:
78 if 'wsgiorg.routing_args' not in environ:
79 environ['wsgiorg.routing_args'] = (None, {})
79 environ['wsgiorg.routing_args'] = (None, {})
80 return self.app(environ, start_response)
80 return self.app(environ, start_response)
81
81
82 return super(SkippableRoutesMiddleware, self).__call__(
82 return super(SkippableRoutesMiddleware, self).__call__(
83 environ, start_response)
83 environ, start_response)
84
84
85
85
86 def make_app(global_conf, static_files=True, **app_conf):
86 def make_app(global_conf, static_files=True, **app_conf):
87 """Create a Pylons WSGI application and return it
87 """Create a Pylons WSGI application and return it
88
88
89 ``global_conf``
89 ``global_conf``
90 The inherited configuration for this application. Normally from
90 The inherited configuration for this application. Normally from
91 the [DEFAULT] section of the Paste ini file.
91 the [DEFAULT] section of the Paste ini file.
92
92
93 ``app_conf``
93 ``app_conf``
94 The application's local configuration. Normally specified in
94 The application's local configuration. Normally specified in
95 the [app:<name>] section of the Paste ini file (where <name>
95 the [app:<name>] section of the Paste ini file (where <name>
96 defaults to main).
96 defaults to main).
97
97
98 """
98 """
99 # Apply compatibility patches
99 # Apply compatibility patches
100 patches.kombu_1_5_1_python_2_7_11()
100 patches.kombu_1_5_1_python_2_7_11()
101 patches.inspect_getargspec()
101 patches.inspect_getargspec()
102
102
103 # Configure the Pylons environment
103 # Configure the Pylons environment
104 config = load_environment(global_conf, app_conf)
104 config = load_environment(global_conf, app_conf)
105
105
106 # The Pylons WSGI app
106 # The Pylons WSGI app
107 app = PylonsApp(config=config)
107 app = PylonsApp(config=config)
108 if rhodecode.is_test:
108 if rhodecode.is_test:
109 app = csrf.CSRFDetector(app)
109 app = csrf.CSRFDetector(app)
110
110
111 expected_origin = config.get('expected_origin')
111 expected_origin = config.get('expected_origin')
112 if expected_origin:
112 if expected_origin:
113 # The API can be accessed from other Origins.
113 # The API can be accessed from other Origins.
114 app = csrf.OriginChecker(app, expected_origin,
114 app = csrf.OriginChecker(app, expected_origin,
115 skip_urls=[routes.util.url_for('api')])
115 skip_urls=[routes.util.url_for('api')])
116
116
117 # Establish the Registry for this application
117 # Establish the Registry for this application
118 app = RegistryManager(app)
118 app = RegistryManager(app)
119
119
120 app.config = config
120 app.config = config
121
121
122 return app
122 return app
123
123
124
124
125 def make_pyramid_app(global_config, **settings):
125 def make_pyramid_app(global_config, **settings):
126 """
126 """
127 Constructs the WSGI application based on Pyramid and wraps the Pylons based
127 Constructs the WSGI application based on Pyramid and wraps the Pylons based
128 application.
128 application.
129
129
130 Specials:
130 Specials:
131
131
132 * We migrate from Pylons to Pyramid. While doing this, we keep both
132 * We migrate from Pylons to Pyramid. While doing this, we keep both
133 frameworks functional. This involves moving some WSGI middlewares around
133 frameworks functional. This involves moving some WSGI middlewares around
134 and providing access to some data internals, so that the old code is
134 and providing access to some data internals, so that the old code is
135 still functional.
135 still functional.
136
136
137 * The application can also be integrated like a plugin via the call to
137 * The application can also be integrated like a plugin via the call to
138 `includeme`. This is accompanied with the other utility functions which
138 `includeme`. This is accompanied with the other utility functions which
139 are called. Changing this should be done with great care to not break
139 are called. Changing this should be done with great care to not break
140 cases when these fragments are assembled from another place.
140 cases when these fragments are assembled from another place.
141
141
142 """
142 """
143 # The edition string should be available in pylons too, so we add it here
143 # The edition string should be available in pylons too, so we add it here
144 # before copying the settings.
144 # before copying the settings.
145 settings.setdefault('rhodecode.edition', 'Community Edition')
145 settings.setdefault('rhodecode.edition', 'Community Edition')
146
146
147 # As long as our Pylons application does expect "unprepared" settings, make
147 # As long as our Pylons application does expect "unprepared" settings, make
148 # sure that we keep an unmodified copy. This avoids unintentional change of
148 # sure that we keep an unmodified copy. This avoids unintentional change of
149 # behavior in the old application.
149 # behavior in the old application.
150 settings_pylons = settings.copy()
150 settings_pylons = settings.copy()
151
151
152 sanitize_settings_and_apply_defaults(settings)
152 sanitize_settings_and_apply_defaults(settings)
153 config = Configurator(settings=settings)
153 config = Configurator(settings=settings)
154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
155
155
156 load_pyramid_environment(global_config, settings)
156 load_pyramid_environment(global_config, settings)
157
157
158 includeme_first(config)
158 includeme_first(config)
159 includeme(config)
159 includeme(config)
160 pyramid_app = config.make_wsgi_app()
160 pyramid_app = config.make_wsgi_app()
161 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
161 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
162 pyramid_app.config = config
162 pyramid_app.config = config
163
163
164 # creating the app uses a connection - return it after we are done
164 # creating the app uses a connection - return it after we are done
165 meta.Session.remove()
165 meta.Session.remove()
166
166
167 return pyramid_app
167 return pyramid_app
168
168
169
169
170 def make_not_found_view(config):
170 def make_not_found_view(config):
171 """
171 """
172 This creates the view which should be registered as not-found-view to
172 This creates the view which should be registered as not-found-view to
173 pyramid. Basically it contains of the old pylons app, converted to a view.
173 pyramid. Basically it contains of the old pylons app, converted to a view.
174 Additionally it is wrapped by some other middlewares.
174 Additionally it is wrapped by some other middlewares.
175 """
175 """
176 settings = config.registry.settings
176 settings = config.registry.settings
177 vcs_server_enabled = settings['vcs.server.enable']
177 vcs_server_enabled = settings['vcs.server.enable']
178
178
179 # Make pylons app from unprepared settings.
179 # Make pylons app from unprepared settings.
180 pylons_app = make_app(
180 pylons_app = make_app(
181 config.registry._pylons_compat_global_config,
181 config.registry._pylons_compat_global_config,
182 **config.registry._pylons_compat_settings)
182 **config.registry._pylons_compat_settings)
183 config.registry._pylons_compat_config = pylons_app.config
183 config.registry._pylons_compat_config = pylons_app.config
184
184
185 # Appenlight monitoring.
185 # Appenlight monitoring.
186 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
186 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
187 pylons_app, settings)
187 pylons_app, settings)
188
188
189 # The pylons app is executed inside of the pyramid 404 exception handler.
189 # The pylons app is executed inside of the pyramid 404 exception handler.
190 # Exceptions which are raised inside of it are not handled by pyramid
190 # Exceptions which are raised inside of it are not handled by pyramid
191 # again. Therefore we add a middleware that invokes the error handler in
191 # again. Therefore we add a middleware that invokes the error handler in
192 # case of an exception or error response. This way we return proper error
192 # case of an exception or error response. This way we return proper error
193 # HTML pages in case of an error.
193 # HTML pages in case of an error.
194 reraise = (settings.get('debugtoolbar.enabled', False) or
194 reraise = (settings.get('debugtoolbar.enabled', False) or
195 rhodecode.disable_error_handler)
195 rhodecode.disable_error_handler)
196 pylons_app = PylonsErrorHandlingMiddleware(
196 pylons_app = PylonsErrorHandlingMiddleware(
197 pylons_app, error_handler, reraise)
197 pylons_app, error_handler, reraise)
198
198
199 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
199 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
200 # view to handle the request. Therefore it is wrapped around the pylons
200 # view to handle the request. Therefore it is wrapped around the pylons
201 # app. It has to be outside of the error handling otherwise error responses
201 # app. It has to be outside of the error handling otherwise error responses
202 # from the vcsserver are converted to HTML error pages. This confuses the
202 # from the vcsserver are converted to HTML error pages. This confuses the
203 # command line tools and the user won't get a meaningful error message.
203 # command line tools and the user won't get a meaningful error message.
204 if vcs_server_enabled:
204 if vcs_server_enabled:
205 pylons_app = VCSMiddleware(
205 pylons_app = VCSMiddleware(
206 pylons_app, settings, appenlight_client, registry=config.registry)
206 pylons_app, settings, appenlight_client, registry=config.registry)
207
207
208 # Convert WSGI app to pyramid view and return it.
208 # Convert WSGI app to pyramid view and return it.
209 return wsgiapp(pylons_app)
209 return wsgiapp(pylons_app)
210
210
211
211
212 def add_pylons_compat_data(registry, global_config, settings):
212 def add_pylons_compat_data(registry, global_config, settings):
213 """
213 """
214 Attach data to the registry to support the Pylons integration.
214 Attach data to the registry to support the Pylons integration.
215 """
215 """
216 registry._pylons_compat_global_config = global_config
216 registry._pylons_compat_global_config = global_config
217 registry._pylons_compat_settings = settings
217 registry._pylons_compat_settings = settings
218
218
219
219
220 def error_handler(exception, request):
220 def error_handler(exception, request):
221 from rhodecode.model.settings import SettingsModel
221 from rhodecode.model.settings import SettingsModel
222 from rhodecode.lib.utils2 import AttributeDict
222 from rhodecode.lib.utils2 import AttributeDict
223
223
224 try:
224 try:
225 rc_config = SettingsModel().get_all_settings()
225 rc_config = SettingsModel().get_all_settings()
226 except Exception:
226 except Exception:
227 log.exception('failed to fetch settings')
227 log.exception('failed to fetch settings')
228 rc_config = {}
228 rc_config = {}
229
229
230 base_response = HTTPInternalServerError()
230 base_response = HTTPInternalServerError()
231 # prefer original exception for the response since it may have headers set
231 # prefer original exception for the response since it may have headers set
232 if isinstance(exception, HTTPError):
232 if isinstance(exception, HTTPError):
233 base_response = exception
233 base_response = exception
234
234
235 c = AttributeDict()
235 c = AttributeDict()
236 c.error_message = base_response.status
236 c.error_message = base_response.status
237 c.error_explanation = base_response.explanation or str(base_response)
237 c.error_explanation = base_response.explanation or str(base_response)
238 c.visual = AttributeDict()
238 c.visual = AttributeDict()
239
239
240 c.visual.rhodecode_support_url = (
240 c.visual.rhodecode_support_url = (
241 request.registry.settings.get('rhodecode_support_url') or
241 request.registry.settings.get('rhodecode_support_url') or
242 request.route_url('rhodecode_support')
242 request.route_url('rhodecode_support')
243 )
243 )
244 c.redirect_time = 0
244 c.redirect_time = 0
245 c.rhodecode_name = rc_config.get('rhodecode_title', '')
245 c.rhodecode_name = rc_config.get('rhodecode_title', '')
246 if not c.rhodecode_name:
246 if not c.rhodecode_name:
247 c.rhodecode_name = 'Rhodecode'
247 c.rhodecode_name = 'Rhodecode'
248
248
249 c.causes = []
249 c.causes = []
250 if hasattr(base_response, 'causes'):
250 if hasattr(base_response, 'causes'):
251 c.causes = base_response.causes
251 c.causes = base_response.causes
252
252
253 response = render_to_response(
253 response = render_to_response(
254 '/errors/error_document.html', {'c': c}, request=request,
254 '/errors/error_document.mako', {'c': c}, request=request,
255 response=base_response)
255 response=base_response)
256
256
257 return response
257 return response
258
258
259
259
260 def includeme(config):
260 def includeme(config):
261 settings = config.registry.settings
261 settings = config.registry.settings
262
262
263 # plugin information
263 # plugin information
264 config.registry.rhodecode_plugins = OrderedDict()
264 config.registry.rhodecode_plugins = OrderedDict()
265
265
266 config.add_directive(
266 config.add_directive(
267 'register_rhodecode_plugin', register_rhodecode_plugin)
267 'register_rhodecode_plugin', register_rhodecode_plugin)
268
268
269 if asbool(settings.get('appenlight', 'false')):
269 if asbool(settings.get('appenlight', 'false')):
270 config.include('appenlight_client.ext.pyramid_tween')
270 config.include('appenlight_client.ext.pyramid_tween')
271
271
272 # Includes which are required. The application would fail without them.
272 # Includes which are required. The application would fail without them.
273 config.include('pyramid_mako')
273 config.include('pyramid_mako')
274 config.include('pyramid_beaker')
274 config.include('pyramid_beaker')
275 config.include('rhodecode.channelstream')
275 config.include('rhodecode.channelstream')
276 config.include('rhodecode.admin')
276 config.include('rhodecode.admin')
277 config.include('rhodecode.authentication')
277 config.include('rhodecode.authentication')
278 config.include('rhodecode.integrations')
278 config.include('rhodecode.integrations')
279 config.include('rhodecode.login')
279 config.include('rhodecode.login')
280 config.include('rhodecode.tweens')
280 config.include('rhodecode.tweens')
281 config.include('rhodecode.api')
281 config.include('rhodecode.api')
282 config.include('rhodecode.svn_support')
282 config.include('rhodecode.svn_support')
283 config.add_route(
283 config.add_route(
284 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
284 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
285
285
286 # Add subscribers.
286 # Add subscribers.
287 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
287 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
288
288
289 # Set the authorization policy.
289 # Set the authorization policy.
290 authz_policy = ACLAuthorizationPolicy()
290 authz_policy = ACLAuthorizationPolicy()
291 config.set_authorization_policy(authz_policy)
291 config.set_authorization_policy(authz_policy)
292
292
293 # Set the default renderer for HTML templates to mako.
293 # Set the default renderer for HTML templates to mako.
294 config.add_mako_renderer('.html')
294 config.add_mako_renderer('.html')
295
295
296 # include RhodeCode plugins
296 # include RhodeCode plugins
297 includes = aslist(settings.get('rhodecode.includes', []))
297 includes = aslist(settings.get('rhodecode.includes', []))
298 for inc in includes:
298 for inc in includes:
299 config.include(inc)
299 config.include(inc)
300
300
301 # This is the glue which allows us to migrate in chunks. By registering the
301 # This is the glue which allows us to migrate in chunks. By registering the
302 # pylons based application as the "Not Found" view in Pyramid, we will
302 # pylons based application as the "Not Found" view in Pyramid, we will
303 # fallback to the old application each time the new one does not yet know
303 # fallback to the old application each time the new one does not yet know
304 # how to handle a request.
304 # how to handle a request.
305 config.add_notfound_view(make_not_found_view(config))
305 config.add_notfound_view(make_not_found_view(config))
306
306
307 if not settings.get('debugtoolbar.enabled', False):
307 if not settings.get('debugtoolbar.enabled', False):
308 # if no toolbar, then any exception gets caught and rendered
308 # if no toolbar, then any exception gets caught and rendered
309 config.add_view(error_handler, context=Exception)
309 config.add_view(error_handler, context=Exception)
310
310
311 config.add_view(error_handler, context=HTTPError)
311 config.add_view(error_handler, context=HTTPError)
312
312
313
313
314 def includeme_first(config):
314 def includeme_first(config):
315 # redirect automatic browser favicon.ico requests to correct place
315 # redirect automatic browser favicon.ico requests to correct place
316 def favicon_redirect(context, request):
316 def favicon_redirect(context, request):
317 return HTTPFound(
317 return HTTPFound(
318 request.static_path('rhodecode:public/images/favicon.ico'))
318 request.static_path('rhodecode:public/images/favicon.ico'))
319
319
320 config.add_view(favicon_redirect, route_name='favicon')
320 config.add_view(favicon_redirect, route_name='favicon')
321 config.add_route('favicon', '/favicon.ico')
321 config.add_route('favicon', '/favicon.ico')
322
322
323 config.add_static_view(
323 config.add_static_view(
324 '_static/deform', 'deform:static')
324 '_static/deform', 'deform:static')
325 config.add_static_view(
325 config.add_static_view(
326 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
326 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
327
327
328
328
329 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
329 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
330 """
330 """
331 Apply outer WSGI middlewares around the application.
331 Apply outer WSGI middlewares around the application.
332
332
333 Part of this has been moved up from the Pylons layer, so that the
333 Part of this has been moved up from the Pylons layer, so that the
334 data is also available if old Pylons code is hit through an already ported
334 data is also available if old Pylons code is hit through an already ported
335 view.
335 view.
336 """
336 """
337 settings = config.registry.settings
337 settings = config.registry.settings
338
338
339 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
339 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
340 pyramid_app = HttpsFixup(pyramid_app, settings)
340 pyramid_app = HttpsFixup(pyramid_app, settings)
341
341
342 # Add RoutesMiddleware to support the pylons compatibility tween during
342 # Add RoutesMiddleware to support the pylons compatibility tween during
343 # migration to pyramid.
343 # migration to pyramid.
344 pyramid_app = SkippableRoutesMiddleware(
344 pyramid_app = SkippableRoutesMiddleware(
345 pyramid_app, config.registry._pylons_compat_config['routes.map'],
345 pyramid_app, config.registry._pylons_compat_config['routes.map'],
346 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
346 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
347
347
348 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
348 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
349
349
350 if settings['gzip_responses']:
350 if settings['gzip_responses']:
351 pyramid_app = make_gzip_middleware(
351 pyramid_app = make_gzip_middleware(
352 pyramid_app, settings, compress_level=1)
352 pyramid_app, settings, compress_level=1)
353
353
354
354
355 # this should be the outer most middleware in the wsgi stack since
355 # this should be the outer most middleware in the wsgi stack since
356 # middleware like Routes make database calls
356 # middleware like Routes make database calls
357 def pyramid_app_with_cleanup(environ, start_response):
357 def pyramid_app_with_cleanup(environ, start_response):
358 try:
358 try:
359 return pyramid_app(environ, start_response)
359 return pyramid_app(environ, start_response)
360 finally:
360 finally:
361 # Dispose current database session and rollback uncommitted
361 # Dispose current database session and rollback uncommitted
362 # transactions.
362 # transactions.
363 meta.Session.remove()
363 meta.Session.remove()
364
364
365 # In a single threaded mode server, on non sqlite db we should have
365 # In a single threaded mode server, on non sqlite db we should have
366 # '0 Current Checked out connections' at the end of a request,
366 # '0 Current Checked out connections' at the end of a request,
367 # if not, then something, somewhere is leaving a connection open
367 # if not, then something, somewhere is leaving a connection open
368 pool = meta.Base.metadata.bind.engine.pool
368 pool = meta.Base.metadata.bind.engine.pool
369 log.debug('sa pool status: %s', pool.status())
369 log.debug('sa pool status: %s', pool.status())
370
370
371
371
372 return pyramid_app_with_cleanup
372 return pyramid_app_with_cleanup
373
373
374
374
375 def sanitize_settings_and_apply_defaults(settings):
375 def sanitize_settings_and_apply_defaults(settings):
376 """
376 """
377 Applies settings defaults and does all type conversion.
377 Applies settings defaults and does all type conversion.
378
378
379 We would move all settings parsing and preparation into this place, so that
379 We would move all settings parsing and preparation into this place, so that
380 we have only one place left which deals with this part. The remaining parts
380 we have only one place left which deals with this part. The remaining parts
381 of the application would start to rely fully on well prepared settings.
381 of the application would start to rely fully on well prepared settings.
382
382
383 This piece would later be split up per topic to avoid a big fat monster
383 This piece would later be split up per topic to avoid a big fat monster
384 function.
384 function.
385 """
385 """
386
386
387 # Pyramid's mako renderer has to search in the templates folder so that the
387 # Pyramid's mako renderer has to search in the templates folder so that the
388 # old templates still work. Ported and new templates are expected to use
388 # old templates still work. Ported and new templates are expected to use
389 # real asset specifications for the includes.
389 # real asset specifications for the includes.
390 mako_directories = settings.setdefault('mako.directories', [
390 mako_directories = settings.setdefault('mako.directories', [
391 # Base templates of the original Pylons application
391 # Base templates of the original Pylons application
392 'rhodecode:templates',
392 'rhodecode:templates',
393 ])
393 ])
394 log.debug(
394 log.debug(
395 "Using the following Mako template directories: %s",
395 "Using the following Mako template directories: %s",
396 mako_directories)
396 mako_directories)
397
397
398 # Default includes, possible to change as a user
398 # Default includes, possible to change as a user
399 pyramid_includes = settings.setdefault('pyramid.includes', [
399 pyramid_includes = settings.setdefault('pyramid.includes', [
400 'rhodecode.lib.middleware.request_wrapper',
400 'rhodecode.lib.middleware.request_wrapper',
401 ])
401 ])
402 log.debug(
402 log.debug(
403 "Using the following pyramid.includes: %s",
403 "Using the following pyramid.includes: %s",
404 pyramid_includes)
404 pyramid_includes)
405
405
406 # TODO: johbo: Re-think this, usually the call to config.include
406 # TODO: johbo: Re-think this, usually the call to config.include
407 # should allow to pass in a prefix.
407 # should allow to pass in a prefix.
408 settings.setdefault('rhodecode.api.url', '/_admin/api')
408 settings.setdefault('rhodecode.api.url', '/_admin/api')
409
409
410 # Sanitize generic settings.
410 # Sanitize generic settings.
411 _list_setting(settings, 'default_encoding', 'UTF-8')
411 _list_setting(settings, 'default_encoding', 'UTF-8')
412 _bool_setting(settings, 'is_test', 'false')
412 _bool_setting(settings, 'is_test', 'false')
413 _bool_setting(settings, 'gzip_responses', 'false')
413 _bool_setting(settings, 'gzip_responses', 'false')
414
414
415 # Call split out functions that sanitize settings for each topic.
415 # Call split out functions that sanitize settings for each topic.
416 _sanitize_appenlight_settings(settings)
416 _sanitize_appenlight_settings(settings)
417 _sanitize_vcs_settings(settings)
417 _sanitize_vcs_settings(settings)
418
418
419 return settings
419 return settings
420
420
421
421
422 def _sanitize_appenlight_settings(settings):
422 def _sanitize_appenlight_settings(settings):
423 _bool_setting(settings, 'appenlight', 'false')
423 _bool_setting(settings, 'appenlight', 'false')
424
424
425
425
426 def _sanitize_vcs_settings(settings):
426 def _sanitize_vcs_settings(settings):
427 """
427 """
428 Applies settings defaults and does type conversion for all VCS related
428 Applies settings defaults and does type conversion for all VCS related
429 settings.
429 settings.
430 """
430 """
431 _string_setting(settings, 'vcs.svn.compatible_version', '')
431 _string_setting(settings, 'vcs.svn.compatible_version', '')
432 _string_setting(settings, 'git_rev_filter', '--all')
432 _string_setting(settings, 'git_rev_filter', '--all')
433 _string_setting(settings, 'vcs.hooks.protocol', 'http')
433 _string_setting(settings, 'vcs.hooks.protocol', 'http')
434 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
434 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
435 _string_setting(settings, 'vcs.server', '')
435 _string_setting(settings, 'vcs.server', '')
436 _string_setting(settings, 'vcs.server.log_level', 'debug')
436 _string_setting(settings, 'vcs.server.log_level', 'debug')
437 _string_setting(settings, 'vcs.server.protocol', 'http')
437 _string_setting(settings, 'vcs.server.protocol', 'http')
438 _bool_setting(settings, 'startup.import_repos', 'false')
438 _bool_setting(settings, 'startup.import_repos', 'false')
439 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
439 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
440 _bool_setting(settings, 'vcs.server.enable', 'true')
440 _bool_setting(settings, 'vcs.server.enable', 'true')
441 _bool_setting(settings, 'vcs.start_server', 'false')
441 _bool_setting(settings, 'vcs.start_server', 'false')
442 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
442 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
443 _int_setting(settings, 'vcs.connection_timeout', 3600)
443 _int_setting(settings, 'vcs.connection_timeout', 3600)
444
444
445 # Support legacy values of vcs.scm_app_implementation. Legacy
445 # Support legacy values of vcs.scm_app_implementation. Legacy
446 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
446 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
447 # which is now mapped to 'http'.
447 # which is now mapped to 'http'.
448 scm_app_impl = settings['vcs.scm_app_implementation']
448 scm_app_impl = settings['vcs.scm_app_implementation']
449 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
449 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
450 settings['vcs.scm_app_implementation'] = 'http'
450 settings['vcs.scm_app_implementation'] = 'http'
451
451
452
452
453 def _int_setting(settings, name, default):
453 def _int_setting(settings, name, default):
454 settings[name] = int(settings.get(name, default))
454 settings[name] = int(settings.get(name, default))
455
455
456
456
457 def _bool_setting(settings, name, default):
457 def _bool_setting(settings, name, default):
458 input = settings.get(name, default)
458 input = settings.get(name, default)
459 if isinstance(input, unicode):
459 if isinstance(input, unicode):
460 input = input.encode('utf8')
460 input = input.encode('utf8')
461 settings[name] = asbool(input)
461 settings[name] = asbool(input)
462
462
463
463
464 def _list_setting(settings, name, default):
464 def _list_setting(settings, name, default):
465 raw_value = settings.get(name, default)
465 raw_value = settings.get(name, default)
466
466
467 old_separator = ','
467 old_separator = ','
468 if old_separator in raw_value:
468 if old_separator in raw_value:
469 # If we get a comma separated list, pass it to our own function.
469 # If we get a comma separated list, pass it to our own function.
470 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
470 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
471 else:
471 else:
472 # Otherwise we assume it uses pyramids space/newline separation.
472 # Otherwise we assume it uses pyramids space/newline separation.
473 settings[name] = aslist(raw_value)
473 settings[name] = aslist(raw_value)
474
474
475
475
476 def _string_setting(settings, name, default, lower=True):
476 def _string_setting(settings, name, default, lower=True):
477 value = settings.get(name, default)
477 value = settings.get(name, default)
478 if lower:
478 if lower:
479 value = value.lower()
479 value = value.lower()
480 settings[name] = value
480 settings[name] = value
@@ -1,173 +1,173 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Controller for Admin panel of RhodeCode Enterprise
22 Controller for Admin panel of RhodeCode Enterprise
23 """
23 """
24
24
25
25
26 import logging
26 import logging
27
27
28 from pylons import request, tmpl_context as c, url
28 from pylons import request, tmpl_context as c, url
29 from pylons.controllers.util import redirect
29 from pylons.controllers.util import redirect
30 from sqlalchemy.orm import joinedload
30 from sqlalchemy.orm import joinedload
31 from whoosh.qparser.default import QueryParser, query
31 from whoosh.qparser.default import QueryParser, query
32 from whoosh.qparser.dateparse import DateParserPlugin
32 from whoosh.qparser.dateparse import DateParserPlugin
33 from whoosh.fields import (TEXT, Schema, DATETIME)
33 from whoosh.fields import (TEXT, Schema, DATETIME)
34 from sqlalchemy.sql.expression import or_, and_, func
34 from sqlalchemy.sql.expression import or_, and_, func
35
35
36 from rhodecode.model.db import UserLog, PullRequest
36 from rhodecode.model.db import UserLog, PullRequest
37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from rhodecode.lib.base import BaseController, render
38 from rhodecode.lib.base import BaseController, render
39 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
39 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
40 from rhodecode.lib.helpers import Page
40 from rhodecode.lib.helpers import Page
41
41
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
45 # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
46 # querylang to build sql queries and filter journals
46 # querylang to build sql queries and filter journals
47 JOURNAL_SCHEMA = Schema(
47 JOURNAL_SCHEMA = Schema(
48 username=TEXT(),
48 username=TEXT(),
49 date=DATETIME(),
49 date=DATETIME(),
50 action=TEXT(),
50 action=TEXT(),
51 repository=TEXT(),
51 repository=TEXT(),
52 ip=TEXT(),
52 ip=TEXT(),
53 )
53 )
54
54
55
55
56 def _journal_filter(user_log, search_term):
56 def _journal_filter(user_log, search_term):
57 """
57 """
58 Filters sqlalchemy user_log based on search_term with whoosh Query language
58 Filters sqlalchemy user_log based on search_term with whoosh Query language
59 http://packages.python.org/Whoosh/querylang.html
59 http://packages.python.org/Whoosh/querylang.html
60
60
61 :param user_log:
61 :param user_log:
62 :param search_term:
62 :param search_term:
63 """
63 """
64 log.debug('Initial search term: %r' % search_term)
64 log.debug('Initial search term: %r' % search_term)
65 qry = None
65 qry = None
66 if search_term:
66 if search_term:
67 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
67 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
68 qp.add_plugin(DateParserPlugin())
68 qp.add_plugin(DateParserPlugin())
69 qry = qp.parse(unicode(search_term))
69 qry = qp.parse(unicode(search_term))
70 log.debug('Filtering using parsed query %r' % qry)
70 log.debug('Filtering using parsed query %r' % qry)
71
71
72 def wildcard_handler(col, wc_term):
72 def wildcard_handler(col, wc_term):
73 if wc_term.startswith('*') and not wc_term.endswith('*'):
73 if wc_term.startswith('*') and not wc_term.endswith('*'):
74 # postfix == endswith
74 # postfix == endswith
75 wc_term = remove_prefix(wc_term, prefix='*')
75 wc_term = remove_prefix(wc_term, prefix='*')
76 return func.lower(col).endswith(wc_term)
76 return func.lower(col).endswith(wc_term)
77 elif wc_term.startswith('*') and wc_term.endswith('*'):
77 elif wc_term.startswith('*') and wc_term.endswith('*'):
78 # wildcard == ilike
78 # wildcard == ilike
79 wc_term = remove_prefix(wc_term, prefix='*')
79 wc_term = remove_prefix(wc_term, prefix='*')
80 wc_term = remove_suffix(wc_term, suffix='*')
80 wc_term = remove_suffix(wc_term, suffix='*')
81 return func.lower(col).contains(wc_term)
81 return func.lower(col).contains(wc_term)
82
82
83 def get_filterion(field, val, term):
83 def get_filterion(field, val, term):
84
84
85 if field == 'repository':
85 if field == 'repository':
86 field = getattr(UserLog, 'repository_name')
86 field = getattr(UserLog, 'repository_name')
87 elif field == 'ip':
87 elif field == 'ip':
88 field = getattr(UserLog, 'user_ip')
88 field = getattr(UserLog, 'user_ip')
89 elif field == 'date':
89 elif field == 'date':
90 field = getattr(UserLog, 'action_date')
90 field = getattr(UserLog, 'action_date')
91 elif field == 'username':
91 elif field == 'username':
92 field = getattr(UserLog, 'username')
92 field = getattr(UserLog, 'username')
93 else:
93 else:
94 field = getattr(UserLog, field)
94 field = getattr(UserLog, field)
95 log.debug('filter field: %s val=>%s' % (field, val))
95 log.debug('filter field: %s val=>%s' % (field, val))
96
96
97 # sql filtering
97 # sql filtering
98 if isinstance(term, query.Wildcard):
98 if isinstance(term, query.Wildcard):
99 return wildcard_handler(field, val)
99 return wildcard_handler(field, val)
100 elif isinstance(term, query.Prefix):
100 elif isinstance(term, query.Prefix):
101 return func.lower(field).startswith(func.lower(val))
101 return func.lower(field).startswith(func.lower(val))
102 elif isinstance(term, query.DateRange):
102 elif isinstance(term, query.DateRange):
103 return and_(field >= val[0], field <= val[1])
103 return and_(field >= val[0], field <= val[1])
104 return func.lower(field) == func.lower(val)
104 return func.lower(field) == func.lower(val)
105
105
106 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
106 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
107 query.DateRange)):
107 query.DateRange)):
108 if not isinstance(qry, query.And):
108 if not isinstance(qry, query.And):
109 qry = [qry]
109 qry = [qry]
110 for term in qry:
110 for term in qry:
111 field = term.fieldname
111 field = term.fieldname
112 val = (term.text if not isinstance(term, query.DateRange)
112 val = (term.text if not isinstance(term, query.DateRange)
113 else [term.startdate, term.enddate])
113 else [term.startdate, term.enddate])
114 user_log = user_log.filter(get_filterion(field, val, term))
114 user_log = user_log.filter(get_filterion(field, val, term))
115 elif isinstance(qry, query.Or):
115 elif isinstance(qry, query.Or):
116 filters = []
116 filters = []
117 for term in qry:
117 for term in qry:
118 field = term.fieldname
118 field = term.fieldname
119 val = (term.text if not isinstance(term, query.DateRange)
119 val = (term.text if not isinstance(term, query.DateRange)
120 else [term.startdate, term.enddate])
120 else [term.startdate, term.enddate])
121 filters.append(get_filterion(field, val, term))
121 filters.append(get_filterion(field, val, term))
122 user_log = user_log.filter(or_(*filters))
122 user_log = user_log.filter(or_(*filters))
123
123
124 return user_log
124 return user_log
125
125
126
126
127 class AdminController(BaseController):
127 class AdminController(BaseController):
128
128
129 @LoginRequired()
129 @LoginRequired()
130 def __before__(self):
130 def __before__(self):
131 super(AdminController, self).__before__()
131 super(AdminController, self).__before__()
132
132
133 @HasPermissionAllDecorator('hg.admin')
133 @HasPermissionAllDecorator('hg.admin')
134 def index(self):
134 def index(self):
135 users_log = UserLog.query()\
135 users_log = UserLog.query()\
136 .options(joinedload(UserLog.user))\
136 .options(joinedload(UserLog.user))\
137 .options(joinedload(UserLog.repository))
137 .options(joinedload(UserLog.repository))
138
138
139 # FILTERING
139 # FILTERING
140 c.search_term = request.GET.get('filter')
140 c.search_term = request.GET.get('filter')
141 try:
141 try:
142 users_log = _journal_filter(users_log, c.search_term)
142 users_log = _journal_filter(users_log, c.search_term)
143 except Exception:
143 except Exception:
144 # we want this to crash for now
144 # we want this to crash for now
145 raise
145 raise
146
146
147 users_log = users_log.order_by(UserLog.action_date.desc())
147 users_log = users_log.order_by(UserLog.action_date.desc())
148
148
149 p = safe_int(request.GET.get('page', 1), 1)
149 p = safe_int(request.GET.get('page', 1), 1)
150
150
151 def url_generator(**kw):
151 def url_generator(**kw):
152 return url.current(filter=c.search_term, **kw)
152 return url.current(filter=c.search_term, **kw)
153
153
154 c.users_log = Page(users_log, page=p, items_per_page=10,
154 c.users_log = Page(users_log, page=p, items_per_page=10,
155 url=url_generator)
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 if request.is_xhr:
158 if request.is_xhr:
159 return c.log_data
159 return c.log_data
160 return render('admin/admin.html')
160 return render('admin/admin.mako')
161
161
162 # global redirect doesn't need permissions
162 # global redirect doesn't need permissions
163 def pull_requests(self, pull_request_id):
163 def pull_requests(self, pull_request_id):
164 """
164 """
165 Global redirect for Pull Requests
165 Global redirect for Pull Requests
166
166
167 :param pull_request_id: id of pull requests in the system
167 :param pull_request_id: id of pull requests in the system
168 """
168 """
169 pull_request = PullRequest.get_or_404(pull_request_id)
169 pull_request = PullRequest.get_or_404(pull_request_id)
170 repo_name = pull_request.target_repo.repo_name
170 repo_name = pull_request.target_repo.repo_name
171 return redirect(url(
171 return redirect(url(
172 'pullrequest_show', repo_name=repo_name,
172 'pullrequest_show', repo_name=repo_name,
173 pull_request_id=pull_request_id))
173 pull_request_id=pull_request_id))
@@ -1,102 +1,102 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 default settings controller for RhodeCode Enterprise
22 default settings controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import logging
25 import logging
26 import formencode
26 import formencode
27 from formencode import htmlfill
27 from formencode import htmlfill
28
28
29 from pylons import request, tmpl_context as c, url
29 from pylons import request, tmpl_context as c, url
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from rhodecode.lib import auth
33 from rhodecode.lib import auth
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
36 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.model.forms import DefaultsForm
37 from rhodecode.model.forms import DefaultsForm
38 from rhodecode.model.meta import Session
38 from rhodecode.model.meta import Session
39 from rhodecode import BACKENDS
39 from rhodecode import BACKENDS
40 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.settings import SettingsModel
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class DefaultsController(BaseController):
45 class DefaultsController(BaseController):
46
46
47 @LoginRequired()
47 @LoginRequired()
48 def __before__(self):
48 def __before__(self):
49 super(DefaultsController, self).__before__()
49 super(DefaultsController, self).__before__()
50
50
51 @HasPermissionAllDecorator('hg.admin')
51 @HasPermissionAllDecorator('hg.admin')
52 def index(self):
52 def index(self):
53 """GET /defaults: All items in the collection"""
53 """GET /defaults: All items in the collection"""
54 # url('admin_defaults_repositories')
54 # url('admin_defaults_repositories')
55 c.backends = BACKENDS.keys()
55 c.backends = BACKENDS.keys()
56 c.active = 'repositories'
56 c.active = 'repositories'
57 defaults = SettingsModel().get_default_repo_settings()
57 defaults = SettingsModel().get_default_repo_settings()
58
58
59 return htmlfill.render(
59 return htmlfill.render(
60 render('admin/defaults/defaults.html'),
60 render('admin/defaults/defaults.mako'),
61 defaults=defaults,
61 defaults=defaults,
62 encoding="UTF-8",
62 encoding="UTF-8",
63 force_defaults=False
63 force_defaults=False
64 )
64 )
65
65
66 @HasPermissionAllDecorator('hg.admin')
66 @HasPermissionAllDecorator('hg.admin')
67 @auth.CSRFRequired()
67 @auth.CSRFRequired()
68 def update_repository_defaults(self):
68 def update_repository_defaults(self):
69 """PUT /defaults/repositories: Update an existing item"""
69 """PUT /defaults/repositories: Update an existing item"""
70 # Forms posted to this method should contain a hidden field:
70 # Forms posted to this method should contain a hidden field:
71 # Or using helpers:
71 # Or using helpers:
72 # h.form(url('admin_defaults_repositories'),
72 # h.form(url('admin_defaults_repositories'),
73 # method='post')
73 # method='post')
74 # url('admin_defaults_repositories')
74 # url('admin_defaults_repositories')
75 c.active = 'repositories'
75 c.active = 'repositories'
76 _form = DefaultsForm()()
76 _form = DefaultsForm()()
77
77
78 try:
78 try:
79 form_result = _form.to_python(dict(request.POST))
79 form_result = _form.to_python(dict(request.POST))
80 for k, v in form_result.iteritems():
80 for k, v in form_result.iteritems():
81 setting = SettingsModel().create_or_update_setting(k, v)
81 setting = SettingsModel().create_or_update_setting(k, v)
82 Session().add(setting)
82 Session().add(setting)
83 Session().commit()
83 Session().commit()
84 h.flash(_('Default settings updated successfully'),
84 h.flash(_('Default settings updated successfully'),
85 category='success')
85 category='success')
86
86
87 except formencode.Invalid as errors:
87 except formencode.Invalid as errors:
88 defaults = errors.value
88 defaults = errors.value
89
89
90 return htmlfill.render(
90 return htmlfill.render(
91 render('admin/defaults/defaults.html'),
91 render('admin/defaults/defaults.mako'),
92 defaults=defaults,
92 defaults=defaults,
93 errors=errors.error_dict or {},
93 errors=errors.error_dict or {},
94 prefix_error=False,
94 prefix_error=False,
95 encoding="UTF-8",
95 encoding="UTF-8",
96 force_defaults=False)
96 force_defaults=False)
97 except Exception:
97 except Exception:
98 log.exception('Exception in update action')
98 log.exception('Exception in update action')
99 h.flash(_('Error occurred during update of default values'),
99 h.flash(_('Error occurred during update of default values'),
100 category='error')
100 category='error')
101
101
102 return redirect(url('admin_defaults_repositories'))
102 return redirect(url('admin_defaults_repositories'))
@@ -1,365 +1,365 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 gist controller for RhodeCode
23 gist controller for RhodeCode
24 """
24 """
25
25
26 import time
26 import time
27 import logging
27 import logging
28
28
29 import formencode
29 import formencode
30 import peppercorn
30 import peppercorn
31
31
32 from pylons import request, response, tmpl_context as c, url
32 from pylons import request, response, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPNotFound, HTTPForbidden
35 from webob.exc import HTTPNotFound, HTTPForbidden
36 from sqlalchemy.sql.expression import or_
36 from sqlalchemy.sql.expression import or_
37
37
38
38
39 from rhodecode.model.gist import GistModel
39 from rhodecode.model.gist import GistModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.db import Gist, User
41 from rhodecode.model.db import Gist, User
42 from rhodecode.lib import auth
42 from rhodecode.lib import auth
43 from rhodecode.lib import helpers as h
43 from rhodecode.lib import helpers as h
44 from rhodecode.lib.base import BaseController, render
44 from rhodecode.lib.base import BaseController, render
45 from rhodecode.lib.auth import LoginRequired, NotAnonymous
45 from rhodecode.lib.auth import LoginRequired, NotAnonymous
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import time_to_datetime
47 from rhodecode.lib.utils2 import time_to_datetime
48 from rhodecode.lib.ext_json import json
48 from rhodecode.lib.ext_json import json
49 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
49 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
50 from rhodecode.model import validation_schema
50 from rhodecode.model import validation_schema
51 from rhodecode.model.validation_schema.schemas import gist_schema
51 from rhodecode.model.validation_schema.schemas import gist_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class GistsController(BaseController):
57 class GistsController(BaseController):
58 """REST Controller styled on the Atom Publishing Protocol"""
58 """REST Controller styled on the Atom Publishing Protocol"""
59
59
60 def __load_defaults(self, extra_values=None):
60 def __load_defaults(self, extra_values=None):
61 c.lifetime_values = [
61 c.lifetime_values = [
62 (-1, _('forever')),
62 (-1, _('forever')),
63 (5, _('5 minutes')),
63 (5, _('5 minutes')),
64 (60, _('1 hour')),
64 (60, _('1 hour')),
65 (60 * 24, _('1 day')),
65 (60 * 24, _('1 day')),
66 (60 * 24 * 30, _('1 month')),
66 (60 * 24 * 30, _('1 month')),
67 ]
67 ]
68 if extra_values:
68 if extra_values:
69 c.lifetime_values.append(extra_values)
69 c.lifetime_values.append(extra_values)
70 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
70 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
71 c.acl_options = [
71 c.acl_options = [
72 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
72 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
73 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
73 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
74 ]
74 ]
75
75
76 @LoginRequired()
76 @LoginRequired()
77 def index(self):
77 def index(self):
78 """GET /admin/gists: All items in the collection"""
78 """GET /admin/gists: All items in the collection"""
79 # url('gists')
79 # url('gists')
80 not_default_user = c.rhodecode_user.username != User.DEFAULT_USER
80 not_default_user = c.rhodecode_user.username != User.DEFAULT_USER
81 c.show_private = request.GET.get('private') and not_default_user
81 c.show_private = request.GET.get('private') and not_default_user
82 c.show_public = request.GET.get('public') and not_default_user
82 c.show_public = request.GET.get('public') and not_default_user
83 c.show_all = request.GET.get('all') and c.rhodecode_user.admin
83 c.show_all = request.GET.get('all') and c.rhodecode_user.admin
84
84
85 gists = _gists = Gist().query()\
85 gists = _gists = Gist().query()\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 .order_by(Gist.created_on.desc())
87 .order_by(Gist.created_on.desc())
88
88
89 c.active = 'public'
89 c.active = 'public'
90 # MY private
90 # MY private
91 if c.show_private and not c.show_public:
91 if c.show_private and not c.show_public:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
93 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
94 c.active = 'my_private'
94 c.active = 'my_private'
95 # MY public
95 # MY public
96 elif c.show_public and not c.show_private:
96 elif c.show_public and not c.show_private:
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
98 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
99 c.active = 'my_public'
99 c.active = 'my_public'
100 # MY public+private
100 # MY public+private
101 elif c.show_private and c.show_public:
101 elif c.show_private and c.show_public:
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 Gist.gist_type == Gist.GIST_PRIVATE))\
103 Gist.gist_type == Gist.GIST_PRIVATE))\
104 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
104 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
105 c.active = 'my_all'
105 c.active = 'my_all'
106 # Show all by super-admin
106 # Show all by super-admin
107 elif c.show_all:
107 elif c.show_all:
108 c.active = 'all'
108 c.active = 'all'
109 gists = _gists
109 gists = _gists
110
110
111 # default show ALL public gists
111 # default show ALL public gists
112 if not c.show_public and not c.show_private and not c.show_all:
112 if not c.show_public and not c.show_private and not c.show_all:
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 c.active = 'public'
114 c.active = 'public'
115
115
116 from rhodecode.lib.utils import PartialRenderer
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 data = []
119 data = []
120
120
121 for gist in gists:
121 for gist in gists:
122 data.append({
122 data.append({
123 'created_on': _render('gist_created', gist.created_on),
123 'created_on': _render('gist_created', gist.created_on),
124 'created_on_raw': gist.created_on,
124 'created_on_raw': gist.created_on,
125 'type': _render('gist_type', gist.gist_type),
125 'type': _render('gist_type', gist.gist_type),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 'author_raw': h.escape(gist.owner.full_contact),
128 'author_raw': h.escape(gist.owner.full_contact),
129 'expires': _render('gist_expires', gist.gist_expires),
129 'expires': _render('gist_expires', gist.gist_expires),
130 'description': _render('gist_description', gist.gist_description)
130 'description': _render('gist_description', gist.gist_description)
131 })
131 })
132 c.data = json.dumps(data)
132 c.data = json.dumps(data)
133 return render('admin/gists/index.html')
133 return render('admin/gists/index.mako')
134
134
135 @LoginRequired()
135 @LoginRequired()
136 @NotAnonymous()
136 @NotAnonymous()
137 @auth.CSRFRequired()
137 @auth.CSRFRequired()
138 def create(self):
138 def create(self):
139 """POST /admin/gists: Create a new item"""
139 """POST /admin/gists: Create a new item"""
140 # url('gists')
140 # url('gists')
141 self.__load_defaults()
141 self.__load_defaults()
142
142
143 data = dict(request.POST)
143 data = dict(request.POST)
144 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
144 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
145 data['nodes'] = [{
145 data['nodes'] = [{
146 'filename': data['filename'],
146 'filename': data['filename'],
147 'content': data.get('content'),
147 'content': data.get('content'),
148 'mimetype': data.get('mimetype') # None is autodetect
148 'mimetype': data.get('mimetype') # None is autodetect
149 }]
149 }]
150
150
151 data['gist_type'] = (
151 data['gist_type'] = (
152 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
152 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
153 data['gist_acl_level'] = (
153 data['gist_acl_level'] = (
154 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
154 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
155
155
156 schema = gist_schema.GistSchema().bind(
156 schema = gist_schema.GistSchema().bind(
157 lifetime_options=[x[0] for x in c.lifetime_values])
157 lifetime_options=[x[0] for x in c.lifetime_values])
158
158
159 try:
159 try:
160
160
161 schema_data = schema.deserialize(data)
161 schema_data = schema.deserialize(data)
162 # convert to safer format with just KEYs so we sure no duplicates
162 # convert to safer format with just KEYs so we sure no duplicates
163 schema_data['nodes'] = gist_schema.sequence_to_nodes(
163 schema_data['nodes'] = gist_schema.sequence_to_nodes(
164 schema_data['nodes'])
164 schema_data['nodes'])
165
165
166 gist = GistModel().create(
166 gist = GistModel().create(
167 gist_id=schema_data['gistid'], # custom access id not real ID
167 gist_id=schema_data['gistid'], # custom access id not real ID
168 description=schema_data['description'],
168 description=schema_data['description'],
169 owner=c.rhodecode_user.user_id,
169 owner=c.rhodecode_user.user_id,
170 gist_mapping=schema_data['nodes'],
170 gist_mapping=schema_data['nodes'],
171 gist_type=schema_data['gist_type'],
171 gist_type=schema_data['gist_type'],
172 lifetime=schema_data['lifetime'],
172 lifetime=schema_data['lifetime'],
173 gist_acl_level=schema_data['gist_acl_level']
173 gist_acl_level=schema_data['gist_acl_level']
174 )
174 )
175 Session().commit()
175 Session().commit()
176 new_gist_id = gist.gist_access_id
176 new_gist_id = gist.gist_access_id
177 except validation_schema.Invalid as errors:
177 except validation_schema.Invalid as errors:
178 defaults = data
178 defaults = data
179 errors = errors.asdict()
179 errors = errors.asdict()
180
180
181 if 'nodes.0.content' in errors:
181 if 'nodes.0.content' in errors:
182 errors['content'] = errors['nodes.0.content']
182 errors['content'] = errors['nodes.0.content']
183 del errors['nodes.0.content']
183 del errors['nodes.0.content']
184 if 'nodes.0.filename' in errors:
184 if 'nodes.0.filename' in errors:
185 errors['filename'] = errors['nodes.0.filename']
185 errors['filename'] = errors['nodes.0.filename']
186 del errors['nodes.0.filename']
186 del errors['nodes.0.filename']
187
187
188 return formencode.htmlfill.render(
188 return formencode.htmlfill.render(
189 render('admin/gists/new.html'),
189 render('admin/gists/new.mako'),
190 defaults=defaults,
190 defaults=defaults,
191 errors=errors,
191 errors=errors,
192 prefix_error=False,
192 prefix_error=False,
193 encoding="UTF-8",
193 encoding="UTF-8",
194 force_defaults=False
194 force_defaults=False
195 )
195 )
196
196
197 except Exception:
197 except Exception:
198 log.exception("Exception while trying to create a gist")
198 log.exception("Exception while trying to create a gist")
199 h.flash(_('Error occurred during gist creation'), category='error')
199 h.flash(_('Error occurred during gist creation'), category='error')
200 return redirect(url('new_gist'))
200 return redirect(url('new_gist'))
201 return redirect(url('gist', gist_id=new_gist_id))
201 return redirect(url('gist', gist_id=new_gist_id))
202
202
203 @LoginRequired()
203 @LoginRequired()
204 @NotAnonymous()
204 @NotAnonymous()
205 def new(self, format='html'):
205 def new(self, format='html'):
206 """GET /admin/gists/new: Form to create a new item"""
206 """GET /admin/gists/new: Form to create a new item"""
207 # url('new_gist')
207 # url('new_gist')
208 self.__load_defaults()
208 self.__load_defaults()
209 return render('admin/gists/new.html')
209 return render('admin/gists/new.mako')
210
210
211 @LoginRequired()
211 @LoginRequired()
212 @NotAnonymous()
212 @NotAnonymous()
213 @auth.CSRFRequired()
213 @auth.CSRFRequired()
214 def delete(self, gist_id):
214 def delete(self, gist_id):
215 """DELETE /admin/gists/gist_id: Delete an existing item"""
215 """DELETE /admin/gists/gist_id: Delete an existing item"""
216 # Forms posted to this method should contain a hidden field:
216 # Forms posted to this method should contain a hidden field:
217 # <input type="hidden" name="_method" value="DELETE" />
217 # <input type="hidden" name="_method" value="DELETE" />
218 # Or using helpers:
218 # Or using helpers:
219 # h.form(url('gist', gist_id=ID),
219 # h.form(url('gist', gist_id=ID),
220 # method='delete')
220 # method='delete')
221 # url('gist', gist_id=ID)
221 # url('gist', gist_id=ID)
222 c.gist = Gist.get_or_404(gist_id)
222 c.gist = Gist.get_or_404(gist_id)
223
223
224 owner = c.gist.gist_owner == c.rhodecode_user.user_id
224 owner = c.gist.gist_owner == c.rhodecode_user.user_id
225 if not (h.HasPermissionAny('hg.admin')() or owner):
225 if not (h.HasPermissionAny('hg.admin')() or owner):
226 raise HTTPForbidden()
226 raise HTTPForbidden()
227
227
228 GistModel().delete(c.gist)
228 GistModel().delete(c.gist)
229 Session().commit()
229 Session().commit()
230 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
230 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
231
231
232 return redirect(url('gists'))
232 return redirect(url('gists'))
233
233
234 def _add_gist_to_context(self, gist_id):
234 def _add_gist_to_context(self, gist_id):
235 c.gist = Gist.get_or_404(gist_id)
235 c.gist = Gist.get_or_404(gist_id)
236
236
237 # Check if this gist is expired
237 # Check if this gist is expired
238 if c.gist.gist_expires != -1:
238 if c.gist.gist_expires != -1:
239 if time.time() > c.gist.gist_expires:
239 if time.time() > c.gist.gist_expires:
240 log.error(
240 log.error(
241 'Gist expired at %s', time_to_datetime(c.gist.gist_expires))
241 'Gist expired at %s', time_to_datetime(c.gist.gist_expires))
242 raise HTTPNotFound()
242 raise HTTPNotFound()
243
243
244 # check if this gist requires a login
244 # check if this gist requires a login
245 is_default_user = c.rhodecode_user.username == User.DEFAULT_USER
245 is_default_user = c.rhodecode_user.username == User.DEFAULT_USER
246 if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
246 if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
247 log.error("Anonymous user %s tried to access protected gist `%s`",
247 log.error("Anonymous user %s tried to access protected gist `%s`",
248 c.rhodecode_user, gist_id)
248 c.rhodecode_user, gist_id)
249 raise HTTPNotFound()
249 raise HTTPNotFound()
250
250
251 @LoginRequired()
251 @LoginRequired()
252 def show(self, gist_id, revision='tip', format='html', f_path=None):
252 def show(self, gist_id, revision='tip', format='html', f_path=None):
253 """GET /admin/gists/gist_id: Show a specific item"""
253 """GET /admin/gists/gist_id: Show a specific item"""
254 # url('gist', gist_id=ID)
254 # url('gist', gist_id=ID)
255 self._add_gist_to_context(gist_id)
255 self._add_gist_to_context(gist_id)
256 c.render = not request.GET.get('no-render', False)
256 c.render = not request.GET.get('no-render', False)
257
257
258 try:
258 try:
259 c.file_last_commit, c.files = GistModel().get_gist_files(
259 c.file_last_commit, c.files = GistModel().get_gist_files(
260 gist_id, revision=revision)
260 gist_id, revision=revision)
261 except VCSError:
261 except VCSError:
262 log.exception("Exception in gist show")
262 log.exception("Exception in gist show")
263 raise HTTPNotFound()
263 raise HTTPNotFound()
264 if format == 'raw':
264 if format == 'raw':
265 content = '\n\n'.join([f.content for f in c.files
265 content = '\n\n'.join([f.content for f in c.files
266 if (f_path is None or f.path == f_path)])
266 if (f_path is None or f.path == f_path)])
267 response.content_type = 'text/plain'
267 response.content_type = 'text/plain'
268 return content
268 return content
269 return render('admin/gists/show.html')
269 return render('admin/gists/show.mako')
270
270
271 @LoginRequired()
271 @LoginRequired()
272 @NotAnonymous()
272 @NotAnonymous()
273 @auth.CSRFRequired()
273 @auth.CSRFRequired()
274 def edit(self, gist_id):
274 def edit(self, gist_id):
275 self.__load_defaults()
275 self.__load_defaults()
276 self._add_gist_to_context(gist_id)
276 self._add_gist_to_context(gist_id)
277
277
278 owner = c.gist.gist_owner == c.rhodecode_user.user_id
278 owner = c.gist.gist_owner == c.rhodecode_user.user_id
279 if not (h.HasPermissionAny('hg.admin')() or owner):
279 if not (h.HasPermissionAny('hg.admin')() or owner):
280 raise HTTPForbidden()
280 raise HTTPForbidden()
281
281
282 data = peppercorn.parse(request.POST.items())
282 data = peppercorn.parse(request.POST.items())
283
283
284 schema = gist_schema.GistSchema()
284 schema = gist_schema.GistSchema()
285 schema = schema.bind(
285 schema = schema.bind(
286 # '0' is special value to leave lifetime untouched
286 # '0' is special value to leave lifetime untouched
287 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
287 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
288 )
288 )
289
289
290 try:
290 try:
291 schema_data = schema.deserialize(data)
291 schema_data = schema.deserialize(data)
292 # convert to safer format with just KEYs so we sure no duplicates
292 # convert to safer format with just KEYs so we sure no duplicates
293 schema_data['nodes'] = gist_schema.sequence_to_nodes(
293 schema_data['nodes'] = gist_schema.sequence_to_nodes(
294 schema_data['nodes'])
294 schema_data['nodes'])
295
295
296 GistModel().update(
296 GistModel().update(
297 gist=c.gist,
297 gist=c.gist,
298 description=schema_data['description'],
298 description=schema_data['description'],
299 owner=c.gist.owner,
299 owner=c.gist.owner,
300 gist_mapping=schema_data['nodes'],
300 gist_mapping=schema_data['nodes'],
301 lifetime=schema_data['lifetime'],
301 lifetime=schema_data['lifetime'],
302 gist_acl_level=schema_data['gist_acl_level']
302 gist_acl_level=schema_data['gist_acl_level']
303 )
303 )
304
304
305 Session().commit()
305 Session().commit()
306 h.flash(_('Successfully updated gist content'), category='success')
306 h.flash(_('Successfully updated gist content'), category='success')
307 except NodeNotChangedError:
307 except NodeNotChangedError:
308 # raised if nothing was changed in repo itself. We anyway then
308 # raised if nothing was changed in repo itself. We anyway then
309 # store only DB stuff for gist
309 # store only DB stuff for gist
310 Session().commit()
310 Session().commit()
311 h.flash(_('Successfully updated gist data'), category='success')
311 h.flash(_('Successfully updated gist data'), category='success')
312 except validation_schema.Invalid as errors:
312 except validation_schema.Invalid as errors:
313 errors = errors.asdict()
313 errors = errors.asdict()
314 h.flash(_('Error occurred during update of gist {}: {}').format(
314 h.flash(_('Error occurred during update of gist {}: {}').format(
315 gist_id, errors), category='error')
315 gist_id, errors), category='error')
316 except Exception:
316 except Exception:
317 log.exception("Exception in gist edit")
317 log.exception("Exception in gist edit")
318 h.flash(_('Error occurred during update of gist %s') % gist_id,
318 h.flash(_('Error occurred during update of gist %s') % gist_id,
319 category='error')
319 category='error')
320
320
321 return redirect(url('gist', gist_id=gist_id))
321 return redirect(url('gist', gist_id=gist_id))
322
322
323 @LoginRequired()
323 @LoginRequired()
324 @NotAnonymous()
324 @NotAnonymous()
325 def edit_form(self, gist_id, format='html'):
325 def edit_form(self, gist_id, format='html'):
326 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
326 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
327 # url('edit_gist', gist_id=ID)
327 # url('edit_gist', gist_id=ID)
328 self._add_gist_to_context(gist_id)
328 self._add_gist_to_context(gist_id)
329
329
330 owner = c.gist.gist_owner == c.rhodecode_user.user_id
330 owner = c.gist.gist_owner == c.rhodecode_user.user_id
331 if not (h.HasPermissionAny('hg.admin')() or owner):
331 if not (h.HasPermissionAny('hg.admin')() or owner):
332 raise HTTPForbidden()
332 raise HTTPForbidden()
333
333
334 try:
334 try:
335 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
335 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
336 except VCSError:
336 except VCSError:
337 log.exception("Exception in gist edit")
337 log.exception("Exception in gist edit")
338 raise HTTPNotFound()
338 raise HTTPNotFound()
339
339
340 if c.gist.gist_expires == -1:
340 if c.gist.gist_expires == -1:
341 expiry = _('never')
341 expiry = _('never')
342 else:
342 else:
343 # this cannot use timeago, since it's used in select2 as a value
343 # this cannot use timeago, since it's used in select2 as a value
344 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
344 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
345 self.__load_defaults(
345 self.__load_defaults(
346 extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry}))
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 @LoginRequired()
349 @LoginRequired()
350 @NotAnonymous()
350 @NotAnonymous()
351 @jsonify
351 @jsonify
352 def check_revision(self, gist_id):
352 def check_revision(self, gist_id):
353 c.gist = Gist.get_or_404(gist_id)
353 c.gist = Gist.get_or_404(gist_id)
354 last_rev = c.gist.scm_instance().get_commit()
354 last_rev = c.gist.scm_instance().get_commit()
355 success = True
355 success = True
356 revision = request.GET.get('revision')
356 revision = request.GET.get('revision')
357
357
358 ##TODO: maybe move this to model ?
358 ##TODO: maybe move this to model ?
359 if revision != last_rev.raw_id:
359 if revision != last_rev.raw_id:
360 log.error('Last revision %s is different then submitted %s'
360 log.error('Last revision %s is different then submitted %s'
361 % (revision, last_rev))
361 % (revision, last_rev))
362 # our gist has newer version than we
362 # our gist has newer version than we
363 success = False
363 success = False
364
364
365 return {'success': success}
365 return {'success': success}
@@ -1,468 +1,468 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 my account controller for RhodeCode admin
23 my account controller for RhodeCode admin
24 """
24 """
25
25
26 import logging
26 import logging
27 import datetime
27 import datetime
28
28
29 import formencode
29 import formencode
30 from formencode import htmlfill
30 from formencode import htmlfill
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from pylons import request, tmpl_context as c, url, session
32 from pylons import request, tmpl_context as c, url, session
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from sqlalchemy.orm import joinedload
35 from sqlalchemy.orm import joinedload
36 from webob.exc import HTTPBadGateway
36 from webob.exc import HTTPBadGateway
37
37
38 from rhodecode import forms
38 from rhodecode import forms
39 from rhodecode.lib import helpers as h
39 from rhodecode.lib import helpers as h
40 from rhodecode.lib import auth
40 from rhodecode.lib import auth
41 from rhodecode.lib.auth import (
41 from rhodecode.lib.auth import (
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
43 from rhodecode.lib.base import BaseController, render
43 from rhodecode.lib.base import BaseController, render
44 from rhodecode.lib.utils import jsonify
44 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
46 from rhodecode.lib.ext_json import json
46 from rhodecode.lib.ext_json import json
47 from rhodecode.lib.channelstream import channelstream_request, \
47 from rhodecode.lib.channelstream import channelstream_request, \
48 ChannelstreamException
48 ChannelstreamException
49
49
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 from rhodecode.model.forms import UserForm
53 from rhodecode.model.forms import UserForm
54 from rhodecode.model.scm import RepoList
54 from rhodecode.model.scm import RepoList
55 from rhodecode.model.user import UserModel
55 from rhodecode.model.user import UserModel
56 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.auth_token import AuthTokenModel
57 from rhodecode.model.auth_token import AuthTokenModel
58 from rhodecode.model.meta import Session
58 from rhodecode.model.meta import Session
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.comment import ChangesetCommentsModel
60 from rhodecode.model.comment import ChangesetCommentsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class MyAccountController(BaseController):
65 class MyAccountController(BaseController):
66 """REST Controller styled on the Atom Publishing Protocol"""
66 """REST Controller styled on the Atom Publishing Protocol"""
67 # To properly map this controller, ensure your config/routing.py
67 # To properly map this controller, ensure your config/routing.py
68 # file has a resource setup:
68 # file has a resource setup:
69 # map.resource('setting', 'settings', controller='admin/settings',
69 # map.resource('setting', 'settings', controller='admin/settings',
70 # path_prefix='/admin', name_prefix='admin_')
70 # path_prefix='/admin', name_prefix='admin_')
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @NotAnonymous()
73 @NotAnonymous()
74 def __before__(self):
74 def __before__(self):
75 super(MyAccountController, self).__before__()
75 super(MyAccountController, self).__before__()
76
76
77 def __load_data(self):
77 def __load_data(self):
78 c.user = User.get(c.rhodecode_user.user_id)
78 c.user = User.get(c.rhodecode_user.user_id)
79 if c.user.username == User.DEFAULT_USER:
79 if c.user.username == User.DEFAULT_USER:
80 h.flash(_("You can't edit this user since it's"
80 h.flash(_("You can't edit this user since it's"
81 " crucial for entire application"), category='warning')
81 " crucial for entire application"), category='warning')
82 return redirect(url('users'))
82 return redirect(url('users'))
83
83
84 c.auth_user = AuthUser(
84 c.auth_user = AuthUser(
85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
86
86
87 def _load_my_repos_data(self, watched=False):
87 def _load_my_repos_data(self, watched=False):
88 if watched:
88 if watched:
89 admin = False
89 admin = False
90 follows_repos = Session().query(UserFollowing)\
90 follows_repos = Session().query(UserFollowing)\
91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
92 .options(joinedload(UserFollowing.follows_repository))\
92 .options(joinedload(UserFollowing.follows_repository))\
93 .all()
93 .all()
94 repo_list = [x.follows_repository for x in follows_repos]
94 repo_list = [x.follows_repository for x in follows_repos]
95 else:
95 else:
96 admin = True
96 admin = True
97 repo_list = Repository.get_all_repos(
97 repo_list = Repository.get_all_repos(
98 user_id=c.rhodecode_user.user_id)
98 user_id=c.rhodecode_user.user_id)
99 repo_list = RepoList(repo_list, perm_set=[
99 repo_list = RepoList(repo_list, perm_set=[
100 'repository.read', 'repository.write', 'repository.admin'])
100 'repository.read', 'repository.write', 'repository.admin'])
101
101
102 repos_data = RepoModel().get_repos_as_dict(
102 repos_data = RepoModel().get_repos_as_dict(
103 repo_list=repo_list, admin=admin)
103 repo_list=repo_list, admin=admin)
104 # json used to render the grid
104 # json used to render the grid
105 return json.dumps(repos_data)
105 return json.dumps(repos_data)
106
106
107 @auth.CSRFRequired()
107 @auth.CSRFRequired()
108 def my_account_update(self):
108 def my_account_update(self):
109 """
109 """
110 POST /_admin/my_account Updates info of my account
110 POST /_admin/my_account Updates info of my account
111 """
111 """
112 # url('my_account')
112 # url('my_account')
113 c.active = 'profile_edit'
113 c.active = 'profile_edit'
114 self.__load_data()
114 self.__load_data()
115 c.perm_user = c.auth_user
115 c.perm_user = c.auth_user
116 c.extern_type = c.user.extern_type
116 c.extern_type = c.user.extern_type
117 c.extern_name = c.user.extern_name
117 c.extern_name = c.user.extern_name
118
118
119 defaults = c.user.get_dict()
119 defaults = c.user.get_dict()
120 update = False
120 update = False
121 _form = UserForm(edit=True,
121 _form = UserForm(edit=True,
122 old_data={'user_id': c.rhodecode_user.user_id,
122 old_data={'user_id': c.rhodecode_user.user_id,
123 'email': c.rhodecode_user.email})()
123 'email': c.rhodecode_user.email})()
124 form_result = {}
124 form_result = {}
125 try:
125 try:
126 post_data = dict(request.POST)
126 post_data = dict(request.POST)
127 post_data['new_password'] = ''
127 post_data['new_password'] = ''
128 post_data['password_confirmation'] = ''
128 post_data['password_confirmation'] = ''
129 form_result = _form.to_python(post_data)
129 form_result = _form.to_python(post_data)
130 # skip updating those attrs for my account
130 # skip updating those attrs for my account
131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
132 'new_password', 'password_confirmation']
132 'new_password', 'password_confirmation']
133 # TODO: plugin should define if username can be updated
133 # TODO: plugin should define if username can be updated
134 if c.extern_type != "rhodecode":
134 if c.extern_type != "rhodecode":
135 # forbid updating username for external accounts
135 # forbid updating username for external accounts
136 skip_attrs.append('username')
136 skip_attrs.append('username')
137
137
138 UserModel().update_user(
138 UserModel().update_user(
139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
140 h.flash(_('Your account was updated successfully'),
140 h.flash(_('Your account was updated successfully'),
141 category='success')
141 category='success')
142 Session().commit()
142 Session().commit()
143 update = True
143 update = True
144
144
145 except formencode.Invalid as errors:
145 except formencode.Invalid as errors:
146 return htmlfill.render(
146 return htmlfill.render(
147 render('admin/my_account/my_account.html'),
147 render('admin/my_account/my_account.mako'),
148 defaults=errors.value,
148 defaults=errors.value,
149 errors=errors.error_dict or {},
149 errors=errors.error_dict or {},
150 prefix_error=False,
150 prefix_error=False,
151 encoding="UTF-8",
151 encoding="UTF-8",
152 force_defaults=False)
152 force_defaults=False)
153 except Exception:
153 except Exception:
154 log.exception("Exception updating user")
154 log.exception("Exception updating user")
155 h.flash(_('Error occurred during update of user %s')
155 h.flash(_('Error occurred during update of user %s')
156 % form_result.get('username'), category='error')
156 % form_result.get('username'), category='error')
157
157
158 if update:
158 if update:
159 return redirect('my_account')
159 return redirect('my_account')
160
160
161 return htmlfill.render(
161 return htmlfill.render(
162 render('admin/my_account/my_account.html'),
162 render('admin/my_account/my_account.mako'),
163 defaults=defaults,
163 defaults=defaults,
164 encoding="UTF-8",
164 encoding="UTF-8",
165 force_defaults=False
165 force_defaults=False
166 )
166 )
167
167
168 def my_account(self):
168 def my_account(self):
169 """
169 """
170 GET /_admin/my_account Displays info about my account
170 GET /_admin/my_account Displays info about my account
171 """
171 """
172 # url('my_account')
172 # url('my_account')
173 c.active = 'profile'
173 c.active = 'profile'
174 self.__load_data()
174 self.__load_data()
175
175
176 defaults = c.user.get_dict()
176 defaults = c.user.get_dict()
177 return htmlfill.render(
177 return htmlfill.render(
178 render('admin/my_account/my_account.html'),
178 render('admin/my_account/my_account.mako'),
179 defaults=defaults, encoding="UTF-8", force_defaults=False)
179 defaults=defaults, encoding="UTF-8", force_defaults=False)
180
180
181 def my_account_edit(self):
181 def my_account_edit(self):
182 """
182 """
183 GET /_admin/my_account/edit Displays edit form of my account
183 GET /_admin/my_account/edit Displays edit form of my account
184 """
184 """
185 c.active = 'profile_edit'
185 c.active = 'profile_edit'
186 self.__load_data()
186 self.__load_data()
187 c.perm_user = c.auth_user
187 c.perm_user = c.auth_user
188 c.extern_type = c.user.extern_type
188 c.extern_type = c.user.extern_type
189 c.extern_name = c.user.extern_name
189 c.extern_name = c.user.extern_name
190
190
191 defaults = c.user.get_dict()
191 defaults = c.user.get_dict()
192 return htmlfill.render(
192 return htmlfill.render(
193 render('admin/my_account/my_account.html'),
193 render('admin/my_account/my_account.mako'),
194 defaults=defaults,
194 defaults=defaults,
195 encoding="UTF-8",
195 encoding="UTF-8",
196 force_defaults=False
196 force_defaults=False
197 )
197 )
198
198
199 @auth.CSRFRequired(except_methods=['GET'])
199 @auth.CSRFRequired(except_methods=['GET'])
200 def my_account_password(self):
200 def my_account_password(self):
201 c.active = 'password'
201 c.active = 'password'
202 self.__load_data()
202 self.__load_data()
203 c.extern_type = c.user.extern_type
203 c.extern_type = c.user.extern_type
204
204
205 schema = user_schema.ChangePasswordSchema().bind(
205 schema = user_schema.ChangePasswordSchema().bind(
206 username=c.rhodecode_user.username)
206 username=c.rhodecode_user.username)
207
207
208 form = forms.Form(schema,
208 form = forms.Form(schema,
209 buttons=(forms.buttons.save, forms.buttons.reset))
209 buttons=(forms.buttons.save, forms.buttons.reset))
210
210
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
212 controls = request.POST.items()
212 controls = request.POST.items()
213 try:
213 try:
214 valid_data = form.validate(controls)
214 valid_data = form.validate(controls)
215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
216 instance = c.rhodecode_user.get_instance()
216 instance = c.rhodecode_user.get_instance()
217 instance.update_userdata(force_password_change=False)
217 instance.update_userdata(force_password_change=False)
218 Session().commit()
218 Session().commit()
219 except forms.ValidationFailure as e:
219 except forms.ValidationFailure as e:
220 request.session.flash(
220 request.session.flash(
221 _('Error occurred during update of user password'),
221 _('Error occurred during update of user password'),
222 queue='error')
222 queue='error')
223 form = e
223 form = e
224 except Exception:
224 except Exception:
225 log.exception("Exception updating password")
225 log.exception("Exception updating password")
226 request.session.flash(
226 request.session.flash(
227 _('Error occurred during update of user password'),
227 _('Error occurred during update of user password'),
228 queue='error')
228 queue='error')
229 else:
229 else:
230 session.setdefault('rhodecode_user', {}).update(
230 session.setdefault('rhodecode_user', {}).update(
231 {'password': md5(instance.password)})
231 {'password': md5(instance.password)})
232 session.save()
232 session.save()
233 request.session.flash(
233 request.session.flash(
234 _("Successfully updated password"), queue='success')
234 _("Successfully updated password"), queue='success')
235 return redirect(url('my_account_password'))
235 return redirect(url('my_account_password'))
236
236
237 c.form = form
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 def my_account_repos(self):
240 def my_account_repos(self):
241 c.active = 'repos'
241 c.active = 'repos'
242 self.__load_data()
242 self.__load_data()
243
243
244 # json used to render the grid
244 # json used to render the grid
245 c.data = self._load_my_repos_data()
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 def my_account_watched(self):
248 def my_account_watched(self):
249 c.active = 'watched'
249 c.active = 'watched'
250 self.__load_data()
250 self.__load_data()
251
251
252 # json used to render the grid
252 # json used to render the grid
253 c.data = self._load_my_repos_data(watched=True)
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 def my_account_perms(self):
256 def my_account_perms(self):
257 c.active = 'perms'
257 c.active = 'perms'
258 self.__load_data()
258 self.__load_data()
259 c.perm_user = c.auth_user
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 def my_account_emails(self):
263 def my_account_emails(self):
264 c.active = 'emails'
264 c.active = 'emails'
265 self.__load_data()
265 self.__load_data()
266
266
267 c.user_email_map = UserEmailMap.query()\
267 c.user_email_map = UserEmailMap.query()\
268 .filter(UserEmailMap.user == c.user).all()
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 @auth.CSRFRequired()
271 @auth.CSRFRequired()
272 def my_account_emails_add(self):
272 def my_account_emails_add(self):
273 email = request.POST.get('new_email')
273 email = request.POST.get('new_email')
274
274
275 try:
275 try:
276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
277 Session().commit()
277 Session().commit()
278 h.flash(_("Added new email address `%s` for user account") % email,
278 h.flash(_("Added new email address `%s` for user account") % email,
279 category='success')
279 category='success')
280 except formencode.Invalid as error:
280 except formencode.Invalid as error:
281 msg = error.error_dict['email']
281 msg = error.error_dict['email']
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 except Exception:
283 except Exception:
284 log.exception("Exception in my_account_emails")
284 log.exception("Exception in my_account_emails")
285 h.flash(_('An error occurred during email saving'),
285 h.flash(_('An error occurred during email saving'),
286 category='error')
286 category='error')
287 return redirect(url('my_account_emails'))
287 return redirect(url('my_account_emails'))
288
288
289 @auth.CSRFRequired()
289 @auth.CSRFRequired()
290 def my_account_emails_delete(self):
290 def my_account_emails_delete(self):
291 email_id = request.POST.get('del_email_id')
291 email_id = request.POST.get('del_email_id')
292 user_model = UserModel()
292 user_model = UserModel()
293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
294 Session().commit()
294 Session().commit()
295 h.flash(_("Removed email address from user account"),
295 h.flash(_("Removed email address from user account"),
296 category='success')
296 category='success')
297 return redirect(url('my_account_emails'))
297 return redirect(url('my_account_emails'))
298
298
299 def _extract_ordering(self, request):
299 def _extract_ordering(self, request):
300 column_index = safe_int(request.GET.get('order[0][column]'))
300 column_index = safe_int(request.GET.get('order[0][column]'))
301 order_dir = request.GET.get('order[0][dir]', 'desc')
301 order_dir = request.GET.get('order[0][dir]', 'desc')
302 order_by = request.GET.get(
302 order_by = request.GET.get(
303 'columns[%s][data][sort]' % column_index, 'name_raw')
303 'columns[%s][data][sort]' % column_index, 'name_raw')
304 return order_by, order_dir
304 return order_by, order_dir
305
305
306 def _get_pull_requests_list(self, statuses):
306 def _get_pull_requests_list(self, statuses):
307 start = safe_int(request.GET.get('start'), 0)
307 start = safe_int(request.GET.get('start'), 0)
308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
309 order_by, order_dir = self._extract_ordering(request)
309 order_by, order_dir = self._extract_ordering(request)
310
310
311 pull_requests = PullRequestModel().get_im_participating_in(
311 pull_requests = PullRequestModel().get_im_participating_in(
312 user_id=c.rhodecode_user.user_id,
312 user_id=c.rhodecode_user.user_id,
313 statuses=statuses,
313 statuses=statuses,
314 offset=start, length=length, order_by=order_by,
314 offset=start, length=length, order_by=order_by,
315 order_dir=order_dir)
315 order_dir=order_dir)
316
316
317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
318 user_id=c.rhodecode_user.user_id, statuses=statuses)
318 user_id=c.rhodecode_user.user_id, statuses=statuses)
319
319
320 from rhodecode.lib.utils import PartialRenderer
320 from rhodecode.lib.utils import PartialRenderer
321 _render = PartialRenderer('data_table/_dt_elements.html')
321 _render = PartialRenderer('data_table/_dt_elements.mako')
322 data = []
322 data = []
323 for pr in pull_requests:
323 for pr in pull_requests:
324 repo_id = pr.target_repo_id
324 repo_id = pr.target_repo_id
325 comments = ChangesetCommentsModel().get_all_comments(
325 comments = ChangesetCommentsModel().get_all_comments(
326 repo_id, pull_request=pr)
326 repo_id, pull_request=pr)
327 owned = pr.user_id == c.rhodecode_user.user_id
327 owned = pr.user_id == c.rhodecode_user.user_id
328 status = pr.calculated_review_status()
328 status = pr.calculated_review_status()
329
329
330 data.append({
330 data.append({
331 'target_repo': _render('pullrequest_target_repo',
331 'target_repo': _render('pullrequest_target_repo',
332 pr.target_repo.repo_name),
332 pr.target_repo.repo_name),
333 'name': _render('pullrequest_name',
333 'name': _render('pullrequest_name',
334 pr.pull_request_id, pr.target_repo.repo_name,
334 pr.pull_request_id, pr.target_repo.repo_name,
335 short=True),
335 short=True),
336 'name_raw': pr.pull_request_id,
336 'name_raw': pr.pull_request_id,
337 'status': _render('pullrequest_status', status),
337 'status': _render('pullrequest_status', status),
338 'title': _render(
338 'title': _render(
339 'pullrequest_title', pr.title, pr.description),
339 'pullrequest_title', pr.title, pr.description),
340 'description': h.escape(pr.description),
340 'description': h.escape(pr.description),
341 'updated_on': _render('pullrequest_updated_on',
341 'updated_on': _render('pullrequest_updated_on',
342 h.datetime_to_time(pr.updated_on)),
342 h.datetime_to_time(pr.updated_on)),
343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
344 'created_on': _render('pullrequest_updated_on',
344 'created_on': _render('pullrequest_updated_on',
345 h.datetime_to_time(pr.created_on)),
345 h.datetime_to_time(pr.created_on)),
346 'created_on_raw': h.datetime_to_time(pr.created_on),
346 'created_on_raw': h.datetime_to_time(pr.created_on),
347 'author': _render('pullrequest_author',
347 'author': _render('pullrequest_author',
348 pr.author.full_contact, ),
348 pr.author.full_contact, ),
349 'author_raw': pr.author.full_name,
349 'author_raw': pr.author.full_name,
350 'comments': _render('pullrequest_comments', len(comments)),
350 'comments': _render('pullrequest_comments', len(comments)),
351 'comments_raw': len(comments),
351 'comments_raw': len(comments),
352 'closed': pr.is_closed(),
352 'closed': pr.is_closed(),
353 'owned': owned
353 'owned': owned
354 })
354 })
355 # json used to render the grid
355 # json used to render the grid
356 data = ({
356 data = ({
357 'data': data,
357 'data': data,
358 'recordsTotal': pull_requests_total_count,
358 'recordsTotal': pull_requests_total_count,
359 'recordsFiltered': pull_requests_total_count,
359 'recordsFiltered': pull_requests_total_count,
360 })
360 })
361 return data
361 return data
362
362
363 def my_account_pullrequests(self):
363 def my_account_pullrequests(self):
364 c.active = 'pullrequests'
364 c.active = 'pullrequests'
365 self.__load_data()
365 self.__load_data()
366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
367
367
368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
369 if c.show_closed:
369 if c.show_closed:
370 statuses += [PullRequest.STATUS_CLOSED]
370 statuses += [PullRequest.STATUS_CLOSED]
371 data = self._get_pull_requests_list(statuses)
371 data = self._get_pull_requests_list(statuses)
372 if not request.is_xhr:
372 if not request.is_xhr:
373 c.data_participate = json.dumps(data['data'])
373 c.data_participate = json.dumps(data['data'])
374 c.records_total_participate = data['recordsTotal']
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 else:
376 else:
377 return json.dumps(data)
377 return json.dumps(data)
378
378
379 def my_account_auth_tokens(self):
379 def my_account_auth_tokens(self):
380 c.active = 'auth_tokens'
380 c.active = 'auth_tokens'
381 self.__load_data()
381 self.__load_data()
382 show_expired = True
382 show_expired = True
383 c.lifetime_values = [
383 c.lifetime_values = [
384 (str(-1), _('forever')),
384 (str(-1), _('forever')),
385 (str(5), _('5 minutes')),
385 (str(5), _('5 minutes')),
386 (str(60), _('1 hour')),
386 (str(60), _('1 hour')),
387 (str(60 * 24), _('1 day')),
387 (str(60 * 24), _('1 day')),
388 (str(60 * 24 * 30), _('1 month')),
388 (str(60 * 24 * 30), _('1 month')),
389 ]
389 ]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
392 for x in AuthTokenModel.cls.ROLES]
392 for x in AuthTokenModel.cls.ROLES]
393 c.role_options = [(c.role_values, _("Role"))]
393 c.role_options = [(c.role_values, _("Role"))]
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
395 c.rhodecode_user.user_id, show_expired=show_expired)
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 @auth.CSRFRequired()
398 @auth.CSRFRequired()
399 def my_account_auth_tokens_add(self):
399 def my_account_auth_tokens_add(self):
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
401 description = request.POST.get('description')
401 description = request.POST.get('description')
402 role = request.POST.get('role')
402 role = request.POST.get('role')
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
404 role)
404 role)
405 Session().commit()
405 Session().commit()
406 h.flash(_("Auth token successfully created"), category='success')
406 h.flash(_("Auth token successfully created"), category='success')
407 return redirect(url('my_account_auth_tokens'))
407 return redirect(url('my_account_auth_tokens'))
408
408
409 @auth.CSRFRequired()
409 @auth.CSRFRequired()
410 def my_account_auth_tokens_delete(self):
410 def my_account_auth_tokens_delete(self):
411 auth_token = request.POST.get('del_auth_token')
411 auth_token = request.POST.get('del_auth_token')
412 user_id = c.rhodecode_user.user_id
412 user_id = c.rhodecode_user.user_id
413 if request.POST.get('del_auth_token_builtin'):
413 if request.POST.get('del_auth_token_builtin'):
414 user = User.get(user_id)
414 user = User.get(user_id)
415 if user:
415 if user:
416 user.api_key = generate_auth_token(user.username)
416 user.api_key = generate_auth_token(user.username)
417 Session().add(user)
417 Session().add(user)
418 Session().commit()
418 Session().commit()
419 h.flash(_("Auth token successfully reset"), category='success')
419 h.flash(_("Auth token successfully reset"), category='success')
420 elif auth_token:
420 elif auth_token:
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
422 Session().commit()
422 Session().commit()
423 h.flash(_("Auth token successfully deleted"), category='success')
423 h.flash(_("Auth token successfully deleted"), category='success')
424
424
425 return redirect(url('my_account_auth_tokens'))
425 return redirect(url('my_account_auth_tokens'))
426
426
427 def my_notifications(self):
427 def my_notifications(self):
428 c.active = 'notifications'
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 @auth.CSRFRequired()
431 @auth.CSRFRequired()
432 @jsonify
432 @jsonify
433 def my_notifications_toggle_visibility(self):
433 def my_notifications_toggle_visibility(self):
434 user = c.rhodecode_user.get_instance()
434 user = c.rhodecode_user.get_instance()
435 new_status = not user.user_data.get('notification_status', True)
435 new_status = not user.user_data.get('notification_status', True)
436 user.update_userdata(notification_status=new_status)
436 user.update_userdata(notification_status=new_status)
437 Session().commit()
437 Session().commit()
438 return user.user_data['notification_status']
438 return user.user_data['notification_status']
439
439
440 @auth.CSRFRequired()
440 @auth.CSRFRequired()
441 @jsonify
441 @jsonify
442 def my_account_notifications_test_channelstream(self):
442 def my_account_notifications_test_channelstream(self):
443 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
443 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
444 c.rhodecode_user.username, datetime.datetime.now())
444 c.rhodecode_user.username, datetime.datetime.now())
445 payload = {
445 payload = {
446 'type': 'message',
446 'type': 'message',
447 'timestamp': datetime.datetime.utcnow(),
447 'timestamp': datetime.datetime.utcnow(),
448 'user': 'system',
448 'user': 'system',
449 #'channel': 'broadcast',
449 #'channel': 'broadcast',
450 'pm_users': [c.rhodecode_user.username],
450 'pm_users': [c.rhodecode_user.username],
451 'message': {
451 'message': {
452 'message': message,
452 'message': message,
453 'level': 'info',
453 'level': 'info',
454 'topic': '/notifications'
454 'topic': '/notifications'
455 }
455 }
456 }
456 }
457
457
458 registry = get_current_registry()
458 registry = get_current_registry()
459 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
459 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
460 channelstream_config = rhodecode_plugins.get('channelstream', {})
460 channelstream_config = rhodecode_plugins.get('channelstream', {})
461
461
462 try:
462 try:
463 channelstream_request(channelstream_config, [payload], '/message')
463 channelstream_request(channelstream_config, [payload], '/message')
464 except ChannelstreamException as e:
464 except ChannelstreamException as e:
465 log.exception('Failed to send channelstream data')
465 log.exception('Failed to send channelstream data')
466 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
466 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
467 return {"response": 'Channelstream data sent. '
467 return {"response": 'Channelstream data sent. '
468 'You should see a new live message now.'}
468 'You should see a new live message now.'}
@@ -1,178 +1,178 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 notifications controller for RhodeCode
23 notifications controller for RhodeCode
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons import request
29 from pylons import request
30 from pylons import tmpl_context as c, url
30 from pylons import tmpl_context as c, url
31 from pylons.controllers.util import redirect, abort
31 from pylons.controllers.util import redirect, abort
32 import webhelpers.paginate
32 import webhelpers.paginate
33 from webob.exc import HTTPBadRequest
33 from webob.exc import HTTPBadRequest
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous
37 from rhodecode.lib.base import BaseController, render
37 from rhodecode.lib.base import BaseController, render
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.helpers import Page
39 from rhodecode.lib.helpers import Page
40 from rhodecode.lib.utils2 import safe_int
40 from rhodecode.lib.utils2 import safe_int
41 from rhodecode.model.db import Notification
41 from rhodecode.model.db import Notification
42 from rhodecode.model.notification import NotificationModel
42 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class NotificationsController(BaseController):
49 class NotificationsController(BaseController):
50 """REST Controller styled on the Atom Publishing Protocol"""
50 """REST Controller styled on the Atom Publishing Protocol"""
51 # To properly map this controller, ensure your config/routing.py
51 # To properly map this controller, ensure your config/routing.py
52 # file has a resource setup:
52 # file has a resource setup:
53 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 # map.resource('notification', 'notifications', controller='_admin/notifications',
54 # path_prefix='/_admin', name_prefix='_admin_')
54 # path_prefix='/_admin', name_prefix='_admin_')
55
55
56 @LoginRequired()
56 @LoginRequired()
57 @NotAnonymous()
57 @NotAnonymous()
58 def __before__(self):
58 def __before__(self):
59 super(NotificationsController, self).__before__()
59 super(NotificationsController, self).__before__()
60
60
61 def index(self):
61 def index(self):
62 """GET /_admin/notifications: All items in the collection"""
62 """GET /_admin/notifications: All items in the collection"""
63 # url('notifications')
63 # url('notifications')
64 c.user = c.rhodecode_user
64 c.user = c.rhodecode_user
65 notif = NotificationModel().get_for_user(c.rhodecode_user.user_id,
65 notif = NotificationModel().get_for_user(c.rhodecode_user.user_id,
66 filter_=request.GET.getall('type'))
66 filter_=request.GET.getall('type'))
67
67
68 p = safe_int(request.GET.get('page', 1), 1)
68 p = safe_int(request.GET.get('page', 1), 1)
69 notifications_url = webhelpers.paginate.PageURL(
69 notifications_url = webhelpers.paginate.PageURL(
70 url('notifications'), request.GET)
70 url('notifications'), request.GET)
71 c.notifications = Page(notif, page=p, items_per_page=10,
71 c.notifications = Page(notif, page=p, items_per_page=10,
72 url=notifications_url)
72 url=notifications_url)
73 c.pull_request_type = Notification.TYPE_PULL_REQUEST
73 c.pull_request_type = Notification.TYPE_PULL_REQUEST
74 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
74 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
75 Notification.TYPE_PULL_REQUEST_COMMENT]
75 Notification.TYPE_PULL_REQUEST_COMMENT]
76
76
77 _current_filter = request.GET.getall('type')
77 _current_filter = request.GET.getall('type')
78 c.current_filter = 'all'
78 c.current_filter = 'all'
79 if _current_filter == [c.pull_request_type]:
79 if _current_filter == [c.pull_request_type]:
80 c.current_filter = 'pull_request'
80 c.current_filter = 'pull_request'
81 elif _current_filter == c.comment_type:
81 elif _current_filter == c.comment_type:
82 c.current_filter = 'comment'
82 c.current_filter = 'comment'
83
83
84 if request.is_xhr:
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 @auth.CSRFRequired()
90 @auth.CSRFRequired()
91 def mark_all_read(self):
91 def mark_all_read(self):
92 if request.is_xhr:
92 if request.is_xhr:
93 nm = NotificationModel()
93 nm = NotificationModel()
94 # mark all read
94 # mark all read
95 nm.mark_all_read_for_user(c.rhodecode_user.user_id,
95 nm.mark_all_read_for_user(c.rhodecode_user.user_id,
96 filter_=request.GET.getall('type'))
96 filter_=request.GET.getall('type'))
97 Session().commit()
97 Session().commit()
98 c.user = c.rhodecode_user
98 c.user = c.rhodecode_user
99 notif = nm.get_for_user(c.rhodecode_user.user_id,
99 notif = nm.get_for_user(c.rhodecode_user.user_id,
100 filter_=request.GET.getall('type'))
100 filter_=request.GET.getall('type'))
101 notifications_url = webhelpers.paginate.PageURL(
101 notifications_url = webhelpers.paginate.PageURL(
102 url('notifications'), request.GET)
102 url('notifications'), request.GET)
103 c.notifications = Page(notif, page=1, items_per_page=10,
103 c.notifications = Page(notif, page=1, items_per_page=10,
104 url=notifications_url)
104 url=notifications_url)
105 return render('admin/notifications/notifications_data.html')
105 return render('admin/notifications/notifications_data.mako')
106
106
107 def _has_permissions(self, notification):
107 def _has_permissions(self, notification):
108 def is_owner():
108 def is_owner():
109 user_id = c.rhodecode_user.user_id
109 user_id = c.rhodecode_user.user_id
110 for user_notification in notification.notifications_to_users:
110 for user_notification in notification.notifications_to_users:
111 if user_notification.user.user_id == user_id:
111 if user_notification.user.user_id == user_id:
112 return True
112 return True
113 return False
113 return False
114 return h.HasPermissionAny('hg.admin')() or is_owner()
114 return h.HasPermissionAny('hg.admin')() or is_owner()
115
115
116 @auth.CSRFRequired()
116 @auth.CSRFRequired()
117 def update(self, notification_id):
117 def update(self, notification_id):
118 """PUT /_admin/notifications/id: Update an existing item"""
118 """PUT /_admin/notifications/id: Update an existing item"""
119 # Forms posted to this method should contain a hidden field:
119 # Forms posted to this method should contain a hidden field:
120 # <input type="hidden" name="_method" value="PUT" />
120 # <input type="hidden" name="_method" value="PUT" />
121 # Or using helpers:
121 # Or using helpers:
122 # h.form(url('notification', notification_id=ID),
122 # h.form(url('notification', notification_id=ID),
123 # method='put')
123 # method='put')
124 # url('notification', notification_id=ID)
124 # url('notification', notification_id=ID)
125 try:
125 try:
126 no = Notification.get(notification_id)
126 no = Notification.get(notification_id)
127 if self._has_permissions(no):
127 if self._has_permissions(no):
128 # deletes only notification2user
128 # deletes only notification2user
129 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
129 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
130 Session().commit()
130 Session().commit()
131 return 'ok'
131 return 'ok'
132 except Exception:
132 except Exception:
133 Session().rollback()
133 Session().rollback()
134 log.exception("Exception updating a notification item")
134 log.exception("Exception updating a notification item")
135 raise HTTPBadRequest()
135 raise HTTPBadRequest()
136
136
137 @auth.CSRFRequired()
137 @auth.CSRFRequired()
138 def delete(self, notification_id):
138 def delete(self, notification_id):
139 """DELETE /_admin/notifications/id: Delete an existing item"""
139 """DELETE /_admin/notifications/id: Delete an existing item"""
140 # Forms posted to this method should contain a hidden field:
140 # Forms posted to this method should contain a hidden field:
141 # <input type="hidden" name="_method" value="DELETE" />
141 # <input type="hidden" name="_method" value="DELETE" />
142 # Or using helpers:
142 # Or using helpers:
143 # h.form(url('notification', notification_id=ID),
143 # h.form(url('notification', notification_id=ID),
144 # method='delete')
144 # method='delete')
145 # url('notification', notification_id=ID)
145 # url('notification', notification_id=ID)
146 try:
146 try:
147 no = Notification.get(notification_id)
147 no = Notification.get(notification_id)
148 if self._has_permissions(no):
148 if self._has_permissions(no):
149 # deletes only notification2user
149 # deletes only notification2user
150 NotificationModel().delete(c.rhodecode_user.user_id, no)
150 NotificationModel().delete(c.rhodecode_user.user_id, no)
151 Session().commit()
151 Session().commit()
152 return 'ok'
152 return 'ok'
153 except Exception:
153 except Exception:
154 Session().rollback()
154 Session().rollback()
155 log.exception("Exception deleting a notification item")
155 log.exception("Exception deleting a notification item")
156 raise HTTPBadRequest()
156 raise HTTPBadRequest()
157
157
158 def show(self, notification_id):
158 def show(self, notification_id):
159 """GET /_admin/notifications/id: Show a specific item"""
159 """GET /_admin/notifications/id: Show a specific item"""
160 # url('notification', notification_id=ID)
160 # url('notification', notification_id=ID)
161 c.user = c.rhodecode_user
161 c.user = c.rhodecode_user
162 no = Notification.get(notification_id)
162 no = Notification.get(notification_id)
163
163
164 if no and self._has_permissions(no):
164 if no and self._has_permissions(no):
165 unotification = NotificationModel()\
165 unotification = NotificationModel()\
166 .get_user_notification(c.user.user_id, no)
166 .get_user_notification(c.user.user_id, no)
167
167
168 # if this association to user is not valid, we don't want to show
168 # if this association to user is not valid, we don't want to show
169 # this message
169 # this message
170 if unotification:
170 if unotification:
171 if not unotification.read:
171 if not unotification.read:
172 unotification.mark_as_read()
172 unotification.mark_as_read()
173 Session().commit()
173 Session().commit()
174 c.notification = no
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 return abort(403)
178 return abort(403)
@@ -1,249 +1,249 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 permissions controller for RhodeCode Enterprise
23 permissions controller for RhodeCode Enterprise
24 """
24 """
25
25
26
26
27 import logging
27 import logging
28
28
29 import formencode
29 import formencode
30 from formencode import htmlfill
30 from formencode import htmlfill
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import auth
36 from rhodecode.lib import auth
37 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
37 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
38 from rhodecode.lib.base import BaseController, render
38 from rhodecode.lib.base import BaseController, render
39 from rhodecode.model.db import User, UserIpMap
39 from rhodecode.model.db import User, UserIpMap
40 from rhodecode.model.forms import (
40 from rhodecode.model.forms import (
41 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
41 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
42 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
43 from rhodecode.model.permission import PermissionModel
43 from rhodecode.model.permission import PermissionModel
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class PermissionsController(BaseController):
49 class PermissionsController(BaseController):
50 """REST Controller styled on the Atom Publishing Protocol"""
50 """REST Controller styled on the Atom Publishing Protocol"""
51 # To properly map this controller, ensure your config/routing.py
51 # To properly map this controller, ensure your config/routing.py
52 # file has a resource setup:
52 # file has a resource setup:
53 # map.resource('permission', 'permissions')
53 # map.resource('permission', 'permissions')
54
54
55 @LoginRequired()
55 @LoginRequired()
56 def __before__(self):
56 def __before__(self):
57 super(PermissionsController, self).__before__()
57 super(PermissionsController, self).__before__()
58
58
59 def __load_data(self):
59 def __load_data(self):
60 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
60 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
61
61
62 @HasPermissionAllDecorator('hg.admin')
62 @HasPermissionAllDecorator('hg.admin')
63 def permission_application(self):
63 def permission_application(self):
64 c.active = 'application'
64 c.active = 'application'
65 self.__load_data()
65 self.__load_data()
66
66
67 c.user = User.get_default_user()
67 c.user = User.get_default_user()
68
68
69 # TODO: johbo: The default user might be based on outdated state which
69 # TODO: johbo: The default user might be based on outdated state which
70 # has been loaded from the cache. A call to refresh() ensures that the
70 # has been loaded from the cache. A call to refresh() ensures that the
71 # latest state from the database is used.
71 # latest state from the database is used.
72 Session().refresh(c.user)
72 Session().refresh(c.user)
73
73
74 app_settings = SettingsModel().get_all_settings()
74 app_settings = SettingsModel().get_all_settings()
75 defaults = {
75 defaults = {
76 'anonymous': c.user.active,
76 'anonymous': c.user.active,
77 'default_register_message': app_settings.get(
77 'default_register_message': app_settings.get(
78 'rhodecode_register_message')
78 'rhodecode_register_message')
79 }
79 }
80 defaults.update(c.user.get_default_perms())
80 defaults.update(c.user.get_default_perms())
81
81
82 return htmlfill.render(
82 return htmlfill.render(
83 render('admin/permissions/permissions.html'),
83 render('admin/permissions/permissions.mako'),
84 defaults=defaults,
84 defaults=defaults,
85 encoding="UTF-8",
85 encoding="UTF-8",
86 force_defaults=False)
86 force_defaults=False)
87
87
88 @HasPermissionAllDecorator('hg.admin')
88 @HasPermissionAllDecorator('hg.admin')
89 @auth.CSRFRequired()
89 @auth.CSRFRequired()
90 def permission_application_update(self):
90 def permission_application_update(self):
91 c.active = 'application'
91 c.active = 'application'
92 self.__load_data()
92 self.__load_data()
93 _form = ApplicationPermissionsForm(
93 _form = ApplicationPermissionsForm(
94 [x[0] for x in c.register_choices],
94 [x[0] for x in c.register_choices],
95 [x[0] for x in c.password_reset_choices],
95 [x[0] for x in c.password_reset_choices],
96 [x[0] for x in c.extern_activate_choices])()
96 [x[0] for x in c.extern_activate_choices])()
97
97
98 try:
98 try:
99 form_result = _form.to_python(dict(request.POST))
99 form_result = _form.to_python(dict(request.POST))
100 form_result.update({'perm_user_name': User.DEFAULT_USER})
100 form_result.update({'perm_user_name': User.DEFAULT_USER})
101 PermissionModel().update_application_permissions(form_result)
101 PermissionModel().update_application_permissions(form_result)
102
102
103 settings = [
103 settings = [
104 ('register_message', 'default_register_message'),
104 ('register_message', 'default_register_message'),
105 ]
105 ]
106 for setting, form_key in settings:
106 for setting, form_key in settings:
107 sett = SettingsModel().create_or_update_setting(
107 sett = SettingsModel().create_or_update_setting(
108 setting, form_result[form_key])
108 setting, form_result[form_key])
109 Session().add(sett)
109 Session().add(sett)
110
110
111 Session().commit()
111 Session().commit()
112 h.flash(_('Application permissions updated successfully'),
112 h.flash(_('Application permissions updated successfully'),
113 category='success')
113 category='success')
114
114
115 except formencode.Invalid as errors:
115 except formencode.Invalid as errors:
116 defaults = errors.value
116 defaults = errors.value
117
117
118 return htmlfill.render(
118 return htmlfill.render(
119 render('admin/permissions/permissions.html'),
119 render('admin/permissions/permissions.mako'),
120 defaults=defaults,
120 defaults=defaults,
121 errors=errors.error_dict or {},
121 errors=errors.error_dict or {},
122 prefix_error=False,
122 prefix_error=False,
123 encoding="UTF-8",
123 encoding="UTF-8",
124 force_defaults=False)
124 force_defaults=False)
125 except Exception:
125 except Exception:
126 log.exception("Exception during update of permissions")
126 log.exception("Exception during update of permissions")
127 h.flash(_('Error occurred during update of permissions'),
127 h.flash(_('Error occurred during update of permissions'),
128 category='error')
128 category='error')
129
129
130 return redirect(url('admin_permissions_application'))
130 return redirect(url('admin_permissions_application'))
131
131
132 @HasPermissionAllDecorator('hg.admin')
132 @HasPermissionAllDecorator('hg.admin')
133 def permission_objects(self):
133 def permission_objects(self):
134 c.active = 'objects'
134 c.active = 'objects'
135 self.__load_data()
135 self.__load_data()
136 c.user = User.get_default_user()
136 c.user = User.get_default_user()
137 defaults = {}
137 defaults = {}
138 defaults.update(c.user.get_default_perms())
138 defaults.update(c.user.get_default_perms())
139 return htmlfill.render(
139 return htmlfill.render(
140 render('admin/permissions/permissions.html'),
140 render('admin/permissions/permissions.mako'),
141 defaults=defaults,
141 defaults=defaults,
142 encoding="UTF-8",
142 encoding="UTF-8",
143 force_defaults=False)
143 force_defaults=False)
144
144
145 @HasPermissionAllDecorator('hg.admin')
145 @HasPermissionAllDecorator('hg.admin')
146 @auth.CSRFRequired()
146 @auth.CSRFRequired()
147 def permission_objects_update(self):
147 def permission_objects_update(self):
148 c.active = 'objects'
148 c.active = 'objects'
149 self.__load_data()
149 self.__load_data()
150 _form = ObjectPermissionsForm(
150 _form = ObjectPermissionsForm(
151 [x[0] for x in c.repo_perms_choices],
151 [x[0] for x in c.repo_perms_choices],
152 [x[0] for x in c.group_perms_choices],
152 [x[0] for x in c.group_perms_choices],
153 [x[0] for x in c.user_group_perms_choices])()
153 [x[0] for x in c.user_group_perms_choices])()
154
154
155 try:
155 try:
156 form_result = _form.to_python(dict(request.POST))
156 form_result = _form.to_python(dict(request.POST))
157 form_result.update({'perm_user_name': User.DEFAULT_USER})
157 form_result.update({'perm_user_name': User.DEFAULT_USER})
158 PermissionModel().update_object_permissions(form_result)
158 PermissionModel().update_object_permissions(form_result)
159
159
160 Session().commit()
160 Session().commit()
161 h.flash(_('Object permissions updated successfully'),
161 h.flash(_('Object permissions updated successfully'),
162 category='success')
162 category='success')
163
163
164 except formencode.Invalid as errors:
164 except formencode.Invalid as errors:
165 defaults = errors.value
165 defaults = errors.value
166
166
167 return htmlfill.render(
167 return htmlfill.render(
168 render('admin/permissions/permissions.html'),
168 render('admin/permissions/permissions.mako'),
169 defaults=defaults,
169 defaults=defaults,
170 errors=errors.error_dict or {},
170 errors=errors.error_dict or {},
171 prefix_error=False,
171 prefix_error=False,
172 encoding="UTF-8",
172 encoding="UTF-8",
173 force_defaults=False)
173 force_defaults=False)
174 except Exception:
174 except Exception:
175 log.exception("Exception during update of permissions")
175 log.exception("Exception during update of permissions")
176 h.flash(_('Error occurred during update of permissions'),
176 h.flash(_('Error occurred during update of permissions'),
177 category='error')
177 category='error')
178
178
179 return redirect(url('admin_permissions_object'))
179 return redirect(url('admin_permissions_object'))
180
180
181 @HasPermissionAllDecorator('hg.admin')
181 @HasPermissionAllDecorator('hg.admin')
182 def permission_global(self):
182 def permission_global(self):
183 c.active = 'global'
183 c.active = 'global'
184 self.__load_data()
184 self.__load_data()
185
185
186 c.user = User.get_default_user()
186 c.user = User.get_default_user()
187 defaults = {}
187 defaults = {}
188 defaults.update(c.user.get_default_perms())
188 defaults.update(c.user.get_default_perms())
189
189
190 return htmlfill.render(
190 return htmlfill.render(
191 render('admin/permissions/permissions.html'),
191 render('admin/permissions/permissions.mako'),
192 defaults=defaults,
192 defaults=defaults,
193 encoding="UTF-8",
193 encoding="UTF-8",
194 force_defaults=False)
194 force_defaults=False)
195
195
196 @HasPermissionAllDecorator('hg.admin')
196 @HasPermissionAllDecorator('hg.admin')
197 @auth.CSRFRequired()
197 @auth.CSRFRequired()
198 def permission_global_update(self):
198 def permission_global_update(self):
199 c.active = 'global'
199 c.active = 'global'
200 self.__load_data()
200 self.__load_data()
201 _form = UserPermissionsForm(
201 _form = UserPermissionsForm(
202 [x[0] for x in c.repo_create_choices],
202 [x[0] for x in c.repo_create_choices],
203 [x[0] for x in c.repo_create_on_write_choices],
203 [x[0] for x in c.repo_create_on_write_choices],
204 [x[0] for x in c.repo_group_create_choices],
204 [x[0] for x in c.repo_group_create_choices],
205 [x[0] for x in c.user_group_create_choices],
205 [x[0] for x in c.user_group_create_choices],
206 [x[0] for x in c.fork_choices],
206 [x[0] for x in c.fork_choices],
207 [x[0] for x in c.inherit_default_permission_choices])()
207 [x[0] for x in c.inherit_default_permission_choices])()
208
208
209 try:
209 try:
210 form_result = _form.to_python(dict(request.POST))
210 form_result = _form.to_python(dict(request.POST))
211 form_result.update({'perm_user_name': User.DEFAULT_USER})
211 form_result.update({'perm_user_name': User.DEFAULT_USER})
212 PermissionModel().update_user_permissions(form_result)
212 PermissionModel().update_user_permissions(form_result)
213
213
214 Session().commit()
214 Session().commit()
215 h.flash(_('Global permissions updated successfully'),
215 h.flash(_('Global permissions updated successfully'),
216 category='success')
216 category='success')
217
217
218 except formencode.Invalid as errors:
218 except formencode.Invalid as errors:
219 defaults = errors.value
219 defaults = errors.value
220
220
221 return htmlfill.render(
221 return htmlfill.render(
222 render('admin/permissions/permissions.html'),
222 render('admin/permissions/permissions.mako'),
223 defaults=defaults,
223 defaults=defaults,
224 errors=errors.error_dict or {},
224 errors=errors.error_dict or {},
225 prefix_error=False,
225 prefix_error=False,
226 encoding="UTF-8",
226 encoding="UTF-8",
227 force_defaults=False)
227 force_defaults=False)
228 except Exception:
228 except Exception:
229 log.exception("Exception during update of permissions")
229 log.exception("Exception during update of permissions")
230 h.flash(_('Error occurred during update of permissions'),
230 h.flash(_('Error occurred during update of permissions'),
231 category='error')
231 category='error')
232
232
233 return redirect(url('admin_permissions_global'))
233 return redirect(url('admin_permissions_global'))
234
234
235 @HasPermissionAllDecorator('hg.admin')
235 @HasPermissionAllDecorator('hg.admin')
236 def permission_ips(self):
236 def permission_ips(self):
237 c.active = 'ips'
237 c.active = 'ips'
238 c.user = User.get_default_user()
238 c.user = User.get_default_user()
239 c.user_ip_map = (
239 c.user_ip_map = (
240 UserIpMap.query().filter(UserIpMap.user == c.user).all())
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 @HasPermissionAllDecorator('hg.admin')
244 @HasPermissionAllDecorator('hg.admin')
245 def permission_perms(self):
245 def permission_perms(self):
246 c.active = 'perms'
246 c.active = 'perms'
247 c.user = User.get_default_user()
247 c.user = User.get_default_user()
248 c.perm_user = c.user.AuthUser
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Repository groups controller for RhodeCode
23 Repository groups controller for RhodeCode
24 """
24 """
25
25
26 import logging
26 import logging
27 import formencode
27 import formencode
28
28
29 from formencode import htmlfill
29 from formencode import htmlfill
30
30
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import abort, redirect
32 from pylons.controllers.util import abort, redirect
33 from pylons.i18n.translation import _, ungettext
33 from pylons.i18n.translation import _, ungettext
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, NotAnonymous, HasPermissionAll,
39 LoginRequired, NotAnonymous, HasPermissionAll,
40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.base import BaseController, render
42 from rhodecode.model.db import RepoGroup, User
42 from rhodecode.model.db import RepoGroup, User
43 from rhodecode.model.scm import RepoGroupList
43 from rhodecode.model.scm import RepoGroupList
44 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.lib.utils2 import safe_int
47 from rhodecode.lib.utils2 import safe_int
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class RepoGroupsController(BaseController):
53 class RepoGroupsController(BaseController):
54 """REST Controller styled on the Atom Publishing Protocol"""
54 """REST Controller styled on the Atom Publishing Protocol"""
55
55
56 @LoginRequired()
56 @LoginRequired()
57 def __before__(self):
57 def __before__(self):
58 super(RepoGroupsController, self).__before__()
58 super(RepoGroupsController, self).__before__()
59
59
60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
61 if self._can_create_repo_group():
61 if self._can_create_repo_group():
62 # we're global admin, we're ok and we can create TOP level groups
62 # we're global admin, we're ok and we can create TOP level groups
63 allow_empty_group = True
63 allow_empty_group = True
64
64
65 # override the choices for this form, we need to filter choices
65 # override the choices for this form, we need to filter choices
66 # and display only those we have ADMIN right
66 # and display only those we have ADMIN right
67 groups_with_admin_rights = RepoGroupList(
67 groups_with_admin_rights = RepoGroupList(
68 RepoGroup.query().all(),
68 RepoGroup.query().all(),
69 perm_set=['group.admin'])
69 perm_set=['group.admin'])
70 c.repo_groups = RepoGroup.groups_choices(
70 c.repo_groups = RepoGroup.groups_choices(
71 groups=groups_with_admin_rights,
71 groups=groups_with_admin_rights,
72 show_empty_group=allow_empty_group)
72 show_empty_group=allow_empty_group)
73
73
74 if repo_group:
74 if repo_group:
75 # exclude filtered ids
75 # exclude filtered ids
76 exclude_group_ids = [repo_group.group_id]
76 exclude_group_ids = [repo_group.group_id]
77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
78 c.repo_groups)
78 c.repo_groups)
79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
80 parent_group = repo_group.parent_group
80 parent_group = repo_group.parent_group
81
81
82 add_parent_group = (parent_group and (
82 add_parent_group = (parent_group and (
83 unicode(parent_group.group_id) not in c.repo_groups_choices))
83 unicode(parent_group.group_id) not in c.repo_groups_choices))
84 if add_parent_group:
84 if add_parent_group:
85 c.repo_groups_choices.append(unicode(parent_group.group_id))
85 c.repo_groups_choices.append(unicode(parent_group.group_id))
86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
87
87
88 def __load_data(self, group_id):
88 def __load_data(self, group_id):
89 """
89 """
90 Load defaults settings for edit, and update
90 Load defaults settings for edit, and update
91
91
92 :param group_id:
92 :param group_id:
93 """
93 """
94 repo_group = RepoGroup.get_or_404(group_id)
94 repo_group = RepoGroup.get_or_404(group_id)
95 data = repo_group.get_dict()
95 data = repo_group.get_dict()
96 data['group_name'] = repo_group.name
96 data['group_name'] = repo_group.name
97
97
98 # fill owner
98 # fill owner
99 if repo_group.user:
99 if repo_group.user:
100 data.update({'user': repo_group.user.username})
100 data.update({'user': repo_group.user.username})
101 else:
101 else:
102 replacement_user = User.get_first_super_admin().username
102 replacement_user = User.get_first_super_admin().username
103 data.update({'user': replacement_user})
103 data.update({'user': replacement_user})
104
104
105 # fill repository group users
105 # fill repository group users
106 for p in repo_group.repo_group_to_perm:
106 for p in repo_group.repo_group_to_perm:
107 data.update({
107 data.update({
108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
109
109
110 # fill repository group user groups
110 # fill repository group user groups
111 for p in repo_group.users_group_to_perm:
111 for p in repo_group.users_group_to_perm:
112 data.update({
112 data.update({
113 'g_perm_%s' % p.users_group.users_group_id:
113 'g_perm_%s' % p.users_group.users_group_id:
114 p.permission.permission_name})
114 p.permission.permission_name})
115 # html and form expects -1 as empty parent group
115 # html and form expects -1 as empty parent group
116 data['group_parent_id'] = data['group_parent_id'] or -1
116 data['group_parent_id'] = data['group_parent_id'] or -1
117 return data
117 return data
118
118
119 def _revoke_perms_on_yourself(self, form_result):
119 def _revoke_perms_on_yourself(self, form_result):
120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
121 form_result['perm_updates'])
121 form_result['perm_updates'])
122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
123 form_result['perm_additions'])
123 form_result['perm_additions'])
124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
125 form_result['perm_deletions'])
125 form_result['perm_deletions'])
126 admin_perm = 'group.admin'
126 admin_perm = 'group.admin'
127 if _updates and _updates[0][1] != admin_perm or \
127 if _updates and _updates[0][1] != admin_perm or \
128 _additions and _additions[0][1] != admin_perm or \
128 _additions and _additions[0][1] != admin_perm or \
129 _deletions and _deletions[0][1] != admin_perm:
129 _deletions and _deletions[0][1] != admin_perm:
130 return True
130 return True
131 return False
131 return False
132
132
133 def _can_create_repo_group(self, parent_group_id=None):
133 def _can_create_repo_group(self, parent_group_id=None):
134 is_admin = HasPermissionAll('hg.admin')('group create controller')
134 is_admin = HasPermissionAll('hg.admin')('group create controller')
135 create_repo_group = HasPermissionAll(
135 create_repo_group = HasPermissionAll(
136 'hg.repogroup.create.true')('group create controller')
136 'hg.repogroup.create.true')('group create controller')
137 if is_admin or (create_repo_group and not parent_group_id):
137 if is_admin or (create_repo_group and not parent_group_id):
138 # we're global admin, or we have global repo group create
138 # we're global admin, or we have global repo group create
139 # permission
139 # permission
140 # we're ok and we can create TOP level groups
140 # we're ok and we can create TOP level groups
141 return True
141 return True
142 elif parent_group_id:
142 elif parent_group_id:
143 # we check the permission if we can write to parent group
143 # we check the permission if we can write to parent group
144 group = RepoGroup.get(parent_group_id)
144 group = RepoGroup.get(parent_group_id)
145 group_name = group.group_name if group else None
145 group_name = group.group_name if group else None
146 if HasRepoGroupPermissionAll('group.admin')(
146 if HasRepoGroupPermissionAll('group.admin')(
147 group_name, 'check if user is an admin of group'):
147 group_name, 'check if user is an admin of group'):
148 # we're an admin of passed in group, we're ok.
148 # we're an admin of passed in group, we're ok.
149 return True
149 return True
150 else:
150 else:
151 return False
151 return False
152 return False
152 return False
153
153
154 @NotAnonymous()
154 @NotAnonymous()
155 def index(self):
155 def index(self):
156 """GET /repo_groups: All items in the collection"""
156 """GET /repo_groups: All items in the collection"""
157 # url('repo_groups')
157 # url('repo_groups')
158
158
159 repo_group_list = RepoGroup.get_all_repo_groups()
159 repo_group_list = RepoGroup.get_all_repo_groups()
160 _perms = ['group.admin']
160 _perms = ['group.admin']
161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
163 repo_group_list=repo_group_list_acl, admin=True)
163 repo_group_list=repo_group_list_acl, admin=True)
164 c.data = json.dumps(repo_group_data)
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 # perm checks inside
167 # perm checks inside
168 @NotAnonymous()
168 @NotAnonymous()
169 @auth.CSRFRequired()
169 @auth.CSRFRequired()
170 def create(self):
170 def create(self):
171 """POST /repo_groups: Create a new item"""
171 """POST /repo_groups: Create a new item"""
172 # url('repo_groups')
172 # url('repo_groups')
173
173
174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
175 can_create = self._can_create_repo_group(parent_group_id)
175 can_create = self._can_create_repo_group(parent_group_id)
176
176
177 self.__load_defaults()
177 self.__load_defaults()
178 # permissions for can create group based on parent_id are checked
178 # permissions for can create group based on parent_id are checked
179 # here in the Form
179 # here in the Form
180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
181 repo_group_form = RepoGroupForm(available_groups=available_groups,
181 repo_group_form = RepoGroupForm(available_groups=available_groups,
182 can_create_in_root=can_create)()
182 can_create_in_root=can_create)()
183 try:
183 try:
184 owner = c.rhodecode_user
184 owner = c.rhodecode_user
185 form_result = repo_group_form.to_python(dict(request.POST))
185 form_result = repo_group_form.to_python(dict(request.POST))
186 RepoGroupModel().create(
186 RepoGroupModel().create(
187 group_name=form_result['group_name_full'],
187 group_name=form_result['group_name_full'],
188 group_description=form_result['group_description'],
188 group_description=form_result['group_description'],
189 owner=owner.user_id,
189 owner=owner.user_id,
190 copy_permissions=form_result['group_copy_permissions']
190 copy_permissions=form_result['group_copy_permissions']
191 )
191 )
192 Session().commit()
192 Session().commit()
193 _new_group_name = form_result['group_name_full']
193 _new_group_name = form_result['group_name_full']
194 repo_group_url = h.link_to(
194 repo_group_url = h.link_to(
195 _new_group_name,
195 _new_group_name,
196 h.url('repo_group_home', group_name=_new_group_name))
196 h.url('repo_group_home', group_name=_new_group_name))
197 h.flash(h.literal(_('Created repository group %s')
197 h.flash(h.literal(_('Created repository group %s')
198 % repo_group_url), category='success')
198 % repo_group_url), category='success')
199 # TODO: in futureaction_logger(, '', '', '', self.sa)
199 # TODO: in futureaction_logger(, '', '', '', self.sa)
200 except formencode.Invalid as errors:
200 except formencode.Invalid as errors:
201 return htmlfill.render(
201 return htmlfill.render(
202 render('admin/repo_groups/repo_group_add.html'),
202 render('admin/repo_groups/repo_group_add.mako'),
203 defaults=errors.value,
203 defaults=errors.value,
204 errors=errors.error_dict or {},
204 errors=errors.error_dict or {},
205 prefix_error=False,
205 prefix_error=False,
206 encoding="UTF-8",
206 encoding="UTF-8",
207 force_defaults=False)
207 force_defaults=False)
208 except Exception:
208 except Exception:
209 log.exception("Exception during creation of repository group")
209 log.exception("Exception during creation of repository group")
210 h.flash(_('Error occurred during creation of repository group %s')
210 h.flash(_('Error occurred during creation of repository group %s')
211 % request.POST.get('group_name'), category='error')
211 % request.POST.get('group_name'), category='error')
212
212
213 # TODO: maybe we should get back to the main view, not the admin one
213 # TODO: maybe we should get back to the main view, not the admin one
214 return redirect(url('repo_groups', parent_group=parent_group_id))
214 return redirect(url('repo_groups', parent_group=parent_group_id))
215
215
216 # perm checks inside
216 # perm checks inside
217 @NotAnonymous()
217 @NotAnonymous()
218 def new(self):
218 def new(self):
219 """GET /repo_groups/new: Form to create a new item"""
219 """GET /repo_groups/new: Form to create a new item"""
220 # url('new_repo_group')
220 # url('new_repo_group')
221 # perm check for admin, create_group perm or admin of parent_group
221 # perm check for admin, create_group perm or admin of parent_group
222 parent_group_id = safe_int(request.GET.get('parent_group'))
222 parent_group_id = safe_int(request.GET.get('parent_group'))
223 if not self._can_create_repo_group(parent_group_id):
223 if not self._can_create_repo_group(parent_group_id):
224 return abort(403)
224 return abort(403)
225
225
226 self.__load_defaults()
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 @HasRepoGroupPermissionAnyDecorator('group.admin')
229 @HasRepoGroupPermissionAnyDecorator('group.admin')
230 @auth.CSRFRequired()
230 @auth.CSRFRequired()
231 def update(self, group_name):
231 def update(self, group_name):
232 """PUT /repo_groups/group_name: Update an existing item"""
232 """PUT /repo_groups/group_name: Update an existing item"""
233 # Forms posted to this method should contain a hidden field:
233 # Forms posted to this method should contain a hidden field:
234 # <input type="hidden" name="_method" value="PUT" />
234 # <input type="hidden" name="_method" value="PUT" />
235 # Or using helpers:
235 # Or using helpers:
236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
237 # url('repo_group_home', group_name=GROUP_NAME)
237 # url('repo_group_home', group_name=GROUP_NAME)
238
238
239 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
239 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
240 can_create_in_root = self._can_create_repo_group()
240 can_create_in_root = self._can_create_repo_group()
241 show_root_location = can_create_in_root
241 show_root_location = can_create_in_root
242 if not c.repo_group.parent_group:
242 if not c.repo_group.parent_group:
243 # this group don't have a parrent so we should show empty value
243 # this group don't have a parrent so we should show empty value
244 show_root_location = True
244 show_root_location = True
245 self.__load_defaults(allow_empty_group=show_root_location,
245 self.__load_defaults(allow_empty_group=show_root_location,
246 repo_group=c.repo_group)
246 repo_group=c.repo_group)
247
247
248 repo_group_form = RepoGroupForm(
248 repo_group_form = RepoGroupForm(
249 edit=True, old_data=c.repo_group.get_dict(),
249 edit=True, old_data=c.repo_group.get_dict(),
250 available_groups=c.repo_groups_choices,
250 available_groups=c.repo_groups_choices,
251 can_create_in_root=can_create_in_root, allow_disabled=True)()
251 can_create_in_root=can_create_in_root, allow_disabled=True)()
252
252
253 try:
253 try:
254 form_result = repo_group_form.to_python(dict(request.POST))
254 form_result = repo_group_form.to_python(dict(request.POST))
255 gr_name = form_result['group_name']
255 gr_name = form_result['group_name']
256 new_gr = RepoGroupModel().update(group_name, form_result)
256 new_gr = RepoGroupModel().update(group_name, form_result)
257 Session().commit()
257 Session().commit()
258 h.flash(_('Updated repository group %s') % (gr_name,),
258 h.flash(_('Updated repository group %s') % (gr_name,),
259 category='success')
259 category='success')
260 # we now have new name !
260 # we now have new name !
261 group_name = new_gr.group_name
261 group_name = new_gr.group_name
262 # TODO: in future action_logger(, '', '', '', self.sa)
262 # TODO: in future action_logger(, '', '', '', self.sa)
263 except formencode.Invalid as errors:
263 except formencode.Invalid as errors:
264 c.active = 'settings'
264 c.active = 'settings'
265 return htmlfill.render(
265 return htmlfill.render(
266 render('admin/repo_groups/repo_group_edit.html'),
266 render('admin/repo_groups/repo_group_edit.mako'),
267 defaults=errors.value,
267 defaults=errors.value,
268 errors=errors.error_dict or {},
268 errors=errors.error_dict or {},
269 prefix_error=False,
269 prefix_error=False,
270 encoding="UTF-8",
270 encoding="UTF-8",
271 force_defaults=False)
271 force_defaults=False)
272 except Exception:
272 except Exception:
273 log.exception("Exception during update or repository group")
273 log.exception("Exception during update or repository group")
274 h.flash(_('Error occurred during update of repository group %s')
274 h.flash(_('Error occurred during update of repository group %s')
275 % request.POST.get('group_name'), category='error')
275 % request.POST.get('group_name'), category='error')
276
276
277 return redirect(url('edit_repo_group', group_name=group_name))
277 return redirect(url('edit_repo_group', group_name=group_name))
278
278
279 @HasRepoGroupPermissionAnyDecorator('group.admin')
279 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 @auth.CSRFRequired()
280 @auth.CSRFRequired()
281 def delete(self, group_name):
281 def delete(self, group_name):
282 """DELETE /repo_groups/group_name: Delete an existing item"""
282 """DELETE /repo_groups/group_name: Delete an existing item"""
283 # Forms posted to this method should contain a hidden field:
283 # Forms posted to this method should contain a hidden field:
284 # <input type="hidden" name="_method" value="DELETE" />
284 # <input type="hidden" name="_method" value="DELETE" />
285 # Or using helpers:
285 # Or using helpers:
286 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
286 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
287 # url('repo_group_home', group_name=GROUP_NAME)
287 # url('repo_group_home', group_name=GROUP_NAME)
288
288
289 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
289 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
290 repos = gr.repositories.all()
290 repos = gr.repositories.all()
291 if repos:
291 if repos:
292 msg = ungettext(
292 msg = ungettext(
293 'This group contains %(num)d repository and cannot be deleted',
293 'This group contains %(num)d repository and cannot be deleted',
294 'This group contains %(num)d repositories and cannot be'
294 'This group contains %(num)d repositories and cannot be'
295 ' deleted',
295 ' deleted',
296 len(repos)) % {'num': len(repos)}
296 len(repos)) % {'num': len(repos)}
297 h.flash(msg, category='warning')
297 h.flash(msg, category='warning')
298 return redirect(url('repo_groups'))
298 return redirect(url('repo_groups'))
299
299
300 children = gr.children.all()
300 children = gr.children.all()
301 if children:
301 if children:
302 msg = ungettext(
302 msg = ungettext(
303 'This group contains %(num)d subgroup and cannot be deleted',
303 'This group contains %(num)d subgroup and cannot be deleted',
304 'This group contains %(num)d subgroups and cannot be deleted',
304 'This group contains %(num)d subgroups and cannot be deleted',
305 len(children)) % {'num': len(children)}
305 len(children)) % {'num': len(children)}
306 h.flash(msg, category='warning')
306 h.flash(msg, category='warning')
307 return redirect(url('repo_groups'))
307 return redirect(url('repo_groups'))
308
308
309 try:
309 try:
310 RepoGroupModel().delete(group_name)
310 RepoGroupModel().delete(group_name)
311 Session().commit()
311 Session().commit()
312 h.flash(_('Removed repository group %s') % group_name,
312 h.flash(_('Removed repository group %s') % group_name,
313 category='success')
313 category='success')
314 # TODO: in future action_logger(, '', '', '', self.sa)
314 # TODO: in future action_logger(, '', '', '', self.sa)
315 except Exception:
315 except Exception:
316 log.exception("Exception during deletion of repository group")
316 log.exception("Exception during deletion of repository group")
317 h.flash(_('Error occurred during deletion of repository group %s')
317 h.flash(_('Error occurred during deletion of repository group %s')
318 % group_name, category='error')
318 % group_name, category='error')
319
319
320 return redirect(url('repo_groups'))
320 return redirect(url('repo_groups'))
321
321
322 @HasRepoGroupPermissionAnyDecorator('group.admin')
322 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 def edit(self, group_name):
323 def edit(self, group_name):
324 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
324 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
325 # url('edit_repo_group', group_name=GROUP_NAME)
325 # url('edit_repo_group', group_name=GROUP_NAME)
326 c.active = 'settings'
326 c.active = 'settings'
327
327
328 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
328 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
329 # we can only allow moving empty group if it's already a top-level
329 # we can only allow moving empty group if it's already a top-level
330 # group, ie has no parents, or we're admin
330 # group, ie has no parents, or we're admin
331 can_create_in_root = self._can_create_repo_group()
331 can_create_in_root = self._can_create_repo_group()
332 show_root_location = can_create_in_root
332 show_root_location = can_create_in_root
333 if not c.repo_group.parent_group:
333 if not c.repo_group.parent_group:
334 # this group don't have a parrent so we should show empty value
334 # this group don't have a parrent so we should show empty value
335 show_root_location = True
335 show_root_location = True
336 self.__load_defaults(allow_empty_group=show_root_location,
336 self.__load_defaults(allow_empty_group=show_root_location,
337 repo_group=c.repo_group)
337 repo_group=c.repo_group)
338 defaults = self.__load_data(c.repo_group.group_id)
338 defaults = self.__load_data(c.repo_group.group_id)
339
339
340 return htmlfill.render(
340 return htmlfill.render(
341 render('admin/repo_groups/repo_group_edit.html'),
341 render('admin/repo_groups/repo_group_edit.mako'),
342 defaults=defaults,
342 defaults=defaults,
343 encoding="UTF-8",
343 encoding="UTF-8",
344 force_defaults=False
344 force_defaults=False
345 )
345 )
346
346
347 @HasRepoGroupPermissionAnyDecorator('group.admin')
347 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 def edit_repo_group_advanced(self, group_name):
348 def edit_repo_group_advanced(self, group_name):
349 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
349 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 # url('edit_repo_group', group_name=GROUP_NAME)
350 # url('edit_repo_group', group_name=GROUP_NAME)
351 c.active = 'advanced'
351 c.active = 'advanced'
352 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
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 @HasRepoGroupPermissionAnyDecorator('group.admin')
356 @HasRepoGroupPermissionAnyDecorator('group.admin')
357 def edit_repo_group_perms(self, group_name):
357 def edit_repo_group_perms(self, group_name):
358 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
358 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
359 # url('edit_repo_group', group_name=GROUP_NAME)
359 # url('edit_repo_group', group_name=GROUP_NAME)
360 c.active = 'perms'
360 c.active = 'perms'
361 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
361 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
362 self.__load_defaults()
362 self.__load_defaults()
363 defaults = self.__load_data(c.repo_group.group_id)
363 defaults = self.__load_data(c.repo_group.group_id)
364
364
365 return htmlfill.render(
365 return htmlfill.render(
366 render('admin/repo_groups/repo_group_edit.html'),
366 render('admin/repo_groups/repo_group_edit.mako'),
367 defaults=defaults,
367 defaults=defaults,
368 encoding="UTF-8",
368 encoding="UTF-8",
369 force_defaults=False
369 force_defaults=False
370 )
370 )
371
371
372 @HasRepoGroupPermissionAnyDecorator('group.admin')
372 @HasRepoGroupPermissionAnyDecorator('group.admin')
373 @auth.CSRFRequired()
373 @auth.CSRFRequired()
374 def update_perms(self, group_name):
374 def update_perms(self, group_name):
375 """
375 """
376 Update permissions for given repository group
376 Update permissions for given repository group
377
377
378 :param group_name:
378 :param group_name:
379 """
379 """
380
380
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
382 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
383 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
383 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
384 request.POST)
384 request.POST)
385
385
386 if not c.rhodecode_user.is_admin:
386 if not c.rhodecode_user.is_admin:
387 if self._revoke_perms_on_yourself(form):
387 if self._revoke_perms_on_yourself(form):
388 msg = _('Cannot change permission for yourself as admin')
388 msg = _('Cannot change permission for yourself as admin')
389 h.flash(msg, category='warning')
389 h.flash(msg, category='warning')
390 return redirect(
390 return redirect(
391 url('edit_repo_group_perms', group_name=group_name))
391 url('edit_repo_group_perms', group_name=group_name))
392
392
393 # iterate over all members(if in recursive mode) of this groups and
393 # iterate over all members(if in recursive mode) of this groups and
394 # set the permissions !
394 # set the permissions !
395 # this can be potentially heavy operation
395 # this can be potentially heavy operation
396 RepoGroupModel().update_permissions(
396 RepoGroupModel().update_permissions(
397 c.repo_group,
397 c.repo_group,
398 form['perm_additions'], form['perm_updates'],
398 form['perm_additions'], form['perm_updates'],
399 form['perm_deletions'], form['recursive'])
399 form['perm_deletions'], form['recursive'])
400
400
401 # TODO: implement this
401 # TODO: implement this
402 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
402 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
403 # repo_name, self.ip_addr, self.sa)
403 # repo_name, self.ip_addr, self.sa)
404 Session().commit()
404 Session().commit()
405 h.flash(_('Repository Group permissions updated'), category='success')
405 h.flash(_('Repository Group permissions updated'), category='success')
406 return redirect(url('edit_repo_group_perms', group_name=group_name))
406 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -1,888 +1,888 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Repositories controller for RhodeCode
23 Repositories controller for RhodeCode
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import formencode
29 import formencode
30 from formencode import htmlfill
30 from formencode import htmlfill
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.lib import auth, helpers as h
37 from rhodecode.lib import auth, helpers as h
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator,
39 LoginRequired, HasPermissionAllDecorator,
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.exceptions import AttachedForksError
44 from rhodecode.lib.exceptions import AttachedForksError
45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
46 from rhodecode.lib.utils2 import safe_int, str2bool
46 from rhodecode.lib.utils2 import safe_int, str2bool
47 from rhodecode.lib.vcs import RepositoryError
47 from rhodecode.lib.vcs import RepositoryError
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
52 IssueTrackerPatternsForm)
52 IssueTrackerPatternsForm)
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.repo import RepoModel
54 from rhodecode.model.repo import RepoModel
55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
56 from rhodecode.model.settings import (
56 from rhodecode.model.settings import (
57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
58 SettingNotFound)
58 SettingNotFound)
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class ReposController(BaseRepoController):
63 class ReposController(BaseRepoController):
64 """
64 """
65 REST Controller styled on the Atom Publishing Protocol"""
65 REST Controller styled on the Atom Publishing Protocol"""
66 # To properly map this controller, ensure your config/routing.py
66 # To properly map this controller, ensure your config/routing.py
67 # file has a resource setup:
67 # file has a resource setup:
68 # map.resource('repo', 'repos')
68 # map.resource('repo', 'repos')
69
69
70 @LoginRequired()
70 @LoginRequired()
71 def __before__(self):
71 def __before__(self):
72 super(ReposController, self).__before__()
72 super(ReposController, self).__before__()
73
73
74 def _load_repo(self, repo_name):
74 def _load_repo(self, repo_name):
75 repo_obj = Repository.get_by_repo_name(repo_name)
75 repo_obj = Repository.get_by_repo_name(repo_name)
76
76
77 if repo_obj is None:
77 if repo_obj is None:
78 h.not_mapped_error(repo_name)
78 h.not_mapped_error(repo_name)
79 return redirect(url('repos'))
79 return redirect(url('repos'))
80
80
81 return repo_obj
81 return repo_obj
82
82
83 def __load_defaults(self, repo=None):
83 def __load_defaults(self, repo=None):
84 acl_groups = RepoGroupList(RepoGroup.query().all(),
84 acl_groups = RepoGroupList(RepoGroup.query().all(),
85 perm_set=['group.write', 'group.admin'])
85 perm_set=['group.write', 'group.admin'])
86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
88
88
89 # in case someone no longer have a group.write access to a repository
89 # in case someone no longer have a group.write access to a repository
90 # pre fill the list with this entry, we don't care if this is the same
90 # pre fill the list with this entry, we don't care if this is the same
91 # but it will allow saving repo data properly.
91 # but it will allow saving repo data properly.
92
92
93 repo_group = None
93 repo_group = None
94 if repo:
94 if repo:
95 repo_group = repo.group
95 repo_group = repo.group
96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
97 c.repo_groups_choices.append(unicode(repo_group.group_id))
97 c.repo_groups_choices.append(unicode(repo_group.group_id))
98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
99
99
100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
101 c.landing_revs_choices = choices
101 c.landing_revs_choices = choices
102
102
103 def __load_data(self, repo_name=None):
103 def __load_data(self, repo_name=None):
104 """
104 """
105 Load defaults settings for edit, and update
105 Load defaults settings for edit, and update
106
106
107 :param repo_name:
107 :param repo_name:
108 """
108 """
109 c.repo_info = self._load_repo(repo_name)
109 c.repo_info = self._load_repo(repo_name)
110 self.__load_defaults(c.repo_info)
110 self.__load_defaults(c.repo_info)
111
111
112 # override defaults for exact repo info here git/hg etc
112 # override defaults for exact repo info here git/hg etc
113 if not c.repository_requirements_missing:
113 if not c.repository_requirements_missing:
114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
115 c.repo_info)
115 c.repo_info)
116 c.landing_revs_choices = choices
116 c.landing_revs_choices = choices
117 defaults = RepoModel()._get_defaults(repo_name)
117 defaults = RepoModel()._get_defaults(repo_name)
118
118
119 return defaults
119 return defaults
120
120
121 def _log_creation_exception(self, e, repo_name):
121 def _log_creation_exception(self, e, repo_name):
122 reason = None
122 reason = None
123 if len(e.args) == 2:
123 if len(e.args) == 2:
124 reason = e.args[1]
124 reason = e.args[1]
125
125
126 if reason == 'INVALID_CERTIFICATE':
126 if reason == 'INVALID_CERTIFICATE':
127 log.exception(
127 log.exception(
128 'Exception creating a repository: invalid certificate')
128 'Exception creating a repository: invalid certificate')
129 msg = (_('Error creating repository %s: invalid certificate')
129 msg = (_('Error creating repository %s: invalid certificate')
130 % repo_name)
130 % repo_name)
131 else:
131 else:
132 log.exception("Exception creating a repository")
132 log.exception("Exception creating a repository")
133 msg = (_('Error creating repository %s')
133 msg = (_('Error creating repository %s')
134 % repo_name)
134 % repo_name)
135
135
136 return msg
136 return msg
137
137
138 @NotAnonymous()
138 @NotAnonymous()
139 def index(self, format='html'):
139 def index(self, format='html'):
140 """GET /repos: All items in the collection"""
140 """GET /repos: All items in the collection"""
141 # url('repos')
141 # url('repos')
142
142
143 repo_list = Repository.get_all_repos()
143 repo_list = Repository.get_all_repos()
144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
145 repos_data = RepoModel().get_repos_as_dict(
145 repos_data = RepoModel().get_repos_as_dict(
146 repo_list=c.repo_list, admin=True, super_user_actions=True)
146 repo_list=c.repo_list, admin=True, super_user_actions=True)
147 # json used to render the grid
147 # json used to render the grid
148 c.data = json.dumps(repos_data)
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 # perms check inside
152 # perms check inside
153 @NotAnonymous()
153 @NotAnonymous()
154 @auth.CSRFRequired()
154 @auth.CSRFRequired()
155 def create(self):
155 def create(self):
156 """
156 """
157 POST /repos: Create a new item"""
157 POST /repos: Create a new item"""
158 # url('repos')
158 # url('repos')
159
159
160 self.__load_defaults()
160 self.__load_defaults()
161 form_result = {}
161 form_result = {}
162 task_id = None
162 task_id = None
163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
164 try:
164 try:
165 # CanWriteToGroup validators checks permissions of this POST
165 # CanWriteToGroup validators checks permissions of this POST
166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
167 landing_revs=c.landing_revs_choices)()\
167 landing_revs=c.landing_revs_choices)()\
168 .to_python(dict(request.POST))
168 .to_python(dict(request.POST))
169
169
170 # create is done sometimes async on celery, db transaction
170 # create is done sometimes async on celery, db transaction
171 # management is handled there.
171 # management is handled there.
172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
173 from celery.result import BaseAsyncResult
173 from celery.result import BaseAsyncResult
174 if isinstance(task, BaseAsyncResult):
174 if isinstance(task, BaseAsyncResult):
175 task_id = task.task_id
175 task_id = task.task_id
176 except formencode.Invalid as errors:
176 except formencode.Invalid as errors:
177 return htmlfill.render(
177 return htmlfill.render(
178 render('admin/repos/repo_add.html'),
178 render('admin/repos/repo_add.mako'),
179 defaults=errors.value,
179 defaults=errors.value,
180 errors=errors.error_dict or {},
180 errors=errors.error_dict or {},
181 prefix_error=False,
181 prefix_error=False,
182 encoding="UTF-8",
182 encoding="UTF-8",
183 force_defaults=False)
183 force_defaults=False)
184
184
185 except Exception as e:
185 except Exception as e:
186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
187 h.flash(msg, category='error')
187 h.flash(msg, category='error')
188 return redirect(url('home'))
188 return redirect(url('home'))
189
189
190 return redirect(h.url('repo_creating_home',
190 return redirect(h.url('repo_creating_home',
191 repo_name=form_result['repo_name_full'],
191 repo_name=form_result['repo_name_full'],
192 task_id=task_id))
192 task_id=task_id))
193
193
194 # perms check inside
194 # perms check inside
195 @NotAnonymous()
195 @NotAnonymous()
196 def create_repository(self):
196 def create_repository(self):
197 """GET /_admin/create_repository: Form to create a new item"""
197 """GET /_admin/create_repository: Form to create a new item"""
198 new_repo = request.GET.get('repo', '')
198 new_repo = request.GET.get('repo', '')
199 parent_group = safe_int(request.GET.get('parent_group'))
199 parent_group = safe_int(request.GET.get('parent_group'))
200 _gr = RepoGroup.get(parent_group)
200 _gr = RepoGroup.get(parent_group)
201
201
202 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
202 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
203 # you're not super admin nor have global create permissions,
203 # you're not super admin nor have global create permissions,
204 # but maybe you have at least write permission to a parent group ?
204 # but maybe you have at least write permission to a parent group ?
205
205
206 gr_name = _gr.group_name if _gr else None
206 gr_name = _gr.group_name if _gr else None
207 # create repositories with write permission on group is set to true
207 # create repositories with write permission on group is set to true
208 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
208 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
209 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
209 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
210 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
210 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
211 if not (group_admin or (group_write and create_on_write)):
211 if not (group_admin or (group_write and create_on_write)):
212 raise HTTPForbidden
212 raise HTTPForbidden
213
213
214 acl_groups = RepoGroupList(RepoGroup.query().all(),
214 acl_groups = RepoGroupList(RepoGroup.query().all(),
215 perm_set=['group.write', 'group.admin'])
215 perm_set=['group.write', 'group.admin'])
216 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
216 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
217 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
217 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
218 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
218 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
219 c.personal_repo_group = c.rhodecode_user.personal_repo_group
219 c.personal_repo_group = c.rhodecode_user.personal_repo_group
220 c.new_repo = repo_name_slug(new_repo)
220 c.new_repo = repo_name_slug(new_repo)
221
221
222 # apply the defaults from defaults page
222 # apply the defaults from defaults page
223 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
223 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
224 # set checkbox to autochecked
224 # set checkbox to autochecked
225 defaults['repo_copy_permissions'] = True
225 defaults['repo_copy_permissions'] = True
226
226
227 parent_group_choice = '-1'
227 parent_group_choice = '-1'
228 if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group:
228 if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group:
229 parent_group_choice = c.rhodecode_user.personal_repo_group
229 parent_group_choice = c.rhodecode_user.personal_repo_group
230
230
231 if parent_group and _gr:
231 if parent_group and _gr:
232 if parent_group in [x[0] for x in c.repo_groups]:
232 if parent_group in [x[0] for x in c.repo_groups]:
233 parent_group_choice = unicode(parent_group)
233 parent_group_choice = unicode(parent_group)
234
234
235 defaults.update({'repo_group': parent_group_choice})
235 defaults.update({'repo_group': parent_group_choice})
236
236
237 return htmlfill.render(
237 return htmlfill.render(
238 render('admin/repos/repo_add.html'),
238 render('admin/repos/repo_add.mako'),
239 defaults=defaults,
239 defaults=defaults,
240 errors={},
240 errors={},
241 prefix_error=False,
241 prefix_error=False,
242 encoding="UTF-8",
242 encoding="UTF-8",
243 force_defaults=False
243 force_defaults=False
244 )
244 )
245
245
246 @NotAnonymous()
246 @NotAnonymous()
247 def repo_creating(self, repo_name):
247 def repo_creating(self, repo_name):
248 c.repo = repo_name
248 c.repo = repo_name
249 c.task_id = request.GET.get('task_id')
249 c.task_id = request.GET.get('task_id')
250 if not c.repo:
250 if not c.repo:
251 raise HTTPNotFound()
251 raise HTTPNotFound()
252 return render('admin/repos/repo_creating.html')
252 return render('admin/repos/repo_creating.mako')
253
253
254 @NotAnonymous()
254 @NotAnonymous()
255 @jsonify
255 @jsonify
256 def repo_check(self, repo_name):
256 def repo_check(self, repo_name):
257 c.repo = repo_name
257 c.repo = repo_name
258 task_id = request.GET.get('task_id')
258 task_id = request.GET.get('task_id')
259
259
260 if task_id and task_id not in ['None']:
260 if task_id and task_id not in ['None']:
261 import rhodecode
261 import rhodecode
262 from celery.result import AsyncResult
262 from celery.result import AsyncResult
263 if rhodecode.CELERY_ENABLED:
263 if rhodecode.CELERY_ENABLED:
264 task = AsyncResult(task_id)
264 task = AsyncResult(task_id)
265 if task.failed():
265 if task.failed():
266 msg = self._log_creation_exception(task.result, c.repo)
266 msg = self._log_creation_exception(task.result, c.repo)
267 h.flash(msg, category='error')
267 h.flash(msg, category='error')
268 return redirect(url('home'), code=501)
268 return redirect(url('home'), code=501)
269
269
270 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
271 if repo and repo.repo_state == Repository.STATE_CREATED:
271 if repo and repo.repo_state == Repository.STATE_CREATED:
272 if repo.clone_uri:
272 if repo.clone_uri:
273 clone_uri = repo.clone_uri_hidden
273 clone_uri = repo.clone_uri_hidden
274 h.flash(_('Created repository %s from %s')
274 h.flash(_('Created repository %s from %s')
275 % (repo.repo_name, clone_uri), category='success')
275 % (repo.repo_name, clone_uri), category='success')
276 else:
276 else:
277 repo_url = h.link_to(repo.repo_name,
277 repo_url = h.link_to(repo.repo_name,
278 h.url('summary_home',
278 h.url('summary_home',
279 repo_name=repo.repo_name))
279 repo_name=repo.repo_name))
280 fork = repo.fork
280 fork = repo.fork
281 if fork:
281 if fork:
282 fork_name = fork.repo_name
282 fork_name = fork.repo_name
283 h.flash(h.literal(_('Forked repository %s as %s')
283 h.flash(h.literal(_('Forked repository %s as %s')
284 % (fork_name, repo_url)), category='success')
284 % (fork_name, repo_url)), category='success')
285 else:
285 else:
286 h.flash(h.literal(_('Created repository %s') % repo_url),
286 h.flash(h.literal(_('Created repository %s') % repo_url),
287 category='success')
287 category='success')
288 return {'result': True}
288 return {'result': True}
289 return {'result': False}
289 return {'result': False}
290
290
291 @HasRepoPermissionAllDecorator('repository.admin')
291 @HasRepoPermissionAllDecorator('repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 def update(self, repo_name):
293 def update(self, repo_name):
294 """
294 """
295 PUT /repos/repo_name: Update an existing item"""
295 PUT /repos/repo_name: Update an existing item"""
296 # Forms posted to this method should contain a hidden field:
296 # Forms posted to this method should contain a hidden field:
297 # <input type="hidden" name="_method" value="PUT" />
297 # <input type="hidden" name="_method" value="PUT" />
298 # Or using helpers:
298 # Or using helpers:
299 # h.form(url('repo', repo_name=ID),
299 # h.form(url('repo', repo_name=ID),
300 # method='put')
300 # method='put')
301 # url('repo', repo_name=ID)
301 # url('repo', repo_name=ID)
302
302
303 self.__load_data(repo_name)
303 self.__load_data(repo_name)
304 c.active = 'settings'
304 c.active = 'settings'
305 c.repo_fields = RepositoryField.query()\
305 c.repo_fields = RepositoryField.query()\
306 .filter(RepositoryField.repository == c.repo_info).all()
306 .filter(RepositoryField.repository == c.repo_info).all()
307
307
308 repo_model = RepoModel()
308 repo_model = RepoModel()
309 changed_name = repo_name
309 changed_name = repo_name
310
310
311 c.personal_repo_group = c.rhodecode_user.personal_repo_group
311 c.personal_repo_group = c.rhodecode_user.personal_repo_group
312 # override the choices with extracted revisions !
312 # override the choices with extracted revisions !
313 repo = Repository.get_by_repo_name(repo_name)
313 repo = Repository.get_by_repo_name(repo_name)
314 old_data = {
314 old_data = {
315 'repo_name': repo_name,
315 'repo_name': repo_name,
316 'repo_group': repo.group.get_dict() if repo.group else {},
316 'repo_group': repo.group.get_dict() if repo.group else {},
317 'repo_type': repo.repo_type,
317 'repo_type': repo.repo_type,
318 }
318 }
319 _form = RepoForm(
319 _form = RepoForm(
320 edit=True, old_data=old_data, repo_groups=c.repo_groups_choices,
320 edit=True, old_data=old_data, repo_groups=c.repo_groups_choices,
321 landing_revs=c.landing_revs_choices, allow_disabled=True)()
321 landing_revs=c.landing_revs_choices, allow_disabled=True)()
322
322
323 try:
323 try:
324 form_result = _form.to_python(dict(request.POST))
324 form_result = _form.to_python(dict(request.POST))
325 repo = repo_model.update(repo_name, **form_result)
325 repo = repo_model.update(repo_name, **form_result)
326 ScmModel().mark_for_invalidation(repo_name)
326 ScmModel().mark_for_invalidation(repo_name)
327 h.flash(_('Repository %s updated successfully') % repo_name,
327 h.flash(_('Repository %s updated successfully') % repo_name,
328 category='success')
328 category='success')
329 changed_name = repo.repo_name
329 changed_name = repo.repo_name
330 action_logger(c.rhodecode_user, 'admin_updated_repo',
330 action_logger(c.rhodecode_user, 'admin_updated_repo',
331 changed_name, self.ip_addr, self.sa)
331 changed_name, self.ip_addr, self.sa)
332 Session().commit()
332 Session().commit()
333 except formencode.Invalid as errors:
333 except formencode.Invalid as errors:
334 defaults = self.__load_data(repo_name)
334 defaults = self.__load_data(repo_name)
335 defaults.update(errors.value)
335 defaults.update(errors.value)
336 return htmlfill.render(
336 return htmlfill.render(
337 render('admin/repos/repo_edit.html'),
337 render('admin/repos/repo_edit.mako'),
338 defaults=defaults,
338 defaults=defaults,
339 errors=errors.error_dict or {},
339 errors=errors.error_dict or {},
340 prefix_error=False,
340 prefix_error=False,
341 encoding="UTF-8",
341 encoding="UTF-8",
342 force_defaults=False)
342 force_defaults=False)
343
343
344 except Exception:
344 except Exception:
345 log.exception("Exception during update of repository")
345 log.exception("Exception during update of repository")
346 h.flash(_('Error occurred during update of repository %s') \
346 h.flash(_('Error occurred during update of repository %s') \
347 % repo_name, category='error')
347 % repo_name, category='error')
348 return redirect(url('edit_repo', repo_name=changed_name))
348 return redirect(url('edit_repo', repo_name=changed_name))
349
349
350 @HasRepoPermissionAllDecorator('repository.admin')
350 @HasRepoPermissionAllDecorator('repository.admin')
351 @auth.CSRFRequired()
351 @auth.CSRFRequired()
352 def delete(self, repo_name):
352 def delete(self, repo_name):
353 """
353 """
354 DELETE /repos/repo_name: Delete an existing item"""
354 DELETE /repos/repo_name: Delete an existing item"""
355 # Forms posted to this method should contain a hidden field:
355 # Forms posted to this method should contain a hidden field:
356 # <input type="hidden" name="_method" value="DELETE" />
356 # <input type="hidden" name="_method" value="DELETE" />
357 # Or using helpers:
357 # Or using helpers:
358 # h.form(url('repo', repo_name=ID),
358 # h.form(url('repo', repo_name=ID),
359 # method='delete')
359 # method='delete')
360 # url('repo', repo_name=ID)
360 # url('repo', repo_name=ID)
361
361
362 repo_model = RepoModel()
362 repo_model = RepoModel()
363 repo = repo_model.get_by_repo_name(repo_name)
363 repo = repo_model.get_by_repo_name(repo_name)
364 if not repo:
364 if not repo:
365 h.not_mapped_error(repo_name)
365 h.not_mapped_error(repo_name)
366 return redirect(url('repos'))
366 return redirect(url('repos'))
367 try:
367 try:
368 _forks = repo.forks.count()
368 _forks = repo.forks.count()
369 handle_forks = None
369 handle_forks = None
370 if _forks and request.POST.get('forks'):
370 if _forks and request.POST.get('forks'):
371 do = request.POST['forks']
371 do = request.POST['forks']
372 if do == 'detach_forks':
372 if do == 'detach_forks':
373 handle_forks = 'detach'
373 handle_forks = 'detach'
374 h.flash(_('Detached %s forks') % _forks, category='success')
374 h.flash(_('Detached %s forks') % _forks, category='success')
375 elif do == 'delete_forks':
375 elif do == 'delete_forks':
376 handle_forks = 'delete'
376 handle_forks = 'delete'
377 h.flash(_('Deleted %s forks') % _forks, category='success')
377 h.flash(_('Deleted %s forks') % _forks, category='success')
378 repo_model.delete(repo, forks=handle_forks)
378 repo_model.delete(repo, forks=handle_forks)
379 action_logger(c.rhodecode_user, 'admin_deleted_repo',
379 action_logger(c.rhodecode_user, 'admin_deleted_repo',
380 repo_name, self.ip_addr, self.sa)
380 repo_name, self.ip_addr, self.sa)
381 ScmModel().mark_for_invalidation(repo_name)
381 ScmModel().mark_for_invalidation(repo_name)
382 h.flash(_('Deleted repository %s') % repo_name, category='success')
382 h.flash(_('Deleted repository %s') % repo_name, category='success')
383 Session().commit()
383 Session().commit()
384 except AttachedForksError:
384 except AttachedForksError:
385 h.flash(_('Cannot delete %s it still contains attached forks')
385 h.flash(_('Cannot delete %s it still contains attached forks')
386 % repo_name, category='warning')
386 % repo_name, category='warning')
387
387
388 except Exception:
388 except Exception:
389 log.exception("Exception during deletion of repository")
389 log.exception("Exception during deletion of repository")
390 h.flash(_('An error occurred during deletion of %s') % repo_name,
390 h.flash(_('An error occurred during deletion of %s') % repo_name,
391 category='error')
391 category='error')
392
392
393 return redirect(url('repos'))
393 return redirect(url('repos'))
394
394
395 @HasPermissionAllDecorator('hg.admin')
395 @HasPermissionAllDecorator('hg.admin')
396 def show(self, repo_name, format='html'):
396 def show(self, repo_name, format='html'):
397 """GET /repos/repo_name: Show a specific item"""
397 """GET /repos/repo_name: Show a specific item"""
398 # url('repo', repo_name=ID)
398 # url('repo', repo_name=ID)
399
399
400 @HasRepoPermissionAllDecorator('repository.admin')
400 @HasRepoPermissionAllDecorator('repository.admin')
401 def edit(self, repo_name):
401 def edit(self, repo_name):
402 """GET /repo_name/settings: Form to edit an existing item"""
402 """GET /repo_name/settings: Form to edit an existing item"""
403 # url('edit_repo', repo_name=ID)
403 # url('edit_repo', repo_name=ID)
404 defaults = self.__load_data(repo_name)
404 defaults = self.__load_data(repo_name)
405 if 'clone_uri' in defaults:
405 if 'clone_uri' in defaults:
406 del defaults['clone_uri']
406 del defaults['clone_uri']
407
407
408 c.repo_fields = RepositoryField.query()\
408 c.repo_fields = RepositoryField.query()\
409 .filter(RepositoryField.repository == c.repo_info).all()
409 .filter(RepositoryField.repository == c.repo_info).all()
410 c.personal_repo_group = c.rhodecode_user.personal_repo_group
410 c.personal_repo_group = c.rhodecode_user.personal_repo_group
411 c.active = 'settings'
411 c.active = 'settings'
412 return htmlfill.render(
412 return htmlfill.render(
413 render('admin/repos/repo_edit.html'),
413 render('admin/repos/repo_edit.mako'),
414 defaults=defaults,
414 defaults=defaults,
415 encoding="UTF-8",
415 encoding="UTF-8",
416 force_defaults=False)
416 force_defaults=False)
417
417
418 @HasRepoPermissionAllDecorator('repository.admin')
418 @HasRepoPermissionAllDecorator('repository.admin')
419 def edit_permissions(self, repo_name):
419 def edit_permissions(self, repo_name):
420 """GET /repo_name/settings: Form to edit an existing item"""
420 """GET /repo_name/settings: Form to edit an existing item"""
421 # url('edit_repo', repo_name=ID)
421 # url('edit_repo', repo_name=ID)
422 c.repo_info = self._load_repo(repo_name)
422 c.repo_info = self._load_repo(repo_name)
423 c.active = 'permissions'
423 c.active = 'permissions'
424 defaults = RepoModel()._get_defaults(repo_name)
424 defaults = RepoModel()._get_defaults(repo_name)
425
425
426 return htmlfill.render(
426 return htmlfill.render(
427 render('admin/repos/repo_edit.html'),
427 render('admin/repos/repo_edit.mako'),
428 defaults=defaults,
428 defaults=defaults,
429 encoding="UTF-8",
429 encoding="UTF-8",
430 force_defaults=False)
430 force_defaults=False)
431
431
432 @HasRepoPermissionAllDecorator('repository.admin')
432 @HasRepoPermissionAllDecorator('repository.admin')
433 @auth.CSRFRequired()
433 @auth.CSRFRequired()
434 def edit_permissions_update(self, repo_name):
434 def edit_permissions_update(self, repo_name):
435 form = RepoPermsForm()().to_python(request.POST)
435 form = RepoPermsForm()().to_python(request.POST)
436 RepoModel().update_permissions(repo_name,
436 RepoModel().update_permissions(repo_name,
437 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
437 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
438
438
439 #TODO: implement this
439 #TODO: implement this
440 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
440 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
441 # repo_name, self.ip_addr, self.sa)
441 # repo_name, self.ip_addr, self.sa)
442 Session().commit()
442 Session().commit()
443 h.flash(_('Repository permissions updated'), category='success')
443 h.flash(_('Repository permissions updated'), category='success')
444 return redirect(url('edit_repo_perms', repo_name=repo_name))
444 return redirect(url('edit_repo_perms', repo_name=repo_name))
445
445
446 @HasRepoPermissionAllDecorator('repository.admin')
446 @HasRepoPermissionAllDecorator('repository.admin')
447 def edit_fields(self, repo_name):
447 def edit_fields(self, repo_name):
448 """GET /repo_name/settings: Form to edit an existing item"""
448 """GET /repo_name/settings: Form to edit an existing item"""
449 # url('edit_repo', repo_name=ID)
449 # url('edit_repo', repo_name=ID)
450 c.repo_info = self._load_repo(repo_name)
450 c.repo_info = self._load_repo(repo_name)
451 c.repo_fields = RepositoryField.query()\
451 c.repo_fields = RepositoryField.query()\
452 .filter(RepositoryField.repository == c.repo_info).all()
452 .filter(RepositoryField.repository == c.repo_info).all()
453 c.active = 'fields'
453 c.active = 'fields'
454 if request.POST:
454 if request.POST:
455
455
456 return redirect(url('repo_edit_fields'))
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 @HasRepoPermissionAllDecorator('repository.admin')
459 @HasRepoPermissionAllDecorator('repository.admin')
460 @auth.CSRFRequired()
460 @auth.CSRFRequired()
461 def create_repo_field(self, repo_name):
461 def create_repo_field(self, repo_name):
462 try:
462 try:
463 form_result = RepoFieldForm()().to_python(dict(request.POST))
463 form_result = RepoFieldForm()().to_python(dict(request.POST))
464 RepoModel().add_repo_field(
464 RepoModel().add_repo_field(
465 repo_name, form_result['new_field_key'],
465 repo_name, form_result['new_field_key'],
466 field_type=form_result['new_field_type'],
466 field_type=form_result['new_field_type'],
467 field_value=form_result['new_field_value'],
467 field_value=form_result['new_field_value'],
468 field_label=form_result['new_field_label'],
468 field_label=form_result['new_field_label'],
469 field_desc=form_result['new_field_desc'])
469 field_desc=form_result['new_field_desc'])
470
470
471 Session().commit()
471 Session().commit()
472 except Exception as e:
472 except Exception as e:
473 log.exception("Exception creating field")
473 log.exception("Exception creating field")
474 msg = _('An error occurred during creation of field')
474 msg = _('An error occurred during creation of field')
475 if isinstance(e, formencode.Invalid):
475 if isinstance(e, formencode.Invalid):
476 msg += ". " + e.msg
476 msg += ". " + e.msg
477 h.flash(msg, category='error')
477 h.flash(msg, category='error')
478 return redirect(url('edit_repo_fields', repo_name=repo_name))
478 return redirect(url('edit_repo_fields', repo_name=repo_name))
479
479
480 @HasRepoPermissionAllDecorator('repository.admin')
480 @HasRepoPermissionAllDecorator('repository.admin')
481 @auth.CSRFRequired()
481 @auth.CSRFRequired()
482 def delete_repo_field(self, repo_name, field_id):
482 def delete_repo_field(self, repo_name, field_id):
483 field = RepositoryField.get_or_404(field_id)
483 field = RepositoryField.get_or_404(field_id)
484 try:
484 try:
485 RepoModel().delete_repo_field(repo_name, field.field_key)
485 RepoModel().delete_repo_field(repo_name, field.field_key)
486 Session().commit()
486 Session().commit()
487 except Exception as e:
487 except Exception as e:
488 log.exception("Exception during removal of field")
488 log.exception("Exception during removal of field")
489 msg = _('An error occurred during removal of field')
489 msg = _('An error occurred during removal of field')
490 h.flash(msg, category='error')
490 h.flash(msg, category='error')
491 return redirect(url('edit_repo_fields', repo_name=repo_name))
491 return redirect(url('edit_repo_fields', repo_name=repo_name))
492
492
493 @HasRepoPermissionAllDecorator('repository.admin')
493 @HasRepoPermissionAllDecorator('repository.admin')
494 def edit_advanced(self, repo_name):
494 def edit_advanced(self, repo_name):
495 """GET /repo_name/settings: Form to edit an existing item"""
495 """GET /repo_name/settings: Form to edit an existing item"""
496 # url('edit_repo', repo_name=ID)
496 # url('edit_repo', repo_name=ID)
497 c.repo_info = self._load_repo(repo_name)
497 c.repo_info = self._load_repo(repo_name)
498 c.default_user_id = User.get_default_user().user_id
498 c.default_user_id = User.get_default_user().user_id
499 c.in_public_journal = UserFollowing.query()\
499 c.in_public_journal = UserFollowing.query()\
500 .filter(UserFollowing.user_id == c.default_user_id)\
500 .filter(UserFollowing.user_id == c.default_user_id)\
501 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
501 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
502
502
503 c.active = 'advanced'
503 c.active = 'advanced'
504 c.has_origin_repo_read_perm = False
504 c.has_origin_repo_read_perm = False
505 if c.repo_info.fork:
505 if c.repo_info.fork:
506 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
506 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
507 'repository.write', 'repository.read', 'repository.admin')(
507 'repository.write', 'repository.read', 'repository.admin')(
508 c.repo_info.fork.repo_name, 'repo set as fork page')
508 c.repo_info.fork.repo_name, 'repo set as fork page')
509
509
510 if request.POST:
510 if request.POST:
511 return redirect(url('repo_edit_advanced'))
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 @HasRepoPermissionAllDecorator('repository.admin')
514 @HasRepoPermissionAllDecorator('repository.admin')
515 @auth.CSRFRequired()
515 @auth.CSRFRequired()
516 def edit_advanced_journal(self, repo_name):
516 def edit_advanced_journal(self, repo_name):
517 """
517 """
518 Set's this repository to be visible in public journal,
518 Set's this repository to be visible in public journal,
519 in other words assing default user to follow this repo
519 in other words assing default user to follow this repo
520
520
521 :param repo_name:
521 :param repo_name:
522 """
522 """
523
523
524 try:
524 try:
525 repo_id = Repository.get_by_repo_name(repo_name).repo_id
525 repo_id = Repository.get_by_repo_name(repo_name).repo_id
526 user_id = User.get_default_user().user_id
526 user_id = User.get_default_user().user_id
527 self.scm_model.toggle_following_repo(repo_id, user_id)
527 self.scm_model.toggle_following_repo(repo_id, user_id)
528 h.flash(_('Updated repository visibility in public journal'),
528 h.flash(_('Updated repository visibility in public journal'),
529 category='success')
529 category='success')
530 Session().commit()
530 Session().commit()
531 except Exception:
531 except Exception:
532 h.flash(_('An error occurred during setting this'
532 h.flash(_('An error occurred during setting this'
533 ' repository in public journal'),
533 ' repository in public journal'),
534 category='error')
534 category='error')
535
535
536 return redirect(url('edit_repo_advanced', repo_name=repo_name))
536 return redirect(url('edit_repo_advanced', repo_name=repo_name))
537
537
538 @HasRepoPermissionAllDecorator('repository.admin')
538 @HasRepoPermissionAllDecorator('repository.admin')
539 @auth.CSRFRequired()
539 @auth.CSRFRequired()
540 def edit_advanced_fork(self, repo_name):
540 def edit_advanced_fork(self, repo_name):
541 """
541 """
542 Mark given repository as a fork of another
542 Mark given repository as a fork of another
543
543
544 :param repo_name:
544 :param repo_name:
545 """
545 """
546
546
547 new_fork_id = request.POST.get('id_fork_of')
547 new_fork_id = request.POST.get('id_fork_of')
548 try:
548 try:
549
549
550 if new_fork_id and not new_fork_id.isdigit():
550 if new_fork_id and not new_fork_id.isdigit():
551 log.error('Given fork id %s is not an INT', new_fork_id)
551 log.error('Given fork id %s is not an INT', new_fork_id)
552
552
553 fork_id = safe_int(new_fork_id)
553 fork_id = safe_int(new_fork_id)
554 repo = ScmModel().mark_as_fork(repo_name, fork_id,
554 repo = ScmModel().mark_as_fork(repo_name, fork_id,
555 c.rhodecode_user.username)
555 c.rhodecode_user.username)
556 fork = repo.fork.repo_name if repo.fork else _('Nothing')
556 fork = repo.fork.repo_name if repo.fork else _('Nothing')
557 Session().commit()
557 Session().commit()
558 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
558 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
559 category='success')
559 category='success')
560 except RepositoryError as e:
560 except RepositoryError as e:
561 log.exception("Repository Error occurred")
561 log.exception("Repository Error occurred")
562 h.flash(str(e), category='error')
562 h.flash(str(e), category='error')
563 except Exception as e:
563 except Exception as e:
564 log.exception("Exception while editing fork")
564 log.exception("Exception while editing fork")
565 h.flash(_('An error occurred during this operation'),
565 h.flash(_('An error occurred during this operation'),
566 category='error')
566 category='error')
567
567
568 return redirect(url('edit_repo_advanced', repo_name=repo_name))
568 return redirect(url('edit_repo_advanced', repo_name=repo_name))
569
569
570 @HasRepoPermissionAllDecorator('repository.admin')
570 @HasRepoPermissionAllDecorator('repository.admin')
571 @auth.CSRFRequired()
571 @auth.CSRFRequired()
572 def edit_advanced_locking(self, repo_name):
572 def edit_advanced_locking(self, repo_name):
573 """
573 """
574 Unlock repository when it is locked !
574 Unlock repository when it is locked !
575
575
576 :param repo_name:
576 :param repo_name:
577 """
577 """
578 try:
578 try:
579 repo = Repository.get_by_repo_name(repo_name)
579 repo = Repository.get_by_repo_name(repo_name)
580 if request.POST.get('set_lock'):
580 if request.POST.get('set_lock'):
581 Repository.lock(repo, c.rhodecode_user.user_id,
581 Repository.lock(repo, c.rhodecode_user.user_id,
582 lock_reason=Repository.LOCK_WEB)
582 lock_reason=Repository.LOCK_WEB)
583 h.flash(_('Locked repository'), category='success')
583 h.flash(_('Locked repository'), category='success')
584 elif request.POST.get('set_unlock'):
584 elif request.POST.get('set_unlock'):
585 Repository.unlock(repo)
585 Repository.unlock(repo)
586 h.flash(_('Unlocked repository'), category='success')
586 h.flash(_('Unlocked repository'), category='success')
587 except Exception as e:
587 except Exception as e:
588 log.exception("Exception during unlocking")
588 log.exception("Exception during unlocking")
589 h.flash(_('An error occurred during unlocking'),
589 h.flash(_('An error occurred during unlocking'),
590 category='error')
590 category='error')
591 return redirect(url('edit_repo_advanced', repo_name=repo_name))
591 return redirect(url('edit_repo_advanced', repo_name=repo_name))
592
592
593 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
593 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
594 @auth.CSRFRequired()
594 @auth.CSRFRequired()
595 def toggle_locking(self, repo_name):
595 def toggle_locking(self, repo_name):
596 """
596 """
597 Toggle locking of repository by simple GET call to url
597 Toggle locking of repository by simple GET call to url
598
598
599 :param repo_name:
599 :param repo_name:
600 """
600 """
601
601
602 try:
602 try:
603 repo = Repository.get_by_repo_name(repo_name)
603 repo = Repository.get_by_repo_name(repo_name)
604
604
605 if repo.enable_locking:
605 if repo.enable_locking:
606 if repo.locked[0]:
606 if repo.locked[0]:
607 Repository.unlock(repo)
607 Repository.unlock(repo)
608 action = _('Unlocked')
608 action = _('Unlocked')
609 else:
609 else:
610 Repository.lock(repo, c.rhodecode_user.user_id,
610 Repository.lock(repo, c.rhodecode_user.user_id,
611 lock_reason=Repository.LOCK_WEB)
611 lock_reason=Repository.LOCK_WEB)
612 action = _('Locked')
612 action = _('Locked')
613
613
614 h.flash(_('Repository has been %s') % action,
614 h.flash(_('Repository has been %s') % action,
615 category='success')
615 category='success')
616 except Exception:
616 except Exception:
617 log.exception("Exception during unlocking")
617 log.exception("Exception during unlocking")
618 h.flash(_('An error occurred during unlocking'),
618 h.flash(_('An error occurred during unlocking'),
619 category='error')
619 category='error')
620 return redirect(url('summary_home', repo_name=repo_name))
620 return redirect(url('summary_home', repo_name=repo_name))
621
621
622 @HasRepoPermissionAllDecorator('repository.admin')
622 @HasRepoPermissionAllDecorator('repository.admin')
623 @auth.CSRFRequired()
623 @auth.CSRFRequired()
624 def edit_caches(self, repo_name):
624 def edit_caches(self, repo_name):
625 """PUT /{repo_name}/settings/caches: invalidate the repo caches."""
625 """PUT /{repo_name}/settings/caches: invalidate the repo caches."""
626 try:
626 try:
627 ScmModel().mark_for_invalidation(repo_name, delete=True)
627 ScmModel().mark_for_invalidation(repo_name, delete=True)
628 Session().commit()
628 Session().commit()
629 h.flash(_('Cache invalidation successful'),
629 h.flash(_('Cache invalidation successful'),
630 category='success')
630 category='success')
631 except Exception:
631 except Exception:
632 log.exception("Exception during cache invalidation")
632 log.exception("Exception during cache invalidation")
633 h.flash(_('An error occurred during cache invalidation'),
633 h.flash(_('An error occurred during cache invalidation'),
634 category='error')
634 category='error')
635
635
636 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
636 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
637
637
638 @HasRepoPermissionAllDecorator('repository.admin')
638 @HasRepoPermissionAllDecorator('repository.admin')
639 def edit_caches_form(self, repo_name):
639 def edit_caches_form(self, repo_name):
640 """GET /repo_name/settings: Form to edit an existing item"""
640 """GET /repo_name/settings: Form to edit an existing item"""
641 # url('edit_repo', repo_name=ID)
641 # url('edit_repo', repo_name=ID)
642 c.repo_info = self._load_repo(repo_name)
642 c.repo_info = self._load_repo(repo_name)
643 c.active = 'caches'
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 @HasRepoPermissionAllDecorator('repository.admin')
647 @HasRepoPermissionAllDecorator('repository.admin')
648 @auth.CSRFRequired()
648 @auth.CSRFRequired()
649 def edit_remote(self, repo_name):
649 def edit_remote(self, repo_name):
650 """PUT /{repo_name}/settings/remote: edit the repo remote."""
650 """PUT /{repo_name}/settings/remote: edit the repo remote."""
651 try:
651 try:
652 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
652 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
653 h.flash(_('Pulled from remote location'), category='success')
653 h.flash(_('Pulled from remote location'), category='success')
654 except Exception:
654 except Exception:
655 log.exception("Exception during pull from remote")
655 log.exception("Exception during pull from remote")
656 h.flash(_('An error occurred during pull from remote location'),
656 h.flash(_('An error occurred during pull from remote location'),
657 category='error')
657 category='error')
658 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
658 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
659
659
660 @HasRepoPermissionAllDecorator('repository.admin')
660 @HasRepoPermissionAllDecorator('repository.admin')
661 def edit_remote_form(self, repo_name):
661 def edit_remote_form(self, repo_name):
662 """GET /repo_name/settings: Form to edit an existing item"""
662 """GET /repo_name/settings: Form to edit an existing item"""
663 # url('edit_repo', repo_name=ID)
663 # url('edit_repo', repo_name=ID)
664 c.repo_info = self._load_repo(repo_name)
664 c.repo_info = self._load_repo(repo_name)
665 c.active = 'remote'
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 @HasRepoPermissionAllDecorator('repository.admin')
669 @HasRepoPermissionAllDecorator('repository.admin')
670 @auth.CSRFRequired()
670 @auth.CSRFRequired()
671 def edit_statistics(self, repo_name):
671 def edit_statistics(self, repo_name):
672 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
672 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
673 try:
673 try:
674 RepoModel().delete_stats(repo_name)
674 RepoModel().delete_stats(repo_name)
675 Session().commit()
675 Session().commit()
676 except Exception as e:
676 except Exception as e:
677 log.error(traceback.format_exc())
677 log.error(traceback.format_exc())
678 h.flash(_('An error occurred during deletion of repository stats'),
678 h.flash(_('An error occurred during deletion of repository stats'),
679 category='error')
679 category='error')
680 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
680 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
681
681
682 @HasRepoPermissionAllDecorator('repository.admin')
682 @HasRepoPermissionAllDecorator('repository.admin')
683 def edit_statistics_form(self, repo_name):
683 def edit_statistics_form(self, repo_name):
684 """GET /repo_name/settings: Form to edit an existing item"""
684 """GET /repo_name/settings: Form to edit an existing item"""
685 # url('edit_repo', repo_name=ID)
685 # url('edit_repo', repo_name=ID)
686 c.repo_info = self._load_repo(repo_name)
686 c.repo_info = self._load_repo(repo_name)
687 repo = c.repo_info.scm_instance()
687 repo = c.repo_info.scm_instance()
688
688
689 if c.repo_info.stats:
689 if c.repo_info.stats:
690 # this is on what revision we ended up so we add +1 for count
690 # this is on what revision we ended up so we add +1 for count
691 last_rev = c.repo_info.stats.stat_on_revision + 1
691 last_rev = c.repo_info.stats.stat_on_revision + 1
692 else:
692 else:
693 last_rev = 0
693 last_rev = 0
694 c.stats_revision = last_rev
694 c.stats_revision = last_rev
695
695
696 c.repo_last_rev = repo.count()
696 c.repo_last_rev = repo.count()
697
697
698 if last_rev == 0 or c.repo_last_rev == 0:
698 if last_rev == 0 or c.repo_last_rev == 0:
699 c.stats_percentage = 0
699 c.stats_percentage = 0
700 else:
700 else:
701 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
701 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
702
702
703 c.active = 'statistics'
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 @HasRepoPermissionAllDecorator('repository.admin')
707 @HasRepoPermissionAllDecorator('repository.admin')
708 @auth.CSRFRequired()
708 @auth.CSRFRequired()
709 def repo_issuetracker_test(self, repo_name):
709 def repo_issuetracker_test(self, repo_name):
710 if request.is_xhr:
710 if request.is_xhr:
711 return h.urlify_commit_message(
711 return h.urlify_commit_message(
712 request.POST.get('test_text', ''),
712 request.POST.get('test_text', ''),
713 repo_name)
713 repo_name)
714 else:
714 else:
715 raise HTTPBadRequest()
715 raise HTTPBadRequest()
716
716
717 @HasRepoPermissionAllDecorator('repository.admin')
717 @HasRepoPermissionAllDecorator('repository.admin')
718 @auth.CSRFRequired()
718 @auth.CSRFRequired()
719 def repo_issuetracker_delete(self, repo_name):
719 def repo_issuetracker_delete(self, repo_name):
720 uid = request.POST.get('uid')
720 uid = request.POST.get('uid')
721 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
721 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
722 try:
722 try:
723 repo_settings.delete_entries(uid)
723 repo_settings.delete_entries(uid)
724 except Exception:
724 except Exception:
725 h.flash(_('Error occurred during deleting issue tracker entry'),
725 h.flash(_('Error occurred during deleting issue tracker entry'),
726 category='error')
726 category='error')
727 else:
727 else:
728 h.flash(_('Removed issue tracker entry'), category='success')
728 h.flash(_('Removed issue tracker entry'), category='success')
729 return redirect(url('repo_settings_issuetracker',
729 return redirect(url('repo_settings_issuetracker',
730 repo_name=repo_name))
730 repo_name=repo_name))
731
731
732 def _update_patterns(self, form, repo_settings):
732 def _update_patterns(self, form, repo_settings):
733 for uid in form['delete_patterns']:
733 for uid in form['delete_patterns']:
734 repo_settings.delete_entries(uid)
734 repo_settings.delete_entries(uid)
735
735
736 for pattern in form['patterns']:
736 for pattern in form['patterns']:
737 for setting, value, type_ in pattern:
737 for setting, value, type_ in pattern:
738 sett = repo_settings.create_or_update_setting(
738 sett = repo_settings.create_or_update_setting(
739 setting, value, type_)
739 setting, value, type_)
740 Session().add(sett)
740 Session().add(sett)
741
741
742 Session().commit()
742 Session().commit()
743
743
744 @HasRepoPermissionAllDecorator('repository.admin')
744 @HasRepoPermissionAllDecorator('repository.admin')
745 @auth.CSRFRequired()
745 @auth.CSRFRequired()
746 def repo_issuetracker_save(self, repo_name):
746 def repo_issuetracker_save(self, repo_name):
747 # Save inheritance
747 # Save inheritance
748 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
748 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
749 inherited = (request.POST.get('inherit_global_issuetracker')
749 inherited = (request.POST.get('inherit_global_issuetracker')
750 == "inherited")
750 == "inherited")
751 repo_settings.inherit_global_settings = inherited
751 repo_settings.inherit_global_settings = inherited
752 Session().commit()
752 Session().commit()
753
753
754 form = IssueTrackerPatternsForm()().to_python(request.POST)
754 form = IssueTrackerPatternsForm()().to_python(request.POST)
755 if form:
755 if form:
756 self._update_patterns(form, repo_settings)
756 self._update_patterns(form, repo_settings)
757
757
758 h.flash(_('Updated issue tracker entries'), category='success')
758 h.flash(_('Updated issue tracker entries'), category='success')
759 return redirect(url('repo_settings_issuetracker',
759 return redirect(url('repo_settings_issuetracker',
760 repo_name=repo_name))
760 repo_name=repo_name))
761
761
762 @HasRepoPermissionAllDecorator('repository.admin')
762 @HasRepoPermissionAllDecorator('repository.admin')
763 def repo_issuetracker(self, repo_name):
763 def repo_issuetracker(self, repo_name):
764 """GET /admin/settings/issue-tracker: All items in the collection"""
764 """GET /admin/settings/issue-tracker: All items in the collection"""
765 c.active = 'issuetracker'
765 c.active = 'issuetracker'
766 c.data = 'data'
766 c.data = 'data'
767 c.repo_info = self._load_repo(repo_name)
767 c.repo_info = self._load_repo(repo_name)
768
768
769 repo = Repository.get_by_repo_name(repo_name)
769 repo = Repository.get_by_repo_name(repo_name)
770 c.settings_model = IssueTrackerSettingsModel(repo=repo)
770 c.settings_model = IssueTrackerSettingsModel(repo=repo)
771 c.global_patterns = c.settings_model.get_global_settings()
771 c.global_patterns = c.settings_model.get_global_settings()
772 c.repo_patterns = c.settings_model.get_repo_settings()
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 @HasRepoPermissionAllDecorator('repository.admin')
776 @HasRepoPermissionAllDecorator('repository.admin')
777 def repo_settings_vcs(self, repo_name):
777 def repo_settings_vcs(self, repo_name):
778 """GET /{repo_name}/settings/vcs/: All items in the collection"""
778 """GET /{repo_name}/settings/vcs/: All items in the collection"""
779
779
780 model = VcsSettingsModel(repo=repo_name)
780 model = VcsSettingsModel(repo=repo_name)
781
781
782 c.active = 'vcs'
782 c.active = 'vcs'
783 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
783 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
784 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
784 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
785 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
785 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
786 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
786 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
787 c.repo_info = self._load_repo(repo_name)
787 c.repo_info = self._load_repo(repo_name)
788 defaults = self._vcs_form_defaults(repo_name)
788 defaults = self._vcs_form_defaults(repo_name)
789 c.inherit_global_settings = defaults['inherit_global_settings']
789 c.inherit_global_settings = defaults['inherit_global_settings']
790 c.labs_active = str2bool(
790 c.labs_active = str2bool(
791 rhodecode.CONFIG.get('labs_settings_active', 'true'))
791 rhodecode.CONFIG.get('labs_settings_active', 'true'))
792
792
793 return htmlfill.render(
793 return htmlfill.render(
794 render('admin/repos/repo_edit.html'),
794 render('admin/repos/repo_edit.mako'),
795 defaults=defaults,
795 defaults=defaults,
796 encoding="UTF-8",
796 encoding="UTF-8",
797 force_defaults=False)
797 force_defaults=False)
798
798
799 @HasRepoPermissionAllDecorator('repository.admin')
799 @HasRepoPermissionAllDecorator('repository.admin')
800 @auth.CSRFRequired()
800 @auth.CSRFRequired()
801 def repo_settings_vcs_update(self, repo_name):
801 def repo_settings_vcs_update(self, repo_name):
802 """POST /{repo_name}/settings/vcs/: All items in the collection"""
802 """POST /{repo_name}/settings/vcs/: All items in the collection"""
803 c.active = 'vcs'
803 c.active = 'vcs'
804
804
805 model = VcsSettingsModel(repo=repo_name)
805 model = VcsSettingsModel(repo=repo_name)
806 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
806 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
807 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
807 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
808 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
808 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
809 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
809 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
810 c.repo_info = self._load_repo(repo_name)
810 c.repo_info = self._load_repo(repo_name)
811 defaults = self._vcs_form_defaults(repo_name)
811 defaults = self._vcs_form_defaults(repo_name)
812 c.inherit_global_settings = defaults['inherit_global_settings']
812 c.inherit_global_settings = defaults['inherit_global_settings']
813
813
814 application_form = RepoVcsSettingsForm(repo_name)()
814 application_form = RepoVcsSettingsForm(repo_name)()
815 try:
815 try:
816 form_result = application_form.to_python(dict(request.POST))
816 form_result = application_form.to_python(dict(request.POST))
817 except formencode.Invalid as errors:
817 except formencode.Invalid as errors:
818 h.flash(
818 h.flash(
819 _("Some form inputs contain invalid data."),
819 _("Some form inputs contain invalid data."),
820 category='error')
820 category='error')
821 return htmlfill.render(
821 return htmlfill.render(
822 render('admin/repos/repo_edit.html'),
822 render('admin/repos/repo_edit.mako'),
823 defaults=errors.value,
823 defaults=errors.value,
824 errors=errors.error_dict or {},
824 errors=errors.error_dict or {},
825 prefix_error=False,
825 prefix_error=False,
826 encoding="UTF-8",
826 encoding="UTF-8",
827 force_defaults=False
827 force_defaults=False
828 )
828 )
829
829
830 try:
830 try:
831 inherit_global_settings = form_result['inherit_global_settings']
831 inherit_global_settings = form_result['inherit_global_settings']
832 model.create_or_update_repo_settings(
832 model.create_or_update_repo_settings(
833 form_result, inherit_global_settings=inherit_global_settings)
833 form_result, inherit_global_settings=inherit_global_settings)
834 except Exception:
834 except Exception:
835 log.exception("Exception while updating settings")
835 log.exception("Exception while updating settings")
836 h.flash(
836 h.flash(
837 _('Error occurred during updating repository VCS settings'),
837 _('Error occurred during updating repository VCS settings'),
838 category='error')
838 category='error')
839 else:
839 else:
840 Session().commit()
840 Session().commit()
841 h.flash(_('Updated VCS settings'), category='success')
841 h.flash(_('Updated VCS settings'), category='success')
842 return redirect(url('repo_vcs_settings', repo_name=repo_name))
842 return redirect(url('repo_vcs_settings', repo_name=repo_name))
843
843
844 return htmlfill.render(
844 return htmlfill.render(
845 render('admin/repos/repo_edit.html'),
845 render('admin/repos/repo_edit.mako'),
846 defaults=self._vcs_form_defaults(repo_name),
846 defaults=self._vcs_form_defaults(repo_name),
847 encoding="UTF-8",
847 encoding="UTF-8",
848 force_defaults=False)
848 force_defaults=False)
849
849
850 @HasRepoPermissionAllDecorator('repository.admin')
850 @HasRepoPermissionAllDecorator('repository.admin')
851 @auth.CSRFRequired()
851 @auth.CSRFRequired()
852 @jsonify
852 @jsonify
853 def repo_delete_svn_pattern(self, repo_name):
853 def repo_delete_svn_pattern(self, repo_name):
854 if not request.is_xhr:
854 if not request.is_xhr:
855 return False
855 return False
856
856
857 delete_pattern_id = request.POST.get('delete_svn_pattern')
857 delete_pattern_id = request.POST.get('delete_svn_pattern')
858 model = VcsSettingsModel(repo=repo_name)
858 model = VcsSettingsModel(repo=repo_name)
859 try:
859 try:
860 model.delete_repo_svn_pattern(delete_pattern_id)
860 model.delete_repo_svn_pattern(delete_pattern_id)
861 except SettingNotFound:
861 except SettingNotFound:
862 raise HTTPBadRequest()
862 raise HTTPBadRequest()
863
863
864 Session().commit()
864 Session().commit()
865 return True
865 return True
866
866
867 def _vcs_form_defaults(self, repo_name):
867 def _vcs_form_defaults(self, repo_name):
868 model = VcsSettingsModel(repo=repo_name)
868 model = VcsSettingsModel(repo=repo_name)
869 global_defaults = model.get_global_settings()
869 global_defaults = model.get_global_settings()
870
870
871 repo_defaults = {}
871 repo_defaults = {}
872 repo_defaults.update(global_defaults)
872 repo_defaults.update(global_defaults)
873 repo_defaults.update(model.get_repo_settings())
873 repo_defaults.update(model.get_repo_settings())
874
874
875 global_defaults = {
875 global_defaults = {
876 '{}_inherited'.format(k): global_defaults[k]
876 '{}_inherited'.format(k): global_defaults[k]
877 for k in global_defaults}
877 for k in global_defaults}
878
878
879 defaults = {
879 defaults = {
880 'inherit_global_settings': model.inherit_global_settings
880 'inherit_global_settings': model.inherit_global_settings
881 }
881 }
882 defaults.update(global_defaults)
882 defaults.update(global_defaults)
883 defaults.update(repo_defaults)
883 defaults.update(repo_defaults)
884 defaults.update({
884 defaults.update({
885 'new_svn_branch': '',
885 'new_svn_branch': '',
886 'new_svn_tag': '',
886 'new_svn_tag': '',
887 })
887 })
888 return defaults
888 return defaults
@@ -1,842 +1,842 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 settings controller for rhodecode admin
23 settings controller for rhodecode admin
24 """
24 """
25
25
26 import collections
26 import collections
27 import logging
27 import logging
28 import urllib2
28 import urllib2
29
29
30 import datetime
30 import datetime
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33 import packaging.version
33 import packaging.version
34 from pylons import request, tmpl_context as c, url, config
34 from pylons import request, tmpl_context as c, url, config
35 from pylons.controllers.util import redirect
35 from pylons.controllers.util import redirect
36 from pylons.i18n.translation import _, lazy_ugettext
36 from pylons.i18n.translation import _, lazy_ugettext
37 from pyramid.threadlocal import get_current_registry
37 from pyramid.threadlocal import get_current_registry
38 from webob.exc import HTTPBadRequest
38 from webob.exc import HTTPBadRequest
39
39
40 import rhodecode
40 import rhodecode
41 from rhodecode.admin.navigation import navigation_list
41 from rhodecode.admin.navigation import navigation_list
42 from rhodecode.lib import auth
42 from rhodecode.lib import auth
43 from rhodecode.lib import helpers as h
43 from rhodecode.lib import helpers as h
44 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
44 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
45 from rhodecode.lib.base import BaseController, render
45 from rhodecode.lib.base import BaseController, render
46 from rhodecode.lib.celerylib import tasks, run_task
46 from rhodecode.lib.celerylib import tasks, run_task
47 from rhodecode.lib.utils import repo2db_mapper
47 from rhodecode.lib.utils import repo2db_mapper
48 from rhodecode.lib.utils2 import (
48 from rhodecode.lib.utils2 import (
49 str2bool, safe_unicode, AttributeDict, safe_int)
49 str2bool, safe_unicode, AttributeDict, safe_int)
50 from rhodecode.lib.compat import OrderedDict
50 from rhodecode.lib.compat import OrderedDict
51 from rhodecode.lib.ext_json import json
51 from rhodecode.lib.ext_json import json
52 from rhodecode.lib.utils import jsonify
52 from rhodecode.lib.utils import jsonify
53
53
54 from rhodecode.model.db import RhodeCodeUi, Repository
54 from rhodecode.model.db import RhodeCodeUi, Repository
55 from rhodecode.model.forms import ApplicationSettingsForm, \
55 from rhodecode.model.forms import ApplicationSettingsForm, \
56 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
56 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
57 LabsSettingsForm, IssueTrackerPatternsForm
57 LabsSettingsForm, IssueTrackerPatternsForm
58 from rhodecode.model.repo_group import RepoGroupModel
58 from rhodecode.model.repo_group import RepoGroupModel
59
59
60 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.notification import EmailNotificationModel
61 from rhodecode.model.notification import EmailNotificationModel
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.settings import (
63 from rhodecode.model.settings import (
64 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
64 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
65 SettingsModel)
65 SettingsModel)
66
66
67 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
67 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
68 from rhodecode.svn_support.config_keys import generate_config
68 from rhodecode.svn_support.config_keys import generate_config
69
69
70
70
71 log = logging.getLogger(__name__)
71 log = logging.getLogger(__name__)
72
72
73
73
74 class SettingsController(BaseController):
74 class SettingsController(BaseController):
75 """REST Controller styled on the Atom Publishing Protocol"""
75 """REST Controller styled on the Atom Publishing Protocol"""
76 # To properly map this controller, ensure your config/routing.py
76 # To properly map this controller, ensure your config/routing.py
77 # file has a resource setup:
77 # file has a resource setup:
78 # map.resource('setting', 'settings', controller='admin/settings',
78 # map.resource('setting', 'settings', controller='admin/settings',
79 # path_prefix='/admin', name_prefix='admin_')
79 # path_prefix='/admin', name_prefix='admin_')
80
80
81 @LoginRequired()
81 @LoginRequired()
82 def __before__(self):
82 def __before__(self):
83 super(SettingsController, self).__before__()
83 super(SettingsController, self).__before__()
84 c.labs_active = str2bool(
84 c.labs_active = str2bool(
85 rhodecode.CONFIG.get('labs_settings_active', 'true'))
85 rhodecode.CONFIG.get('labs_settings_active', 'true'))
86 c.navlist = navigation_list(request)
86 c.navlist = navigation_list(request)
87
87
88 def _get_hg_ui_settings(self):
88 def _get_hg_ui_settings(self):
89 ret = RhodeCodeUi.query().all()
89 ret = RhodeCodeUi.query().all()
90
90
91 if not ret:
91 if not ret:
92 raise Exception('Could not get application ui settings !')
92 raise Exception('Could not get application ui settings !')
93 settings = {}
93 settings = {}
94 for each in ret:
94 for each in ret:
95 k = each.ui_key
95 k = each.ui_key
96 v = each.ui_value
96 v = each.ui_value
97 if k == '/':
97 if k == '/':
98 k = 'root_path'
98 k = 'root_path'
99
99
100 if k in ['push_ssl', 'publish']:
100 if k in ['push_ssl', 'publish']:
101 v = str2bool(v)
101 v = str2bool(v)
102
102
103 if k.find('.') != -1:
103 if k.find('.') != -1:
104 k = k.replace('.', '_')
104 k = k.replace('.', '_')
105
105
106 if each.ui_section in ['hooks', 'extensions']:
106 if each.ui_section in ['hooks', 'extensions']:
107 v = each.ui_active
107 v = each.ui_active
108
108
109 settings[each.ui_section + '_' + k] = v
109 settings[each.ui_section + '_' + k] = v
110 return settings
110 return settings
111
111
112 @HasPermissionAllDecorator('hg.admin')
112 @HasPermissionAllDecorator('hg.admin')
113 @auth.CSRFRequired()
113 @auth.CSRFRequired()
114 @jsonify
114 @jsonify
115 def delete_svn_pattern(self):
115 def delete_svn_pattern(self):
116 if not request.is_xhr:
116 if not request.is_xhr:
117 raise HTTPBadRequest()
117 raise HTTPBadRequest()
118
118
119 delete_pattern_id = request.POST.get('delete_svn_pattern')
119 delete_pattern_id = request.POST.get('delete_svn_pattern')
120 model = VcsSettingsModel()
120 model = VcsSettingsModel()
121 try:
121 try:
122 model.delete_global_svn_pattern(delete_pattern_id)
122 model.delete_global_svn_pattern(delete_pattern_id)
123 except SettingNotFound:
123 except SettingNotFound:
124 raise HTTPBadRequest()
124 raise HTTPBadRequest()
125
125
126 Session().commit()
126 Session().commit()
127 return True
127 return True
128
128
129 @HasPermissionAllDecorator('hg.admin')
129 @HasPermissionAllDecorator('hg.admin')
130 @auth.CSRFRequired()
130 @auth.CSRFRequired()
131 def settings_vcs_update(self):
131 def settings_vcs_update(self):
132 """POST /admin/settings: All items in the collection"""
132 """POST /admin/settings: All items in the collection"""
133 # url('admin_settings_vcs')
133 # url('admin_settings_vcs')
134 c.active = 'vcs'
134 c.active = 'vcs'
135
135
136 model = VcsSettingsModel()
136 model = VcsSettingsModel()
137 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
137 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
138 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
138 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
139
139
140 # TODO: Replace with request.registry after migrating to pyramid.
140 # TODO: Replace with request.registry after migrating to pyramid.
141 pyramid_settings = get_current_registry().settings
141 pyramid_settings = get_current_registry().settings
142 c.svn_proxy_generate_config = pyramid_settings[generate_config]
142 c.svn_proxy_generate_config = pyramid_settings[generate_config]
143
143
144 application_form = ApplicationUiSettingsForm()()
144 application_form = ApplicationUiSettingsForm()()
145
145
146 try:
146 try:
147 form_result = application_form.to_python(dict(request.POST))
147 form_result = application_form.to_python(dict(request.POST))
148 except formencode.Invalid as errors:
148 except formencode.Invalid as errors:
149 h.flash(
149 h.flash(
150 _("Some form inputs contain invalid data."),
150 _("Some form inputs contain invalid data."),
151 category='error')
151 category='error')
152 return htmlfill.render(
152 return htmlfill.render(
153 render('admin/settings/settings.html'),
153 render('admin/settings/settings.mako'),
154 defaults=errors.value,
154 defaults=errors.value,
155 errors=errors.error_dict or {},
155 errors=errors.error_dict or {},
156 prefix_error=False,
156 prefix_error=False,
157 encoding="UTF-8",
157 encoding="UTF-8",
158 force_defaults=False
158 force_defaults=False
159 )
159 )
160
160
161 try:
161 try:
162 if c.visual.allow_repo_location_change:
162 if c.visual.allow_repo_location_change:
163 model.update_global_path_setting(
163 model.update_global_path_setting(
164 form_result['paths_root_path'])
164 form_result['paths_root_path'])
165
165
166 model.update_global_ssl_setting(form_result['web_push_ssl'])
166 model.update_global_ssl_setting(form_result['web_push_ssl'])
167 model.update_global_hook_settings(form_result)
167 model.update_global_hook_settings(form_result)
168
168
169 model.create_or_update_global_svn_settings(form_result)
169 model.create_or_update_global_svn_settings(form_result)
170 model.create_or_update_global_hg_settings(form_result)
170 model.create_or_update_global_hg_settings(form_result)
171 model.create_or_update_global_pr_settings(form_result)
171 model.create_or_update_global_pr_settings(form_result)
172 except Exception:
172 except Exception:
173 log.exception("Exception while updating settings")
173 log.exception("Exception while updating settings")
174 h.flash(_('Error occurred during updating '
174 h.flash(_('Error occurred during updating '
175 'application settings'), category='error')
175 'application settings'), category='error')
176 else:
176 else:
177 Session().commit()
177 Session().commit()
178 h.flash(_('Updated VCS settings'), category='success')
178 h.flash(_('Updated VCS settings'), category='success')
179 return redirect(url('admin_settings_vcs'))
179 return redirect(url('admin_settings_vcs'))
180
180
181 return htmlfill.render(
181 return htmlfill.render(
182 render('admin/settings/settings.html'),
182 render('admin/settings/settings.mako'),
183 defaults=self._form_defaults(),
183 defaults=self._form_defaults(),
184 encoding="UTF-8",
184 encoding="UTF-8",
185 force_defaults=False)
185 force_defaults=False)
186
186
187 @HasPermissionAllDecorator('hg.admin')
187 @HasPermissionAllDecorator('hg.admin')
188 def settings_vcs(self):
188 def settings_vcs(self):
189 """GET /admin/settings: All items in the collection"""
189 """GET /admin/settings: All items in the collection"""
190 # url('admin_settings_vcs')
190 # url('admin_settings_vcs')
191 c.active = 'vcs'
191 c.active = 'vcs'
192 model = VcsSettingsModel()
192 model = VcsSettingsModel()
193 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
193 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
194 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
194 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
195
195
196 # TODO: Replace with request.registry after migrating to pyramid.
196 # TODO: Replace with request.registry after migrating to pyramid.
197 pyramid_settings = get_current_registry().settings
197 pyramid_settings = get_current_registry().settings
198 c.svn_proxy_generate_config = pyramid_settings[generate_config]
198 c.svn_proxy_generate_config = pyramid_settings[generate_config]
199
199
200 return htmlfill.render(
200 return htmlfill.render(
201 render('admin/settings/settings.html'),
201 render('admin/settings/settings.mako'),
202 defaults=self._form_defaults(),
202 defaults=self._form_defaults(),
203 encoding="UTF-8",
203 encoding="UTF-8",
204 force_defaults=False)
204 force_defaults=False)
205
205
206 @HasPermissionAllDecorator('hg.admin')
206 @HasPermissionAllDecorator('hg.admin')
207 @auth.CSRFRequired()
207 @auth.CSRFRequired()
208 def settings_mapping_update(self):
208 def settings_mapping_update(self):
209 """POST /admin/settings/mapping: All items in the collection"""
209 """POST /admin/settings/mapping: All items in the collection"""
210 # url('admin_settings_mapping')
210 # url('admin_settings_mapping')
211 c.active = 'mapping'
211 c.active = 'mapping'
212 rm_obsolete = request.POST.get('destroy', False)
212 rm_obsolete = request.POST.get('destroy', False)
213 invalidate_cache = request.POST.get('invalidate', False)
213 invalidate_cache = request.POST.get('invalidate', False)
214 log.debug(
214 log.debug(
215 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
215 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
216
216
217 if invalidate_cache:
217 if invalidate_cache:
218 log.debug('invalidating all repositories cache')
218 log.debug('invalidating all repositories cache')
219 for repo in Repository.get_all():
219 for repo in Repository.get_all():
220 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
220 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
221
221
222 filesystem_repos = ScmModel().repo_scan()
222 filesystem_repos = ScmModel().repo_scan()
223 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
223 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
224 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
224 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
225 h.flash(_('Repositories successfully '
225 h.flash(_('Repositories successfully '
226 'rescanned added: %s ; removed: %s') %
226 'rescanned added: %s ; removed: %s') %
227 (_repr(added), _repr(removed)),
227 (_repr(added), _repr(removed)),
228 category='success')
228 category='success')
229 return redirect(url('admin_settings_mapping'))
229 return redirect(url('admin_settings_mapping'))
230
230
231 @HasPermissionAllDecorator('hg.admin')
231 @HasPermissionAllDecorator('hg.admin')
232 def settings_mapping(self):
232 def settings_mapping(self):
233 """GET /admin/settings/mapping: All items in the collection"""
233 """GET /admin/settings/mapping: All items in the collection"""
234 # url('admin_settings_mapping')
234 # url('admin_settings_mapping')
235 c.active = 'mapping'
235 c.active = 'mapping'
236
236
237 return htmlfill.render(
237 return htmlfill.render(
238 render('admin/settings/settings.html'),
238 render('admin/settings/settings.mako'),
239 defaults=self._form_defaults(),
239 defaults=self._form_defaults(),
240 encoding="UTF-8",
240 encoding="UTF-8",
241 force_defaults=False)
241 force_defaults=False)
242
242
243 @HasPermissionAllDecorator('hg.admin')
243 @HasPermissionAllDecorator('hg.admin')
244 @auth.CSRFRequired()
244 @auth.CSRFRequired()
245 def settings_global_update(self):
245 def settings_global_update(self):
246 """POST /admin/settings/global: All items in the collection"""
246 """POST /admin/settings/global: All items in the collection"""
247 # url('admin_settings_global')
247 # url('admin_settings_global')
248 c.active = 'global'
248 c.active = 'global'
249 c.personal_repo_group_default_pattern = RepoGroupModel()\
249 c.personal_repo_group_default_pattern = RepoGroupModel()\
250 .get_personal_group_name_pattern()
250 .get_personal_group_name_pattern()
251 application_form = ApplicationSettingsForm()()
251 application_form = ApplicationSettingsForm()()
252 try:
252 try:
253 form_result = application_form.to_python(dict(request.POST))
253 form_result = application_form.to_python(dict(request.POST))
254 except formencode.Invalid as errors:
254 except formencode.Invalid as errors:
255 return htmlfill.render(
255 return htmlfill.render(
256 render('admin/settings/settings.html'),
256 render('admin/settings/settings.mako'),
257 defaults=errors.value,
257 defaults=errors.value,
258 errors=errors.error_dict or {},
258 errors=errors.error_dict or {},
259 prefix_error=False,
259 prefix_error=False,
260 encoding="UTF-8",
260 encoding="UTF-8",
261 force_defaults=False)
261 force_defaults=False)
262
262
263 try:
263 try:
264 settings = [
264 settings = [
265 ('title', 'rhodecode_title', 'unicode'),
265 ('title', 'rhodecode_title', 'unicode'),
266 ('realm', 'rhodecode_realm', 'unicode'),
266 ('realm', 'rhodecode_realm', 'unicode'),
267 ('pre_code', 'rhodecode_pre_code', 'unicode'),
267 ('pre_code', 'rhodecode_pre_code', 'unicode'),
268 ('post_code', 'rhodecode_post_code', 'unicode'),
268 ('post_code', 'rhodecode_post_code', 'unicode'),
269 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
269 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
270 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
270 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
271 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
271 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
272 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
272 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
273 ]
273 ]
274 for setting, form_key, type_ in settings:
274 for setting, form_key, type_ in settings:
275 sett = SettingsModel().create_or_update_setting(
275 sett = SettingsModel().create_or_update_setting(
276 setting, form_result[form_key], type_)
276 setting, form_result[form_key], type_)
277 Session().add(sett)
277 Session().add(sett)
278
278
279 Session().commit()
279 Session().commit()
280 SettingsModel().invalidate_settings_cache()
280 SettingsModel().invalidate_settings_cache()
281 h.flash(_('Updated application settings'), category='success')
281 h.flash(_('Updated application settings'), category='success')
282 except Exception:
282 except Exception:
283 log.exception("Exception while updating application settings")
283 log.exception("Exception while updating application settings")
284 h.flash(
284 h.flash(
285 _('Error occurred during updating application settings'),
285 _('Error occurred during updating application settings'),
286 category='error')
286 category='error')
287
287
288 return redirect(url('admin_settings_global'))
288 return redirect(url('admin_settings_global'))
289
289
290 @HasPermissionAllDecorator('hg.admin')
290 @HasPermissionAllDecorator('hg.admin')
291 def settings_global(self):
291 def settings_global(self):
292 """GET /admin/settings/global: All items in the collection"""
292 """GET /admin/settings/global: All items in the collection"""
293 # url('admin_settings_global')
293 # url('admin_settings_global')
294 c.active = 'global'
294 c.active = 'global'
295 c.personal_repo_group_default_pattern = RepoGroupModel()\
295 c.personal_repo_group_default_pattern = RepoGroupModel()\
296 .get_personal_group_name_pattern()
296 .get_personal_group_name_pattern()
297
297
298 return htmlfill.render(
298 return htmlfill.render(
299 render('admin/settings/settings.html'),
299 render('admin/settings/settings.mako'),
300 defaults=self._form_defaults(),
300 defaults=self._form_defaults(),
301 encoding="UTF-8",
301 encoding="UTF-8",
302 force_defaults=False)
302 force_defaults=False)
303
303
304 @HasPermissionAllDecorator('hg.admin')
304 @HasPermissionAllDecorator('hg.admin')
305 @auth.CSRFRequired()
305 @auth.CSRFRequired()
306 def settings_visual_update(self):
306 def settings_visual_update(self):
307 """POST /admin/settings/visual: All items in the collection"""
307 """POST /admin/settings/visual: All items in the collection"""
308 # url('admin_settings_visual')
308 # url('admin_settings_visual')
309 c.active = 'visual'
309 c.active = 'visual'
310 application_form = ApplicationVisualisationForm()()
310 application_form = ApplicationVisualisationForm()()
311 try:
311 try:
312 form_result = application_form.to_python(dict(request.POST))
312 form_result = application_form.to_python(dict(request.POST))
313 except formencode.Invalid as errors:
313 except formencode.Invalid as errors:
314 return htmlfill.render(
314 return htmlfill.render(
315 render('admin/settings/settings.html'),
315 render('admin/settings/settings.mako'),
316 defaults=errors.value,
316 defaults=errors.value,
317 errors=errors.error_dict or {},
317 errors=errors.error_dict or {},
318 prefix_error=False,
318 prefix_error=False,
319 encoding="UTF-8",
319 encoding="UTF-8",
320 force_defaults=False
320 force_defaults=False
321 )
321 )
322
322
323 try:
323 try:
324 settings = [
324 settings = [
325 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
325 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
326 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
326 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
327 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
327 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
328 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
328 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
329 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
329 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
330 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
330 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
331 ('show_version', 'rhodecode_show_version', 'bool'),
331 ('show_version', 'rhodecode_show_version', 'bool'),
332 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
332 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
333 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
333 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
334 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
334 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
335 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
335 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
336 ('support_url', 'rhodecode_support_url', 'unicode'),
336 ('support_url', 'rhodecode_support_url', 'unicode'),
337 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
337 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
338 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
338 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
339 ]
339 ]
340 for setting, form_key, type_ in settings:
340 for setting, form_key, type_ in settings:
341 sett = SettingsModel().create_or_update_setting(
341 sett = SettingsModel().create_or_update_setting(
342 setting, form_result[form_key], type_)
342 setting, form_result[form_key], type_)
343 Session().add(sett)
343 Session().add(sett)
344
344
345 Session().commit()
345 Session().commit()
346 SettingsModel().invalidate_settings_cache()
346 SettingsModel().invalidate_settings_cache()
347 h.flash(_('Updated visualisation settings'), category='success')
347 h.flash(_('Updated visualisation settings'), category='success')
348 except Exception:
348 except Exception:
349 log.exception("Exception updating visualization settings")
349 log.exception("Exception updating visualization settings")
350 h.flash(_('Error occurred during updating '
350 h.flash(_('Error occurred during updating '
351 'visualisation settings'),
351 'visualisation settings'),
352 category='error')
352 category='error')
353
353
354 return redirect(url('admin_settings_visual'))
354 return redirect(url('admin_settings_visual'))
355
355
356 @HasPermissionAllDecorator('hg.admin')
356 @HasPermissionAllDecorator('hg.admin')
357 def settings_visual(self):
357 def settings_visual(self):
358 """GET /admin/settings/visual: All items in the collection"""
358 """GET /admin/settings/visual: All items in the collection"""
359 # url('admin_settings_visual')
359 # url('admin_settings_visual')
360 c.active = 'visual'
360 c.active = 'visual'
361
361
362 return htmlfill.render(
362 return htmlfill.render(
363 render('admin/settings/settings.html'),
363 render('admin/settings/settings.mako'),
364 defaults=self._form_defaults(),
364 defaults=self._form_defaults(),
365 encoding="UTF-8",
365 encoding="UTF-8",
366 force_defaults=False)
366 force_defaults=False)
367
367
368 @HasPermissionAllDecorator('hg.admin')
368 @HasPermissionAllDecorator('hg.admin')
369 @auth.CSRFRequired()
369 @auth.CSRFRequired()
370 def settings_issuetracker_test(self):
370 def settings_issuetracker_test(self):
371 if request.is_xhr:
371 if request.is_xhr:
372 return h.urlify_commit_message(
372 return h.urlify_commit_message(
373 request.POST.get('test_text', ''),
373 request.POST.get('test_text', ''),
374 'repo_group/test_repo1')
374 'repo_group/test_repo1')
375 else:
375 else:
376 raise HTTPBadRequest()
376 raise HTTPBadRequest()
377
377
378 @HasPermissionAllDecorator('hg.admin')
378 @HasPermissionAllDecorator('hg.admin')
379 @auth.CSRFRequired()
379 @auth.CSRFRequired()
380 def settings_issuetracker_delete(self):
380 def settings_issuetracker_delete(self):
381 uid = request.POST.get('uid')
381 uid = request.POST.get('uid')
382 IssueTrackerSettingsModel().delete_entries(uid)
382 IssueTrackerSettingsModel().delete_entries(uid)
383 h.flash(_('Removed issue tracker entry'), category='success')
383 h.flash(_('Removed issue tracker entry'), category='success')
384 return redirect(url('admin_settings_issuetracker'))
384 return redirect(url('admin_settings_issuetracker'))
385
385
386 @HasPermissionAllDecorator('hg.admin')
386 @HasPermissionAllDecorator('hg.admin')
387 def settings_issuetracker(self):
387 def settings_issuetracker(self):
388 """GET /admin/settings/issue-tracker: All items in the collection"""
388 """GET /admin/settings/issue-tracker: All items in the collection"""
389 # url('admin_settings_issuetracker')
389 # url('admin_settings_issuetracker')
390 c.active = 'issuetracker'
390 c.active = 'issuetracker'
391 defaults = SettingsModel().get_all_settings()
391 defaults = SettingsModel().get_all_settings()
392
392
393 entry_key = 'rhodecode_issuetracker_pat_'
393 entry_key = 'rhodecode_issuetracker_pat_'
394
394
395 c.issuetracker_entries = {}
395 c.issuetracker_entries = {}
396 for k, v in defaults.items():
396 for k, v in defaults.items():
397 if k.startswith(entry_key):
397 if k.startswith(entry_key):
398 uid = k[len(entry_key):]
398 uid = k[len(entry_key):]
399 c.issuetracker_entries[uid] = None
399 c.issuetracker_entries[uid] = None
400
400
401 for uid in c.issuetracker_entries:
401 for uid in c.issuetracker_entries:
402 c.issuetracker_entries[uid] = AttributeDict({
402 c.issuetracker_entries[uid] = AttributeDict({
403 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
403 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
404 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
404 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
405 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
405 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
406 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
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 @HasPermissionAllDecorator('hg.admin')
411 @HasPermissionAllDecorator('hg.admin')
412 @auth.CSRFRequired()
412 @auth.CSRFRequired()
413 def settings_issuetracker_save(self):
413 def settings_issuetracker_save(self):
414 settings_model = IssueTrackerSettingsModel()
414 settings_model = IssueTrackerSettingsModel()
415
415
416 form = IssueTrackerPatternsForm()().to_python(request.POST)
416 form = IssueTrackerPatternsForm()().to_python(request.POST)
417 if form:
417 if form:
418 for uid in form.get('delete_patterns', []):
418 for uid in form.get('delete_patterns', []):
419 settings_model.delete_entries(uid)
419 settings_model.delete_entries(uid)
420
420
421 for pattern in form.get('patterns', []):
421 for pattern in form.get('patterns', []):
422 for setting, value, type_ in pattern:
422 for setting, value, type_ in pattern:
423 sett = settings_model.create_or_update_setting(
423 sett = settings_model.create_or_update_setting(
424 setting, value, type_)
424 setting, value, type_)
425 Session().add(sett)
425 Session().add(sett)
426
426
427 Session().commit()
427 Session().commit()
428
428
429 SettingsModel().invalidate_settings_cache()
429 SettingsModel().invalidate_settings_cache()
430 h.flash(_('Updated issue tracker entries'), category='success')
430 h.flash(_('Updated issue tracker entries'), category='success')
431 return redirect(url('admin_settings_issuetracker'))
431 return redirect(url('admin_settings_issuetracker'))
432
432
433 @HasPermissionAllDecorator('hg.admin')
433 @HasPermissionAllDecorator('hg.admin')
434 @auth.CSRFRequired()
434 @auth.CSRFRequired()
435 def settings_email_update(self):
435 def settings_email_update(self):
436 """POST /admin/settings/email: All items in the collection"""
436 """POST /admin/settings/email: All items in the collection"""
437 # url('admin_settings_email')
437 # url('admin_settings_email')
438 c.active = 'email'
438 c.active = 'email'
439
439
440 test_email = request.POST.get('test_email')
440 test_email = request.POST.get('test_email')
441
441
442 if not test_email:
442 if not test_email:
443 h.flash(_('Please enter email address'), category='error')
443 h.flash(_('Please enter email address'), category='error')
444 return redirect(url('admin_settings_email'))
444 return redirect(url('admin_settings_email'))
445
445
446 email_kwargs = {
446 email_kwargs = {
447 'date': datetime.datetime.now(),
447 'date': datetime.datetime.now(),
448 'user': c.rhodecode_user,
448 'user': c.rhodecode_user,
449 'rhodecode_version': c.rhodecode_version
449 'rhodecode_version': c.rhodecode_version
450 }
450 }
451
451
452 (subject, headers, email_body,
452 (subject, headers, email_body,
453 email_body_plaintext) = EmailNotificationModel().render_email(
453 email_body_plaintext) = EmailNotificationModel().render_email(
454 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
454 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
455
455
456 recipients = [test_email] if test_email else None
456 recipients = [test_email] if test_email else None
457
457
458 run_task(tasks.send_email, recipients, subject,
458 run_task(tasks.send_email, recipients, subject,
459 email_body_plaintext, email_body)
459 email_body_plaintext, email_body)
460
460
461 h.flash(_('Send email task created'), category='success')
461 h.flash(_('Send email task created'), category='success')
462 return redirect(url('admin_settings_email'))
462 return redirect(url('admin_settings_email'))
463
463
464 @HasPermissionAllDecorator('hg.admin')
464 @HasPermissionAllDecorator('hg.admin')
465 def settings_email(self):
465 def settings_email(self):
466 """GET /admin/settings/email: All items in the collection"""
466 """GET /admin/settings/email: All items in the collection"""
467 # url('admin_settings_email')
467 # url('admin_settings_email')
468 c.active = 'email'
468 c.active = 'email'
469 c.rhodecode_ini = rhodecode.CONFIG
469 c.rhodecode_ini = rhodecode.CONFIG
470
470
471 return htmlfill.render(
471 return htmlfill.render(
472 render('admin/settings/settings.html'),
472 render('admin/settings/settings.mako'),
473 defaults=self._form_defaults(),
473 defaults=self._form_defaults(),
474 encoding="UTF-8",
474 encoding="UTF-8",
475 force_defaults=False)
475 force_defaults=False)
476
476
477 @HasPermissionAllDecorator('hg.admin')
477 @HasPermissionAllDecorator('hg.admin')
478 @auth.CSRFRequired()
478 @auth.CSRFRequired()
479 def settings_hooks_update(self):
479 def settings_hooks_update(self):
480 """POST or DELETE /admin/settings/hooks: All items in the collection"""
480 """POST or DELETE /admin/settings/hooks: All items in the collection"""
481 # url('admin_settings_hooks')
481 # url('admin_settings_hooks')
482 c.active = 'hooks'
482 c.active = 'hooks'
483 if c.visual.allow_custom_hooks_settings:
483 if c.visual.allow_custom_hooks_settings:
484 ui_key = request.POST.get('new_hook_ui_key')
484 ui_key = request.POST.get('new_hook_ui_key')
485 ui_value = request.POST.get('new_hook_ui_value')
485 ui_value = request.POST.get('new_hook_ui_value')
486
486
487 hook_id = request.POST.get('hook_id')
487 hook_id = request.POST.get('hook_id')
488 new_hook = False
488 new_hook = False
489
489
490 model = SettingsModel()
490 model = SettingsModel()
491 try:
491 try:
492 if ui_value and ui_key:
492 if ui_value and ui_key:
493 model.create_or_update_hook(ui_key, ui_value)
493 model.create_or_update_hook(ui_key, ui_value)
494 h.flash(_('Added new hook'), category='success')
494 h.flash(_('Added new hook'), category='success')
495 new_hook = True
495 new_hook = True
496 elif hook_id:
496 elif hook_id:
497 RhodeCodeUi.delete(hook_id)
497 RhodeCodeUi.delete(hook_id)
498 Session().commit()
498 Session().commit()
499
499
500 # check for edits
500 # check for edits
501 update = False
501 update = False
502 _d = request.POST.dict_of_lists()
502 _d = request.POST.dict_of_lists()
503 for k, v in zip(_d.get('hook_ui_key', []),
503 for k, v in zip(_d.get('hook_ui_key', []),
504 _d.get('hook_ui_value_new', [])):
504 _d.get('hook_ui_value_new', [])):
505 model.create_or_update_hook(k, v)
505 model.create_or_update_hook(k, v)
506 update = True
506 update = True
507
507
508 if update and not new_hook:
508 if update and not new_hook:
509 h.flash(_('Updated hooks'), category='success')
509 h.flash(_('Updated hooks'), category='success')
510 Session().commit()
510 Session().commit()
511 except Exception:
511 except Exception:
512 log.exception("Exception during hook creation")
512 log.exception("Exception during hook creation")
513 h.flash(_('Error occurred during hook creation'),
513 h.flash(_('Error occurred during hook creation'),
514 category='error')
514 category='error')
515
515
516 return redirect(url('admin_settings_hooks'))
516 return redirect(url('admin_settings_hooks'))
517
517
518 @HasPermissionAllDecorator('hg.admin')
518 @HasPermissionAllDecorator('hg.admin')
519 def settings_hooks(self):
519 def settings_hooks(self):
520 """GET /admin/settings/hooks: All items in the collection"""
520 """GET /admin/settings/hooks: All items in the collection"""
521 # url('admin_settings_hooks')
521 # url('admin_settings_hooks')
522 c.active = 'hooks'
522 c.active = 'hooks'
523
523
524 model = SettingsModel()
524 model = SettingsModel()
525 c.hooks = model.get_builtin_hooks()
525 c.hooks = model.get_builtin_hooks()
526 c.custom_hooks = model.get_custom_hooks()
526 c.custom_hooks = model.get_custom_hooks()
527
527
528 return htmlfill.render(
528 return htmlfill.render(
529 render('admin/settings/settings.html'),
529 render('admin/settings/settings.mako'),
530 defaults=self._form_defaults(),
530 defaults=self._form_defaults(),
531 encoding="UTF-8",
531 encoding="UTF-8",
532 force_defaults=False)
532 force_defaults=False)
533
533
534 @HasPermissionAllDecorator('hg.admin')
534 @HasPermissionAllDecorator('hg.admin')
535 def settings_search(self):
535 def settings_search(self):
536 """GET /admin/settings/search: All items in the collection"""
536 """GET /admin/settings/search: All items in the collection"""
537 # url('admin_settings_search')
537 # url('admin_settings_search')
538 c.active = 'search'
538 c.active = 'search'
539
539
540 from rhodecode.lib.index import searcher_from_config
540 from rhodecode.lib.index import searcher_from_config
541 searcher = searcher_from_config(config)
541 searcher = searcher_from_config(config)
542 c.statistics = searcher.statistics()
542 c.statistics = searcher.statistics()
543
543
544 return render('admin/settings/settings.html')
544 return render('admin/settings/settings.mako')
545
545
546 @HasPermissionAllDecorator('hg.admin')
546 @HasPermissionAllDecorator('hg.admin')
547 def settings_system(self):
547 def settings_system(self):
548 """GET /admin/settings/system: All items in the collection"""
548 """GET /admin/settings/system: All items in the collection"""
549 # url('admin_settings_system')
549 # url('admin_settings_system')
550 snapshot = str2bool(request.GET.get('snapshot'))
550 snapshot = str2bool(request.GET.get('snapshot'))
551 defaults = self._form_defaults()
551 defaults = self._form_defaults()
552
552
553 c.active = 'system'
553 c.active = 'system'
554 c.rhodecode_update_url = defaults.get('rhodecode_update_url')
554 c.rhodecode_update_url = defaults.get('rhodecode_update_url')
555 server_info = ScmModel().get_server_info(request.environ)
555 server_info = ScmModel().get_server_info(request.environ)
556
556
557 for key, val in server_info.iteritems():
557 for key, val in server_info.iteritems():
558 setattr(c, key, val)
558 setattr(c, key, val)
559
559
560 def val(name, subkey='human_value'):
560 def val(name, subkey='human_value'):
561 return server_info[name][subkey]
561 return server_info[name][subkey]
562
562
563 def state(name):
563 def state(name):
564 return server_info[name]['state']
564 return server_info[name]['state']
565
565
566 def val2(name):
566 def val2(name):
567 val = server_info[name]['human_value']
567 val = server_info[name]['human_value']
568 state = server_info[name]['state']
568 state = server_info[name]['state']
569 return val, state
569 return val, state
570
570
571 c.data_items = [
571 c.data_items = [
572 # update info
572 # update info
573 (_('Update info'), h.literal(
573 (_('Update info'), h.literal(
574 '<span class="link" id="check_for_update" >%s.</span>' % (
574 '<span class="link" id="check_for_update" >%s.</span>' % (
575 _('Check for updates')) +
575 _('Check for updates')) +
576 '<br/> <span >%s.</span>' % (_('Note: please make sure this server can access `%s` for the update link to work') % c.rhodecode_update_url)
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 # RhodeCode specific
579 # RhodeCode specific
580 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
580 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
581 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
581 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
582 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
582 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
583 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
583 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
584 ('', '', ''), # spacer
584 ('', '', ''), # spacer
585
585
586 # Database
586 # Database
587 (_('Database'), val('database')['url'], state('database')),
587 (_('Database'), val('database')['url'], state('database')),
588 (_('Database version'), val('database')['version'], state('database')),
588 (_('Database version'), val('database')['version'], state('database')),
589 ('', '', ''), # spacer
589 ('', '', ''), # spacer
590
590
591 # Platform/Python
591 # Platform/Python
592 (_('Platform'), val('platform')['name'], state('platform')),
592 (_('Platform'), val('platform')['name'], state('platform')),
593 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
593 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
594 (_('Python version'), val('python')['version'], state('python')),
594 (_('Python version'), val('python')['version'], state('python')),
595 (_('Python path'), val('python')['executable'], state('python')),
595 (_('Python path'), val('python')['executable'], state('python')),
596 ('', '', ''), # spacer
596 ('', '', ''), # spacer
597
597
598 # Systems stats
598 # Systems stats
599 (_('CPU'), val('cpu'), state('cpu')),
599 (_('CPU'), val('cpu'), state('cpu')),
600 (_('Load'), val('load')['text'], state('load')),
600 (_('Load'), val('load')['text'], state('load')),
601 (_('Memory'), val('memory')['text'], state('memory')),
601 (_('Memory'), val('memory')['text'], state('memory')),
602 (_('Uptime'), val('uptime')['text'], state('uptime')),
602 (_('Uptime'), val('uptime')['text'], state('uptime')),
603 ('', '', ''), # spacer
603 ('', '', ''), # spacer
604
604
605 # Repo storage
605 # Repo storage
606 (_('Storage location'), val('storage')['path'], state('storage')),
606 (_('Storage location'), val('storage')['path'], state('storage')),
607 (_('Storage info'), val('storage')['text'], state('storage')),
607 (_('Storage info'), val('storage')['text'], state('storage')),
608 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
608 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
609
609
610 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
610 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
611 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
611 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
612
612
613 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
613 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
614 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
614 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
615
615
616 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
616 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
617 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
617 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
618
618
619 (_('Search info'), val('search')['text'], state('search')),
619 (_('Search info'), val('search')['text'], state('search')),
620 (_('Search location'), val('search')['location'], state('search')),
620 (_('Search location'), val('search')['location'], state('search')),
621 ('', '', ''), # spacer
621 ('', '', ''), # spacer
622
622
623 # VCS specific
623 # VCS specific
624 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
624 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
625 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
625 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
626 (_('GIT'), val('git'), state('git')),
626 (_('GIT'), val('git'), state('git')),
627 (_('HG'), val('hg'), state('hg')),
627 (_('HG'), val('hg'), state('hg')),
628 (_('SVN'), val('svn'), state('svn')),
628 (_('SVN'), val('svn'), state('svn')),
629
629
630 ]
630 ]
631
631
632 # TODO: marcink, figure out how to allow only selected users to do this
632 # TODO: marcink, figure out how to allow only selected users to do this
633 c.allowed_to_snapshot = c.rhodecode_user.admin
633 c.allowed_to_snapshot = c.rhodecode_user.admin
634
634
635 if snapshot:
635 if snapshot:
636 if c.allowed_to_snapshot:
636 if c.allowed_to_snapshot:
637 c.data_items.pop(0) # remove server info
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 else:
639 else:
640 h.flash('You are not allowed to do this', category='warning')
640 h.flash('You are not allowed to do this', category='warning')
641
641
642 return htmlfill.render(
642 return htmlfill.render(
643 render('admin/settings/settings.html'),
643 render('admin/settings/settings.mako'),
644 defaults=defaults,
644 defaults=defaults,
645 encoding="UTF-8",
645 encoding="UTF-8",
646 force_defaults=False)
646 force_defaults=False)
647
647
648 @staticmethod
648 @staticmethod
649 def get_update_data(update_url):
649 def get_update_data(update_url):
650 """Return the JSON update data."""
650 """Return the JSON update data."""
651 ver = rhodecode.__version__
651 ver = rhodecode.__version__
652 log.debug('Checking for upgrade on `%s` server', update_url)
652 log.debug('Checking for upgrade on `%s` server', update_url)
653 opener = urllib2.build_opener()
653 opener = urllib2.build_opener()
654 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
654 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
655 response = opener.open(update_url)
655 response = opener.open(update_url)
656 response_data = response.read()
656 response_data = response.read()
657 data = json.loads(response_data)
657 data = json.loads(response_data)
658
658
659 return data
659 return data
660
660
661 @HasPermissionAllDecorator('hg.admin')
661 @HasPermissionAllDecorator('hg.admin')
662 def settings_system_update(self):
662 def settings_system_update(self):
663 """GET /admin/settings/system/updates: All items in the collection"""
663 """GET /admin/settings/system/updates: All items in the collection"""
664 # url('admin_settings_system_update')
664 # url('admin_settings_system_update')
665 defaults = self._form_defaults()
665 defaults = self._form_defaults()
666 update_url = defaults.get('rhodecode_update_url', '')
666 update_url = defaults.get('rhodecode_update_url', '')
667
667
668 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
668 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
669 try:
669 try:
670 data = self.get_update_data(update_url)
670 data = self.get_update_data(update_url)
671 except urllib2.URLError as e:
671 except urllib2.URLError as e:
672 log.exception("Exception contacting upgrade server")
672 log.exception("Exception contacting upgrade server")
673 return _err('Failed to contact upgrade server: %r' % e)
673 return _err('Failed to contact upgrade server: %r' % e)
674 except ValueError as e:
674 except ValueError as e:
675 log.exception("Bad data sent from update server")
675 log.exception("Bad data sent from update server")
676 return _err('Bad data sent from update server')
676 return _err('Bad data sent from update server')
677
677
678 latest = data['versions'][0]
678 latest = data['versions'][0]
679
679
680 c.update_url = update_url
680 c.update_url = update_url
681 c.latest_data = latest
681 c.latest_data = latest
682 c.latest_ver = latest['version']
682 c.latest_ver = latest['version']
683 c.cur_ver = rhodecode.__version__
683 c.cur_ver = rhodecode.__version__
684 c.should_upgrade = False
684 c.should_upgrade = False
685
685
686 if (packaging.version.Version(c.latest_ver) >
686 if (packaging.version.Version(c.latest_ver) >
687 packaging.version.Version(c.cur_ver)):
687 packaging.version.Version(c.cur_ver)):
688 c.should_upgrade = True
688 c.should_upgrade = True
689 c.important_notices = latest['general']
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 @HasPermissionAllDecorator('hg.admin')
693 @HasPermissionAllDecorator('hg.admin')
694 def settings_supervisor(self):
694 def settings_supervisor(self):
695 c.rhodecode_ini = rhodecode.CONFIG
695 c.rhodecode_ini = rhodecode.CONFIG
696 c.active = 'supervisor'
696 c.active = 'supervisor'
697
697
698 c.supervisor_procs = OrderedDict([
698 c.supervisor_procs = OrderedDict([
699 (SUPERVISOR_MASTER, {}),
699 (SUPERVISOR_MASTER, {}),
700 ])
700 ])
701
701
702 c.log_size = 10240
702 c.log_size = 10240
703 supervisor = SupervisorModel()
703 supervisor = SupervisorModel()
704
704
705 _connection = supervisor.get_connection(
705 _connection = supervisor.get_connection(
706 c.rhodecode_ini.get('supervisor.uri'))
706 c.rhodecode_ini.get('supervisor.uri'))
707 c.connection_error = None
707 c.connection_error = None
708 try:
708 try:
709 _connection.supervisor.getAllProcessInfo()
709 _connection.supervisor.getAllProcessInfo()
710 except Exception as e:
710 except Exception as e:
711 c.connection_error = str(e)
711 c.connection_error = str(e)
712 log.exception("Exception reading supervisor data")
712 log.exception("Exception reading supervisor data")
713 return render('admin/settings/settings.html')
713 return render('admin/settings/settings.mako')
714
714
715 groupid = c.rhodecode_ini.get('supervisor.group_id')
715 groupid = c.rhodecode_ini.get('supervisor.group_id')
716
716
717 # feed our group processes to the main
717 # feed our group processes to the main
718 for proc in supervisor.get_group_processes(_connection, groupid):
718 for proc in supervisor.get_group_processes(_connection, groupid):
719 c.supervisor_procs[proc['name']] = {}
719 c.supervisor_procs[proc['name']] = {}
720
720
721 for k in c.supervisor_procs.keys():
721 for k in c.supervisor_procs.keys():
722 try:
722 try:
723 # master process info
723 # master process info
724 if k == SUPERVISOR_MASTER:
724 if k == SUPERVISOR_MASTER:
725 _data = supervisor.get_master_state(_connection)
725 _data = supervisor.get_master_state(_connection)
726 _data['name'] = 'supervisor master'
726 _data['name'] = 'supervisor master'
727 _data['description'] = 'pid %s, id: %s, ver: %s' % (
727 _data['description'] = 'pid %s, id: %s, ver: %s' % (
728 _data['pid'], _data['id'], _data['ver'])
728 _data['pid'], _data['id'], _data['ver'])
729 c.supervisor_procs[k] = _data
729 c.supervisor_procs[k] = _data
730 else:
730 else:
731 procid = groupid + ":" + k
731 procid = groupid + ":" + k
732 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
732 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
733 except Exception as e:
733 except Exception as e:
734 log.exception("Exception reading supervisor data")
734 log.exception("Exception reading supervisor data")
735 c.supervisor_procs[k] = {'_rhodecode_error': str(e)}
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 @HasPermissionAllDecorator('hg.admin')
739 @HasPermissionAllDecorator('hg.admin')
740 def settings_supervisor_log(self, procid):
740 def settings_supervisor_log(self, procid):
741 import rhodecode
741 import rhodecode
742 c.rhodecode_ini = rhodecode.CONFIG
742 c.rhodecode_ini = rhodecode.CONFIG
743 c.active = 'supervisor_tail'
743 c.active = 'supervisor_tail'
744
744
745 supervisor = SupervisorModel()
745 supervisor = SupervisorModel()
746 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
746 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
747 groupid = c.rhodecode_ini.get('supervisor.group_id')
747 groupid = c.rhodecode_ini.get('supervisor.group_id')
748 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
748 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
749
749
750 c.log_size = 10240
750 c.log_size = 10240
751 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
751 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
752 c.log = supervisor.read_process_log(_connection, procid, offset, 0)
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 @HasPermissionAllDecorator('hg.admin')
756 @HasPermissionAllDecorator('hg.admin')
757 @auth.CSRFRequired()
757 @auth.CSRFRequired()
758 def settings_labs_update(self):
758 def settings_labs_update(self):
759 """POST /admin/settings/labs: All items in the collection"""
759 """POST /admin/settings/labs: All items in the collection"""
760 # url('admin_settings/labs', method={'POST'})
760 # url('admin_settings/labs', method={'POST'})
761 c.active = 'labs'
761 c.active = 'labs'
762
762
763 application_form = LabsSettingsForm()()
763 application_form = LabsSettingsForm()()
764 try:
764 try:
765 form_result = application_form.to_python(dict(request.POST))
765 form_result = application_form.to_python(dict(request.POST))
766 except formencode.Invalid as errors:
766 except formencode.Invalid as errors:
767 h.flash(
767 h.flash(
768 _('Some form inputs contain invalid data.'),
768 _('Some form inputs contain invalid data.'),
769 category='error')
769 category='error')
770 return htmlfill.render(
770 return htmlfill.render(
771 render('admin/settings/settings.html'),
771 render('admin/settings/settings.mako'),
772 defaults=errors.value,
772 defaults=errors.value,
773 errors=errors.error_dict or {},
773 errors=errors.error_dict or {},
774 prefix_error=False,
774 prefix_error=False,
775 encoding='UTF-8',
775 encoding='UTF-8',
776 force_defaults=False
776 force_defaults=False
777 )
777 )
778
778
779 try:
779 try:
780 session = Session()
780 session = Session()
781 for setting in _LAB_SETTINGS:
781 for setting in _LAB_SETTINGS:
782 setting_name = setting.key[len('rhodecode_'):]
782 setting_name = setting.key[len('rhodecode_'):]
783 sett = SettingsModel().create_or_update_setting(
783 sett = SettingsModel().create_or_update_setting(
784 setting_name, form_result[setting.key], setting.type)
784 setting_name, form_result[setting.key], setting.type)
785 session.add(sett)
785 session.add(sett)
786
786
787 except Exception:
787 except Exception:
788 log.exception('Exception while updating lab settings')
788 log.exception('Exception while updating lab settings')
789 h.flash(_('Error occurred during updating labs settings'),
789 h.flash(_('Error occurred during updating labs settings'),
790 category='error')
790 category='error')
791 else:
791 else:
792 Session().commit()
792 Session().commit()
793 SettingsModel().invalidate_settings_cache()
793 SettingsModel().invalidate_settings_cache()
794 h.flash(_('Updated Labs settings'), category='success')
794 h.flash(_('Updated Labs settings'), category='success')
795 return redirect(url('admin_settings_labs'))
795 return redirect(url('admin_settings_labs'))
796
796
797 return htmlfill.render(
797 return htmlfill.render(
798 render('admin/settings/settings.html'),
798 render('admin/settings/settings.mako'),
799 defaults=self._form_defaults(),
799 defaults=self._form_defaults(),
800 encoding='UTF-8',
800 encoding='UTF-8',
801 force_defaults=False)
801 force_defaults=False)
802
802
803 @HasPermissionAllDecorator('hg.admin')
803 @HasPermissionAllDecorator('hg.admin')
804 def settings_labs(self):
804 def settings_labs(self):
805 """GET /admin/settings/labs: All items in the collection"""
805 """GET /admin/settings/labs: All items in the collection"""
806 # url('admin_settings_labs')
806 # url('admin_settings_labs')
807 if not c.labs_active:
807 if not c.labs_active:
808 redirect(url('admin_settings'))
808 redirect(url('admin_settings'))
809
809
810 c.active = 'labs'
810 c.active = 'labs'
811 c.lab_settings = _LAB_SETTINGS
811 c.lab_settings = _LAB_SETTINGS
812
812
813 return htmlfill.render(
813 return htmlfill.render(
814 render('admin/settings/settings.html'),
814 render('admin/settings/settings.mako'),
815 defaults=self._form_defaults(),
815 defaults=self._form_defaults(),
816 encoding='UTF-8',
816 encoding='UTF-8',
817 force_defaults=False)
817 force_defaults=False)
818
818
819 def _form_defaults(self):
819 def _form_defaults(self):
820 defaults = SettingsModel().get_all_settings()
820 defaults = SettingsModel().get_all_settings()
821 defaults.update(self._get_hg_ui_settings())
821 defaults.update(self._get_hg_ui_settings())
822 defaults.update({
822 defaults.update({
823 'new_svn_branch': '',
823 'new_svn_branch': '',
824 'new_svn_tag': '',
824 'new_svn_tag': '',
825 })
825 })
826 return defaults
826 return defaults
827
827
828
828
829 # :param key: name of the setting including the 'rhodecode_' prefix
829 # :param key: name of the setting including the 'rhodecode_' prefix
830 # :param type: the RhodeCodeSetting type to use.
830 # :param type: the RhodeCodeSetting type to use.
831 # :param group: the i18ned group in which we should dispaly this setting
831 # :param group: the i18ned group in which we should dispaly this setting
832 # :param label: the i18ned label we should display for this setting
832 # :param label: the i18ned label we should display for this setting
833 # :param help: the i18ned help we should dispaly for this setting
833 # :param help: the i18ned help we should dispaly for this setting
834 LabSetting = collections.namedtuple(
834 LabSetting = collections.namedtuple(
835 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
835 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
836
836
837
837
838 # This list has to be kept in sync with the form
838 # This list has to be kept in sync with the form
839 # rhodecode.model.forms.LabsSettingsForm.
839 # rhodecode.model.forms.LabsSettingsForm.
840 _LAB_SETTINGS = [
840 _LAB_SETTINGS = [
841
841
842 ]
842 ]
@@ -1,487 +1,487 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 User Groups crud controller for pylons
22 User Groups crud controller for pylons
23 """
23 """
24
24
25 import logging
25 import logging
26 import formencode
26 import formencode
27
27
28 import peppercorn
28 import peppercorn
29 from formencode import htmlfill
29 from formencode import htmlfill
30 from pylons import request, tmpl_context as c, url, config
30 from pylons import request, tmpl_context as c, url, config
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 from sqlalchemy.orm import joinedload
34 from sqlalchemy.orm import joinedload
35
35
36 from rhodecode.lib import auth
36 from rhodecode.lib import auth
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.exceptions import UserGroupAssignedException,\
38 from rhodecode.lib.exceptions import UserGroupAssignedException,\
39 RepoGroupAssignmentError
39 RepoGroupAssignmentError
40 from rhodecode.lib.utils import jsonify, action_logger
40 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
41 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
43 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
44 HasPermissionAnyDecorator, XHRRequired)
44 HasPermissionAnyDecorator, XHRRequired)
45 from rhodecode.lib.base import BaseController, render
45 from rhodecode.lib.base import BaseController, render
46 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.permission import PermissionModel
47 from rhodecode.model.scm import UserGroupList
47 from rhodecode.model.scm import UserGroupList
48 from rhodecode.model.user_group import UserGroupModel
48 from rhodecode.model.user_group import UserGroupModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
50 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
51 from rhodecode.model.forms import (
51 from rhodecode.model.forms import (
52 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
52 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
53 UserPermissionsForm)
53 UserPermissionsForm)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.lib.utils import action_logger
55 from rhodecode.lib.utils import action_logger
56 from rhodecode.lib.ext_json import json
56 from rhodecode.lib.ext_json import json
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 class UserGroupsController(BaseController):
61 class UserGroupsController(BaseController):
62 """REST Controller styled on the Atom Publishing Protocol"""
62 """REST Controller styled on the Atom Publishing Protocol"""
63
63
64 @LoginRequired()
64 @LoginRequired()
65 def __before__(self):
65 def __before__(self):
66 super(UserGroupsController, self).__before__()
66 super(UserGroupsController, self).__before__()
67 c.available_permissions = config['available_permissions']
67 c.available_permissions = config['available_permissions']
68 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
68 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
69
69
70 def __load_data(self, user_group_id):
70 def __load_data(self, user_group_id):
71 c.group_members_obj = [x.user for x in c.user_group.members]
71 c.group_members_obj = [x.user for x in c.user_group.members]
72 c.group_members_obj.sort(key=lambda u: u.username.lower())
72 c.group_members_obj.sort(key=lambda u: u.username.lower())
73 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
73 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
74
74
75 def __load_defaults(self, user_group_id):
75 def __load_defaults(self, user_group_id):
76 """
76 """
77 Load defaults settings for edit, and update
77 Load defaults settings for edit, and update
78
78
79 :param user_group_id:
79 :param user_group_id:
80 """
80 """
81 user_group = UserGroup.get_or_404(user_group_id)
81 user_group = UserGroup.get_or_404(user_group_id)
82 data = user_group.get_dict()
82 data = user_group.get_dict()
83 # fill owner
83 # fill owner
84 if user_group.user:
84 if user_group.user:
85 data.update({'user': user_group.user.username})
85 data.update({'user': user_group.user.username})
86 else:
86 else:
87 replacement_user = User.get_first_super_admin().username
87 replacement_user = User.get_first_super_admin().username
88 data.update({'user': replacement_user})
88 data.update({'user': replacement_user})
89 return data
89 return data
90
90
91 def _revoke_perms_on_yourself(self, form_result):
91 def _revoke_perms_on_yourself(self, form_result):
92 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
92 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
93 form_result['perm_updates'])
93 form_result['perm_updates'])
94 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
94 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
95 form_result['perm_additions'])
95 form_result['perm_additions'])
96 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
96 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
97 form_result['perm_deletions'])
97 form_result['perm_deletions'])
98 admin_perm = 'usergroup.admin'
98 admin_perm = 'usergroup.admin'
99 if _updates and _updates[0][1] != admin_perm or \
99 if _updates and _updates[0][1] != admin_perm or \
100 _additions and _additions[0][1] != admin_perm or \
100 _additions and _additions[0][1] != admin_perm or \
101 _deletions and _deletions[0][1] != admin_perm:
101 _deletions and _deletions[0][1] != admin_perm:
102 return True
102 return True
103 return False
103 return False
104
104
105 # permission check inside
105 # permission check inside
106 @NotAnonymous()
106 @NotAnonymous()
107 def index(self):
107 def index(self):
108 """GET /users_groups: All items in the collection"""
108 """GET /users_groups: All items in the collection"""
109 # url('users_groups')
109 # url('users_groups')
110
110
111 from rhodecode.lib.utils import PartialRenderer
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 def user_group_name(user_group_id, user_group_name):
114 def user_group_name(user_group_id, user_group_name):
115 return _render("user_group_name", user_group_id, user_group_name)
115 return _render("user_group_name", user_group_id, user_group_name)
116
116
117 def user_group_actions(user_group_id, user_group_name):
117 def user_group_actions(user_group_id, user_group_name):
118 return _render("user_group_actions", user_group_id, user_group_name)
118 return _render("user_group_actions", user_group_id, user_group_name)
119
119
120 ## json generate
120 ## json generate
121 group_iter = UserGroupList(UserGroup.query().all(),
121 group_iter = UserGroupList(UserGroup.query().all(),
122 perm_set=['usergroup.admin'])
122 perm_set=['usergroup.admin'])
123
123
124 user_groups_data = []
124 user_groups_data = []
125 for user_gr in group_iter:
125 for user_gr in group_iter:
126 user_groups_data.append({
126 user_groups_data.append({
127 "group_name": user_group_name(
127 "group_name": user_group_name(
128 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
128 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
129 "group_name_raw": user_gr.users_group_name,
129 "group_name_raw": user_gr.users_group_name,
130 "desc": h.escape(user_gr.user_group_description),
130 "desc": h.escape(user_gr.user_group_description),
131 "members": len(user_gr.members),
131 "members": len(user_gr.members),
132 "active": h.bool2icon(user_gr.users_group_active),
132 "active": h.bool2icon(user_gr.users_group_active),
133 "owner": h.escape(h.link_to_user(user_gr.user.username)),
133 "owner": h.escape(h.link_to_user(user_gr.user.username)),
134 "action": user_group_actions(
134 "action": user_group_actions(
135 user_gr.users_group_id, user_gr.users_group_name)
135 user_gr.users_group_id, user_gr.users_group_name)
136 })
136 })
137
137
138 c.data = json.dumps(user_groups_data)
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 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
141 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
142 @auth.CSRFRequired()
142 @auth.CSRFRequired()
143 def create(self):
143 def create(self):
144 """POST /users_groups: Create a new item"""
144 """POST /users_groups: Create a new item"""
145 # url('users_groups')
145 # url('users_groups')
146
146
147 users_group_form = UserGroupForm()()
147 users_group_form = UserGroupForm()()
148 try:
148 try:
149 form_result = users_group_form.to_python(dict(request.POST))
149 form_result = users_group_form.to_python(dict(request.POST))
150 user_group = UserGroupModel().create(
150 user_group = UserGroupModel().create(
151 name=form_result['users_group_name'],
151 name=form_result['users_group_name'],
152 description=form_result['user_group_description'],
152 description=form_result['user_group_description'],
153 owner=c.rhodecode_user.user_id,
153 owner=c.rhodecode_user.user_id,
154 active=form_result['users_group_active'])
154 active=form_result['users_group_active'])
155 Session().flush()
155 Session().flush()
156
156
157 user_group_name = form_result['users_group_name']
157 user_group_name = form_result['users_group_name']
158 action_logger(c.rhodecode_user,
158 action_logger(c.rhodecode_user,
159 'admin_created_users_group:%s' % user_group_name,
159 'admin_created_users_group:%s' % user_group_name,
160 None, self.ip_addr, self.sa)
160 None, self.ip_addr, self.sa)
161 user_group_link = h.link_to(h.escape(user_group_name),
161 user_group_link = h.link_to(h.escape(user_group_name),
162 url('edit_users_group',
162 url('edit_users_group',
163 user_group_id=user_group.users_group_id))
163 user_group_id=user_group.users_group_id))
164 h.flash(h.literal(_('Created user group %(user_group_link)s')
164 h.flash(h.literal(_('Created user group %(user_group_link)s')
165 % {'user_group_link': user_group_link}),
165 % {'user_group_link': user_group_link}),
166 category='success')
166 category='success')
167 Session().commit()
167 Session().commit()
168 except formencode.Invalid as errors:
168 except formencode.Invalid as errors:
169 return htmlfill.render(
169 return htmlfill.render(
170 render('admin/user_groups/user_group_add.html'),
170 render('admin/user_groups/user_group_add.mako'),
171 defaults=errors.value,
171 defaults=errors.value,
172 errors=errors.error_dict or {},
172 errors=errors.error_dict or {},
173 prefix_error=False,
173 prefix_error=False,
174 encoding="UTF-8",
174 encoding="UTF-8",
175 force_defaults=False)
175 force_defaults=False)
176 except Exception:
176 except Exception:
177 log.exception("Exception creating user group")
177 log.exception("Exception creating user group")
178 h.flash(_('Error occurred during creation of user group %s') \
178 h.flash(_('Error occurred during creation of user group %s') \
179 % request.POST.get('users_group_name'), category='error')
179 % request.POST.get('users_group_name'), category='error')
180
180
181 return redirect(
181 return redirect(
182 url('edit_users_group', user_group_id=user_group.users_group_id))
182 url('edit_users_group', user_group_id=user_group.users_group_id))
183
183
184 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
184 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
185 def new(self):
185 def new(self):
186 """GET /user_groups/new: Form to create a new item"""
186 """GET /user_groups/new: Form to create a new item"""
187 # url('new_users_group')
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 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
190 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
191 @auth.CSRFRequired()
191 @auth.CSRFRequired()
192 def update(self, user_group_id):
192 def update(self, user_group_id):
193 """PUT /user_groups/user_group_id: Update an existing item"""
193 """PUT /user_groups/user_group_id: Update an existing item"""
194 # Forms posted to this method should contain a hidden field:
194 # Forms posted to this method should contain a hidden field:
195 # <input type="hidden" name="_method" value="PUT" />
195 # <input type="hidden" name="_method" value="PUT" />
196 # Or using helpers:
196 # Or using helpers:
197 # h.form(url('users_group', user_group_id=ID),
197 # h.form(url('users_group', user_group_id=ID),
198 # method='put')
198 # method='put')
199 # url('users_group', user_group_id=ID)
199 # url('users_group', user_group_id=ID)
200
200
201 user_group_id = safe_int(user_group_id)
201 user_group_id = safe_int(user_group_id)
202 c.user_group = UserGroup.get_or_404(user_group_id)
202 c.user_group = UserGroup.get_or_404(user_group_id)
203 c.active = 'settings'
203 c.active = 'settings'
204 self.__load_data(user_group_id)
204 self.__load_data(user_group_id)
205
205
206 users_group_form = UserGroupForm(
206 users_group_form = UserGroupForm(
207 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
207 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
208
208
209 try:
209 try:
210 form_result = users_group_form.to_python(request.POST)
210 form_result = users_group_form.to_python(request.POST)
211 pstruct = peppercorn.parse(request.POST.items())
211 pstruct = peppercorn.parse(request.POST.items())
212 form_result['users_group_members'] = pstruct['user_group_members']
212 form_result['users_group_members'] = pstruct['user_group_members']
213
213
214 UserGroupModel().update(c.user_group, form_result)
214 UserGroupModel().update(c.user_group, form_result)
215 updated_user_group = form_result['users_group_name']
215 updated_user_group = form_result['users_group_name']
216 action_logger(c.rhodecode_user,
216 action_logger(c.rhodecode_user,
217 'admin_updated_users_group:%s' % updated_user_group,
217 'admin_updated_users_group:%s' % updated_user_group,
218 None, self.ip_addr, self.sa)
218 None, self.ip_addr, self.sa)
219 h.flash(_('Updated user group %s') % updated_user_group,
219 h.flash(_('Updated user group %s') % updated_user_group,
220 category='success')
220 category='success')
221 Session().commit()
221 Session().commit()
222 except formencode.Invalid as errors:
222 except formencode.Invalid as errors:
223 defaults = errors.value
223 defaults = errors.value
224 e = errors.error_dict or {}
224 e = errors.error_dict or {}
225
225
226 return htmlfill.render(
226 return htmlfill.render(
227 render('admin/user_groups/user_group_edit.html'),
227 render('admin/user_groups/user_group_edit.mako'),
228 defaults=defaults,
228 defaults=defaults,
229 errors=e,
229 errors=e,
230 prefix_error=False,
230 prefix_error=False,
231 encoding="UTF-8",
231 encoding="UTF-8",
232 force_defaults=False)
232 force_defaults=False)
233 except Exception:
233 except Exception:
234 log.exception("Exception during update of user group")
234 log.exception("Exception during update of user group")
235 h.flash(_('Error occurred during update of user group %s')
235 h.flash(_('Error occurred during update of user group %s')
236 % request.POST.get('users_group_name'), category='error')
236 % request.POST.get('users_group_name'), category='error')
237
237
238 return redirect(url('edit_users_group', user_group_id=user_group_id))
238 return redirect(url('edit_users_group', user_group_id=user_group_id))
239
239
240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
241 @auth.CSRFRequired()
241 @auth.CSRFRequired()
242 def delete(self, user_group_id):
242 def delete(self, user_group_id):
243 """DELETE /user_groups/user_group_id: Delete an existing item"""
243 """DELETE /user_groups/user_group_id: Delete an existing item"""
244 # Forms posted to this method should contain a hidden field:
244 # Forms posted to this method should contain a hidden field:
245 # <input type="hidden" name="_method" value="DELETE" />
245 # <input type="hidden" name="_method" value="DELETE" />
246 # Or using helpers:
246 # Or using helpers:
247 # h.form(url('users_group', user_group_id=ID),
247 # h.form(url('users_group', user_group_id=ID),
248 # method='delete')
248 # method='delete')
249 # url('users_group', user_group_id=ID)
249 # url('users_group', user_group_id=ID)
250 user_group_id = safe_int(user_group_id)
250 user_group_id = safe_int(user_group_id)
251 c.user_group = UserGroup.get_or_404(user_group_id)
251 c.user_group = UserGroup.get_or_404(user_group_id)
252 force = str2bool(request.POST.get('force'))
252 force = str2bool(request.POST.get('force'))
253
253
254 try:
254 try:
255 UserGroupModel().delete(c.user_group, force=force)
255 UserGroupModel().delete(c.user_group, force=force)
256 Session().commit()
256 Session().commit()
257 h.flash(_('Successfully deleted user group'), category='success')
257 h.flash(_('Successfully deleted user group'), category='success')
258 except UserGroupAssignedException as e:
258 except UserGroupAssignedException as e:
259 h.flash(str(e), category='error')
259 h.flash(str(e), category='error')
260 except Exception:
260 except Exception:
261 log.exception("Exception during deletion of user group")
261 log.exception("Exception during deletion of user group")
262 h.flash(_('An error occurred during deletion of user group'),
262 h.flash(_('An error occurred during deletion of user group'),
263 category='error')
263 category='error')
264 return redirect(url('users_groups'))
264 return redirect(url('users_groups'))
265
265
266 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
266 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
267 def edit(self, user_group_id):
267 def edit(self, user_group_id):
268 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
268 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
269 # url('edit_users_group', user_group_id=ID)
269 # url('edit_users_group', user_group_id=ID)
270
270
271 user_group_id = safe_int(user_group_id)
271 user_group_id = safe_int(user_group_id)
272 c.user_group = UserGroup.get_or_404(user_group_id)
272 c.user_group = UserGroup.get_or_404(user_group_id)
273 c.active = 'settings'
273 c.active = 'settings'
274 self.__load_data(user_group_id)
274 self.__load_data(user_group_id)
275
275
276 defaults = self.__load_defaults(user_group_id)
276 defaults = self.__load_defaults(user_group_id)
277
277
278 return htmlfill.render(
278 return htmlfill.render(
279 render('admin/user_groups/user_group_edit.html'),
279 render('admin/user_groups/user_group_edit.mako'),
280 defaults=defaults,
280 defaults=defaults,
281 encoding="UTF-8",
281 encoding="UTF-8",
282 force_defaults=False
282 force_defaults=False
283 )
283 )
284
284
285 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
285 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
286 def edit_perms(self, user_group_id):
286 def edit_perms(self, user_group_id):
287 user_group_id = safe_int(user_group_id)
287 user_group_id = safe_int(user_group_id)
288 c.user_group = UserGroup.get_or_404(user_group_id)
288 c.user_group = UserGroup.get_or_404(user_group_id)
289 c.active = 'perms'
289 c.active = 'perms'
290
290
291 defaults = {}
291 defaults = {}
292 # fill user group users
292 # fill user group users
293 for p in c.user_group.user_user_group_to_perm:
293 for p in c.user_group.user_user_group_to_perm:
294 defaults.update({'u_perm_%s' % p.user.user_id:
294 defaults.update({'u_perm_%s' % p.user.user_id:
295 p.permission.permission_name})
295 p.permission.permission_name})
296
296
297 for p in c.user_group.user_group_user_group_to_perm:
297 for p in c.user_group.user_group_user_group_to_perm:
298 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
298 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
299 p.permission.permission_name})
299 p.permission.permission_name})
300
300
301 return htmlfill.render(
301 return htmlfill.render(
302 render('admin/user_groups/user_group_edit.html'),
302 render('admin/user_groups/user_group_edit.mako'),
303 defaults=defaults,
303 defaults=defaults,
304 encoding="UTF-8",
304 encoding="UTF-8",
305 force_defaults=False
305 force_defaults=False
306 )
306 )
307
307
308 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
308 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
309 @auth.CSRFRequired()
309 @auth.CSRFRequired()
310 def update_perms(self, user_group_id):
310 def update_perms(self, user_group_id):
311 """
311 """
312 grant permission for given usergroup
312 grant permission for given usergroup
313
313
314 :param user_group_id:
314 :param user_group_id:
315 """
315 """
316 user_group_id = safe_int(user_group_id)
316 user_group_id = safe_int(user_group_id)
317 c.user_group = UserGroup.get_or_404(user_group_id)
317 c.user_group = UserGroup.get_or_404(user_group_id)
318 form = UserGroupPermsForm()().to_python(request.POST)
318 form = UserGroupPermsForm()().to_python(request.POST)
319
319
320 if not c.rhodecode_user.is_admin:
320 if not c.rhodecode_user.is_admin:
321 if self._revoke_perms_on_yourself(form):
321 if self._revoke_perms_on_yourself(form):
322 msg = _('Cannot change permission for yourself as admin')
322 msg = _('Cannot change permission for yourself as admin')
323 h.flash(msg, category='warning')
323 h.flash(msg, category='warning')
324 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
324 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
325
325
326 try:
326 try:
327 UserGroupModel().update_permissions(user_group_id,
327 UserGroupModel().update_permissions(user_group_id,
328 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
328 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
329 except RepoGroupAssignmentError:
329 except RepoGroupAssignmentError:
330 h.flash(_('Target group cannot be the same'), category='error')
330 h.flash(_('Target group cannot be the same'), category='error')
331 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
331 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
332 #TODO: implement this
332 #TODO: implement this
333 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
333 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
334 # repo_name, self.ip_addr, self.sa)
334 # repo_name, self.ip_addr, self.sa)
335 Session().commit()
335 Session().commit()
336 h.flash(_('User Group permissions updated'), category='success')
336 h.flash(_('User Group permissions updated'), category='success')
337 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
337 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
338
338
339 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
339 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
340 def edit_perms_summary(self, user_group_id):
340 def edit_perms_summary(self, user_group_id):
341 user_group_id = safe_int(user_group_id)
341 user_group_id = safe_int(user_group_id)
342 c.user_group = UserGroup.get_or_404(user_group_id)
342 c.user_group = UserGroup.get_or_404(user_group_id)
343 c.active = 'perms_summary'
343 c.active = 'perms_summary'
344 permissions = {
344 permissions = {
345 'repositories': {},
345 'repositories': {},
346 'repositories_groups': {},
346 'repositories_groups': {},
347 }
347 }
348 ugroup_repo_perms = UserGroupRepoToPerm.query()\
348 ugroup_repo_perms = UserGroupRepoToPerm.query()\
349 .options(joinedload(UserGroupRepoToPerm.permission))\
349 .options(joinedload(UserGroupRepoToPerm.permission))\
350 .options(joinedload(UserGroupRepoToPerm.repository))\
350 .options(joinedload(UserGroupRepoToPerm.repository))\
351 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
351 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
352 .all()
352 .all()
353
353
354 for gr in ugroup_repo_perms:
354 for gr in ugroup_repo_perms:
355 permissions['repositories'][gr.repository.repo_name] \
355 permissions['repositories'][gr.repository.repo_name] \
356 = gr.permission.permission_name
356 = gr.permission.permission_name
357
357
358 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
358 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
359 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
359 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
360 .options(joinedload(UserGroupRepoGroupToPerm.group))\
360 .options(joinedload(UserGroupRepoGroupToPerm.group))\
361 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
361 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
362 .all()
362 .all()
363
363
364 for gr in ugroup_group_perms:
364 for gr in ugroup_group_perms:
365 permissions['repositories_groups'][gr.group.group_name] \
365 permissions['repositories_groups'][gr.group.group_name] \
366 = gr.permission.permission_name
366 = gr.permission.permission_name
367 c.permissions = permissions
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 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
370 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
371 def edit_global_perms(self, user_group_id):
371 def edit_global_perms(self, user_group_id):
372 user_group_id = safe_int(user_group_id)
372 user_group_id = safe_int(user_group_id)
373 c.user_group = UserGroup.get_or_404(user_group_id)
373 c.user_group = UserGroup.get_or_404(user_group_id)
374 c.active = 'global_perms'
374 c.active = 'global_perms'
375
375
376 c.default_user = User.get_default_user()
376 c.default_user = User.get_default_user()
377 defaults = c.user_group.get_dict()
377 defaults = c.user_group.get_dict()
378 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
378 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
379 defaults.update(c.user_group.get_default_perms())
379 defaults.update(c.user_group.get_default_perms())
380
380
381 return htmlfill.render(
381 return htmlfill.render(
382 render('admin/user_groups/user_group_edit.html'),
382 render('admin/user_groups/user_group_edit.mako'),
383 defaults=defaults,
383 defaults=defaults,
384 encoding="UTF-8",
384 encoding="UTF-8",
385 force_defaults=False
385 force_defaults=False
386 )
386 )
387
387
388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
389 @auth.CSRFRequired()
389 @auth.CSRFRequired()
390 def update_global_perms(self, user_group_id):
390 def update_global_perms(self, user_group_id):
391 """PUT /users_perm/user_group_id: Update an existing item"""
391 """PUT /users_perm/user_group_id: Update an existing item"""
392 # url('users_group_perm', user_group_id=ID, method='put')
392 # url('users_group_perm', user_group_id=ID, method='put')
393 user_group_id = safe_int(user_group_id)
393 user_group_id = safe_int(user_group_id)
394 user_group = UserGroup.get_or_404(user_group_id)
394 user_group = UserGroup.get_or_404(user_group_id)
395 c.active = 'global_perms'
395 c.active = 'global_perms'
396
396
397 try:
397 try:
398 # first stage that verifies the checkbox
398 # first stage that verifies the checkbox
399 _form = UserIndividualPermissionsForm()
399 _form = UserIndividualPermissionsForm()
400 form_result = _form.to_python(dict(request.POST))
400 form_result = _form.to_python(dict(request.POST))
401 inherit_perms = form_result['inherit_default_permissions']
401 inherit_perms = form_result['inherit_default_permissions']
402 user_group.inherit_default_permissions = inherit_perms
402 user_group.inherit_default_permissions = inherit_perms
403 Session().add(user_group)
403 Session().add(user_group)
404
404
405 if not inherit_perms:
405 if not inherit_perms:
406 # only update the individual ones if we un check the flag
406 # only update the individual ones if we un check the flag
407 _form = UserPermissionsForm(
407 _form = UserPermissionsForm(
408 [x[0] for x in c.repo_create_choices],
408 [x[0] for x in c.repo_create_choices],
409 [x[0] for x in c.repo_create_on_write_choices],
409 [x[0] for x in c.repo_create_on_write_choices],
410 [x[0] for x in c.repo_group_create_choices],
410 [x[0] for x in c.repo_group_create_choices],
411 [x[0] for x in c.user_group_create_choices],
411 [x[0] for x in c.user_group_create_choices],
412 [x[0] for x in c.fork_choices],
412 [x[0] for x in c.fork_choices],
413 [x[0] for x in c.inherit_default_permission_choices])()
413 [x[0] for x in c.inherit_default_permission_choices])()
414
414
415 form_result = _form.to_python(dict(request.POST))
415 form_result = _form.to_python(dict(request.POST))
416 form_result.update({'perm_user_group_id': user_group.users_group_id})
416 form_result.update({'perm_user_group_id': user_group.users_group_id})
417
417
418 PermissionModel().update_user_group_permissions(form_result)
418 PermissionModel().update_user_group_permissions(form_result)
419
419
420 Session().commit()
420 Session().commit()
421 h.flash(_('User Group global permissions updated successfully'),
421 h.flash(_('User Group global permissions updated successfully'),
422 category='success')
422 category='success')
423
423
424 except formencode.Invalid as errors:
424 except formencode.Invalid as errors:
425 defaults = errors.value
425 defaults = errors.value
426 c.user_group = user_group
426 c.user_group = user_group
427 return htmlfill.render(
427 return htmlfill.render(
428 render('admin/user_groups/user_group_edit.html'),
428 render('admin/user_groups/user_group_edit.mako'),
429 defaults=defaults,
429 defaults=defaults,
430 errors=errors.error_dict or {},
430 errors=errors.error_dict or {},
431 prefix_error=False,
431 prefix_error=False,
432 encoding="UTF-8",
432 encoding="UTF-8",
433 force_defaults=False)
433 force_defaults=False)
434
434
435 except Exception:
435 except Exception:
436 log.exception("Exception during permissions saving")
436 log.exception("Exception during permissions saving")
437 h.flash(_('An error occurred during permissions saving'),
437 h.flash(_('An error occurred during permissions saving'),
438 category='error')
438 category='error')
439
439
440 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
440 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
441
441
442 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
442 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
443 def edit_advanced(self, user_group_id):
443 def edit_advanced(self, user_group_id):
444 user_group_id = safe_int(user_group_id)
444 user_group_id = safe_int(user_group_id)
445 c.user_group = UserGroup.get_or_404(user_group_id)
445 c.user_group = UserGroup.get_or_404(user_group_id)
446 c.active = 'advanced'
446 c.active = 'advanced'
447 c.group_members_obj = sorted(
447 c.group_members_obj = sorted(
448 (x.user for x in c.user_group.members),
448 (x.user for x in c.user_group.members),
449 key=lambda u: u.username.lower())
449 key=lambda u: u.username.lower())
450
450
451 c.group_to_repos = sorted(
451 c.group_to_repos = sorted(
452 (x.repository for x in c.user_group.users_group_repo_to_perm),
452 (x.repository for x in c.user_group.users_group_repo_to_perm),
453 key=lambda u: u.repo_name.lower())
453 key=lambda u: u.repo_name.lower())
454
454
455 c.group_to_repo_groups = sorted(
455 c.group_to_repo_groups = sorted(
456 (x.group for x in c.user_group.users_group_repo_group_to_perm),
456 (x.group for x in c.user_group.users_group_repo_group_to_perm),
457 key=lambda u: u.group_name.lower())
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 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
461 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
462 @XHRRequired()
462 @XHRRequired()
463 @jsonify
463 @jsonify
464 def user_group_members(self, user_group_id):
464 def user_group_members(self, user_group_id):
465 user_group_id = safe_int(user_group_id)
465 user_group_id = safe_int(user_group_id)
466 user_group = UserGroup.get_or_404(user_group_id)
466 user_group = UserGroup.get_or_404(user_group_id)
467 group_members_obj = sorted((x.user for x in user_group.members),
467 group_members_obj = sorted((x.user for x in user_group.members),
468 key=lambda u: u.username.lower())
468 key=lambda u: u.username.lower())
469
469
470 group_members = [
470 group_members = [
471 {
471 {
472 'id': user.user_id,
472 'id': user.user_id,
473 'first_name': user.name,
473 'first_name': user.name,
474 'last_name': user.lastname,
474 'last_name': user.lastname,
475 'username': user.username,
475 'username': user.username,
476 'icon_link': h.gravatar_url(user.email, 30),
476 'icon_link': h.gravatar_url(user.email, 30),
477 'value_display': h.person(user.email),
477 'value_display': h.person(user.email),
478 'value': user.username,
478 'value': user.username,
479 'value_type': 'user',
479 'value_type': 'user',
480 'active': user.active,
480 'active': user.active,
481 }
481 }
482 for user in group_members_obj
482 for user in group_members_obj
483 ]
483 ]
484
484
485 return {
485 return {
486 'members': group_members
486 'members': group_members
487 }
487 }
@@ -1,748 +1,748 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Users crud controller for pylons
22 Users crud controller for pylons
23 """
23 """
24
24
25 import logging
25 import logging
26 import formencode
26 import formencode
27
27
28 from formencode import htmlfill
28 from formencode import htmlfill
29 from pylons import request, tmpl_context as c, url, config
29 from pylons import request, tmpl_context as c, url, config
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.lib.exceptions import (
34 from rhodecode.lib.exceptions import (
35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
36 UserOwnsUserGroupsException, UserCreationError)
36 UserOwnsUserGroupsException, UserCreationError)
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import auth
38 from rhodecode.lib import auth
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
41 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.base import BaseController, render
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
43
43
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
46 from rhodecode.model.forms import (
46 from rhodecode.model.forms import (
47 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
47 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
48 from rhodecode.model.repo_group import RepoGroupModel
48 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.permission import PermissionModel
52 from rhodecode.lib.utils import action_logger
52 from rhodecode.lib.utils import action_logger
53 from rhodecode.lib.ext_json import json
53 from rhodecode.lib.ext_json import json
54 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
54 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class UsersController(BaseController):
59 class UsersController(BaseController):
60 """REST Controller styled on the Atom Publishing Protocol"""
60 """REST Controller styled on the Atom Publishing Protocol"""
61
61
62 @LoginRequired()
62 @LoginRequired()
63 def __before__(self):
63 def __before__(self):
64 super(UsersController, self).__before__()
64 super(UsersController, self).__before__()
65 c.available_permissions = config['available_permissions']
65 c.available_permissions = config['available_permissions']
66 c.allowed_languages = [
66 c.allowed_languages = [
67 ('en', 'English (en)'),
67 ('en', 'English (en)'),
68 ('de', 'German (de)'),
68 ('de', 'German (de)'),
69 ('fr', 'French (fr)'),
69 ('fr', 'French (fr)'),
70 ('it', 'Italian (it)'),
70 ('it', 'Italian (it)'),
71 ('ja', 'Japanese (ja)'),
71 ('ja', 'Japanese (ja)'),
72 ('pl', 'Polish (pl)'),
72 ('pl', 'Polish (pl)'),
73 ('pt', 'Portuguese (pt)'),
73 ('pt', 'Portuguese (pt)'),
74 ('ru', 'Russian (ru)'),
74 ('ru', 'Russian (ru)'),
75 ('zh', 'Chinese (zh)'),
75 ('zh', 'Chinese (zh)'),
76 ]
76 ]
77 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
77 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
78
78
79 @HasPermissionAllDecorator('hg.admin')
79 @HasPermissionAllDecorator('hg.admin')
80 def index(self):
80 def index(self):
81 """GET /users: All items in the collection"""
81 """GET /users: All items in the collection"""
82 # url('users')
82 # url('users')
83
83
84 from rhodecode.lib.utils import PartialRenderer
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 def username(user_id, username):
87 def username(user_id, username):
88 return _render("user_name", user_id, username)
88 return _render("user_name", user_id, username)
89
89
90 def user_actions(user_id, username):
90 def user_actions(user_id, username):
91 return _render("user_actions", user_id, username)
91 return _render("user_actions", user_id, username)
92
92
93 # json generate
93 # json generate
94 c.users_list = User.query()\
94 c.users_list = User.query()\
95 .filter(User.username != User.DEFAULT_USER) \
95 .filter(User.username != User.DEFAULT_USER) \
96 .all()
96 .all()
97
97
98 users_data = []
98 users_data = []
99 for user in c.users_list:
99 for user in c.users_list:
100 users_data.append({
100 users_data.append({
101 "username": h.gravatar_with_user(user.username),
101 "username": h.gravatar_with_user(user.username),
102 "username_raw": user.username,
102 "username_raw": user.username,
103 "email": user.email,
103 "email": user.email,
104 "first_name": h.escape(user.name),
104 "first_name": h.escape(user.name),
105 "last_name": h.escape(user.lastname),
105 "last_name": h.escape(user.lastname),
106 "last_login": h.format_date(user.last_login),
106 "last_login": h.format_date(user.last_login),
107 "last_login_raw": datetime_to_time(user.last_login),
107 "last_login_raw": datetime_to_time(user.last_login),
108 "last_activity": h.format_date(
108 "last_activity": h.format_date(
109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
110 "last_activity_raw": user.user_data.get('last_activity', 0),
110 "last_activity_raw": user.user_data.get('last_activity', 0),
111 "active": h.bool2icon(user.active),
111 "active": h.bool2icon(user.active),
112 "active_raw": user.active,
112 "active_raw": user.active,
113 "admin": h.bool2icon(user.admin),
113 "admin": h.bool2icon(user.admin),
114 "admin_raw": user.admin,
114 "admin_raw": user.admin,
115 "extern_type": user.extern_type,
115 "extern_type": user.extern_type,
116 "extern_name": user.extern_name,
116 "extern_name": user.extern_name,
117 "action": user_actions(user.user_id, user.username),
117 "action": user_actions(user.user_id, user.username),
118 })
118 })
119
119
120
120
121 c.data = json.dumps(users_data)
121 c.data = json.dumps(users_data)
122 return render('admin/users/users.html')
122 return render('admin/users/users.mako')
123
123
124 def _get_personal_repo_group_template_vars(self):
124 def _get_personal_repo_group_template_vars(self):
125 DummyUser = AttributeDict({
125 DummyUser = AttributeDict({
126 'username': '${username}',
126 'username': '${username}',
127 'user_id': '${user_id}',
127 'user_id': '${user_id}',
128 })
128 })
129 c.default_create_repo_group = RepoGroupModel() \
129 c.default_create_repo_group = RepoGroupModel() \
130 .get_default_create_personal_repo_group()
130 .get_default_create_personal_repo_group()
131 c.personal_repo_group_name = RepoGroupModel() \
131 c.personal_repo_group_name = RepoGroupModel() \
132 .get_personal_group_name(DummyUser)
132 .get_personal_group_name(DummyUser)
133
133
134 @HasPermissionAllDecorator('hg.admin')
134 @HasPermissionAllDecorator('hg.admin')
135 @auth.CSRFRequired()
135 @auth.CSRFRequired()
136 def create(self):
136 def create(self):
137 """POST /users: Create a new item"""
137 """POST /users: Create a new item"""
138 # url('users')
138 # url('users')
139 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
139 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
140 user_model = UserModel()
140 user_model = UserModel()
141 user_form = UserForm()()
141 user_form = UserForm()()
142 try:
142 try:
143 form_result = user_form.to_python(dict(request.POST))
143 form_result = user_form.to_python(dict(request.POST))
144 user = user_model.create(form_result)
144 user = user_model.create(form_result)
145 Session().flush()
145 Session().flush()
146 username = form_result['username']
146 username = form_result['username']
147 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
147 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
148 None, self.ip_addr, self.sa)
148 None, self.ip_addr, self.sa)
149
149
150 user_link = h.link_to(h.escape(username),
150 user_link = h.link_to(h.escape(username),
151 url('edit_user',
151 url('edit_user',
152 user_id=user.user_id))
152 user_id=user.user_id))
153 h.flash(h.literal(_('Created user %(user_link)s')
153 h.flash(h.literal(_('Created user %(user_link)s')
154 % {'user_link': user_link}), category='success')
154 % {'user_link': user_link}), category='success')
155 Session().commit()
155 Session().commit()
156 except formencode.Invalid as errors:
156 except formencode.Invalid as errors:
157 self._get_personal_repo_group_template_vars()
157 self._get_personal_repo_group_template_vars()
158 return htmlfill.render(
158 return htmlfill.render(
159 render('admin/users/user_add.html'),
159 render('admin/users/user_add.mako'),
160 defaults=errors.value,
160 defaults=errors.value,
161 errors=errors.error_dict or {},
161 errors=errors.error_dict or {},
162 prefix_error=False,
162 prefix_error=False,
163 encoding="UTF-8",
163 encoding="UTF-8",
164 force_defaults=False)
164 force_defaults=False)
165 except UserCreationError as e:
165 except UserCreationError as e:
166 h.flash(e, 'error')
166 h.flash(e, 'error')
167 except Exception:
167 except Exception:
168 log.exception("Exception creation of user")
168 log.exception("Exception creation of user")
169 h.flash(_('Error occurred during creation of user %s')
169 h.flash(_('Error occurred during creation of user %s')
170 % request.POST.get('username'), category='error')
170 % request.POST.get('username'), category='error')
171 return redirect(url('users'))
171 return redirect(url('users'))
172
172
173 @HasPermissionAllDecorator('hg.admin')
173 @HasPermissionAllDecorator('hg.admin')
174 def new(self):
174 def new(self):
175 """GET /users/new: Form to create a new item"""
175 """GET /users/new: Form to create a new item"""
176 # url('new_user')
176 # url('new_user')
177 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
177 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
178 self._get_personal_repo_group_template_vars()
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 @HasPermissionAllDecorator('hg.admin')
181 @HasPermissionAllDecorator('hg.admin')
182 @auth.CSRFRequired()
182 @auth.CSRFRequired()
183 def update(self, user_id):
183 def update(self, user_id):
184 """PUT /users/user_id: Update an existing item"""
184 """PUT /users/user_id: Update an existing item"""
185 # Forms posted to this method should contain a hidden field:
185 # Forms posted to this method should contain a hidden field:
186 # <input type="hidden" name="_method" value="PUT" />
186 # <input type="hidden" name="_method" value="PUT" />
187 # Or using helpers:
187 # Or using helpers:
188 # h.form(url('update_user', user_id=ID),
188 # h.form(url('update_user', user_id=ID),
189 # method='put')
189 # method='put')
190 # url('user', user_id=ID)
190 # url('user', user_id=ID)
191 user_id = safe_int(user_id)
191 user_id = safe_int(user_id)
192 c.user = User.get_or_404(user_id)
192 c.user = User.get_or_404(user_id)
193 c.active = 'profile'
193 c.active = 'profile'
194 c.extern_type = c.user.extern_type
194 c.extern_type = c.user.extern_type
195 c.extern_name = c.user.extern_name
195 c.extern_name = c.user.extern_name
196 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
196 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
197 available_languages = [x[0] for x in c.allowed_languages]
197 available_languages = [x[0] for x in c.allowed_languages]
198 _form = UserForm(edit=True, available_languages=available_languages,
198 _form = UserForm(edit=True, available_languages=available_languages,
199 old_data={'user_id': user_id,
199 old_data={'user_id': user_id,
200 'email': c.user.email})()
200 'email': c.user.email})()
201 form_result = {}
201 form_result = {}
202 try:
202 try:
203 form_result = _form.to_python(dict(request.POST))
203 form_result = _form.to_python(dict(request.POST))
204 skip_attrs = ['extern_type', 'extern_name']
204 skip_attrs = ['extern_type', 'extern_name']
205 # TODO: plugin should define if username can be updated
205 # TODO: plugin should define if username can be updated
206 if c.extern_type != "rhodecode":
206 if c.extern_type != "rhodecode":
207 # forbid updating username for external accounts
207 # forbid updating username for external accounts
208 skip_attrs.append('username')
208 skip_attrs.append('username')
209
209
210 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
210 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
211 usr = form_result['username']
211 usr = form_result['username']
212 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
212 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
213 None, self.ip_addr, self.sa)
213 None, self.ip_addr, self.sa)
214 h.flash(_('User updated successfully'), category='success')
214 h.flash(_('User updated successfully'), category='success')
215 Session().commit()
215 Session().commit()
216 except formencode.Invalid as errors:
216 except formencode.Invalid as errors:
217 defaults = errors.value
217 defaults = errors.value
218 e = errors.error_dict or {}
218 e = errors.error_dict or {}
219
219
220 return htmlfill.render(
220 return htmlfill.render(
221 render('admin/users/user_edit.html'),
221 render('admin/users/user_edit.mako'),
222 defaults=defaults,
222 defaults=defaults,
223 errors=e,
223 errors=e,
224 prefix_error=False,
224 prefix_error=False,
225 encoding="UTF-8",
225 encoding="UTF-8",
226 force_defaults=False)
226 force_defaults=False)
227 except UserCreationError as e:
227 except UserCreationError as e:
228 h.flash(e, 'error')
228 h.flash(e, 'error')
229 except Exception:
229 except Exception:
230 log.exception("Exception updating user")
230 log.exception("Exception updating user")
231 h.flash(_('Error occurred during update of user %s')
231 h.flash(_('Error occurred during update of user %s')
232 % form_result.get('username'), category='error')
232 % form_result.get('username'), category='error')
233 return redirect(url('edit_user', user_id=user_id))
233 return redirect(url('edit_user', user_id=user_id))
234
234
235 @HasPermissionAllDecorator('hg.admin')
235 @HasPermissionAllDecorator('hg.admin')
236 @auth.CSRFRequired()
236 @auth.CSRFRequired()
237 def delete(self, user_id):
237 def delete(self, user_id):
238 """DELETE /users/user_id: Delete an existing item"""
238 """DELETE /users/user_id: Delete an existing item"""
239 # Forms posted to this method should contain a hidden field:
239 # Forms posted to this method should contain a hidden field:
240 # <input type="hidden" name="_method" value="DELETE" />
240 # <input type="hidden" name="_method" value="DELETE" />
241 # Or using helpers:
241 # Or using helpers:
242 # h.form(url('delete_user', user_id=ID),
242 # h.form(url('delete_user', user_id=ID),
243 # method='delete')
243 # method='delete')
244 # url('user', user_id=ID)
244 # url('user', user_id=ID)
245 user_id = safe_int(user_id)
245 user_id = safe_int(user_id)
246 c.user = User.get_or_404(user_id)
246 c.user = User.get_or_404(user_id)
247
247
248 _repos = c.user.repositories
248 _repos = c.user.repositories
249 _repo_groups = c.user.repository_groups
249 _repo_groups = c.user.repository_groups
250 _user_groups = c.user.user_groups
250 _user_groups = c.user.user_groups
251
251
252 handle_repos = None
252 handle_repos = None
253 handle_repo_groups = None
253 handle_repo_groups = None
254 handle_user_groups = None
254 handle_user_groups = None
255 # dummy call for flash of handle
255 # dummy call for flash of handle
256 set_handle_flash_repos = lambda: None
256 set_handle_flash_repos = lambda: None
257 set_handle_flash_repo_groups = lambda: None
257 set_handle_flash_repo_groups = lambda: None
258 set_handle_flash_user_groups = lambda: None
258 set_handle_flash_user_groups = lambda: None
259
259
260 if _repos and request.POST.get('user_repos'):
260 if _repos and request.POST.get('user_repos'):
261 do = request.POST['user_repos']
261 do = request.POST['user_repos']
262 if do == 'detach':
262 if do == 'detach':
263 handle_repos = 'detach'
263 handle_repos = 'detach'
264 set_handle_flash_repos = lambda: h.flash(
264 set_handle_flash_repos = lambda: h.flash(
265 _('Detached %s repositories') % len(_repos),
265 _('Detached %s repositories') % len(_repos),
266 category='success')
266 category='success')
267 elif do == 'delete':
267 elif do == 'delete':
268 handle_repos = 'delete'
268 handle_repos = 'delete'
269 set_handle_flash_repos = lambda: h.flash(
269 set_handle_flash_repos = lambda: h.flash(
270 _('Deleted %s repositories') % len(_repos),
270 _('Deleted %s repositories') % len(_repos),
271 category='success')
271 category='success')
272
272
273 if _repo_groups and request.POST.get('user_repo_groups'):
273 if _repo_groups and request.POST.get('user_repo_groups'):
274 do = request.POST['user_repo_groups']
274 do = request.POST['user_repo_groups']
275 if do == 'detach':
275 if do == 'detach':
276 handle_repo_groups = 'detach'
276 handle_repo_groups = 'detach'
277 set_handle_flash_repo_groups = lambda: h.flash(
277 set_handle_flash_repo_groups = lambda: h.flash(
278 _('Detached %s repository groups') % len(_repo_groups),
278 _('Detached %s repository groups') % len(_repo_groups),
279 category='success')
279 category='success')
280 elif do == 'delete':
280 elif do == 'delete':
281 handle_repo_groups = 'delete'
281 handle_repo_groups = 'delete'
282 set_handle_flash_repo_groups = lambda: h.flash(
282 set_handle_flash_repo_groups = lambda: h.flash(
283 _('Deleted %s repository groups') % len(_repo_groups),
283 _('Deleted %s repository groups') % len(_repo_groups),
284 category='success')
284 category='success')
285
285
286 if _user_groups and request.POST.get('user_user_groups'):
286 if _user_groups and request.POST.get('user_user_groups'):
287 do = request.POST['user_user_groups']
287 do = request.POST['user_user_groups']
288 if do == 'detach':
288 if do == 'detach':
289 handle_user_groups = 'detach'
289 handle_user_groups = 'detach'
290 set_handle_flash_user_groups = lambda: h.flash(
290 set_handle_flash_user_groups = lambda: h.flash(
291 _('Detached %s user groups') % len(_user_groups),
291 _('Detached %s user groups') % len(_user_groups),
292 category='success')
292 category='success')
293 elif do == 'delete':
293 elif do == 'delete':
294 handle_user_groups = 'delete'
294 handle_user_groups = 'delete'
295 set_handle_flash_user_groups = lambda: h.flash(
295 set_handle_flash_user_groups = lambda: h.flash(
296 _('Deleted %s user groups') % len(_user_groups),
296 _('Deleted %s user groups') % len(_user_groups),
297 category='success')
297 category='success')
298
298
299 try:
299 try:
300 UserModel().delete(c.user, handle_repos=handle_repos,
300 UserModel().delete(c.user, handle_repos=handle_repos,
301 handle_repo_groups=handle_repo_groups,
301 handle_repo_groups=handle_repo_groups,
302 handle_user_groups=handle_user_groups)
302 handle_user_groups=handle_user_groups)
303 Session().commit()
303 Session().commit()
304 set_handle_flash_repos()
304 set_handle_flash_repos()
305 set_handle_flash_repo_groups()
305 set_handle_flash_repo_groups()
306 set_handle_flash_user_groups()
306 set_handle_flash_user_groups()
307 h.flash(_('Successfully deleted user'), category='success')
307 h.flash(_('Successfully deleted user'), category='success')
308 except (UserOwnsReposException, UserOwnsRepoGroupsException,
308 except (UserOwnsReposException, UserOwnsRepoGroupsException,
309 UserOwnsUserGroupsException, DefaultUserException) as e:
309 UserOwnsUserGroupsException, DefaultUserException) as e:
310 h.flash(e, category='warning')
310 h.flash(e, category='warning')
311 except Exception:
311 except Exception:
312 log.exception("Exception during deletion of user")
312 log.exception("Exception during deletion of user")
313 h.flash(_('An error occurred during deletion of user'),
313 h.flash(_('An error occurred during deletion of user'),
314 category='error')
314 category='error')
315 return redirect(url('users'))
315 return redirect(url('users'))
316
316
317 @HasPermissionAllDecorator('hg.admin')
317 @HasPermissionAllDecorator('hg.admin')
318 @auth.CSRFRequired()
318 @auth.CSRFRequired()
319 def reset_password(self, user_id):
319 def reset_password(self, user_id):
320 """
320 """
321 toggle reset password flag for this user
321 toggle reset password flag for this user
322
322
323 :param user_id:
323 :param user_id:
324 """
324 """
325 user_id = safe_int(user_id)
325 user_id = safe_int(user_id)
326 c.user = User.get_or_404(user_id)
326 c.user = User.get_or_404(user_id)
327 try:
327 try:
328 old_value = c.user.user_data.get('force_password_change')
328 old_value = c.user.user_data.get('force_password_change')
329 c.user.update_userdata(force_password_change=not old_value)
329 c.user.update_userdata(force_password_change=not old_value)
330 Session().commit()
330 Session().commit()
331 if old_value:
331 if old_value:
332 msg = _('Force password change disabled for user')
332 msg = _('Force password change disabled for user')
333 else:
333 else:
334 msg = _('Force password change enabled for user')
334 msg = _('Force password change enabled for user')
335 h.flash(msg, category='success')
335 h.flash(msg, category='success')
336 except Exception:
336 except Exception:
337 log.exception("Exception during password reset for user")
337 log.exception("Exception during password reset for user")
338 h.flash(_('An error occurred during password reset for user'),
338 h.flash(_('An error occurred during password reset for user'),
339 category='error')
339 category='error')
340
340
341 return redirect(url('edit_user_advanced', user_id=user_id))
341 return redirect(url('edit_user_advanced', user_id=user_id))
342
342
343 @HasPermissionAllDecorator('hg.admin')
343 @HasPermissionAllDecorator('hg.admin')
344 @auth.CSRFRequired()
344 @auth.CSRFRequired()
345 def create_personal_repo_group(self, user_id):
345 def create_personal_repo_group(self, user_id):
346 """
346 """
347 Create personal repository group for this user
347 Create personal repository group for this user
348
348
349 :param user_id:
349 :param user_id:
350 """
350 """
351 from rhodecode.model.repo_group import RepoGroupModel
351 from rhodecode.model.repo_group import RepoGroupModel
352
352
353 user_id = safe_int(user_id)
353 user_id = safe_int(user_id)
354 c.user = User.get_or_404(user_id)
354 c.user = User.get_or_404(user_id)
355 personal_repo_group = RepoGroup.get_user_personal_repo_group(
355 personal_repo_group = RepoGroup.get_user_personal_repo_group(
356 c.user.user_id)
356 c.user.user_id)
357 if personal_repo_group:
357 if personal_repo_group:
358 return redirect(url('edit_user_advanced', user_id=user_id))
358 return redirect(url('edit_user_advanced', user_id=user_id))
359
359
360 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
360 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
361 c.user)
361 c.user)
362 named_personal_group = RepoGroup.get_by_group_name(
362 named_personal_group = RepoGroup.get_by_group_name(
363 personal_repo_group_name)
363 personal_repo_group_name)
364 try:
364 try:
365
365
366 if named_personal_group and named_personal_group.user_id == c.user.user_id:
366 if named_personal_group and named_personal_group.user_id == c.user.user_id:
367 # migrate the same named group, and mark it as personal
367 # migrate the same named group, and mark it as personal
368 named_personal_group.personal = True
368 named_personal_group.personal = True
369 Session().add(named_personal_group)
369 Session().add(named_personal_group)
370 Session().commit()
370 Session().commit()
371 msg = _('Linked repository group `%s` as personal' % (
371 msg = _('Linked repository group `%s` as personal' % (
372 personal_repo_group_name,))
372 personal_repo_group_name,))
373 h.flash(msg, category='success')
373 h.flash(msg, category='success')
374 elif not named_personal_group:
374 elif not named_personal_group:
375 RepoGroupModel().create_personal_repo_group(c.user)
375 RepoGroupModel().create_personal_repo_group(c.user)
376
376
377 msg = _('Created repository group `%s`' % (
377 msg = _('Created repository group `%s`' % (
378 personal_repo_group_name,))
378 personal_repo_group_name,))
379 h.flash(msg, category='success')
379 h.flash(msg, category='success')
380 else:
380 else:
381 msg = _('Repository group `%s` is already taken' % (
381 msg = _('Repository group `%s` is already taken' % (
382 personal_repo_group_name,))
382 personal_repo_group_name,))
383 h.flash(msg, category='warning')
383 h.flash(msg, category='warning')
384 except Exception:
384 except Exception:
385 log.exception("Exception during repository group creation")
385 log.exception("Exception during repository group creation")
386 msg = _(
386 msg = _(
387 'An error occurred during repository group creation for user')
387 'An error occurred during repository group creation for user')
388 h.flash(msg, category='error')
388 h.flash(msg, category='error')
389 Session().rollback()
389 Session().rollback()
390
390
391 return redirect(url('edit_user_advanced', user_id=user_id))
391 return redirect(url('edit_user_advanced', user_id=user_id))
392
392
393 @HasPermissionAllDecorator('hg.admin')
393 @HasPermissionAllDecorator('hg.admin')
394 def show(self, user_id):
394 def show(self, user_id):
395 """GET /users/user_id: Show a specific item"""
395 """GET /users/user_id: Show a specific item"""
396 # url('user', user_id=ID)
396 # url('user', user_id=ID)
397 User.get_or_404(-1)
397 User.get_or_404(-1)
398
398
399 @HasPermissionAllDecorator('hg.admin')
399 @HasPermissionAllDecorator('hg.admin')
400 def edit(self, user_id):
400 def edit(self, user_id):
401 """GET /users/user_id/edit: Form to edit an existing item"""
401 """GET /users/user_id/edit: Form to edit an existing item"""
402 # url('edit_user', user_id=ID)
402 # url('edit_user', user_id=ID)
403 user_id = safe_int(user_id)
403 user_id = safe_int(user_id)
404 c.user = User.get_or_404(user_id)
404 c.user = User.get_or_404(user_id)
405 if c.user.username == User.DEFAULT_USER:
405 if c.user.username == User.DEFAULT_USER:
406 h.flash(_("You can't edit this user"), category='warning')
406 h.flash(_("You can't edit this user"), category='warning')
407 return redirect(url('users'))
407 return redirect(url('users'))
408
408
409 c.active = 'profile'
409 c.active = 'profile'
410 c.extern_type = c.user.extern_type
410 c.extern_type = c.user.extern_type
411 c.extern_name = c.user.extern_name
411 c.extern_name = c.user.extern_name
412 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
412 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
413
413
414 defaults = c.user.get_dict()
414 defaults = c.user.get_dict()
415 defaults.update({'language': c.user.user_data.get('language')})
415 defaults.update({'language': c.user.user_data.get('language')})
416 return htmlfill.render(
416 return htmlfill.render(
417 render('admin/users/user_edit.html'),
417 render('admin/users/user_edit.mako'),
418 defaults=defaults,
418 defaults=defaults,
419 encoding="UTF-8",
419 encoding="UTF-8",
420 force_defaults=False)
420 force_defaults=False)
421
421
422 @HasPermissionAllDecorator('hg.admin')
422 @HasPermissionAllDecorator('hg.admin')
423 def edit_advanced(self, user_id):
423 def edit_advanced(self, user_id):
424 user_id = safe_int(user_id)
424 user_id = safe_int(user_id)
425 user = c.user = User.get_or_404(user_id)
425 user = c.user = User.get_or_404(user_id)
426 if user.username == User.DEFAULT_USER:
426 if user.username == User.DEFAULT_USER:
427 h.flash(_("You can't edit this user"), category='warning')
427 h.flash(_("You can't edit this user"), category='warning')
428 return redirect(url('users'))
428 return redirect(url('users'))
429
429
430 c.active = 'advanced'
430 c.active = 'advanced'
431 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
431 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
432 c.personal_repo_group = c.perm_user.personal_repo_group
432 c.personal_repo_group = c.perm_user.personal_repo_group
433 c.personal_repo_group_name = RepoGroupModel()\
433 c.personal_repo_group_name = RepoGroupModel()\
434 .get_personal_group_name(user)
434 .get_personal_group_name(user)
435 c.first_admin = User.get_first_super_admin()
435 c.first_admin = User.get_first_super_admin()
436 defaults = user.get_dict()
436 defaults = user.get_dict()
437
437
438 # Interim workaround if the user participated on any pull requests as a
438 # Interim workaround if the user participated on any pull requests as a
439 # reviewer.
439 # reviewer.
440 has_review = bool(PullRequestReviewers.query().filter(
440 has_review = bool(PullRequestReviewers.query().filter(
441 PullRequestReviewers.user_id == user_id).first())
441 PullRequestReviewers.user_id == user_id).first())
442 c.can_delete_user = not has_review
442 c.can_delete_user = not has_review
443 c.can_delete_user_message = _(
443 c.can_delete_user_message = _(
444 'The user participates as reviewer in pull requests and '
444 'The user participates as reviewer in pull requests and '
445 'cannot be deleted. You can set the user to '
445 'cannot be deleted. You can set the user to '
446 '"inactive" instead of deleting it.') if has_review else ''
446 '"inactive" instead of deleting it.') if has_review else ''
447
447
448 return htmlfill.render(
448 return htmlfill.render(
449 render('admin/users/user_edit.html'),
449 render('admin/users/user_edit.mako'),
450 defaults=defaults,
450 defaults=defaults,
451 encoding="UTF-8",
451 encoding="UTF-8",
452 force_defaults=False)
452 force_defaults=False)
453
453
454 @HasPermissionAllDecorator('hg.admin')
454 @HasPermissionAllDecorator('hg.admin')
455 def edit_auth_tokens(self, user_id):
455 def edit_auth_tokens(self, user_id):
456 user_id = safe_int(user_id)
456 user_id = safe_int(user_id)
457 c.user = User.get_or_404(user_id)
457 c.user = User.get_or_404(user_id)
458 if c.user.username == User.DEFAULT_USER:
458 if c.user.username == User.DEFAULT_USER:
459 h.flash(_("You can't edit this user"), category='warning')
459 h.flash(_("You can't edit this user"), category='warning')
460 return redirect(url('users'))
460 return redirect(url('users'))
461
461
462 c.active = 'auth_tokens'
462 c.active = 'auth_tokens'
463 show_expired = True
463 show_expired = True
464 c.lifetime_values = [
464 c.lifetime_values = [
465 (str(-1), _('forever')),
465 (str(-1), _('forever')),
466 (str(5), _('5 minutes')),
466 (str(5), _('5 minutes')),
467 (str(60), _('1 hour')),
467 (str(60), _('1 hour')),
468 (str(60 * 24), _('1 day')),
468 (str(60 * 24), _('1 day')),
469 (str(60 * 24 * 30), _('1 month')),
469 (str(60 * 24 * 30), _('1 month')),
470 ]
470 ]
471 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
471 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
472 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
472 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
473 for x in AuthTokenModel.cls.ROLES]
473 for x in AuthTokenModel.cls.ROLES]
474 c.role_options = [(c.role_values, _("Role"))]
474 c.role_options = [(c.role_values, _("Role"))]
475 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
475 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
476 c.user.user_id, show_expired=show_expired)
476 c.user.user_id, show_expired=show_expired)
477 defaults = c.user.get_dict()
477 defaults = c.user.get_dict()
478 return htmlfill.render(
478 return htmlfill.render(
479 render('admin/users/user_edit.html'),
479 render('admin/users/user_edit.mako'),
480 defaults=defaults,
480 defaults=defaults,
481 encoding="UTF-8",
481 encoding="UTF-8",
482 force_defaults=False)
482 force_defaults=False)
483
483
484 @HasPermissionAllDecorator('hg.admin')
484 @HasPermissionAllDecorator('hg.admin')
485 @auth.CSRFRequired()
485 @auth.CSRFRequired()
486 def add_auth_token(self, user_id):
486 def add_auth_token(self, user_id):
487 user_id = safe_int(user_id)
487 user_id = safe_int(user_id)
488 c.user = User.get_or_404(user_id)
488 c.user = User.get_or_404(user_id)
489 if c.user.username == User.DEFAULT_USER:
489 if c.user.username == User.DEFAULT_USER:
490 h.flash(_("You can't edit this user"), category='warning')
490 h.flash(_("You can't edit this user"), category='warning')
491 return redirect(url('users'))
491 return redirect(url('users'))
492
492
493 lifetime = safe_int(request.POST.get('lifetime'), -1)
493 lifetime = safe_int(request.POST.get('lifetime'), -1)
494 description = request.POST.get('description')
494 description = request.POST.get('description')
495 role = request.POST.get('role')
495 role = request.POST.get('role')
496 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
496 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
497 Session().commit()
497 Session().commit()
498 h.flash(_("Auth token successfully created"), category='success')
498 h.flash(_("Auth token successfully created"), category='success')
499 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
499 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
500
500
501 @HasPermissionAllDecorator('hg.admin')
501 @HasPermissionAllDecorator('hg.admin')
502 @auth.CSRFRequired()
502 @auth.CSRFRequired()
503 def delete_auth_token(self, user_id):
503 def delete_auth_token(self, user_id):
504 user_id = safe_int(user_id)
504 user_id = safe_int(user_id)
505 c.user = User.get_or_404(user_id)
505 c.user = User.get_or_404(user_id)
506 if c.user.username == User.DEFAULT_USER:
506 if c.user.username == User.DEFAULT_USER:
507 h.flash(_("You can't edit this user"), category='warning')
507 h.flash(_("You can't edit this user"), category='warning')
508 return redirect(url('users'))
508 return redirect(url('users'))
509
509
510 auth_token = request.POST.get('del_auth_token')
510 auth_token = request.POST.get('del_auth_token')
511 if request.POST.get('del_auth_token_builtin'):
511 if request.POST.get('del_auth_token_builtin'):
512 user = User.get(c.user.user_id)
512 user = User.get(c.user.user_id)
513 if user:
513 if user:
514 user.api_key = generate_auth_token(user.username)
514 user.api_key = generate_auth_token(user.username)
515 Session().add(user)
515 Session().add(user)
516 Session().commit()
516 Session().commit()
517 h.flash(_("Auth token successfully reset"), category='success')
517 h.flash(_("Auth token successfully reset"), category='success')
518 elif auth_token:
518 elif auth_token:
519 AuthTokenModel().delete(auth_token, c.user.user_id)
519 AuthTokenModel().delete(auth_token, c.user.user_id)
520 Session().commit()
520 Session().commit()
521 h.flash(_("Auth token successfully deleted"), category='success')
521 h.flash(_("Auth token successfully deleted"), category='success')
522
522
523 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
523 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
524
524
525 @HasPermissionAllDecorator('hg.admin')
525 @HasPermissionAllDecorator('hg.admin')
526 def edit_global_perms(self, user_id):
526 def edit_global_perms(self, user_id):
527 user_id = safe_int(user_id)
527 user_id = safe_int(user_id)
528 c.user = User.get_or_404(user_id)
528 c.user = User.get_or_404(user_id)
529 if c.user.username == User.DEFAULT_USER:
529 if c.user.username == User.DEFAULT_USER:
530 h.flash(_("You can't edit this user"), category='warning')
530 h.flash(_("You can't edit this user"), category='warning')
531 return redirect(url('users'))
531 return redirect(url('users'))
532
532
533 c.active = 'global_perms'
533 c.active = 'global_perms'
534
534
535 c.default_user = User.get_default_user()
535 c.default_user = User.get_default_user()
536 defaults = c.user.get_dict()
536 defaults = c.user.get_dict()
537 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
537 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
538 defaults.update(c.default_user.get_default_perms())
538 defaults.update(c.default_user.get_default_perms())
539 defaults.update(c.user.get_default_perms())
539 defaults.update(c.user.get_default_perms())
540
540
541 return htmlfill.render(
541 return htmlfill.render(
542 render('admin/users/user_edit.html'),
542 render('admin/users/user_edit.mako'),
543 defaults=defaults,
543 defaults=defaults,
544 encoding="UTF-8",
544 encoding="UTF-8",
545 force_defaults=False)
545 force_defaults=False)
546
546
547 @HasPermissionAllDecorator('hg.admin')
547 @HasPermissionAllDecorator('hg.admin')
548 @auth.CSRFRequired()
548 @auth.CSRFRequired()
549 def update_global_perms(self, user_id):
549 def update_global_perms(self, user_id):
550 """PUT /users_perm/user_id: Update an existing item"""
550 """PUT /users_perm/user_id: Update an existing item"""
551 # url('user_perm', user_id=ID, method='put')
551 # url('user_perm', user_id=ID, method='put')
552 user_id = safe_int(user_id)
552 user_id = safe_int(user_id)
553 user = User.get_or_404(user_id)
553 user = User.get_or_404(user_id)
554 c.active = 'global_perms'
554 c.active = 'global_perms'
555 try:
555 try:
556 # first stage that verifies the checkbox
556 # first stage that verifies the checkbox
557 _form = UserIndividualPermissionsForm()
557 _form = UserIndividualPermissionsForm()
558 form_result = _form.to_python(dict(request.POST))
558 form_result = _form.to_python(dict(request.POST))
559 inherit_perms = form_result['inherit_default_permissions']
559 inherit_perms = form_result['inherit_default_permissions']
560 user.inherit_default_permissions = inherit_perms
560 user.inherit_default_permissions = inherit_perms
561 Session().add(user)
561 Session().add(user)
562
562
563 if not inherit_perms:
563 if not inherit_perms:
564 # only update the individual ones if we un check the flag
564 # only update the individual ones if we un check the flag
565 _form = UserPermissionsForm(
565 _form = UserPermissionsForm(
566 [x[0] for x in c.repo_create_choices],
566 [x[0] for x in c.repo_create_choices],
567 [x[0] for x in c.repo_create_on_write_choices],
567 [x[0] for x in c.repo_create_on_write_choices],
568 [x[0] for x in c.repo_group_create_choices],
568 [x[0] for x in c.repo_group_create_choices],
569 [x[0] for x in c.user_group_create_choices],
569 [x[0] for x in c.user_group_create_choices],
570 [x[0] for x in c.fork_choices],
570 [x[0] for x in c.fork_choices],
571 [x[0] for x in c.inherit_default_permission_choices])()
571 [x[0] for x in c.inherit_default_permission_choices])()
572
572
573 form_result = _form.to_python(dict(request.POST))
573 form_result = _form.to_python(dict(request.POST))
574 form_result.update({'perm_user_id': user.user_id})
574 form_result.update({'perm_user_id': user.user_id})
575
575
576 PermissionModel().update_user_permissions(form_result)
576 PermissionModel().update_user_permissions(form_result)
577
577
578 Session().commit()
578 Session().commit()
579 h.flash(_('User global permissions updated successfully'),
579 h.flash(_('User global permissions updated successfully'),
580 category='success')
580 category='success')
581
581
582 Session().commit()
582 Session().commit()
583 except formencode.Invalid as errors:
583 except formencode.Invalid as errors:
584 defaults = errors.value
584 defaults = errors.value
585 c.user = user
585 c.user = user
586 return htmlfill.render(
586 return htmlfill.render(
587 render('admin/users/user_edit.html'),
587 render('admin/users/user_edit.mako'),
588 defaults=defaults,
588 defaults=defaults,
589 errors=errors.error_dict or {},
589 errors=errors.error_dict or {},
590 prefix_error=False,
590 prefix_error=False,
591 encoding="UTF-8",
591 encoding="UTF-8",
592 force_defaults=False)
592 force_defaults=False)
593 except Exception:
593 except Exception:
594 log.exception("Exception during permissions saving")
594 log.exception("Exception during permissions saving")
595 h.flash(_('An error occurred during permissions saving'),
595 h.flash(_('An error occurred during permissions saving'),
596 category='error')
596 category='error')
597 return redirect(url('edit_user_global_perms', user_id=user_id))
597 return redirect(url('edit_user_global_perms', user_id=user_id))
598
598
599 @HasPermissionAllDecorator('hg.admin')
599 @HasPermissionAllDecorator('hg.admin')
600 def edit_perms_summary(self, user_id):
600 def edit_perms_summary(self, user_id):
601 user_id = safe_int(user_id)
601 user_id = safe_int(user_id)
602 c.user = User.get_or_404(user_id)
602 c.user = User.get_or_404(user_id)
603 if c.user.username == User.DEFAULT_USER:
603 if c.user.username == User.DEFAULT_USER:
604 h.flash(_("You can't edit this user"), category='warning')
604 h.flash(_("You can't edit this user"), category='warning')
605 return redirect(url('users'))
605 return redirect(url('users'))
606
606
607 c.active = 'perms_summary'
607 c.active = 'perms_summary'
608 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
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 @HasPermissionAllDecorator('hg.admin')
612 @HasPermissionAllDecorator('hg.admin')
613 def edit_emails(self, user_id):
613 def edit_emails(self, user_id):
614 user_id = safe_int(user_id)
614 user_id = safe_int(user_id)
615 c.user = User.get_or_404(user_id)
615 c.user = User.get_or_404(user_id)
616 if c.user.username == User.DEFAULT_USER:
616 if c.user.username == User.DEFAULT_USER:
617 h.flash(_("You can't edit this user"), category='warning')
617 h.flash(_("You can't edit this user"), category='warning')
618 return redirect(url('users'))
618 return redirect(url('users'))
619
619
620 c.active = 'emails'
620 c.active = 'emails'
621 c.user_email_map = UserEmailMap.query() \
621 c.user_email_map = UserEmailMap.query() \
622 .filter(UserEmailMap.user == c.user).all()
622 .filter(UserEmailMap.user == c.user).all()
623
623
624 defaults = c.user.get_dict()
624 defaults = c.user.get_dict()
625 return htmlfill.render(
625 return htmlfill.render(
626 render('admin/users/user_edit.html'),
626 render('admin/users/user_edit.mako'),
627 defaults=defaults,
627 defaults=defaults,
628 encoding="UTF-8",
628 encoding="UTF-8",
629 force_defaults=False)
629 force_defaults=False)
630
630
631 @HasPermissionAllDecorator('hg.admin')
631 @HasPermissionAllDecorator('hg.admin')
632 @auth.CSRFRequired()
632 @auth.CSRFRequired()
633 def add_email(self, user_id):
633 def add_email(self, user_id):
634 """POST /user_emails:Add an existing item"""
634 """POST /user_emails:Add an existing item"""
635 # url('user_emails', user_id=ID, method='put')
635 # url('user_emails', user_id=ID, method='put')
636 user_id = safe_int(user_id)
636 user_id = safe_int(user_id)
637 c.user = User.get_or_404(user_id)
637 c.user = User.get_or_404(user_id)
638
638
639 email = request.POST.get('new_email')
639 email = request.POST.get('new_email')
640 user_model = UserModel()
640 user_model = UserModel()
641
641
642 try:
642 try:
643 user_model.add_extra_email(user_id, email)
643 user_model.add_extra_email(user_id, email)
644 Session().commit()
644 Session().commit()
645 h.flash(_("Added new email address `%s` for user account") % email,
645 h.flash(_("Added new email address `%s` for user account") % email,
646 category='success')
646 category='success')
647 except formencode.Invalid as error:
647 except formencode.Invalid as error:
648 msg = error.error_dict['email']
648 msg = error.error_dict['email']
649 h.flash(msg, category='error')
649 h.flash(msg, category='error')
650 except Exception:
650 except Exception:
651 log.exception("Exception during email saving")
651 log.exception("Exception during email saving")
652 h.flash(_('An error occurred during email saving'),
652 h.flash(_('An error occurred during email saving'),
653 category='error')
653 category='error')
654 return redirect(url('edit_user_emails', user_id=user_id))
654 return redirect(url('edit_user_emails', user_id=user_id))
655
655
656 @HasPermissionAllDecorator('hg.admin')
656 @HasPermissionAllDecorator('hg.admin')
657 @auth.CSRFRequired()
657 @auth.CSRFRequired()
658 def delete_email(self, user_id):
658 def delete_email(self, user_id):
659 """DELETE /user_emails_delete/user_id: Delete an existing item"""
659 """DELETE /user_emails_delete/user_id: Delete an existing item"""
660 # url('user_emails_delete', user_id=ID, method='delete')
660 # url('user_emails_delete', user_id=ID, method='delete')
661 user_id = safe_int(user_id)
661 user_id = safe_int(user_id)
662 c.user = User.get_or_404(user_id)
662 c.user = User.get_or_404(user_id)
663 email_id = request.POST.get('del_email_id')
663 email_id = request.POST.get('del_email_id')
664 user_model = UserModel()
664 user_model = UserModel()
665 user_model.delete_extra_email(user_id, email_id)
665 user_model.delete_extra_email(user_id, email_id)
666 Session().commit()
666 Session().commit()
667 h.flash(_("Removed email address from user account"), category='success')
667 h.flash(_("Removed email address from user account"), category='success')
668 return redirect(url('edit_user_emails', user_id=user_id))
668 return redirect(url('edit_user_emails', user_id=user_id))
669
669
670 @HasPermissionAllDecorator('hg.admin')
670 @HasPermissionAllDecorator('hg.admin')
671 def edit_ips(self, user_id):
671 def edit_ips(self, user_id):
672 user_id = safe_int(user_id)
672 user_id = safe_int(user_id)
673 c.user = User.get_or_404(user_id)
673 c.user = User.get_or_404(user_id)
674 if c.user.username == User.DEFAULT_USER:
674 if c.user.username == User.DEFAULT_USER:
675 h.flash(_("You can't edit this user"), category='warning')
675 h.flash(_("You can't edit this user"), category='warning')
676 return redirect(url('users'))
676 return redirect(url('users'))
677
677
678 c.active = 'ips'
678 c.active = 'ips'
679 c.user_ip_map = UserIpMap.query() \
679 c.user_ip_map = UserIpMap.query() \
680 .filter(UserIpMap.user == c.user).all()
680 .filter(UserIpMap.user == c.user).all()
681
681
682 c.inherit_default_ips = c.user.inherit_default_permissions
682 c.inherit_default_ips = c.user.inherit_default_permissions
683 c.default_user_ip_map = UserIpMap.query() \
683 c.default_user_ip_map = UserIpMap.query() \
684 .filter(UserIpMap.user == User.get_default_user()).all()
684 .filter(UserIpMap.user == User.get_default_user()).all()
685
685
686 defaults = c.user.get_dict()
686 defaults = c.user.get_dict()
687 return htmlfill.render(
687 return htmlfill.render(
688 render('admin/users/user_edit.html'),
688 render('admin/users/user_edit.mako'),
689 defaults=defaults,
689 defaults=defaults,
690 encoding="UTF-8",
690 encoding="UTF-8",
691 force_defaults=False)
691 force_defaults=False)
692
692
693 @HasPermissionAllDecorator('hg.admin')
693 @HasPermissionAllDecorator('hg.admin')
694 @auth.CSRFRequired()
694 @auth.CSRFRequired()
695 def add_ip(self, user_id):
695 def add_ip(self, user_id):
696 """POST /user_ips:Add an existing item"""
696 """POST /user_ips:Add an existing item"""
697 # url('user_ips', user_id=ID, method='put')
697 # url('user_ips', user_id=ID, method='put')
698
698
699 user_id = safe_int(user_id)
699 user_id = safe_int(user_id)
700 c.user = User.get_or_404(user_id)
700 c.user = User.get_or_404(user_id)
701 user_model = UserModel()
701 user_model = UserModel()
702 try:
702 try:
703 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
703 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
704 except Exception as e:
704 except Exception as e:
705 ip_list = []
705 ip_list = []
706 log.exception("Exception during ip saving")
706 log.exception("Exception during ip saving")
707 h.flash(_('An error occurred during ip saving:%s' % (e,)),
707 h.flash(_('An error occurred during ip saving:%s' % (e,)),
708 category='error')
708 category='error')
709
709
710 desc = request.POST.get('description')
710 desc = request.POST.get('description')
711 added = []
711 added = []
712 for ip in ip_list:
712 for ip in ip_list:
713 try:
713 try:
714 user_model.add_extra_ip(user_id, ip, desc)
714 user_model.add_extra_ip(user_id, ip, desc)
715 Session().commit()
715 Session().commit()
716 added.append(ip)
716 added.append(ip)
717 except formencode.Invalid as error:
717 except formencode.Invalid as error:
718 msg = error.error_dict['ip']
718 msg = error.error_dict['ip']
719 h.flash(msg, category='error')
719 h.flash(msg, category='error')
720 except Exception:
720 except Exception:
721 log.exception("Exception during ip saving")
721 log.exception("Exception during ip saving")
722 h.flash(_('An error occurred during ip saving'),
722 h.flash(_('An error occurred during ip saving'),
723 category='error')
723 category='error')
724 if added:
724 if added:
725 h.flash(
725 h.flash(
726 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
726 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
727 category='success')
727 category='success')
728 if 'default_user' in request.POST:
728 if 'default_user' in request.POST:
729 return redirect(url('admin_permissions_ips'))
729 return redirect(url('admin_permissions_ips'))
730 return redirect(url('edit_user_ips', user_id=user_id))
730 return redirect(url('edit_user_ips', user_id=user_id))
731
731
732 @HasPermissionAllDecorator('hg.admin')
732 @HasPermissionAllDecorator('hg.admin')
733 @auth.CSRFRequired()
733 @auth.CSRFRequired()
734 def delete_ip(self, user_id):
734 def delete_ip(self, user_id):
735 """DELETE /user_ips_delete/user_id: Delete an existing item"""
735 """DELETE /user_ips_delete/user_id: Delete an existing item"""
736 # url('user_ips_delete', user_id=ID, method='delete')
736 # url('user_ips_delete', user_id=ID, method='delete')
737 user_id = safe_int(user_id)
737 user_id = safe_int(user_id)
738 c.user = User.get_or_404(user_id)
738 c.user = User.get_or_404(user_id)
739
739
740 ip_id = request.POST.get('del_ip_id')
740 ip_id = request.POST.get('del_ip_id')
741 user_model = UserModel()
741 user_model = UserModel()
742 user_model.delete_extra_ip(user_id, ip_id)
742 user_model.delete_extra_ip(user_id, ip_id)
743 Session().commit()
743 Session().commit()
744 h.flash(_("Removed ip address from user whitelist"), category='success')
744 h.flash(_("Removed ip address from user whitelist"), category='success')
745
745
746 if 'default_user' in request.POST:
746 if 'default_user' in request.POST:
747 return redirect(url('admin_permissions_ips'))
747 return redirect(url('admin_permissions_ips'))
748 return redirect(url('edit_user_ips', user_id=user_id))
748 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,46 +1,46 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 """
20 """
21 Bookmarks controller for rhodecode
21 Bookmarks controller for rhodecode
22 """
22 """
23
23
24 import logging
24 import logging
25
25
26 from pylons import tmpl_context as c
26 from pylons import tmpl_context as c
27 from webob.exc import HTTPNotFound
27 from webob.exc import HTTPNotFound
28
28
29 from rhodecode.controllers.base_references import BaseReferencesController
29 from rhodecode.controllers.base_references import BaseReferencesController
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class BookmarksController(BaseReferencesController):
35 class BookmarksController(BaseReferencesController):
36
36
37 partials_template = 'bookmarks/bookmarks_data.html'
37 partials_template = 'bookmarks/bookmarks_data.mako'
38 template = 'bookmarks/bookmarks.html'
38 template = 'bookmarks/bookmarks.mako'
39
39
40 def __before__(self):
40 def __before__(self):
41 super(BookmarksController, self).__before__()
41 super(BookmarksController, self).__before__()
42 if not h.is_hg(c.rhodecode_repo):
42 if not h.is_hg(c.rhodecode_repo):
43 raise HTTPNotFound()
43 raise HTTPNotFound()
44
44
45 def _get_reference_items(self, repo):
45 def _get_reference_items(self, repo):
46 return repo.bookmarks.items()
46 return repo.bookmarks.items()
@@ -1,45 +1,45 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 branches controller for rhodecode
22 branches controller for rhodecode
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from pylons import tmpl_context as c
27 from pylons import tmpl_context as c
28
28
29 from rhodecode.controllers.base_references import BaseReferencesController
29 from rhodecode.controllers.base_references import BaseReferencesController
30
30
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class BranchesController(BaseReferencesController):
35 class BranchesController(BaseReferencesController):
36
36
37 partials_template = 'branches/branches_data.html'
37 partials_template = 'branches/branches_data.mako'
38 template = 'branches/branches.html'
38 template = 'branches/branches.mako'
39
39
40 def __before__(self):
40 def __before__(self):
41 super(BranchesController, self).__before__()
41 super(BranchesController, self).__before__()
42 c.closed_branches = c.rhodecode_repo.branches_closed
42 c.closed_branches = c.rhodecode_repo.branches_closed
43
43
44 def _get_reference_items(self, repo):
44 def _get_reference_items(self, repo):
45 return repo.branches_all.items()
45 return repo.branches_all.items()
@@ -1,222 +1,222 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 changelog controller for rhodecode
22 changelog controller for rhodecode
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from pylons import request, url, session, tmpl_context as c
27 from pylons import request, url, session, tmpl_context as c
28 from pylons.controllers.util import redirect
28 from pylons.controllers.util import redirect
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30 from webob.exc import HTTPNotFound, HTTPBadRequest
30 from webob.exc import HTTPNotFound, HTTPBadRequest
31
31
32 import rhodecode.lib.helpers as h
32 import rhodecode.lib.helpers as h
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.base import BaseRepoController, render
34 from rhodecode.lib.base import BaseRepoController, render
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.graphmod import _colored, _dagwalker
37 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.helpers import RepoPage
38 from rhodecode.lib.utils2 import safe_int, safe_str
38 from rhodecode.lib.utils2 import safe_int, safe_str
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 RepositoryError, CommitDoesNotExistError,
40 RepositoryError, CommitDoesNotExistError,
41 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 DEFAULT_CHANGELOG_SIZE = 20
45 DEFAULT_CHANGELOG_SIZE = 20
46
46
47
47
48 def _load_changelog_summary():
48 def _load_changelog_summary():
49 p = safe_int(request.GET.get('page'), 1)
49 p = safe_int(request.GET.get('page'), 1)
50 size = safe_int(request.GET.get('size'), 10)
50 size = safe_int(request.GET.get('size'), 10)
51
51
52 def url_generator(**kw):
52 def url_generator(**kw):
53 return url('summary_home',
53 return url('summary_home',
54 repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw)
54 repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw)
55
55
56 pre_load = ['author', 'branch', 'date', 'message']
56 pre_load = ['author', 'branch', 'date', 'message']
57 try:
57 try:
58 collection = c.rhodecode_repo.get_commits(pre_load=pre_load)
58 collection = c.rhodecode_repo.get_commits(pre_load=pre_load)
59 except EmptyRepositoryError:
59 except EmptyRepositoryError:
60 collection = c.rhodecode_repo
60 collection = c.rhodecode_repo
61
61
62 c.repo_commits = RepoPage(
62 c.repo_commits = RepoPage(
63 collection, page=p, items_per_page=size, url=url_generator)
63 collection, page=p, items_per_page=size, url=url_generator)
64 page_ids = [x.raw_id for x in c.repo_commits]
64 page_ids = [x.raw_id for x in c.repo_commits]
65 c.comments = c.rhodecode_db_repo.get_comments(page_ids)
65 c.comments = c.rhodecode_db_repo.get_comments(page_ids)
66 c.statuses = c.rhodecode_db_repo.statuses(page_ids)
66 c.statuses = c.rhodecode_db_repo.statuses(page_ids)
67
67
68
68
69 class ChangelogController(BaseRepoController):
69 class ChangelogController(BaseRepoController):
70
70
71 def __before__(self):
71 def __before__(self):
72 super(ChangelogController, self).__before__()
72 super(ChangelogController, self).__before__()
73 c.affected_files_cut_off = 60
73 c.affected_files_cut_off = 60
74
74
75 def __get_commit_or_redirect(
75 def __get_commit_or_redirect(
76 self, commit_id, repo, redirect_after=True, partial=False):
76 self, commit_id, repo, redirect_after=True, partial=False):
77 """
77 """
78 This is a safe way to get a commit. If an error occurs it
78 This is a safe way to get a commit. If an error occurs it
79 redirects to a commit with a proper message. If partial is set
79 redirects to a commit with a proper message. If partial is set
80 then it does not do redirect raise and throws an exception instead.
80 then it does not do redirect raise and throws an exception instead.
81
81
82 :param commit_id: commit to fetch
82 :param commit_id: commit to fetch
83 :param repo: repo instance
83 :param repo: repo instance
84 """
84 """
85 try:
85 try:
86 return c.rhodecode_repo.get_commit(commit_id)
86 return c.rhodecode_repo.get_commit(commit_id)
87 except EmptyRepositoryError:
87 except EmptyRepositoryError:
88 if not redirect_after:
88 if not redirect_after:
89 return None
89 return None
90 h.flash(h.literal(_('There are no commits yet')),
90 h.flash(h.literal(_('There are no commits yet')),
91 category='warning')
91 category='warning')
92 redirect(url('changelog_home', repo_name=repo.repo_name))
92 redirect(url('changelog_home', repo_name=repo.repo_name))
93 except RepositoryError as e:
93 except RepositoryError as e:
94 msg = safe_str(e)
94 msg = safe_str(e)
95 log.exception(msg)
95 log.exception(msg)
96 h.flash(msg, category='warning')
96 h.flash(msg, category='warning')
97 if not partial:
97 if not partial:
98 redirect(h.url('changelog_home', repo_name=repo.repo_name))
98 redirect(h.url('changelog_home', repo_name=repo.repo_name))
99 raise HTTPBadRequest()
99 raise HTTPBadRequest()
100
100
101 def _graph(self, repo, commits):
101 def _graph(self, repo, commits):
102 """
102 """
103 Generates a DAG graph for repo
103 Generates a DAG graph for repo
104
104
105 :param repo: repo instance
105 :param repo: repo instance
106 :param commits: list of commits
106 :param commits: list of commits
107 """
107 """
108 if not commits:
108 if not commits:
109 c.jsdata = json.dumps([])
109 c.jsdata = json.dumps([])
110 return
110 return
111
111
112 dag = _dagwalker(repo, commits)
112 dag = _dagwalker(repo, commits)
113 data = [['', vtx, edges] for vtx, edges in _colored(dag)]
113 data = [['', vtx, edges] for vtx, edges in _colored(dag)]
114 c.jsdata = json.dumps(data)
114 c.jsdata = json.dumps(data)
115
115
116 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
116 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
117 if branch_name not in c.rhodecode_repo.branches_all:
117 if branch_name not in c.rhodecode_repo.branches_all:
118 h.flash('Branch {} is not found.'.format(branch_name),
118 h.flash('Branch {} is not found.'.format(branch_name),
119 category='warning')
119 category='warning')
120 redirect(url('changelog_file_home', repo_name=repo_name,
120 redirect(url('changelog_file_home', repo_name=repo_name,
121 revision=branch_name, f_path=f_path or ''))
121 revision=branch_name, f_path=f_path or ''))
122
122
123 @LoginRequired()
123 @LoginRequired()
124 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
124 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
125 'repository.admin')
125 'repository.admin')
126 def index(self, repo_name, revision=None, f_path=None):
126 def index(self, repo_name, revision=None, f_path=None):
127 commit_id = revision
127 commit_id = revision
128 limit = 100
128 limit = 100
129 hist_limit = safe_int(request.GET.get('limit')) or None
129 hist_limit = safe_int(request.GET.get('limit')) or None
130 if request.GET.get('size'):
130 if request.GET.get('size'):
131 c.size = safe_int(request.GET.get('size'), 1)
131 c.size = safe_int(request.GET.get('size'), 1)
132 session['changelog_size'] = c.size
132 session['changelog_size'] = c.size
133 session.save()
133 session.save()
134 else:
134 else:
135 c.size = int(session.get('changelog_size', DEFAULT_CHANGELOG_SIZE))
135 c.size = int(session.get('changelog_size', DEFAULT_CHANGELOG_SIZE))
136
136
137 # min size must be 1 and less than limit
137 # min size must be 1 and less than limit
138 c.size = max(c.size, 1) if c.size <= limit else limit
138 c.size = max(c.size, 1) if c.size <= limit else limit
139
139
140 p = safe_int(request.GET.get('page', 1), 1)
140 p = safe_int(request.GET.get('page', 1), 1)
141 c.branch_name = branch_name = request.GET.get('branch', None)
141 c.branch_name = branch_name = request.GET.get('branch', None)
142 c.book_name = book_name = request.GET.get('bookmark', None)
142 c.book_name = book_name = request.GET.get('bookmark', None)
143
143
144 c.selected_name = branch_name or book_name
144 c.selected_name = branch_name or book_name
145 if not commit_id and branch_name:
145 if not commit_id and branch_name:
146 self._check_if_valid_branch(branch_name, repo_name, f_path)
146 self._check_if_valid_branch(branch_name, repo_name, f_path)
147
147
148 c.changelog_for_path = f_path
148 c.changelog_for_path = f_path
149 pre_load = ['author', 'branch', 'date', 'message', 'parents']
149 pre_load = ['author', 'branch', 'date', 'message', 'parents']
150 try:
150 try:
151 if f_path:
151 if f_path:
152 log.debug('generating changelog for path %s', f_path)
152 log.debug('generating changelog for path %s', f_path)
153 # get the history for the file !
153 # get the history for the file !
154 base_commit = c.rhodecode_repo.get_commit(revision)
154 base_commit = c.rhodecode_repo.get_commit(revision)
155 try:
155 try:
156 collection = base_commit.get_file_history(
156 collection = base_commit.get_file_history(
157 f_path, limit=hist_limit, pre_load=pre_load)
157 f_path, limit=hist_limit, pre_load=pre_load)
158 if (collection
158 if (collection
159 and request.environ.get('HTTP_X_PARTIAL_XHR')):
159 and request.environ.get('HTTP_X_PARTIAL_XHR')):
160 # for ajax call we remove first one since we're looking
160 # for ajax call we remove first one since we're looking
161 # at it right now in the context of a file commit
161 # at it right now in the context of a file commit
162 collection.pop(0)
162 collection.pop(0)
163 except (NodeDoesNotExistError, CommitError):
163 except (NodeDoesNotExistError, CommitError):
164 # this node is not present at tip!
164 # this node is not present at tip!
165 try:
165 try:
166 commit = self.__get_commit_or_redirect(
166 commit = self.__get_commit_or_redirect(
167 commit_id, repo_name)
167 commit_id, repo_name)
168 collection = commit.get_file_history(f_path)
168 collection = commit.get_file_history(f_path)
169 except RepositoryError as e:
169 except RepositoryError as e:
170 h.flash(safe_str(e), category='warning')
170 h.flash(safe_str(e), category='warning')
171 redirect(h.url('changelog_home', repo_name=repo_name))
171 redirect(h.url('changelog_home', repo_name=repo_name))
172 collection = list(reversed(collection))
172 collection = list(reversed(collection))
173 else:
173 else:
174 collection = c.rhodecode_repo.get_commits(
174 collection = c.rhodecode_repo.get_commits(
175 branch_name=branch_name, pre_load=pre_load)
175 branch_name=branch_name, pre_load=pre_load)
176
176
177 c.total_cs = len(collection)
177 c.total_cs = len(collection)
178 c.showing_commits = min(c.size, c.total_cs)
178 c.showing_commits = min(c.size, c.total_cs)
179 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
179 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
180 items_per_page=c.size, branch=branch_name)
180 items_per_page=c.size, branch=branch_name)
181 page_commit_ids = [x.raw_id for x in c.pagination]
181 page_commit_ids = [x.raw_id for x in c.pagination]
182 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
182 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
183 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
183 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
184 except EmptyRepositoryError as e:
184 except EmptyRepositoryError as e:
185 h.flash(safe_str(e), category='warning')
185 h.flash(safe_str(e), category='warning')
186 return redirect(url('summary_home', repo_name=repo_name))
186 return redirect(url('summary_home', repo_name=repo_name))
187 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
187 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
188 msg = safe_str(e)
188 msg = safe_str(e)
189 log.exception(msg)
189 log.exception(msg)
190 h.flash(msg, category='error')
190 h.flash(msg, category='error')
191 return redirect(url('changelog_home', repo_name=repo_name))
191 return redirect(url('changelog_home', repo_name=repo_name))
192
192
193 if (request.environ.get('HTTP_X_PARTIAL_XHR')
193 if (request.environ.get('HTTP_X_PARTIAL_XHR')
194 or request.environ.get('HTTP_X_PJAX')):
194 or request.environ.get('HTTP_X_PJAX')):
195 # loading from ajax, we don't want the first result, it's popped
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 if f_path:
198 if f_path:
199 revs = []
199 revs = []
200 else:
200 else:
201 revs = c.pagination
201 revs = c.pagination
202 self._graph(c.rhodecode_repo, revs)
202 self._graph(c.rhodecode_repo, revs)
203
203
204 return render('changelog/changelog.html')
204 return render('changelog/changelog.mako')
205
205
206 @LoginRequired()
206 @LoginRequired()
207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
208 'repository.admin')
208 'repository.admin')
209 def changelog_details(self, commit_id):
209 def changelog_details(self, commit_id):
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 c.commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
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 raise HTTPNotFound()
213 raise HTTPNotFound()
214
214
215 @LoginRequired()
215 @LoginRequired()
216 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
216 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
217 'repository.admin')
217 'repository.admin')
218 def changelog_summary(self, repo_name):
218 def changelog_summary(self, repo_name):
219 if request.environ.get('HTTP_X_PJAX'):
219 if request.environ.get('HTTP_X_PJAX'):
220 _load_changelog_summary()
220 _load_changelog_summary()
221 return render('changelog/changelog_summary_data.html')
221 return render('changelog/changelog_summary_data.mako')
222 raise HTTPNotFound()
222 raise HTTPNotFound()
@@ -1,468 +1,468 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = ChangesetCommentsModel().get_comments(
213 c.comments = ChangesetCommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 # Iterate over ranges (default commit view is always one commit)
229 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
230 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
231 c.changes[commit.raw_id] = []
232
232
233 commit2 = commit
233 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
235
236 _diff = c.rhodecode_repo.get_diff(
236 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
237 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
239 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
240 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
241 file_limit=file_limit, show_full_diff=fulldiff)
242
242
243 commit_changes = OrderedDict()
243 commit_changes = OrderedDict()
244 if method == 'show':
244 if method == 'show':
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
247
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249
249
250 def _node_getter(commit):
250 def _node_getter(commit):
251 def get_node(fname):
251 def get_node(fname):
252 try:
252 try:
253 return commit.get_node(fname)
253 return commit.get_node(fname)
254 except NodeDoesNotExistError:
254 except NodeDoesNotExistError:
255 return None
255 return None
256 return get_node
256 return get_node
257
257
258 inline_comments = ChangesetCommentsModel().get_inline_comments(
258 inline_comments = ChangesetCommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
261 inline_comments)
261 inline_comments)
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
264 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
265 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
266 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
267 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
269 c.changes[commit.raw_id] = diffset
270 else:
270 else:
271 # downloads/raw we only need RAW diff nothing else
271 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
272 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
274
275 # sort comments by how they were generated
275 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
277
278
278
279 if len(c.commit_ranges) == 1:
279 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
280 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
281 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
283 if method == 'download':
284 response.content_type = 'text/plain'
284 response.content_type = 'text/plain'
285 response.content_disposition = (
285 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
287 return diff
288 elif method == 'patch':
288 elif method == 'patch':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
290 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.html')
291 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
292 elif method == 'raw':
293 response.content_type = 'text/plain'
293 response.content_type = 'text/plain'
294 return diff
294 return diff
295 elif method == 'show':
295 elif method == 'show':
296 if len(c.commit_ranges) == 1:
296 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.html')
297 return render('changeset/changeset.mako')
298 else:
298 else:
299 c.ancestor = None
299 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
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 @LoginRequired()
303 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
305 'repository.admin')
306 def index(self, revision, method='show'):
306 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
307 return self._index(revision, method=method)
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
311 'repository.admin')
312 def changeset_raw(self, revision):
312 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
313 return self._index(revision, method='raw')
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
317 'repository.admin')
318 def changeset_patch(self, revision):
318 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
319 return self._index(revision, method='patch')
320
320
321 @LoginRequired()
321 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
323 'repository.admin')
324 def changeset_download(self, revision):
324 def changeset_download(self, revision):
325 return self._index(revision, method='download')
325 return self._index(revision, method='download')
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @NotAnonymous()
328 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
330 'repository.admin')
331 @auth.CSRFRequired()
331 @auth.CSRFRequired()
332 @jsonify
332 @jsonify
333 def comment(self, repo_name, revision):
333 def comment(self, repo_name, revision):
334 commit_id = revision
334 commit_id = revision
335 status = request.POST.get('changeset_status', None)
335 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
336 text = request.POST.get('text')
337 if status:
337 if status:
338 text = text or (_('Status change %(transition_icon)s %(status)s')
338 text = text or (_('Status change %(transition_icon)s %(status)s')
339 % {'transition_icon': '>',
339 % {'transition_icon': '>',
340 'status': ChangesetStatus.get_status_lbl(status)})
340 'status': ChangesetStatus.get_status_lbl(status)})
341
341
342 multi_commit_ids = filter(
342 multi_commit_ids = filter(
343 lambda s: s not in ['', None],
343 lambda s: s not in ['', None],
344 request.POST.get('commit_ids', '').split(','),)
344 request.POST.get('commit_ids', '').split(','),)
345
345
346 commit_ids = multi_commit_ids or [commit_id]
346 commit_ids = multi_commit_ids or [commit_id]
347 comment = None
347 comment = None
348 for current_id in filter(None, commit_ids):
348 for current_id in filter(None, commit_ids):
349 c.co = comment = ChangesetCommentsModel().create(
349 c.co = comment = ChangesetCommentsModel().create(
350 text=text,
350 text=text,
351 repo=c.rhodecode_db_repo.repo_id,
351 repo=c.rhodecode_db_repo.repo_id,
352 user=c.rhodecode_user.user_id,
352 user=c.rhodecode_user.user_id,
353 revision=current_id,
353 revision=current_id,
354 f_path=request.POST.get('f_path'),
354 f_path=request.POST.get('f_path'),
355 line_no=request.POST.get('line'),
355 line_no=request.POST.get('line'),
356 status_change=(ChangesetStatus.get_status_lbl(status)
356 status_change=(ChangesetStatus.get_status_lbl(status)
357 if status else None),
357 if status else None),
358 status_change_type=status
358 status_change_type=status
359 )
359 )
360 # get status if set !
360 # get status if set !
361 if status:
361 if status:
362 # if latest status was from pull request and it's closed
362 # if latest status was from pull request and it's closed
363 # disallow changing status !
363 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
364 # dont_allow_on_closed_pull_request = True !
365
365
366 try:
366 try:
367 ChangesetStatusModel().set_status(
367 ChangesetStatusModel().set_status(
368 c.rhodecode_db_repo.repo_id,
368 c.rhodecode_db_repo.repo_id,
369 status,
369 status,
370 c.rhodecode_user.user_id,
370 c.rhodecode_user.user_id,
371 comment,
371 comment,
372 revision=current_id,
372 revision=current_id,
373 dont_allow_on_closed_pull_request=True
373 dont_allow_on_closed_pull_request=True
374 )
374 )
375 except StatusChangeOnClosedPullRequestError:
375 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
376 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
377 'a closed pull request is not allowed')
378 log.exception(msg)
378 log.exception(msg)
379 h.flash(msg, category='warning')
379 h.flash(msg, category='warning')
380 return redirect(h.url(
380 return redirect(h.url(
381 'changeset_home', repo_name=repo_name,
381 'changeset_home', repo_name=repo_name,
382 revision=current_id))
382 revision=current_id))
383
383
384 # finalize, commit and redirect
384 # finalize, commit and redirect
385 Session().commit()
385 Session().commit()
386
386
387 data = {
387 data = {
388 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
388 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
389 }
389 }
390 if comment:
390 if comment:
391 data.update(comment.get_dict())
391 data.update(comment.get_dict())
392 data.update({'rendered_text':
392 data.update({'rendered_text':
393 render('changeset/changeset_comment_block.html')})
393 render('changeset/changeset_comment_block.mako')})
394
394
395 return data
395 return data
396
396
397 @LoginRequired()
397 @LoginRequired()
398 @NotAnonymous()
398 @NotAnonymous()
399 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
400 'repository.admin')
400 'repository.admin')
401 @auth.CSRFRequired()
401 @auth.CSRFRequired()
402 def preview_comment(self):
402 def preview_comment(self):
403 # Technically a CSRF token is not needed as no state changes with this
403 # Technically a CSRF token is not needed as no state changes with this
404 # call. However, as this is a POST is better to have it, so automated
404 # call. However, as this is a POST is better to have it, so automated
405 # tools don't flag it as potential CSRF.
405 # tools don't flag it as potential CSRF.
406 # Post is required because the payload could be bigger than the maximum
406 # Post is required because the payload could be bigger than the maximum
407 # allowed by GET.
407 # allowed by GET.
408 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
408 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
409 raise HTTPBadRequest()
409 raise HTTPBadRequest()
410 text = request.POST.get('text')
410 text = request.POST.get('text')
411 renderer = request.POST.get('renderer') or 'rst'
411 renderer = request.POST.get('renderer') or 'rst'
412 if text:
412 if text:
413 return h.render(text, renderer=renderer, mentions=True)
413 return h.render(text, renderer=renderer, mentions=True)
414 return ''
414 return ''
415
415
416 @LoginRequired()
416 @LoginRequired()
417 @NotAnonymous()
417 @NotAnonymous()
418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 'repository.admin')
419 'repository.admin')
420 @auth.CSRFRequired()
420 @auth.CSRFRequired()
421 @jsonify
421 @jsonify
422 def delete_comment(self, repo_name, comment_id):
422 def delete_comment(self, repo_name, comment_id):
423 comment = ChangesetComment.get(comment_id)
423 comment = ChangesetComment.get(comment_id)
424 owner = (comment.author.user_id == c.rhodecode_user.user_id)
424 owner = (comment.author.user_id == c.rhodecode_user.user_id)
425 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
425 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
426 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
426 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
427 ChangesetCommentsModel().delete(comment=comment)
427 ChangesetCommentsModel().delete(comment=comment)
428 Session().commit()
428 Session().commit()
429 return True
429 return True
430 else:
430 else:
431 raise HTTPForbidden()
431 raise HTTPForbidden()
432
432
433 @LoginRequired()
433 @LoginRequired()
434 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
434 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
435 'repository.admin')
435 'repository.admin')
436 @jsonify
436 @jsonify
437 def changeset_info(self, repo_name, revision):
437 def changeset_info(self, repo_name, revision):
438 if request.is_xhr:
438 if request.is_xhr:
439 try:
439 try:
440 return c.rhodecode_repo.get_commit(commit_id=revision)
440 return c.rhodecode_repo.get_commit(commit_id=revision)
441 except CommitDoesNotExistError as e:
441 except CommitDoesNotExistError as e:
442 return EmptyCommit(message=str(e))
442 return EmptyCommit(message=str(e))
443 else:
443 else:
444 raise HTTPBadRequest()
444 raise HTTPBadRequest()
445
445
446 @LoginRequired()
446 @LoginRequired()
447 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
447 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
448 'repository.admin')
448 'repository.admin')
449 @jsonify
449 @jsonify
450 def changeset_children(self, repo_name, revision):
450 def changeset_children(self, repo_name, revision):
451 if request.is_xhr:
451 if request.is_xhr:
452 commit = c.rhodecode_repo.get_commit(commit_id=revision)
452 commit = c.rhodecode_repo.get_commit(commit_id=revision)
453 result = {"results": commit.children}
453 result = {"results": commit.children}
454 return result
454 return result
455 else:
455 else:
456 raise HTTPBadRequest()
456 raise HTTPBadRequest()
457
457
458 @LoginRequired()
458 @LoginRequired()
459 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
459 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
460 'repository.admin')
460 'repository.admin')
461 @jsonify
461 @jsonify
462 def changeset_parents(self, repo_name, revision):
462 def changeset_parents(self, repo_name, revision):
463 if request.is_xhr:
463 if request.is_xhr:
464 commit = c.rhodecode_repo.get_commit(commit_id=revision)
464 commit = c.rhodecode_repo.get_commit(commit_id=revision)
465 result = {"results": commit.parents}
465 result = {"results": commit.parents}
466 return result
466 return result
467 else:
467 else:
468 raise HTTPBadRequest()
468 raise HTTPBadRequest()
@@ -1,282 +1,282 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Compare controller for showing differences between two commits/refs/tags etc.
22 Compare controller for showing differences between two commits/refs/tags etc.
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from webob.exc import HTTPBadRequest
27 from webob.exc import HTTPBadRequest
28 from pylons import request, tmpl_context as c, url
28 from pylons import request, tmpl_context as c, url
29 from pylons.controllers.util import redirect
29 from pylons.controllers.util import redirect
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
32 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.utils import safe_str
37 from rhodecode.lib.utils import safe_str
38 from rhodecode.lib.utils2 import safe_unicode, str2bool
38 from rhodecode.lib.utils2 import safe_unicode, str2bool
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
40 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
41 NodeDoesNotExistError)
41 NodeDoesNotExistError)
42 from rhodecode.model.db import Repository, ChangesetStatus
42 from rhodecode.model.db import Repository, ChangesetStatus
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class CompareController(BaseRepoController):
47 class CompareController(BaseRepoController):
48
48
49 def __before__(self):
49 def __before__(self):
50 super(CompareController, self).__before__()
50 super(CompareController, self).__before__()
51
51
52 def _get_commit_or_redirect(
52 def _get_commit_or_redirect(
53 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 self, ref, ref_type, repo, redirect_after=True, partial=False):
54 """
54 """
55 This is a safe way to get a commit. If an error occurs it
55 This is a safe way to get a commit. If an error occurs it
56 redirects to a commit with a proper message. If partial is set
56 redirects to a commit with a proper message. If partial is set
57 then it does not do redirect raise and throws an exception instead.
57 then it does not do redirect raise and throws an exception instead.
58 """
58 """
59 try:
59 try:
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return repo.scm_instance().EMPTY_COMMIT
63 return repo.scm_instance().EMPTY_COMMIT
64 h.flash(h.literal(_('There are no commits yet')),
64 h.flash(h.literal(_('There are no commits yet')),
65 category='warning')
65 category='warning')
66 redirect(url('summary_home', repo_name=repo.repo_name))
66 redirect(url('summary_home', repo_name=repo.repo_name))
67
67
68 except RepositoryError as e:
68 except RepositoryError as e:
69 msg = safe_str(e)
69 msg = safe_str(e)
70 log.exception(msg)
70 log.exception(msg)
71 h.flash(msg, category='warning')
71 h.flash(msg, category='warning')
72 if not partial:
72 if not partial:
73 redirect(h.url('summary_home', repo_name=repo.repo_name))
73 redirect(h.url('summary_home', repo_name=repo.repo_name))
74 raise HTTPBadRequest()
74 raise HTTPBadRequest()
75
75
76 @LoginRequired()
76 @LoginRequired()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 def index(self, repo_name):
79 def index(self, repo_name):
80 c.compare_home = True
80 c.compare_home = True
81 c.commit_ranges = []
81 c.commit_ranges = []
82 c.collapse_all_commits = False
82 c.collapse_all_commits = False
83 c.diffset = None
83 c.diffset = None
84 c.limited_diff = False
84 c.limited_diff = False
85 source_repo = c.rhodecode_db_repo.repo_name
85 source_repo = c.rhodecode_db_repo.repo_name
86 target_repo = request.GET.get('target_repo', source_repo)
86 target_repo = request.GET.get('target_repo', source_repo)
87 c.source_repo = Repository.get_by_repo_name(source_repo)
87 c.source_repo = Repository.get_by_repo_name(source_repo)
88 c.target_repo = Repository.get_by_repo_name(target_repo)
88 c.target_repo = Repository.get_by_repo_name(target_repo)
89 c.source_ref = c.target_ref = _('Select commit')
89 c.source_ref = c.target_ref = _('Select commit')
90 c.source_ref_type = ""
90 c.source_ref_type = ""
91 c.target_ref_type = ""
91 c.target_ref_type = ""
92 c.commit_statuses = ChangesetStatus.STATUSES
92 c.commit_statuses = ChangesetStatus.STATUSES
93 c.preview_mode = False
93 c.preview_mode = False
94 c.file_path = None
94 c.file_path = None
95 return render('compare/compare_diff.html')
95 return render('compare/compare_diff.mako')
96
96
97 @LoginRequired()
97 @LoginRequired()
98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
99 'repository.admin')
99 'repository.admin')
100 def compare(self, repo_name, source_ref_type, source_ref,
100 def compare(self, repo_name, source_ref_type, source_ref,
101 target_ref_type, target_ref):
101 target_ref_type, target_ref):
102 # source_ref will be evaluated in source_repo
102 # source_ref will be evaluated in source_repo
103 source_repo_name = c.rhodecode_db_repo.repo_name
103 source_repo_name = c.rhodecode_db_repo.repo_name
104 source_path, source_id = parse_path_ref(source_ref)
104 source_path, source_id = parse_path_ref(source_ref)
105
105
106 # target_ref will be evaluated in target_repo
106 # target_ref will be evaluated in target_repo
107 target_repo_name = request.GET.get('target_repo', source_repo_name)
107 target_repo_name = request.GET.get('target_repo', source_repo_name)
108 target_path, target_id = parse_path_ref(
108 target_path, target_id = parse_path_ref(
109 target_ref, default_path=request.GET.get('f_path', ''))
109 target_ref, default_path=request.GET.get('f_path', ''))
110
110
111 c.file_path = target_path
111 c.file_path = target_path
112 c.commit_statuses = ChangesetStatus.STATUSES
112 c.commit_statuses = ChangesetStatus.STATUSES
113
113
114 # if merge is True
114 # if merge is True
115 # Show what changes since the shared ancestor commit of target/source
115 # Show what changes since the shared ancestor commit of target/source
116 # the source would get if it was merged with target. Only commits
116 # the source would get if it was merged with target. Only commits
117 # which are in target but not in source will be shown.
117 # which are in target but not in source will be shown.
118 merge = str2bool(request.GET.get('merge'))
118 merge = str2bool(request.GET.get('merge'))
119 # if merge is False
119 # if merge is False
120 # Show a raw diff of source/target refs even if no ancestor exists
120 # Show a raw diff of source/target refs even if no ancestor exists
121
121
122 # c.fulldiff disables cut_off_limit
122 # c.fulldiff disables cut_off_limit
123 c.fulldiff = str2bool(request.GET.get('fulldiff'))
123 c.fulldiff = str2bool(request.GET.get('fulldiff'))
124
124
125 # if partial, returns just compare_commits.html (commits log)
125 # if partial, returns just compare_commits.html (commits log)
126 partial = request.is_xhr
126 partial = request.is_xhr
127
127
128 # swap url for compare_diff page
128 # swap url for compare_diff page
129 c.swap_url = h.url(
129 c.swap_url = h.url(
130 'compare_url',
130 'compare_url',
131 repo_name=target_repo_name,
131 repo_name=target_repo_name,
132 source_ref_type=target_ref_type,
132 source_ref_type=target_ref_type,
133 source_ref=target_ref,
133 source_ref=target_ref,
134 target_repo=source_repo_name,
134 target_repo=source_repo_name,
135 target_ref_type=source_ref_type,
135 target_ref_type=source_ref_type,
136 target_ref=source_ref,
136 target_ref=source_ref,
137 merge=merge and '1' or '',
137 merge=merge and '1' or '',
138 f_path=target_path)
138 f_path=target_path)
139
139
140 source_repo = Repository.get_by_repo_name(source_repo_name)
140 source_repo = Repository.get_by_repo_name(source_repo_name)
141 target_repo = Repository.get_by_repo_name(target_repo_name)
141 target_repo = Repository.get_by_repo_name(target_repo_name)
142
142
143 if source_repo is None:
143 if source_repo is None:
144 msg = _('Could not find the original repo: %(repo)s') % {
144 msg = _('Could not find the original repo: %(repo)s') % {
145 'repo': source_repo}
145 'repo': source_repo}
146
146
147 log.error(msg)
147 log.error(msg)
148 h.flash(msg, category='error')
148 h.flash(msg, category='error')
149 return redirect(url('compare_home', repo_name=c.repo_name))
149 return redirect(url('compare_home', repo_name=c.repo_name))
150
150
151 if target_repo is None:
151 if target_repo is None:
152 msg = _('Could not find the other repo: %(repo)s') % {
152 msg = _('Could not find the other repo: %(repo)s') % {
153 'repo': target_repo_name}
153 'repo': target_repo_name}
154 log.error(msg)
154 log.error(msg)
155 h.flash(msg, category='error')
155 h.flash(msg, category='error')
156 return redirect(url('compare_home', repo_name=c.repo_name))
156 return redirect(url('compare_home', repo_name=c.repo_name))
157
157
158 source_scm = source_repo.scm_instance()
158 source_scm = source_repo.scm_instance()
159 target_scm = target_repo.scm_instance()
159 target_scm = target_repo.scm_instance()
160
160
161 source_alias = source_scm.alias
161 source_alias = source_scm.alias
162 target_alias = target_scm.alias
162 target_alias = target_scm.alias
163 if source_alias != target_alias:
163 if source_alias != target_alias:
164 msg = _('The comparison of two different kinds of remote repos '
164 msg = _('The comparison of two different kinds of remote repos '
165 'is not available')
165 'is not available')
166 log.error(msg)
166 log.error(msg)
167 h.flash(msg, category='error')
167 h.flash(msg, category='error')
168 return redirect(url('compare_home', repo_name=c.repo_name))
168 return redirect(url('compare_home', repo_name=c.repo_name))
169
169
170 source_commit = self._get_commit_or_redirect(
170 source_commit = self._get_commit_or_redirect(
171 ref=source_id, ref_type=source_ref_type, repo=source_repo,
171 ref=source_id, ref_type=source_ref_type, repo=source_repo,
172 partial=partial)
172 partial=partial)
173 target_commit = self._get_commit_or_redirect(
173 target_commit = self._get_commit_or_redirect(
174 ref=target_id, ref_type=target_ref_type, repo=target_repo,
174 ref=target_id, ref_type=target_ref_type, repo=target_repo,
175 partial=partial)
175 partial=partial)
176
176
177 c.compare_home = False
177 c.compare_home = False
178 c.source_repo = source_repo
178 c.source_repo = source_repo
179 c.target_repo = target_repo
179 c.target_repo = target_repo
180 c.source_ref = source_ref
180 c.source_ref = source_ref
181 c.target_ref = target_ref
181 c.target_ref = target_ref
182 c.source_ref_type = source_ref_type
182 c.source_ref_type = source_ref_type
183 c.target_ref_type = target_ref_type
183 c.target_ref_type = target_ref_type
184
184
185 pre_load = ["author", "branch", "date", "message"]
185 pre_load = ["author", "branch", "date", "message"]
186 c.ancestor = None
186 c.ancestor = None
187
187
188 if c.file_path:
188 if c.file_path:
189 if source_commit == target_commit:
189 if source_commit == target_commit:
190 c.commit_ranges = []
190 c.commit_ranges = []
191 else:
191 else:
192 c.commit_ranges = [target_commit]
192 c.commit_ranges = [target_commit]
193 else:
193 else:
194 try:
194 try:
195 c.commit_ranges = source_scm.compare(
195 c.commit_ranges = source_scm.compare(
196 source_commit.raw_id, target_commit.raw_id,
196 source_commit.raw_id, target_commit.raw_id,
197 target_scm, merge, pre_load=pre_load)
197 target_scm, merge, pre_load=pre_load)
198 if merge:
198 if merge:
199 c.ancestor = source_scm.get_common_ancestor(
199 c.ancestor = source_scm.get_common_ancestor(
200 source_commit.raw_id, target_commit.raw_id, target_scm)
200 source_commit.raw_id, target_commit.raw_id, target_scm)
201 except RepositoryRequirementError:
201 except RepositoryRequirementError:
202 msg = _('Could not compare repos with different '
202 msg = _('Could not compare repos with different '
203 'large file settings')
203 'large file settings')
204 log.error(msg)
204 log.error(msg)
205 if partial:
205 if partial:
206 return msg
206 return msg
207 h.flash(msg, category='error')
207 h.flash(msg, category='error')
208 return redirect(url('compare_home', repo_name=c.repo_name))
208 return redirect(url('compare_home', repo_name=c.repo_name))
209
209
210 c.statuses = c.rhodecode_db_repo.statuses(
210 c.statuses = c.rhodecode_db_repo.statuses(
211 [x.raw_id for x in c.commit_ranges])
211 [x.raw_id for x in c.commit_ranges])
212
212
213 # auto collapse if we have more than limit
213 # auto collapse if we have more than limit
214 collapse_limit = diffs.DiffProcessor._collapse_commits_over
214 collapse_limit = diffs.DiffProcessor._collapse_commits_over
215 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
215 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
216
216
217 if partial: # for PR ajax commits loader
217 if partial: # for PR ajax commits loader
218 if not c.ancestor:
218 if not c.ancestor:
219 return '' # cannot merge if there is no ancestor
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 if c.ancestor:
222 if c.ancestor:
223 # case we want a simple diff without incoming commits,
223 # case we want a simple diff without incoming commits,
224 # previewing what will be merged.
224 # previewing what will be merged.
225 # Make the diff on target repo (which is known to have target_ref)
225 # Make the diff on target repo (which is known to have target_ref)
226 log.debug('Using ancestor %s as source_ref instead of %s'
226 log.debug('Using ancestor %s as source_ref instead of %s'
227 % (c.ancestor, source_ref))
227 % (c.ancestor, source_ref))
228 source_repo = target_repo
228 source_repo = target_repo
229 source_commit = target_repo.get_commit(commit_id=c.ancestor)
229 source_commit = target_repo.get_commit(commit_id=c.ancestor)
230
230
231 # diff_limit will cut off the whole diff if the limit is applied
231 # diff_limit will cut off the whole diff if the limit is applied
232 # otherwise it will just hide the big files from the front-end
232 # otherwise it will just hide the big files from the front-end
233 diff_limit = self.cut_off_limit_diff
233 diff_limit = self.cut_off_limit_diff
234 file_limit = self.cut_off_limit_file
234 file_limit = self.cut_off_limit_file
235
235
236 log.debug('calculating diff between '
236 log.debug('calculating diff between '
237 'source_ref:%s and target_ref:%s for repo `%s`',
237 'source_ref:%s and target_ref:%s for repo `%s`',
238 source_commit, target_commit,
238 source_commit, target_commit,
239 safe_unicode(source_repo.scm_instance().path))
239 safe_unicode(source_repo.scm_instance().path))
240
240
241 if source_commit.repository != target_commit.repository:
241 if source_commit.repository != target_commit.repository:
242 msg = _(
242 msg = _(
243 "Repositories unrelated. "
243 "Repositories unrelated. "
244 "Cannot compare commit %(commit1)s from repository %(repo1)s "
244 "Cannot compare commit %(commit1)s from repository %(repo1)s "
245 "with commit %(commit2)s from repository %(repo2)s.") % {
245 "with commit %(commit2)s from repository %(repo2)s.") % {
246 'commit1': h.show_id(source_commit),
246 'commit1': h.show_id(source_commit),
247 'repo1': source_repo.repo_name,
247 'repo1': source_repo.repo_name,
248 'commit2': h.show_id(target_commit),
248 'commit2': h.show_id(target_commit),
249 'repo2': target_repo.repo_name,
249 'repo2': target_repo.repo_name,
250 }
250 }
251 h.flash(msg, category='error')
251 h.flash(msg, category='error')
252 raise HTTPBadRequest()
252 raise HTTPBadRequest()
253
253
254 txtdiff = source_repo.scm_instance().get_diff(
254 txtdiff = source_repo.scm_instance().get_diff(
255 commit1=source_commit, commit2=target_commit,
255 commit1=source_commit, commit2=target_commit,
256 path=target_path, path1=source_path)
256 path=target_path, path1=source_path)
257
257
258 diff_processor = diffs.DiffProcessor(
258 diff_processor = diffs.DiffProcessor(
259 txtdiff, format='newdiff', diff_limit=diff_limit,
259 txtdiff, format='newdiff', diff_limit=diff_limit,
260 file_limit=file_limit, show_full_diff=c.fulldiff)
260 file_limit=file_limit, show_full_diff=c.fulldiff)
261 _parsed = diff_processor.prepare()
261 _parsed = diff_processor.prepare()
262
262
263 def _node_getter(commit):
263 def _node_getter(commit):
264 """ Returns a function that returns a node for a commit or None """
264 """ Returns a function that returns a node for a commit or None """
265 def get_node(fname):
265 def get_node(fname):
266 try:
266 try:
267 return commit.get_node(fname)
267 return commit.get_node(fname)
268 except NodeDoesNotExistError:
268 except NodeDoesNotExistError:
269 return None
269 return None
270 return get_node
270 return get_node
271
271
272 c.diffset = codeblocks.DiffSet(
272 c.diffset = codeblocks.DiffSet(
273 repo_name=source_repo.repo_name,
273 repo_name=source_repo.repo_name,
274 source_node_getter=_node_getter(source_commit),
274 source_node_getter=_node_getter(source_commit),
275 target_node_getter=_node_getter(target_commit),
275 target_node_getter=_node_getter(target_commit),
276 ).render_patchset(_parsed, source_ref, target_ref)
276 ).render_patchset(_parsed, source_ref, target_ref)
277
277
278 c.preview_mode = merge
278 c.preview_mode = merge
279 c.source_commit = source_commit
279 c.source_commit = source_commit
280 c.target_commit = target_commit
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Files controller for RhodeCode Enterprise
22 Files controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 import os
27 import os
28 import shutil
28 import shutil
29 import tempfile
29 import tempfile
30
30
31 from pylons import request, response, tmpl_context as c, url
31 from pylons import request, response, tmpl_context as c, url
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from webob.exc import HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPNotFound, HTTPBadRequest
35
35
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import diffs, helpers as h, caches
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.codeblocks import (
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils import jsonify, action_logger
42 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
43 convert_line_endings, detect_mode, safe_str, str2bool)
43 convert_line_endings, detect_mode, safe_str, str2bool)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 from rhodecode.lib.base import BaseRepoController, render
46 from rhodecode.lib.base import BaseRepoController, render
47 from rhodecode.lib.vcs import path as vcspath
47 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.conf import settings
49 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 NodeDoesNotExistError, CommitError, NodeError)
53 NodeDoesNotExistError, CommitError, NodeError)
54 from rhodecode.lib.vcs.nodes import FileNode
54 from rhodecode.lib.vcs.nodes import FileNode
55
55
56 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
59
59
60 from rhodecode.controllers.changeset import (
60 from rhodecode.controllers.changeset import (
61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 from rhodecode.lib.exceptions import NonRelativePathError
62 from rhodecode.lib.exceptions import NonRelativePathError
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class FilesController(BaseRepoController):
67 class FilesController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(FilesController, self).__before__()
70 super(FilesController, self).__before__()
71 c.cut_off_limit = self.cut_off_limit_file
71 c.cut_off_limit = self.cut_off_limit_file
72
72
73 def _get_default_encoding(self):
73 def _get_default_encoding(self):
74 enc_list = getattr(c, 'default_encodings', [])
74 enc_list = getattr(c, 'default_encodings', [])
75 return enc_list[0] if enc_list else 'UTF-8'
75 return enc_list[0] if enc_list else 'UTF-8'
76
76
77 def __get_commit_or_redirect(self, commit_id, repo_name,
77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 redirect_after=True):
78 redirect_after=True):
79 """
79 """
80 This is a safe way to get commit. If an error occurs it redirects to
80 This is a safe way to get commit. If an error occurs it redirects to
81 tip with proper message
81 tip with proper message
82
82
83 :param commit_id: id of commit to fetch
83 :param commit_id: id of commit to fetch
84 :param repo_name: repo name to redirect after
84 :param repo_name: repo name to redirect after
85 :param redirect_after: toggle redirection
85 :param redirect_after: toggle redirection
86 """
86 """
87 try:
87 try:
88 return c.rhodecode_repo.get_commit(commit_id)
88 return c.rhodecode_repo.get_commit(commit_id)
89 except EmptyRepositoryError:
89 except EmptyRepositoryError:
90 if not redirect_after:
90 if not redirect_after:
91 return None
91 return None
92 url_ = url('files_add_home',
92 url_ = url('files_add_home',
93 repo_name=c.repo_name,
93 repo_name=c.repo_name,
94 revision=0, f_path='', anchor='edit')
94 revision=0, f_path='', anchor='edit')
95 if h.HasRepoPermissionAny(
95 if h.HasRepoPermissionAny(
96 'repository.write', 'repository.admin')(c.repo_name):
96 'repository.write', 'repository.admin')(c.repo_name):
97 add_new = h.link_to(
97 add_new = h.link_to(
98 _('Click here to add a new file.'),
98 _('Click here to add a new file.'),
99 url_, class_="alert-link")
99 url_, class_="alert-link")
100 else:
100 else:
101 add_new = ""
101 add_new = ""
102 h.flash(h.literal(
102 h.flash(h.literal(
103 _('There are no files yet. %s') % add_new), category='warning')
103 _('There are no files yet. %s') % add_new), category='warning')
104 redirect(h.url('summary_home', repo_name=repo_name))
104 redirect(h.url('summary_home', repo_name=repo_name))
105 except (CommitDoesNotExistError, LookupError):
105 except (CommitDoesNotExistError, LookupError):
106 msg = _('No such commit exists for this repository')
106 msg = _('No such commit exists for this repository')
107 h.flash(msg, category='error')
107 h.flash(msg, category='error')
108 raise HTTPNotFound()
108 raise HTTPNotFound()
109 except RepositoryError as e:
109 except RepositoryError as e:
110 h.flash(safe_str(e), category='error')
110 h.flash(safe_str(e), category='error')
111 raise HTTPNotFound()
111 raise HTTPNotFound()
112
112
113 def __get_filenode_or_redirect(self, repo_name, commit, path):
113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 """
114 """
115 Returns file_node, if error occurs or given path is directory,
115 Returns file_node, if error occurs or given path is directory,
116 it'll redirect to top level path
116 it'll redirect to top level path
117
117
118 :param repo_name: repo_name
118 :param repo_name: repo_name
119 :param commit: given commit
119 :param commit: given commit
120 :param path: path to lookup
120 :param path: path to lookup
121 """
121 """
122 try:
122 try:
123 file_node = commit.get_node(path)
123 file_node = commit.get_node(path)
124 if file_node.is_dir():
124 if file_node.is_dir():
125 raise RepositoryError('The given path is a directory')
125 raise RepositoryError('The given path is a directory')
126 except CommitDoesNotExistError:
126 except CommitDoesNotExistError:
127 msg = _('No such commit exists for this repository')
127 msg = _('No such commit exists for this repository')
128 log.exception(msg)
128 log.exception(msg)
129 h.flash(msg, category='error')
129 h.flash(msg, category='error')
130 raise HTTPNotFound()
130 raise HTTPNotFound()
131 except RepositoryError as e:
131 except RepositoryError as e:
132 h.flash(safe_str(e), category='error')
132 h.flash(safe_str(e), category='error')
133 raise HTTPNotFound()
133 raise HTTPNotFound()
134
134
135 return file_node
135 return file_node
136
136
137 def __get_tree_cache_manager(self, repo_name, namespace_type):
137 def __get_tree_cache_manager(self, repo_name, namespace_type):
138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
139 return caches.get_cache_manager('repo_cache_long', _namespace)
139 return caches.get_cache_manager('repo_cache_long', _namespace)
140
140
141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
142 full_load=False, force=False):
142 full_load=False, force=False):
143 def _cached_tree():
143 def _cached_tree():
144 log.debug('Generating cached file tree for %s, %s, %s',
144 log.debug('Generating cached file tree for %s, %s, %s',
145 repo_name, commit_id, f_path)
145 repo_name, commit_id, f_path)
146 c.full_load = full_load
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 cache_manager = self.__get_tree_cache_manager(
149 cache_manager = self.__get_tree_cache_manager(
150 repo_name, caches.FILE_TREE)
150 repo_name, caches.FILE_TREE)
151
151
152 cache_key = caches.compute_key_from_params(
152 cache_key = caches.compute_key_from_params(
153 repo_name, commit_id, f_path)
153 repo_name, commit_id, f_path)
154
154
155 if force:
155 if force:
156 # we want to force recompute of caches
156 # we want to force recompute of caches
157 cache_manager.remove_value(cache_key)
157 cache_manager.remove_value(cache_key)
158
158
159 return cache_manager.get(cache_key, createfunc=_cached_tree)
159 return cache_manager.get(cache_key, createfunc=_cached_tree)
160
160
161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
162 def _cached_nodes():
162 def _cached_nodes():
163 log.debug('Generating cached nodelist for %s, %s, %s',
163 log.debug('Generating cached nodelist for %s, %s, %s',
164 repo_name, commit_id, f_path)
164 repo_name, commit_id, f_path)
165 _d, _f = ScmModel().get_nodes(
165 _d, _f = ScmModel().get_nodes(
166 repo_name, commit_id, f_path, flat=False)
166 repo_name, commit_id, f_path, flat=False)
167 return _d + _f
167 return _d + _f
168
168
169 cache_manager = self.__get_tree_cache_manager(
169 cache_manager = self.__get_tree_cache_manager(
170 repo_name, caches.FILE_SEARCH_TREE_META)
170 repo_name, caches.FILE_SEARCH_TREE_META)
171
171
172 cache_key = caches.compute_key_from_params(
172 cache_key = caches.compute_key_from_params(
173 repo_name, commit_id, f_path)
173 repo_name, commit_id, f_path)
174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
175
175
176 @LoginRequired()
176 @LoginRequired()
177 @HasRepoPermissionAnyDecorator(
177 @HasRepoPermissionAnyDecorator(
178 'repository.read', 'repository.write', 'repository.admin')
178 'repository.read', 'repository.write', 'repository.admin')
179 def index(
179 def index(
180 self, repo_name, revision, f_path, annotate=False, rendered=False):
180 self, repo_name, revision, f_path, annotate=False, rendered=False):
181 commit_id = revision
181 commit_id = revision
182
182
183 # redirect to given commit_id from form if given
183 # redirect to given commit_id from form if given
184 get_commit_id = request.GET.get('at_rev', None)
184 get_commit_id = request.GET.get('at_rev', None)
185 if get_commit_id:
185 if get_commit_id:
186 self.__get_commit_or_redirect(get_commit_id, repo_name)
186 self.__get_commit_or_redirect(get_commit_id, repo_name)
187
187
188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
189 c.branch = request.GET.get('branch', None)
189 c.branch = request.GET.get('branch', None)
190 c.f_path = f_path
190 c.f_path = f_path
191 c.annotate = annotate
191 c.annotate = annotate
192 # default is false, but .rst/.md files later are autorendered, we can
192 # default is false, but .rst/.md files later are autorendered, we can
193 # overwrite autorendering by setting this GET flag
193 # overwrite autorendering by setting this GET flag
194 c.renderer = rendered or not request.GET.get('no-render', False)
194 c.renderer = rendered or not request.GET.get('no-render', False)
195
195
196 # prev link
196 # prev link
197 try:
197 try:
198 prev_commit = c.commit.prev(c.branch)
198 prev_commit = c.commit.prev(c.branch)
199 c.prev_commit = prev_commit
199 c.prev_commit = prev_commit
200 c.url_prev = url('files_home', repo_name=c.repo_name,
200 c.url_prev = url('files_home', repo_name=c.repo_name,
201 revision=prev_commit.raw_id, f_path=f_path)
201 revision=prev_commit.raw_id, f_path=f_path)
202 if c.branch:
202 if c.branch:
203 c.url_prev += '?branch=%s' % c.branch
203 c.url_prev += '?branch=%s' % c.branch
204 except (CommitDoesNotExistError, VCSError):
204 except (CommitDoesNotExistError, VCSError):
205 c.url_prev = '#'
205 c.url_prev = '#'
206 c.prev_commit = EmptyCommit()
206 c.prev_commit = EmptyCommit()
207
207
208 # next link
208 # next link
209 try:
209 try:
210 next_commit = c.commit.next(c.branch)
210 next_commit = c.commit.next(c.branch)
211 c.next_commit = next_commit
211 c.next_commit = next_commit
212 c.url_next = url('files_home', repo_name=c.repo_name,
212 c.url_next = url('files_home', repo_name=c.repo_name,
213 revision=next_commit.raw_id, f_path=f_path)
213 revision=next_commit.raw_id, f_path=f_path)
214 if c.branch:
214 if c.branch:
215 c.url_next += '?branch=%s' % c.branch
215 c.url_next += '?branch=%s' % c.branch
216 except (CommitDoesNotExistError, VCSError):
216 except (CommitDoesNotExistError, VCSError):
217 c.url_next = '#'
217 c.url_next = '#'
218 c.next_commit = EmptyCommit()
218 c.next_commit = EmptyCommit()
219
219
220 # files or dirs
220 # files or dirs
221 try:
221 try:
222 c.file = c.commit.get_node(f_path)
222 c.file = c.commit.get_node(f_path)
223 c.file_author = True
223 c.file_author = True
224 c.file_tree = ''
224 c.file_tree = ''
225 if c.file.is_file():
225 if c.file.is_file():
226 c.file_source_page = 'true'
226 c.file_source_page = 'true'
227 c.file_last_commit = c.file.last_commit
227 c.file_last_commit = c.file.last_commit
228 if c.file.size < self.cut_off_limit_file:
228 if c.file.size < self.cut_off_limit_file:
229 if c.annotate: # annotation has precedence over renderer
229 if c.annotate: # annotation has precedence over renderer
230 c.annotated_lines = filenode_as_annotated_lines_tokens(
230 c.annotated_lines = filenode_as_annotated_lines_tokens(
231 c.file
231 c.file
232 )
232 )
233 else:
233 else:
234 c.renderer = (
234 c.renderer = (
235 c.renderer and h.renderer_from_filename(c.file.path)
235 c.renderer and h.renderer_from_filename(c.file.path)
236 )
236 )
237 if not c.renderer:
237 if not c.renderer:
238 c.lines = filenode_as_lines_tokens(c.file)
238 c.lines = filenode_as_lines_tokens(c.file)
239
239
240 c.on_branch_head = self._is_valid_head(
240 c.on_branch_head = self._is_valid_head(
241 commit_id, c.rhodecode_repo)
241 commit_id, c.rhodecode_repo)
242 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
242 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
243
243
244 author = c.file_last_commit.author
244 author = c.file_last_commit.author
245 c.authors = [(h.email(author),
245 c.authors = [(h.email(author),
246 h.person(author, 'username_or_name_or_email'))]
246 h.person(author, 'username_or_name_or_email'))]
247 else:
247 else:
248 c.file_source_page = 'false'
248 c.file_source_page = 'false'
249 c.authors = []
249 c.authors = []
250 c.file_tree = self._get_tree_at_commit(
250 c.file_tree = self._get_tree_at_commit(
251 repo_name, c.commit.raw_id, f_path)
251 repo_name, c.commit.raw_id, f_path)
252
252
253 except RepositoryError as e:
253 except RepositoryError as e:
254 h.flash(safe_str(e), category='error')
254 h.flash(safe_str(e), category='error')
255 raise HTTPNotFound()
255 raise HTTPNotFound()
256
256
257 if request.environ.get('HTTP_X_PJAX'):
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 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
263 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
264 'repository.admin')
264 'repository.admin')
265 @jsonify
265 @jsonify
266 def history(self, repo_name, revision, f_path):
266 def history(self, repo_name, revision, f_path):
267 commit = self.__get_commit_or_redirect(revision, repo_name)
267 commit = self.__get_commit_or_redirect(revision, repo_name)
268 f_path = f_path
268 f_path = f_path
269 _file = commit.get_node(f_path)
269 _file = commit.get_node(f_path)
270 if _file.is_file():
270 if _file.is_file():
271 file_history, _hist = self._get_node_history(commit, f_path)
271 file_history, _hist = self._get_node_history(commit, f_path)
272
272
273 res = []
273 res = []
274 for obj in file_history:
274 for obj in file_history:
275 res.append({
275 res.append({
276 'text': obj[1],
276 'text': obj[1],
277 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
277 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
278 })
278 })
279
279
280 data = {
280 data = {
281 'more': False,
281 'more': False,
282 'results': res
282 'results': res
283 }
283 }
284 return data
284 return data
285
285
286 @LoginRequired()
286 @LoginRequired()
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 'repository.admin')
288 'repository.admin')
289 def authors(self, repo_name, revision, f_path):
289 def authors(self, repo_name, revision, f_path):
290 commit = self.__get_commit_or_redirect(revision, repo_name)
290 commit = self.__get_commit_or_redirect(revision, repo_name)
291 file_node = commit.get_node(f_path)
291 file_node = commit.get_node(f_path)
292 if file_node.is_file():
292 if file_node.is_file():
293 c.file_last_commit = file_node.last_commit
293 c.file_last_commit = file_node.last_commit
294 if request.GET.get('annotate') == '1':
294 if request.GET.get('annotate') == '1':
295 # use _hist from annotation if annotation mode is on
295 # use _hist from annotation if annotation mode is on
296 commit_ids = set(x[1] for x in file_node.annotate)
296 commit_ids = set(x[1] for x in file_node.annotate)
297 _hist = (
297 _hist = (
298 c.rhodecode_repo.get_commit(commit_id)
298 c.rhodecode_repo.get_commit(commit_id)
299 for commit_id in commit_ids)
299 for commit_id in commit_ids)
300 else:
300 else:
301 _f_history, _hist = self._get_node_history(commit, f_path)
301 _f_history, _hist = self._get_node_history(commit, f_path)
302 c.file_author = False
302 c.file_author = False
303 c.authors = []
303 c.authors = []
304 for author in set(commit.author for commit in _hist):
304 for author in set(commit.author for commit in _hist):
305 c.authors.append((
305 c.authors.append((
306 h.email(author),
306 h.email(author),
307 h.person(author, 'username_or_name_or_email')))
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 @LoginRequired()
310 @LoginRequired()
311 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 'repository.admin')
312 'repository.admin')
313 def rawfile(self, repo_name, revision, f_path):
313 def rawfile(self, repo_name, revision, f_path):
314 """
314 """
315 Action for download as raw
315 Action for download as raw
316 """
316 """
317 commit = self.__get_commit_or_redirect(revision, repo_name)
317 commit = self.__get_commit_or_redirect(revision, repo_name)
318 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
318 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
319
319
320 response.content_disposition = 'attachment; filename=%s' % \
320 response.content_disposition = 'attachment; filename=%s' % \
321 safe_str(f_path.split(Repository.NAME_SEP)[-1])
321 safe_str(f_path.split(Repository.NAME_SEP)[-1])
322
322
323 response.content_type = file_node.mimetype
323 response.content_type = file_node.mimetype
324 charset = self._get_default_encoding()
324 charset = self._get_default_encoding()
325 if charset:
325 if charset:
326 response.charset = charset
326 response.charset = charset
327
327
328 return file_node.content
328 return file_node.content
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 'repository.admin')
332 'repository.admin')
333 def raw(self, repo_name, revision, f_path):
333 def raw(self, repo_name, revision, f_path):
334 """
334 """
335 Action for show as raw, some mimetypes are "rendered",
335 Action for show as raw, some mimetypes are "rendered",
336 those include images, icons.
336 those include images, icons.
337 """
337 """
338 commit = self.__get_commit_or_redirect(revision, repo_name)
338 commit = self.__get_commit_or_redirect(revision, repo_name)
339 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
339 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
340
340
341 raw_mimetype_mapping = {
341 raw_mimetype_mapping = {
342 # map original mimetype to a mimetype used for "show as raw"
342 # map original mimetype to a mimetype used for "show as raw"
343 # you can also provide a content-disposition to override the
343 # you can also provide a content-disposition to override the
344 # default "attachment" disposition.
344 # default "attachment" disposition.
345 # orig_type: (new_type, new_dispo)
345 # orig_type: (new_type, new_dispo)
346
346
347 # show images inline:
347 # show images inline:
348 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
348 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
349 # for example render an SVG with javascript inside or even render
349 # for example render an SVG with javascript inside or even render
350 # HTML.
350 # HTML.
351 'image/x-icon': ('image/x-icon', 'inline'),
351 'image/x-icon': ('image/x-icon', 'inline'),
352 'image/png': ('image/png', 'inline'),
352 'image/png': ('image/png', 'inline'),
353 'image/gif': ('image/gif', 'inline'),
353 'image/gif': ('image/gif', 'inline'),
354 'image/jpeg': ('image/jpeg', 'inline'),
354 'image/jpeg': ('image/jpeg', 'inline'),
355 }
355 }
356
356
357 mimetype = file_node.mimetype
357 mimetype = file_node.mimetype
358 try:
358 try:
359 mimetype, dispo = raw_mimetype_mapping[mimetype]
359 mimetype, dispo = raw_mimetype_mapping[mimetype]
360 except KeyError:
360 except KeyError:
361 # we don't know anything special about this, handle it safely
361 # we don't know anything special about this, handle it safely
362 if file_node.is_binary:
362 if file_node.is_binary:
363 # do same as download raw for binary files
363 # do same as download raw for binary files
364 mimetype, dispo = 'application/octet-stream', 'attachment'
364 mimetype, dispo = 'application/octet-stream', 'attachment'
365 else:
365 else:
366 # do not just use the original mimetype, but force text/plain,
366 # do not just use the original mimetype, but force text/plain,
367 # otherwise it would serve text/html and that might be unsafe.
367 # otherwise it would serve text/html and that might be unsafe.
368 # Note: underlying vcs library fakes text/plain mimetype if the
368 # Note: underlying vcs library fakes text/plain mimetype if the
369 # mimetype can not be determined and it thinks it is not
369 # mimetype can not be determined and it thinks it is not
370 # binary.This might lead to erroneous text display in some
370 # binary.This might lead to erroneous text display in some
371 # cases, but helps in other cases, like with text files
371 # cases, but helps in other cases, like with text files
372 # without extension.
372 # without extension.
373 mimetype, dispo = 'text/plain', 'inline'
373 mimetype, dispo = 'text/plain', 'inline'
374
374
375 if dispo == 'attachment':
375 if dispo == 'attachment':
376 dispo = 'attachment; filename=%s' % safe_str(
376 dispo = 'attachment; filename=%s' % safe_str(
377 f_path.split(os.sep)[-1])
377 f_path.split(os.sep)[-1])
378
378
379 response.content_disposition = dispo
379 response.content_disposition = dispo
380 response.content_type = mimetype
380 response.content_type = mimetype
381 charset = self._get_default_encoding()
381 charset = self._get_default_encoding()
382 if charset:
382 if charset:
383 response.charset = charset
383 response.charset = charset
384 return file_node.content
384 return file_node.content
385
385
386 @CSRFRequired()
386 @CSRFRequired()
387 @LoginRequired()
387 @LoginRequired()
388 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
388 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
389 def delete(self, repo_name, revision, f_path):
389 def delete(self, repo_name, revision, f_path):
390 commit_id = revision
390 commit_id = revision
391
391
392 repo = c.rhodecode_db_repo
392 repo = c.rhodecode_db_repo
393 if repo.enable_locking and repo.locked[0]:
393 if repo.enable_locking and repo.locked[0]:
394 h.flash(_('This repository has been locked by %s on %s')
394 h.flash(_('This repository has been locked by %s on %s')
395 % (h.person_by_id(repo.locked[0]),
395 % (h.person_by_id(repo.locked[0]),
396 h.format_date(h.time_to_datetime(repo.locked[1]))),
396 h.format_date(h.time_to_datetime(repo.locked[1]))),
397 'warning')
397 'warning')
398 return redirect(h.url('files_home',
398 return redirect(h.url('files_home',
399 repo_name=repo_name, revision='tip'))
399 repo_name=repo_name, revision='tip'))
400
400
401 if not self._is_valid_head(commit_id, repo.scm_instance()):
401 if not self._is_valid_head(commit_id, repo.scm_instance()):
402 h.flash(_('You can only delete files with revision '
402 h.flash(_('You can only delete files with revision '
403 'being a valid branch '), category='warning')
403 'being a valid branch '), category='warning')
404 return redirect(h.url('files_home',
404 return redirect(h.url('files_home',
405 repo_name=repo_name, revision='tip',
405 repo_name=repo_name, revision='tip',
406 f_path=f_path))
406 f_path=f_path))
407
407
408 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
408 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
409 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
409 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
410
410
411 c.default_message = _(
411 c.default_message = _(
412 'Deleted file %s via RhodeCode Enterprise') % (f_path)
412 'Deleted file %s via RhodeCode Enterprise') % (f_path)
413 c.f_path = f_path
413 c.f_path = f_path
414 node_path = f_path
414 node_path = f_path
415 author = c.rhodecode_user.full_contact
415 author = c.rhodecode_user.full_contact
416 message = request.POST.get('message') or c.default_message
416 message = request.POST.get('message') or c.default_message
417 try:
417 try:
418 nodes = {
418 nodes = {
419 node_path: {
419 node_path: {
420 'content': ''
420 'content': ''
421 }
421 }
422 }
422 }
423 self.scm_model.delete_nodes(
423 self.scm_model.delete_nodes(
424 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
424 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
425 message=message,
425 message=message,
426 nodes=nodes,
426 nodes=nodes,
427 parent_commit=c.commit,
427 parent_commit=c.commit,
428 author=author,
428 author=author,
429 )
429 )
430
430
431 h.flash(_('Successfully deleted file %s') % f_path,
431 h.flash(_('Successfully deleted file %s') % f_path,
432 category='success')
432 category='success')
433 except Exception:
433 except Exception:
434 msg = _('Error occurred during commit')
434 msg = _('Error occurred during commit')
435 log.exception(msg)
435 log.exception(msg)
436 h.flash(msg, category='error')
436 h.flash(msg, category='error')
437 return redirect(url('changeset_home',
437 return redirect(url('changeset_home',
438 repo_name=c.repo_name, revision='tip'))
438 repo_name=c.repo_name, revision='tip'))
439
439
440 @LoginRequired()
440 @LoginRequired()
441 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
441 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
442 def delete_home(self, repo_name, revision, f_path):
442 def delete_home(self, repo_name, revision, f_path):
443 commit_id = revision
443 commit_id = revision
444
444
445 repo = c.rhodecode_db_repo
445 repo = c.rhodecode_db_repo
446 if repo.enable_locking and repo.locked[0]:
446 if repo.enable_locking and repo.locked[0]:
447 h.flash(_('This repository has been locked by %s on %s')
447 h.flash(_('This repository has been locked by %s on %s')
448 % (h.person_by_id(repo.locked[0]),
448 % (h.person_by_id(repo.locked[0]),
449 h.format_date(h.time_to_datetime(repo.locked[1]))),
449 h.format_date(h.time_to_datetime(repo.locked[1]))),
450 'warning')
450 'warning')
451 return redirect(h.url('files_home',
451 return redirect(h.url('files_home',
452 repo_name=repo_name, revision='tip'))
452 repo_name=repo_name, revision='tip'))
453
453
454 if not self._is_valid_head(commit_id, repo.scm_instance()):
454 if not self._is_valid_head(commit_id, repo.scm_instance()):
455 h.flash(_('You can only delete files with revision '
455 h.flash(_('You can only delete files with revision '
456 'being a valid branch '), category='warning')
456 'being a valid branch '), category='warning')
457 return redirect(h.url('files_home',
457 return redirect(h.url('files_home',
458 repo_name=repo_name, revision='tip',
458 repo_name=repo_name, revision='tip',
459 f_path=f_path))
459 f_path=f_path))
460
460
461 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
461 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
462 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
462 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
463
463
464 c.default_message = _(
464 c.default_message = _(
465 'Deleted file %s via RhodeCode Enterprise') % (f_path)
465 'Deleted file %s via RhodeCode Enterprise') % (f_path)
466 c.f_path = f_path
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 @CSRFRequired()
470 @CSRFRequired()
471 @LoginRequired()
471 @LoginRequired()
472 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
472 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
473 def edit(self, repo_name, revision, f_path):
473 def edit(self, repo_name, revision, f_path):
474 commit_id = revision
474 commit_id = revision
475
475
476 repo = c.rhodecode_db_repo
476 repo = c.rhodecode_db_repo
477 if repo.enable_locking and repo.locked[0]:
477 if repo.enable_locking and repo.locked[0]:
478 h.flash(_('This repository has been locked by %s on %s')
478 h.flash(_('This repository has been locked by %s on %s')
479 % (h.person_by_id(repo.locked[0]),
479 % (h.person_by_id(repo.locked[0]),
480 h.format_date(h.time_to_datetime(repo.locked[1]))),
480 h.format_date(h.time_to_datetime(repo.locked[1]))),
481 'warning')
481 'warning')
482 return redirect(h.url('files_home',
482 return redirect(h.url('files_home',
483 repo_name=repo_name, revision='tip'))
483 repo_name=repo_name, revision='tip'))
484
484
485 if not self._is_valid_head(commit_id, repo.scm_instance()):
485 if not self._is_valid_head(commit_id, repo.scm_instance()):
486 h.flash(_('You can only edit files with revision '
486 h.flash(_('You can only edit files with revision '
487 'being a valid branch '), category='warning')
487 'being a valid branch '), category='warning')
488 return redirect(h.url('files_home',
488 return redirect(h.url('files_home',
489 repo_name=repo_name, revision='tip',
489 repo_name=repo_name, revision='tip',
490 f_path=f_path))
490 f_path=f_path))
491
491
492 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
492 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
493 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
493 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
494
494
495 if c.file.is_binary:
495 if c.file.is_binary:
496 return redirect(url('files_home', repo_name=c.repo_name,
496 return redirect(url('files_home', repo_name=c.repo_name,
497 revision=c.commit.raw_id, f_path=f_path))
497 revision=c.commit.raw_id, f_path=f_path))
498 c.default_message = _(
498 c.default_message = _(
499 'Edited file %s via RhodeCode Enterprise') % (f_path)
499 'Edited file %s via RhodeCode Enterprise') % (f_path)
500 c.f_path = f_path
500 c.f_path = f_path
501 old_content = c.file.content
501 old_content = c.file.content
502 sl = old_content.splitlines(1)
502 sl = old_content.splitlines(1)
503 first_line = sl[0] if sl else ''
503 first_line = sl[0] if sl else ''
504
504
505 # modes: 0 - Unix, 1 - Mac, 2 - DOS
505 # modes: 0 - Unix, 1 - Mac, 2 - DOS
506 mode = detect_mode(first_line, 0)
506 mode = detect_mode(first_line, 0)
507 content = convert_line_endings(request.POST.get('content', ''), mode)
507 content = convert_line_endings(request.POST.get('content', ''), mode)
508
508
509 message = request.POST.get('message') or c.default_message
509 message = request.POST.get('message') or c.default_message
510 org_f_path = c.file.unicode_path
510 org_f_path = c.file.unicode_path
511 filename = request.POST['filename']
511 filename = request.POST['filename']
512 org_filename = c.file.name
512 org_filename = c.file.name
513
513
514 if content == old_content and filename == org_filename:
514 if content == old_content and filename == org_filename:
515 h.flash(_('No changes'), category='warning')
515 h.flash(_('No changes'), category='warning')
516 return redirect(url('changeset_home', repo_name=c.repo_name,
516 return redirect(url('changeset_home', repo_name=c.repo_name,
517 revision='tip'))
517 revision='tip'))
518 try:
518 try:
519 mapping = {
519 mapping = {
520 org_f_path: {
520 org_f_path: {
521 'org_filename': org_f_path,
521 'org_filename': org_f_path,
522 'filename': os.path.join(c.file.dir_path, filename),
522 'filename': os.path.join(c.file.dir_path, filename),
523 'content': content,
523 'content': content,
524 'lexer': '',
524 'lexer': '',
525 'op': 'mod',
525 'op': 'mod',
526 }
526 }
527 }
527 }
528
528
529 ScmModel().update_nodes(
529 ScmModel().update_nodes(
530 user=c.rhodecode_user.user_id,
530 user=c.rhodecode_user.user_id,
531 repo=c.rhodecode_db_repo,
531 repo=c.rhodecode_db_repo,
532 message=message,
532 message=message,
533 nodes=mapping,
533 nodes=mapping,
534 parent_commit=c.commit,
534 parent_commit=c.commit,
535 )
535 )
536
536
537 h.flash(_('Successfully committed to %s') % f_path,
537 h.flash(_('Successfully committed to %s') % f_path,
538 category='success')
538 category='success')
539 except Exception:
539 except Exception:
540 msg = _('Error occurred during commit')
540 msg = _('Error occurred during commit')
541 log.exception(msg)
541 log.exception(msg)
542 h.flash(msg, category='error')
542 h.flash(msg, category='error')
543 return redirect(url('changeset_home',
543 return redirect(url('changeset_home',
544 repo_name=c.repo_name, revision='tip'))
544 repo_name=c.repo_name, revision='tip'))
545
545
546 @LoginRequired()
546 @LoginRequired()
547 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
547 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
548 def edit_home(self, repo_name, revision, f_path):
548 def edit_home(self, repo_name, revision, f_path):
549 commit_id = revision
549 commit_id = revision
550
550
551 repo = c.rhodecode_db_repo
551 repo = c.rhodecode_db_repo
552 if repo.enable_locking and repo.locked[0]:
552 if repo.enable_locking and repo.locked[0]:
553 h.flash(_('This repository has been locked by %s on %s')
553 h.flash(_('This repository has been locked by %s on %s')
554 % (h.person_by_id(repo.locked[0]),
554 % (h.person_by_id(repo.locked[0]),
555 h.format_date(h.time_to_datetime(repo.locked[1]))),
555 h.format_date(h.time_to_datetime(repo.locked[1]))),
556 'warning')
556 'warning')
557 return redirect(h.url('files_home',
557 return redirect(h.url('files_home',
558 repo_name=repo_name, revision='tip'))
558 repo_name=repo_name, revision='tip'))
559
559
560 if not self._is_valid_head(commit_id, repo.scm_instance()):
560 if not self._is_valid_head(commit_id, repo.scm_instance()):
561 h.flash(_('You can only edit files with revision '
561 h.flash(_('You can only edit files with revision '
562 'being a valid branch '), category='warning')
562 'being a valid branch '), category='warning')
563 return redirect(h.url('files_home',
563 return redirect(h.url('files_home',
564 repo_name=repo_name, revision='tip',
564 repo_name=repo_name, revision='tip',
565 f_path=f_path))
565 f_path=f_path))
566
566
567 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
567 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
568 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
568 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
569
569
570 if c.file.is_binary:
570 if c.file.is_binary:
571 return redirect(url('files_home', repo_name=c.repo_name,
571 return redirect(url('files_home', repo_name=c.repo_name,
572 revision=c.commit.raw_id, f_path=f_path))
572 revision=c.commit.raw_id, f_path=f_path))
573 c.default_message = _(
573 c.default_message = _(
574 'Edited file %s via RhodeCode Enterprise') % (f_path)
574 'Edited file %s via RhodeCode Enterprise') % (f_path)
575 c.f_path = f_path
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 def _is_valid_head(self, commit_id, repo):
579 def _is_valid_head(self, commit_id, repo):
580 # check if commit is a branch identifier- basically we cannot
580 # check if commit is a branch identifier- basically we cannot
581 # create multiple heads via file editing
581 # create multiple heads via file editing
582 valid_heads = repo.branches.keys() + repo.branches.values()
582 valid_heads = repo.branches.keys() + repo.branches.values()
583
583
584 if h.is_svn(repo) and not repo.is_empty():
584 if h.is_svn(repo) and not repo.is_empty():
585 # Note: Subversion only has one head, we add it here in case there
585 # Note: Subversion only has one head, we add it here in case there
586 # is no branch matched.
586 # is no branch matched.
587 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
587 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
588
588
589 # check if commit is a branch name or branch hash
589 # check if commit is a branch name or branch hash
590 return commit_id in valid_heads
590 return commit_id in valid_heads
591
591
592 @CSRFRequired()
592 @CSRFRequired()
593 @LoginRequired()
593 @LoginRequired()
594 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
594 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
595 def add(self, repo_name, revision, f_path):
595 def add(self, repo_name, revision, f_path):
596 repo = Repository.get_by_repo_name(repo_name)
596 repo = Repository.get_by_repo_name(repo_name)
597 if repo.enable_locking and repo.locked[0]:
597 if repo.enable_locking and repo.locked[0]:
598 h.flash(_('This repository has been locked by %s on %s')
598 h.flash(_('This repository has been locked by %s on %s')
599 % (h.person_by_id(repo.locked[0]),
599 % (h.person_by_id(repo.locked[0]),
600 h.format_date(h.time_to_datetime(repo.locked[1]))),
600 h.format_date(h.time_to_datetime(repo.locked[1]))),
601 'warning')
601 'warning')
602 return redirect(h.url('files_home',
602 return redirect(h.url('files_home',
603 repo_name=repo_name, revision='tip'))
603 repo_name=repo_name, revision='tip'))
604
604
605 r_post = request.POST
605 r_post = request.POST
606
606
607 c.commit = self.__get_commit_or_redirect(
607 c.commit = self.__get_commit_or_redirect(
608 revision, repo_name, redirect_after=False)
608 revision, repo_name, redirect_after=False)
609 if c.commit is None:
609 if c.commit is None:
610 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
610 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
611 c.default_message = (_('Added file via RhodeCode Enterprise'))
611 c.default_message = (_('Added file via RhodeCode Enterprise'))
612 c.f_path = f_path
612 c.f_path = f_path
613 unix_mode = 0
613 unix_mode = 0
614 content = convert_line_endings(r_post.get('content', ''), unix_mode)
614 content = convert_line_endings(r_post.get('content', ''), unix_mode)
615
615
616 message = r_post.get('message') or c.default_message
616 message = r_post.get('message') or c.default_message
617 filename = r_post.get('filename')
617 filename = r_post.get('filename')
618 location = r_post.get('location', '') # dir location
618 location = r_post.get('location', '') # dir location
619 file_obj = r_post.get('upload_file', None)
619 file_obj = r_post.get('upload_file', None)
620
620
621 if file_obj is not None and hasattr(file_obj, 'filename'):
621 if file_obj is not None and hasattr(file_obj, 'filename'):
622 filename = file_obj.filename
622 filename = file_obj.filename
623 content = file_obj.file
623 content = file_obj.file
624
624
625 if hasattr(content, 'file'):
625 if hasattr(content, 'file'):
626 # non posix systems store real file under file attr
626 # non posix systems store real file under file attr
627 content = content.file
627 content = content.file
628
628
629 # If there's no commit, redirect to repo summary
629 # If there's no commit, redirect to repo summary
630 if type(c.commit) is EmptyCommit:
630 if type(c.commit) is EmptyCommit:
631 redirect_url = "summary_home"
631 redirect_url = "summary_home"
632 else:
632 else:
633 redirect_url = "changeset_home"
633 redirect_url = "changeset_home"
634
634
635 if not filename:
635 if not filename:
636 h.flash(_('No filename'), category='warning')
636 h.flash(_('No filename'), category='warning')
637 return redirect(url(redirect_url, repo_name=c.repo_name,
637 return redirect(url(redirect_url, repo_name=c.repo_name,
638 revision='tip'))
638 revision='tip'))
639
639
640 # extract the location from filename,
640 # extract the location from filename,
641 # allows using foo/bar.txt syntax to create subdirectories
641 # allows using foo/bar.txt syntax to create subdirectories
642 subdir_loc = filename.rsplit('/', 1)
642 subdir_loc = filename.rsplit('/', 1)
643 if len(subdir_loc) == 2:
643 if len(subdir_loc) == 2:
644 location = os.path.join(location, subdir_loc[0])
644 location = os.path.join(location, subdir_loc[0])
645
645
646 # strip all crap out of file, just leave the basename
646 # strip all crap out of file, just leave the basename
647 filename = os.path.basename(filename)
647 filename = os.path.basename(filename)
648 node_path = os.path.join(location, filename)
648 node_path = os.path.join(location, filename)
649 author = c.rhodecode_user.full_contact
649 author = c.rhodecode_user.full_contact
650
650
651 try:
651 try:
652 nodes = {
652 nodes = {
653 node_path: {
653 node_path: {
654 'content': content
654 'content': content
655 }
655 }
656 }
656 }
657 self.scm_model.create_nodes(
657 self.scm_model.create_nodes(
658 user=c.rhodecode_user.user_id,
658 user=c.rhodecode_user.user_id,
659 repo=c.rhodecode_db_repo,
659 repo=c.rhodecode_db_repo,
660 message=message,
660 message=message,
661 nodes=nodes,
661 nodes=nodes,
662 parent_commit=c.commit,
662 parent_commit=c.commit,
663 author=author,
663 author=author,
664 )
664 )
665
665
666 h.flash(_('Successfully committed to %s') % node_path,
666 h.flash(_('Successfully committed to %s') % node_path,
667 category='success')
667 category='success')
668 except NonRelativePathError as e:
668 except NonRelativePathError as e:
669 h.flash(_(
669 h.flash(_(
670 'The location specified must be a relative path and must not '
670 'The location specified must be a relative path and must not '
671 'contain .. in the path'), category='warning')
671 'contain .. in the path'), category='warning')
672 return redirect(url('changeset_home', repo_name=c.repo_name,
672 return redirect(url('changeset_home', repo_name=c.repo_name,
673 revision='tip'))
673 revision='tip'))
674 except (NodeError, NodeAlreadyExistsError) as e:
674 except (NodeError, NodeAlreadyExistsError) as e:
675 h.flash(_(e), category='error')
675 h.flash(_(e), category='error')
676 except Exception:
676 except Exception:
677 msg = _('Error occurred during commit')
677 msg = _('Error occurred during commit')
678 log.exception(msg)
678 log.exception(msg)
679 h.flash(msg, category='error')
679 h.flash(msg, category='error')
680 return redirect(url('changeset_home',
680 return redirect(url('changeset_home',
681 repo_name=c.repo_name, revision='tip'))
681 repo_name=c.repo_name, revision='tip'))
682
682
683 @LoginRequired()
683 @LoginRequired()
684 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
684 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
685 def add_home(self, repo_name, revision, f_path):
685 def add_home(self, repo_name, revision, f_path):
686
686
687 repo = Repository.get_by_repo_name(repo_name)
687 repo = Repository.get_by_repo_name(repo_name)
688 if repo.enable_locking and repo.locked[0]:
688 if repo.enable_locking and repo.locked[0]:
689 h.flash(_('This repository has been locked by %s on %s')
689 h.flash(_('This repository has been locked by %s on %s')
690 % (h.person_by_id(repo.locked[0]),
690 % (h.person_by_id(repo.locked[0]),
691 h.format_date(h.time_to_datetime(repo.locked[1]))),
691 h.format_date(h.time_to_datetime(repo.locked[1]))),
692 'warning')
692 'warning')
693 return redirect(h.url('files_home',
693 return redirect(h.url('files_home',
694 repo_name=repo_name, revision='tip'))
694 repo_name=repo_name, revision='tip'))
695
695
696 c.commit = self.__get_commit_or_redirect(
696 c.commit = self.__get_commit_or_redirect(
697 revision, repo_name, redirect_after=False)
697 revision, repo_name, redirect_after=False)
698 if c.commit is None:
698 if c.commit is None:
699 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
699 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
700 c.default_message = (_('Added file via RhodeCode Enterprise'))
700 c.default_message = (_('Added file via RhodeCode Enterprise'))
701 c.f_path = f_path
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 @LoginRequired()
705 @LoginRequired()
706 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
706 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
707 'repository.admin')
707 'repository.admin')
708 def archivefile(self, repo_name, fname):
708 def archivefile(self, repo_name, fname):
709 fileformat = None
709 fileformat = None
710 commit_id = None
710 commit_id = None
711 ext = None
711 ext = None
712 subrepos = request.GET.get('subrepos') == 'true'
712 subrepos = request.GET.get('subrepos') == 'true'
713
713
714 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
714 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
715 archive_spec = fname.split(ext_data[1])
715 archive_spec = fname.split(ext_data[1])
716 if len(archive_spec) == 2 and archive_spec[1] == '':
716 if len(archive_spec) == 2 and archive_spec[1] == '':
717 fileformat = a_type or ext_data[1]
717 fileformat = a_type or ext_data[1]
718 commit_id = archive_spec[0]
718 commit_id = archive_spec[0]
719 ext = ext_data[1]
719 ext = ext_data[1]
720
720
721 dbrepo = RepoModel().get_by_repo_name(repo_name)
721 dbrepo = RepoModel().get_by_repo_name(repo_name)
722 if not dbrepo.enable_downloads:
722 if not dbrepo.enable_downloads:
723 return _('Downloads disabled')
723 return _('Downloads disabled')
724
724
725 try:
725 try:
726 commit = c.rhodecode_repo.get_commit(commit_id)
726 commit = c.rhodecode_repo.get_commit(commit_id)
727 content_type = settings.ARCHIVE_SPECS[fileformat][0]
727 content_type = settings.ARCHIVE_SPECS[fileformat][0]
728 except CommitDoesNotExistError:
728 except CommitDoesNotExistError:
729 return _('Unknown revision %s') % commit_id
729 return _('Unknown revision %s') % commit_id
730 except EmptyRepositoryError:
730 except EmptyRepositoryError:
731 return _('Empty repository')
731 return _('Empty repository')
732 except KeyError:
732 except KeyError:
733 return _('Unknown archive type')
733 return _('Unknown archive type')
734
734
735 # archive cache
735 # archive cache
736 from rhodecode import CONFIG
736 from rhodecode import CONFIG
737
737
738 archive_name = '%s-%s%s%s' % (
738 archive_name = '%s-%s%s%s' % (
739 safe_str(repo_name.replace('/', '_')),
739 safe_str(repo_name.replace('/', '_')),
740 '-sub' if subrepos else '',
740 '-sub' if subrepos else '',
741 safe_str(commit.short_id), ext)
741 safe_str(commit.short_id), ext)
742
742
743 use_cached_archive = False
743 use_cached_archive = False
744 archive_cache_enabled = CONFIG.get(
744 archive_cache_enabled = CONFIG.get(
745 'archive_cache_dir') and not request.GET.get('no_cache')
745 'archive_cache_dir') and not request.GET.get('no_cache')
746
746
747 if archive_cache_enabled:
747 if archive_cache_enabled:
748 # check if we it's ok to write
748 # check if we it's ok to write
749 if not os.path.isdir(CONFIG['archive_cache_dir']):
749 if not os.path.isdir(CONFIG['archive_cache_dir']):
750 os.makedirs(CONFIG['archive_cache_dir'])
750 os.makedirs(CONFIG['archive_cache_dir'])
751 cached_archive_path = os.path.join(
751 cached_archive_path = os.path.join(
752 CONFIG['archive_cache_dir'], archive_name)
752 CONFIG['archive_cache_dir'], archive_name)
753 if os.path.isfile(cached_archive_path):
753 if os.path.isfile(cached_archive_path):
754 log.debug('Found cached archive in %s', cached_archive_path)
754 log.debug('Found cached archive in %s', cached_archive_path)
755 fd, archive = None, cached_archive_path
755 fd, archive = None, cached_archive_path
756 use_cached_archive = True
756 use_cached_archive = True
757 else:
757 else:
758 log.debug('Archive %s is not yet cached', archive_name)
758 log.debug('Archive %s is not yet cached', archive_name)
759
759
760 if not use_cached_archive:
760 if not use_cached_archive:
761 # generate new archive
761 # generate new archive
762 fd, archive = tempfile.mkstemp()
762 fd, archive = tempfile.mkstemp()
763 log.debug('Creating new temp archive in %s' % (archive,))
763 log.debug('Creating new temp archive in %s' % (archive,))
764 try:
764 try:
765 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
765 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
766 except ImproperArchiveTypeError:
766 except ImproperArchiveTypeError:
767 return _('Unknown archive type')
767 return _('Unknown archive type')
768 if archive_cache_enabled:
768 if archive_cache_enabled:
769 # if we generated the archive and we have cache enabled
769 # if we generated the archive and we have cache enabled
770 # let's use this for future
770 # let's use this for future
771 log.debug('Storing new archive in %s' % (cached_archive_path,))
771 log.debug('Storing new archive in %s' % (cached_archive_path,))
772 shutil.move(archive, cached_archive_path)
772 shutil.move(archive, cached_archive_path)
773 archive = cached_archive_path
773 archive = cached_archive_path
774
774
775 def get_chunked_archive(archive):
775 def get_chunked_archive(archive):
776 with open(archive, 'rb') as stream:
776 with open(archive, 'rb') as stream:
777 while True:
777 while True:
778 data = stream.read(16 * 1024)
778 data = stream.read(16 * 1024)
779 if not data:
779 if not data:
780 if fd: # fd means we used temporary file
780 if fd: # fd means we used temporary file
781 os.close(fd)
781 os.close(fd)
782 if not archive_cache_enabled:
782 if not archive_cache_enabled:
783 log.debug('Destroying temp archive %s', archive)
783 log.debug('Destroying temp archive %s', archive)
784 os.remove(archive)
784 os.remove(archive)
785 break
785 break
786 yield data
786 yield data
787
787
788 # store download action
788 # store download action
789 action_logger(user=c.rhodecode_user,
789 action_logger(user=c.rhodecode_user,
790 action='user_downloaded_archive:%s' % archive_name,
790 action='user_downloaded_archive:%s' % archive_name,
791 repo=repo_name, ipaddr=self.ip_addr, commit=True)
791 repo=repo_name, ipaddr=self.ip_addr, commit=True)
792 response.content_disposition = str(
792 response.content_disposition = str(
793 'attachment; filename=%s' % archive_name)
793 'attachment; filename=%s' % archive_name)
794 response.content_type = str(content_type)
794 response.content_type = str(content_type)
795
795
796 return get_chunked_archive(archive)
796 return get_chunked_archive(archive)
797
797
798 @LoginRequired()
798 @LoginRequired()
799 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
799 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
800 'repository.admin')
800 'repository.admin')
801 def diff(self, repo_name, f_path):
801 def diff(self, repo_name, f_path):
802
802
803 c.action = request.GET.get('diff')
803 c.action = request.GET.get('diff')
804 diff1 = request.GET.get('diff1', '')
804 diff1 = request.GET.get('diff1', '')
805 diff2 = request.GET.get('diff2', '')
805 diff2 = request.GET.get('diff2', '')
806
806
807 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
807 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
808
808
809 ignore_whitespace = str2bool(request.GET.get('ignorews'))
809 ignore_whitespace = str2bool(request.GET.get('ignorews'))
810 line_context = request.GET.get('context', 3)
810 line_context = request.GET.get('context', 3)
811
811
812 if not any((diff1, diff2)):
812 if not any((diff1, diff2)):
813 h.flash(
813 h.flash(
814 'Need query parameter "diff1" or "diff2" to generate a diff.',
814 'Need query parameter "diff1" or "diff2" to generate a diff.',
815 category='error')
815 category='error')
816 raise HTTPBadRequest()
816 raise HTTPBadRequest()
817
817
818 if c.action not in ['download', 'raw']:
818 if c.action not in ['download', 'raw']:
819 # redirect to new view if we render diff
819 # redirect to new view if we render diff
820 return redirect(
820 return redirect(
821 url('compare_url', repo_name=repo_name,
821 url('compare_url', repo_name=repo_name,
822 source_ref_type='rev',
822 source_ref_type='rev',
823 source_ref=diff1,
823 source_ref=diff1,
824 target_repo=c.repo_name,
824 target_repo=c.repo_name,
825 target_ref_type='rev',
825 target_ref_type='rev',
826 target_ref=diff2,
826 target_ref=diff2,
827 f_path=f_path))
827 f_path=f_path))
828
828
829 try:
829 try:
830 node1 = self._get_file_node(diff1, path1)
830 node1 = self._get_file_node(diff1, path1)
831 node2 = self._get_file_node(diff2, f_path)
831 node2 = self._get_file_node(diff2, f_path)
832 except (RepositoryError, NodeError):
832 except (RepositoryError, NodeError):
833 log.exception("Exception while trying to get node from repository")
833 log.exception("Exception while trying to get node from repository")
834 return redirect(url(
834 return redirect(url(
835 'files_home', repo_name=c.repo_name, f_path=f_path))
835 'files_home', repo_name=c.repo_name, f_path=f_path))
836
836
837 if all(isinstance(node.commit, EmptyCommit)
837 if all(isinstance(node.commit, EmptyCommit)
838 for node in (node1, node2)):
838 for node in (node1, node2)):
839 raise HTTPNotFound
839 raise HTTPNotFound
840
840
841 c.commit_1 = node1.commit
841 c.commit_1 = node1.commit
842 c.commit_2 = node2.commit
842 c.commit_2 = node2.commit
843
843
844 if c.action == 'download':
844 if c.action == 'download':
845 _diff = diffs.get_gitdiff(node1, node2,
845 _diff = diffs.get_gitdiff(node1, node2,
846 ignore_whitespace=ignore_whitespace,
846 ignore_whitespace=ignore_whitespace,
847 context=line_context)
847 context=line_context)
848 diff = diffs.DiffProcessor(_diff, format='gitdiff')
848 diff = diffs.DiffProcessor(_diff, format='gitdiff')
849
849
850 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
850 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
851 response.content_type = 'text/plain'
851 response.content_type = 'text/plain'
852 response.content_disposition = (
852 response.content_disposition = (
853 'attachment; filename=%s' % (diff_name,)
853 'attachment; filename=%s' % (diff_name,)
854 )
854 )
855 charset = self._get_default_encoding()
855 charset = self._get_default_encoding()
856 if charset:
856 if charset:
857 response.charset = charset
857 response.charset = charset
858 return diff.as_raw()
858 return diff.as_raw()
859
859
860 elif c.action == 'raw':
860 elif c.action == 'raw':
861 _diff = diffs.get_gitdiff(node1, node2,
861 _diff = diffs.get_gitdiff(node1, node2,
862 ignore_whitespace=ignore_whitespace,
862 ignore_whitespace=ignore_whitespace,
863 context=line_context)
863 context=line_context)
864 diff = diffs.DiffProcessor(_diff, format='gitdiff')
864 diff = diffs.DiffProcessor(_diff, format='gitdiff')
865 response.content_type = 'text/plain'
865 response.content_type = 'text/plain'
866 charset = self._get_default_encoding()
866 charset = self._get_default_encoding()
867 if charset:
867 if charset:
868 response.charset = charset
868 response.charset = charset
869 return diff.as_raw()
869 return diff.as_raw()
870
870
871 else:
871 else:
872 return redirect(
872 return redirect(
873 url('compare_url', repo_name=repo_name,
873 url('compare_url', repo_name=repo_name,
874 source_ref_type='rev',
874 source_ref_type='rev',
875 source_ref=diff1,
875 source_ref=diff1,
876 target_repo=c.repo_name,
876 target_repo=c.repo_name,
877 target_ref_type='rev',
877 target_ref_type='rev',
878 target_ref=diff2,
878 target_ref=diff2,
879 f_path=f_path))
879 f_path=f_path))
880
880
881 @LoginRequired()
881 @LoginRequired()
882 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
882 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
883 'repository.admin')
883 'repository.admin')
884 def diff_2way(self, repo_name, f_path):
884 def diff_2way(self, repo_name, f_path):
885 """
885 """
886 Kept only to make OLD links work
886 Kept only to make OLD links work
887 """
887 """
888 diff1 = request.GET.get('diff1', '')
888 diff1 = request.GET.get('diff1', '')
889 diff2 = request.GET.get('diff2', '')
889 diff2 = request.GET.get('diff2', '')
890
890
891 if not any((diff1, diff2)):
891 if not any((diff1, diff2)):
892 h.flash(
892 h.flash(
893 'Need query parameter "diff1" or "diff2" to generate a diff.',
893 'Need query parameter "diff1" or "diff2" to generate a diff.',
894 category='error')
894 category='error')
895 raise HTTPBadRequest()
895 raise HTTPBadRequest()
896
896
897 return redirect(
897 return redirect(
898 url('compare_url', repo_name=repo_name,
898 url('compare_url', repo_name=repo_name,
899 source_ref_type='rev',
899 source_ref_type='rev',
900 source_ref=diff1,
900 source_ref=diff1,
901 target_repo=c.repo_name,
901 target_repo=c.repo_name,
902 target_ref_type='rev',
902 target_ref_type='rev',
903 target_ref=diff2,
903 target_ref=diff2,
904 f_path=f_path,
904 f_path=f_path,
905 diffmode='sideside'))
905 diffmode='sideside'))
906
906
907 def _get_file_node(self, commit_id, f_path):
907 def _get_file_node(self, commit_id, f_path):
908 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
908 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
909 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
909 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
910 try:
910 try:
911 node = commit.get_node(f_path)
911 node = commit.get_node(f_path)
912 if node.is_dir():
912 if node.is_dir():
913 raise NodeError('%s path is a %s not a file'
913 raise NodeError('%s path is a %s not a file'
914 % (node, type(node)))
914 % (node, type(node)))
915 except NodeDoesNotExistError:
915 except NodeDoesNotExistError:
916 commit = EmptyCommit(
916 commit = EmptyCommit(
917 commit_id=commit_id,
917 commit_id=commit_id,
918 idx=commit.idx,
918 idx=commit.idx,
919 repo=commit.repository,
919 repo=commit.repository,
920 alias=commit.repository.alias,
920 alias=commit.repository.alias,
921 message=commit.message,
921 message=commit.message,
922 author=commit.author,
922 author=commit.author,
923 date=commit.date)
923 date=commit.date)
924 node = FileNode(f_path, '', commit=commit)
924 node = FileNode(f_path, '', commit=commit)
925 else:
925 else:
926 commit = EmptyCommit(
926 commit = EmptyCommit(
927 repo=c.rhodecode_repo,
927 repo=c.rhodecode_repo,
928 alias=c.rhodecode_repo.alias)
928 alias=c.rhodecode_repo.alias)
929 node = FileNode(f_path, '', commit=commit)
929 node = FileNode(f_path, '', commit=commit)
930 return node
930 return node
931
931
932 def _get_node_history(self, commit, f_path, commits=None):
932 def _get_node_history(self, commit, f_path, commits=None):
933 """
933 """
934 get commit history for given node
934 get commit history for given node
935
935
936 :param commit: commit to calculate history
936 :param commit: commit to calculate history
937 :param f_path: path for node to calculate history for
937 :param f_path: path for node to calculate history for
938 :param commits: if passed don't calculate history and take
938 :param commits: if passed don't calculate history and take
939 commits defined in this list
939 commits defined in this list
940 """
940 """
941 # calculate history based on tip
941 # calculate history based on tip
942 tip = c.rhodecode_repo.get_commit()
942 tip = c.rhodecode_repo.get_commit()
943 if commits is None:
943 if commits is None:
944 pre_load = ["author", "branch"]
944 pre_load = ["author", "branch"]
945 try:
945 try:
946 commits = tip.get_file_history(f_path, pre_load=pre_load)
946 commits = tip.get_file_history(f_path, pre_load=pre_load)
947 except (NodeDoesNotExistError, CommitError):
947 except (NodeDoesNotExistError, CommitError):
948 # this node is not present at tip!
948 # this node is not present at tip!
949 commits = commit.get_file_history(f_path, pre_load=pre_load)
949 commits = commit.get_file_history(f_path, pre_load=pre_load)
950
950
951 history = []
951 history = []
952 commits_group = ([], _("Changesets"))
952 commits_group = ([], _("Changesets"))
953 for commit in commits:
953 for commit in commits:
954 branch = ' (%s)' % commit.branch if commit.branch else ''
954 branch = ' (%s)' % commit.branch if commit.branch else ''
955 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
955 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
956 commits_group[0].append((commit.raw_id, n_desc,))
956 commits_group[0].append((commit.raw_id, n_desc,))
957 history.append(commits_group)
957 history.append(commits_group)
958
958
959 symbolic_reference = self._symbolic_reference
959 symbolic_reference = self._symbolic_reference
960
960
961 if c.rhodecode_repo.alias == 'svn':
961 if c.rhodecode_repo.alias == 'svn':
962 adjusted_f_path = self._adjust_file_path_for_svn(
962 adjusted_f_path = self._adjust_file_path_for_svn(
963 f_path, c.rhodecode_repo)
963 f_path, c.rhodecode_repo)
964 if adjusted_f_path != f_path:
964 if adjusted_f_path != f_path:
965 log.debug(
965 log.debug(
966 'Recognized svn tag or branch in file "%s", using svn '
966 'Recognized svn tag or branch in file "%s", using svn '
967 'specific symbolic references', f_path)
967 'specific symbolic references', f_path)
968 f_path = adjusted_f_path
968 f_path = adjusted_f_path
969 symbolic_reference = self._symbolic_reference_svn
969 symbolic_reference = self._symbolic_reference_svn
970
970
971 branches = self._create_references(
971 branches = self._create_references(
972 c.rhodecode_repo.branches, symbolic_reference, f_path)
972 c.rhodecode_repo.branches, symbolic_reference, f_path)
973 branches_group = (branches, _("Branches"))
973 branches_group = (branches, _("Branches"))
974
974
975 tags = self._create_references(
975 tags = self._create_references(
976 c.rhodecode_repo.tags, symbolic_reference, f_path)
976 c.rhodecode_repo.tags, symbolic_reference, f_path)
977 tags_group = (tags, _("Tags"))
977 tags_group = (tags, _("Tags"))
978
978
979 history.append(branches_group)
979 history.append(branches_group)
980 history.append(tags_group)
980 history.append(tags_group)
981
981
982 return history, commits
982 return history, commits
983
983
984 def _adjust_file_path_for_svn(self, f_path, repo):
984 def _adjust_file_path_for_svn(self, f_path, repo):
985 """
985 """
986 Computes the relative path of `f_path`.
986 Computes the relative path of `f_path`.
987
987
988 This is mainly based on prefix matching of the recognized tags and
988 This is mainly based on prefix matching of the recognized tags and
989 branches in the underlying repository.
989 branches in the underlying repository.
990 """
990 """
991 tags_and_branches = itertools.chain(
991 tags_and_branches = itertools.chain(
992 repo.branches.iterkeys(),
992 repo.branches.iterkeys(),
993 repo.tags.iterkeys())
993 repo.tags.iterkeys())
994 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
994 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
995
995
996 for name in tags_and_branches:
996 for name in tags_and_branches:
997 if f_path.startswith(name + '/'):
997 if f_path.startswith(name + '/'):
998 f_path = vcspath.relpath(f_path, name)
998 f_path = vcspath.relpath(f_path, name)
999 break
999 break
1000 return f_path
1000 return f_path
1001
1001
1002 def _create_references(
1002 def _create_references(
1003 self, branches_or_tags, symbolic_reference, f_path):
1003 self, branches_or_tags, symbolic_reference, f_path):
1004 items = []
1004 items = []
1005 for name, commit_id in branches_or_tags.items():
1005 for name, commit_id in branches_or_tags.items():
1006 sym_ref = symbolic_reference(commit_id, name, f_path)
1006 sym_ref = symbolic_reference(commit_id, name, f_path)
1007 items.append((sym_ref, name))
1007 items.append((sym_ref, name))
1008 return items
1008 return items
1009
1009
1010 def _symbolic_reference(self, commit_id, name, f_path):
1010 def _symbolic_reference(self, commit_id, name, f_path):
1011 return commit_id
1011 return commit_id
1012
1012
1013 def _symbolic_reference_svn(self, commit_id, name, f_path):
1013 def _symbolic_reference_svn(self, commit_id, name, f_path):
1014 new_f_path = vcspath.join(name, f_path)
1014 new_f_path = vcspath.join(name, f_path)
1015 return u'%s@%s' % (new_f_path, commit_id)
1015 return u'%s@%s' % (new_f_path, commit_id)
1016
1016
1017 @LoginRequired()
1017 @LoginRequired()
1018 @XHRRequired()
1018 @XHRRequired()
1019 @HasRepoPermissionAnyDecorator(
1019 @HasRepoPermissionAnyDecorator(
1020 'repository.read', 'repository.write', 'repository.admin')
1020 'repository.read', 'repository.write', 'repository.admin')
1021 @jsonify
1021 @jsonify
1022 def nodelist(self, repo_name, revision, f_path):
1022 def nodelist(self, repo_name, revision, f_path):
1023 commit = self.__get_commit_or_redirect(revision, repo_name)
1023 commit = self.__get_commit_or_redirect(revision, repo_name)
1024
1024
1025 metadata = self._get_nodelist_at_commit(
1025 metadata = self._get_nodelist_at_commit(
1026 repo_name, commit.raw_id, f_path)
1026 repo_name, commit.raw_id, f_path)
1027 return {'nodes': metadata}
1027 return {'nodes': metadata}
1028
1028
1029 @LoginRequired()
1029 @LoginRequired()
1030 @XHRRequired()
1030 @XHRRequired()
1031 @HasRepoPermissionAnyDecorator(
1031 @HasRepoPermissionAnyDecorator(
1032 'repository.read', 'repository.write', 'repository.admin')
1032 'repository.read', 'repository.write', 'repository.admin')
1033 def nodetree_full(self, repo_name, commit_id, f_path):
1033 def nodetree_full(self, repo_name, commit_id, f_path):
1034 """
1034 """
1035 Returns rendered html of file tree that contains commit date,
1035 Returns rendered html of file tree that contains commit date,
1036 author, revision for the specified combination of
1036 author, revision for the specified combination of
1037 repo, commit_id and file path
1037 repo, commit_id and file path
1038
1038
1039 :param repo_name: name of the repository
1039 :param repo_name: name of the repository
1040 :param commit_id: commit_id of file tree
1040 :param commit_id: commit_id of file tree
1041 :param f_path: file path of the requested directory
1041 :param f_path: file path of the requested directory
1042 """
1042 """
1043
1043
1044 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1044 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1045 try:
1045 try:
1046 dir_node = commit.get_node(f_path)
1046 dir_node = commit.get_node(f_path)
1047 except RepositoryError as e:
1047 except RepositoryError as e:
1048 return 'error {}'.format(safe_str(e))
1048 return 'error {}'.format(safe_str(e))
1049
1049
1050 if dir_node.is_file():
1050 if dir_node.is_file():
1051 return ''
1051 return ''
1052
1052
1053 c.file = dir_node
1053 c.file = dir_node
1054 c.commit = commit
1054 c.commit = commit
1055
1055
1056 # using force=True here, make a little trick. We flush the cache and
1056 # using force=True here, make a little trick. We flush the cache and
1057 # compute it using the same key as without full_load, so the fully
1057 # compute it using the same key as without full_load, so the fully
1058 # loaded cached tree is now returned instead of partial
1058 # loaded cached tree is now returned instead of partial
1059 return self._get_tree_at_commit(
1059 return self._get_tree_at_commit(
1060 repo_name, commit.raw_id, dir_node.path, full_load=True,
1060 repo_name, commit.raw_id, dir_node.path, full_load=True,
1061 force=True)
1061 force=True)
@@ -1,58 +1,58 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Followers controller for rhodecode
22 Followers controller for rhodecode
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from pylons import tmpl_context as c, request
27 from pylons import tmpl_context as c, request
28
28
29 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.helpers import Page
30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
31 from rhodecode.lib.base import BaseRepoController, render
31 from rhodecode.lib.base import BaseRepoController, render
32 from rhodecode.model.db import UserFollowing
32 from rhodecode.model.db import UserFollowing
33 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class FollowersController(BaseRepoController):
38 class FollowersController(BaseRepoController):
39
39
40 def __before__(self):
40 def __before__(self):
41 super(FollowersController, self).__before__()
41 super(FollowersController, self).__before__()
42
42
43 @LoginRequired()
43 @LoginRequired()
44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
45 'repository.admin')
45 'repository.admin')
46 def followers(self, repo_name):
46 def followers(self, repo_name):
47 p = safe_int(request.GET.get('page', 1), 1)
47 p = safe_int(request.GET.get('page', 1), 1)
48 repo_id = c.rhodecode_db_repo.repo_id
48 repo_id = c.rhodecode_db_repo.repo_id
49 d = UserFollowing.get_repo_followers(repo_id)\
49 d = UserFollowing.get_repo_followers(repo_id)\
50 .order_by(UserFollowing.follows_from)
50 .order_by(UserFollowing.follows_from)
51 c.followers_pager = Page(d, page=p, items_per_page=20)
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 if request.environ.get('HTTP_X_PJAX'):
55 if request.environ.get('HTTP_X_PJAX'):
56 return c.followers_data
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 forks controller for rhodecode
22 forks controller for rhodecode
23 """
23 """
24
24
25 import formencode
25 import formencode
26 import logging
26 import logging
27 from formencode import htmlfill
27 from formencode import htmlfill
28
28
29 from pylons import tmpl_context as c, request, url
29 from pylons import tmpl_context as c, request, url
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 import rhodecode.lib.helpers as h
33 import rhodecode.lib.helpers as h
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib.helpers import Page
36 from rhodecode.lib.helpers import Page
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
40 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.forms import RepoForkForm
43 from rhodecode.model.forms import RepoForkForm
44 from rhodecode.model.scm import ScmModel, RepoGroupList
44 from rhodecode.model.scm import ScmModel, RepoGroupList
45 from rhodecode.lib.utils2 import safe_int
45 from rhodecode.lib.utils2 import safe_int
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class ForksController(BaseRepoController):
50 class ForksController(BaseRepoController):
51
51
52 def __before__(self):
52 def __before__(self):
53 super(ForksController, self).__before__()
53 super(ForksController, self).__before__()
54
54
55 def __load_defaults(self):
55 def __load_defaults(self):
56 acl_groups = RepoGroupList(
56 acl_groups = RepoGroupList(
57 RepoGroup.query().all(),
57 RepoGroup.query().all(),
58 perm_set=['group.write', 'group.admin'])
58 perm_set=['group.write', 'group.admin'])
59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
62 c.landing_revs_choices = choices
62 c.landing_revs_choices = choices
63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
64
64
65 def __load_data(self, repo_name=None):
65 def __load_data(self, repo_name=None):
66 """
66 """
67 Load defaults settings for edit, and update
67 Load defaults settings for edit, and update
68
68
69 :param repo_name:
69 :param repo_name:
70 """
70 """
71 self.__load_defaults()
71 self.__load_defaults()
72
72
73 c.repo_info = Repository.get_by_repo_name(repo_name)
73 c.repo_info = Repository.get_by_repo_name(repo_name)
74 repo = c.repo_info.scm_instance()
74 repo = c.repo_info.scm_instance()
75
75
76 if c.repo_info is None:
76 if c.repo_info is None:
77 h.not_mapped_error(repo_name)
77 h.not_mapped_error(repo_name)
78 return redirect(url('repos'))
78 return redirect(url('repos'))
79
79
80 c.default_user_id = User.get_default_user().user_id
80 c.default_user_id = User.get_default_user().user_id
81 c.in_public_journal = UserFollowing.query()\
81 c.in_public_journal = UserFollowing.query()\
82 .filter(UserFollowing.user_id == c.default_user_id)\
82 .filter(UserFollowing.user_id == c.default_user_id)\
83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
84
84
85 if c.repo_info.stats:
85 if c.repo_info.stats:
86 last_rev = c.repo_info.stats.stat_on_revision+1
86 last_rev = c.repo_info.stats.stat_on_revision+1
87 else:
87 else:
88 last_rev = 0
88 last_rev = 0
89 c.stats_revision = last_rev
89 c.stats_revision = last_rev
90
90
91 c.repo_last_rev = repo.count()
91 c.repo_last_rev = repo.count()
92
92
93 if last_rev == 0 or c.repo_last_rev == 0:
93 if last_rev == 0 or c.repo_last_rev == 0:
94 c.stats_percentage = 0
94 c.stats_percentage = 0
95 else:
95 else:
96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
97 c.repo_last_rev) * 100)
97 c.repo_last_rev) * 100)
98
98
99 defaults = RepoModel()._get_defaults(repo_name)
99 defaults = RepoModel()._get_defaults(repo_name)
100 # alter the description to indicate a fork
100 # alter the description to indicate a fork
101 defaults['description'] = ('fork of repository: %s \n%s'
101 defaults['description'] = ('fork of repository: %s \n%s'
102 % (defaults['repo_name'],
102 % (defaults['repo_name'],
103 defaults['description']))
103 defaults['description']))
104 # add suffix to fork
104 # add suffix to fork
105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
106
106
107 return defaults
107 return defaults
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
111 'repository.admin')
111 'repository.admin')
112 @HasAcceptedRepoType('git', 'hg')
112 @HasAcceptedRepoType('git', 'hg')
113 def forks(self, repo_name):
113 def forks(self, repo_name):
114 p = safe_int(request.GET.get('page', 1), 1)
114 p = safe_int(request.GET.get('page', 1), 1)
115 repo_id = c.rhodecode_db_repo.repo_id
115 repo_id = c.rhodecode_db_repo.repo_id
116 d = []
116 d = []
117 for r in Repository.get_repo_forks(repo_id):
117 for r in Repository.get_repo_forks(repo_id):
118 if not HasRepoPermissionAny(
118 if not HasRepoPermissionAny(
119 'repository.read', 'repository.write', 'repository.admin'
119 'repository.read', 'repository.write', 'repository.admin'
120 )(r.repo_name, 'get forks check'):
120 )(r.repo_name, 'get forks check'):
121 continue
121 continue
122 d.append(r)
122 d.append(r)
123 c.forks_pager = Page(d, page=p, items_per_page=20)
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 if request.environ.get('HTTP_X_PJAX'):
127 if request.environ.get('HTTP_X_PJAX'):
128 return c.forks_data
128 return c.forks_data
129
129
130 return render('/forks/forks.html')
130 return render('/forks/forks.mako')
131
131
132 @LoginRequired()
132 @LoginRequired()
133 @NotAnonymous()
133 @NotAnonymous()
134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 'repository.admin')
136 'repository.admin')
137 @HasAcceptedRepoType('git', 'hg')
137 @HasAcceptedRepoType('git', 'hg')
138 def fork(self, repo_name):
138 def fork(self, repo_name):
139 c.repo_info = Repository.get_by_repo_name(repo_name)
139 c.repo_info = Repository.get_by_repo_name(repo_name)
140 if not c.repo_info:
140 if not c.repo_info:
141 h.not_mapped_error(repo_name)
141 h.not_mapped_error(repo_name)
142 return redirect(url('home'))
142 return redirect(url('home'))
143
143
144 defaults = self.__load_data(repo_name)
144 defaults = self.__load_data(repo_name)
145
145
146 return htmlfill.render(
146 return htmlfill.render(
147 render('forks/fork.html'),
147 render('forks/fork.mako'),
148 defaults=defaults,
148 defaults=defaults,
149 encoding="UTF-8",
149 encoding="UTF-8",
150 force_defaults=False
150 force_defaults=False
151 )
151 )
152
152
153 @LoginRequired()
153 @LoginRequired()
154 @NotAnonymous()
154 @NotAnonymous()
155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
157 'repository.admin')
157 'repository.admin')
158 @HasAcceptedRepoType('git', 'hg')
158 @HasAcceptedRepoType('git', 'hg')
159 @auth.CSRFRequired()
159 @auth.CSRFRequired()
160 def fork_create(self, repo_name):
160 def fork_create(self, repo_name):
161 self.__load_defaults()
161 self.__load_defaults()
162 c.repo_info = Repository.get_by_repo_name(repo_name)
162 c.repo_info = Repository.get_by_repo_name(repo_name)
163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
164 repo_groups=c.repo_groups_choices,
164 repo_groups=c.repo_groups_choices,
165 landing_revs=c.landing_revs_choices)()
165 landing_revs=c.landing_revs_choices)()
166 form_result = {}
166 form_result = {}
167 task_id = None
167 task_id = None
168 try:
168 try:
169 form_result = _form.to_python(dict(request.POST))
169 form_result = _form.to_python(dict(request.POST))
170 # create fork is done sometimes async on celery, db transaction
170 # create fork is done sometimes async on celery, db transaction
171 # management is handled there.
171 # management is handled there.
172 task = RepoModel().create_fork(
172 task = RepoModel().create_fork(
173 form_result, c.rhodecode_user.user_id)
173 form_result, c.rhodecode_user.user_id)
174 from celery.result import BaseAsyncResult
174 from celery.result import BaseAsyncResult
175 if isinstance(task, BaseAsyncResult):
175 if isinstance(task, BaseAsyncResult):
176 task_id = task.task_id
176 task_id = task.task_id
177 except formencode.Invalid as errors:
177 except formencode.Invalid as errors:
178 c.new_repo = errors.value['repo_name']
178 c.new_repo = errors.value['repo_name']
179 return htmlfill.render(
179 return htmlfill.render(
180 render('forks/fork.html'),
180 render('forks/fork.mako'),
181 defaults=errors.value,
181 defaults=errors.value,
182 errors=errors.error_dict or {},
182 errors=errors.error_dict or {},
183 prefix_error=False,
183 prefix_error=False,
184 encoding="UTF-8",
184 encoding="UTF-8",
185 force_defaults=False)
185 force_defaults=False)
186 except Exception:
186 except Exception:
187 log.exception(
187 log.exception(
188 u'Exception while trying to fork the repository %s', repo_name)
188 u'Exception while trying to fork the repository %s', repo_name)
189 msg = (
189 msg = (
190 _('An error occurred during repository forking %s') %
190 _('An error occurred during repository forking %s') %
191 (repo_name, ))
191 (repo_name, ))
192 h.flash(msg, category='error')
192 h.flash(msg, category='error')
193
193
194 return redirect(h.url('repo_creating_home',
194 return redirect(h.url('repo_creating_home',
195 repo_name=form_result['repo_name_full'],
195 repo_name=form_result['repo_name_full'],
196 task_id=task_id))
196 task_id=task_id))
@@ -1,288 +1,288 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Home controller for RhodeCode Enterprise
22 Home controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import logging
25 import logging
26 import time
26 import time
27 import re
27 import re
28
28
29 from pylons import tmpl_context as c, request, url, config
29 from pylons import tmpl_context as c, request, url, config
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31 from sqlalchemy.sql import func
31 from sqlalchemy.sql import func
32
32
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasPermissionAllDecorator, AuthUser,
34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.lib.index import searcher_from_config
37 from rhodecode.lib.index import searcher_from_config
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.utils import jsonify
39 from rhodecode.lib.utils import jsonify
40 from rhodecode.lib.utils2 import safe_unicode, str2bool
40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 from rhodecode.model.db import Repository, RepoGroup
41 from rhodecode.model.db import Repository, RepoGroup
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.scm import RepoList, RepoGroupList
44 from rhodecode.model.scm import RepoList, RepoGroupList
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class HomeController(BaseController):
50 class HomeController(BaseController):
51 def __before__(self):
51 def __before__(self):
52 super(HomeController, self).__before__()
52 super(HomeController, self).__before__()
53
53
54 def ping(self):
54 def ping(self):
55 """
55 """
56 Ping, doesn't require login, good for checking out the platform
56 Ping, doesn't require login, good for checking out the platform
57 """
57 """
58 instance_id = getattr(c, 'rhodecode_instanceid', '')
58 instance_id = getattr(c, 'rhodecode_instanceid', '')
59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
60
60
61 @LoginRequired()
61 @LoginRequired()
62 @HasPermissionAllDecorator('hg.admin')
62 @HasPermissionAllDecorator('hg.admin')
63 def error_test(self):
63 def error_test(self):
64 """
64 """
65 Test exception handling and emails on errors
65 Test exception handling and emails on errors
66 """
66 """
67 class TestException(Exception):
67 class TestException(Exception):
68 pass
68 pass
69
69
70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
71 % (c.rhodecode_name, time.time()))
71 % (c.rhodecode_name, time.time()))
72 raise TestException(msg)
72 raise TestException(msg)
73
73
74 def _get_groups_and_repos(self, repo_group_id=None):
74 def _get_groups_and_repos(self, repo_group_id=None):
75 # repo groups groups
75 # repo groups groups
76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
77 _perms = ['group.read', 'group.write', 'group.admin']
77 _perms = ['group.read', 'group.write', 'group.admin']
78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
80 repo_group_list=repo_group_list_acl, admin=False)
80 repo_group_list=repo_group_list_acl, admin=False)
81
81
82 # repositories
82 # repositories
83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
84 _perms = ['repository.read', 'repository.write', 'repository.admin']
84 _perms = ['repository.read', 'repository.write', 'repository.admin']
85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
86 repo_data = RepoModel().get_repos_as_dict(
86 repo_data = RepoModel().get_repos_as_dict(
87 repo_list=repo_list_acl, admin=False)
87 repo_list=repo_list_acl, admin=False)
88
88
89 return repo_data, repo_group_data
89 return repo_data, repo_group_data
90
90
91 @LoginRequired()
91 @LoginRequired()
92 def index(self):
92 def index(self):
93 c.repo_group = None
93 c.repo_group = None
94
94
95 repo_data, repo_group_data = self._get_groups_and_repos()
95 repo_data, repo_group_data = self._get_groups_and_repos()
96 # json used to render the grids
96 # json used to render the grids
97 c.repos_data = json.dumps(repo_data)
97 c.repos_data = json.dumps(repo_data)
98 c.repo_groups_data = json.dumps(repo_group_data)
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 @LoginRequired()
102 @LoginRequired()
103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
104 'group.admin')
104 'group.admin')
105 def index_repo_group(self, group_name):
105 def index_repo_group(self, group_name):
106 """GET /repo_group_name: Show a specific item"""
106 """GET /repo_group_name: Show a specific item"""
107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
108 repo_data, repo_group_data = self._get_groups_and_repos(
108 repo_data, repo_group_data = self._get_groups_and_repos(
109 c.repo_group.group_id)
109 c.repo_group.group_id)
110
110
111 # json used to render the grids
111 # json used to render the grids
112 c.repos_data = json.dumps(repo_data)
112 c.repos_data = json.dumps(repo_data)
113 c.repo_groups_data = json.dumps(repo_group_data)
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 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
117 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
118 query = Repository.query()\
118 query = Repository.query()\
119 .order_by(func.length(Repository.repo_name))\
119 .order_by(func.length(Repository.repo_name))\
120 .order_by(Repository.repo_name)
120 .order_by(Repository.repo_name)
121
121
122 if repo_type:
122 if repo_type:
123 query = query.filter(Repository.repo_type == repo_type)
123 query = query.filter(Repository.repo_type == repo_type)
124
124
125 if name_contains:
125 if name_contains:
126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 query = query.filter(
127 query = query.filter(
128 Repository.repo_name.ilike(ilike_expression))
128 Repository.repo_name.ilike(ilike_expression))
129 query = query.limit(limit)
129 query = query.limit(limit)
130
130
131 all_repos = query.all()
131 all_repos = query.all()
132 repo_iter = self.scm_model.get_repos(all_repos)
132 repo_iter = self.scm_model.get_repos(all_repos)
133 return [
133 return [
134 {
134 {
135 'id': obj['name'],
135 'id': obj['name'],
136 'text': obj['name'],
136 'text': obj['name'],
137 'type': 'repo',
137 'type': 'repo',
138 'obj': obj['dbrepo'],
138 'obj': obj['dbrepo'],
139 'url': url('summary_home', repo_name=obj['name'])
139 'url': url('summary_home', repo_name=obj['name'])
140 }
140 }
141 for obj in repo_iter]
141 for obj in repo_iter]
142
142
143 def _get_repo_group_list(self, name_contains=None, limit=20):
143 def _get_repo_group_list(self, name_contains=None, limit=20):
144 query = RepoGroup.query()\
144 query = RepoGroup.query()\
145 .order_by(func.length(RepoGroup.group_name))\
145 .order_by(func.length(RepoGroup.group_name))\
146 .order_by(RepoGroup.group_name)
146 .order_by(RepoGroup.group_name)
147
147
148 if name_contains:
148 if name_contains:
149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
150 query = query.filter(
150 query = query.filter(
151 RepoGroup.group_name.ilike(ilike_expression))
151 RepoGroup.group_name.ilike(ilike_expression))
152 query = query.limit(limit)
152 query = query.limit(limit)
153
153
154 all_groups = query.all()
154 all_groups = query.all()
155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
156 return [
156 return [
157 {
157 {
158 'id': obj.group_name,
158 'id': obj.group_name,
159 'text': obj.group_name,
159 'text': obj.group_name,
160 'type': 'group',
160 'type': 'group',
161 'obj': {},
161 'obj': {},
162 'url': url('repo_group_home', group_name=obj.group_name)
162 'url': url('repo_group_home', group_name=obj.group_name)
163 }
163 }
164 for obj in repo_groups_iter]
164 for obj in repo_groups_iter]
165
165
166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
167 if not hash_starts_with or len(hash_starts_with) < 3:
167 if not hash_starts_with or len(hash_starts_with) < 3:
168 return []
168 return []
169
169
170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
171
171
172 if len(commit_hashes) != 1:
172 if len(commit_hashes) != 1:
173 return []
173 return []
174
174
175 commit_hash_prefix = commit_hashes[0]
175 commit_hash_prefix = commit_hashes[0]
176
176
177 auth_user = AuthUser(
177 auth_user = AuthUser(
178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
179 searcher = searcher_from_config(config)
179 searcher = searcher_from_config(config)
180 result = searcher.search(
180 result = searcher.search(
181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
182
182
183 return [
183 return [
184 {
184 {
185 'id': entry['commit_id'],
185 'id': entry['commit_id'],
186 'text': entry['commit_id'],
186 'text': entry['commit_id'],
187 'type': 'commit',
187 'type': 'commit',
188 'obj': {'repo': entry['repository']},
188 'obj': {'repo': entry['repository']},
189 'url': url('changeset_home',
189 'url': url('changeset_home',
190 repo_name=entry['repository'], revision=entry['commit_id'])
190 repo_name=entry['repository'], revision=entry['commit_id'])
191 }
191 }
192 for entry in result['results']]
192 for entry in result['results']]
193
193
194 @LoginRequired()
194 @LoginRequired()
195 @XHRRequired()
195 @XHRRequired()
196 @jsonify
196 @jsonify
197 def goto_switcher_data(self):
197 def goto_switcher_data(self):
198 query = request.GET.get('query')
198 query = request.GET.get('query')
199 log.debug('generating goto switcher list, query %s', query)
199 log.debug('generating goto switcher list, query %s', query)
200
200
201 res = []
201 res = []
202 repo_groups = self._get_repo_group_list(query)
202 repo_groups = self._get_repo_group_list(query)
203 if repo_groups:
203 if repo_groups:
204 res.append({
204 res.append({
205 'text': _('Groups'),
205 'text': _('Groups'),
206 'children': repo_groups
206 'children': repo_groups
207 })
207 })
208
208
209 repos = self._get_repo_list(query)
209 repos = self._get_repo_list(query)
210 if repos:
210 if repos:
211 res.append({
211 res.append({
212 'text': _('Repositories'),
212 'text': _('Repositories'),
213 'children': repos
213 'children': repos
214 })
214 })
215
215
216 commits = self._get_hash_commit_list(query)
216 commits = self._get_hash_commit_list(query)
217 if commits:
217 if commits:
218 unique_repos = {}
218 unique_repos = {}
219 for commit in commits:
219 for commit in commits:
220 unique_repos.setdefault(commit['obj']['repo'], []
220 unique_repos.setdefault(commit['obj']['repo'], []
221 ).append(commit)
221 ).append(commit)
222
222
223 for repo in unique_repos:
223 for repo in unique_repos:
224 res.append({
224 res.append({
225 'text': _('Commits in %(repo)s') % {'repo': repo},
225 'text': _('Commits in %(repo)s') % {'repo': repo},
226 'children': unique_repos[repo]
226 'children': unique_repos[repo]
227 })
227 })
228
228
229 data = {
229 data = {
230 'more': False,
230 'more': False,
231 'results': res
231 'results': res
232 }
232 }
233 return data
233 return data
234
234
235 @LoginRequired()
235 @LoginRequired()
236 @XHRRequired()
236 @XHRRequired()
237 @jsonify
237 @jsonify
238 def repo_list_data(self):
238 def repo_list_data(self):
239 query = request.GET.get('query')
239 query = request.GET.get('query')
240 repo_type = request.GET.get('repo_type')
240 repo_type = request.GET.get('repo_type')
241 log.debug('generating repo list, query:%s', query)
241 log.debug('generating repo list, query:%s', query)
242
242
243 res = []
243 res = []
244 repos = self._get_repo_list(query, repo_type=repo_type)
244 repos = self._get_repo_list(query, repo_type=repo_type)
245 if repos:
245 if repos:
246 res.append({
246 res.append({
247 'text': _('Repositories'),
247 'text': _('Repositories'),
248 'children': repos
248 'children': repos
249 })
249 })
250
250
251 data = {
251 data = {
252 'more': False,
252 'more': False,
253 'results': res
253 'results': res
254 }
254 }
255 return data
255 return data
256
256
257 @LoginRequired()
257 @LoginRequired()
258 @XHRRequired()
258 @XHRRequired()
259 @jsonify
259 @jsonify
260 def user_autocomplete_data(self):
260 def user_autocomplete_data(self):
261 query = request.GET.get('query')
261 query = request.GET.get('query')
262 active = str2bool(request.GET.get('active') or True)
262 active = str2bool(request.GET.get('active') or True)
263
263
264 repo_model = RepoModel()
264 repo_model = RepoModel()
265 _users = repo_model.get_users(
265 _users = repo_model.get_users(
266 name_contains=query, only_active=active)
266 name_contains=query, only_active=active)
267
267
268 if request.GET.get('user_groups'):
268 if request.GET.get('user_groups'):
269 # extend with user groups
269 # extend with user groups
270 _user_groups = repo_model.get_user_groups(
270 _user_groups = repo_model.get_user_groups(
271 name_contains=query, only_active=active)
271 name_contains=query, only_active=active)
272 _users = _users + _user_groups
272 _users = _users + _user_groups
273
273
274 return {'suggestions': _users}
274 return {'suggestions': _users}
275
275
276 @LoginRequired()
276 @LoginRequired()
277 @XHRRequired()
277 @XHRRequired()
278 @jsonify
278 @jsonify
279 def user_group_autocomplete_data(self):
279 def user_group_autocomplete_data(self):
280 query = request.GET.get('query')
280 query = request.GET.get('query')
281 active = str2bool(request.GET.get('active') or True)
281 active = str2bool(request.GET.get('active') or True)
282
282
283 repo_model = RepoModel()
283 repo_model = RepoModel()
284 _user_groups = repo_model.get_user_groups(
284 _user_groups = repo_model.get_user_groups(
285 name_contains=query, only_active=active)
285 name_contains=query, only_active=active)
286 _user_groups = _user_groups
286 _user_groups = _user_groups
287
287
288 return {'suggestions': _user_groups}
288 return {'suggestions': _user_groups}
@@ -1,306 +1,306 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Journal / user event log controller for rhodecode
22 Journal / user event log controller for rhodecode
23 """
23 """
24
24
25 import logging
25 import logging
26 from itertools import groupby
26 from itertools import groupby
27
27
28 from sqlalchemy import or_
28 from sqlalchemy import or_
29 from sqlalchemy.orm import joinedload
29 from sqlalchemy.orm import joinedload
30
30
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32
32
33 from webob.exc import HTTPBadRequest
33 from webob.exc import HTTPBadRequest
34 from pylons import request, tmpl_context as c, response, url
34 from pylons import request, tmpl_context as c, response, url
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.controllers.admin.admin import _journal_filter
37 from rhodecode.controllers.admin.admin import _journal_filter
38 from rhodecode.model.db import UserLog, UserFollowing, User
38 from rhodecode.model.db import UserLog, UserFollowing, User
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
41 from rhodecode.lib.helpers import Page
41 from rhodecode.lib.helpers import Page
42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 from rhodecode.lib.base import BaseController, render
43 from rhodecode.lib.base import BaseController, render
44 from rhodecode.lib.utils2 import safe_int, AttributeDict
44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class JournalController(BaseController):
49 class JournalController(BaseController):
50
50
51 def __before__(self):
51 def __before__(self):
52 super(JournalController, self).__before__()
52 super(JournalController, self).__before__()
53 self.language = 'en-us'
53 self.language = 'en-us'
54 self.ttl = "5"
54 self.ttl = "5"
55 self.feed_nr = 20
55 self.feed_nr = 20
56 c.search_term = request.GET.get('filter')
56 c.search_term = request.GET.get('filter')
57
57
58 def _get_daily_aggregate(self, journal):
58 def _get_daily_aggregate(self, journal):
59 groups = []
59 groups = []
60 for k, g in groupby(journal, lambda x: x.action_as_day):
60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 user_group = []
61 user_group = []
62 #groupby username if it's a present value, else fallback to journal username
62 #groupby username if it's a present value, else fallback to journal username
63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 l = list(g2)
64 l = list(g2)
65 user_group.append((l[0].user, l))
65 user_group.append((l[0].user, l))
66
66
67 groups.append((k, user_group,))
67 groups.append((k, user_group,))
68
68
69 return groups
69 return groups
70
70
71 def _get_journal_data(self, following_repos):
71 def _get_journal_data(self, following_repos):
72 repo_ids = [x.follows_repository.repo_id for x in following_repos
72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 if x.follows_repository is not None]
73 if x.follows_repository is not None]
74 user_ids = [x.follows_user.user_id for x in following_repos
74 user_ids = [x.follows_user.user_id for x in following_repos
75 if x.follows_user is not None]
75 if x.follows_user is not None]
76
76
77 filtering_criterion = None
77 filtering_criterion = None
78
78
79 if repo_ids and user_ids:
79 if repo_ids and user_ids:
80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 UserLog.user_id.in_(user_ids))
81 UserLog.user_id.in_(user_ids))
82 if repo_ids and not user_ids:
82 if repo_ids and not user_ids:
83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 if not repo_ids and user_ids:
84 if not repo_ids and user_ids:
85 filtering_criterion = UserLog.user_id.in_(user_ids)
85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 if filtering_criterion is not None:
86 if filtering_criterion is not None:
87 journal = self.sa.query(UserLog)\
87 journal = self.sa.query(UserLog)\
88 .options(joinedload(UserLog.user))\
88 .options(joinedload(UserLog.user))\
89 .options(joinedload(UserLog.repository))
89 .options(joinedload(UserLog.repository))
90 #filter
90 #filter
91 try:
91 try:
92 journal = _journal_filter(journal, c.search_term)
92 journal = _journal_filter(journal, c.search_term)
93 except Exception:
93 except Exception:
94 # we want this to crash for now
94 # we want this to crash for now
95 raise
95 raise
96 journal = journal.filter(filtering_criterion)\
96 journal = journal.filter(filtering_criterion)\
97 .order_by(UserLog.action_date.desc())
97 .order_by(UserLog.action_date.desc())
98 else:
98 else:
99 journal = []
99 journal = []
100
100
101 return journal
101 return journal
102
102
103 def _atom_feed(self, repos, public=True):
103 def _atom_feed(self, repos, public=True):
104 journal = self._get_journal_data(repos)
104 journal = self._get_journal_data(repos)
105 if public:
105 if public:
106 _link = url('public_journal_atom', qualified=True)
106 _link = url('public_journal_atom', qualified=True)
107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 'atom feed')
108 'atom feed')
109 else:
109 else:
110 _link = url('journal_atom', qualified=True)
110 _link = url('journal_atom', qualified=True)
111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112
112
113 feed = Atom1Feed(title=_desc,
113 feed = Atom1Feed(title=_desc,
114 link=_link,
114 link=_link,
115 description=_desc,
115 description=_desc,
116 language=self.language,
116 language=self.language,
117 ttl=self.ttl)
117 ttl=self.ttl)
118
118
119 for entry in journal[:self.feed_nr]:
119 for entry in journal[:self.feed_nr]:
120 user = entry.user
120 user = entry.user
121 if user is None:
121 if user is None:
122 #fix deleted users
122 #fix deleted users
123 user = AttributeDict({'short_contact': entry.username,
123 user = AttributeDict({'short_contact': entry.username,
124 'email': '',
124 'email': '',
125 'full_contact': ''})
125 'full_contact': ''})
126 action, action_extra, ico = h.action_parser(entry, feed=True)
126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 title = "%s - %s %s" % (user.short_contact, action(),
127 title = "%s - %s %s" % (user.short_contact, action(),
128 entry.repository.repo_name)
128 entry.repository.repo_name)
129 desc = action_extra()
129 desc = action_extra()
130 _url = None
130 _url = None
131 if entry.repository is not None:
131 if entry.repository is not None:
132 _url = url('changelog_home',
132 _url = url('changelog_home',
133 repo_name=entry.repository.repo_name,
133 repo_name=entry.repository.repo_name,
134 qualified=True)
134 qualified=True)
135
135
136 feed.add_item(title=title,
136 feed.add_item(title=title,
137 pubdate=entry.action_date,
137 pubdate=entry.action_date,
138 link=_url or url('', qualified=True),
138 link=_url or url('', qualified=True),
139 author_email=user.email,
139 author_email=user.email,
140 author_name=user.full_contact,
140 author_name=user.full_contact,
141 description=desc)
141 description=desc)
142
142
143 response.content_type = feed.mime_type
143 response.content_type = feed.mime_type
144 return feed.writeString('utf-8')
144 return feed.writeString('utf-8')
145
145
146 def _rss_feed(self, repos, public=True):
146 def _rss_feed(self, repos, public=True):
147 journal = self._get_journal_data(repos)
147 journal = self._get_journal_data(repos)
148 if public:
148 if public:
149 _link = url('public_journal_atom', qualified=True)
149 _link = url('public_journal_atom', qualified=True)
150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 'rss feed')
151 'rss feed')
152 else:
152 else:
153 _link = url('journal_atom', qualified=True)
153 _link = url('journal_atom', qualified=True)
154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155
155
156 feed = Rss201rev2Feed(title=_desc,
156 feed = Rss201rev2Feed(title=_desc,
157 link=_link,
157 link=_link,
158 description=_desc,
158 description=_desc,
159 language=self.language,
159 language=self.language,
160 ttl=self.ttl)
160 ttl=self.ttl)
161
161
162 for entry in journal[:self.feed_nr]:
162 for entry in journal[:self.feed_nr]:
163 user = entry.user
163 user = entry.user
164 if user is None:
164 if user is None:
165 #fix deleted users
165 #fix deleted users
166 user = AttributeDict({'short_contact': entry.username,
166 user = AttributeDict({'short_contact': entry.username,
167 'email': '',
167 'email': '',
168 'full_contact': ''})
168 'full_contact': ''})
169 action, action_extra, ico = h.action_parser(entry, feed=True)
169 action, action_extra, ico = h.action_parser(entry, feed=True)
170 title = "%s - %s %s" % (user.short_contact, action(),
170 title = "%s - %s %s" % (user.short_contact, action(),
171 entry.repository.repo_name)
171 entry.repository.repo_name)
172 desc = action_extra()
172 desc = action_extra()
173 _url = None
173 _url = None
174 if entry.repository is not None:
174 if entry.repository is not None:
175 _url = url('changelog_home',
175 _url = url('changelog_home',
176 repo_name=entry.repository.repo_name,
176 repo_name=entry.repository.repo_name,
177 qualified=True)
177 qualified=True)
178
178
179 feed.add_item(title=title,
179 feed.add_item(title=title,
180 pubdate=entry.action_date,
180 pubdate=entry.action_date,
181 link=_url or url('', qualified=True),
181 link=_url or url('', qualified=True),
182 author_email=user.email,
182 author_email=user.email,
183 author_name=user.full_contact,
183 author_name=user.full_contact,
184 description=desc)
184 description=desc)
185
185
186 response.content_type = feed.mime_type
186 response.content_type = feed.mime_type
187 return feed.writeString('utf-8')
187 return feed.writeString('utf-8')
188
188
189 @LoginRequired()
189 @LoginRequired()
190 @NotAnonymous()
190 @NotAnonymous()
191 def index(self):
191 def index(self):
192 # Return a rendered template
192 # Return a rendered template
193 p = safe_int(request.GET.get('page', 1), 1)
193 p = safe_int(request.GET.get('page', 1), 1)
194 c.user = User.get(c.rhodecode_user.user_id)
194 c.user = User.get(c.rhodecode_user.user_id)
195 following = self.sa.query(UserFollowing)\
195 following = self.sa.query(UserFollowing)\
196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 .options(joinedload(UserFollowing.follows_repository))\
197 .options(joinedload(UserFollowing.follows_repository))\
198 .all()
198 .all()
199
199
200 journal = self._get_journal_data(following)
200 journal = self._get_journal_data(following)
201
201
202 def url_generator(**kw):
202 def url_generator(**kw):
203 return url.current(filter=c.search_term, **kw)
203 return url.current(filter=c.search_term, **kw)
204
204
205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
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 if request.is_xhr:
209 if request.is_xhr:
210 return c.journal_data
210 return c.journal_data
211
211
212 return render('journal/journal.html')
212 return render('journal/journal.mako')
213
213
214 @LoginRequired(auth_token_access=True)
214 @LoginRequired(auth_token_access=True)
215 @NotAnonymous()
215 @NotAnonymous()
216 def journal_atom(self):
216 def journal_atom(self):
217 """
217 """
218 Produce an atom-1.0 feed via feedgenerator module
218 Produce an atom-1.0 feed via feedgenerator module
219 """
219 """
220 following = self.sa.query(UserFollowing)\
220 following = self.sa.query(UserFollowing)\
221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 .options(joinedload(UserFollowing.follows_repository))\
222 .options(joinedload(UserFollowing.follows_repository))\
223 .all()
223 .all()
224 return self._atom_feed(following, public=False)
224 return self._atom_feed(following, public=False)
225
225
226 @LoginRequired(auth_token_access=True)
226 @LoginRequired(auth_token_access=True)
227 @NotAnonymous()
227 @NotAnonymous()
228 def journal_rss(self):
228 def journal_rss(self):
229 """
229 """
230 Produce an rss feed via feedgenerator module
230 Produce an rss feed via feedgenerator module
231 """
231 """
232 following = self.sa.query(UserFollowing)\
232 following = self.sa.query(UserFollowing)\
233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 .options(joinedload(UserFollowing.follows_repository))\
234 .options(joinedload(UserFollowing.follows_repository))\
235 .all()
235 .all()
236 return self._rss_feed(following, public=False)
236 return self._rss_feed(following, public=False)
237
237
238 @CSRFRequired()
238 @CSRFRequired()
239 @LoginRequired()
239 @LoginRequired()
240 @NotAnonymous()
240 @NotAnonymous()
241 def toggle_following(self):
241 def toggle_following(self):
242 user_id = request.POST.get('follows_user_id')
242 user_id = request.POST.get('follows_user_id')
243 if user_id:
243 if user_id:
244 try:
244 try:
245 self.scm_model.toggle_following_user(
245 self.scm_model.toggle_following_user(
246 user_id, c.rhodecode_user.user_id)
246 user_id, c.rhodecode_user.user_id)
247 Session().commit()
247 Session().commit()
248 return 'ok'
248 return 'ok'
249 except Exception:
249 except Exception:
250 raise HTTPBadRequest()
250 raise HTTPBadRequest()
251
251
252 repo_id = request.POST.get('follows_repo_id')
252 repo_id = request.POST.get('follows_repo_id')
253 if repo_id:
253 if repo_id:
254 try:
254 try:
255 self.scm_model.toggle_following_repo(
255 self.scm_model.toggle_following_repo(
256 repo_id, c.rhodecode_user.user_id)
256 repo_id, c.rhodecode_user.user_id)
257 Session().commit()
257 Session().commit()
258 return 'ok'
258 return 'ok'
259 except Exception:
259 except Exception:
260 raise HTTPBadRequest()
260 raise HTTPBadRequest()
261
261
262
262
263 @LoginRequired()
263 @LoginRequired()
264 def public_journal(self):
264 def public_journal(self):
265 # Return a rendered template
265 # Return a rendered template
266 p = safe_int(request.GET.get('page', 1), 1)
266 p = safe_int(request.GET.get('page', 1), 1)
267
267
268 c.following = self.sa.query(UserFollowing)\
268 c.following = self.sa.query(UserFollowing)\
269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 .options(joinedload(UserFollowing.follows_repository))\
270 .options(joinedload(UserFollowing.follows_repository))\
271 .all()
271 .all()
272
272
273 journal = self._get_journal_data(c.following)
273 journal = self._get_journal_data(c.following)
274
274
275 c.journal_pager = Page(journal, page=p, items_per_page=20)
275 c.journal_pager = Page(journal, page=p, items_per_page=20)
276
276
277 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
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 if request.is_xhr:
280 if request.is_xhr:
281 return c.journal_data
281 return c.journal_data
282 return render('journal/public_journal.html')
282 return render('journal/public_journal.mako')
283
283
284 @LoginRequired(auth_token_access=True)
284 @LoginRequired(auth_token_access=True)
285 def public_journal_atom(self):
285 def public_journal_atom(self):
286 """
286 """
287 Produce an atom-1.0 feed via feedgenerator module
287 Produce an atom-1.0 feed via feedgenerator module
288 """
288 """
289 c.following = self.sa.query(UserFollowing)\
289 c.following = self.sa.query(UserFollowing)\
290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 .options(joinedload(UserFollowing.follows_repository))\
291 .options(joinedload(UserFollowing.follows_repository))\
292 .all()
292 .all()
293
293
294 return self._atom_feed(c.following)
294 return self._atom_feed(c.following)
295
295
296 @LoginRequired(auth_token_access=True)
296 @LoginRequired(auth_token_access=True)
297 def public_journal_rss(self):
297 def public_journal_rss(self):
298 """
298 """
299 Produce an rss2 feed via feedgenerator module
299 Produce an rss2 feed via feedgenerator module
300 """
300 """
301 c.following = self.sa.query(UserFollowing)\
301 c.following = self.sa.query(UserFollowing)\
302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 .options(joinedload(UserFollowing.follows_repository))\
303 .options(joinedload(UserFollowing.follows_repository))\
304 .all()
304 .all()
305
305
306 return self._rss_feed(c.following)
306 return self._rss_feed(c.following)
@@ -1,1020 +1,1020 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import ChangesetCommentsModel
58 from rhodecode.model.comment import ChangesetCommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.html')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.html')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = ChangesetCommentsModel().get_all_comments(
261 comments = ChangesetCommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
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 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = ChangesetCommentsModel()
799 cc_model = ChangesetCommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
815 inline_comments, version=at_version, include_aggregates=True)
816
816
817 c.at_version_num = at_version if at_version and at_version != 'latest' else None
817 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 is_outdated = lambda co: \
818 is_outdated = lambda co: \
819 not c.at_version_num \
819 not c.at_version_num \
820 or co.pull_request_version_id <= c.at_version_num
820 or co.pull_request_version_id <= c.at_version_num
821
821
822 # inline_comments_until_version
822 # inline_comments_until_version
823 if c.at_version_num:
823 if c.at_version_num:
824 # if we use version, then do not show later comments
824 # if we use version, then do not show later comments
825 # than current version
825 # than current version
826 paths = collections.defaultdict(lambda: collections.defaultdict(list))
826 paths = collections.defaultdict(lambda: collections.defaultdict(list))
827 for fname, per_line_comments in inline_comments.iteritems():
827 for fname, per_line_comments in inline_comments.iteritems():
828 for lno, comments in per_line_comments.iteritems():
828 for lno, comments in per_line_comments.iteritems():
829 for co in comments:
829 for co in comments:
830 if co.pull_request_version_id and is_outdated(co):
830 if co.pull_request_version_id and is_outdated(co):
831 paths[co.f_path][co.line_no].append(co)
831 paths[co.f_path][co.line_no].append(co)
832 inline_comments = paths
832 inline_comments = paths
833
833
834 # outdated comments
834 # outdated comments
835 c.outdated_cnt = 0
835 c.outdated_cnt = 0
836 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
836 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
837 outdated_comments = cc_model.get_outdated_comments(
837 outdated_comments = cc_model.get_outdated_comments(
838 c.rhodecode_db_repo.repo_id,
838 c.rhodecode_db_repo.repo_id,
839 pull_request=pull_request_at_ver)
839 pull_request=pull_request_at_ver)
840
840
841 # Count outdated comments and check for deleted files
841 # Count outdated comments and check for deleted files
842 is_outdated = lambda co: \
842 is_outdated = lambda co: \
843 not c.at_version_num \
843 not c.at_version_num \
844 or co.pull_request_version_id < c.at_version_num
844 or co.pull_request_version_id < c.at_version_num
845 for file_name, lines in outdated_comments.iteritems():
845 for file_name, lines in outdated_comments.iteritems():
846 for comments in lines.values():
846 for comments in lines.values():
847 comments = [comm for comm in comments if is_outdated(comm)]
847 comments = [comm for comm in comments if is_outdated(comm)]
848 c.outdated_cnt += len(comments)
848 c.outdated_cnt += len(comments)
849
849
850 # load compare data into template context
850 # load compare data into template context
851 self._load_compare_data(pull_request_at_ver, inline_comments)
851 self._load_compare_data(pull_request_at_ver, inline_comments)
852
852
853 # this is a hack to properly display links, when creating PR, the
853 # this is a hack to properly display links, when creating PR, the
854 # compare view and others uses different notation, and
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 # We need to swap that here to generate it properly on the html side
856 # We need to swap that here to generate it properly on the html side
857 c.target_repo = c.source_repo
857 c.target_repo = c.source_repo
858
858
859 # general comments
859 # general comments
860 c.comments = cc_model.get_comments(
860 c.comments = cc_model.get_comments(
861 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
861 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
862
862
863 if c.allowed_to_update:
863 if c.allowed_to_update:
864 force_close = ('forced_closed', _('Close Pull Request'))
864 force_close = ('forced_closed', _('Close Pull Request'))
865 statuses = ChangesetStatus.STATUSES + [force_close]
865 statuses = ChangesetStatus.STATUSES + [force_close]
866 else:
866 else:
867 statuses = ChangesetStatus.STATUSES
867 statuses = ChangesetStatus.STATUSES
868 c.commit_statuses = statuses
868 c.commit_statuses = statuses
869
869
870 c.ancestor = None # TODO: add ancestor here
870 c.ancestor = None # TODO: add ancestor here
871 c.pull_request = pull_request_display_obj
871 c.pull_request = pull_request_display_obj
872 c.pull_request_latest = pull_request_latest
872 c.pull_request_latest = pull_request_latest
873 c.at_version = at_version
873 c.at_version = at_version
874
874
875 c.versions = pull_request_display_obj.versions()
875 c.versions = pull_request_display_obj.versions()
876 c.changes = None
876 c.changes = None
877 c.file_changes = None
877 c.file_changes = None
878
878
879 c.show_version_changes = 1 # control flag, not used yet
879 c.show_version_changes = 1 # control flag, not used yet
880
880
881 if at_version and c.show_version_changes:
881 if at_version and c.show_version_changes:
882 c.changes, c.file_changes = self._get_pr_version_changes(
882 c.changes, c.file_changes = self._get_pr_version_changes(
883 version, pull_request_latest)
883 version, pull_request_latest)
884
884
885 return render('/pullrequests/pullrequest_show.html')
885 return render('/pullrequests/pullrequest_show.mako')
886
886
887 @LoginRequired()
887 @LoginRequired()
888 @NotAnonymous()
888 @NotAnonymous()
889 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
889 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
890 'repository.admin')
890 'repository.admin')
891 @auth.CSRFRequired()
891 @auth.CSRFRequired()
892 @jsonify
892 @jsonify
893 def comment(self, repo_name, pull_request_id):
893 def comment(self, repo_name, pull_request_id):
894 pull_request_id = safe_int(pull_request_id)
894 pull_request_id = safe_int(pull_request_id)
895 pull_request = PullRequest.get_or_404(pull_request_id)
895 pull_request = PullRequest.get_or_404(pull_request_id)
896 if pull_request.is_closed():
896 if pull_request.is_closed():
897 raise HTTPForbidden()
897 raise HTTPForbidden()
898
898
899 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
899 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
900 # as a changeset status, still we want to send it in one value.
900 # as a changeset status, still we want to send it in one value.
901 status = request.POST.get('changeset_status', None)
901 status = request.POST.get('changeset_status', None)
902 text = request.POST.get('text')
902 text = request.POST.get('text')
903 if status and '_closed' in status:
903 if status and '_closed' in status:
904 close_pr = True
904 close_pr = True
905 status = status.replace('_closed', '')
905 status = status.replace('_closed', '')
906 else:
906 else:
907 close_pr = False
907 close_pr = False
908
908
909 forced = (status == 'forced')
909 forced = (status == 'forced')
910 if forced:
910 if forced:
911 status = 'rejected'
911 status = 'rejected'
912
912
913 allowed_to_change_status = PullRequestModel().check_user_change_status(
913 allowed_to_change_status = PullRequestModel().check_user_change_status(
914 pull_request, c.rhodecode_user)
914 pull_request, c.rhodecode_user)
915
915
916 if status and allowed_to_change_status:
916 if status and allowed_to_change_status:
917 message = (_('Status change %(transition_icon)s %(status)s')
917 message = (_('Status change %(transition_icon)s %(status)s')
918 % {'transition_icon': '>',
918 % {'transition_icon': '>',
919 'status': ChangesetStatus.get_status_lbl(status)})
919 'status': ChangesetStatus.get_status_lbl(status)})
920 if close_pr:
920 if close_pr:
921 message = _('Closing with') + ' ' + message
921 message = _('Closing with') + ' ' + message
922 text = text or message
922 text = text or message
923 comm = ChangesetCommentsModel().create(
923 comm = ChangesetCommentsModel().create(
924 text=text,
924 text=text,
925 repo=c.rhodecode_db_repo.repo_id,
925 repo=c.rhodecode_db_repo.repo_id,
926 user=c.rhodecode_user.user_id,
926 user=c.rhodecode_user.user_id,
927 pull_request=pull_request_id,
927 pull_request=pull_request_id,
928 f_path=request.POST.get('f_path'),
928 f_path=request.POST.get('f_path'),
929 line_no=request.POST.get('line'),
929 line_no=request.POST.get('line'),
930 status_change=(ChangesetStatus.get_status_lbl(status)
930 status_change=(ChangesetStatus.get_status_lbl(status)
931 if status and allowed_to_change_status else None),
931 if status and allowed_to_change_status else None),
932 status_change_type=(status
932 status_change_type=(status
933 if status and allowed_to_change_status else None),
933 if status and allowed_to_change_status else None),
934 closing_pr=close_pr
934 closing_pr=close_pr
935 )
935 )
936
936
937 if allowed_to_change_status:
937 if allowed_to_change_status:
938 old_calculated_status = pull_request.calculated_review_status()
938 old_calculated_status = pull_request.calculated_review_status()
939 # get status if set !
939 # get status if set !
940 if status:
940 if status:
941 ChangesetStatusModel().set_status(
941 ChangesetStatusModel().set_status(
942 c.rhodecode_db_repo.repo_id,
942 c.rhodecode_db_repo.repo_id,
943 status,
943 status,
944 c.rhodecode_user.user_id,
944 c.rhodecode_user.user_id,
945 comm,
945 comm,
946 pull_request=pull_request_id
946 pull_request=pull_request_id
947 )
947 )
948
948
949 Session().flush()
949 Session().flush()
950 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
950 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
951 # we now calculate the status of pull request, and based on that
951 # we now calculate the status of pull request, and based on that
952 # calculation we set the commits status
952 # calculation we set the commits status
953 calculated_status = pull_request.calculated_review_status()
953 calculated_status = pull_request.calculated_review_status()
954 if old_calculated_status != calculated_status:
954 if old_calculated_status != calculated_status:
955 PullRequestModel()._trigger_pull_request_hook(
955 PullRequestModel()._trigger_pull_request_hook(
956 pull_request, c.rhodecode_user, 'review_status_change')
956 pull_request, c.rhodecode_user, 'review_status_change')
957
957
958 calculated_status_lbl = ChangesetStatus.get_status_lbl(
958 calculated_status_lbl = ChangesetStatus.get_status_lbl(
959 calculated_status)
959 calculated_status)
960
960
961 if close_pr:
961 if close_pr:
962 status_completed = (
962 status_completed = (
963 calculated_status in [ChangesetStatus.STATUS_APPROVED,
963 calculated_status in [ChangesetStatus.STATUS_APPROVED,
964 ChangesetStatus.STATUS_REJECTED])
964 ChangesetStatus.STATUS_REJECTED])
965 if forced or status_completed:
965 if forced or status_completed:
966 PullRequestModel().close_pull_request(
966 PullRequestModel().close_pull_request(
967 pull_request_id, c.rhodecode_user)
967 pull_request_id, c.rhodecode_user)
968 else:
968 else:
969 h.flash(_('Closing pull request on other statuses than '
969 h.flash(_('Closing pull request on other statuses than '
970 'rejected or approved is forbidden. '
970 'rejected or approved is forbidden. '
971 'Calculated status from all reviewers '
971 'Calculated status from all reviewers '
972 'is currently: %s') % calculated_status_lbl,
972 'is currently: %s') % calculated_status_lbl,
973 category='warning')
973 category='warning')
974
974
975 Session().commit()
975 Session().commit()
976
976
977 if not request.is_xhr:
977 if not request.is_xhr:
978 return redirect(h.url('pullrequest_show', repo_name=repo_name,
978 return redirect(h.url('pullrequest_show', repo_name=repo_name,
979 pull_request_id=pull_request_id))
979 pull_request_id=pull_request_id))
980
980
981 data = {
981 data = {
982 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
982 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
983 }
983 }
984 if comm:
984 if comm:
985 c.co = comm
985 c.co = comm
986 data.update(comm.get_dict())
986 data.update(comm.get_dict())
987 data.update({'rendered_text':
987 data.update({'rendered_text':
988 render('changeset/changeset_comment_block.html')})
988 render('changeset/changeset_comment_block.mako')})
989
989
990 return data
990 return data
991
991
992 @LoginRequired()
992 @LoginRequired()
993 @NotAnonymous()
993 @NotAnonymous()
994 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
994 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
995 'repository.admin')
995 'repository.admin')
996 @auth.CSRFRequired()
996 @auth.CSRFRequired()
997 @jsonify
997 @jsonify
998 def delete_comment(self, repo_name, comment_id):
998 def delete_comment(self, repo_name, comment_id):
999 return self._delete_comment(comment_id)
999 return self._delete_comment(comment_id)
1000
1000
1001 def _delete_comment(self, comment_id):
1001 def _delete_comment(self, comment_id):
1002 comment_id = safe_int(comment_id)
1002 comment_id = safe_int(comment_id)
1003 co = ChangesetComment.get_or_404(comment_id)
1003 co = ChangesetComment.get_or_404(comment_id)
1004 if co.pull_request.is_closed():
1004 if co.pull_request.is_closed():
1005 # don't allow deleting comments on closed pull request
1005 # don't allow deleting comments on closed pull request
1006 raise HTTPForbidden()
1006 raise HTTPForbidden()
1007
1007
1008 is_owner = co.author.user_id == c.rhodecode_user.user_id
1008 is_owner = co.author.user_id == c.rhodecode_user.user_id
1009 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1009 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1010 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1010 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1011 old_calculated_status = co.pull_request.calculated_review_status()
1011 old_calculated_status = co.pull_request.calculated_review_status()
1012 ChangesetCommentsModel().delete(comment=co)
1012 ChangesetCommentsModel().delete(comment=co)
1013 Session().commit()
1013 Session().commit()
1014 calculated_status = co.pull_request.calculated_review_status()
1014 calculated_status = co.pull_request.calculated_review_status()
1015 if old_calculated_status != calculated_status:
1015 if old_calculated_status != calculated_status:
1016 PullRequestModel()._trigger_pull_request_hook(
1016 PullRequestModel()._trigger_pull_request_hook(
1017 co.pull_request, c.rhodecode_user, 'review_status_change')
1017 co.pull_request, c.rhodecode_user, 'review_status_change')
1018 return True
1018 return True
1019 else:
1019 else:
1020 raise HTTPForbidden()
1020 raise HTTPForbidden()
@@ -1,111 +1,111 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Search controller for RhodeCode
22 Search controller for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import urllib
26 import urllib
27
27
28 from pylons import request, config, tmpl_context as c
28 from pylons import request, config, tmpl_context as c
29
29
30 from webhelpers.util import update_params
30 from webhelpers.util import update_params
31
31
32 from rhodecode.lib.auth import LoginRequired, AuthUser
32 from rhodecode.lib.auth import LoginRequired, AuthUser
33 from rhodecode.lib.base import BaseRepoController, render
33 from rhodecode.lib.base import BaseRepoController, render
34 from rhodecode.lib.helpers import Page
34 from rhodecode.lib.helpers import Page
35 from rhodecode.lib.utils2 import safe_str, safe_int
35 from rhodecode.lib.utils2 import safe_str, safe_int
36 from rhodecode.lib.index import searcher_from_config
36 from rhodecode.lib.index import searcher_from_config
37 from rhodecode.model import validation_schema
37 from rhodecode.model import validation_schema
38 from rhodecode.model.validation_schema.schemas import search_schema
38 from rhodecode.model.validation_schema.schemas import search_schema
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class SearchController(BaseRepoController):
43 class SearchController(BaseRepoController):
44
44
45 @LoginRequired()
45 @LoginRequired()
46 def index(self, repo_name=None):
46 def index(self, repo_name=None):
47
47
48 searcher = searcher_from_config(config)
48 searcher = searcher_from_config(config)
49 formatted_results = []
49 formatted_results = []
50 execution_time = ''
50 execution_time = ''
51
51
52 schema = search_schema.SearchParamsSchema()
52 schema = search_schema.SearchParamsSchema()
53
53
54 search_params = {}
54 search_params = {}
55 errors = []
55 errors = []
56 try:
56 try:
57 search_params = schema.deserialize(
57 search_params = schema.deserialize(
58 dict(search_query=request.GET.get('q'),
58 dict(search_query=request.GET.get('q'),
59 search_type=request.GET.get('type'),
59 search_type=request.GET.get('type'),
60 search_sort=request.GET.get('sort'),
60 search_sort=request.GET.get('sort'),
61 page_limit=request.GET.get('page_limit'),
61 page_limit=request.GET.get('page_limit'),
62 requested_page=request.GET.get('page'))
62 requested_page=request.GET.get('page'))
63 )
63 )
64 except validation_schema.Invalid as e:
64 except validation_schema.Invalid as e:
65 errors = e.children
65 errors = e.children
66
66
67 def url_generator(**kw):
67 def url_generator(**kw):
68 q = urllib.quote(safe_str(search_query))
68 q = urllib.quote(safe_str(search_query))
69 return update_params(
69 return update_params(
70 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
70 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
71
71
72 search_query = search_params.get('search_query')
72 search_query = search_params.get('search_query')
73 search_type = search_params.get('search_type')
73 search_type = search_params.get('search_type')
74 search_sort = search_params.get('search_sort')
74 search_sort = search_params.get('search_sort')
75 if search_params.get('search_query'):
75 if search_params.get('search_query'):
76 page_limit = search_params['page_limit']
76 page_limit = search_params['page_limit']
77 requested_page = search_params['requested_page']
77 requested_page = search_params['requested_page']
78
78
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
80 ip_addr=self.ip_addr)
80 ip_addr=self.ip_addr)
81
81
82 try:
82 try:
83 search_result = searcher.search(
83 search_result = searcher.search(
84 search_query, search_type, c.perm_user, repo_name,
84 search_query, search_type, c.perm_user, repo_name,
85 requested_page, page_limit, search_sort)
85 requested_page, page_limit, search_sort)
86
86
87 formatted_results = Page(
87 formatted_results = Page(
88 search_result['results'], page=requested_page,
88 search_result['results'], page=requested_page,
89 item_count=search_result['count'],
89 item_count=search_result['count'],
90 items_per_page=page_limit, url=url_generator)
90 items_per_page=page_limit, url=url_generator)
91 finally:
91 finally:
92 searcher.cleanup()
92 searcher.cleanup()
93
93
94 if not search_result['error']:
94 if not search_result['error']:
95 execution_time = '%s results (%.3f seconds)' % (
95 execution_time = '%s results (%.3f seconds)' % (
96 search_result['count'],
96 search_result['count'],
97 search_result['runtime'])
97 search_result['runtime'])
98 elif not errors:
98 elif not errors:
99 node = schema['search_query']
99 node = schema['search_query']
100 errors = [
100 errors = [
101 validation_schema.Invalid(node, search_result['error'])]
101 validation_schema.Invalid(node, search_result['error'])]
102
102
103 c.sort = search_sort
103 c.sort = search_sort
104 c.url_generator = url_generator
104 c.url_generator = url_generator
105 c.errors = errors
105 c.errors = errors
106 c.formatted_results = formatted_results
106 c.formatted_results = formatted_results
107 c.runtime = execution_time
107 c.runtime = execution_time
108 c.cur_query = search_query
108 c.cur_query = search_query
109 c.search_type = search_type
109 c.search_type = search_type
110 # Return a rendered template
110 # Return a rendered template
111 return render('/search/search.html')
111 return render('/search/search.mako')
@@ -1,318 +1,318 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Summary controller for RhodeCode Enterprise
22 Summary controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import logging
25 import logging
26 from string import lower
26 from string import lower
27
27
28 from pylons import tmpl_context as c, request
28 from pylons import tmpl_context as c, request
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30 from beaker.cache import cache_region, region_invalidate
30 from beaker.cache import cache_region, region_invalidate
31
31
32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 from rhodecode.controllers import utils
33 from rhodecode.controllers import utils
34 from rhodecode.controllers.changelog import _load_changelog_summary
34 from rhodecode.controllers.changelog import _load_changelog_summary
35 from rhodecode.lib import caches, helpers as h
35 from rhodecode.lib import caches, helpers as h
36 from rhodecode.lib.utils import jsonify
36 from rhodecode.lib.utils import jsonify
37 from rhodecode.lib.utils2 import safe_str
37 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
40 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.lib.markup_renderer import MarkupRenderer
41 from rhodecode.lib.markup_renderer import MarkupRenderer
42 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitError, EmptyRepositoryError, NodeDoesNotExistError)
45 CommitError, EmptyRepositoryError, NodeDoesNotExistError)
46 from rhodecode.model.db import Statistics, CacheKey, User
46 from rhodecode.model.db import Statistics, CacheKey, User
47 from rhodecode.model.repo import ReadmeFinder
47 from rhodecode.model.repo import ReadmeFinder
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class SummaryController(BaseRepoController):
53 class SummaryController(BaseRepoController):
54
54
55 def __before__(self):
55 def __before__(self):
56 super(SummaryController, self).__before__()
56 super(SummaryController, self).__before__()
57
57
58 def __get_readme_data(self, db_repo):
58 def __get_readme_data(self, db_repo):
59 repo_name = db_repo.repo_name
59 repo_name = db_repo.repo_name
60 log.debug('Looking for README file')
60 log.debug('Looking for README file')
61 default_renderer = c.visual.default_renderer
61 default_renderer = c.visual.default_renderer
62
62
63 @cache_region('long_term')
63 @cache_region('long_term')
64 def _generate_readme(cache_key):
64 def _generate_readme(cache_key):
65 readme_data = None
65 readme_data = None
66 readme_node = None
66 readme_node = None
67 readme_filename = None
67 readme_filename = None
68 commit = self._get_landing_commit_or_none(db_repo)
68 commit = self._get_landing_commit_or_none(db_repo)
69 if commit:
69 if commit:
70 log.debug("Searching for a README file.")
70 log.debug("Searching for a README file.")
71 readme_node = ReadmeFinder(default_renderer).search(commit)
71 readme_node = ReadmeFinder(default_renderer).search(commit)
72 if readme_node:
72 if readme_node:
73 readme_data = self._render_readme_or_none(commit, readme_node)
73 readme_data = self._render_readme_or_none(commit, readme_node)
74 readme_filename = readme_node.path
74 readme_filename = readme_node.path
75 return readme_data, readme_filename
75 return readme_data, readme_filename
76
76
77 invalidator_context = CacheKey.repo_context_cache(
77 invalidator_context = CacheKey.repo_context_cache(
78 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
78 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
79
79
80 with invalidator_context as context:
80 with invalidator_context as context:
81 context.invalidate()
81 context.invalidate()
82 computed = context.compute()
82 computed = context.compute()
83
83
84 return computed
84 return computed
85
85
86 def _get_landing_commit_or_none(self, db_repo):
86 def _get_landing_commit_or_none(self, db_repo):
87 log.debug("Getting the landing commit.")
87 log.debug("Getting the landing commit.")
88 try:
88 try:
89 commit = db_repo.get_landing_commit()
89 commit = db_repo.get_landing_commit()
90 if not isinstance(commit, EmptyCommit):
90 if not isinstance(commit, EmptyCommit):
91 return commit
91 return commit
92 else:
92 else:
93 log.debug("Repository is empty, no README to render.")
93 log.debug("Repository is empty, no README to render.")
94 except CommitError:
94 except CommitError:
95 log.exception(
95 log.exception(
96 "Problem getting commit when trying to render the README.")
96 "Problem getting commit when trying to render the README.")
97
97
98 def _render_readme_or_none(self, commit, readme_node):
98 def _render_readme_or_none(self, commit, readme_node):
99 log.debug(
99 log.debug(
100 'Found README file `%s` rendering...', readme_node.path)
100 'Found README file `%s` rendering...', readme_node.path)
101 renderer = MarkupRenderer()
101 renderer = MarkupRenderer()
102 try:
102 try:
103 return renderer.render(
103 return renderer.render(
104 readme_node.content, filename=readme_node.path)
104 readme_node.content, filename=readme_node.path)
105 except Exception:
105 except Exception:
106 log.exception(
106 log.exception(
107 "Exception while trying to render the README")
107 "Exception while trying to render the README")
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasRepoPermissionAnyDecorator(
110 @HasRepoPermissionAnyDecorator(
111 'repository.read', 'repository.write', 'repository.admin')
111 'repository.read', 'repository.write', 'repository.admin')
112 def index(self, repo_name):
112 def index(self, repo_name):
113
113
114 # Prepare the clone URL
114 # Prepare the clone URL
115
115
116 username = ''
116 username = ''
117 if c.rhodecode_user.username != User.DEFAULT_USER:
117 if c.rhodecode_user.username != User.DEFAULT_USER:
118 username = safe_str(c.rhodecode_user.username)
118 username = safe_str(c.rhodecode_user.username)
119
119
120 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
120 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
121 if '{repo}' in _def_clone_uri:
121 if '{repo}' in _def_clone_uri:
122 _def_clone_uri_by_id = _def_clone_uri.replace(
122 _def_clone_uri_by_id = _def_clone_uri.replace(
123 '{repo}', '_{repoid}')
123 '{repo}', '_{repoid}')
124 elif '{repoid}' in _def_clone_uri:
124 elif '{repoid}' in _def_clone_uri:
125 _def_clone_uri_by_id = _def_clone_uri.replace(
125 _def_clone_uri_by_id = _def_clone_uri.replace(
126 '_{repoid}', '{repo}')
126 '_{repoid}', '{repo}')
127
127
128 c.clone_repo_url = c.rhodecode_db_repo.clone_url(
128 c.clone_repo_url = c.rhodecode_db_repo.clone_url(
129 user=username, uri_tmpl=_def_clone_uri)
129 user=username, uri_tmpl=_def_clone_uri)
130 c.clone_repo_url_id = c.rhodecode_db_repo.clone_url(
130 c.clone_repo_url_id = c.rhodecode_db_repo.clone_url(
131 user=username, uri_tmpl=_def_clone_uri_by_id)
131 user=username, uri_tmpl=_def_clone_uri_by_id)
132
132
133 # If enabled, get statistics data
133 # If enabled, get statistics data
134
134
135 c.show_stats = bool(c.rhodecode_db_repo.enable_statistics)
135 c.show_stats = bool(c.rhodecode_db_repo.enable_statistics)
136
136
137 stats = self.sa.query(Statistics)\
137 stats = self.sa.query(Statistics)\
138 .filter(Statistics.repository == c.rhodecode_db_repo)\
138 .filter(Statistics.repository == c.rhodecode_db_repo)\
139 .scalar()
139 .scalar()
140
140
141 c.stats_percentage = 0
141 c.stats_percentage = 0
142
142
143 if stats and stats.languages:
143 if stats and stats.languages:
144 c.no_data = False is c.rhodecode_db_repo.enable_statistics
144 c.no_data = False is c.rhodecode_db_repo.enable_statistics
145 lang_stats_d = json.loads(stats.languages)
145 lang_stats_d = json.loads(stats.languages)
146
146
147 # Sort first by decreasing count and second by the file extension,
147 # Sort first by decreasing count and second by the file extension,
148 # so we have a consistent output.
148 # so we have a consistent output.
149 lang_stats_items = sorted(lang_stats_d.iteritems(),
149 lang_stats_items = sorted(lang_stats_d.iteritems(),
150 key=lambda k: (-k[1], k[0]))[:10]
150 key=lambda k: (-k[1], k[0]))[:10]
151 lang_stats = [(x, {"count": y,
151 lang_stats = [(x, {"count": y,
152 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
152 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
153 for x, y in lang_stats_items]
153 for x, y in lang_stats_items]
154
154
155 c.trending_languages = json.dumps(lang_stats)
155 c.trending_languages = json.dumps(lang_stats)
156 else:
156 else:
157 c.no_data = True
157 c.no_data = True
158 c.trending_languages = json.dumps({})
158 c.trending_languages = json.dumps({})
159
159
160 c.enable_downloads = c.rhodecode_db_repo.enable_downloads
160 c.enable_downloads = c.rhodecode_db_repo.enable_downloads
161 c.repository_followers = self.scm_model.get_followers(
161 c.repository_followers = self.scm_model.get_followers(
162 c.rhodecode_db_repo)
162 c.rhodecode_db_repo)
163 c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo)
163 c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo)
164 c.repository_is_user_following = self.scm_model.is_following_repo(
164 c.repository_is_user_following = self.scm_model.is_following_repo(
165 c.repo_name, c.rhodecode_user.user_id)
165 c.repo_name, c.rhodecode_user.user_id)
166
166
167 if c.repository_requirements_missing:
167 if c.repository_requirements_missing:
168 return render('summary/missing_requirements.html')
168 return render('summary/missing_requirements.mako')
169
169
170 c.readme_data, c.readme_file = \
170 c.readme_data, c.readme_file = \
171 self.__get_readme_data(c.rhodecode_db_repo)
171 self.__get_readme_data(c.rhodecode_db_repo)
172
172
173 _load_changelog_summary()
173 _load_changelog_summary()
174
174
175 if request.is_xhr:
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 @LoginRequired()
180 @LoginRequired()
181 @XHRRequired()
181 @XHRRequired()
182 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
184 @jsonify
184 @jsonify
185 def repo_stats(self, repo_name, commit_id):
185 def repo_stats(self, repo_name, commit_id):
186 _namespace = caches.get_repo_namespace_key(
186 _namespace = caches.get_repo_namespace_key(
187 caches.SUMMARY_STATS, repo_name)
187 caches.SUMMARY_STATS, repo_name)
188 show_stats = bool(c.rhodecode_db_repo.enable_statistics)
188 show_stats = bool(c.rhodecode_db_repo.enable_statistics)
189 cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
189 cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
190 _cache_key = caches.compute_key_from_params(
190 _cache_key = caches.compute_key_from_params(
191 repo_name, commit_id, show_stats)
191 repo_name, commit_id, show_stats)
192
192
193 def compute_stats():
193 def compute_stats():
194 code_stats = {}
194 code_stats = {}
195 size = 0
195 size = 0
196 try:
196 try:
197 scm_instance = c.rhodecode_db_repo.scm_instance()
197 scm_instance = c.rhodecode_db_repo.scm_instance()
198 commit = scm_instance.get_commit(commit_id)
198 commit = scm_instance.get_commit(commit_id)
199
199
200 for node in commit.get_filenodes_generator():
200 for node in commit.get_filenodes_generator():
201 size += node.size
201 size += node.size
202 if not show_stats:
202 if not show_stats:
203 continue
203 continue
204 ext = lower(node.extension)
204 ext = lower(node.extension)
205 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
205 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
206 if ext_info:
206 if ext_info:
207 if ext in code_stats:
207 if ext in code_stats:
208 code_stats[ext]['count'] += 1
208 code_stats[ext]['count'] += 1
209 else:
209 else:
210 code_stats[ext] = {"count": 1, "desc": ext_info}
210 code_stats[ext] = {"count": 1, "desc": ext_info}
211 except EmptyRepositoryError:
211 except EmptyRepositoryError:
212 pass
212 pass
213 return {'size': h.format_byte_size_binary(size),
213 return {'size': h.format_byte_size_binary(size),
214 'code_stats': code_stats}
214 'code_stats': code_stats}
215
215
216 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
216 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
217 return stats
217 return stats
218
218
219 def _switcher_reference_data(self, repo_name, references, is_svn):
219 def _switcher_reference_data(self, repo_name, references, is_svn):
220 """Prepare reference data for given `references`"""
220 """Prepare reference data for given `references`"""
221 items = []
221 items = []
222 for name, commit_id in references.items():
222 for name, commit_id in references.items():
223 use_commit_id = '/' in name or is_svn
223 use_commit_id = '/' in name or is_svn
224 items.append({
224 items.append({
225 'name': name,
225 'name': name,
226 'commit_id': commit_id,
226 'commit_id': commit_id,
227 'files_url': h.url(
227 'files_url': h.url(
228 'files_home',
228 'files_home',
229 repo_name=repo_name,
229 repo_name=repo_name,
230 f_path=name if is_svn else '',
230 f_path=name if is_svn else '',
231 revision=commit_id if use_commit_id else name,
231 revision=commit_id if use_commit_id else name,
232 at=name)
232 at=name)
233 })
233 })
234 return items
234 return items
235
235
236 @LoginRequired()
236 @LoginRequired()
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 'repository.admin')
238 'repository.admin')
239 @jsonify
239 @jsonify
240 def repo_refs_data(self, repo_name):
240 def repo_refs_data(self, repo_name):
241 repo = c.rhodecode_repo
241 repo = c.rhodecode_repo
242 refs_to_create = [
242 refs_to_create = [
243 (_("Branch"), repo.branches, 'branch'),
243 (_("Branch"), repo.branches, 'branch'),
244 (_("Tag"), repo.tags, 'tag'),
244 (_("Tag"), repo.tags, 'tag'),
245 (_("Bookmark"), repo.bookmarks, 'book'),
245 (_("Bookmark"), repo.bookmarks, 'book'),
246 ]
246 ]
247 res = self._create_reference_data(repo, repo_name, refs_to_create)
247 res = self._create_reference_data(repo, repo_name, refs_to_create)
248 data = {
248 data = {
249 'more': False,
249 'more': False,
250 'results': res
250 'results': res
251 }
251 }
252 return data
252 return data
253
253
254 @LoginRequired()
254 @LoginRequired()
255 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
256 'repository.admin')
256 'repository.admin')
257 @jsonify
257 @jsonify
258 def repo_default_reviewers_data(self, repo_name):
258 def repo_default_reviewers_data(self, repo_name):
259 return {
259 return {
260 'reviewers': [utils.reviewer_as_json(
260 'reviewers': [utils.reviewer_as_json(
261 user=c.rhodecode_db_repo.user, reasons=None)]
261 user=c.rhodecode_db_repo.user, reasons=None)]
262 }
262 }
263
263
264 @jsonify
264 @jsonify
265 def repo_refs_changelog_data(self, repo_name):
265 def repo_refs_changelog_data(self, repo_name):
266 repo = c.rhodecode_repo
266 repo = c.rhodecode_repo
267
267
268 refs_to_create = [
268 refs_to_create = [
269 (_("Branches"), repo.branches, 'branch'),
269 (_("Branches"), repo.branches, 'branch'),
270 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
270 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
271 # TODO: enable when vcs can handle bookmarks filters
271 # TODO: enable when vcs can handle bookmarks filters
272 # (_("Bookmarks"), repo.bookmarks, "book"),
272 # (_("Bookmarks"), repo.bookmarks, "book"),
273 ]
273 ]
274 res = self._create_reference_data(repo, repo_name, refs_to_create)
274 res = self._create_reference_data(repo, repo_name, refs_to_create)
275 data = {
275 data = {
276 'more': False,
276 'more': False,
277 'results': res
277 'results': res
278 }
278 }
279 return data
279 return data
280
280
281 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
281 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
282 format_ref_id = utils.get_format_ref_id(repo)
282 format_ref_id = utils.get_format_ref_id(repo)
283
283
284 result = []
284 result = []
285 for title, refs, ref_type in refs_to_create:
285 for title, refs, ref_type in refs_to_create:
286 if refs:
286 if refs:
287 result.append({
287 result.append({
288 'text': title,
288 'text': title,
289 'children': self._create_reference_items(
289 'children': self._create_reference_items(
290 repo, full_repo_name, refs, ref_type, format_ref_id),
290 repo, full_repo_name, refs, ref_type, format_ref_id),
291 })
291 })
292 return result
292 return result
293
293
294 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
294 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
295 format_ref_id):
295 format_ref_id):
296 result = []
296 result = []
297 is_svn = h.is_svn(repo)
297 is_svn = h.is_svn(repo)
298 for ref_name, raw_id in refs.iteritems():
298 for ref_name, raw_id in refs.iteritems():
299 files_url = self._create_files_url(
299 files_url = self._create_files_url(
300 repo, full_repo_name, ref_name, raw_id, is_svn)
300 repo, full_repo_name, ref_name, raw_id, is_svn)
301 result.append({
301 result.append({
302 'text': ref_name,
302 'text': ref_name,
303 'id': format_ref_id(ref_name, raw_id),
303 'id': format_ref_id(ref_name, raw_id),
304 'raw_id': raw_id,
304 'raw_id': raw_id,
305 'type': ref_type,
305 'type': ref_type,
306 'files_url': files_url,
306 'files_url': files_url,
307 })
307 })
308 return result
308 return result
309
309
310 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id,
310 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id,
311 is_svn):
311 is_svn):
312 use_commit_id = '/' in ref_name or is_svn
312 use_commit_id = '/' in ref_name or is_svn
313 return h.url(
313 return h.url(
314 'files_home',
314 'files_home',
315 repo_name=full_repo_name,
315 repo_name=full_repo_name,
316 f_path=ref_name if is_svn else '',
316 f_path=ref_name if is_svn else '',
317 revision=raw_id if use_commit_id else ref_name,
317 revision=raw_id if use_commit_id else ref_name,
318 at=ref_name)
318 at=ref_name)
@@ -1,38 +1,38 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Tags controller for rhodecode
22 Tags controller for rhodecode
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.controllers.base_references import BaseReferencesController
27 from rhodecode.controllers.base_references import BaseReferencesController
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class TagsController(BaseReferencesController):
32 class TagsController(BaseReferencesController):
33
33
34 partials_template = 'tags/tags_data.html'
34 partials_template = 'tags/tags_data.mako'
35 template = 'tags/tags.html'
35 template = 'tags/tags.mako'
36
36
37 def _get_reference_items(self, repo):
37 def _get_reference_items(self, repo):
38 return repo.tags.items()
38 return repo.tags.items()
@@ -1,43 +1,43 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Users profile controller
22 Users profile controller
23 """
23 """
24
24
25 from pylons import tmpl_context as c
25 from pylons import tmpl_context as c
26 from webob.exc import HTTPNotFound
26 from webob.exc import HTTPNotFound
27
27
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous
29 from rhodecode.lib.base import BaseController, render
29 from rhodecode.lib.base import BaseController, render
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.user import UserModel
31 from rhodecode.model.user import UserModel
32
32
33
33
34 class UsersController(BaseController):
34 class UsersController(BaseController):
35 @LoginRequired()
35 @LoginRequired()
36 @NotAnonymous()
36 @NotAnonymous()
37 def user_profile(self, username):
37 def user_profile(self, username):
38 c.user = UserModel().get_by_username(username)
38 c.user = UserModel().get_by_username(username)
39 if not c.user or c.user.username == User.DEFAULT_USER:
39 if not c.user or c.user.username == User.DEFAULT_USER:
40 raise HTTPNotFound()
40 raise HTTPNotFound()
41
41
42 c.active = 'user_profile'
42 c.active = 'user_profile'
43 return render('users/user.html')
43 return render('users/user.mako')
@@ -1,238 +1,238 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from rhodecode.model.db import Repository, Integration, RepoGroup
23 from rhodecode.model.db import Repository, Integration, RepoGroup
24 from rhodecode.config.routing import (
24 from rhodecode.config.routing import (
25 ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
25 ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
26 from rhodecode.integrations import integration_type_registry
26 from rhodecode.integrations import integration_type_registry
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 def includeme(config):
31 def includeme(config):
32
32
33 # global integrations
33 # global integrations
34
34
35 config.add_route('global_integrations_new',
35 config.add_route('global_integrations_new',
36 ADMIN_PREFIX + '/integrations/new')
36 ADMIN_PREFIX + '/integrations/new')
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
38 attr='new_integration',
38 attr='new_integration',
39 renderer='rhodecode:templates/admin/integrations/new.html',
39 renderer='rhodecode:templates/admin/integrations/new.mako',
40 request_method='GET',
40 request_method='GET',
41 route_name='global_integrations_new')
41 route_name='global_integrations_new')
42
42
43 config.add_route('global_integrations_home',
43 config.add_route('global_integrations_home',
44 ADMIN_PREFIX + '/integrations')
44 ADMIN_PREFIX + '/integrations')
45 config.add_route('global_integrations_list',
45 config.add_route('global_integrations_list',
46 ADMIN_PREFIX + '/integrations/{integration}')
46 ADMIN_PREFIX + '/integrations/{integration}')
47 for route_name in ['global_integrations_home', 'global_integrations_list']:
47 for route_name in ['global_integrations_home', 'global_integrations_list']:
48 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
48 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
49 attr='index',
49 attr='index',
50 renderer='rhodecode:templates/admin/integrations/list.html',
50 renderer='rhodecode:templates/admin/integrations/list.mako',
51 request_method='GET',
51 request_method='GET',
52 route_name=route_name)
52 route_name=route_name)
53
53
54 config.add_route('global_integrations_create',
54 config.add_route('global_integrations_create',
55 ADMIN_PREFIX + '/integrations/{integration}/new',
55 ADMIN_PREFIX + '/integrations/{integration}/new',
56 custom_predicates=(valid_integration,))
56 custom_predicates=(valid_integration,))
57 config.add_route('global_integrations_edit',
57 config.add_route('global_integrations_edit',
58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
59 custom_predicates=(valid_integration,))
59 custom_predicates=(valid_integration,))
60
60
61
61
62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
64 attr='settings_get',
64 attr='settings_get',
65 renderer='rhodecode:templates/admin/integrations/form.html',
65 renderer='rhodecode:templates/admin/integrations/form.mako',
66 request_method='GET',
66 request_method='GET',
67 route_name=route_name)
67 route_name=route_name)
68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
69 attr='settings_post',
69 attr='settings_post',
70 renderer='rhodecode:templates/admin/integrations/form.html',
70 renderer='rhodecode:templates/admin/integrations/form.mako',
71 request_method='POST',
71 request_method='POST',
72 route_name=route_name)
72 route_name=route_name)
73
73
74
74
75 # repo group integrations
75 # repo group integrations
76 config.add_route('repo_group_integrations_home',
76 config.add_route('repo_group_integrations_home',
77 add_route_requirements(
77 add_route_requirements(
78 '{repo_group_name}/settings/integrations',
78 '{repo_group_name}/settings/integrations',
79 URL_NAME_REQUIREMENTS
79 URL_NAME_REQUIREMENTS
80 ),
80 ),
81 custom_predicates=(valid_repo_group,)
81 custom_predicates=(valid_repo_group,)
82 )
82 )
83 config.add_route('repo_group_integrations_list',
83 config.add_route('repo_group_integrations_list',
84 add_route_requirements(
84 add_route_requirements(
85 '{repo_group_name}/settings/integrations/{integration}',
85 '{repo_group_name}/settings/integrations/{integration}',
86 URL_NAME_REQUIREMENTS
86 URL_NAME_REQUIREMENTS
87 ),
87 ),
88 custom_predicates=(valid_repo_group, valid_integration))
88 custom_predicates=(valid_repo_group, valid_integration))
89 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
89 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
90 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
90 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
91 attr='index',
91 attr='index',
92 renderer='rhodecode:templates/admin/integrations/list.html',
92 renderer='rhodecode:templates/admin/integrations/list.mako',
93 request_method='GET',
93 request_method='GET',
94 route_name=route_name)
94 route_name=route_name)
95
95
96 config.add_route('repo_group_integrations_new',
96 config.add_route('repo_group_integrations_new',
97 add_route_requirements(
97 add_route_requirements(
98 '{repo_group_name}/settings/integrations/new',
98 '{repo_group_name}/settings/integrations/new',
99 URL_NAME_REQUIREMENTS
99 URL_NAME_REQUIREMENTS
100 ),
100 ),
101 custom_predicates=(valid_repo_group,))
101 custom_predicates=(valid_repo_group,))
102 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
102 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
103 attr='new_integration',
103 attr='new_integration',
104 renderer='rhodecode:templates/admin/integrations/new.html',
104 renderer='rhodecode:templates/admin/integrations/new.mako',
105 request_method='GET',
105 request_method='GET',
106 route_name='repo_group_integrations_new')
106 route_name='repo_group_integrations_new')
107
107
108 config.add_route('repo_group_integrations_create',
108 config.add_route('repo_group_integrations_create',
109 add_route_requirements(
109 add_route_requirements(
110 '{repo_group_name}/settings/integrations/{integration}/new',
110 '{repo_group_name}/settings/integrations/{integration}/new',
111 URL_NAME_REQUIREMENTS
111 URL_NAME_REQUIREMENTS
112 ),
112 ),
113 custom_predicates=(valid_repo_group, valid_integration))
113 custom_predicates=(valid_repo_group, valid_integration))
114 config.add_route('repo_group_integrations_edit',
114 config.add_route('repo_group_integrations_edit',
115 add_route_requirements(
115 add_route_requirements(
116 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
116 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
117 URL_NAME_REQUIREMENTS
117 URL_NAME_REQUIREMENTS
118 ),
118 ),
119 custom_predicates=(valid_repo_group, valid_integration))
119 custom_predicates=(valid_repo_group, valid_integration))
120 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
120 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
121 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
121 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
122 attr='settings_get',
122 attr='settings_get',
123 renderer='rhodecode:templates/admin/integrations/form.html',
123 renderer='rhodecode:templates/admin/integrations/form.mako',
124 request_method='GET',
124 request_method='GET',
125 route_name=route_name)
125 route_name=route_name)
126 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
126 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
127 attr='settings_post',
127 attr='settings_post',
128 renderer='rhodecode:templates/admin/integrations/form.html',
128 renderer='rhodecode:templates/admin/integrations/form.mako',
129 request_method='POST',
129 request_method='POST',
130 route_name=route_name)
130 route_name=route_name)
131
131
132
132
133 # repo integrations
133 # repo integrations
134 config.add_route('repo_integrations_home',
134 config.add_route('repo_integrations_home',
135 add_route_requirements(
135 add_route_requirements(
136 '{repo_name}/settings/integrations',
136 '{repo_name}/settings/integrations',
137 URL_NAME_REQUIREMENTS
137 URL_NAME_REQUIREMENTS
138 ),
138 ),
139 custom_predicates=(valid_repo,))
139 custom_predicates=(valid_repo,))
140 config.add_route('repo_integrations_list',
140 config.add_route('repo_integrations_list',
141 add_route_requirements(
141 add_route_requirements(
142 '{repo_name}/settings/integrations/{integration}',
142 '{repo_name}/settings/integrations/{integration}',
143 URL_NAME_REQUIREMENTS
143 URL_NAME_REQUIREMENTS
144 ),
144 ),
145 custom_predicates=(valid_repo, valid_integration))
145 custom_predicates=(valid_repo, valid_integration))
146 for route_name in ['repo_integrations_home', 'repo_integrations_list']:
146 for route_name in ['repo_integrations_home', 'repo_integrations_list']:
147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
148 attr='index',
148 attr='index',
149 request_method='GET',
149 request_method='GET',
150 renderer='rhodecode:templates/admin/integrations/list.html',
150 renderer='rhodecode:templates/admin/integrations/list.mako',
151 route_name=route_name)
151 route_name=route_name)
152
152
153 config.add_route('repo_integrations_new',
153 config.add_route('repo_integrations_new',
154 add_route_requirements(
154 add_route_requirements(
155 '{repo_name}/settings/integrations/new',
155 '{repo_name}/settings/integrations/new',
156 URL_NAME_REQUIREMENTS
156 URL_NAME_REQUIREMENTS
157 ),
157 ),
158 custom_predicates=(valid_repo,))
158 custom_predicates=(valid_repo,))
159 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
159 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
160 attr='new_integration',
160 attr='new_integration',
161 renderer='rhodecode:templates/admin/integrations/new.html',
161 renderer='rhodecode:templates/admin/integrations/new.mako',
162 request_method='GET',
162 request_method='GET',
163 route_name='repo_integrations_new')
163 route_name='repo_integrations_new')
164
164
165 config.add_route('repo_integrations_create',
165 config.add_route('repo_integrations_create',
166 add_route_requirements(
166 add_route_requirements(
167 '{repo_name}/settings/integrations/{integration}/new',
167 '{repo_name}/settings/integrations/{integration}/new',
168 URL_NAME_REQUIREMENTS
168 URL_NAME_REQUIREMENTS
169 ),
169 ),
170 custom_predicates=(valid_repo, valid_integration))
170 custom_predicates=(valid_repo, valid_integration))
171 config.add_route('repo_integrations_edit',
171 config.add_route('repo_integrations_edit',
172 add_route_requirements(
172 add_route_requirements(
173 '{repo_name}/settings/integrations/{integration}/{integration_id}',
173 '{repo_name}/settings/integrations/{integration}/{integration_id}',
174 URL_NAME_REQUIREMENTS
174 URL_NAME_REQUIREMENTS
175 ),
175 ),
176 custom_predicates=(valid_repo, valid_integration))
176 custom_predicates=(valid_repo, valid_integration))
177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
179 attr='settings_get',
179 attr='settings_get',
180 renderer='rhodecode:templates/admin/integrations/form.html',
180 renderer='rhodecode:templates/admin/integrations/form.mako',
181 request_method='GET',
181 request_method='GET',
182 route_name=route_name)
182 route_name=route_name)
183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
184 attr='settings_post',
184 attr='settings_post',
185 renderer='rhodecode:templates/admin/integrations/form.html',
185 renderer='rhodecode:templates/admin/integrations/form.mako',
186 request_method='POST',
186 request_method='POST',
187 route_name=route_name)
187 route_name=route_name)
188
188
189
189
190 def valid_repo(info, request):
190 def valid_repo(info, request):
191 repo = Repository.get_by_repo_name(info['match']['repo_name'])
191 repo = Repository.get_by_repo_name(info['match']['repo_name'])
192 if repo:
192 if repo:
193 return True
193 return True
194
194
195
195
196 def valid_repo_group(info, request):
196 def valid_repo_group(info, request):
197 repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name'])
197 repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name'])
198 if repo_group:
198 if repo_group:
199 return True
199 return True
200 return False
200 return False
201
201
202
202
203 def valid_integration(info, request):
203 def valid_integration(info, request):
204 integration_type = info['match']['integration']
204 integration_type = info['match']['integration']
205 integration_id = info['match'].get('integration_id')
205 integration_id = info['match'].get('integration_id')
206 repo_name = info['match'].get('repo_name')
206 repo_name = info['match'].get('repo_name')
207 repo_group_name = info['match'].get('repo_group_name')
207 repo_group_name = info['match'].get('repo_group_name')
208
208
209 if integration_type not in integration_type_registry:
209 if integration_type not in integration_type_registry:
210 return False
210 return False
211
211
212 repo, repo_group = None, None
212 repo, repo_group = None, None
213 if repo_name:
213 if repo_name:
214 repo = Repository.get_by_repo_name(repo_name)
214 repo = Repository.get_by_repo_name(repo_name)
215 if not repo:
215 if not repo:
216 return False
216 return False
217
217
218 if repo_group_name:
218 if repo_group_name:
219 repo_group = RepoGroup.get_by_group_name(repo_group_name)
219 repo_group = RepoGroup.get_by_group_name(repo_group_name)
220 if not repo_group:
220 if not repo_group:
221 return False
221 return False
222
222
223 if repo_name and repo_group:
223 if repo_name and repo_group:
224 raise Exception('Either repo or repo_group can be set, not both')
224 raise Exception('Either repo or repo_group can be set, not both')
225
225
226
226
227 if integration_id:
227 if integration_id:
228 integration = Integration.get(integration_id)
228 integration = Integration.get(integration_id)
229 if not integration:
229 if not integration:
230 return False
230 return False
231 if integration.integration_type != integration_type:
231 if integration.integration_type != integration_type:
232 return False
232 return False
233 if repo and repo.repo_id != integration.repo_id:
233 if repo and repo.repo_id != integration.repo_id:
234 return False
234 return False
235 if repo_group and repo_group.group_id != integration.repo_group_id:
235 if repo_group and repo_group.group_id != integration.repo_group_id:
236 return False
236 return False
237
237
238 return True
238 return True
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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