##// END OF EJS Templates
cleanup: replace redirect with WebOb exceptions...
Søren Løvborg -
r5543:d9b78d8f default
parent child Browse files
Show More
@@ -1,149 +1,149 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.auth_settings
15 kallithea.controllers.admin.auth_settings
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pluggable authentication controller for Kallithea
18 pluggable authentication controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Nov 26, 2010
22 :created_on: Nov 26, 2010
23 :author: akesterson
23 :author: akesterson
24 """
24 """
25
25
26 import logging
26 import logging
27 import formencode.htmlfill
27 import formencode.htmlfill
28 import traceback
28 import traceback
29
29
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from webob.exc import HTTPFound
33
33
34 from kallithea.lib import helpers as h
34 from kallithea.lib import helpers as h
35 from kallithea.lib.compat import formatted_json
35 from kallithea.lib.compat import formatted_json
36 from kallithea.lib.base import BaseController, render
36 from kallithea.lib.base import BaseController, render
37 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
37 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from kallithea.lib import auth_modules
38 from kallithea.lib import auth_modules
39 from kallithea.model.forms import AuthSettingsForm
39 from kallithea.model.forms import AuthSettingsForm
40 from kallithea.model.db import Setting
40 from kallithea.model.db import Setting
41 from kallithea.model.meta import Session
41 from kallithea.model.meta import Session
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class AuthSettingsController(BaseController):
46 class AuthSettingsController(BaseController):
47
47
48 @LoginRequired()
48 @LoginRequired()
49 @HasPermissionAllDecorator('hg.admin')
49 @HasPermissionAllDecorator('hg.admin')
50 def __before__(self):
50 def __before__(self):
51 super(AuthSettingsController, self).__before__()
51 super(AuthSettingsController, self).__before__()
52
52
53 def __load_defaults(self):
53 def __load_defaults(self):
54 c.available_plugins = [
54 c.available_plugins = [
55 'kallithea.lib.auth_modules.auth_internal',
55 'kallithea.lib.auth_modules.auth_internal',
56 'kallithea.lib.auth_modules.auth_container',
56 'kallithea.lib.auth_modules.auth_container',
57 'kallithea.lib.auth_modules.auth_ldap',
57 'kallithea.lib.auth_modules.auth_ldap',
58 'kallithea.lib.auth_modules.auth_crowd',
58 'kallithea.lib.auth_modules.auth_crowd',
59 'kallithea.lib.auth_modules.auth_pam'
59 'kallithea.lib.auth_modules.auth_pam'
60 ]
60 ]
61 c.enabled_plugins = Setting.get_auth_plugins()
61 c.enabled_plugins = Setting.get_auth_plugins()
62
62
63 def __render(self, defaults, errors):
63 def __render(self, defaults, errors):
64 c.defaults = {}
64 c.defaults = {}
65 c.plugin_settings = {}
65 c.plugin_settings = {}
66 c.plugin_shortnames = {}
66 c.plugin_shortnames = {}
67
67
68 for module in c.enabled_plugins:
68 for module in c.enabled_plugins:
69 plugin = auth_modules.loadplugin(module)
69 plugin = auth_modules.loadplugin(module)
70 plugin_name = plugin.name
70 plugin_name = plugin.name
71 c.plugin_shortnames[module] = plugin_name
71 c.plugin_shortnames[module] = plugin_name
72 c.plugin_settings[module] = plugin.plugin_settings()
72 c.plugin_settings[module] = plugin.plugin_settings()
73 for v in c.plugin_settings[module]:
73 for v in c.plugin_settings[module]:
74 fullname = ("auth_" + plugin_name + "_" + v["name"])
74 fullname = ("auth_" + plugin_name + "_" + v["name"])
75 if "default" in v:
75 if "default" in v:
76 c.defaults[fullname] = v["default"]
76 c.defaults[fullname] = v["default"]
77 # Current values will be the default on the form, if there are any
77 # Current values will be the default on the form, if there are any
78 setting = Setting.get_by_name(fullname)
78 setting = Setting.get_by_name(fullname)
79 if setting is not None:
79 if setting is not None:
80 c.defaults[fullname] = setting.app_settings_value
80 c.defaults[fullname] = setting.app_settings_value
81 # we want to show , separated list of enabled plugins
81 # we want to show , separated list of enabled plugins
82 c.defaults['auth_plugins'] = ','.join(c.enabled_plugins)
82 c.defaults['auth_plugins'] = ','.join(c.enabled_plugins)
83
83
84 if defaults:
84 if defaults:
85 c.defaults.update(defaults)
85 c.defaults.update(defaults)
86
86
87 log.debug(formatted_json(defaults))
87 log.debug(formatted_json(defaults))
88 return formencode.htmlfill.render(
88 return formencode.htmlfill.render(
89 render('admin/auth/auth_settings.html'),
89 render('admin/auth/auth_settings.html'),
90 defaults=c.defaults,
90 defaults=c.defaults,
91 errors=errors,
91 errors=errors,
92 prefix_error=False,
92 prefix_error=False,
93 encoding="UTF-8",
93 encoding="UTF-8",
94 force_defaults=False)
94 force_defaults=False)
95
95
96 def index(self):
96 def index(self):
97 self.__load_defaults()
97 self.__load_defaults()
98 return self.__render(defaults=None, errors=None)
98 return self.__render(defaults=None, errors=None)
99
99
100 def auth_settings(self):
100 def auth_settings(self):
101 """POST create and store auth settings"""
101 """POST create and store auth settings"""
102 self.__load_defaults()
102 self.__load_defaults()
103 log.debug("POST Result: %s", formatted_json(dict(request.POST)))
103 log.debug("POST Result: %s", formatted_json(dict(request.POST)))
104
104
105 # First, parse only the plugin list (not the plugin settings).
105 # First, parse only the plugin list (not the plugin settings).
106 _auth_plugins_validator = AuthSettingsForm([]).fields['auth_plugins']
106 _auth_plugins_validator = AuthSettingsForm([]).fields['auth_plugins']
107 try:
107 try:
108 new_enabled_plugins = _auth_plugins_validator.to_python(request.POST.get('auth_plugins'))
108 new_enabled_plugins = _auth_plugins_validator.to_python(request.POST.get('auth_plugins'))
109 except formencode.Invalid:
109 except formencode.Invalid:
110 # User provided an invalid plugin list. Just fall back to
110 # User provided an invalid plugin list. Just fall back to
111 # the list of currently enabled plugins. (We'll re-validate
111 # the list of currently enabled plugins. (We'll re-validate
112 # and show an error message to the user, below.)
112 # and show an error message to the user, below.)
113 pass
113 pass
114 else:
114 else:
115 # Hide plugins that the user has asked to be disabled, but
115 # Hide plugins that the user has asked to be disabled, but
116 # do not show plugins that the user has asked to be enabled
116 # do not show plugins that the user has asked to be enabled
117 # (yet), since that'll cause validation errors and/or wrong
117 # (yet), since that'll cause validation errors and/or wrong
118 # settings being applied (e.g. checkboxes being cleared),
118 # settings being applied (e.g. checkboxes being cleared),
119 # since the plugin settings will not be in the POST data.
119 # since the plugin settings will not be in the POST data.
120 c.enabled_plugins = [ p for p in c.enabled_plugins if p in new_enabled_plugins ]
120 c.enabled_plugins = [ p for p in c.enabled_plugins if p in new_enabled_plugins ]
121
121
122 # Next, parse everything including plugin settings.
122 # Next, parse everything including plugin settings.
123 _form = AuthSettingsForm(c.enabled_plugins)()
123 _form = AuthSettingsForm(c.enabled_plugins)()
124
124
125 try:
125 try:
126 form_result = _form.to_python(dict(request.POST))
126 form_result = _form.to_python(dict(request.POST))
127 for k, v in form_result.items():
127 for k, v in form_result.items():
128 if k == 'auth_plugins':
128 if k == 'auth_plugins':
129 # we want to store it comma separated inside our settings
129 # we want to store it comma separated inside our settings
130 v = ','.join(v)
130 v = ','.join(v)
131 log.debug("%s = %s", k, str(v))
131 log.debug("%s = %s", k, str(v))
132 setting = Setting.create_or_update(k, v)
132 setting = Setting.create_or_update(k, v)
133 Session().add(setting)
133 Session().add(setting)
134 Session().commit()
134 Session().commit()
135 h.flash(_('Auth settings updated successfully'),
135 h.flash(_('Auth settings updated successfully'),
136 category='success')
136 category='success')
137 except formencode.Invalid as errors:
137 except formencode.Invalid as errors:
138 log.error(traceback.format_exc())
138 log.error(traceback.format_exc())
139 e = errors.error_dict or {}
139 e = errors.error_dict or {}
140 return self.__render(
140 return self.__render(
141 defaults=errors.value,
141 defaults=errors.value,
142 errors=e,
142 errors=e,
143 )
143 )
144 except Exception:
144 except Exception:
145 log.error(traceback.format_exc())
145 log.error(traceback.format_exc())
146 h.flash(_('error occurred during update of auth settings'),
146 h.flash(_('error occurred during update of auth settings'),
147 category='error')
147 category='error')
148
148
149 return redirect(url('auth_home'))
149 raise HTTPFound(location=url('auth_home'))
@@ -1,132 +1,132 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.defaults
15 kallithea.controllers.admin.defaults
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 default settings controller for Kallithea
18 default settings controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 27, 2010
22 :created_on: Apr 27, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31 from formencode import htmlfill
31 from formencode import htmlfill
32
32
33 from pylons import request, tmpl_context as c, url
33 from pylons import request, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.base import BaseController, render
39 from kallithea.lib.base import BaseController, render
40 from kallithea.model.forms import DefaultsForm
40 from kallithea.model.forms import DefaultsForm
41 from kallithea.model.meta import Session
41 from kallithea.model.meta import Session
42 from kallithea import BACKENDS
42 from kallithea import BACKENDS
43 from kallithea.model.db import Setting
43 from kallithea.model.db import Setting
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class DefaultsController(BaseController):
48 class DefaultsController(BaseController):
49 """REST Controller styled on the Atom Publishing Protocol"""
49 """REST Controller styled on the Atom Publishing Protocol"""
50 # To properly map this controller, ensure your config/routing.py
50 # To properly map this controller, ensure your config/routing.py
51 # file has a resource setup:
51 # file has a resource setup:
52 # map.resource('default', 'defaults')
52 # map.resource('default', 'defaults')
53
53
54 @LoginRequired()
54 @LoginRequired()
55 @HasPermissionAllDecorator('hg.admin')
55 @HasPermissionAllDecorator('hg.admin')
56 def __before__(self):
56 def __before__(self):
57 super(DefaultsController, self).__before__()
57 super(DefaultsController, self).__before__()
58
58
59 def index(self, format='html'):
59 def index(self, format='html'):
60 """GET /defaults: All items in the collection"""
60 """GET /defaults: All items in the collection"""
61 # url('defaults')
61 # url('defaults')
62 c.backends = BACKENDS.keys()
62 c.backends = BACKENDS.keys()
63 defaults = Setting.get_default_repo_settings()
63 defaults = Setting.get_default_repo_settings()
64
64
65 return htmlfill.render(
65 return htmlfill.render(
66 render('admin/defaults/defaults.html'),
66 render('admin/defaults/defaults.html'),
67 defaults=defaults,
67 defaults=defaults,
68 encoding="UTF-8",
68 encoding="UTF-8",
69 force_defaults=False
69 force_defaults=False
70 )
70 )
71
71
72 def create(self):
72 def create(self):
73 """POST /defaults: Create a new item"""
73 """POST /defaults: Create a new item"""
74 # url('defaults')
74 # url('defaults')
75
75
76 def new(self, format='html'):
76 def new(self, format='html'):
77 """GET /defaults/new: Form to create a new item"""
77 """GET /defaults/new: Form to create a new item"""
78 # url('new_default')
78 # url('new_default')
79
79
80 def update(self, id):
80 def update(self, id):
81 """PUT /defaults/id: Update an existing item"""
81 """PUT /defaults/id: Update an existing item"""
82 # Forms posted to this method should contain a hidden field:
82 # Forms posted to this method should contain a hidden field:
83 # <input type="hidden" name="_method" value="PUT" />
83 # <input type="hidden" name="_method" value="PUT" />
84 # Or using helpers:
84 # Or using helpers:
85 # h.form(url('default', id=ID),
85 # h.form(url('default', id=ID),
86 # method='put')
86 # method='put')
87 # url('default', id=ID)
87 # url('default', id=ID)
88
88
89 _form = DefaultsForm()()
89 _form = DefaultsForm()()
90
90
91 try:
91 try:
92 form_result = _form.to_python(dict(request.POST))
92 form_result = _form.to_python(dict(request.POST))
93 for k, v in form_result.iteritems():
93 for k, v in form_result.iteritems():
94 setting = Setting.create_or_update(k, v)
94 setting = Setting.create_or_update(k, v)
95 Session().add(setting)
95 Session().add(setting)
96 Session().commit()
96 Session().commit()
97 h.flash(_('Default settings updated successfully'),
97 h.flash(_('Default settings updated successfully'),
98 category='success')
98 category='success')
99
99
100 except formencode.Invalid as errors:
100 except formencode.Invalid as errors:
101 defaults = errors.value
101 defaults = errors.value
102
102
103 return htmlfill.render(
103 return htmlfill.render(
104 render('admin/defaults/defaults.html'),
104 render('admin/defaults/defaults.html'),
105 defaults=defaults,
105 defaults=defaults,
106 errors=errors.error_dict or {},
106 errors=errors.error_dict or {},
107 prefix_error=False,
107 prefix_error=False,
108 encoding="UTF-8",
108 encoding="UTF-8",
109 force_defaults=False)
109 force_defaults=False)
110 except Exception:
110 except Exception:
111 log.error(traceback.format_exc())
111 log.error(traceback.format_exc())
112 h.flash(_('Error occurred during update of defaults'),
112 h.flash(_('Error occurred during update of defaults'),
113 category='error')
113 category='error')
114
114
115 return redirect(url('defaults'))
115 raise HTTPFound(location=url('defaults'))
116
116
117 def delete(self, id):
117 def delete(self, id):
118 """DELETE /defaults/id: Delete an existing item"""
118 """DELETE /defaults/id: Delete 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="DELETE" />
120 # <input type="hidden" name="_method" value="DELETE" />
121 # Or using helpers:
121 # Or using helpers:
122 # h.form(url('default', id=ID),
122 # h.form(url('default', id=ID),
123 # method='delete')
123 # method='delete')
124 # url('default', id=ID)
124 # url('default', id=ID)
125
125
126 def show(self, id, format='html'):
126 def show(self, id, format='html'):
127 """GET /defaults/id: Show a specific item"""
127 """GET /defaults/id: Show a specific item"""
128 # url('default', id=ID)
128 # url('default', id=ID)
129
129
130 def edit(self, id, format='html'):
130 def edit(self, id, format='html'):
131 """GET /defaults/id/edit: Form to edit an existing item"""
131 """GET /defaults/id/edit: Form to edit an existing item"""
132 # url('edit_default', id=ID)
132 # url('edit_default', id=ID)
@@ -1,293 +1,292 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.gists
15 kallithea.controllers.admin.gists
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 gist controller for Kallithea
18 gist controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: May 9, 2013
22 :created_on: May 9, 2013
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import time
28 import time
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import formencode.htmlfill
31 import formencode.htmlfill
32
32
33 from pylons import request, response, tmpl_context as c, url
33 from pylons import request, response, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
36
36
37 from kallithea.model.forms import GistForm
37 from kallithea.model.forms import GistForm
38 from kallithea.model.gist import GistModel
38 from kallithea.model.gist import GistModel
39 from kallithea.model.meta import Session
39 from kallithea.model.meta import Session
40 from kallithea.model.db import Gist, User
40 from kallithea.model.db import Gist, User
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.base import BaseController, render
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.auth import LoginRequired, NotAnonymous
43 from kallithea.lib.auth import LoginRequired, NotAnonymous
44 from kallithea.lib.utils import jsonify
44 from kallithea.lib.utils import jsonify
45 from kallithea.lib.utils2 import safe_int, time_to_datetime
45 from kallithea.lib.utils2 import safe_int, time_to_datetime
46 from kallithea.lib.helpers import Page
46 from kallithea.lib.helpers import Page
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 from sqlalchemy.sql.expression import or_
47 from sqlalchemy.sql.expression import or_
49 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
50
49
51 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
52
51
53
52
54 class GistsController(BaseController):
53 class GistsController(BaseController):
55 """REST Controller styled on the Atom Publishing Protocol"""
54 """REST Controller styled on the Atom Publishing Protocol"""
56
55
57 def __load_defaults(self, extra_values=None):
56 def __load_defaults(self, extra_values=None):
58 c.lifetime_values = [
57 c.lifetime_values = [
59 (str(-1), _('Forever')),
58 (str(-1), _('Forever')),
60 (str(5), _('5 minutes')),
59 (str(5), _('5 minutes')),
61 (str(60), _('1 hour')),
60 (str(60), _('1 hour')),
62 (str(60 * 24), _('1 day')),
61 (str(60 * 24), _('1 day')),
63 (str(60 * 24 * 30), _('1 month')),
62 (str(60 * 24 * 30), _('1 month')),
64 ]
63 ]
65 if extra_values:
64 if extra_values:
66 c.lifetime_values.append(extra_values)
65 c.lifetime_values.append(extra_values)
67 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68
67
69 @LoginRequired()
68 @LoginRequired()
70 def index(self):
69 def index(self):
71 """GET /admin/gists: All items in the collection"""
70 """GET /admin/gists: All items in the collection"""
72 # url('gists')
71 # url('gists')
73 not_default_user = c.authuser.username != User.DEFAULT_USER
72 not_default_user = c.authuser.username != User.DEFAULT_USER
74 c.show_private = request.GET.get('private') and not_default_user
73 c.show_private = request.GET.get('private') and not_default_user
75 c.show_public = request.GET.get('public') and not_default_user
74 c.show_public = request.GET.get('public') and not_default_user
76
75
77 gists = Gist().query()\
76 gists = Gist().query()\
78 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
77 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
79 .order_by(Gist.created_on.desc())
78 .order_by(Gist.created_on.desc())
80
79
81 # MY private
80 # MY private
82 if c.show_private and not c.show_public:
81 if c.show_private and not c.show_public:
83 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
82 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
84 .filter(Gist.gist_owner == c.authuser.user_id)
83 .filter(Gist.gist_owner == c.authuser.user_id)
85 # MY public
84 # MY public
86 elif c.show_public and not c.show_private:
85 elif c.show_public and not c.show_private:
87 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
86 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
88 .filter(Gist.gist_owner == c.authuser.user_id)
87 .filter(Gist.gist_owner == c.authuser.user_id)
89
88
90 # MY public+private
89 # MY public+private
91 elif c.show_private and c.show_public:
90 elif c.show_private and c.show_public:
92 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
91 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
93 Gist.gist_type == Gist.GIST_PRIVATE))\
92 Gist.gist_type == Gist.GIST_PRIVATE))\
94 .filter(Gist.gist_owner == c.authuser.user_id)
93 .filter(Gist.gist_owner == c.authuser.user_id)
95
94
96 # default show ALL public gists
95 # default show ALL public gists
97 if not c.show_public and not c.show_private:
96 if not c.show_public and not c.show_private:
98 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
97 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
99
98
100 c.gists = gists
99 c.gists = gists
101 p = safe_int(request.GET.get('page', 1), 1)
100 p = safe_int(request.GET.get('page', 1), 1)
102 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
101 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
103 return render('admin/gists/index.html')
102 return render('admin/gists/index.html')
104
103
105 @LoginRequired()
104 @LoginRequired()
106 @NotAnonymous()
105 @NotAnonymous()
107 def create(self):
106 def create(self):
108 """POST /admin/gists: Create a new item"""
107 """POST /admin/gists: Create a new item"""
109 # url('gists')
108 # url('gists')
110 self.__load_defaults()
109 self.__load_defaults()
111 gist_form = GistForm([x[0] for x in c.lifetime_values])()
110 gist_form = GistForm([x[0] for x in c.lifetime_values])()
112 try:
111 try:
113 form_result = gist_form.to_python(dict(request.POST))
112 form_result = gist_form.to_python(dict(request.POST))
114 #TODO: multiple files support, from the form
113 #TODO: multiple files support, from the form
115 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
114 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
116 nodes = {
115 nodes = {
117 filename: {
116 filename: {
118 'content': form_result['content'],
117 'content': form_result['content'],
119 'lexer': form_result['mimetype'] # None is autodetect
118 'lexer': form_result['mimetype'] # None is autodetect
120 }
119 }
121 }
120 }
122 _public = form_result['public']
121 _public = form_result['public']
123 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
122 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
124 gist = GistModel().create(
123 gist = GistModel().create(
125 description=form_result['description'],
124 description=form_result['description'],
126 owner=c.authuser.user_id,
125 owner=c.authuser.user_id,
127 gist_mapping=nodes,
126 gist_mapping=nodes,
128 gist_type=gist_type,
127 gist_type=gist_type,
129 lifetime=form_result['lifetime']
128 lifetime=form_result['lifetime']
130 )
129 )
131 Session().commit()
130 Session().commit()
132 new_gist_id = gist.gist_access_id
131 new_gist_id = gist.gist_access_id
133 except formencode.Invalid as errors:
132 except formencode.Invalid as errors:
134 defaults = errors.value
133 defaults = errors.value
135
134
136 return formencode.htmlfill.render(
135 return formencode.htmlfill.render(
137 render('admin/gists/new.html'),
136 render('admin/gists/new.html'),
138 defaults=defaults,
137 defaults=defaults,
139 errors=errors.error_dict or {},
138 errors=errors.error_dict or {},
140 prefix_error=False,
139 prefix_error=False,
141 encoding="UTF-8",
140 encoding="UTF-8",
142 force_defaults=False)
141 force_defaults=False)
143
142
144 except Exception as e:
143 except Exception as e:
145 log.error(traceback.format_exc())
144 log.error(traceback.format_exc())
146 h.flash(_('Error occurred during gist creation'), category='error')
145 h.flash(_('Error occurred during gist creation'), category='error')
147 return redirect(url('new_gist'))
146 raise HTTPFound(location=url('new_gist'))
148 return redirect(url('gist', gist_id=new_gist_id))
147 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
149
148
150 @LoginRequired()
149 @LoginRequired()
151 @NotAnonymous()
150 @NotAnonymous()
152 def new(self, format='html'):
151 def new(self, format='html'):
153 """GET /admin/gists/new: Form to create a new item"""
152 """GET /admin/gists/new: Form to create a new item"""
154 # url('new_gist')
153 # url('new_gist')
155 self.__load_defaults()
154 self.__load_defaults()
156 return render('admin/gists/new.html')
155 return render('admin/gists/new.html')
157
156
158 @LoginRequired()
157 @LoginRequired()
159 @NotAnonymous()
158 @NotAnonymous()
160 def update(self, gist_id):
159 def update(self, gist_id):
161 """PUT /admin/gists/gist_id: Update an existing item"""
160 """PUT /admin/gists/gist_id: Update an existing item"""
162 # Forms posted to this method should contain a hidden field:
161 # Forms posted to this method should contain a hidden field:
163 # <input type="hidden" name="_method" value="PUT" />
162 # <input type="hidden" name="_method" value="PUT" />
164 # Or using helpers:
163 # Or using helpers:
165 # h.form(url('gist', gist_id=ID),
164 # h.form(url('gist', gist_id=ID),
166 # method='put')
165 # method='put')
167 # url('gist', gist_id=ID)
166 # url('gist', gist_id=ID)
168
167
169 @LoginRequired()
168 @LoginRequired()
170 @NotAnonymous()
169 @NotAnonymous()
171 def delete(self, gist_id):
170 def delete(self, gist_id):
172 """DELETE /admin/gists/gist_id: Delete an existing item"""
171 """DELETE /admin/gists/gist_id: Delete an existing item"""
173 # Forms posted to this method should contain a hidden field:
172 # Forms posted to this method should contain a hidden field:
174 # <input type="hidden" name="_method" value="DELETE" />
173 # <input type="hidden" name="_method" value="DELETE" />
175 # Or using helpers:
174 # Or using helpers:
176 # h.form(url('gist', gist_id=ID),
175 # h.form(url('gist', gist_id=ID),
177 # method='delete')
176 # method='delete')
178 # url('gist', gist_id=ID)
177 # url('gist', gist_id=ID)
179 gist = GistModel().get_gist(gist_id)
178 gist = GistModel().get_gist(gist_id)
180 owner = gist.gist_owner == c.authuser.user_id
179 owner = gist.gist_owner == c.authuser.user_id
181 if h.HasPermissionAny('hg.admin')() or owner:
180 if h.HasPermissionAny('hg.admin')() or owner:
182 GistModel().delete(gist)
181 GistModel().delete(gist)
183 Session().commit()
182 Session().commit()
184 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
183 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
185 else:
184 else:
186 raise HTTPForbidden()
185 raise HTTPForbidden()
187
186
188 return redirect(url('gists'))
187 raise HTTPFound(location=url('gists'))
189
188
190 @LoginRequired()
189 @LoginRequired()
191 def show(self, gist_id, revision='tip', format='html', f_path=None):
190 def show(self, gist_id, revision='tip', format='html', f_path=None):
192 """GET /admin/gists/gist_id: Show a specific item"""
191 """GET /admin/gists/gist_id: Show a specific item"""
193 # url('gist', gist_id=ID)
192 # url('gist', gist_id=ID)
194 c.gist = Gist.get_or_404(gist_id)
193 c.gist = Gist.get_or_404(gist_id)
195
194
196 #check if this gist is not expired
195 #check if this gist is not expired
197 if c.gist.gist_expires != -1:
196 if c.gist.gist_expires != -1:
198 if time.time() > c.gist.gist_expires:
197 if time.time() > c.gist.gist_expires:
199 log.error('Gist expired at %s',
198 log.error('Gist expired at %s',
200 time_to_datetime(c.gist.gist_expires))
199 time_to_datetime(c.gist.gist_expires))
201 raise HTTPNotFound()
200 raise HTTPNotFound()
202 try:
201 try:
203 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
202 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
204 revision=revision)
203 revision=revision)
205 except VCSError:
204 except VCSError:
206 log.error(traceback.format_exc())
205 log.error(traceback.format_exc())
207 raise HTTPNotFound()
206 raise HTTPNotFound()
208 if format == 'raw':
207 if format == 'raw':
209 content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
208 content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
210 response.content_type = 'text/plain'
209 response.content_type = 'text/plain'
211 return content
210 return content
212 return render('admin/gists/show.html')
211 return render('admin/gists/show.html')
213
212
214 @LoginRequired()
213 @LoginRequired()
215 @NotAnonymous()
214 @NotAnonymous()
216 def edit(self, gist_id, format='html'):
215 def edit(self, gist_id, format='html'):
217 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
216 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
218 # url('edit_gist', gist_id=ID)
217 # url('edit_gist', gist_id=ID)
219 c.gist = Gist.get_or_404(gist_id)
218 c.gist = Gist.get_or_404(gist_id)
220
219
221 #check if this gist is not expired
220 #check if this gist is not expired
222 if c.gist.gist_expires != -1:
221 if c.gist.gist_expires != -1:
223 if time.time() > c.gist.gist_expires:
222 if time.time() > c.gist.gist_expires:
224 log.error('Gist expired at %s',
223 log.error('Gist expired at %s',
225 time_to_datetime(c.gist.gist_expires))
224 time_to_datetime(c.gist.gist_expires))
226 raise HTTPNotFound()
225 raise HTTPNotFound()
227 try:
226 try:
228 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
227 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
229 except VCSError:
228 except VCSError:
230 log.error(traceback.format_exc())
229 log.error(traceback.format_exc())
231 raise HTTPNotFound()
230 raise HTTPNotFound()
232
231
233 self.__load_defaults(extra_values=('0', _('Unmodified')))
232 self.__load_defaults(extra_values=('0', _('Unmodified')))
234 rendered = render('admin/gists/edit.html')
233 rendered = render('admin/gists/edit.html')
235
234
236 if request.POST:
235 if request.POST:
237 rpost = request.POST
236 rpost = request.POST
238 nodes = {}
237 nodes = {}
239 for org_filename, filename, mimetype, content in zip(
238 for org_filename, filename, mimetype, content in zip(
240 rpost.getall('org_files'),
239 rpost.getall('org_files'),
241 rpost.getall('files'),
240 rpost.getall('files'),
242 rpost.getall('mimetypes'),
241 rpost.getall('mimetypes'),
243 rpost.getall('contents')):
242 rpost.getall('contents')):
244
243
245 nodes[org_filename] = {
244 nodes[org_filename] = {
246 'org_filename': org_filename,
245 'org_filename': org_filename,
247 'filename': filename,
246 'filename': filename,
248 'content': content,
247 'content': content,
249 'lexer': mimetype,
248 'lexer': mimetype,
250 }
249 }
251 try:
250 try:
252 GistModel().update(
251 GistModel().update(
253 gist=c.gist,
252 gist=c.gist,
254 description=rpost['description'],
253 description=rpost['description'],
255 owner=c.gist.owner,
254 owner=c.gist.owner,
256 gist_mapping=nodes,
255 gist_mapping=nodes,
257 gist_type=c.gist.gist_type,
256 gist_type=c.gist.gist_type,
258 lifetime=rpost['lifetime']
257 lifetime=rpost['lifetime']
259 )
258 )
260
259
261 Session().commit()
260 Session().commit()
262 h.flash(_('Successfully updated gist content'), category='success')
261 h.flash(_('Successfully updated gist content'), category='success')
263 except NodeNotChangedError:
262 except NodeNotChangedError:
264 # raised if nothing was changed in repo itself. We anyway then
263 # raised if nothing was changed in repo itself. We anyway then
265 # store only DB stuff for gist
264 # store only DB stuff for gist
266 Session().commit()
265 Session().commit()
267 h.flash(_('Successfully updated gist data'), category='success')
266 h.flash(_('Successfully updated gist data'), category='success')
268 except Exception:
267 except Exception:
269 log.error(traceback.format_exc())
268 log.error(traceback.format_exc())
270 h.flash(_('Error occurred during update of gist %s') % gist_id,
269 h.flash(_('Error occurred during update of gist %s') % gist_id,
271 category='error')
270 category='error')
272
271
273 return redirect(url('gist', gist_id=gist_id))
272 raise HTTPFound(location=url('gist', gist_id=gist_id))
274
273
275 return rendered
274 return rendered
276
275
277 @LoginRequired()
276 @LoginRequired()
278 @NotAnonymous()
277 @NotAnonymous()
279 @jsonify
278 @jsonify
280 def check_revision(self, gist_id):
279 def check_revision(self, gist_id):
281 c.gist = Gist.get_or_404(gist_id)
280 c.gist = Gist.get_or_404(gist_id)
282 last_rev = c.gist.scm_instance.get_changeset()
281 last_rev = c.gist.scm_instance.get_changeset()
283 success = True
282 success = True
284 revision = request.POST.get('revision')
283 revision = request.POST.get('revision')
285
284
286 ##TODO: maybe move this to model ?
285 ##TODO: maybe move this to model ?
287 if revision != last_rev.raw_id:
286 if revision != last_rev.raw_id:
288 log.error('Last revision %s is different than submitted %s',
287 log.error('Last revision %s is different than submitted %s',
289 revision, last_rev)
288 revision, last_rev)
290 # our gist has newer version than we
289 # our gist has newer version than we
291 success = False
290 success = False
292
291
293 return {'success': success}
292 return {'success': success}
@@ -1,277 +1,277 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.my_account
15 kallithea.controllers.admin.my_account
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 my account controller for Kallithea admin
18 my account controller for Kallithea admin
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: August 20, 2013
22 :created_on: August 20, 2013
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from sqlalchemy import func
32 from sqlalchemy import func
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from pylons import request, tmpl_context as c, url
34 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37
37
38 from kallithea import EXTERN_TYPE_INTERNAL
38 from kallithea import EXTERN_TYPE_INTERNAL
39 from kallithea.lib import helpers as h
39 from kallithea.lib import helpers as h
40 from kallithea.lib import auth_modules
40 from kallithea.lib import auth_modules
41 from kallithea.lib.auth import LoginRequired, NotAnonymous, AuthUser
41 from kallithea.lib.auth import LoginRequired, NotAnonymous, AuthUser
42 from kallithea.lib.base import BaseController, render
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.utils2 import generate_api_key, safe_int
43 from kallithea.lib.utils2 import generate_api_key, safe_int
44 from kallithea.lib.compat import json
44 from kallithea.lib.compat import json
45 from kallithea.model.db import Repository, UserEmailMap, User, UserFollowing
45 from kallithea.model.db import Repository, UserEmailMap, User, UserFollowing
46 from kallithea.model.forms import UserForm, PasswordChangeForm
46 from kallithea.model.forms import UserForm, PasswordChangeForm
47 from kallithea.model.user import UserModel
47 from kallithea.model.user import UserModel
48 from kallithea.model.repo import RepoModel
48 from kallithea.model.repo import RepoModel
49 from kallithea.model.api_key import ApiKeyModel
49 from kallithea.model.api_key import ApiKeyModel
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MyAccountController(BaseController):
55 class MyAccountController(BaseController):
56 """REST Controller styled on the Atom Publishing Protocol"""
56 """REST Controller styled on the Atom Publishing Protocol"""
57 # To properly map this controller, ensure your config/routing.py
57 # To properly map this controller, ensure your config/routing.py
58 # file has a resource setup:
58 # file has a resource setup:
59 # map.resource('setting', 'settings', controller='admin/settings',
59 # map.resource('setting', 'settings', controller='admin/settings',
60 # path_prefix='/admin', name_prefix='admin_')
60 # path_prefix='/admin', name_prefix='admin_')
61
61
62 @LoginRequired()
62 @LoginRequired()
63 @NotAnonymous()
63 @NotAnonymous()
64 def __before__(self):
64 def __before__(self):
65 super(MyAccountController, self).__before__()
65 super(MyAccountController, self).__before__()
66
66
67 def __load_data(self):
67 def __load_data(self):
68 c.user = User.get(self.authuser.user_id)
68 c.user = User.get(self.authuser.user_id)
69 if c.user.username == User.DEFAULT_USER:
69 if c.user.username == User.DEFAULT_USER:
70 h.flash(_("You can't edit this user since it's"
70 h.flash(_("You can't edit this user since it's"
71 " crucial for entire application"), category='warning')
71 " crucial for entire application"), category='warning')
72 return redirect(url('users'))
72 raise HTTPFound(location=url('users'))
73 c.EXTERN_TYPE_INTERNAL = EXTERN_TYPE_INTERNAL
73 c.EXTERN_TYPE_INTERNAL = EXTERN_TYPE_INTERNAL
74
74
75 def _load_my_repos_data(self, watched=False):
75 def _load_my_repos_data(self, watched=False):
76 if watched:
76 if watched:
77 admin = False
77 admin = False
78 repos_list = [x.follows_repository for x in
78 repos_list = [x.follows_repository for x in
79 Session().query(UserFollowing).filter(
79 Session().query(UserFollowing).filter(
80 UserFollowing.user_id ==
80 UserFollowing.user_id ==
81 self.authuser.user_id).all()]
81 self.authuser.user_id).all()]
82 else:
82 else:
83 admin = True
83 admin = True
84 repos_list = Session().query(Repository)\
84 repos_list = Session().query(Repository)\
85 .filter(Repository.user_id ==
85 .filter(Repository.user_id ==
86 self.authuser.user_id)\
86 self.authuser.user_id)\
87 .order_by(func.lower(Repository.repo_name)).all()
87 .order_by(func.lower(Repository.repo_name)).all()
88
88
89 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
89 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
90 admin=admin)
90 admin=admin)
91 #json used to render the grid
91 #json used to render the grid
92 return json.dumps(repos_data)
92 return json.dumps(repos_data)
93
93
94 def my_account(self):
94 def my_account(self):
95 """
95 """
96 GET /_admin/my_account Displays info about my account
96 GET /_admin/my_account Displays info about my account
97 """
97 """
98 # url('my_account')
98 # url('my_account')
99 c.active = 'profile'
99 c.active = 'profile'
100 self.__load_data()
100 self.__load_data()
101 c.perm_user = AuthUser(user_id=self.authuser.user_id)
101 c.perm_user = AuthUser(user_id=self.authuser.user_id)
102 c.ip_addr = self.ip_addr
102 c.ip_addr = self.ip_addr
103 managed_fields = auth_modules.get_managed_fields(c.user)
103 managed_fields = auth_modules.get_managed_fields(c.user)
104 def_user_perms = User.get_default_user().AuthUser.permissions['global']
104 def_user_perms = User.get_default_user().AuthUser.permissions['global']
105 if 'hg.register.none' in def_user_perms:
105 if 'hg.register.none' in def_user_perms:
106 managed_fields.extend(['username', 'firstname', 'lastname', 'email'])
106 managed_fields.extend(['username', 'firstname', 'lastname', 'email'])
107
107
108 c.readonly = lambda n: 'readonly' if n in managed_fields else None
108 c.readonly = lambda n: 'readonly' if n in managed_fields else None
109
109
110 defaults = c.user.get_dict()
110 defaults = c.user.get_dict()
111 update = False
111 update = False
112 if request.POST:
112 if request.POST:
113 _form = UserForm(edit=True,
113 _form = UserForm(edit=True,
114 old_data={'user_id': self.authuser.user_id,
114 old_data={'user_id': self.authuser.user_id,
115 'email': self.authuser.email})()
115 'email': self.authuser.email})()
116 form_result = {}
116 form_result = {}
117 try:
117 try:
118 post_data = dict(request.POST)
118 post_data = dict(request.POST)
119 post_data['new_password'] = ''
119 post_data['new_password'] = ''
120 post_data['password_confirmation'] = ''
120 post_data['password_confirmation'] = ''
121 form_result = _form.to_python(post_data)
121 form_result = _form.to_python(post_data)
122 # skip updating those attrs for my account
122 # skip updating those attrs for my account
123 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
123 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 'new_password', 'password_confirmation',
124 'new_password', 'password_confirmation',
125 ] + managed_fields
125 ] + managed_fields
126
126
127 UserModel().update(self.authuser.user_id, form_result,
127 UserModel().update(self.authuser.user_id, form_result,
128 skip_attrs=skip_attrs)
128 skip_attrs=skip_attrs)
129 h.flash(_('Your account was updated successfully'),
129 h.flash(_('Your account was updated successfully'),
130 category='success')
130 category='success')
131 Session().commit()
131 Session().commit()
132 update = True
132 update = True
133
133
134 except formencode.Invalid as errors:
134 except formencode.Invalid as errors:
135 return htmlfill.render(
135 return htmlfill.render(
136 render('admin/my_account/my_account.html'),
136 render('admin/my_account/my_account.html'),
137 defaults=errors.value,
137 defaults=errors.value,
138 errors=errors.error_dict or {},
138 errors=errors.error_dict or {},
139 prefix_error=False,
139 prefix_error=False,
140 encoding="UTF-8",
140 encoding="UTF-8",
141 force_defaults=False)
141 force_defaults=False)
142 except Exception:
142 except Exception:
143 log.error(traceback.format_exc())
143 log.error(traceback.format_exc())
144 h.flash(_('Error occurred during update of user %s') \
144 h.flash(_('Error occurred during update of user %s') \
145 % form_result.get('username'), category='error')
145 % form_result.get('username'), category='error')
146 if update:
146 if update:
147 return redirect('my_account')
147 raise HTTPFound(location='my_account')
148 return htmlfill.render(
148 return htmlfill.render(
149 render('admin/my_account/my_account.html'),
149 render('admin/my_account/my_account.html'),
150 defaults=defaults,
150 defaults=defaults,
151 encoding="UTF-8",
151 encoding="UTF-8",
152 force_defaults=False)
152 force_defaults=False)
153
153
154 def my_account_password(self):
154 def my_account_password(self):
155 c.active = 'password'
155 c.active = 'password'
156 self.__load_data()
156 self.__load_data()
157
157
158 managed_fields = auth_modules.get_managed_fields(c.user)
158 managed_fields = auth_modules.get_managed_fields(c.user)
159 c.can_change_password = 'password' not in managed_fields
159 c.can_change_password = 'password' not in managed_fields
160
160
161 if request.POST and c.can_change_password:
161 if request.POST and c.can_change_password:
162 _form = PasswordChangeForm(self.authuser.username)()
162 _form = PasswordChangeForm(self.authuser.username)()
163 try:
163 try:
164 form_result = _form.to_python(request.POST)
164 form_result = _form.to_python(request.POST)
165 UserModel().update(self.authuser.user_id, form_result)
165 UserModel().update(self.authuser.user_id, form_result)
166 Session().commit()
166 Session().commit()
167 h.flash(_("Successfully updated password"), category='success')
167 h.flash(_("Successfully updated password"), category='success')
168 except formencode.Invalid as errors:
168 except formencode.Invalid as errors:
169 return htmlfill.render(
169 return htmlfill.render(
170 render('admin/my_account/my_account.html'),
170 render('admin/my_account/my_account.html'),
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.error(traceback.format_exc())
177 log.error(traceback.format_exc())
178 h.flash(_('Error occurred during update of user password'),
178 h.flash(_('Error occurred during update of user password'),
179 category='error')
179 category='error')
180 return render('admin/my_account/my_account.html')
180 return render('admin/my_account/my_account.html')
181
181
182 def my_account_repos(self):
182 def my_account_repos(self):
183 c.active = 'repos'
183 c.active = 'repos'
184 self.__load_data()
184 self.__load_data()
185
185
186 #json used to render the grid
186 #json used to render the grid
187 c.data = self._load_my_repos_data()
187 c.data = self._load_my_repos_data()
188 return render('admin/my_account/my_account.html')
188 return render('admin/my_account/my_account.html')
189
189
190 def my_account_watched(self):
190 def my_account_watched(self):
191 c.active = 'watched'
191 c.active = 'watched'
192 self.__load_data()
192 self.__load_data()
193
193
194 #json used to render the grid
194 #json used to render the grid
195 c.data = self._load_my_repos_data(watched=True)
195 c.data = self._load_my_repos_data(watched=True)
196 return render('admin/my_account/my_account.html')
196 return render('admin/my_account/my_account.html')
197
197
198 def my_account_perms(self):
198 def my_account_perms(self):
199 c.active = 'perms'
199 c.active = 'perms'
200 self.__load_data()
200 self.__load_data()
201 c.perm_user = AuthUser(user_id=self.authuser.user_id)
201 c.perm_user = AuthUser(user_id=self.authuser.user_id)
202 c.ip_addr = self.ip_addr
202 c.ip_addr = self.ip_addr
203
203
204 return render('admin/my_account/my_account.html')
204 return render('admin/my_account/my_account.html')
205
205
206 def my_account_emails(self):
206 def my_account_emails(self):
207 c.active = 'emails'
207 c.active = 'emails'
208 self.__load_data()
208 self.__load_data()
209
209
210 c.user_email_map = UserEmailMap.query()\
210 c.user_email_map = UserEmailMap.query()\
211 .filter(UserEmailMap.user == c.user).all()
211 .filter(UserEmailMap.user == c.user).all()
212 return render('admin/my_account/my_account.html')
212 return render('admin/my_account/my_account.html')
213
213
214 def my_account_emails_add(self):
214 def my_account_emails_add(self):
215 email = request.POST.get('new_email')
215 email = request.POST.get('new_email')
216
216
217 try:
217 try:
218 UserModel().add_extra_email(self.authuser.user_id, email)
218 UserModel().add_extra_email(self.authuser.user_id, email)
219 Session().commit()
219 Session().commit()
220 h.flash(_("Added email %s to user") % email, category='success')
220 h.flash(_("Added email %s to user") % email, category='success')
221 except formencode.Invalid as error:
221 except formencode.Invalid as error:
222 msg = error.error_dict['email']
222 msg = error.error_dict['email']
223 h.flash(msg, category='error')
223 h.flash(msg, category='error')
224 except Exception:
224 except Exception:
225 log.error(traceback.format_exc())
225 log.error(traceback.format_exc())
226 h.flash(_('An error occurred during email saving'),
226 h.flash(_('An error occurred during email saving'),
227 category='error')
227 category='error')
228 return redirect(url('my_account_emails'))
228 raise HTTPFound(location=url('my_account_emails'))
229
229
230 def my_account_emails_delete(self):
230 def my_account_emails_delete(self):
231 email_id = request.POST.get('del_email_id')
231 email_id = request.POST.get('del_email_id')
232 user_model = UserModel()
232 user_model = UserModel()
233 user_model.delete_extra_email(self.authuser.user_id, email_id)
233 user_model.delete_extra_email(self.authuser.user_id, email_id)
234 Session().commit()
234 Session().commit()
235 h.flash(_("Removed email from user"), category='success')
235 h.flash(_("Removed email from user"), category='success')
236 return redirect(url('my_account_emails'))
236 raise HTTPFound(location=url('my_account_emails'))
237
237
238 def my_account_api_keys(self):
238 def my_account_api_keys(self):
239 c.active = 'api_keys'
239 c.active = 'api_keys'
240 self.__load_data()
240 self.__load_data()
241 show_expired = True
241 show_expired = True
242 c.lifetime_values = [
242 c.lifetime_values = [
243 (str(-1), _('Forever')),
243 (str(-1), _('Forever')),
244 (str(5), _('5 minutes')),
244 (str(5), _('5 minutes')),
245 (str(60), _('1 hour')),
245 (str(60), _('1 hour')),
246 (str(60 * 24), _('1 day')),
246 (str(60 * 24), _('1 day')),
247 (str(60 * 24 * 30), _('1 month')),
247 (str(60 * 24 * 30), _('1 month')),
248 ]
248 ]
249 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
249 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
250 c.user_api_keys = ApiKeyModel().get_api_keys(self.authuser.user_id,
250 c.user_api_keys = ApiKeyModel().get_api_keys(self.authuser.user_id,
251 show_expired=show_expired)
251 show_expired=show_expired)
252 return render('admin/my_account/my_account.html')
252 return render('admin/my_account/my_account.html')
253
253
254 def my_account_api_keys_add(self):
254 def my_account_api_keys_add(self):
255 lifetime = safe_int(request.POST.get('lifetime'), -1)
255 lifetime = safe_int(request.POST.get('lifetime'), -1)
256 description = request.POST.get('description')
256 description = request.POST.get('description')
257 ApiKeyModel().create(self.authuser.user_id, description, lifetime)
257 ApiKeyModel().create(self.authuser.user_id, description, lifetime)
258 Session().commit()
258 Session().commit()
259 h.flash(_("API key successfully created"), category='success')
259 h.flash(_("API key successfully created"), category='success')
260 return redirect(url('my_account_api_keys'))
260 raise HTTPFound(location=url('my_account_api_keys'))
261
261
262 def my_account_api_keys_delete(self):
262 def my_account_api_keys_delete(self):
263 api_key = request.POST.get('del_api_key')
263 api_key = request.POST.get('del_api_key')
264 user_id = self.authuser.user_id
264 user_id = self.authuser.user_id
265 if request.POST.get('del_api_key_builtin'):
265 if request.POST.get('del_api_key_builtin'):
266 user = User.get(user_id)
266 user = User.get(user_id)
267 if user is not None:
267 if user is not None:
268 user.api_key = generate_api_key()
268 user.api_key = generate_api_key()
269 Session().add(user)
269 Session().add(user)
270 Session().commit()
270 Session().commit()
271 h.flash(_("API key successfully reset"), category='success')
271 h.flash(_("API key successfully reset"), category='success')
272 elif api_key:
272 elif api_key:
273 ApiKeyModel().delete(api_key, self.authuser.user_id)
273 ApiKeyModel().delete(api_key, self.authuser.user_id)
274 Session().commit()
274 Session().commit()
275 h.flash(_("API key successfully deleted"), category='success')
275 h.flash(_("API key successfully deleted"), category='success')
276
276
277 return redirect(url('my_account_api_keys'))
277 raise HTTPFound(location=url('my_account_api_keys'))
@@ -1,196 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.permissions
15 kallithea.controllers.admin.permissions
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 permissions controller for Kallithea
18 permissions controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 27, 2010
22 :created_on: Apr 27, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33
33
34 from pylons import request, tmpl_context as c, url
34 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37
37
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.base import BaseController, render
41 from kallithea.model.forms import DefaultPermissionsForm
41 from kallithea.model.forms import DefaultPermissionsForm
42 from kallithea.model.permission import PermissionModel
42 from kallithea.model.permission import PermissionModel
43 from kallithea.model.db import User, UserIpMap
43 from kallithea.model.db import User, UserIpMap
44 from kallithea.model.meta import Session
44 from kallithea.model.meta import Session
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 @HasPermissionAllDecorator('hg.admin')
56 @HasPermissionAllDecorator('hg.admin')
57 def __before__(self):
57 def __before__(self):
58 super(PermissionsController, self).__before__()
58 super(PermissionsController, self).__before__()
59
59
60 def __load_data(self):
60 def __load_data(self):
61 c.repo_perms_choices = [('repository.none', _('None'),),
61 c.repo_perms_choices = [('repository.none', _('None'),),
62 ('repository.read', _('Read'),),
62 ('repository.read', _('Read'),),
63 ('repository.write', _('Write'),),
63 ('repository.write', _('Write'),),
64 ('repository.admin', _('Admin'),)]
64 ('repository.admin', _('Admin'),)]
65 c.group_perms_choices = [('group.none', _('None'),),
65 c.group_perms_choices = [('group.none', _('None'),),
66 ('group.read', _('Read'),),
66 ('group.read', _('Read'),),
67 ('group.write', _('Write'),),
67 ('group.write', _('Write'),),
68 ('group.admin', _('Admin'),)]
68 ('group.admin', _('Admin'),)]
69 c.user_group_perms_choices = [('usergroup.none', _('None'),),
69 c.user_group_perms_choices = [('usergroup.none', _('None'),),
70 ('usergroup.read', _('Read'),),
70 ('usergroup.read', _('Read'),),
71 ('usergroup.write', _('Write'),),
71 ('usergroup.write', _('Write'),),
72 ('usergroup.admin', _('Admin'),)]
72 ('usergroup.admin', _('Admin'),)]
73 c.register_choices = [
73 c.register_choices = [
74 ('hg.register.none',
74 ('hg.register.none',
75 _('Disabled')),
75 _('Disabled')),
76 ('hg.register.manual_activate',
76 ('hg.register.manual_activate',
77 _('Allowed with manual account activation')),
77 _('Allowed with manual account activation')),
78 ('hg.register.auto_activate',
78 ('hg.register.auto_activate',
79 _('Allowed with automatic account activation')), ]
79 _('Allowed with automatic account activation')), ]
80
80
81 c.extern_activate_choices = [
81 c.extern_activate_choices = [
82 ('hg.extern_activate.manual', _('Manual activation of external account')),
82 ('hg.extern_activate.manual', _('Manual activation of external account')),
83 ('hg.extern_activate.auto', _('Automatic activation of external account')),
83 ('hg.extern_activate.auto', _('Automatic activation of external account')),
84 ]
84 ]
85
85
86 c.repo_create_choices = [('hg.create.none', _('Disabled')),
86 c.repo_create_choices = [('hg.create.none', _('Disabled')),
87 ('hg.create.repository', _('Enabled'))]
87 ('hg.create.repository', _('Enabled'))]
88
88
89 c.repo_create_on_write_choices = [
89 c.repo_create_on_write_choices = [
90 ('hg.create.write_on_repogroup.true', _('Enabled')),
90 ('hg.create.write_on_repogroup.true', _('Enabled')),
91 ('hg.create.write_on_repogroup.false', _('Disabled')),
91 ('hg.create.write_on_repogroup.false', _('Disabled')),
92 ]
92 ]
93
93
94 c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')),
94 c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')),
95 ('hg.usergroup.create.true', _('Enabled'))]
95 ('hg.usergroup.create.true', _('Enabled'))]
96
96
97 c.repo_group_create_choices = [('hg.repogroup.create.false', _('Disabled')),
97 c.repo_group_create_choices = [('hg.repogroup.create.false', _('Disabled')),
98 ('hg.repogroup.create.true', _('Enabled'))]
98 ('hg.repogroup.create.true', _('Enabled'))]
99
99
100 c.fork_choices = [('hg.fork.none', _('Disabled')),
100 c.fork_choices = [('hg.fork.none', _('Disabled')),
101 ('hg.fork.repository', _('Enabled'))]
101 ('hg.fork.repository', _('Enabled'))]
102
102
103 def permission_globals(self):
103 def permission_globals(self):
104 c.active = 'globals'
104 c.active = 'globals'
105 self.__load_data()
105 self.__load_data()
106 if request.POST:
106 if request.POST:
107 _form = DefaultPermissionsForm(
107 _form = DefaultPermissionsForm(
108 [x[0] for x in c.repo_perms_choices],
108 [x[0] for x in c.repo_perms_choices],
109 [x[0] for x in c.group_perms_choices],
109 [x[0] for x in c.group_perms_choices],
110 [x[0] for x in c.user_group_perms_choices],
110 [x[0] for x in c.user_group_perms_choices],
111 [x[0] for x in c.repo_create_choices],
111 [x[0] for x in c.repo_create_choices],
112 [x[0] for x in c.repo_create_on_write_choices],
112 [x[0] for x in c.repo_create_on_write_choices],
113 [x[0] for x in c.repo_group_create_choices],
113 [x[0] for x in c.repo_group_create_choices],
114 [x[0] for x in c.user_group_create_choices],
114 [x[0] for x in c.user_group_create_choices],
115 [x[0] for x in c.fork_choices],
115 [x[0] for x in c.fork_choices],
116 [x[0] for x in c.register_choices],
116 [x[0] for x in c.register_choices],
117 [x[0] for x in c.extern_activate_choices])()
117 [x[0] for x in c.extern_activate_choices])()
118
118
119 try:
119 try:
120 form_result = _form.to_python(dict(request.POST))
120 form_result = _form.to_python(dict(request.POST))
121 form_result.update({'perm_user_name': 'default'})
121 form_result.update({'perm_user_name': 'default'})
122 PermissionModel().update(form_result)
122 PermissionModel().update(form_result)
123 Session().commit()
123 Session().commit()
124 h.flash(_('Global permissions updated successfully'),
124 h.flash(_('Global permissions updated successfully'),
125 category='success')
125 category='success')
126
126
127 except formencode.Invalid as errors:
127 except formencode.Invalid as errors:
128 defaults = errors.value
128 defaults = errors.value
129
129
130 return htmlfill.render(
130 return htmlfill.render(
131 render('admin/permissions/permissions.html'),
131 render('admin/permissions/permissions.html'),
132 defaults=defaults,
132 defaults=defaults,
133 errors=errors.error_dict or {},
133 errors=errors.error_dict or {},
134 prefix_error=False,
134 prefix_error=False,
135 encoding="UTF-8",
135 encoding="UTF-8",
136 force_defaults=False)
136 force_defaults=False)
137 except Exception:
137 except Exception:
138 log.error(traceback.format_exc())
138 log.error(traceback.format_exc())
139 h.flash(_('Error occurred during update of permissions'),
139 h.flash(_('Error occurred during update of permissions'),
140 category='error')
140 category='error')
141
141
142 return redirect(url('admin_permissions'))
142 raise HTTPFound(location=url('admin_permissions'))
143
143
144 c.user = User.get_default_user()
144 c.user = User.get_default_user()
145 defaults = {'anonymous': c.user.active}
145 defaults = {'anonymous': c.user.active}
146
146
147 for p in c.user.user_perms:
147 for p in c.user.user_perms:
148 if p.permission.permission_name.startswith('repository.'):
148 if p.permission.permission_name.startswith('repository.'):
149 defaults['default_repo_perm'] = p.permission.permission_name
149 defaults['default_repo_perm'] = p.permission.permission_name
150
150
151 if p.permission.permission_name.startswith('group.'):
151 if p.permission.permission_name.startswith('group.'):
152 defaults['default_group_perm'] = p.permission.permission_name
152 defaults['default_group_perm'] = p.permission.permission_name
153
153
154 if p.permission.permission_name.startswith('usergroup.'):
154 if p.permission.permission_name.startswith('usergroup.'):
155 defaults['default_user_group_perm'] = p.permission.permission_name
155 defaults['default_user_group_perm'] = p.permission.permission_name
156
156
157 if p.permission.permission_name.startswith('hg.create.write_on_repogroup'):
157 if p.permission.permission_name.startswith('hg.create.write_on_repogroup'):
158 defaults['create_on_write'] = p.permission.permission_name
158 defaults['create_on_write'] = p.permission.permission_name
159
159
160 elif p.permission.permission_name.startswith('hg.create.'):
160 elif p.permission.permission_name.startswith('hg.create.'):
161 defaults['default_repo_create'] = p.permission.permission_name
161 defaults['default_repo_create'] = p.permission.permission_name
162
162
163 if p.permission.permission_name.startswith('hg.repogroup.'):
163 if p.permission.permission_name.startswith('hg.repogroup.'):
164 defaults['default_repo_group_create'] = p.permission.permission_name
164 defaults['default_repo_group_create'] = p.permission.permission_name
165
165
166 if p.permission.permission_name.startswith('hg.usergroup.'):
166 if p.permission.permission_name.startswith('hg.usergroup.'):
167 defaults['default_user_group_create'] = p.permission.permission_name
167 defaults['default_user_group_create'] = p.permission.permission_name
168
168
169 if p.permission.permission_name.startswith('hg.register.'):
169 if p.permission.permission_name.startswith('hg.register.'):
170 defaults['default_register'] = p.permission.permission_name
170 defaults['default_register'] = p.permission.permission_name
171
171
172 if p.permission.permission_name.startswith('hg.extern_activate.'):
172 if p.permission.permission_name.startswith('hg.extern_activate.'):
173 defaults['default_extern_activate'] = p.permission.permission_name
173 defaults['default_extern_activate'] = p.permission.permission_name
174
174
175 if p.permission.permission_name.startswith('hg.fork.'):
175 if p.permission.permission_name.startswith('hg.fork.'):
176 defaults['default_fork'] = p.permission.permission_name
176 defaults['default_fork'] = p.permission.permission_name
177
177
178 return htmlfill.render(
178 return htmlfill.render(
179 render('admin/permissions/permissions.html'),
179 render('admin/permissions/permissions.html'),
180 defaults=defaults,
180 defaults=defaults,
181 encoding="UTF-8",
181 encoding="UTF-8",
182 force_defaults=False)
182 force_defaults=False)
183
183
184 def permission_ips(self):
184 def permission_ips(self):
185 c.active = 'ips'
185 c.active = 'ips'
186 c.user = User.get_default_user()
186 c.user = User.get_default_user()
187 c.user_ip_map = UserIpMap.query()\
187 c.user_ip_map = UserIpMap.query()\
188 .filter(UserIpMap.user == c.user).all()
188 .filter(UserIpMap.user == c.user).all()
189
189
190 return render('admin/permissions/permissions.html')
190 return render('admin/permissions/permissions.html')
191
191
192 def permission_perms(self):
192 def permission_perms(self):
193 c.active = 'perms'
193 c.active = 'perms'
194 c.user = User.get_default_user()
194 c.user = User.get_default_user()
195 c.perm_user = c.user.AuthUser
195 c.perm_user = c.user.AuthUser
196 return render('admin/permissions/permissions.html')
196 return render('admin/permissions/permissions.html')
@@ -1,459 +1,458 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.repo_groups
15 kallithea.controllers.admin.repo_groups
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Repository groups controller for Kallithea
18 Repository groups controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Mar 23, 2010
22 :created_on: Mar 23, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31 import itertools
31 import itertools
32
32
33 from formencode import htmlfill
33 from formencode import htmlfill
34
34
35 from pylons import request, tmpl_context as c, url
35 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
37 from pylons.i18n.translation import _, ungettext
36 from pylons.i18n.translation import _, ungettext
38 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPInternalServerError
37 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
39
38
40 import kallithea
39 import kallithea
41 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
42 from kallithea.lib.compat import json
41 from kallithea.lib.compat import json
43 from kallithea.lib.auth import LoginRequired, \
42 from kallithea.lib.auth import LoginRequired, \
44 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll, \
43 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll, \
45 HasPermissionAll
44 HasPermissionAll
46 from kallithea.lib.base import BaseController, render
45 from kallithea.lib.base import BaseController, render
47 from kallithea.model.db import RepoGroup, Repository
46 from kallithea.model.db import RepoGroup, Repository
48 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
49 from kallithea.model.repo_group import RepoGroupModel
48 from kallithea.model.repo_group import RepoGroupModel
50 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
51 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
52 from kallithea.model.repo import RepoModel
51 from kallithea.model.repo import RepoModel
53 from kallithea.lib.utils2 import safe_int
52 from kallithea.lib.utils2 import safe_int
54 from sqlalchemy.sql.expression import func
53 from sqlalchemy.sql.expression import func
55
54
56
55
57 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
58
57
59
58
60 class RepoGroupsController(BaseController):
59 class RepoGroupsController(BaseController):
61
60
62 @LoginRequired()
61 @LoginRequired()
63 def __before__(self):
62 def __before__(self):
64 super(RepoGroupsController, self).__before__()
63 super(RepoGroupsController, self).__before__()
65
64
66 def __load_defaults(self, extras=(), exclude=()):
65 def __load_defaults(self, extras=(), exclude=()):
67 """extras is used for keeping current parent ignoring permissions
66 """extras is used for keeping current parent ignoring permissions
68 exclude is used for not moving group to itself TODO: also exclude descendants
67 exclude is used for not moving group to itself TODO: also exclude descendants
69 Note: only admin can create top level groups
68 Note: only admin can create top level groups
70 """
69 """
71 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
70 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
72 exclude_group_ids = set(rg.group_id for rg in exclude)
71 exclude_group_ids = set(rg.group_id for rg in exclude)
73 c.repo_groups = [rg for rg in repo_groups
72 c.repo_groups = [rg for rg in repo_groups
74 if rg[0] not in exclude_group_ids]
73 if rg[0] not in exclude_group_ids]
75
74
76 repo_model = RepoModel()
75 repo_model = RepoModel()
77 c.users_array = repo_model.get_users_js()
76 c.users_array = repo_model.get_users_js()
78 c.user_groups_array = repo_model.get_user_groups_js()
77 c.user_groups_array = repo_model.get_user_groups_js()
79
78
80 def __load_data(self, group_id):
79 def __load_data(self, group_id):
81 """
80 """
82 Load defaults settings for edit, and update
81 Load defaults settings for edit, and update
83
82
84 :param group_id:
83 :param group_id:
85 """
84 """
86 repo_group = RepoGroup.get_or_404(group_id)
85 repo_group = RepoGroup.get_or_404(group_id)
87 data = repo_group.get_dict()
86 data = repo_group.get_dict()
88 data['group_name'] = repo_group.name
87 data['group_name'] = repo_group.name
89
88
90 # fill repository group users
89 # fill repository group users
91 for p in repo_group.repo_group_to_perm:
90 for p in repo_group.repo_group_to_perm:
92 data.update({'u_perm_%s' % p.user.username:
91 data.update({'u_perm_%s' % p.user.username:
93 p.permission.permission_name})
92 p.permission.permission_name})
94
93
95 # fill repository group groups
94 # fill repository group groups
96 for p in repo_group.users_group_to_perm:
95 for p in repo_group.users_group_to_perm:
97 data.update({'g_perm_%s' % p.users_group.users_group_name:
96 data.update({'g_perm_%s' % p.users_group.users_group_name:
98 p.permission.permission_name})
97 p.permission.permission_name})
99
98
100 return data
99 return data
101
100
102 def _revoke_perms_on_yourself(self, form_result):
101 def _revoke_perms_on_yourself(self, form_result):
103 _up = filter(lambda u: c.authuser.username == u[0],
102 _up = filter(lambda u: c.authuser.username == u[0],
104 form_result['perms_updates'])
103 form_result['perms_updates'])
105 _new = filter(lambda u: c.authuser.username == u[0],
104 _new = filter(lambda u: c.authuser.username == u[0],
106 form_result['perms_new'])
105 form_result['perms_new'])
107 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
106 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
108 return True
107 return True
109 return False
108 return False
110
109
111 def index(self, format='html'):
110 def index(self, format='html'):
112 """GET /repo_groups: All items in the collection"""
111 """GET /repo_groups: All items in the collection"""
113 # url('repos_groups')
112 # url('repos_groups')
114 _list = RepoGroup.query()\
113 _list = RepoGroup.query()\
115 .order_by(func.lower(RepoGroup.group_name))\
114 .order_by(func.lower(RepoGroup.group_name))\
116 .all()
115 .all()
117 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
116 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
118 repo_groups_data = []
117 repo_groups_data = []
119 total_records = len(group_iter)
118 total_records = len(group_iter)
120 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
119 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
121 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
120 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
122
121
123 repo_group_name = lambda repo_group_name, children_groups: (
122 repo_group_name = lambda repo_group_name, children_groups: (
124 template.get_def("repo_group_name")
123 template.get_def("repo_group_name")
125 .render(repo_group_name, children_groups, _=_, h=h, c=c)
124 .render(repo_group_name, children_groups, _=_, h=h, c=c)
126 )
125 )
127 repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
126 repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
128 template.get_def("repo_group_actions")
127 template.get_def("repo_group_actions")
129 .render(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
128 .render(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
130 ungettext=ungettext)
129 ungettext=ungettext)
131 )
130 )
132
131
133 for repo_gr in group_iter:
132 for repo_gr in group_iter:
134 children_groups = map(h.safe_unicode,
133 children_groups = map(h.safe_unicode,
135 itertools.chain((g.name for g in repo_gr.parents),
134 itertools.chain((g.name for g in repo_gr.parents),
136 (x.name for x in [repo_gr])))
135 (x.name for x in [repo_gr])))
137 repo_count = repo_gr.repositories.count()
136 repo_count = repo_gr.repositories.count()
138 repo_groups_data.append({
137 repo_groups_data.append({
139 "raw_name": repo_gr.group_name,
138 "raw_name": repo_gr.group_name,
140 "group_name": repo_group_name(repo_gr.group_name, children_groups),
139 "group_name": repo_group_name(repo_gr.group_name, children_groups),
141 "desc": h.escape(repo_gr.group_description),
140 "desc": h.escape(repo_gr.group_description),
142 "repos": repo_count,
141 "repos": repo_count,
143 "owner": h.person(repo_gr.user),
142 "owner": h.person(repo_gr.user),
144 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
143 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
145 repo_count)
144 repo_count)
146 })
145 })
147
146
148 c.data = json.dumps({
147 c.data = json.dumps({
149 "totalRecords": total_records,
148 "totalRecords": total_records,
150 "startIndex": 0,
149 "startIndex": 0,
151 "sort": None,
150 "sort": None,
152 "dir": "asc",
151 "dir": "asc",
153 "records": repo_groups_data
152 "records": repo_groups_data
154 })
153 })
155
154
156 return render('admin/repo_groups/repo_groups.html')
155 return render('admin/repo_groups/repo_groups.html')
157
156
158 def create(self):
157 def create(self):
159 """POST /repo_groups: Create a new item"""
158 """POST /repo_groups: Create a new item"""
160 # url('repos_groups')
159 # url('repos_groups')
161
160
162 self.__load_defaults()
161 self.__load_defaults()
163
162
164 # permissions for can create group based on parent_id are checked
163 # permissions for can create group based on parent_id are checked
165 # here in the Form
164 # here in the Form
166 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
165 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
167 try:
166 try:
168 form_result = repo_group_form.to_python(dict(request.POST))
167 form_result = repo_group_form.to_python(dict(request.POST))
169 gr = RepoGroupModel().create(
168 gr = RepoGroupModel().create(
170 group_name=form_result['group_name'],
169 group_name=form_result['group_name'],
171 group_description=form_result['group_description'],
170 group_description=form_result['group_description'],
172 parent=form_result['group_parent_id'],
171 parent=form_result['group_parent_id'],
173 owner=self.authuser.user_id, # TODO: make editable
172 owner=self.authuser.user_id, # TODO: make editable
174 copy_permissions=form_result['group_copy_permissions']
173 copy_permissions=form_result['group_copy_permissions']
175 )
174 )
176 Session().commit()
175 Session().commit()
177 #TODO: in futureaction_logger(, '', '', '', self.sa)
176 #TODO: in futureaction_logger(, '', '', '', self.sa)
178 except formencode.Invalid as errors:
177 except formencode.Invalid as errors:
179 return htmlfill.render(
178 return htmlfill.render(
180 render('admin/repo_groups/repo_group_add.html'),
179 render('admin/repo_groups/repo_group_add.html'),
181 defaults=errors.value,
180 defaults=errors.value,
182 errors=errors.error_dict or {},
181 errors=errors.error_dict or {},
183 prefix_error=False,
182 prefix_error=False,
184 encoding="UTF-8",
183 encoding="UTF-8",
185 force_defaults=False)
184 force_defaults=False)
186 except Exception:
185 except Exception:
187 log.error(traceback.format_exc())
186 log.error(traceback.format_exc())
188 h.flash(_('Error occurred during creation of repository group %s') \
187 h.flash(_('Error occurred during creation of repository group %s') \
189 % request.POST.get('group_name'), category='error')
188 % request.POST.get('group_name'), category='error')
190 parent_group_id = form_result['group_parent_id']
189 parent_group_id = form_result['group_parent_id']
191 #TODO: maybe we should get back to the main view, not the admin one
190 #TODO: maybe we should get back to the main view, not the admin one
192 return redirect(url('repos_groups', parent_group=parent_group_id))
191 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
193 h.flash(_('Created repository group %s') % gr.group_name,
192 h.flash(_('Created repository group %s') % gr.group_name,
194 category='success')
193 category='success')
195 return redirect(url('repos_group_home', group_name=gr.group_name))
194 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
196
195
197 def new(self):
196 def new(self):
198 """GET /repo_groups/new: Form to create a new item"""
197 """GET /repo_groups/new: Form to create a new item"""
199 # url('new_repos_group')
198 # url('new_repos_group')
200 if HasPermissionAll('hg.admin')('group create'):
199 if HasPermissionAll('hg.admin')('group create'):
201 #we're global admin, we're ok and we can create TOP level groups
200 #we're global admin, we're ok and we can create TOP level groups
202 pass
201 pass
203 else:
202 else:
204 # we pass in parent group into creation form, thus we know
203 # we pass in parent group into creation form, thus we know
205 # what would be the group, we can check perms here !
204 # what would be the group, we can check perms here !
206 group_id = safe_int(request.GET.get('parent_group'))
205 group_id = safe_int(request.GET.get('parent_group'))
207 group = RepoGroup.get(group_id) if group_id else None
206 group = RepoGroup.get(group_id) if group_id else None
208 group_name = group.group_name if group else None
207 group_name = group.group_name if group else None
209 if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
208 if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
210 pass
209 pass
211 else:
210 else:
212 raise HTTPForbidden()
211 raise HTTPForbidden()
213
212
214 self.__load_defaults()
213 self.__load_defaults()
215 return render('admin/repo_groups/repo_group_add.html')
214 return render('admin/repo_groups/repo_group_add.html')
216
215
217 @HasRepoGroupPermissionAnyDecorator('group.admin')
216 @HasRepoGroupPermissionAnyDecorator('group.admin')
218 def update(self, group_name):
217 def update(self, group_name):
219 """PUT /repo_groups/group_name: Update an existing item"""
218 """PUT /repo_groups/group_name: Update an existing item"""
220 # Forms posted to this method should contain a hidden field:
219 # Forms posted to this method should contain a hidden field:
221 # <input type="hidden" name="_method" value="PUT" />
220 # <input type="hidden" name="_method" value="PUT" />
222 # Or using helpers:
221 # Or using helpers:
223 # h.form(url('repos_group', group_name=GROUP_NAME),
222 # h.form(url('repos_group', group_name=GROUP_NAME),
224 # method='put')
223 # method='put')
225 # url('repos_group', group_name=GROUP_NAME)
224 # url('repos_group', group_name=GROUP_NAME)
226
225
227 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
226 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
228 self.__load_defaults(extras=[c.repo_group.parent_group],
227 self.__load_defaults(extras=[c.repo_group.parent_group],
229 exclude=[c.repo_group])
228 exclude=[c.repo_group])
230
229
231 # TODO: kill allow_empty_group - it is only used for redundant form validation!
230 # TODO: kill allow_empty_group - it is only used for redundant form validation!
232 if HasPermissionAll('hg.admin')('group edit'):
231 if HasPermissionAll('hg.admin')('group edit'):
233 #we're global admin, we're ok and we can create TOP level groups
232 #we're global admin, we're ok and we can create TOP level groups
234 allow_empty_group = True
233 allow_empty_group = True
235 elif not c.repo_group.parent_group:
234 elif not c.repo_group.parent_group:
236 allow_empty_group = True
235 allow_empty_group = True
237 else:
236 else:
238 allow_empty_group = False
237 allow_empty_group = False
239 repo_group_form = RepoGroupForm(
238 repo_group_form = RepoGroupForm(
240 edit=True,
239 edit=True,
241 old_data=c.repo_group.get_dict(),
240 old_data=c.repo_group.get_dict(),
242 repo_groups=c.repo_groups,
241 repo_groups=c.repo_groups,
243 can_create_in_root=allow_empty_group,
242 can_create_in_root=allow_empty_group,
244 )()
243 )()
245 try:
244 try:
246 form_result = repo_group_form.to_python(dict(request.POST))
245 form_result = repo_group_form.to_python(dict(request.POST))
247
246
248 new_gr = RepoGroupModel().update(group_name, form_result)
247 new_gr = RepoGroupModel().update(group_name, form_result)
249 Session().commit()
248 Session().commit()
250 h.flash(_('Updated repository group %s') \
249 h.flash(_('Updated repository group %s') \
251 % form_result['group_name'], category='success')
250 % form_result['group_name'], category='success')
252 # we now have new name !
251 # we now have new name !
253 group_name = new_gr.group_name
252 group_name = new_gr.group_name
254 #TODO: in future action_logger(, '', '', '', self.sa)
253 #TODO: in future action_logger(, '', '', '', self.sa)
255 except formencode.Invalid as errors:
254 except formencode.Invalid as errors:
256
255
257 return htmlfill.render(
256 return htmlfill.render(
258 render('admin/repo_groups/repo_group_edit.html'),
257 render('admin/repo_groups/repo_group_edit.html'),
259 defaults=errors.value,
258 defaults=errors.value,
260 errors=errors.error_dict or {},
259 errors=errors.error_dict or {},
261 prefix_error=False,
260 prefix_error=False,
262 encoding="UTF-8",
261 encoding="UTF-8",
263 force_defaults=False)
262 force_defaults=False)
264 except Exception:
263 except Exception:
265 log.error(traceback.format_exc())
264 log.error(traceback.format_exc())
266 h.flash(_('Error occurred during update of repository group %s') \
265 h.flash(_('Error occurred during update of repository group %s') \
267 % request.POST.get('group_name'), category='error')
266 % request.POST.get('group_name'), category='error')
268
267
269 return redirect(url('edit_repo_group', group_name=group_name))
268 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
270
269
271 @HasRepoGroupPermissionAnyDecorator('group.admin')
270 @HasRepoGroupPermissionAnyDecorator('group.admin')
272 def delete(self, group_name):
271 def delete(self, group_name):
273 """DELETE /repo_groups/group_name: Delete an existing item"""
272 """DELETE /repo_groups/group_name: Delete an existing item"""
274 # Forms posted to this method should contain a hidden field:
273 # Forms posted to this method should contain a hidden field:
275 # <input type="hidden" name="_method" value="DELETE" />
274 # <input type="hidden" name="_method" value="DELETE" />
276 # Or using helpers:
275 # Or using helpers:
277 # h.form(url('repos_group', group_name=GROUP_NAME),
276 # h.form(url('repos_group', group_name=GROUP_NAME),
278 # method='delete')
277 # method='delete')
279 # url('repos_group', group_name=GROUP_NAME)
278 # url('repos_group', group_name=GROUP_NAME)
280
279
281 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
280 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
282 repos = gr.repositories.all()
281 repos = gr.repositories.all()
283 if repos:
282 if repos:
284 h.flash(_('This group contains %s repositories and cannot be '
283 h.flash(_('This group contains %s repositories and cannot be '
285 'deleted') % len(repos), category='warning')
284 'deleted') % len(repos), category='warning')
286 return redirect(url('repos_groups'))
285 raise HTTPFound(location=url('repos_groups'))
287
286
288 children = gr.children.all()
287 children = gr.children.all()
289 if children:
288 if children:
290 h.flash(_('This group contains %s subgroups and cannot be deleted'
289 h.flash(_('This group contains %s subgroups and cannot be deleted'
291 % (len(children))), category='warning')
290 % (len(children))), category='warning')
292 return redirect(url('repos_groups'))
291 raise HTTPFound(location=url('repos_groups'))
293
292
294 try:
293 try:
295 RepoGroupModel().delete(group_name)
294 RepoGroupModel().delete(group_name)
296 Session().commit()
295 Session().commit()
297 h.flash(_('Removed repository group %s') % group_name,
296 h.flash(_('Removed repository group %s') % group_name,
298 category='success')
297 category='success')
299 #TODO: in future action_logger(, '', '', '', self.sa)
298 #TODO: in future action_logger(, '', '', '', self.sa)
300 except Exception:
299 except Exception:
301 log.error(traceback.format_exc())
300 log.error(traceback.format_exc())
302 h.flash(_('Error occurred during deletion of repository group %s')
301 h.flash(_('Error occurred during deletion of repository group %s')
303 % group_name, category='error')
302 % group_name, category='error')
304
303
305 if gr.parent_group:
304 if gr.parent_group:
306 return redirect(url('repos_group_home', group_name=gr.parent_group.group_name))
305 raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name))
307 return redirect(url('repos_groups'))
306 raise HTTPFound(location=url('repos_groups'))
308
307
309 def show_by_name(self, group_name):
308 def show_by_name(self, group_name):
310 """
309 """
311 This is a proxy that does a lookup group_name -> id, and shows
310 This is a proxy that does a lookup group_name -> id, and shows
312 the group by id view instead
311 the group by id view instead
313 """
312 """
314 group_name = group_name.rstrip('/')
313 group_name = group_name.rstrip('/')
315 id_ = RepoGroup.get_by_group_name(group_name)
314 id_ = RepoGroup.get_by_group_name(group_name)
316 if id_:
315 if id_:
317 return self.show(group_name)
316 return self.show(group_name)
318 raise HTTPNotFound
317 raise HTTPNotFound
319
318
320 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
319 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
321 'group.admin')
320 'group.admin')
322 def show(self, group_name):
321 def show(self, group_name):
323 """GET /repo_groups/group_name: Show a specific item"""
322 """GET /repo_groups/group_name: Show a specific item"""
324 # url('repos_group', group_name=GROUP_NAME)
323 # url('repos_group', group_name=GROUP_NAME)
325 c.active = 'settings'
324 c.active = 'settings'
326
325
327 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
326 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
328 c.group_repos = c.group.repositories.all()
327 c.group_repos = c.group.repositories.all()
329
328
330 #overwrite our cached list with current filter
329 #overwrite our cached list with current filter
331 c.repo_cnt = 0
330 c.repo_cnt = 0
332
331
333 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
332 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
334 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
333 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
335 c.groups = self.scm_model.get_repo_groups(groups)
334 c.groups = self.scm_model.get_repo_groups(groups)
336
335
337 c.repos_list = Repository.query()\
336 c.repos_list = Repository.query()\
338 .filter(Repository.group_id == c.group.group_id)\
337 .filter(Repository.group_id == c.group.group_id)\
339 .order_by(func.lower(Repository.repo_name))\
338 .order_by(func.lower(Repository.repo_name))\
340 .all()
339 .all()
341
340
342 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
341 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
343 admin=False)
342 admin=False)
344 #json used to render the grid
343 #json used to render the grid
345 c.data = json.dumps(repos_data)
344 c.data = json.dumps(repos_data)
346
345
347 return render('admin/repo_groups/repo_group_show.html')
346 return render('admin/repo_groups/repo_group_show.html')
348
347
349 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 @HasRepoGroupPermissionAnyDecorator('group.admin')
350 def edit(self, group_name):
349 def edit(self, group_name):
351 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
352 # url('edit_repo_group', group_name=GROUP_NAME)
351 # url('edit_repo_group', group_name=GROUP_NAME)
353 c.active = 'settings'
352 c.active = 'settings'
354
353
355 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
354 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
356 self.__load_defaults(extras=[c.repo_group.parent_group],
355 self.__load_defaults(extras=[c.repo_group.parent_group],
357 exclude=[c.repo_group])
356 exclude=[c.repo_group])
358 defaults = self.__load_data(c.repo_group.group_id)
357 defaults = self.__load_data(c.repo_group.group_id)
359
358
360 return htmlfill.render(
359 return htmlfill.render(
361 render('admin/repo_groups/repo_group_edit.html'),
360 render('admin/repo_groups/repo_group_edit.html'),
362 defaults=defaults,
361 defaults=defaults,
363 encoding="UTF-8",
362 encoding="UTF-8",
364 force_defaults=False
363 force_defaults=False
365 )
364 )
366
365
367 @HasRepoGroupPermissionAnyDecorator('group.admin')
366 @HasRepoGroupPermissionAnyDecorator('group.admin')
368 def edit_repo_group_advanced(self, group_name):
367 def edit_repo_group_advanced(self, group_name):
369 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
368 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
370 # url('edit_repo_group', group_name=GROUP_NAME)
369 # url('edit_repo_group', group_name=GROUP_NAME)
371 c.active = 'advanced'
370 c.active = 'advanced'
372 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
371 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
373
372
374 return render('admin/repo_groups/repo_group_edit.html')
373 return render('admin/repo_groups/repo_group_edit.html')
375
374
376 @HasRepoGroupPermissionAnyDecorator('group.admin')
375 @HasRepoGroupPermissionAnyDecorator('group.admin')
377 def edit_repo_group_perms(self, group_name):
376 def edit_repo_group_perms(self, group_name):
378 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
377 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
379 # url('edit_repo_group', group_name=GROUP_NAME)
378 # url('edit_repo_group', group_name=GROUP_NAME)
380 c.active = 'perms'
379 c.active = 'perms'
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
380 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 self.__load_defaults()
381 self.__load_defaults()
383 defaults = self.__load_data(c.repo_group.group_id)
382 defaults = self.__load_data(c.repo_group.group_id)
384
383
385 return htmlfill.render(
384 return htmlfill.render(
386 render('admin/repo_groups/repo_group_edit.html'),
385 render('admin/repo_groups/repo_group_edit.html'),
387 defaults=defaults,
386 defaults=defaults,
388 encoding="UTF-8",
387 encoding="UTF-8",
389 force_defaults=False
388 force_defaults=False
390 )
389 )
391
390
392 @HasRepoGroupPermissionAnyDecorator('group.admin')
391 @HasRepoGroupPermissionAnyDecorator('group.admin')
393 def update_perms(self, group_name):
392 def update_perms(self, group_name):
394 """
393 """
395 Update permissions for given repository group
394 Update permissions for given repository group
396
395
397 :param group_name:
396 :param group_name:
398 """
397 """
399
398
400 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
399 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
401 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
400 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
402 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
401 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
403 if not c.authuser.is_admin:
402 if not c.authuser.is_admin:
404 if self._revoke_perms_on_yourself(form_result):
403 if self._revoke_perms_on_yourself(form_result):
405 msg = _('Cannot revoke permission for yourself as admin')
404 msg = _('Cannot revoke permission for yourself as admin')
406 h.flash(msg, category='warning')
405 h.flash(msg, category='warning')
407 return redirect(url('edit_repo_group_perms', group_name=group_name))
406 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
408 recursive = form_result['recursive']
407 recursive = form_result['recursive']
409 # iterate over all members(if in recursive mode) of this groups and
408 # iterate over all members(if in recursive mode) of this groups and
410 # set the permissions !
409 # set the permissions !
411 # this can be potentially heavy operation
410 # this can be potentially heavy operation
412 RepoGroupModel()._update_permissions(c.repo_group,
411 RepoGroupModel()._update_permissions(c.repo_group,
413 form_result['perms_new'],
412 form_result['perms_new'],
414 form_result['perms_updates'],
413 form_result['perms_updates'],
415 recursive)
414 recursive)
416 #TODO: implement this
415 #TODO: implement this
417 #action_logger(self.authuser, 'admin_changed_repo_permissions',
416 #action_logger(self.authuser, 'admin_changed_repo_permissions',
418 # repo_name, self.ip_addr, self.sa)
417 # repo_name, self.ip_addr, self.sa)
419 Session().commit()
418 Session().commit()
420 h.flash(_('Repository group permissions updated'), category='success')
419 h.flash(_('Repository group permissions updated'), category='success')
421 return redirect(url('edit_repo_group_perms', group_name=group_name))
420 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
422
421
423 @HasRepoGroupPermissionAnyDecorator('group.admin')
422 @HasRepoGroupPermissionAnyDecorator('group.admin')
424 def delete_perms(self, group_name):
423 def delete_perms(self, group_name):
425 """
424 """
426 DELETE an existing repository group permission user
425 DELETE an existing repository group permission user
427
426
428 :param group_name:
427 :param group_name:
429 """
428 """
430 try:
429 try:
431 obj_type = request.POST.get('obj_type')
430 obj_type = request.POST.get('obj_type')
432 obj_id = None
431 obj_id = None
433 if obj_type == 'user':
432 if obj_type == 'user':
434 obj_id = safe_int(request.POST.get('user_id'))
433 obj_id = safe_int(request.POST.get('user_id'))
435 elif obj_type == 'user_group':
434 elif obj_type == 'user_group':
436 obj_id = safe_int(request.POST.get('user_group_id'))
435 obj_id = safe_int(request.POST.get('user_group_id'))
437
436
438 if not c.authuser.is_admin:
437 if not c.authuser.is_admin:
439 if obj_type == 'user' and c.authuser.user_id == obj_id:
438 if obj_type == 'user' and c.authuser.user_id == obj_id:
440 msg = _('Cannot revoke permission for yourself as admin')
439 msg = _('Cannot revoke permission for yourself as admin')
441 h.flash(msg, category='warning')
440 h.flash(msg, category='warning')
442 raise Exception('revoke admin permission on self')
441 raise Exception('revoke admin permission on self')
443 recursive = request.POST.get('recursive', 'none')
442 recursive = request.POST.get('recursive', 'none')
444 if obj_type == 'user':
443 if obj_type == 'user':
445 RepoGroupModel().delete_permission(repo_group=group_name,
444 RepoGroupModel().delete_permission(repo_group=group_name,
446 obj=obj_id, obj_type='user',
445 obj=obj_id, obj_type='user',
447 recursive=recursive)
446 recursive=recursive)
448 elif obj_type == 'user_group':
447 elif obj_type == 'user_group':
449 RepoGroupModel().delete_permission(repo_group=group_name,
448 RepoGroupModel().delete_permission(repo_group=group_name,
450 obj=obj_id,
449 obj=obj_id,
451 obj_type='user_group',
450 obj_type='user_group',
452 recursive=recursive)
451 recursive=recursive)
453
452
454 Session().commit()
453 Session().commit()
455 except Exception:
454 except Exception:
456 log.error(traceback.format_exc())
455 log.error(traceback.format_exc())
457 h.flash(_('An error occurred during revoking of permission'),
456 h.flash(_('An error occurred during revoking of permission'),
458 category='error')
457 category='error')
459 raise HTTPInternalServerError()
458 raise HTTPInternalServerError()
@@ -1,641 +1,640 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.repos
15 kallithea.controllers.admin.repos
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Repositories controller for Kallithea
18 Repositories controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 7, 2010
22 :created_on: Apr 7, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31 from formencode import htmlfill
31 from formencode import htmlfill
32 from webob.exc import HTTPInternalServerError, HTTPForbidden, HTTPNotFound
33 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
36 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
37
36
38 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, \
38 from kallithea.lib.auth import LoginRequired, \
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, \
39 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, \
41 HasRepoPermissionAnyDecorator
40 HasRepoPermissionAnyDecorator
42 from kallithea.lib.base import BaseRepoController, render
41 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.lib.utils import action_logger, jsonify
42 from kallithea.lib.utils import action_logger, jsonify
44 from kallithea.lib.vcs import RepositoryError
43 from kallithea.lib.vcs import RepositoryError
45 from kallithea.model.meta import Session
44 from kallithea.model.meta import Session
46 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup,\
45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup,\
47 Setting, RepositoryField
46 Setting, RepositoryField
48 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
49 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
50 from kallithea.model.repo import RepoModel
49 from kallithea.model.repo import RepoModel
51 from kallithea.lib.compat import json
50 from kallithea.lib.compat import json
52 from kallithea.lib.exceptions import AttachedForksError
51 from kallithea.lib.exceptions import AttachedForksError
53 from kallithea.lib.utils2 import safe_int
52 from kallithea.lib.utils2 import safe_int
54
53
55 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
56
55
57
56
58 class ReposController(BaseRepoController):
57 class ReposController(BaseRepoController):
59 """
58 """
60 REST Controller styled on the Atom Publishing Protocol"""
59 REST Controller styled on the Atom Publishing Protocol"""
61 # To properly map this controller, ensure your config/routing.py
60 # To properly map this controller, ensure your config/routing.py
62 # file has a resource setup:
61 # file has a resource setup:
63 # map.resource('repo', 'repos')
62 # map.resource('repo', 'repos')
64
63
65 @LoginRequired()
64 @LoginRequired()
66 def __before__(self):
65 def __before__(self):
67 super(ReposController, self).__before__()
66 super(ReposController, self).__before__()
68
67
69 def _load_repo(self, repo_name):
68 def _load_repo(self, repo_name):
70 repo_obj = Repository.get_by_repo_name(repo_name)
69 repo_obj = Repository.get_by_repo_name(repo_name)
71
70
72 if repo_obj is None:
71 if repo_obj is None:
73 h.not_mapped_error(repo_name)
72 h.not_mapped_error(repo_name)
74 return redirect(url('repos'))
73 raise HTTPFound(location=url('repos'))
75
74
76 return repo_obj
75 return repo_obj
77
76
78 def __load_defaults(self, repo=None):
77 def __load_defaults(self, repo=None):
79 top_perms = ['hg.create.repository']
78 top_perms = ['hg.create.repository']
80 repo_group_perms = ['group.admin']
79 repo_group_perms = ['group.admin']
81 if HasPermissionAny('hg.create.write_on_repogroup.true')():
80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
82 repo_group_perms.append('group.write')
81 repo_group_perms.append('group.write')
83 extras = [] if repo is None else [repo.group]
82 extras = [] if repo is None else [repo.group]
84
83
85 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
86
85
87 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
88
87
89 def __load_data(self, repo_name=None):
88 def __load_data(self, repo_name=None):
90 """
89 """
91 Load defaults settings for edit, and update
90 Load defaults settings for edit, and update
92
91
93 :param repo_name:
92 :param repo_name:
94 """
93 """
95 c.repo_info = self._load_repo(repo_name)
94 c.repo_info = self._load_repo(repo_name)
96 self.__load_defaults(c.repo_info)
95 self.__load_defaults(c.repo_info)
97
96
98 defaults = RepoModel()._get_defaults(repo_name)
97 defaults = RepoModel()._get_defaults(repo_name)
99 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
98 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
100
99
101 return defaults
100 return defaults
102
101
103 def index(self, format='html'):
102 def index(self, format='html'):
104 """GET /repos: All items in the collection"""
103 """GET /repos: All items in the collection"""
105 # url('repos')
104 # url('repos')
106 _list = Repository.query()\
105 _list = Repository.query()\
107 .order_by(func.lower(Repository.repo_name))\
106 .order_by(func.lower(Repository.repo_name))\
108 .all()
107 .all()
109
108
110 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
109 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
111 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
110 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
112 admin=True,
111 admin=True,
113 super_user_actions=True)
112 super_user_actions=True)
114 #json used to render the grid
113 #json used to render the grid
115 c.data = json.dumps(repos_data)
114 c.data = json.dumps(repos_data)
116
115
117 return render('admin/repos/repos.html')
116 return render('admin/repos/repos.html')
118
117
119 @NotAnonymous()
118 @NotAnonymous()
120 def create(self):
119 def create(self):
121 """
120 """
122 POST /repos: Create a new item"""
121 POST /repos: Create a new item"""
123 # url('repos')
122 # url('repos')
124
123
125 self.__load_defaults()
124 self.__load_defaults()
126 form_result = {}
125 form_result = {}
127 task_id = None
126 task_id = None
128 try:
127 try:
129 # CanWriteGroup validators checks permissions of this POST
128 # CanWriteGroup validators checks permissions of this POST
130 form_result = RepoForm(repo_groups=c.repo_groups,
129 form_result = RepoForm(repo_groups=c.repo_groups,
131 landing_revs=c.landing_revs_choices)()\
130 landing_revs=c.landing_revs_choices)()\
132 .to_python(dict(request.POST))
131 .to_python(dict(request.POST))
133
132
134 # create is done sometimes async on celery, db transaction
133 # create is done sometimes async on celery, db transaction
135 # management is handled there.
134 # management is handled there.
136 task = RepoModel().create(form_result, self.authuser.user_id)
135 task = RepoModel().create(form_result, self.authuser.user_id)
137 from celery.result import BaseAsyncResult
136 from celery.result import BaseAsyncResult
138 if isinstance(task, BaseAsyncResult):
137 if isinstance(task, BaseAsyncResult):
139 task_id = task.task_id
138 task_id = task.task_id
140 except formencode.Invalid as errors:
139 except formencode.Invalid as errors:
141 log.info(errors)
140 log.info(errors)
142 return htmlfill.render(
141 return htmlfill.render(
143 render('admin/repos/repo_add.html'),
142 render('admin/repos/repo_add.html'),
144 defaults=errors.value,
143 defaults=errors.value,
145 errors=errors.error_dict or {},
144 errors=errors.error_dict or {},
146 prefix_error=False,
145 prefix_error=False,
147 force_defaults=False,
146 force_defaults=False,
148 encoding="UTF-8")
147 encoding="UTF-8")
149
148
150 except Exception:
149 except Exception:
151 log.error(traceback.format_exc())
150 log.error(traceback.format_exc())
152 msg = (_('Error creating repository %s')
151 msg = (_('Error creating repository %s')
153 % form_result.get('repo_name'))
152 % form_result.get('repo_name'))
154 h.flash(msg, category='error')
153 h.flash(msg, category='error')
155 return redirect(url('home'))
154 raise HTTPFound(location=url('home'))
156
155
157 return redirect(h.url('repo_creating_home',
156 raise HTTPFound(location=h.url('repo_creating_home',
158 repo_name=form_result['repo_name_full'],
157 repo_name=form_result['repo_name_full'],
159 task_id=task_id))
158 task_id=task_id))
160
159
161 @NotAnonymous()
160 @NotAnonymous()
162 def create_repository(self):
161 def create_repository(self):
163 """GET /_admin/create_repository: Form to create a new item"""
162 """GET /_admin/create_repository: Form to create a new item"""
164 self.__load_defaults()
163 self.__load_defaults()
165 if not c.repo_groups:
164 if not c.repo_groups:
166 raise HTTPForbidden
165 raise HTTPForbidden
167 parent_group = request.GET.get('parent_group')
166 parent_group = request.GET.get('parent_group')
168
167
169 ## apply the defaults from defaults page
168 ## apply the defaults from defaults page
170 defaults = Setting.get_default_repo_settings(strip_prefix=True)
169 defaults = Setting.get_default_repo_settings(strip_prefix=True)
171 if parent_group:
170 if parent_group:
172 prg = RepoGroup.get(parent_group)
171 prg = RepoGroup.get(parent_group)
173 if prg is None or not any(rgc[0] == prg.group_id
172 if prg is None or not any(rgc[0] == prg.group_id
174 for rgc in c.repo_groups):
173 for rgc in c.repo_groups):
175 raise HTTPForbidden
174 raise HTTPForbidden
176 defaults.update({'repo_group': parent_group})
175 defaults.update({'repo_group': parent_group})
177
176
178 return htmlfill.render(
177 return htmlfill.render(
179 render('admin/repos/repo_add.html'),
178 render('admin/repos/repo_add.html'),
180 defaults=defaults,
179 defaults=defaults,
181 errors={},
180 errors={},
182 prefix_error=False,
181 prefix_error=False,
183 encoding="UTF-8",
182 encoding="UTF-8",
184 force_defaults=False)
183 force_defaults=False)
185
184
186 @LoginRequired()
185 @LoginRequired()
187 @NotAnonymous()
186 @NotAnonymous()
188 def repo_creating(self, repo_name):
187 def repo_creating(self, repo_name):
189 c.repo = repo_name
188 c.repo = repo_name
190 c.task_id = request.GET.get('task_id')
189 c.task_id = request.GET.get('task_id')
191 if not c.repo:
190 if not c.repo:
192 raise HTTPNotFound()
191 raise HTTPNotFound()
193 return render('admin/repos/repo_creating.html')
192 return render('admin/repos/repo_creating.html')
194
193
195 @LoginRequired()
194 @LoginRequired()
196 @NotAnonymous()
195 @NotAnonymous()
197 @jsonify
196 @jsonify
198 def repo_check(self, repo_name):
197 def repo_check(self, repo_name):
199 c.repo = repo_name
198 c.repo = repo_name
200 task_id = request.GET.get('task_id')
199 task_id = request.GET.get('task_id')
201
200
202 if task_id and task_id not in ['None']:
201 if task_id and task_id not in ['None']:
203 from kallithea import CELERY_ON
202 from kallithea import CELERY_ON
204 from celery.result import AsyncResult
203 from celery.result import AsyncResult
205 if CELERY_ON:
204 if CELERY_ON:
206 task = AsyncResult(task_id)
205 task = AsyncResult(task_id)
207 if task.failed():
206 if task.failed():
208 raise HTTPInternalServerError(task.traceback)
207 raise HTTPInternalServerError(task.traceback)
209
208
210 repo = Repository.get_by_repo_name(repo_name)
209 repo = Repository.get_by_repo_name(repo_name)
211 if repo and repo.repo_state == Repository.STATE_CREATED:
210 if repo and repo.repo_state == Repository.STATE_CREATED:
212 if repo.clone_uri:
211 if repo.clone_uri:
213 h.flash(_('Created repository %s from %s')
212 h.flash(_('Created repository %s from %s')
214 % (repo.repo_name, repo.clone_uri_hidden), category='success')
213 % (repo.repo_name, repo.clone_uri_hidden), category='success')
215 else:
214 else:
216 repo_url = h.link_to(repo.repo_name,
215 repo_url = h.link_to(repo.repo_name,
217 h.url('summary_home',
216 h.url('summary_home',
218 repo_name=repo.repo_name))
217 repo_name=repo.repo_name))
219 fork = repo.fork
218 fork = repo.fork
220 if fork is not None:
219 if fork is not None:
221 fork_name = fork.repo_name
220 fork_name = fork.repo_name
222 h.flash(h.literal(_('Forked repository %s as %s')
221 h.flash(h.literal(_('Forked repository %s as %s')
223 % (fork_name, repo_url)), category='success')
222 % (fork_name, repo_url)), category='success')
224 else:
223 else:
225 h.flash(h.literal(_('Created repository %s') % repo_url),
224 h.flash(h.literal(_('Created repository %s') % repo_url),
226 category='success')
225 category='success')
227 return {'result': True}
226 return {'result': True}
228 return {'result': False}
227 return {'result': False}
229
228
230 @HasRepoPermissionAllDecorator('repository.admin')
229 @HasRepoPermissionAllDecorator('repository.admin')
231 def update(self, repo_name):
230 def update(self, repo_name):
232 """
231 """
233 PUT /repos/repo_name: Update an existing item"""
232 PUT /repos/repo_name: Update an existing item"""
234 # Forms posted to this method should contain a hidden field:
233 # Forms posted to this method should contain a hidden field:
235 # <input type="hidden" name="_method" value="PUT" />
234 # <input type="hidden" name="_method" value="PUT" />
236 # Or using helpers:
235 # Or using helpers:
237 # h.form(url('put_repo', repo_name=ID),
236 # h.form(url('put_repo', repo_name=ID),
238 # method='put')
237 # method='put')
239 # url('put_repo', repo_name=ID)
238 # url('put_repo', repo_name=ID)
240 c.repo_info = self._load_repo(repo_name)
239 c.repo_info = self._load_repo(repo_name)
241 self.__load_defaults(c.repo_info)
240 self.__load_defaults(c.repo_info)
242 c.active = 'settings'
241 c.active = 'settings'
243 c.repo_fields = RepositoryField.query()\
242 c.repo_fields = RepositoryField.query()\
244 .filter(RepositoryField.repository == c.repo_info).all()
243 .filter(RepositoryField.repository == c.repo_info).all()
245
244
246 repo_model = RepoModel()
245 repo_model = RepoModel()
247 changed_name = repo_name
246 changed_name = repo_name
248 repo = Repository.get_by_repo_name(repo_name)
247 repo = Repository.get_by_repo_name(repo_name)
249 old_data = {
248 old_data = {
250 'repo_name': repo_name,
249 'repo_name': repo_name,
251 'repo_group': repo.group.get_dict() if repo.group else {},
250 'repo_group': repo.group.get_dict() if repo.group else {},
252 'repo_type': repo.repo_type,
251 'repo_type': repo.repo_type,
253 }
252 }
254 _form = RepoForm(edit=True, old_data=old_data,
253 _form = RepoForm(edit=True, old_data=old_data,
255 repo_groups=c.repo_groups,
254 repo_groups=c.repo_groups,
256 landing_revs=c.landing_revs_choices)()
255 landing_revs=c.landing_revs_choices)()
257
256
258 try:
257 try:
259 form_result = _form.to_python(dict(request.POST))
258 form_result = _form.to_python(dict(request.POST))
260 repo = repo_model.update(repo_name, **form_result)
259 repo = repo_model.update(repo_name, **form_result)
261 ScmModel().mark_for_invalidation(repo_name)
260 ScmModel().mark_for_invalidation(repo_name)
262 h.flash(_('Repository %s updated successfully') % repo_name,
261 h.flash(_('Repository %s updated successfully') % repo_name,
263 category='success')
262 category='success')
264 changed_name = repo.repo_name
263 changed_name = repo.repo_name
265 action_logger(self.authuser, 'admin_updated_repo',
264 action_logger(self.authuser, 'admin_updated_repo',
266 changed_name, self.ip_addr, self.sa)
265 changed_name, self.ip_addr, self.sa)
267 Session().commit()
266 Session().commit()
268 except formencode.Invalid as errors:
267 except formencode.Invalid as errors:
269 log.info(errors)
268 log.info(errors)
270 defaults = self.__load_data(repo_name)
269 defaults = self.__load_data(repo_name)
271 defaults.update(errors.value)
270 defaults.update(errors.value)
272 c.users_array = repo_model.get_users_js()
271 c.users_array = repo_model.get_users_js()
273 return htmlfill.render(
272 return htmlfill.render(
274 render('admin/repos/repo_edit.html'),
273 render('admin/repos/repo_edit.html'),
275 defaults=defaults,
274 defaults=defaults,
276 errors=errors.error_dict or {},
275 errors=errors.error_dict or {},
277 prefix_error=False,
276 prefix_error=False,
278 encoding="UTF-8",
277 encoding="UTF-8",
279 force_defaults=False)
278 force_defaults=False)
280
279
281 except Exception:
280 except Exception:
282 log.error(traceback.format_exc())
281 log.error(traceback.format_exc())
283 h.flash(_('Error occurred during update of repository %s') \
282 h.flash(_('Error occurred during update of repository %s') \
284 % repo_name, category='error')
283 % repo_name, category='error')
285 return redirect(url('edit_repo', repo_name=changed_name))
284 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
286
285
287 @HasRepoPermissionAllDecorator('repository.admin')
286 @HasRepoPermissionAllDecorator('repository.admin')
288 def delete(self, repo_name):
287 def delete(self, repo_name):
289 """
288 """
290 DELETE /repos/repo_name: Delete an existing item"""
289 DELETE /repos/repo_name: Delete an existing item"""
291 # Forms posted to this method should contain a hidden field:
290 # Forms posted to this method should contain a hidden field:
292 # <input type="hidden" name="_method" value="DELETE" />
291 # <input type="hidden" name="_method" value="DELETE" />
293 # Or using helpers:
292 # Or using helpers:
294 # h.form(url('delete_repo', repo_name=ID),
293 # h.form(url('delete_repo', repo_name=ID),
295 # method='delete')
294 # method='delete')
296 # url('delete_repo', repo_name=ID)
295 # url('delete_repo', repo_name=ID)
297
296
298 repo_model = RepoModel()
297 repo_model = RepoModel()
299 repo = repo_model.get_by_repo_name(repo_name)
298 repo = repo_model.get_by_repo_name(repo_name)
300 if not repo:
299 if not repo:
301 h.not_mapped_error(repo_name)
300 h.not_mapped_error(repo_name)
302 return redirect(url('repos'))
301 raise HTTPFound(location=url('repos'))
303 try:
302 try:
304 _forks = repo.forks.count()
303 _forks = repo.forks.count()
305 handle_forks = None
304 handle_forks = None
306 if _forks and request.POST.get('forks'):
305 if _forks and request.POST.get('forks'):
307 do = request.POST['forks']
306 do = request.POST['forks']
308 if do == 'detach_forks':
307 if do == 'detach_forks':
309 handle_forks = 'detach'
308 handle_forks = 'detach'
310 h.flash(_('Detached %s forks') % _forks, category='success')
309 h.flash(_('Detached %s forks') % _forks, category='success')
311 elif do == 'delete_forks':
310 elif do == 'delete_forks':
312 handle_forks = 'delete'
311 handle_forks = 'delete'
313 h.flash(_('Deleted %s forks') % _forks, category='success')
312 h.flash(_('Deleted %s forks') % _forks, category='success')
314 repo_model.delete(repo, forks=handle_forks)
313 repo_model.delete(repo, forks=handle_forks)
315 action_logger(self.authuser, 'admin_deleted_repo',
314 action_logger(self.authuser, 'admin_deleted_repo',
316 repo_name, self.ip_addr, self.sa)
315 repo_name, self.ip_addr, self.sa)
317 ScmModel().mark_for_invalidation(repo_name)
316 ScmModel().mark_for_invalidation(repo_name)
318 h.flash(_('Deleted repository %s') % repo_name, category='success')
317 h.flash(_('Deleted repository %s') % repo_name, category='success')
319 Session().commit()
318 Session().commit()
320 except AttachedForksError:
319 except AttachedForksError:
321 h.flash(_('Cannot delete repository %s which still has forks')
320 h.flash(_('Cannot delete repository %s which still has forks')
322 % repo_name, category='warning')
321 % repo_name, category='warning')
323
322
324 except Exception:
323 except Exception:
325 log.error(traceback.format_exc())
324 log.error(traceback.format_exc())
326 h.flash(_('An error occurred during deletion of %s') % repo_name,
325 h.flash(_('An error occurred during deletion of %s') % repo_name,
327 category='error')
326 category='error')
328
327
329 if repo.group:
328 if repo.group:
330 return redirect(url('repos_group_home', group_name=repo.group.group_name))
329 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
331 return redirect(url('repos'))
330 raise HTTPFound(location=url('repos'))
332
331
333 @HasRepoPermissionAllDecorator('repository.admin')
332 @HasRepoPermissionAllDecorator('repository.admin')
334 def edit(self, repo_name):
333 def edit(self, repo_name):
335 """GET /repo_name/settings: Form to edit an existing item"""
334 """GET /repo_name/settings: Form to edit an existing item"""
336 # url('edit_repo', repo_name=ID)
335 # url('edit_repo', repo_name=ID)
337 defaults = self.__load_data(repo_name)
336 defaults = self.__load_data(repo_name)
338 c.repo_fields = RepositoryField.query()\
337 c.repo_fields = RepositoryField.query()\
339 .filter(RepositoryField.repository == c.repo_info).all()
338 .filter(RepositoryField.repository == c.repo_info).all()
340 repo_model = RepoModel()
339 repo_model = RepoModel()
341 c.users_array = repo_model.get_users_js()
340 c.users_array = repo_model.get_users_js()
342 c.active = 'settings'
341 c.active = 'settings'
343 return htmlfill.render(
342 return htmlfill.render(
344 render('admin/repos/repo_edit.html'),
343 render('admin/repos/repo_edit.html'),
345 defaults=defaults,
344 defaults=defaults,
346 encoding="UTF-8",
345 encoding="UTF-8",
347 force_defaults=False)
346 force_defaults=False)
348
347
349 @HasRepoPermissionAllDecorator('repository.admin')
348 @HasRepoPermissionAllDecorator('repository.admin')
350 def edit_permissions(self, repo_name):
349 def edit_permissions(self, repo_name):
351 """GET /repo_name/settings: Form to edit an existing item"""
350 """GET /repo_name/settings: Form to edit an existing item"""
352 # url('edit_repo', repo_name=ID)
351 # url('edit_repo', repo_name=ID)
353 c.repo_info = self._load_repo(repo_name)
352 c.repo_info = self._load_repo(repo_name)
354 repo_model = RepoModel()
353 repo_model = RepoModel()
355 c.users_array = repo_model.get_users_js()
354 c.users_array = repo_model.get_users_js()
356 c.user_groups_array = repo_model.get_user_groups_js()
355 c.user_groups_array = repo_model.get_user_groups_js()
357 c.active = 'permissions'
356 c.active = 'permissions'
358 defaults = RepoModel()._get_defaults(repo_name)
357 defaults = RepoModel()._get_defaults(repo_name)
359
358
360 return htmlfill.render(
359 return htmlfill.render(
361 render('admin/repos/repo_edit.html'),
360 render('admin/repos/repo_edit.html'),
362 defaults=defaults,
361 defaults=defaults,
363 encoding="UTF-8",
362 encoding="UTF-8",
364 force_defaults=False)
363 force_defaults=False)
365
364
366 def edit_permissions_update(self, repo_name):
365 def edit_permissions_update(self, repo_name):
367 form = RepoPermsForm()().to_python(request.POST)
366 form = RepoPermsForm()().to_python(request.POST)
368 RepoModel()._update_permissions(repo_name, form['perms_new'],
367 RepoModel()._update_permissions(repo_name, form['perms_new'],
369 form['perms_updates'])
368 form['perms_updates'])
370 #TODO: implement this
369 #TODO: implement this
371 #action_logger(self.authuser, 'admin_changed_repo_permissions',
370 #action_logger(self.authuser, 'admin_changed_repo_permissions',
372 # repo_name, self.ip_addr, self.sa)
371 # repo_name, self.ip_addr, self.sa)
373 Session().commit()
372 Session().commit()
374 h.flash(_('Repository permissions updated'), category='success')
373 h.flash(_('Repository permissions updated'), category='success')
375 return redirect(url('edit_repo_perms', repo_name=repo_name))
374 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
376
375
377 def edit_permissions_revoke(self, repo_name):
376 def edit_permissions_revoke(self, repo_name):
378 try:
377 try:
379 obj_type = request.POST.get('obj_type')
378 obj_type = request.POST.get('obj_type')
380 obj_id = None
379 obj_id = None
381 if obj_type == 'user':
380 if obj_type == 'user':
382 obj_id = safe_int(request.POST.get('user_id'))
381 obj_id = safe_int(request.POST.get('user_id'))
383 elif obj_type == 'user_group':
382 elif obj_type == 'user_group':
384 obj_id = safe_int(request.POST.get('user_group_id'))
383 obj_id = safe_int(request.POST.get('user_group_id'))
385
384
386 if obj_type == 'user':
385 if obj_type == 'user':
387 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
386 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
388 elif obj_type == 'user_group':
387 elif obj_type == 'user_group':
389 RepoModel().revoke_user_group_permission(
388 RepoModel().revoke_user_group_permission(
390 repo=repo_name, group_name=obj_id
389 repo=repo_name, group_name=obj_id
391 )
390 )
392 #TODO: implement this
391 #TODO: implement this
393 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
392 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
394 # repo_name, self.ip_addr, self.sa)
393 # repo_name, self.ip_addr, self.sa)
395 Session().commit()
394 Session().commit()
396 except Exception:
395 except Exception:
397 log.error(traceback.format_exc())
396 log.error(traceback.format_exc())
398 h.flash(_('An error occurred during revoking of permission'),
397 h.flash(_('An error occurred during revoking of permission'),
399 category='error')
398 category='error')
400 raise HTTPInternalServerError()
399 raise HTTPInternalServerError()
401
400
402 @HasRepoPermissionAllDecorator('repository.admin')
401 @HasRepoPermissionAllDecorator('repository.admin')
403 def edit_fields(self, repo_name):
402 def edit_fields(self, repo_name):
404 """GET /repo_name/settings: Form to edit an existing item"""
403 """GET /repo_name/settings: Form to edit an existing item"""
405 # url('edit_repo', repo_name=ID)
404 # url('edit_repo', repo_name=ID)
406 c.repo_info = self._load_repo(repo_name)
405 c.repo_info = self._load_repo(repo_name)
407 c.repo_fields = RepositoryField.query()\
406 c.repo_fields = RepositoryField.query()\
408 .filter(RepositoryField.repository == c.repo_info).all()
407 .filter(RepositoryField.repository == c.repo_info).all()
409 c.active = 'fields'
408 c.active = 'fields'
410 if request.POST:
409 if request.POST:
411
410
412 return redirect(url('repo_edit_fields'))
411 raise HTTPFound(location=url('repo_edit_fields'))
413 return render('admin/repos/repo_edit.html')
412 return render('admin/repos/repo_edit.html')
414
413
415 @HasRepoPermissionAllDecorator('repository.admin')
414 @HasRepoPermissionAllDecorator('repository.admin')
416 def create_repo_field(self, repo_name):
415 def create_repo_field(self, repo_name):
417 try:
416 try:
418 form_result = RepoFieldForm()().to_python(dict(request.POST))
417 form_result = RepoFieldForm()().to_python(dict(request.POST))
419 new_field = RepositoryField()
418 new_field = RepositoryField()
420 new_field.repository = Repository.get_by_repo_name(repo_name)
419 new_field.repository = Repository.get_by_repo_name(repo_name)
421 new_field.field_key = form_result['new_field_key']
420 new_field.field_key = form_result['new_field_key']
422 new_field.field_type = form_result['new_field_type'] # python type
421 new_field.field_type = form_result['new_field_type'] # python type
423 new_field.field_value = form_result['new_field_value'] # set initial blank value
422 new_field.field_value = form_result['new_field_value'] # set initial blank value
424 new_field.field_desc = form_result['new_field_desc']
423 new_field.field_desc = form_result['new_field_desc']
425 new_field.field_label = form_result['new_field_label']
424 new_field.field_label = form_result['new_field_label']
426 Session().add(new_field)
425 Session().add(new_field)
427 Session().commit()
426 Session().commit()
428 except Exception as e:
427 except Exception as e:
429 log.error(traceback.format_exc())
428 log.error(traceback.format_exc())
430 msg = _('An error occurred during creation of field')
429 msg = _('An error occurred during creation of field')
431 if isinstance(e, formencode.Invalid):
430 if isinstance(e, formencode.Invalid):
432 msg += ". " + e.msg
431 msg += ". " + e.msg
433 h.flash(msg, category='error')
432 h.flash(msg, category='error')
434 return redirect(url('edit_repo_fields', repo_name=repo_name))
433 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
435
434
436 @HasRepoPermissionAllDecorator('repository.admin')
435 @HasRepoPermissionAllDecorator('repository.admin')
437 def delete_repo_field(self, repo_name, field_id):
436 def delete_repo_field(self, repo_name, field_id):
438 field = RepositoryField.get_or_404(field_id)
437 field = RepositoryField.get_or_404(field_id)
439 try:
438 try:
440 Session().delete(field)
439 Session().delete(field)
441 Session().commit()
440 Session().commit()
442 except Exception as e:
441 except Exception as e:
443 log.error(traceback.format_exc())
442 log.error(traceback.format_exc())
444 msg = _('An error occurred during removal of field')
443 msg = _('An error occurred during removal of field')
445 h.flash(msg, category='error')
444 h.flash(msg, category='error')
446 return redirect(url('edit_repo_fields', repo_name=repo_name))
445 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
447
446
448 @HasRepoPermissionAllDecorator('repository.admin')
447 @HasRepoPermissionAllDecorator('repository.admin')
449 def edit_advanced(self, repo_name):
448 def edit_advanced(self, repo_name):
450 """GET /repo_name/settings: Form to edit an existing item"""
449 """GET /repo_name/settings: Form to edit an existing item"""
451 # url('edit_repo', repo_name=ID)
450 # url('edit_repo', repo_name=ID)
452 c.repo_info = self._load_repo(repo_name)
451 c.repo_info = self._load_repo(repo_name)
453 c.default_user_id = User.get_default_user().user_id
452 c.default_user_id = User.get_default_user().user_id
454 c.in_public_journal = UserFollowing.query()\
453 c.in_public_journal = UserFollowing.query()\
455 .filter(UserFollowing.user_id == c.default_user_id)\
454 .filter(UserFollowing.user_id == c.default_user_id)\
456 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
455 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
457
456
458 _repos = Repository.query().order_by(Repository.repo_name).all()
457 _repos = Repository.query().order_by(Repository.repo_name).all()
459 read_access_repos = RepoList(_repos)
458 read_access_repos = RepoList(_repos)
460 c.repos_list = [(None, _('-- Not a fork --'))]
459 c.repos_list = [(None, _('-- Not a fork --'))]
461 c.repos_list += [(x.repo_id, x.repo_name)
460 c.repos_list += [(x.repo_id, x.repo_name)
462 for x in read_access_repos
461 for x in read_access_repos
463 if x.repo_id != c.repo_info.repo_id]
462 if x.repo_id != c.repo_info.repo_id]
464
463
465 defaults = {
464 defaults = {
466 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
465 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
467 }
466 }
468
467
469 c.active = 'advanced'
468 c.active = 'advanced'
470 if request.POST:
469 if request.POST:
471 return redirect(url('repo_edit_advanced'))
470 raise HTTPFound(location=url('repo_edit_advanced'))
472 return htmlfill.render(
471 return htmlfill.render(
473 render('admin/repos/repo_edit.html'),
472 render('admin/repos/repo_edit.html'),
474 defaults=defaults,
473 defaults=defaults,
475 encoding="UTF-8",
474 encoding="UTF-8",
476 force_defaults=False)
475 force_defaults=False)
477
476
478 @HasRepoPermissionAllDecorator('repository.admin')
477 @HasRepoPermissionAllDecorator('repository.admin')
479 def edit_advanced_journal(self, repo_name):
478 def edit_advanced_journal(self, repo_name):
480 """
479 """
481 Sets this repository to be visible in public journal,
480 Sets this repository to be visible in public journal,
482 in other words asking default user to follow this repo
481 in other words asking default user to follow this repo
483
482
484 :param repo_name:
483 :param repo_name:
485 """
484 """
486
485
487 try:
486 try:
488 repo_id = Repository.get_by_repo_name(repo_name).repo_id
487 repo_id = Repository.get_by_repo_name(repo_name).repo_id
489 user_id = User.get_default_user().user_id
488 user_id = User.get_default_user().user_id
490 self.scm_model.toggle_following_repo(repo_id, user_id)
489 self.scm_model.toggle_following_repo(repo_id, user_id)
491 h.flash(_('Updated repository visibility in public journal'),
490 h.flash(_('Updated repository visibility in public journal'),
492 category='success')
491 category='success')
493 Session().commit()
492 Session().commit()
494 except Exception:
493 except Exception:
495 h.flash(_('An error occurred during setting this'
494 h.flash(_('An error occurred during setting this'
496 ' repository in public journal'),
495 ' repository in public journal'),
497 category='error')
496 category='error')
498 return redirect(url('edit_repo_advanced', repo_name=repo_name))
497 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
499
498
500
499
501 @HasRepoPermissionAllDecorator('repository.admin')
500 @HasRepoPermissionAllDecorator('repository.admin')
502 def edit_advanced_fork(self, repo_name):
501 def edit_advanced_fork(self, repo_name):
503 """
502 """
504 Mark given repository as a fork of another
503 Mark given repository as a fork of another
505
504
506 :param repo_name:
505 :param repo_name:
507 """
506 """
508 try:
507 try:
509 fork_id = request.POST.get('id_fork_of')
508 fork_id = request.POST.get('id_fork_of')
510 repo = ScmModel().mark_as_fork(repo_name, fork_id,
509 repo = ScmModel().mark_as_fork(repo_name, fork_id,
511 self.authuser.username)
510 self.authuser.username)
512 fork = repo.fork.repo_name if repo.fork else _('Nothing')
511 fork = repo.fork.repo_name if repo.fork else _('Nothing')
513 Session().commit()
512 Session().commit()
514 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
513 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
515 category='success')
514 category='success')
516 except RepositoryError as e:
515 except RepositoryError as e:
517 log.error(traceback.format_exc())
516 log.error(traceback.format_exc())
518 h.flash(str(e), category='error')
517 h.flash(str(e), category='error')
519 except Exception as e:
518 except Exception as e:
520 log.error(traceback.format_exc())
519 log.error(traceback.format_exc())
521 h.flash(_('An error occurred during this operation'),
520 h.flash(_('An error occurred during this operation'),
522 category='error')
521 category='error')
523
522
524 return redirect(url('edit_repo_advanced', repo_name=repo_name))
523 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
525
524
526 @HasRepoPermissionAllDecorator('repository.admin')
525 @HasRepoPermissionAllDecorator('repository.admin')
527 def edit_advanced_locking(self, repo_name):
526 def edit_advanced_locking(self, repo_name):
528 """
527 """
529 Unlock repository when it is locked !
528 Unlock repository when it is locked !
530
529
531 :param repo_name:
530 :param repo_name:
532 """
531 """
533 try:
532 try:
534 repo = Repository.get_by_repo_name(repo_name)
533 repo = Repository.get_by_repo_name(repo_name)
535 if request.POST.get('set_lock'):
534 if request.POST.get('set_lock'):
536 Repository.lock(repo, c.authuser.user_id)
535 Repository.lock(repo, c.authuser.user_id)
537 h.flash(_('Repository has been locked'), category='success')
536 h.flash(_('Repository has been locked'), category='success')
538 elif request.POST.get('set_unlock'):
537 elif request.POST.get('set_unlock'):
539 Repository.unlock(repo)
538 Repository.unlock(repo)
540 h.flash(_('Repository has been unlocked'), category='success')
539 h.flash(_('Repository has been unlocked'), category='success')
541 except Exception as e:
540 except Exception as e:
542 log.error(traceback.format_exc())
541 log.error(traceback.format_exc())
543 h.flash(_('An error occurred during unlocking'),
542 h.flash(_('An error occurred during unlocking'),
544 category='error')
543 category='error')
545 return redirect(url('edit_repo_advanced', repo_name=repo_name))
544 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
546
545
547 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
546 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
548 def toggle_locking(self, repo_name):
547 def toggle_locking(self, repo_name):
549 """
548 """
550 Toggle locking of repository by simple GET call to url
549 Toggle locking of repository by simple GET call to url
551
550
552 :param repo_name:
551 :param repo_name:
553 """
552 """
554
553
555 try:
554 try:
556 repo = Repository.get_by_repo_name(repo_name)
555 repo = Repository.get_by_repo_name(repo_name)
557
556
558 if repo.enable_locking:
557 if repo.enable_locking:
559 if repo.locked[0]:
558 if repo.locked[0]:
560 Repository.unlock(repo)
559 Repository.unlock(repo)
561 h.flash(_('Repository has been unlocked'), category='success')
560 h.flash(_('Repository has been unlocked'), category='success')
562 else:
561 else:
563 Repository.lock(repo, c.authuser.user_id)
562 Repository.lock(repo, c.authuser.user_id)
564 h.flash(_('Repository has been locked'), category='success')
563 h.flash(_('Repository has been locked'), category='success')
565
564
566 except Exception as e:
565 except Exception as e:
567 log.error(traceback.format_exc())
566 log.error(traceback.format_exc())
568 h.flash(_('An error occurred during unlocking'),
567 h.flash(_('An error occurred during unlocking'),
569 category='error')
568 category='error')
570 return redirect(url('summary_home', repo_name=repo_name))
569 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
571
570
572 @HasRepoPermissionAllDecorator('repository.admin')
571 @HasRepoPermissionAllDecorator('repository.admin')
573 def edit_caches(self, repo_name):
572 def edit_caches(self, repo_name):
574 """GET /repo_name/settings: Form to edit an existing item"""
573 """GET /repo_name/settings: Form to edit an existing item"""
575 # url('edit_repo', repo_name=ID)
574 # url('edit_repo', repo_name=ID)
576 c.repo_info = self._load_repo(repo_name)
575 c.repo_info = self._load_repo(repo_name)
577 c.active = 'caches'
576 c.active = 'caches'
578 if request.POST:
577 if request.POST:
579 try:
578 try:
580 ScmModel().mark_for_invalidation(repo_name)
579 ScmModel().mark_for_invalidation(repo_name)
581 Session().commit()
580 Session().commit()
582 h.flash(_('Cache invalidation successful'),
581 h.flash(_('Cache invalidation successful'),
583 category='success')
582 category='success')
584 except Exception as e:
583 except Exception as e:
585 log.error(traceback.format_exc())
584 log.error(traceback.format_exc())
586 h.flash(_('An error occurred during cache invalidation'),
585 h.flash(_('An error occurred during cache invalidation'),
587 category='error')
586 category='error')
588
587
589 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
588 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
590 return render('admin/repos/repo_edit.html')
589 return render('admin/repos/repo_edit.html')
591
590
592 @HasRepoPermissionAllDecorator('repository.admin')
591 @HasRepoPermissionAllDecorator('repository.admin')
593 def edit_remote(self, repo_name):
592 def edit_remote(self, repo_name):
594 """GET /repo_name/settings: Form to edit an existing item"""
593 """GET /repo_name/settings: Form to edit an existing item"""
595 # url('edit_repo', repo_name=ID)
594 # url('edit_repo', repo_name=ID)
596 c.repo_info = self._load_repo(repo_name)
595 c.repo_info = self._load_repo(repo_name)
597 c.active = 'remote'
596 c.active = 'remote'
598 if request.POST:
597 if request.POST:
599 try:
598 try:
600 ScmModel().pull_changes(repo_name, self.authuser.username)
599 ScmModel().pull_changes(repo_name, self.authuser.username)
601 h.flash(_('Pulled from remote location'), category='success')
600 h.flash(_('Pulled from remote location'), category='success')
602 except Exception as e:
601 except Exception as e:
603 log.error(traceback.format_exc())
602 log.error(traceback.format_exc())
604 h.flash(_('An error occurred during pull from remote location'),
603 h.flash(_('An error occurred during pull from remote location'),
605 category='error')
604 category='error')
606 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
605 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
607 return render('admin/repos/repo_edit.html')
606 return render('admin/repos/repo_edit.html')
608
607
609 @HasRepoPermissionAllDecorator('repository.admin')
608 @HasRepoPermissionAllDecorator('repository.admin')
610 def edit_statistics(self, repo_name):
609 def edit_statistics(self, repo_name):
611 """GET /repo_name/settings: Form to edit an existing item"""
610 """GET /repo_name/settings: Form to edit an existing item"""
612 # url('edit_repo', repo_name=ID)
611 # url('edit_repo', repo_name=ID)
613 c.repo_info = self._load_repo(repo_name)
612 c.repo_info = self._load_repo(repo_name)
614 repo = c.repo_info.scm_instance
613 repo = c.repo_info.scm_instance
615
614
616 if c.repo_info.stats:
615 if c.repo_info.stats:
617 # this is on what revision we ended up so we add +1 for count
616 # this is on what revision we ended up so we add +1 for count
618 last_rev = c.repo_info.stats.stat_on_revision + 1
617 last_rev = c.repo_info.stats.stat_on_revision + 1
619 else:
618 else:
620 last_rev = 0
619 last_rev = 0
621 c.stats_revision = last_rev
620 c.stats_revision = last_rev
622
621
623 c.repo_last_rev = repo.count() if repo.revisions else 0
622 c.repo_last_rev = repo.count() if repo.revisions else 0
624
623
625 if last_rev == 0 or c.repo_last_rev == 0:
624 if last_rev == 0 or c.repo_last_rev == 0:
626 c.stats_percentage = 0
625 c.stats_percentage = 0
627 else:
626 else:
628 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
627 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
629
628
630 c.active = 'statistics'
629 c.active = 'statistics'
631 if request.POST:
630 if request.POST:
632 try:
631 try:
633 RepoModel().delete_stats(repo_name)
632 RepoModel().delete_stats(repo_name)
634 Session().commit()
633 Session().commit()
635 except Exception as e:
634 except Exception as e:
636 log.error(traceback.format_exc())
635 log.error(traceback.format_exc())
637 h.flash(_('An error occurred during deletion of repository stats'),
636 h.flash(_('An error occurred during deletion of repository stats'),
638 category='error')
637 category='error')
639 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
638 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
640
639
641 return render('admin/repos/repo_edit.html')
640 return render('admin/repos/repo_edit.html')
@@ -1,528 +1,528 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.settings
15 kallithea.controllers.admin.settings
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 settings controller for Kallithea admin
18 settings controller for Kallithea admin
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jul 14, 2010
22 :created_on: Jul 14, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from pylons import request, tmpl_context as c, url, config
33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.base import BaseController, render
39 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.celerylib import tasks, run_task
40 from kallithea.lib.celerylib import tasks, run_task
41 from kallithea.lib.exceptions import HgsubversionImportError
41 from kallithea.lib.exceptions import HgsubversionImportError
42 from kallithea.lib.utils import repo2db_mapper, set_app_settings
42 from kallithea.lib.utils import repo2db_mapper, set_app_settings
43 from kallithea.model.db import Ui, Repository, Setting
43 from kallithea.model.db import Ui, Repository, Setting
44 from kallithea.model.forms import ApplicationSettingsForm, \
44 from kallithea.model.forms import ApplicationSettingsForm, \
45 ApplicationUiSettingsForm, ApplicationVisualisationForm
45 ApplicationUiSettingsForm, ApplicationVisualisationForm
46 from kallithea.model.scm import ScmModel
46 from kallithea.model.scm import ScmModel
47 from kallithea.model.notification import EmailNotificationModel
47 from kallithea.model.notification import EmailNotificationModel
48 from kallithea.model.meta import Session
48 from kallithea.model.meta import Session
49 from kallithea.lib.utils2 import str2bool, safe_unicode
49 from kallithea.lib.utils2 import str2bool, safe_unicode
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class SettingsController(BaseController):
53 class SettingsController(BaseController):
54 """REST Controller styled on the Atom Publishing Protocol"""
54 """REST Controller styled on the Atom Publishing Protocol"""
55 # To properly map this controller, ensure your config/routing.py
55 # To properly map this controller, ensure your config/routing.py
56 # file has a resource setup:
56 # file has a resource setup:
57 # map.resource('setting', 'settings', controller='admin/settings',
57 # map.resource('setting', 'settings', controller='admin/settings',
58 # path_prefix='/admin', name_prefix='admin_')
58 # path_prefix='/admin', name_prefix='admin_')
59
59
60 @LoginRequired()
60 @LoginRequired()
61 def __before__(self):
61 def __before__(self):
62 super(SettingsController, self).__before__()
62 super(SettingsController, self).__before__()
63
63
64 def _get_hg_ui_settings(self):
64 def _get_hg_ui_settings(self):
65 ret = Ui.query().all()
65 ret = Ui.query().all()
66
66
67 if not ret:
67 if not ret:
68 raise Exception('Could not get application ui settings !')
68 raise Exception('Could not get application ui settings !')
69 settings = {}
69 settings = {}
70 for each in ret:
70 for each in ret:
71 k = each.ui_key
71 k = each.ui_key
72 v = each.ui_value
72 v = each.ui_value
73 if k == '/':
73 if k == '/':
74 k = 'root_path'
74 k = 'root_path'
75
75
76 if k == 'push_ssl':
76 if k == 'push_ssl':
77 v = str2bool(v)
77 v = str2bool(v)
78
78
79 if k.find('.') != -1:
79 if k.find('.') != -1:
80 k = k.replace('.', '_')
80 k = k.replace('.', '_')
81
81
82 if each.ui_section in ['hooks', 'extensions']:
82 if each.ui_section in ['hooks', 'extensions']:
83 v = each.ui_active
83 v = each.ui_active
84
84
85 settings[each.ui_section + '_' + k] = v
85 settings[each.ui_section + '_' + k] = v
86 return settings
86 return settings
87
87
88 @HasPermissionAllDecorator('hg.admin')
88 @HasPermissionAllDecorator('hg.admin')
89 def settings_vcs(self):
89 def settings_vcs(self):
90 """GET /admin/settings: All items in the collection"""
90 """GET /admin/settings: All items in the collection"""
91 # url('admin_settings')
91 # url('admin_settings')
92 c.active = 'vcs'
92 c.active = 'vcs'
93 if request.POST:
93 if request.POST:
94 application_form = ApplicationUiSettingsForm()()
94 application_form = ApplicationUiSettingsForm()()
95 try:
95 try:
96 form_result = application_form.to_python(dict(request.POST))
96 form_result = application_form.to_python(dict(request.POST))
97 except formencode.Invalid as errors:
97 except formencode.Invalid as errors:
98 return htmlfill.render(
98 return htmlfill.render(
99 render('admin/settings/settings.html'),
99 render('admin/settings/settings.html'),
100 defaults=errors.value,
100 defaults=errors.value,
101 errors=errors.error_dict or {},
101 errors=errors.error_dict or {},
102 prefix_error=False,
102 prefix_error=False,
103 encoding="UTF-8",
103 encoding="UTF-8",
104 force_defaults=False)
104 force_defaults=False)
105
105
106 try:
106 try:
107 sett = Ui.get_by_key('push_ssl')
107 sett = Ui.get_by_key('push_ssl')
108 sett.ui_value = form_result['web_push_ssl']
108 sett.ui_value = form_result['web_push_ssl']
109 Session().add(sett)
109 Session().add(sett)
110 if c.visual.allow_repo_location_change:
110 if c.visual.allow_repo_location_change:
111 sett = Ui.get_by_key('/')
111 sett = Ui.get_by_key('/')
112 sett.ui_value = form_result['paths_root_path']
112 sett.ui_value = form_result['paths_root_path']
113 Session().add(sett)
113 Session().add(sett)
114
114
115 #HOOKS
115 #HOOKS
116 sett = Ui.get_by_key(Ui.HOOK_UPDATE)
116 sett = Ui.get_by_key(Ui.HOOK_UPDATE)
117 sett.ui_active = form_result['hooks_changegroup_update']
117 sett.ui_active = form_result['hooks_changegroup_update']
118 Session().add(sett)
118 Session().add(sett)
119
119
120 sett = Ui.get_by_key(Ui.HOOK_REPO_SIZE)
120 sett = Ui.get_by_key(Ui.HOOK_REPO_SIZE)
121 sett.ui_active = form_result['hooks_changegroup_repo_size']
121 sett.ui_active = form_result['hooks_changegroup_repo_size']
122 Session().add(sett)
122 Session().add(sett)
123
123
124 sett = Ui.get_by_key(Ui.HOOK_PUSH)
124 sett = Ui.get_by_key(Ui.HOOK_PUSH)
125 sett.ui_active = form_result['hooks_changegroup_push_logger']
125 sett.ui_active = form_result['hooks_changegroup_push_logger']
126 Session().add(sett)
126 Session().add(sett)
127
127
128 sett = Ui.get_by_key(Ui.HOOK_PULL)
128 sett = Ui.get_by_key(Ui.HOOK_PULL)
129 sett.ui_active = form_result['hooks_outgoing_pull_logger']
129 sett.ui_active = form_result['hooks_outgoing_pull_logger']
130
130
131 Session().add(sett)
131 Session().add(sett)
132
132
133 ## EXTENSIONS
133 ## EXTENSIONS
134 sett = Ui.get_by_key('largefiles')
134 sett = Ui.get_by_key('largefiles')
135 if not sett:
135 if not sett:
136 #make one if it's not there !
136 #make one if it's not there !
137 sett = Ui()
137 sett = Ui()
138 sett.ui_key = 'largefiles'
138 sett.ui_key = 'largefiles'
139 sett.ui_section = 'extensions'
139 sett.ui_section = 'extensions'
140 sett.ui_active = form_result['extensions_largefiles']
140 sett.ui_active = form_result['extensions_largefiles']
141 Session().add(sett)
141 Session().add(sett)
142
142
143 sett = Ui.get_by_key('hgsubversion')
143 sett = Ui.get_by_key('hgsubversion')
144 if not sett:
144 if not sett:
145 #make one if it's not there !
145 #make one if it's not there !
146 sett = Ui()
146 sett = Ui()
147 sett.ui_key = 'hgsubversion'
147 sett.ui_key = 'hgsubversion'
148 sett.ui_section = 'extensions'
148 sett.ui_section = 'extensions'
149
149
150 sett.ui_active = form_result['extensions_hgsubversion']
150 sett.ui_active = form_result['extensions_hgsubversion']
151 if sett.ui_active:
151 if sett.ui_active:
152 try:
152 try:
153 import hgsubversion # pragma: no cover
153 import hgsubversion # pragma: no cover
154 except ImportError:
154 except ImportError:
155 raise HgsubversionImportError
155 raise HgsubversionImportError
156 Session().add(sett)
156 Session().add(sett)
157
157
158 # sett = Ui.get_by_key('hggit')
158 # sett = Ui.get_by_key('hggit')
159 # if not sett:
159 # if not sett:
160 # #make one if it's not there !
160 # #make one if it's not there !
161 # sett = Ui()
161 # sett = Ui()
162 # sett.ui_key = 'hggit'
162 # sett.ui_key = 'hggit'
163 # sett.ui_section = 'extensions'
163 # sett.ui_section = 'extensions'
164 #
164 #
165 # sett.ui_active = form_result['extensions_hggit']
165 # sett.ui_active = form_result['extensions_hggit']
166 # Session().add(sett)
166 # Session().add(sett)
167
167
168 Session().commit()
168 Session().commit()
169
169
170 h.flash(_('Updated VCS settings'), category='success')
170 h.flash(_('Updated VCS settings'), category='success')
171
171
172 except HgsubversionImportError:
172 except HgsubversionImportError:
173 log.error(traceback.format_exc())
173 log.error(traceback.format_exc())
174 h.flash(_('Unable to activate hgsubversion support. '
174 h.flash(_('Unable to activate hgsubversion support. '
175 'The "hgsubversion" library is missing'),
175 'The "hgsubversion" library is missing'),
176 category='error')
176 category='error')
177
177
178 except Exception:
178 except Exception:
179 log.error(traceback.format_exc())
179 log.error(traceback.format_exc())
180 h.flash(_('Error occurred while updating '
180 h.flash(_('Error occurred while updating '
181 'application settings'), category='error')
181 'application settings'), category='error')
182
182
183 defaults = Setting.get_app_settings()
183 defaults = Setting.get_app_settings()
184 defaults.update(self._get_hg_ui_settings())
184 defaults.update(self._get_hg_ui_settings())
185
185
186 return htmlfill.render(
186 return htmlfill.render(
187 render('admin/settings/settings.html'),
187 render('admin/settings/settings.html'),
188 defaults=defaults,
188 defaults=defaults,
189 encoding="UTF-8",
189 encoding="UTF-8",
190 force_defaults=False)
190 force_defaults=False)
191
191
192 @HasPermissionAllDecorator('hg.admin')
192 @HasPermissionAllDecorator('hg.admin')
193 def settings_mapping(self):
193 def settings_mapping(self):
194 """GET /admin/settings/mapping: All items in the collection"""
194 """GET /admin/settings/mapping: All items in the collection"""
195 # url('admin_settings_mapping')
195 # url('admin_settings_mapping')
196 c.active = 'mapping'
196 c.active = 'mapping'
197 if request.POST:
197 if request.POST:
198 rm_obsolete = request.POST.get('destroy', False)
198 rm_obsolete = request.POST.get('destroy', False)
199 install_git_hooks = request.POST.get('hooks', False)
199 install_git_hooks = request.POST.get('hooks', False)
200 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
200 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
201 invalidate_cache = request.POST.get('invalidate', False)
201 invalidate_cache = request.POST.get('invalidate', False)
202 log.debug('rescanning repo location with destroy obsolete=%s, '
202 log.debug('rescanning repo location with destroy obsolete=%s, '
203 'install git hooks=%s and '
203 'install git hooks=%s and '
204 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
204 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
205
205
206 if invalidate_cache:
206 if invalidate_cache:
207 log.debug('invalidating all repositories cache')
207 log.debug('invalidating all repositories cache')
208 for repo in Repository.get_all():
208 for repo in Repository.get_all():
209 ScmModel().mark_for_invalidation(repo.repo_name)
209 ScmModel().mark_for_invalidation(repo.repo_name)
210
210
211 filesystem_repos = ScmModel().repo_scan()
211 filesystem_repos = ScmModel().repo_scan()
212 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
212 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
213 install_git_hooks=install_git_hooks,
213 install_git_hooks=install_git_hooks,
214 user=c.authuser.username,
214 user=c.authuser.username,
215 overwrite_git_hooks=overwrite_git_hooks)
215 overwrite_git_hooks=overwrite_git_hooks)
216 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
216 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
217 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
217 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
218 for repo_name in added) or '-',
218 for repo_name in added) or '-',
219 ', '.join(h.escape(safe_unicode(repo_name)) for repo_name in removed) or '-')),
219 ', '.join(h.escape(safe_unicode(repo_name)) for repo_name in removed) or '-')),
220 category='success')
220 category='success')
221 return redirect(url('admin_settings_mapping'))
221 raise HTTPFound(location=url('admin_settings_mapping'))
222
222
223 defaults = Setting.get_app_settings()
223 defaults = Setting.get_app_settings()
224 defaults.update(self._get_hg_ui_settings())
224 defaults.update(self._get_hg_ui_settings())
225
225
226 return htmlfill.render(
226 return htmlfill.render(
227 render('admin/settings/settings.html'),
227 render('admin/settings/settings.html'),
228 defaults=defaults,
228 defaults=defaults,
229 encoding="UTF-8",
229 encoding="UTF-8",
230 force_defaults=False)
230 force_defaults=False)
231
231
232 @HasPermissionAllDecorator('hg.admin')
232 @HasPermissionAllDecorator('hg.admin')
233 def settings_global(self):
233 def settings_global(self):
234 """GET /admin/settings/global: All items in the collection"""
234 """GET /admin/settings/global: All items in the collection"""
235 # url('admin_settings_global')
235 # url('admin_settings_global')
236 c.active = 'global'
236 c.active = 'global'
237 if request.POST:
237 if request.POST:
238 application_form = ApplicationSettingsForm()()
238 application_form = ApplicationSettingsForm()()
239 try:
239 try:
240 form_result = application_form.to_python(dict(request.POST))
240 form_result = application_form.to_python(dict(request.POST))
241 except formencode.Invalid as errors:
241 except formencode.Invalid as errors:
242 return htmlfill.render(
242 return htmlfill.render(
243 render('admin/settings/settings.html'),
243 render('admin/settings/settings.html'),
244 defaults=errors.value,
244 defaults=errors.value,
245 errors=errors.error_dict or {},
245 errors=errors.error_dict or {},
246 prefix_error=False,
246 prefix_error=False,
247 encoding="UTF-8",
247 encoding="UTF-8",
248 force_defaults=False)
248 force_defaults=False)
249
249
250 try:
250 try:
251 sett1 = Setting.create_or_update('title',
251 sett1 = Setting.create_or_update('title',
252 form_result['title'])
252 form_result['title'])
253 Session().add(sett1)
253 Session().add(sett1)
254
254
255 sett2 = Setting.create_or_update('realm',
255 sett2 = Setting.create_or_update('realm',
256 form_result['realm'])
256 form_result['realm'])
257 Session().add(sett2)
257 Session().add(sett2)
258
258
259 sett3 = Setting.create_or_update('ga_code',
259 sett3 = Setting.create_or_update('ga_code',
260 form_result['ga_code'])
260 form_result['ga_code'])
261 Session().add(sett3)
261 Session().add(sett3)
262
262
263 sett4 = Setting.create_or_update('captcha_public_key',
263 sett4 = Setting.create_or_update('captcha_public_key',
264 form_result['captcha_public_key'])
264 form_result['captcha_public_key'])
265 Session().add(sett4)
265 Session().add(sett4)
266
266
267 sett5 = Setting.create_or_update('captcha_private_key',
267 sett5 = Setting.create_or_update('captcha_private_key',
268 form_result['captcha_private_key'])
268 form_result['captcha_private_key'])
269 Session().add(sett5)
269 Session().add(sett5)
270
270
271 Session().commit()
271 Session().commit()
272 set_app_settings(config)
272 set_app_settings(config)
273 h.flash(_('Updated application settings'), category='success')
273 h.flash(_('Updated application settings'), category='success')
274
274
275 except Exception:
275 except Exception:
276 log.error(traceback.format_exc())
276 log.error(traceback.format_exc())
277 h.flash(_('Error occurred while updating '
277 h.flash(_('Error occurred while updating '
278 'application settings'),
278 'application settings'),
279 category='error')
279 category='error')
280
280
281 return redirect(url('admin_settings_global'))
281 raise HTTPFound(location=url('admin_settings_global'))
282
282
283 defaults = Setting.get_app_settings()
283 defaults = Setting.get_app_settings()
284 defaults.update(self._get_hg_ui_settings())
284 defaults.update(self._get_hg_ui_settings())
285
285
286 return htmlfill.render(
286 return htmlfill.render(
287 render('admin/settings/settings.html'),
287 render('admin/settings/settings.html'),
288 defaults=defaults,
288 defaults=defaults,
289 encoding="UTF-8",
289 encoding="UTF-8",
290 force_defaults=False)
290 force_defaults=False)
291
291
292 @HasPermissionAllDecorator('hg.admin')
292 @HasPermissionAllDecorator('hg.admin')
293 def settings_visual(self):
293 def settings_visual(self):
294 """GET /admin/settings/visual: All items in the collection"""
294 """GET /admin/settings/visual: All items in the collection"""
295 # url('admin_settings_visual')
295 # url('admin_settings_visual')
296 c.active = 'visual'
296 c.active = 'visual'
297 if request.POST:
297 if request.POST:
298 application_form = ApplicationVisualisationForm()()
298 application_form = ApplicationVisualisationForm()()
299 try:
299 try:
300 form_result = application_form.to_python(dict(request.POST))
300 form_result = application_form.to_python(dict(request.POST))
301 except formencode.Invalid as errors:
301 except formencode.Invalid as errors:
302 return htmlfill.render(
302 return htmlfill.render(
303 render('admin/settings/settings.html'),
303 render('admin/settings/settings.html'),
304 defaults=errors.value,
304 defaults=errors.value,
305 errors=errors.error_dict or {},
305 errors=errors.error_dict or {},
306 prefix_error=False,
306 prefix_error=False,
307 encoding="UTF-8",
307 encoding="UTF-8",
308 force_defaults=False)
308 force_defaults=False)
309
309
310 try:
310 try:
311 settings = [
311 settings = [
312 ('show_public_icon', 'show_public_icon', 'bool'),
312 ('show_public_icon', 'show_public_icon', 'bool'),
313 ('show_private_icon', 'show_private_icon', 'bool'),
313 ('show_private_icon', 'show_private_icon', 'bool'),
314 ('stylify_metatags', 'stylify_metatags', 'bool'),
314 ('stylify_metatags', 'stylify_metatags', 'bool'),
315 ('repository_fields', 'repository_fields', 'bool'),
315 ('repository_fields', 'repository_fields', 'bool'),
316 ('dashboard_items', 'dashboard_items', 'int'),
316 ('dashboard_items', 'dashboard_items', 'int'),
317 ('admin_grid_items', 'admin_grid_items', 'int'),
317 ('admin_grid_items', 'admin_grid_items', 'int'),
318 ('show_version', 'show_version', 'bool'),
318 ('show_version', 'show_version', 'bool'),
319 ('use_gravatar', 'use_gravatar', 'bool'),
319 ('use_gravatar', 'use_gravatar', 'bool'),
320 ('gravatar_url', 'gravatar_url', 'unicode'),
320 ('gravatar_url', 'gravatar_url', 'unicode'),
321 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
321 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
322 ]
322 ]
323 for setting, form_key, type_ in settings:
323 for setting, form_key, type_ in settings:
324 sett = Setting.create_or_update(setting,
324 sett = Setting.create_or_update(setting,
325 form_result[form_key], type_)
325 form_result[form_key], type_)
326 Session().add(sett)
326 Session().add(sett)
327
327
328 Session().commit()
328 Session().commit()
329 set_app_settings(config)
329 set_app_settings(config)
330 h.flash(_('Updated visualisation settings'),
330 h.flash(_('Updated visualisation settings'),
331 category='success')
331 category='success')
332
332
333 except Exception:
333 except Exception:
334 log.error(traceback.format_exc())
334 log.error(traceback.format_exc())
335 h.flash(_('Error occurred during updating '
335 h.flash(_('Error occurred during updating '
336 'visualisation settings'),
336 'visualisation settings'),
337 category='error')
337 category='error')
338
338
339 return redirect(url('admin_settings_visual'))
339 raise HTTPFound(location=url('admin_settings_visual'))
340
340
341 defaults = Setting.get_app_settings()
341 defaults = Setting.get_app_settings()
342 defaults.update(self._get_hg_ui_settings())
342 defaults.update(self._get_hg_ui_settings())
343
343
344 return htmlfill.render(
344 return htmlfill.render(
345 render('admin/settings/settings.html'),
345 render('admin/settings/settings.html'),
346 defaults=defaults,
346 defaults=defaults,
347 encoding="UTF-8",
347 encoding="UTF-8",
348 force_defaults=False)
348 force_defaults=False)
349
349
350 @HasPermissionAllDecorator('hg.admin')
350 @HasPermissionAllDecorator('hg.admin')
351 def settings_email(self):
351 def settings_email(self):
352 """GET /admin/settings/email: All items in the collection"""
352 """GET /admin/settings/email: All items in the collection"""
353 # url('admin_settings_email')
353 # url('admin_settings_email')
354 c.active = 'email'
354 c.active = 'email'
355 if request.POST:
355 if request.POST:
356 test_email = request.POST.get('test_email')
356 test_email = request.POST.get('test_email')
357 test_email_subj = 'Kallithea test email'
357 test_email_subj = 'Kallithea test email'
358 test_body = ('Kallithea Email test, '
358 test_body = ('Kallithea Email test, '
359 'Kallithea version: %s' % c.kallithea_version)
359 'Kallithea version: %s' % c.kallithea_version)
360 if not test_email:
360 if not test_email:
361 h.flash(_('Please enter email address'), category='error')
361 h.flash(_('Please enter email address'), category='error')
362 return redirect(url('admin_settings_email'))
362 raise HTTPFound(location=url('admin_settings_email'))
363
363
364 test_email_txt_body = EmailNotificationModel()\
364 test_email_txt_body = EmailNotificationModel()\
365 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
365 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
366 'txt', body=test_body)
366 'txt', body=test_body)
367 test_email_html_body = EmailNotificationModel()\
367 test_email_html_body = EmailNotificationModel()\
368 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
368 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
369 'html', body=test_body)
369 'html', body=test_body)
370
370
371 recipients = [test_email] if test_email else None
371 recipients = [test_email] if test_email else None
372
372
373 run_task(tasks.send_email, recipients, test_email_subj,
373 run_task(tasks.send_email, recipients, test_email_subj,
374 test_email_txt_body, test_email_html_body)
374 test_email_txt_body, test_email_html_body)
375
375
376 h.flash(_('Send email task created'), category='success')
376 h.flash(_('Send email task created'), category='success')
377 return redirect(url('admin_settings_email'))
377 raise HTTPFound(location=url('admin_settings_email'))
378
378
379 defaults = Setting.get_app_settings()
379 defaults = Setting.get_app_settings()
380 defaults.update(self._get_hg_ui_settings())
380 defaults.update(self._get_hg_ui_settings())
381
381
382 import kallithea
382 import kallithea
383 c.ini = kallithea.CONFIG
383 c.ini = kallithea.CONFIG
384
384
385 return htmlfill.render(
385 return htmlfill.render(
386 render('admin/settings/settings.html'),
386 render('admin/settings/settings.html'),
387 defaults=defaults,
387 defaults=defaults,
388 encoding="UTF-8",
388 encoding="UTF-8",
389 force_defaults=False)
389 force_defaults=False)
390
390
391 @HasPermissionAllDecorator('hg.admin')
391 @HasPermissionAllDecorator('hg.admin')
392 def settings_hooks(self):
392 def settings_hooks(self):
393 """GET /admin/settings/hooks: All items in the collection"""
393 """GET /admin/settings/hooks: All items in the collection"""
394 # url('admin_settings_hooks')
394 # url('admin_settings_hooks')
395 c.active = 'hooks'
395 c.active = 'hooks'
396 if request.POST:
396 if request.POST:
397 if c.visual.allow_custom_hooks_settings:
397 if c.visual.allow_custom_hooks_settings:
398 ui_key = request.POST.get('new_hook_ui_key')
398 ui_key = request.POST.get('new_hook_ui_key')
399 ui_value = request.POST.get('new_hook_ui_value')
399 ui_value = request.POST.get('new_hook_ui_value')
400
400
401 hook_id = request.POST.get('hook_id')
401 hook_id = request.POST.get('hook_id')
402
402
403 try:
403 try:
404 ui_key = ui_key and ui_key.strip()
404 ui_key = ui_key and ui_key.strip()
405 if ui_value and ui_key:
405 if ui_value and ui_key:
406 Ui.create_or_update_hook(ui_key, ui_value)
406 Ui.create_or_update_hook(ui_key, ui_value)
407 h.flash(_('Added new hook'), category='success')
407 h.flash(_('Added new hook'), category='success')
408 elif hook_id:
408 elif hook_id:
409 Ui.delete(hook_id)
409 Ui.delete(hook_id)
410 Session().commit()
410 Session().commit()
411
411
412 # check for edits
412 # check for edits
413 update = False
413 update = False
414 _d = request.POST.dict_of_lists()
414 _d = request.POST.dict_of_lists()
415 for k, v in zip(_d.get('hook_ui_key', []),
415 for k, v in zip(_d.get('hook_ui_key', []),
416 _d.get('hook_ui_value_new', [])):
416 _d.get('hook_ui_value_new', [])):
417 Ui.create_or_update_hook(k, v)
417 Ui.create_or_update_hook(k, v)
418 update = True
418 update = True
419
419
420 if update:
420 if update:
421 h.flash(_('Updated hooks'), category='success')
421 h.flash(_('Updated hooks'), category='success')
422 Session().commit()
422 Session().commit()
423 except Exception:
423 except Exception:
424 log.error(traceback.format_exc())
424 log.error(traceback.format_exc())
425 h.flash(_('Error occurred during hook creation'),
425 h.flash(_('Error occurred during hook creation'),
426 category='error')
426 category='error')
427
427
428 return redirect(url('admin_settings_hooks'))
428 raise HTTPFound(location=url('admin_settings_hooks'))
429
429
430 defaults = Setting.get_app_settings()
430 defaults = Setting.get_app_settings()
431 defaults.update(self._get_hg_ui_settings())
431 defaults.update(self._get_hg_ui_settings())
432
432
433 c.hooks = Ui.get_builtin_hooks()
433 c.hooks = Ui.get_builtin_hooks()
434 c.custom_hooks = Ui.get_custom_hooks()
434 c.custom_hooks = Ui.get_custom_hooks()
435
435
436 return htmlfill.render(
436 return htmlfill.render(
437 render('admin/settings/settings.html'),
437 render('admin/settings/settings.html'),
438 defaults=defaults,
438 defaults=defaults,
439 encoding="UTF-8",
439 encoding="UTF-8",
440 force_defaults=False)
440 force_defaults=False)
441
441
442 @HasPermissionAllDecorator('hg.admin')
442 @HasPermissionAllDecorator('hg.admin')
443 def settings_search(self):
443 def settings_search(self):
444 """GET /admin/settings/search: All items in the collection"""
444 """GET /admin/settings/search: All items in the collection"""
445 # url('admin_settings_search')
445 # url('admin_settings_search')
446 c.active = 'search'
446 c.active = 'search'
447 if request.POST:
447 if request.POST:
448 repo_location = self._get_hg_ui_settings()['paths_root_path']
448 repo_location = self._get_hg_ui_settings()['paths_root_path']
449 full_index = request.POST.get('full_index', False)
449 full_index = request.POST.get('full_index', False)
450 run_task(tasks.whoosh_index, repo_location, full_index)
450 run_task(tasks.whoosh_index, repo_location, full_index)
451 h.flash(_('Whoosh reindex task scheduled'), category='success')
451 h.flash(_('Whoosh reindex task scheduled'), category='success')
452 return redirect(url('admin_settings_search'))
452 raise HTTPFound(location=url('admin_settings_search'))
453
453
454 defaults = Setting.get_app_settings()
454 defaults = Setting.get_app_settings()
455 defaults.update(self._get_hg_ui_settings())
455 defaults.update(self._get_hg_ui_settings())
456
456
457 return htmlfill.render(
457 return htmlfill.render(
458 render('admin/settings/settings.html'),
458 render('admin/settings/settings.html'),
459 defaults=defaults,
459 defaults=defaults,
460 encoding="UTF-8",
460 encoding="UTF-8",
461 force_defaults=False)
461 force_defaults=False)
462
462
463 @HasPermissionAllDecorator('hg.admin')
463 @HasPermissionAllDecorator('hg.admin')
464 def settings_system(self):
464 def settings_system(self):
465 """GET /admin/settings/system: All items in the collection"""
465 """GET /admin/settings/system: All items in the collection"""
466 # url('admin_settings_system')
466 # url('admin_settings_system')
467 c.active = 'system'
467 c.active = 'system'
468
468
469 defaults = Setting.get_app_settings()
469 defaults = Setting.get_app_settings()
470 defaults.update(self._get_hg_ui_settings())
470 defaults.update(self._get_hg_ui_settings())
471
471
472 import kallithea
472 import kallithea
473 c.ini = kallithea.CONFIG
473 c.ini = kallithea.CONFIG
474 c.update_url = defaults.get('update_url')
474 c.update_url = defaults.get('update_url')
475 server_info = Setting.get_server_info()
475 server_info = Setting.get_server_info()
476 for key, val in server_info.iteritems():
476 for key, val in server_info.iteritems():
477 setattr(c, key, val)
477 setattr(c, key, val)
478
478
479 return htmlfill.render(
479 return htmlfill.render(
480 render('admin/settings/settings.html'),
480 render('admin/settings/settings.html'),
481 defaults=defaults,
481 defaults=defaults,
482 encoding="UTF-8",
482 encoding="UTF-8",
483 force_defaults=False)
483 force_defaults=False)
484
484
485 @HasPermissionAllDecorator('hg.admin')
485 @HasPermissionAllDecorator('hg.admin')
486 def settings_system_update(self):
486 def settings_system_update(self):
487 """GET /admin/settings/system/updates: All items in the collection"""
487 """GET /admin/settings/system/updates: All items in the collection"""
488 # url('admin_settings_system_update')
488 # url('admin_settings_system_update')
489 import json
489 import json
490 import urllib2
490 import urllib2
491 from kallithea.lib.verlib import NormalizedVersion
491 from kallithea.lib.verlib import NormalizedVersion
492 from kallithea import __version__
492 from kallithea import __version__
493
493
494 defaults = Setting.get_app_settings()
494 defaults = Setting.get_app_settings()
495 defaults.update(self._get_hg_ui_settings())
495 defaults.update(self._get_hg_ui_settings())
496 _update_url = defaults.get('update_url', '')
496 _update_url = defaults.get('update_url', '')
497 _update_url = "" # FIXME: disabled
497 _update_url = "" # FIXME: disabled
498
498
499 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
499 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
500 try:
500 try:
501 import kallithea
501 import kallithea
502 ver = kallithea.__version__
502 ver = kallithea.__version__
503 log.debug('Checking for upgrade on `%s` server', _update_url)
503 log.debug('Checking for upgrade on `%s` server', _update_url)
504 opener = urllib2.build_opener()
504 opener = urllib2.build_opener()
505 opener.addheaders = [('User-agent', 'Kallithea-SCM/%s' % ver)]
505 opener.addheaders = [('User-agent', 'Kallithea-SCM/%s' % ver)]
506 response = opener.open(_update_url)
506 response = opener.open(_update_url)
507 response_data = response.read()
507 response_data = response.read()
508 data = json.loads(response_data)
508 data = json.loads(response_data)
509 except urllib2.URLError as e:
509 except urllib2.URLError as e:
510 log.error(traceback.format_exc())
510 log.error(traceback.format_exc())
511 return _err('Failed to contact upgrade server: %r' % e)
511 return _err('Failed to contact upgrade server: %r' % e)
512 except ValueError as e:
512 except ValueError as e:
513 log.error(traceback.format_exc())
513 log.error(traceback.format_exc())
514 return _err('Bad data sent from update server')
514 return _err('Bad data sent from update server')
515
515
516 latest = data['versions'][0]
516 latest = data['versions'][0]
517
517
518 c.update_url = _update_url
518 c.update_url = _update_url
519 c.latest_data = latest
519 c.latest_data = latest
520 c.latest_ver = latest['version']
520 c.latest_ver = latest['version']
521 c.cur_ver = __version__
521 c.cur_ver = __version__
522 c.should_upgrade = False
522 c.should_upgrade = False
523
523
524 if NormalizedVersion(c.latest_ver) > NormalizedVersion(c.cur_ver):
524 if NormalizedVersion(c.latest_ver) > NormalizedVersion(c.cur_ver):
525 c.should_upgrade = True
525 c.should_upgrade = True
526 c.important_notices = latest['general']
526 c.important_notices = latest['general']
527
527
528 return render('admin/settings/settings_system_update.html'),
528 return render('admin/settings/settings_system_update.html'),
@@ -1,466 +1,466 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.user_groups
15 kallithea.controllers.admin.user_groups
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 User Groups crud controller for pylons
18 User Groups crud controller for pylons
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jan 25, 2011
22 :created_on: Jan 25, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from pylons import request, tmpl_context as c, url, config
33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 from sqlalchemy.orm import joinedload
37 from sqlalchemy.orm import joinedload
38 from sqlalchemy.sql.expression import func
38 from sqlalchemy.sql.expression import func
39 from webob.exc import HTTPInternalServerError
39 from webob.exc import HTTPInternalServerError
40
40
41 import kallithea
41 import kallithea
42 from kallithea.lib import helpers as h
42 from kallithea.lib import helpers as h
43 from kallithea.lib.exceptions import UserGroupsAssignedException, \
43 from kallithea.lib.exceptions import UserGroupsAssignedException, \
44 RepoGroupAssignmentError
44 RepoGroupAssignmentError
45 from kallithea.lib.utils2 import safe_unicode, safe_int
45 from kallithea.lib.utils2 import safe_unicode, safe_int
46 from kallithea.lib.auth import LoginRequired, \
46 from kallithea.lib.auth import LoginRequired, \
47 HasUserGroupPermissionAnyDecorator, HasPermissionAnyDecorator
47 HasUserGroupPermissionAnyDecorator, HasPermissionAnyDecorator
48 from kallithea.lib.base import BaseController, render
48 from kallithea.lib.base import BaseController, render
49 from kallithea.model.scm import UserGroupList
49 from kallithea.model.scm import UserGroupList
50 from kallithea.model.user_group import UserGroupModel
50 from kallithea.model.user_group import UserGroupModel
51 from kallithea.model.repo import RepoModel
51 from kallithea.model.repo import RepoModel
52 from kallithea.model.db import User, UserGroup, UserGroupToPerm, \
52 from kallithea.model.db import User, UserGroup, UserGroupToPerm, \
53 UserGroupRepoToPerm, UserGroupRepoGroupToPerm
53 UserGroupRepoToPerm, UserGroupRepoGroupToPerm
54 from kallithea.model.forms import UserGroupForm, UserGroupPermsForm, \
54 from kallithea.model.forms import UserGroupForm, UserGroupPermsForm, \
55 CustomDefaultPermissionsForm
55 CustomDefaultPermissionsForm
56 from kallithea.model.meta import Session
56 from kallithea.model.meta import Session
57 from kallithea.lib.utils import action_logger
57 from kallithea.lib.utils import action_logger
58 from kallithea.lib.compat import json
58 from kallithea.lib.compat import json
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class UserGroupsController(BaseController):
63 class UserGroupsController(BaseController):
64 """REST Controller styled on the Atom Publishing Protocol"""
64 """REST Controller styled on the Atom Publishing Protocol"""
65
65
66 @LoginRequired()
66 @LoginRequired()
67 def __before__(self):
67 def __before__(self):
68 super(UserGroupsController, self).__before__()
68 super(UserGroupsController, self).__before__()
69 c.available_permissions = config['available_permissions']
69 c.available_permissions = config['available_permissions']
70
70
71 def __load_data(self, user_group_id):
71 def __load_data(self, user_group_id):
72 c.group_members_obj = sorted((x.user for x in c.user_group.members),
72 c.group_members_obj = sorted((x.user for x in c.user_group.members),
73 key=lambda u: u.username.lower())
73 key=lambda u: u.username.lower())
74
74
75 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
75 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
76 c.available_members = sorted(((x.user_id, x.username) for x in
76 c.available_members = sorted(((x.user_id, x.username) for x in
77 User.query().all()),
77 User.query().all()),
78 key=lambda u: u[1].lower())
78 key=lambda u: u[1].lower())
79
79
80 def __load_defaults(self, user_group_id):
80 def __load_defaults(self, user_group_id):
81 """
81 """
82 Load defaults settings for edit, and update
82 Load defaults settings for edit, and update
83
83
84 :param user_group_id:
84 :param user_group_id:
85 """
85 """
86 user_group = UserGroup.get_or_404(user_group_id)
86 user_group = UserGroup.get_or_404(user_group_id)
87 data = user_group.get_dict()
87 data = user_group.get_dict()
88 return data
88 return data
89
89
90 def index(self, format='html'):
90 def index(self, format='html'):
91 """GET /users_groups: All items in the collection"""
91 """GET /users_groups: All items in the collection"""
92 # url('users_groups')
92 # url('users_groups')
93 _list = UserGroup.query()\
93 _list = UserGroup.query()\
94 .order_by(func.lower(UserGroup.users_group_name))\
94 .order_by(func.lower(UserGroup.users_group_name))\
95 .all()
95 .all()
96 group_iter = UserGroupList(_list, perm_set=['usergroup.admin'])
96 group_iter = UserGroupList(_list, perm_set=['usergroup.admin'])
97 user_groups_data = []
97 user_groups_data = []
98 total_records = len(group_iter)
98 total_records = len(group_iter)
99 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
99 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
100 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
100 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
101
101
102 user_group_name = lambda user_group_id, user_group_name: (
102 user_group_name = lambda user_group_id, user_group_name: (
103 template.get_def("user_group_name")
103 template.get_def("user_group_name")
104 .render(user_group_id, user_group_name, _=_, h=h, c=c)
104 .render(user_group_id, user_group_name, _=_, h=h, c=c)
105 )
105 )
106 user_group_actions = lambda user_group_id, user_group_name: (
106 user_group_actions = lambda user_group_id, user_group_name: (
107 template.get_def("user_group_actions")
107 template.get_def("user_group_actions")
108 .render(user_group_id, user_group_name, _=_, h=h, c=c)
108 .render(user_group_id, user_group_name, _=_, h=h, c=c)
109 )
109 )
110 for user_gr in group_iter:
110 for user_gr in group_iter:
111
111
112 user_groups_data.append({
112 user_groups_data.append({
113 "raw_name": user_gr.users_group_name,
113 "raw_name": user_gr.users_group_name,
114 "group_name": user_group_name(user_gr.users_group_id,
114 "group_name": user_group_name(user_gr.users_group_id,
115 user_gr.users_group_name),
115 user_gr.users_group_name),
116 "desc": h.escape(user_gr.user_group_description),
116 "desc": h.escape(user_gr.user_group_description),
117 "members": len(user_gr.members),
117 "members": len(user_gr.members),
118 "active": h.boolicon(user_gr.users_group_active),
118 "active": h.boolicon(user_gr.users_group_active),
119 "owner": h.person(user_gr.user.username),
119 "owner": h.person(user_gr.user.username),
120 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
120 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
121 })
121 })
122
122
123 c.data = json.dumps({
123 c.data = json.dumps({
124 "totalRecords": total_records,
124 "totalRecords": total_records,
125 "startIndex": 0,
125 "startIndex": 0,
126 "sort": None,
126 "sort": None,
127 "dir": "asc",
127 "dir": "asc",
128 "records": user_groups_data
128 "records": user_groups_data
129 })
129 })
130
130
131 return render('admin/user_groups/user_groups.html')
131 return render('admin/user_groups/user_groups.html')
132
132
133 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
133 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
134 def create(self):
134 def create(self):
135 """POST /users_groups: Create a new item"""
135 """POST /users_groups: Create a new item"""
136 # url('users_groups')
136 # url('users_groups')
137
137
138 users_group_form = UserGroupForm()()
138 users_group_form = UserGroupForm()()
139 try:
139 try:
140 form_result = users_group_form.to_python(dict(request.POST))
140 form_result = users_group_form.to_python(dict(request.POST))
141 ug = UserGroupModel().create(name=form_result['users_group_name'],
141 ug = UserGroupModel().create(name=form_result['users_group_name'],
142 description=form_result['user_group_description'],
142 description=form_result['user_group_description'],
143 owner=self.authuser.user_id,
143 owner=self.authuser.user_id,
144 active=form_result['users_group_active'])
144 active=form_result['users_group_active'])
145
145
146 gr = form_result['users_group_name']
146 gr = form_result['users_group_name']
147 action_logger(self.authuser,
147 action_logger(self.authuser,
148 'admin_created_users_group:%s' % gr,
148 'admin_created_users_group:%s' % gr,
149 None, self.ip_addr, self.sa)
149 None, self.ip_addr, self.sa)
150 h.flash(h.literal(_('Created user group %s') % h.link_to(h.escape(gr), url('edit_users_group', id=ug.users_group_id))),
150 h.flash(h.literal(_('Created user group %s') % h.link_to(h.escape(gr), url('edit_users_group', id=ug.users_group_id))),
151 category='success')
151 category='success')
152 Session().commit()
152 Session().commit()
153 except formencode.Invalid as errors:
153 except formencode.Invalid as errors:
154 return htmlfill.render(
154 return htmlfill.render(
155 render('admin/user_groups/user_group_add.html'),
155 render('admin/user_groups/user_group_add.html'),
156 defaults=errors.value,
156 defaults=errors.value,
157 errors=errors.error_dict or {},
157 errors=errors.error_dict or {},
158 prefix_error=False,
158 prefix_error=False,
159 encoding="UTF-8",
159 encoding="UTF-8",
160 force_defaults=False)
160 force_defaults=False)
161 except Exception:
161 except Exception:
162 log.error(traceback.format_exc())
162 log.error(traceback.format_exc())
163 h.flash(_('Error occurred during creation of user group %s') \
163 h.flash(_('Error occurred during creation of user group %s') \
164 % request.POST.get('users_group_name'), category='error')
164 % request.POST.get('users_group_name'), category='error')
165
165
166 return redirect(url('users_groups'))
166 raise HTTPFound(location=url('users_groups'))
167
167
168 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
168 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
169 def new(self, format='html'):
169 def new(self, format='html'):
170 """GET /user_groups/new: Form to create a new item"""
170 """GET /user_groups/new: Form to create a new item"""
171 # url('new_users_group')
171 # url('new_users_group')
172 return render('admin/user_groups/user_group_add.html')
172 return render('admin/user_groups/user_group_add.html')
173
173
174 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
174 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
175 def update(self, id):
175 def update(self, id):
176 """PUT /user_groups/id: Update an existing item"""
176 """PUT /user_groups/id: Update an existing item"""
177 # Forms posted to this method should contain a hidden field:
177 # Forms posted to this method should contain a hidden field:
178 # <input type="hidden" name="_method" value="PUT" />
178 # <input type="hidden" name="_method" value="PUT" />
179 # Or using helpers:
179 # Or using helpers:
180 # h.form(url('users_group', id=ID),
180 # h.form(url('users_group', id=ID),
181 # method='put')
181 # method='put')
182 # url('users_group', id=ID)
182 # url('users_group', id=ID)
183
183
184 c.user_group = UserGroup.get_or_404(id)
184 c.user_group = UserGroup.get_or_404(id)
185 c.active = 'settings'
185 c.active = 'settings'
186 self.__load_data(id)
186 self.__load_data(id)
187
187
188 available_members = [safe_unicode(x[0]) for x in c.available_members]
188 available_members = [safe_unicode(x[0]) for x in c.available_members]
189
189
190 users_group_form = UserGroupForm(edit=True,
190 users_group_form = UserGroupForm(edit=True,
191 old_data=c.user_group.get_dict(),
191 old_data=c.user_group.get_dict(),
192 available_members=available_members)()
192 available_members=available_members)()
193
193
194 try:
194 try:
195 form_result = users_group_form.to_python(request.POST)
195 form_result = users_group_form.to_python(request.POST)
196 UserGroupModel().update(c.user_group, form_result)
196 UserGroupModel().update(c.user_group, form_result)
197 gr = form_result['users_group_name']
197 gr = form_result['users_group_name']
198 action_logger(self.authuser,
198 action_logger(self.authuser,
199 'admin_updated_users_group:%s' % gr,
199 'admin_updated_users_group:%s' % gr,
200 None, self.ip_addr, self.sa)
200 None, self.ip_addr, self.sa)
201 h.flash(_('Updated user group %s') % gr, category='success')
201 h.flash(_('Updated user group %s') % gr, category='success')
202 Session().commit()
202 Session().commit()
203 except formencode.Invalid as errors:
203 except formencode.Invalid as errors:
204 ug_model = UserGroupModel()
204 ug_model = UserGroupModel()
205 defaults = errors.value
205 defaults = errors.value
206 e = errors.error_dict or {}
206 e = errors.error_dict or {}
207 defaults.update({
207 defaults.update({
208 'create_repo_perm': ug_model.has_perm(id,
208 'create_repo_perm': ug_model.has_perm(id,
209 'hg.create.repository'),
209 'hg.create.repository'),
210 'fork_repo_perm': ug_model.has_perm(id,
210 'fork_repo_perm': ug_model.has_perm(id,
211 'hg.fork.repository'),
211 'hg.fork.repository'),
212 '_method': 'put'
212 '_method': 'put'
213 })
213 })
214
214
215 return htmlfill.render(
215 return htmlfill.render(
216 render('admin/user_groups/user_group_edit.html'),
216 render('admin/user_groups/user_group_edit.html'),
217 defaults=defaults,
217 defaults=defaults,
218 errors=e,
218 errors=e,
219 prefix_error=False,
219 prefix_error=False,
220 encoding="UTF-8",
220 encoding="UTF-8",
221 force_defaults=False)
221 force_defaults=False)
222 except Exception:
222 except Exception:
223 log.error(traceback.format_exc())
223 log.error(traceback.format_exc())
224 h.flash(_('Error occurred during update of user group %s') \
224 h.flash(_('Error occurred during update of user group %s') \
225 % request.POST.get('users_group_name'), category='error')
225 % request.POST.get('users_group_name'), category='error')
226
226
227 return redirect(url('edit_users_group', id=id))
227 raise HTTPFound(location=url('edit_users_group', id=id))
228
228
229 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
229 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
230 def delete(self, id):
230 def delete(self, id):
231 """DELETE /user_groups/id: Delete an existing item"""
231 """DELETE /user_groups/id: Delete an existing item"""
232 # Forms posted to this method should contain a hidden field:
232 # Forms posted to this method should contain a hidden field:
233 # <input type="hidden" name="_method" value="DELETE" />
233 # <input type="hidden" name="_method" value="DELETE" />
234 # Or using helpers:
234 # Or using helpers:
235 # h.form(url('users_group', id=ID),
235 # h.form(url('users_group', id=ID),
236 # method='delete')
236 # method='delete')
237 # url('users_group', id=ID)
237 # url('users_group', id=ID)
238 usr_gr = UserGroup.get_or_404(id)
238 usr_gr = UserGroup.get_or_404(id)
239 try:
239 try:
240 UserGroupModel().delete(usr_gr)
240 UserGroupModel().delete(usr_gr)
241 Session().commit()
241 Session().commit()
242 h.flash(_('Successfully deleted user group'), category='success')
242 h.flash(_('Successfully deleted user group'), category='success')
243 except UserGroupsAssignedException as e:
243 except UserGroupsAssignedException as e:
244 h.flash(e, category='error')
244 h.flash(e, category='error')
245 except Exception:
245 except Exception:
246 log.error(traceback.format_exc())
246 log.error(traceback.format_exc())
247 h.flash(_('An error occurred during deletion of user group'),
247 h.flash(_('An error occurred during deletion of user group'),
248 category='error')
248 category='error')
249 return redirect(url('users_groups'))
249 raise HTTPFound(location=url('users_groups'))
250
250
251 def show(self, id, format='html'):
251 def show(self, id, format='html'):
252 """GET /user_groups/id: Show a specific item"""
252 """GET /user_groups/id: Show a specific item"""
253 # url('users_group', id=ID)
253 # url('users_group', id=ID)
254
254
255 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
255 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
256 def edit(self, id, format='html'):
256 def edit(self, id, format='html'):
257 """GET /user_groups/id/edit: Form to edit an existing item"""
257 """GET /user_groups/id/edit: Form to edit an existing item"""
258 # url('edit_users_group', id=ID)
258 # url('edit_users_group', id=ID)
259
259
260 c.user_group = UserGroup.get_or_404(id)
260 c.user_group = UserGroup.get_or_404(id)
261 c.active = 'settings'
261 c.active = 'settings'
262 self.__load_data(id)
262 self.__load_data(id)
263
263
264 defaults = self.__load_defaults(id)
264 defaults = self.__load_defaults(id)
265
265
266 return htmlfill.render(
266 return htmlfill.render(
267 render('admin/user_groups/user_group_edit.html'),
267 render('admin/user_groups/user_group_edit.html'),
268 defaults=defaults,
268 defaults=defaults,
269 encoding="UTF-8",
269 encoding="UTF-8",
270 force_defaults=False
270 force_defaults=False
271 )
271 )
272
272
273 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
273 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
274 def edit_perms(self, id):
274 def edit_perms(self, id):
275 c.user_group = UserGroup.get_or_404(id)
275 c.user_group = UserGroup.get_or_404(id)
276 c.active = 'perms'
276 c.active = 'perms'
277
277
278 repo_model = RepoModel()
278 repo_model = RepoModel()
279 c.users_array = repo_model.get_users_js()
279 c.users_array = repo_model.get_users_js()
280 c.user_groups_array = repo_model.get_user_groups_js()
280 c.user_groups_array = repo_model.get_user_groups_js()
281
281
282 defaults = {}
282 defaults = {}
283 # fill user group users
283 # fill user group users
284 for p in c.user_group.user_user_group_to_perm:
284 for p in c.user_group.user_user_group_to_perm:
285 defaults.update({'u_perm_%s' % p.user.username:
285 defaults.update({'u_perm_%s' % p.user.username:
286 p.permission.permission_name})
286 p.permission.permission_name})
287
287
288 for p in c.user_group.user_group_user_group_to_perm:
288 for p in c.user_group.user_group_user_group_to_perm:
289 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
289 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
290 p.permission.permission_name})
290 p.permission.permission_name})
291
291
292 return htmlfill.render(
292 return htmlfill.render(
293 render('admin/user_groups/user_group_edit.html'),
293 render('admin/user_groups/user_group_edit.html'),
294 defaults=defaults,
294 defaults=defaults,
295 encoding="UTF-8",
295 encoding="UTF-8",
296 force_defaults=False
296 force_defaults=False
297 )
297 )
298
298
299 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
299 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
300 def update_perms(self, id):
300 def update_perms(self, id):
301 """
301 """
302 grant permission for given usergroup
302 grant permission for given usergroup
303
303
304 :param id:
304 :param id:
305 """
305 """
306 user_group = UserGroup.get_or_404(id)
306 user_group = UserGroup.get_or_404(id)
307 form = UserGroupPermsForm()().to_python(request.POST)
307 form = UserGroupPermsForm()().to_python(request.POST)
308
308
309 # set the permissions !
309 # set the permissions !
310 try:
310 try:
311 UserGroupModel()._update_permissions(user_group, form['perms_new'],
311 UserGroupModel()._update_permissions(user_group, form['perms_new'],
312 form['perms_updates'])
312 form['perms_updates'])
313 except RepoGroupAssignmentError:
313 except RepoGroupAssignmentError:
314 h.flash(_('Target group cannot be the same'), category='error')
314 h.flash(_('Target group cannot be the same'), category='error')
315 return redirect(url('edit_user_group_perms', id=id))
315 raise HTTPFound(location=url('edit_user_group_perms', id=id))
316 #TODO: implement this
316 #TODO: implement this
317 #action_logger(self.authuser, 'admin_changed_repo_permissions',
317 #action_logger(self.authuser, 'admin_changed_repo_permissions',
318 # repo_name, self.ip_addr, self.sa)
318 # repo_name, self.ip_addr, self.sa)
319 Session().commit()
319 Session().commit()
320 h.flash(_('User group permissions updated'), category='success')
320 h.flash(_('User group permissions updated'), category='success')
321 return redirect(url('edit_user_group_perms', id=id))
321 raise HTTPFound(location=url('edit_user_group_perms', id=id))
322
322
323 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
323 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
324 def delete_perms(self, id):
324 def delete_perms(self, id):
325 """
325 """
326 DELETE an existing repository group permission user
326 DELETE an existing repository group permission user
327
327
328 :param group_name:
328 :param group_name:
329 """
329 """
330 try:
330 try:
331 obj_type = request.POST.get('obj_type')
331 obj_type = request.POST.get('obj_type')
332 obj_id = None
332 obj_id = None
333 if obj_type == 'user':
333 if obj_type == 'user':
334 obj_id = safe_int(request.POST.get('user_id'))
334 obj_id = safe_int(request.POST.get('user_id'))
335 elif obj_type == 'user_group':
335 elif obj_type == 'user_group':
336 obj_id = safe_int(request.POST.get('user_group_id'))
336 obj_id = safe_int(request.POST.get('user_group_id'))
337
337
338 if not c.authuser.is_admin:
338 if not c.authuser.is_admin:
339 if obj_type == 'user' and c.authuser.user_id == obj_id:
339 if obj_type == 'user' and c.authuser.user_id == obj_id:
340 msg = _('Cannot revoke permission for yourself as admin')
340 msg = _('Cannot revoke permission for yourself as admin')
341 h.flash(msg, category='warning')
341 h.flash(msg, category='warning')
342 raise Exception('revoke admin permission on self')
342 raise Exception('revoke admin permission on self')
343 if obj_type == 'user':
343 if obj_type == 'user':
344 UserGroupModel().revoke_user_permission(user_group=id,
344 UserGroupModel().revoke_user_permission(user_group=id,
345 user=obj_id)
345 user=obj_id)
346 elif obj_type == 'user_group':
346 elif obj_type == 'user_group':
347 UserGroupModel().revoke_user_group_permission(target_user_group=id,
347 UserGroupModel().revoke_user_group_permission(target_user_group=id,
348 user_group=obj_id)
348 user_group=obj_id)
349 Session().commit()
349 Session().commit()
350 except Exception:
350 except Exception:
351 log.error(traceback.format_exc())
351 log.error(traceback.format_exc())
352 h.flash(_('An error occurred during revoking of permission'),
352 h.flash(_('An error occurred during revoking of permission'),
353 category='error')
353 category='error')
354 raise HTTPInternalServerError()
354 raise HTTPInternalServerError()
355
355
356 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
356 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
357 def edit_default_perms(self, id):
357 def edit_default_perms(self, id):
358 c.user_group = UserGroup.get_or_404(id)
358 c.user_group = UserGroup.get_or_404(id)
359 c.active = 'default_perms'
359 c.active = 'default_perms'
360
360
361 permissions = {
361 permissions = {
362 'repositories': {},
362 'repositories': {},
363 'repositories_groups': {}
363 'repositories_groups': {}
364 }
364 }
365 ugroup_repo_perms = UserGroupRepoToPerm.query()\
365 ugroup_repo_perms = UserGroupRepoToPerm.query()\
366 .options(joinedload(UserGroupRepoToPerm.permission))\
366 .options(joinedload(UserGroupRepoToPerm.permission))\
367 .options(joinedload(UserGroupRepoToPerm.repository))\
367 .options(joinedload(UserGroupRepoToPerm.repository))\
368 .filter(UserGroupRepoToPerm.users_group_id == id)\
368 .filter(UserGroupRepoToPerm.users_group_id == id)\
369 .all()
369 .all()
370
370
371 for gr in ugroup_repo_perms:
371 for gr in ugroup_repo_perms:
372 permissions['repositories'][gr.repository.repo_name] \
372 permissions['repositories'][gr.repository.repo_name] \
373 = gr.permission.permission_name
373 = gr.permission.permission_name
374
374
375 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
375 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
376 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
376 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
377 .options(joinedload(UserGroupRepoGroupToPerm.group))\
377 .options(joinedload(UserGroupRepoGroupToPerm.group))\
378 .filter(UserGroupRepoGroupToPerm.users_group_id == id)\
378 .filter(UserGroupRepoGroupToPerm.users_group_id == id)\
379 .all()
379 .all()
380
380
381 for gr in ugroup_group_perms:
381 for gr in ugroup_group_perms:
382 permissions['repositories_groups'][gr.group.group_name] \
382 permissions['repositories_groups'][gr.group.group_name] \
383 = gr.permission.permission_name
383 = gr.permission.permission_name
384 c.permissions = permissions
384 c.permissions = permissions
385
385
386 ug_model = UserGroupModel()
386 ug_model = UserGroupModel()
387
387
388 defaults = c.user_group.get_dict()
388 defaults = c.user_group.get_dict()
389 defaults.update({
389 defaults.update({
390 'create_repo_perm': ug_model.has_perm(c.user_group,
390 'create_repo_perm': ug_model.has_perm(c.user_group,
391 'hg.create.repository'),
391 'hg.create.repository'),
392 'create_user_group_perm': ug_model.has_perm(c.user_group,
392 'create_user_group_perm': ug_model.has_perm(c.user_group,
393 'hg.usergroup.create.true'),
393 'hg.usergroup.create.true'),
394 'fork_repo_perm': ug_model.has_perm(c.user_group,
394 'fork_repo_perm': ug_model.has_perm(c.user_group,
395 'hg.fork.repository'),
395 'hg.fork.repository'),
396 })
396 })
397
397
398 return htmlfill.render(
398 return htmlfill.render(
399 render('admin/user_groups/user_group_edit.html'),
399 render('admin/user_groups/user_group_edit.html'),
400 defaults=defaults,
400 defaults=defaults,
401 encoding="UTF-8",
401 encoding="UTF-8",
402 force_defaults=False
402 force_defaults=False
403 )
403 )
404
404
405 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
405 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
406 def update_default_perms(self, id):
406 def update_default_perms(self, id):
407 """PUT /users_perm/id: Update an existing item"""
407 """PUT /users_perm/id: Update an existing item"""
408 # url('users_group_perm', id=ID, method='put')
408 # url('users_group_perm', id=ID, method='put')
409
409
410 user_group = UserGroup.get_or_404(id)
410 user_group = UserGroup.get_or_404(id)
411
411
412 try:
412 try:
413 form = CustomDefaultPermissionsForm()()
413 form = CustomDefaultPermissionsForm()()
414 form_result = form.to_python(request.POST)
414 form_result = form.to_python(request.POST)
415
415
416 inherit_perms = form_result['inherit_default_permissions']
416 inherit_perms = form_result['inherit_default_permissions']
417 user_group.inherit_default_permissions = inherit_perms
417 user_group.inherit_default_permissions = inherit_perms
418 Session().add(user_group)
418 Session().add(user_group)
419 usergroup_model = UserGroupModel()
419 usergroup_model = UserGroupModel()
420
420
421 defs = UserGroupToPerm.query()\
421 defs = UserGroupToPerm.query()\
422 .filter(UserGroupToPerm.users_group == user_group)\
422 .filter(UserGroupToPerm.users_group == user_group)\
423 .all()
423 .all()
424 for ug in defs:
424 for ug in defs:
425 Session().delete(ug)
425 Session().delete(ug)
426
426
427 if form_result['create_repo_perm']:
427 if form_result['create_repo_perm']:
428 usergroup_model.grant_perm(id, 'hg.create.repository')
428 usergroup_model.grant_perm(id, 'hg.create.repository')
429 else:
429 else:
430 usergroup_model.grant_perm(id, 'hg.create.none')
430 usergroup_model.grant_perm(id, 'hg.create.none')
431 if form_result['create_user_group_perm']:
431 if form_result['create_user_group_perm']:
432 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
432 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
433 else:
433 else:
434 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
434 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
435 if form_result['fork_repo_perm']:
435 if form_result['fork_repo_perm']:
436 usergroup_model.grant_perm(id, 'hg.fork.repository')
436 usergroup_model.grant_perm(id, 'hg.fork.repository')
437 else:
437 else:
438 usergroup_model.grant_perm(id, 'hg.fork.none')
438 usergroup_model.grant_perm(id, 'hg.fork.none')
439
439
440 h.flash(_("Updated permissions"), category='success')
440 h.flash(_("Updated permissions"), category='success')
441 Session().commit()
441 Session().commit()
442 except Exception:
442 except Exception:
443 log.error(traceback.format_exc())
443 log.error(traceback.format_exc())
444 h.flash(_('An error occurred during permissions saving'),
444 h.flash(_('An error occurred during permissions saving'),
445 category='error')
445 category='error')
446
446
447 return redirect(url('edit_user_group_default_perms', id=id))
447 raise HTTPFound(location=url('edit_user_group_default_perms', id=id))
448
448
449 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
449 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
450 def edit_advanced(self, id):
450 def edit_advanced(self, id):
451 c.user_group = UserGroup.get_or_404(id)
451 c.user_group = UserGroup.get_or_404(id)
452 c.active = 'advanced'
452 c.active = 'advanced'
453 c.group_members_obj = sorted((x.user for x in c.user_group.members),
453 c.group_members_obj = sorted((x.user for x in c.user_group.members),
454 key=lambda u: u.username.lower())
454 key=lambda u: u.username.lower())
455 return render('admin/user_groups/user_group_edit.html')
455 return render('admin/user_groups/user_group_edit.html')
456
456
457
457
458 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
458 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
459 def edit_members(self, id):
459 def edit_members(self, id):
460 c.user_group = UserGroup.get_or_404(id)
460 c.user_group = UserGroup.get_or_404(id)
461 c.active = 'members'
461 c.active = 'members'
462 c.group_members_obj = sorted((x.user for x in c.user_group.members),
462 c.group_members_obj = sorted((x.user for x in c.user_group.members),
463 key=lambda u: u.username.lower())
463 key=lambda u: u.username.lower())
464
464
465 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
465 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
466 return render('admin/user_groups/user_group_edit.html')
466 return render('admin/user_groups/user_group_edit.html')
@@ -1,487 +1,486 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.users
15 kallithea.controllers.admin.users
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Users crud controller for pylons
18 Users crud controller for pylons
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 4, 2010
22 :created_on: Apr 4, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from pylons import request, tmpl_context as c, url, config
33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
36 from sqlalchemy.sql.expression import func
35 from sqlalchemy.sql.expression import func
37 from webob.exc import HTTPNotFound
36 from webob.exc import HTTPFound, HTTPNotFound
38
37
39 import kallithea
38 import kallithea
40 from kallithea.lib.exceptions import DefaultUserException, \
39 from kallithea.lib.exceptions import DefaultUserException, \
41 UserOwnsReposException, UserCreationError
40 UserOwnsReposException, UserCreationError
42 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
43 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
42 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
44 AuthUser
43 AuthUser
45 from kallithea.lib import auth_modules
44 from kallithea.lib import auth_modules
46 from kallithea.lib.auth_modules import auth_internal
45 from kallithea.lib.auth_modules import auth_internal
47 from kallithea.lib.base import BaseController, render
46 from kallithea.lib.base import BaseController, render
48 from kallithea.model.api_key import ApiKeyModel
47 from kallithea.model.api_key import ApiKeyModel
49
48
50 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
51 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
52 from kallithea.model.user import UserModel
51 from kallithea.model.user import UserModel
53 from kallithea.model.meta import Session
52 from kallithea.model.meta import Session
54 from kallithea.lib.utils import action_logger
53 from kallithea.lib.utils import action_logger
55 from kallithea.lib.compat import json
54 from kallithea.lib.compat import json
56 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
55 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
57
56
58 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
59
58
60
59
61 class UsersController(BaseController):
60 class UsersController(BaseController):
62 """REST Controller styled on the Atom Publishing Protocol"""
61 """REST Controller styled on the Atom Publishing Protocol"""
63
62
64 @LoginRequired()
63 @LoginRequired()
65 @HasPermissionAllDecorator('hg.admin')
64 @HasPermissionAllDecorator('hg.admin')
66 def __before__(self):
65 def __before__(self):
67 super(UsersController, self).__before__()
66 super(UsersController, self).__before__()
68 c.available_permissions = config['available_permissions']
67 c.available_permissions = config['available_permissions']
69 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
70
69
71 def index(self, format='html'):
70 def index(self, format='html'):
72 """GET /users: All items in the collection"""
71 """GET /users: All items in the collection"""
73 # url('users')
72 # url('users')
74
73
75 c.users_list = User.query().order_by(User.username)\
74 c.users_list = User.query().order_by(User.username)\
76 .filter(User.username != User.DEFAULT_USER)\
75 .filter(User.username != User.DEFAULT_USER)\
77 .order_by(func.lower(User.username))\
76 .order_by(func.lower(User.username))\
78 .all()
77 .all()
79
78
80 users_data = []
79 users_data = []
81 total_records = len(c.users_list)
80 total_records = len(c.users_list)
82 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
83 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
84
83
85 grav_tmpl = '<div class="gravatar">%s</div>'
84 grav_tmpl = '<div class="gravatar">%s</div>'
86
85
87 username = lambda user_id, username: (
86 username = lambda user_id, username: (
88 template.get_def("user_name")
87 template.get_def("user_name")
89 .render(user_id, username, _=_, h=h, c=c))
88 .render(user_id, username, _=_, h=h, c=c))
90
89
91 user_actions = lambda user_id, username: (
90 user_actions = lambda user_id, username: (
92 template.get_def("user_actions")
91 template.get_def("user_actions")
93 .render(user_id, username, _=_, h=h, c=c))
92 .render(user_id, username, _=_, h=h, c=c))
94
93
95 for user in c.users_list:
94 for user in c.users_list:
96 users_data.append({
95 users_data.append({
97 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
98 "raw_name": user.username,
97 "raw_name": user.username,
99 "username": username(user.user_id, user.username),
98 "username": username(user.user_id, user.username),
100 "firstname": h.escape(user.name),
99 "firstname": h.escape(user.name),
101 "lastname": h.escape(user.lastname),
100 "lastname": h.escape(user.lastname),
102 "last_login": h.fmt_date(user.last_login),
101 "last_login": h.fmt_date(user.last_login),
103 "last_login_raw": datetime_to_time(user.last_login),
102 "last_login_raw": datetime_to_time(user.last_login),
104 "active": h.boolicon(user.active),
103 "active": h.boolicon(user.active),
105 "admin": h.boolicon(user.admin),
104 "admin": h.boolicon(user.admin),
106 "extern_type": user.extern_type,
105 "extern_type": user.extern_type,
107 "extern_name": user.extern_name,
106 "extern_name": user.extern_name,
108 "action": user_actions(user.user_id, user.username),
107 "action": user_actions(user.user_id, user.username),
109 })
108 })
110
109
111 c.data = json.dumps({
110 c.data = json.dumps({
112 "totalRecords": total_records,
111 "totalRecords": total_records,
113 "startIndex": 0,
112 "startIndex": 0,
114 "sort": None,
113 "sort": None,
115 "dir": "asc",
114 "dir": "asc",
116 "records": users_data
115 "records": users_data
117 })
116 })
118
117
119 return render('admin/users/users.html')
118 return render('admin/users/users.html')
120
119
121 def create(self):
120 def create(self):
122 """POST /users: Create a new item"""
121 """POST /users: Create a new item"""
123 # url('users')
122 # url('users')
124 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
123 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
125 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
124 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
126 user_model = UserModel()
125 user_model = UserModel()
127 user_form = UserForm()()
126 user_form = UserForm()()
128 try:
127 try:
129 form_result = user_form.to_python(dict(request.POST))
128 form_result = user_form.to_python(dict(request.POST))
130 user = user_model.create(form_result)
129 user = user_model.create(form_result)
131 usr = form_result['username']
130 usr = form_result['username']
132 action_logger(self.authuser, 'admin_created_user:%s' % usr,
131 action_logger(self.authuser, 'admin_created_user:%s' % usr,
133 None, self.ip_addr, self.sa)
132 None, self.ip_addr, self.sa)
134 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
133 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
135 category='success')
134 category='success')
136 Session().commit()
135 Session().commit()
137 except formencode.Invalid as errors:
136 except formencode.Invalid as errors:
138 return htmlfill.render(
137 return htmlfill.render(
139 render('admin/users/user_add.html'),
138 render('admin/users/user_add.html'),
140 defaults=errors.value,
139 defaults=errors.value,
141 errors=errors.error_dict or {},
140 errors=errors.error_dict or {},
142 prefix_error=False,
141 prefix_error=False,
143 encoding="UTF-8",
142 encoding="UTF-8",
144 force_defaults=False)
143 force_defaults=False)
145 except UserCreationError as e:
144 except UserCreationError as e:
146 h.flash(e, 'error')
145 h.flash(e, 'error')
147 except Exception:
146 except Exception:
148 log.error(traceback.format_exc())
147 log.error(traceback.format_exc())
149 h.flash(_('Error occurred during creation of user %s') \
148 h.flash(_('Error occurred during creation of user %s') \
150 % request.POST.get('username'), category='error')
149 % request.POST.get('username'), category='error')
151 return redirect(url('users'))
150 raise HTTPFound(location=url('users'))
152
151
153 def new(self, format='html'):
152 def new(self, format='html'):
154 """GET /users/new: Form to create a new item"""
153 """GET /users/new: Form to create a new item"""
155 # url('new_user')
154 # url('new_user')
156 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
155 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
157 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
156 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
158 return render('admin/users/user_add.html')
157 return render('admin/users/user_add.html')
159
158
160 def update(self, id):
159 def update(self, id):
161 """PUT /users/id: Update an existing item"""
160 """PUT /users/id: Update an existing item"""
162 # Forms posted to this method should contain a hidden field:
161 # Forms posted to this method should contain a hidden field:
163 # <input type="hidden" name="_method" value="PUT" />
162 # <input type="hidden" name="_method" value="PUT" />
164 # Or using helpers:
163 # Or using helpers:
165 # h.form(url('update_user', id=ID),
164 # h.form(url('update_user', id=ID),
166 # method='put')
165 # method='put')
167 # url('user', id=ID)
166 # url('user', id=ID)
168 user_model = UserModel()
167 user_model = UserModel()
169 user = user_model.get(id)
168 user = user_model.get(id)
170 _form = UserForm(edit=True, old_data={'user_id': id,
169 _form = UserForm(edit=True, old_data={'user_id': id,
171 'email': user.email})()
170 'email': user.email})()
172 form_result = {}
171 form_result = {}
173 try:
172 try:
174 form_result = _form.to_python(dict(request.POST))
173 form_result = _form.to_python(dict(request.POST))
175 skip_attrs = ['extern_type', 'extern_name',
174 skip_attrs = ['extern_type', 'extern_name',
176 ] + auth_modules.get_managed_fields(user)
175 ] + auth_modules.get_managed_fields(user)
177
176
178 user_model.update(id, form_result, skip_attrs=skip_attrs)
177 user_model.update(id, form_result, skip_attrs=skip_attrs)
179 usr = form_result['username']
178 usr = form_result['username']
180 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
179 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
181 None, self.ip_addr, self.sa)
180 None, self.ip_addr, self.sa)
182 h.flash(_('User updated successfully'), category='success')
181 h.flash(_('User updated successfully'), category='success')
183 Session().commit()
182 Session().commit()
184 except formencode.Invalid as errors:
183 except formencode.Invalid as errors:
185 defaults = errors.value
184 defaults = errors.value
186 e = errors.error_dict or {}
185 e = errors.error_dict or {}
187 defaults.update({
186 defaults.update({
188 'create_repo_perm': user_model.has_perm(id,
187 'create_repo_perm': user_model.has_perm(id,
189 'hg.create.repository'),
188 'hg.create.repository'),
190 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
189 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
191 '_method': 'put'
190 '_method': 'put'
192 })
191 })
193 return htmlfill.render(
192 return htmlfill.render(
194 self._render_edit_profile(user),
193 self._render_edit_profile(user),
195 defaults=defaults,
194 defaults=defaults,
196 errors=e,
195 errors=e,
197 prefix_error=False,
196 prefix_error=False,
198 encoding="UTF-8",
197 encoding="UTF-8",
199 force_defaults=False)
198 force_defaults=False)
200 except Exception:
199 except Exception:
201 log.error(traceback.format_exc())
200 log.error(traceback.format_exc())
202 h.flash(_('Error occurred during update of user %s') \
201 h.flash(_('Error occurred during update of user %s') \
203 % form_result.get('username'), category='error')
202 % form_result.get('username'), category='error')
204 return redirect(url('edit_user', id=id))
203 raise HTTPFound(location=url('edit_user', id=id))
205
204
206 def delete(self, id):
205 def delete(self, id):
207 """DELETE /users/id: Delete an existing item"""
206 """DELETE /users/id: Delete an existing item"""
208 # Forms posted to this method should contain a hidden field:
207 # Forms posted to this method should contain a hidden field:
209 # <input type="hidden" name="_method" value="DELETE" />
208 # <input type="hidden" name="_method" value="DELETE" />
210 # Or using helpers:
209 # Or using helpers:
211 # h.form(url('delete_user', id=ID),
210 # h.form(url('delete_user', id=ID),
212 # method='delete')
211 # method='delete')
213 # url('user', id=ID)
212 # url('user', id=ID)
214 usr = User.get_or_404(id)
213 usr = User.get_or_404(id)
215 try:
214 try:
216 UserModel().delete(usr)
215 UserModel().delete(usr)
217 Session().commit()
216 Session().commit()
218 h.flash(_('Successfully deleted user'), category='success')
217 h.flash(_('Successfully deleted user'), category='success')
219 except (UserOwnsReposException, DefaultUserException) as e:
218 except (UserOwnsReposException, DefaultUserException) as e:
220 h.flash(e, category='warning')
219 h.flash(e, category='warning')
221 except Exception:
220 except Exception:
222 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
223 h.flash(_('An error occurred during deletion of user'),
222 h.flash(_('An error occurred during deletion of user'),
224 category='error')
223 category='error')
225 return redirect(url('users'))
224 raise HTTPFound(location=url('users'))
226
225
227 def show(self, id, format='html'):
226 def show(self, id, format='html'):
228 """GET /users/id: Show a specific item"""
227 """GET /users/id: Show a specific item"""
229 # url('user', id=ID)
228 # url('user', id=ID)
230 User.get_or_404(-1)
229 User.get_or_404(-1)
231
230
232 def _get_user_or_raise_if_default(self, id):
231 def _get_user_or_raise_if_default(self, id):
233 try:
232 try:
234 return User.get_or_404(id, allow_default=False)
233 return User.get_or_404(id, allow_default=False)
235 except DefaultUserException:
234 except DefaultUserException:
236 h.flash(_("The default user cannot be edited"), category='warning')
235 h.flash(_("The default user cannot be edited"), category='warning')
237 raise HTTPNotFound
236 raise HTTPNotFound
238
237
239 def _render_edit_profile(self, user):
238 def _render_edit_profile(self, user):
240 c.user = user
239 c.user = user
241 c.active = 'profile'
240 c.active = 'profile'
242 c.perm_user = AuthUser(dbuser=user)
241 c.perm_user = AuthUser(dbuser=user)
243 c.ip_addr = self.ip_addr
242 c.ip_addr = self.ip_addr
244 managed_fields = auth_modules.get_managed_fields(user)
243 managed_fields = auth_modules.get_managed_fields(user)
245 c.readonly = lambda n: 'readonly' if n in managed_fields else None
244 c.readonly = lambda n: 'readonly' if n in managed_fields else None
246 return render('admin/users/user_edit.html')
245 return render('admin/users/user_edit.html')
247
246
248 def edit(self, id, format='html'):
247 def edit(self, id, format='html'):
249 """GET /users/id/edit: Form to edit an existing item"""
248 """GET /users/id/edit: Form to edit an existing item"""
250 # url('edit_user', id=ID)
249 # url('edit_user', id=ID)
251 user = self._get_user_or_raise_if_default(id)
250 user = self._get_user_or_raise_if_default(id)
252 defaults = user.get_dict()
251 defaults = user.get_dict()
253
252
254 return htmlfill.render(
253 return htmlfill.render(
255 self._render_edit_profile(user),
254 self._render_edit_profile(user),
256 defaults=defaults,
255 defaults=defaults,
257 encoding="UTF-8",
256 encoding="UTF-8",
258 force_defaults=False)
257 force_defaults=False)
259
258
260 def edit_advanced(self, id):
259 def edit_advanced(self, id):
261 c.user = self._get_user_or_raise_if_default(id)
260 c.user = self._get_user_or_raise_if_default(id)
262 c.active = 'advanced'
261 c.active = 'advanced'
263 c.perm_user = AuthUser(user_id=id)
262 c.perm_user = AuthUser(user_id=id)
264 c.ip_addr = self.ip_addr
263 c.ip_addr = self.ip_addr
265
264
266 umodel = UserModel()
265 umodel = UserModel()
267 defaults = c.user.get_dict()
266 defaults = c.user.get_dict()
268 defaults.update({
267 defaults.update({
269 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
270 'create_user_group_perm': umodel.has_perm(c.user,
269 'create_user_group_perm': umodel.has_perm(c.user,
271 'hg.usergroup.create.true'),
270 'hg.usergroup.create.true'),
272 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
273 })
272 })
274 return htmlfill.render(
273 return htmlfill.render(
275 render('admin/users/user_edit.html'),
274 render('admin/users/user_edit.html'),
276 defaults=defaults,
275 defaults=defaults,
277 encoding="UTF-8",
276 encoding="UTF-8",
278 force_defaults=False)
277 force_defaults=False)
279
278
280 def edit_api_keys(self, id):
279 def edit_api_keys(self, id):
281 c.user = self._get_user_or_raise_if_default(id)
280 c.user = self._get_user_or_raise_if_default(id)
282 c.active = 'api_keys'
281 c.active = 'api_keys'
283 show_expired = True
282 show_expired = True
284 c.lifetime_values = [
283 c.lifetime_values = [
285 (str(-1), _('Forever')),
284 (str(-1), _('Forever')),
286 (str(5), _('5 minutes')),
285 (str(5), _('5 minutes')),
287 (str(60), _('1 hour')),
286 (str(60), _('1 hour')),
288 (str(60 * 24), _('1 day')),
287 (str(60 * 24), _('1 day')),
289 (str(60 * 24 * 30), _('1 month')),
288 (str(60 * 24 * 30), _('1 month')),
290 ]
289 ]
291 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
290 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
292 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
291 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
293 show_expired=show_expired)
292 show_expired=show_expired)
294 defaults = c.user.get_dict()
293 defaults = c.user.get_dict()
295 return htmlfill.render(
294 return htmlfill.render(
296 render('admin/users/user_edit.html'),
295 render('admin/users/user_edit.html'),
297 defaults=defaults,
296 defaults=defaults,
298 encoding="UTF-8",
297 encoding="UTF-8",
299 force_defaults=False)
298 force_defaults=False)
300
299
301 def add_api_key(self, id):
300 def add_api_key(self, id):
302 c.user = self._get_user_or_raise_if_default(id)
301 c.user = self._get_user_or_raise_if_default(id)
303
302
304 lifetime = safe_int(request.POST.get('lifetime'), -1)
303 lifetime = safe_int(request.POST.get('lifetime'), -1)
305 description = request.POST.get('description')
304 description = request.POST.get('description')
306 ApiKeyModel().create(c.user.user_id, description, lifetime)
305 ApiKeyModel().create(c.user.user_id, description, lifetime)
307 Session().commit()
306 Session().commit()
308 h.flash(_("API key successfully created"), category='success')
307 h.flash(_("API key successfully created"), category='success')
309 return redirect(url('edit_user_api_keys', id=c.user.user_id))
308 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
310
309
311 def delete_api_key(self, id):
310 def delete_api_key(self, id):
312 c.user = self._get_user_or_raise_if_default(id)
311 c.user = self._get_user_or_raise_if_default(id)
313
312
314 api_key = request.POST.get('del_api_key')
313 api_key = request.POST.get('del_api_key')
315 if request.POST.get('del_api_key_builtin'):
314 if request.POST.get('del_api_key_builtin'):
316 user = User.get(c.user.user_id)
315 user = User.get(c.user.user_id)
317 if user is not None:
316 if user is not None:
318 user.api_key = generate_api_key()
317 user.api_key = generate_api_key()
319 Session().add(user)
318 Session().add(user)
320 Session().commit()
319 Session().commit()
321 h.flash(_("API key successfully reset"), category='success')
320 h.flash(_("API key successfully reset"), category='success')
322 elif api_key:
321 elif api_key:
323 ApiKeyModel().delete(api_key, c.user.user_id)
322 ApiKeyModel().delete(api_key, c.user.user_id)
324 Session().commit()
323 Session().commit()
325 h.flash(_("API key successfully deleted"), category='success')
324 h.flash(_("API key successfully deleted"), category='success')
326
325
327 return redirect(url('edit_user_api_keys', id=c.user.user_id))
326 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
328
327
329 def update_account(self, id):
328 def update_account(self, id):
330 pass
329 pass
331
330
332 def edit_perms(self, id):
331 def edit_perms(self, id):
333 c.user = self._get_user_or_raise_if_default(id)
332 c.user = self._get_user_or_raise_if_default(id)
334 c.active = 'perms'
333 c.active = 'perms'
335 c.perm_user = AuthUser(user_id=id)
334 c.perm_user = AuthUser(user_id=id)
336 c.ip_addr = self.ip_addr
335 c.ip_addr = self.ip_addr
337
336
338 umodel = UserModel()
337 umodel = UserModel()
339 defaults = c.user.get_dict()
338 defaults = c.user.get_dict()
340 defaults.update({
339 defaults.update({
341 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
340 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
342 'create_user_group_perm': umodel.has_perm(c.user,
341 'create_user_group_perm': umodel.has_perm(c.user,
343 'hg.usergroup.create.true'),
342 'hg.usergroup.create.true'),
344 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
343 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
345 })
344 })
346 return htmlfill.render(
345 return htmlfill.render(
347 render('admin/users/user_edit.html'),
346 render('admin/users/user_edit.html'),
348 defaults=defaults,
347 defaults=defaults,
349 encoding="UTF-8",
348 encoding="UTF-8",
350 force_defaults=False)
349 force_defaults=False)
351
350
352 def update_perms(self, id):
351 def update_perms(self, id):
353 """PUT /users_perm/id: Update an existing item"""
352 """PUT /users_perm/id: Update an existing item"""
354 # url('user_perm', id=ID, method='put')
353 # url('user_perm', id=ID, method='put')
355 user = self._get_user_or_raise_if_default(id)
354 user = self._get_user_or_raise_if_default(id)
356
355
357 try:
356 try:
358 form = CustomDefaultPermissionsForm()()
357 form = CustomDefaultPermissionsForm()()
359 form_result = form.to_python(request.POST)
358 form_result = form.to_python(request.POST)
360
359
361 inherit_perms = form_result['inherit_default_permissions']
360 inherit_perms = form_result['inherit_default_permissions']
362 user.inherit_default_permissions = inherit_perms
361 user.inherit_default_permissions = inherit_perms
363 Session().add(user)
362 Session().add(user)
364 user_model = UserModel()
363 user_model = UserModel()
365
364
366 defs = UserToPerm.query()\
365 defs = UserToPerm.query()\
367 .filter(UserToPerm.user == user)\
366 .filter(UserToPerm.user == user)\
368 .all()
367 .all()
369 for ug in defs:
368 for ug in defs:
370 Session().delete(ug)
369 Session().delete(ug)
371
370
372 if form_result['create_repo_perm']:
371 if form_result['create_repo_perm']:
373 user_model.grant_perm(id, 'hg.create.repository')
372 user_model.grant_perm(id, 'hg.create.repository')
374 else:
373 else:
375 user_model.grant_perm(id, 'hg.create.none')
374 user_model.grant_perm(id, 'hg.create.none')
376 if form_result['create_user_group_perm']:
375 if form_result['create_user_group_perm']:
377 user_model.grant_perm(id, 'hg.usergroup.create.true')
376 user_model.grant_perm(id, 'hg.usergroup.create.true')
378 else:
377 else:
379 user_model.grant_perm(id, 'hg.usergroup.create.false')
378 user_model.grant_perm(id, 'hg.usergroup.create.false')
380 if form_result['fork_repo_perm']:
379 if form_result['fork_repo_perm']:
381 user_model.grant_perm(id, 'hg.fork.repository')
380 user_model.grant_perm(id, 'hg.fork.repository')
382 else:
381 else:
383 user_model.grant_perm(id, 'hg.fork.none')
382 user_model.grant_perm(id, 'hg.fork.none')
384 h.flash(_("Updated permissions"), category='success')
383 h.flash(_("Updated permissions"), category='success')
385 Session().commit()
384 Session().commit()
386 except Exception:
385 except Exception:
387 log.error(traceback.format_exc())
386 log.error(traceback.format_exc())
388 h.flash(_('An error occurred during permissions saving'),
387 h.flash(_('An error occurred during permissions saving'),
389 category='error')
388 category='error')
390 return redirect(url('edit_user_perms', id=id))
389 raise HTTPFound(location=url('edit_user_perms', id=id))
391
390
392 def edit_emails(self, id):
391 def edit_emails(self, id):
393 c.user = self._get_user_or_raise_if_default(id)
392 c.user = self._get_user_or_raise_if_default(id)
394 c.active = 'emails'
393 c.active = 'emails'
395 c.user_email_map = UserEmailMap.query()\
394 c.user_email_map = UserEmailMap.query()\
396 .filter(UserEmailMap.user == c.user).all()
395 .filter(UserEmailMap.user == c.user).all()
397
396
398 defaults = c.user.get_dict()
397 defaults = c.user.get_dict()
399 return htmlfill.render(
398 return htmlfill.render(
400 render('admin/users/user_edit.html'),
399 render('admin/users/user_edit.html'),
401 defaults=defaults,
400 defaults=defaults,
402 encoding="UTF-8",
401 encoding="UTF-8",
403 force_defaults=False)
402 force_defaults=False)
404
403
405 def add_email(self, id):
404 def add_email(self, id):
406 """POST /user_emails:Add an existing item"""
405 """POST /user_emails:Add an existing item"""
407 # url('user_emails', id=ID, method='put')
406 # url('user_emails', id=ID, method='put')
408 user = self._get_user_or_raise_if_default(id)
407 user = self._get_user_or_raise_if_default(id)
409 email = request.POST.get('new_email')
408 email = request.POST.get('new_email')
410 user_model = UserModel()
409 user_model = UserModel()
411
410
412 try:
411 try:
413 user_model.add_extra_email(id, email)
412 user_model.add_extra_email(id, email)
414 Session().commit()
413 Session().commit()
415 h.flash(_("Added email %s to user") % email, category='success')
414 h.flash(_("Added email %s to user") % email, category='success')
416 except formencode.Invalid as error:
415 except formencode.Invalid as error:
417 msg = error.error_dict['email']
416 msg = error.error_dict['email']
418 h.flash(msg, category='error')
417 h.flash(msg, category='error')
419 except Exception:
418 except Exception:
420 log.error(traceback.format_exc())
419 log.error(traceback.format_exc())
421 h.flash(_('An error occurred during email saving'),
420 h.flash(_('An error occurred during email saving'),
422 category='error')
421 category='error')
423 return redirect(url('edit_user_emails', id=id))
422 raise HTTPFound(location=url('edit_user_emails', id=id))
424
423
425 def delete_email(self, id):
424 def delete_email(self, id):
426 """DELETE /user_emails_delete/id: Delete an existing item"""
425 """DELETE /user_emails_delete/id: Delete an existing item"""
427 # url('user_emails_delete', id=ID, method='delete')
426 # url('user_emails_delete', id=ID, method='delete')
428 user = self._get_user_or_raise_if_default(id)
427 user = self._get_user_or_raise_if_default(id)
429 email_id = request.POST.get('del_email_id')
428 email_id = request.POST.get('del_email_id')
430 user_model = UserModel()
429 user_model = UserModel()
431 user_model.delete_extra_email(id, email_id)
430 user_model.delete_extra_email(id, email_id)
432 Session().commit()
431 Session().commit()
433 h.flash(_("Removed email from user"), category='success')
432 h.flash(_("Removed email from user"), category='success')
434 return redirect(url('edit_user_emails', id=id))
433 raise HTTPFound(location=url('edit_user_emails', id=id))
435
434
436 def edit_ips(self, id):
435 def edit_ips(self, id):
437 c.user = self._get_user_or_raise_if_default(id)
436 c.user = self._get_user_or_raise_if_default(id)
438 c.active = 'ips'
437 c.active = 'ips'
439 c.user_ip_map = UserIpMap.query()\
438 c.user_ip_map = UserIpMap.query()\
440 .filter(UserIpMap.user == c.user).all()
439 .filter(UserIpMap.user == c.user).all()
441
440
442 c.inherit_default_ips = c.user.inherit_default_permissions
441 c.inherit_default_ips = c.user.inherit_default_permissions
443 c.default_user_ip_map = UserIpMap.query()\
442 c.default_user_ip_map = UserIpMap.query()\
444 .filter(UserIpMap.user == User.get_default_user()).all()
443 .filter(UserIpMap.user == User.get_default_user()).all()
445
444
446 defaults = c.user.get_dict()
445 defaults = c.user.get_dict()
447 return htmlfill.render(
446 return htmlfill.render(
448 render('admin/users/user_edit.html'),
447 render('admin/users/user_edit.html'),
449 defaults=defaults,
448 defaults=defaults,
450 encoding="UTF-8",
449 encoding="UTF-8",
451 force_defaults=False)
450 force_defaults=False)
452
451
453 def add_ip(self, id):
452 def add_ip(self, id):
454 """POST /user_ips:Add an existing item"""
453 """POST /user_ips:Add an existing item"""
455 # url('user_ips', id=ID, method='put')
454 # url('user_ips', id=ID, method='put')
456
455
457 ip = request.POST.get('new_ip')
456 ip = request.POST.get('new_ip')
458 user_model = UserModel()
457 user_model = UserModel()
459
458
460 try:
459 try:
461 user_model.add_extra_ip(id, ip)
460 user_model.add_extra_ip(id, ip)
462 Session().commit()
461 Session().commit()
463 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
462 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
464 except formencode.Invalid as error:
463 except formencode.Invalid as error:
465 msg = error.error_dict['ip']
464 msg = error.error_dict['ip']
466 h.flash(msg, category='error')
465 h.flash(msg, category='error')
467 except Exception:
466 except Exception:
468 log.error(traceback.format_exc())
467 log.error(traceback.format_exc())
469 h.flash(_('An error occurred while adding IP address'),
468 h.flash(_('An error occurred while adding IP address'),
470 category='error')
469 category='error')
471
470
472 if 'default_user' in request.POST:
471 if 'default_user' in request.POST:
473 return redirect(url('admin_permissions_ips'))
472 raise HTTPFound(location=url('admin_permissions_ips'))
474 return redirect(url('edit_user_ips', id=id))
473 raise HTTPFound(location=url('edit_user_ips', id=id))
475
474
476 def delete_ip(self, id):
475 def delete_ip(self, id):
477 """DELETE /user_ips_delete/id: Delete an existing item"""
476 """DELETE /user_ips_delete/id: Delete an existing item"""
478 # url('user_ips_delete', id=ID, method='delete')
477 # url('user_ips_delete', id=ID, method='delete')
479 ip_id = request.POST.get('del_ip_id')
478 ip_id = request.POST.get('del_ip_id')
480 user_model = UserModel()
479 user_model = UserModel()
481 user_model.delete_extra_ip(id, ip_id)
480 user_model.delete_extra_ip(id, ip_id)
482 Session().commit()
481 Session().commit()
483 h.flash(_("Removed IP address from user whitelist"), category='success')
482 h.flash(_("Removed IP address from user whitelist"), category='success')
484
483
485 if 'default_user' in request.POST:
484 if 'default_user' in request.POST:
486 return redirect(url('admin_permissions_ips'))
485 raise HTTPFound(location=url('admin_permissions_ips'))
487 return redirect(url('edit_user_ips', id=id))
486 raise HTTPFound(location=url('edit_user_ips', id=id))
@@ -1,197 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changelog
15 kallithea.controllers.changelog
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changelog controller for Kallithea
18 changelog controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons import request, url, session, tmpl_context as c
31 from pylons import request, url, session, tmpl_context as c
32 from pylons.controllers.util import redirect
33 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
34 from webob.exc import HTTPNotFound, HTTPBadRequest
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
35
34
36 import kallithea.lib.helpers as h
35 import kallithea.lib.helpers as h
37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from kallithea.lib.base import BaseRepoController, render
37 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.helpers import RepoPage
38 from kallithea.lib.helpers import RepoPage
40 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
41 from kallithea.lib.graphmod import graph_data
40 from kallithea.lib.graphmod import graph_data
42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
44 from kallithea.lib.utils2 import safe_int, safe_str
43 from kallithea.lib.utils2 import safe_int, safe_str
45
44
46
45
47 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
48
47
49
48
50 def _load_changelog_summary():
49 def _load_changelog_summary():
51 p = safe_int(request.GET.get('page'), 1)
50 p = safe_int(request.GET.get('page'), 1)
52 size = safe_int(request.GET.get('size'), 10)
51 size = safe_int(request.GET.get('size'), 10)
53
52
54 def url_generator(**kw):
53 def url_generator(**kw):
55 return url('changelog_summary_home',
54 return url('changelog_summary_home',
56 repo_name=c.db_repo.repo_name, size=size, **kw)
55 repo_name=c.db_repo.repo_name, size=size, **kw)
57
56
58 collection = c.db_repo_scm_instance
57 collection = c.db_repo_scm_instance
59
58
60 c.repo_changesets = RepoPage(collection, page=p,
59 c.repo_changesets = RepoPage(collection, page=p,
61 items_per_page=size,
60 items_per_page=size,
62 url=url_generator)
61 url=url_generator)
63 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
62 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 c.comments = c.db_repo.get_comments(page_revisions)
63 c.comments = c.db_repo.get_comments(page_revisions)
65 c.statuses = c.db_repo.statuses(page_revisions)
64 c.statuses = c.db_repo.statuses(page_revisions)
66
65
67
66
68 class ChangelogController(BaseRepoController):
67 class ChangelogController(BaseRepoController):
69
68
70 def __before__(self):
69 def __before__(self):
71 super(ChangelogController, self).__before__()
70 super(ChangelogController, self).__before__()
72 c.affected_files_cut_off = 60
71 c.affected_files_cut_off = 60
73
72
74 @staticmethod
73 @staticmethod
75 def __get_cs(rev, repo):
74 def __get_cs(rev, repo):
76 """
75 """
77 Safe way to get changeset. If error occur fail with error message.
76 Safe way to get changeset. If error occur fail with error message.
78
77
79 :param rev: revision to fetch
78 :param rev: revision to fetch
80 :param repo: repo instance
79 :param repo: repo instance
81 """
80 """
82
81
83 try:
82 try:
84 return c.db_repo_scm_instance.get_changeset(rev)
83 return c.db_repo_scm_instance.get_changeset(rev)
85 except EmptyRepositoryError as e:
84 except EmptyRepositoryError as e:
86 h.flash(h.literal(_('There are no changesets yet')),
85 h.flash(h.literal(_('There are no changesets yet')),
87 category='error')
86 category='error')
88 except RepositoryError as e:
87 except RepositoryError as e:
89 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
90 h.flash(safe_str(e), category='error')
89 h.flash(safe_str(e), category='error')
91 raise HTTPBadRequest()
90 raise HTTPBadRequest()
92
91
93 @LoginRequired()
92 @LoginRequired()
94 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
93 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
95 'repository.admin')
94 'repository.admin')
96 def index(self, repo_name, revision=None, f_path=None):
95 def index(self, repo_name, revision=None, f_path=None):
97 # Fix URL after page size form submission via GET
96 # Fix URL after page size form submission via GET
98 # TODO: Somehow just don't send this extra junk in the GET URL
97 # TODO: Somehow just don't send this extra junk in the GET URL
99 if request.GET.get('set'):
98 if request.GET.get('set'):
100 request.GET.pop('set', None)
99 request.GET.pop('set', None)
101 if revision is None:
100 if revision is None:
102 return redirect(url('changelog_home', repo_name=repo_name, **request.GET))
101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
103 return redirect(url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
104
103
105 limit = 2000
104 limit = 2000
106 default = 100
105 default = 100
107 if request.GET.get('size'):
106 if request.GET.get('size'):
108 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
109 session['changelog_size'] = c.size
108 session['changelog_size'] = c.size
110 session.save()
109 session.save()
111 else:
110 else:
112 c.size = int(session.get('changelog_size', default))
111 c.size = int(session.get('changelog_size', default))
113 # min size must be 1
112 # min size must be 1
114 c.size = max(c.size, 1)
113 c.size = max(c.size, 1)
115 p = safe_int(request.GET.get('page', 1), 1)
114 p = safe_int(request.GET.get('page', 1), 1)
116 branch_name = request.GET.get('branch', None)
115 branch_name = request.GET.get('branch', None)
117 if (branch_name and
116 if (branch_name and
118 branch_name not in c.db_repo_scm_instance.branches and
117 branch_name not in c.db_repo_scm_instance.branches and
119 branch_name not in c.db_repo_scm_instance.closed_branches and
118 branch_name not in c.db_repo_scm_instance.closed_branches and
120 not revision):
119 not revision):
121 return redirect(url('changelog_file_home', repo_name=c.repo_name,
120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
122 revision=branch_name, f_path=f_path or ''))
121 revision=branch_name, f_path=f_path or ''))
123
122
124 if revision == 'tip':
123 if revision == 'tip':
125 revision = None
124 revision = None
126
125
127 c.changelog_for_path = f_path
126 c.changelog_for_path = f_path
128 try:
127 try:
129
128
130 if f_path:
129 if f_path:
131 log.debug('generating changelog for path %s', f_path)
130 log.debug('generating changelog for path %s', f_path)
132 # get the history for the file !
131 # get the history for the file !
133 tip_cs = c.db_repo_scm_instance.get_changeset()
132 tip_cs = c.db_repo_scm_instance.get_changeset()
134 try:
133 try:
135 collection = tip_cs.get_file_history(f_path)
134 collection = tip_cs.get_file_history(f_path)
136 except (NodeDoesNotExistError, ChangesetError):
135 except (NodeDoesNotExistError, ChangesetError):
137 #this node is not present at tip !
136 #this node is not present at tip !
138 try:
137 try:
139 cs = self.__get_cs(revision, repo_name)
138 cs = self.__get_cs(revision, repo_name)
140 collection = cs.get_file_history(f_path)
139 collection = cs.get_file_history(f_path)
141 except RepositoryError as e:
140 except RepositoryError as e:
142 h.flash(safe_str(e), category='warning')
141 h.flash(safe_str(e), category='warning')
143 redirect(h.url('changelog_home', repo_name=repo_name))
142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
144 collection = list(reversed(collection))
143 collection = list(reversed(collection))
145 else:
144 else:
146 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
147 branch_name=branch_name)
146 branch_name=branch_name)
148 c.total_cs = len(collection)
147 c.total_cs = len(collection)
149
148
150 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
151 items_per_page=c.size, branch=branch_name,)
150 items_per_page=c.size, branch=branch_name,)
152
151
153 page_revisions = [x.raw_id for x in c.pagination]
152 page_revisions = [x.raw_id for x in c.pagination]
154 c.comments = c.db_repo.get_comments(page_revisions)
153 c.comments = c.db_repo.get_comments(page_revisions)
155 c.statuses = c.db_repo.statuses(page_revisions)
154 c.statuses = c.db_repo.statuses(page_revisions)
156 except EmptyRepositoryError as e:
155 except EmptyRepositoryError as e:
157 h.flash(safe_str(e), category='warning')
156 h.flash(safe_str(e), category='warning')
158 return redirect(url('summary_home', repo_name=c.repo_name))
157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
159 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
160 log.error(traceback.format_exc())
159 log.error(traceback.format_exc())
161 h.flash(safe_str(e), category='error')
160 h.flash(safe_str(e), category='error')
162 return redirect(url('changelog_home', repo_name=c.repo_name))
161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
163
162
164 c.branch_name = branch_name
163 c.branch_name = branch_name
165 c.branch_filters = [('', _('None'))] + \
164 c.branch_filters = [('', _('None'))] + \
166 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
167 if c.db_repo_scm_instance.closed_branches:
166 if c.db_repo_scm_instance.closed_branches:
168 prefix = _('(closed)') + ' '
167 prefix = _('(closed)') + ' '
169 c.branch_filters += [('-', '-')] + \
168 c.branch_filters += [('-', '-')] + \
170 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
171 revs = []
170 revs = []
172 if not f_path:
171 if not f_path:
173 revs = [x.revision for x in c.pagination]
172 revs = [x.revision for x in c.pagination]
174 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
173 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
175
174
176 c.revision = revision # requested revision ref
175 c.revision = revision # requested revision ref
177 c.first_revision = c.pagination[0] # pagination is never empty here!
176 c.first_revision = c.pagination[0] # pagination is never empty here!
178 return render('changelog/changelog.html')
177 return render('changelog/changelog.html')
179
178
180 @LoginRequired()
179 @LoginRequired()
181 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
182 'repository.admin')
181 'repository.admin')
183 def changelog_details(self, cs):
182 def changelog_details(self, cs):
184 if request.environ.get('HTTP_X_PARTIAL_XHR'):
183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
185 c.cs = c.db_repo_scm_instance.get_changeset(cs)
184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
186 return render('changelog/changelog_details.html')
185 return render('changelog/changelog_details.html')
187 raise HTTPNotFound()
186 raise HTTPNotFound()
188
187
189 @LoginRequired()
188 @LoginRequired()
190 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 'repository.admin')
190 'repository.admin')
192 def changelog_summary(self, repo_name):
191 def changelog_summary(self, repo_name):
193 if request.environ.get('HTTP_X_PARTIAL_XHR'):
192 if request.environ.get('HTTP_X_PARTIAL_XHR'):
194 _load_changelog_summary()
193 _load_changelog_summary()
195
194
196 return render('changelog/changelog_summary_data.html')
195 return render('changelog/changelog_summary_data.html')
197 raise HTTPNotFound()
196 raise HTTPNotFound()
@@ -1,477 +1,476 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changeset
15 kallithea.controllers.changeset
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changeset controller for pylons showing changes between
18 changeset controller for pylons showing changes between
19 revisions
19 revisions
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Apr 25, 2010
23 :created_on: Apr 25, 2010
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 import logging
29 import logging
30 import traceback
30 import traceback
31 from collections import defaultdict
31 from collections import defaultdict
32 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
33
32
34 from pylons import tmpl_context as c, request, response
33 from pylons import tmpl_context as c, request, response
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
35 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
36
37 from kallithea.lib.utils import jsonify
37 from kallithea.lib.utils import jsonify
38
39 from kallithea.lib.vcs.exceptions import RepositoryError, \
38 from kallithea.lib.vcs.exceptions import RepositoryError, \
40 ChangesetDoesNotExistError
39 ChangesetDoesNotExistError
41
40
42 from kallithea.lib.compat import json
41 from kallithea.lib.compat import json
43 import kallithea.lib.helpers as h
42 import kallithea.lib.helpers as h
44 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
43 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
45 NotAnonymous
44 NotAnonymous
46 from kallithea.lib.base import BaseRepoController, render
45 from kallithea.lib.base import BaseRepoController, render
47 from kallithea.lib.utils import action_logger
46 from kallithea.lib.utils import action_logger
48 from kallithea.lib.compat import OrderedDict
47 from kallithea.lib.compat import OrderedDict
49 from kallithea.lib import diffs
48 from kallithea.lib import diffs
50 from kallithea.model.db import ChangesetComment, ChangesetStatus
49 from kallithea.model.db import ChangesetComment, ChangesetStatus
51 from kallithea.model.comment import ChangesetCommentsModel
50 from kallithea.model.comment import ChangesetCommentsModel
52 from kallithea.model.changeset_status import ChangesetStatusModel
51 from kallithea.model.changeset_status import ChangesetStatusModel
53 from kallithea.model.meta import Session
52 from kallithea.model.meta import Session
54 from kallithea.model.repo import RepoModel
53 from kallithea.model.repo import RepoModel
55 from kallithea.lib.diffs import LimitedDiffContainer
54 from kallithea.lib.diffs import LimitedDiffContainer
56 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
55 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
57 from kallithea.lib.vcs.backends.base import EmptyChangeset
56 from kallithea.lib.vcs.backends.base import EmptyChangeset
58 from kallithea.lib.utils2 import safe_unicode
57 from kallithea.lib.utils2 import safe_unicode
59 from kallithea.lib.graphmod import graph_data
58 from kallithea.lib.graphmod import graph_data
60
59
61 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
62
61
63
62
64 def _update_with_GET(params, GET):
63 def _update_with_GET(params, GET):
65 for k in ['diff1', 'diff2', 'diff']:
64 for k in ['diff1', 'diff2', 'diff']:
66 params[k] += GET.getall(k)
65 params[k] += GET.getall(k)
67
66
68
67
69 def anchor_url(revision, path, GET):
68 def anchor_url(revision, path, GET):
70 fid = h.FID(revision, path)
69 fid = h.FID(revision, path)
71 return h.url.current(anchor=fid, **dict(GET))
70 return h.url.current(anchor=fid, **dict(GET))
72
71
73
72
74 def get_ignore_ws(fid, GET):
73 def get_ignore_ws(fid, GET):
75 ig_ws_global = GET.get('ignorews')
74 ig_ws_global = GET.get('ignorews')
76 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
75 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
77 if ig_ws:
76 if ig_ws:
78 try:
77 try:
79 return int(ig_ws[0].split(':')[-1])
78 return int(ig_ws[0].split(':')[-1])
80 except ValueError:
79 except ValueError:
81 raise HTTPBadRequest()
80 raise HTTPBadRequest()
82 return ig_ws_global
81 return ig_ws_global
83
82
84
83
85 def _ignorews_url(GET, fileid=None):
84 def _ignorews_url(GET, fileid=None):
86 fileid = str(fileid) if fileid else None
85 fileid = str(fileid) if fileid else None
87 params = defaultdict(list)
86 params = defaultdict(list)
88 _update_with_GET(params, GET)
87 _update_with_GET(params, GET)
89 lbl = _('Show whitespace')
88 lbl = _('Show whitespace')
90 ig_ws = get_ignore_ws(fileid, GET)
89 ig_ws = get_ignore_ws(fileid, GET)
91 ln_ctx = get_line_ctx(fileid, GET)
90 ln_ctx = get_line_ctx(fileid, GET)
92 # global option
91 # global option
93 if fileid is None:
92 if fileid is None:
94 if ig_ws is None:
93 if ig_ws is None:
95 params['ignorews'] += [1]
94 params['ignorews'] += [1]
96 lbl = _('Ignore whitespace')
95 lbl = _('Ignore whitespace')
97 ctx_key = 'context'
96 ctx_key = 'context'
98 ctx_val = ln_ctx
97 ctx_val = ln_ctx
99 # per file options
98 # per file options
100 else:
99 else:
101 if ig_ws is None:
100 if ig_ws is None:
102 params[fileid] += ['WS:1']
101 params[fileid] += ['WS:1']
103 lbl = _('Ignore whitespace')
102 lbl = _('Ignore whitespace')
104
103
105 ctx_key = fileid
104 ctx_key = fileid
106 ctx_val = 'C:%s' % ln_ctx
105 ctx_val = 'C:%s' % ln_ctx
107 # if we have passed in ln_ctx pass it along to our params
106 # if we have passed in ln_ctx pass it along to our params
108 if ln_ctx:
107 if ln_ctx:
109 params[ctx_key] += [ctx_val]
108 params[ctx_key] += [ctx_val]
110
109
111 params['anchor'] = fileid
110 params['anchor'] = fileid
112 icon = h.literal('<i class="icon-strike"></i>')
111 icon = h.literal('<i class="icon-strike"></i>')
113 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
112 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
114
113
115
114
116 def get_line_ctx(fid, GET):
115 def get_line_ctx(fid, GET):
117 ln_ctx_global = GET.get('context')
116 ln_ctx_global = GET.get('context')
118 if fid:
117 if fid:
119 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
118 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
120 else:
119 else:
121 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
120 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
122 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
121 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
123 if ln_ctx:
122 if ln_ctx:
124 ln_ctx = [ln_ctx]
123 ln_ctx = [ln_ctx]
125
124
126 if ln_ctx:
125 if ln_ctx:
127 retval = ln_ctx[0].split(':')[-1]
126 retval = ln_ctx[0].split(':')[-1]
128 else:
127 else:
129 retval = ln_ctx_global
128 retval = ln_ctx_global
130
129
131 try:
130 try:
132 return int(retval)
131 return int(retval)
133 except Exception:
132 except Exception:
134 return 3
133 return 3
135
134
136
135
137 def _context_url(GET, fileid=None):
136 def _context_url(GET, fileid=None):
138 """
137 """
139 Generates url for context lines
138 Generates url for context lines
140
139
141 :param fileid:
140 :param fileid:
142 """
141 """
143
142
144 fileid = str(fileid) if fileid else None
143 fileid = str(fileid) if fileid else None
145 ig_ws = get_ignore_ws(fileid, GET)
144 ig_ws = get_ignore_ws(fileid, GET)
146 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
145 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
147
146
148 params = defaultdict(list)
147 params = defaultdict(list)
149 _update_with_GET(params, GET)
148 _update_with_GET(params, GET)
150
149
151 # global option
150 # global option
152 if fileid is None:
151 if fileid is None:
153 if ln_ctx > 0:
152 if ln_ctx > 0:
154 params['context'] += [ln_ctx]
153 params['context'] += [ln_ctx]
155
154
156 if ig_ws:
155 if ig_ws:
157 ig_ws_key = 'ignorews'
156 ig_ws_key = 'ignorews'
158 ig_ws_val = 1
157 ig_ws_val = 1
159
158
160 # per file option
159 # per file option
161 else:
160 else:
162 params[fileid] += ['C:%s' % ln_ctx]
161 params[fileid] += ['C:%s' % ln_ctx]
163 ig_ws_key = fileid
162 ig_ws_key = fileid
164 ig_ws_val = 'WS:%s' % 1
163 ig_ws_val = 'WS:%s' % 1
165
164
166 if ig_ws:
165 if ig_ws:
167 params[ig_ws_key] += [ig_ws_val]
166 params[ig_ws_key] += [ig_ws_val]
168
167
169 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
168 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
170
169
171 params['anchor'] = fileid
170 params['anchor'] = fileid
172 icon = h.literal('<i class="icon-sort"></i>')
171 icon = h.literal('<i class="icon-sort"></i>')
173 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
172 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
174
173
175
174
176 class ChangesetController(BaseRepoController):
175 class ChangesetController(BaseRepoController):
177
176
178 def __before__(self):
177 def __before__(self):
179 super(ChangesetController, self).__before__()
178 super(ChangesetController, self).__before__()
180 c.affected_files_cut_off = 60
179 c.affected_files_cut_off = 60
181
180
182 def __load_data(self):
181 def __load_data(self):
183 repo_model = RepoModel()
182 repo_model = RepoModel()
184 c.users_array = repo_model.get_users_js()
183 c.users_array = repo_model.get_users_js()
185 c.user_groups_array = repo_model.get_user_groups_js()
184 c.user_groups_array = repo_model.get_user_groups_js()
186
185
187 def _index(self, revision, method):
186 def _index(self, revision, method):
188 c.anchor_url = anchor_url
187 c.anchor_url = anchor_url
189 c.ignorews_url = _ignorews_url
188 c.ignorews_url = _ignorews_url
190 c.context_url = _context_url
189 c.context_url = _context_url
191 c.fulldiff = fulldiff = request.GET.get('fulldiff')
190 c.fulldiff = fulldiff = request.GET.get('fulldiff')
192 #get ranges of revisions if preset
191 #get ranges of revisions if preset
193 rev_range = revision.split('...')[:2]
192 rev_range = revision.split('...')[:2]
194 enable_comments = True
193 enable_comments = True
195 c.cs_repo = c.db_repo
194 c.cs_repo = c.db_repo
196 try:
195 try:
197 if len(rev_range) == 2:
196 if len(rev_range) == 2:
198 enable_comments = False
197 enable_comments = False
199 rev_start = rev_range[0]
198 rev_start = rev_range[0]
200 rev_end = rev_range[1]
199 rev_end = rev_range[1]
201 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
200 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
202 end=rev_end)
201 end=rev_end)
203 else:
202 else:
204 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
203 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
205
204
206 c.cs_ranges = list(rev_ranges)
205 c.cs_ranges = list(rev_ranges)
207 if not c.cs_ranges:
206 if not c.cs_ranges:
208 raise RepositoryError('Changeset range returned empty result')
207 raise RepositoryError('Changeset range returned empty result')
209
208
210 except(ChangesetDoesNotExistError,), e:
209 except(ChangesetDoesNotExistError,), e:
211 log.debug(traceback.format_exc())
210 log.debug(traceback.format_exc())
212 msg = _('Such revision does not exist for this repository')
211 msg = _('Such revision does not exist for this repository')
213 h.flash(msg, category='error')
212 h.flash(msg, category='error')
214 raise HTTPNotFound()
213 raise HTTPNotFound()
215
214
216 c.changes = OrderedDict()
215 c.changes = OrderedDict()
217
216
218 c.lines_added = 0 # count of lines added
217 c.lines_added = 0 # count of lines added
219 c.lines_deleted = 0 # count of lines removes
218 c.lines_deleted = 0 # count of lines removes
220
219
221 c.changeset_statuses = ChangesetStatus.STATUSES
220 c.changeset_statuses = ChangesetStatus.STATUSES
222 comments = dict()
221 comments = dict()
223 c.statuses = []
222 c.statuses = []
224 c.inline_comments = []
223 c.inline_comments = []
225 c.inline_cnt = 0
224 c.inline_cnt = 0
226
225
227 # Iterate over ranges (default changeset view is always one changeset)
226 # Iterate over ranges (default changeset view is always one changeset)
228 for changeset in c.cs_ranges:
227 for changeset in c.cs_ranges:
229 inlines = []
228 inlines = []
230 if method == 'show':
229 if method == 'show':
231 c.statuses.extend([ChangesetStatusModel().get_status(
230 c.statuses.extend([ChangesetStatusModel().get_status(
232 c.db_repo.repo_id, changeset.raw_id)])
231 c.db_repo.repo_id, changeset.raw_id)])
233
232
234 # Changeset comments
233 # Changeset comments
235 comments.update((com.comment_id, com)
234 comments.update((com.comment_id, com)
236 for com in ChangesetCommentsModel()
235 for com in ChangesetCommentsModel()
237 .get_comments(c.db_repo.repo_id,
236 .get_comments(c.db_repo.repo_id,
238 revision=changeset.raw_id))
237 revision=changeset.raw_id))
239
238
240 # Status change comments - mostly from pull requests
239 # Status change comments - mostly from pull requests
241 comments.update((st.changeset_comment_id, st.comment)
240 comments.update((st.changeset_comment_id, st.comment)
242 for st in ChangesetStatusModel()
241 for st in ChangesetStatusModel()
243 .get_statuses(c.db_repo.repo_id,
242 .get_statuses(c.db_repo.repo_id,
244 changeset.raw_id, with_revisions=True)
243 changeset.raw_id, with_revisions=True)
245 if st.changeset_comment_id is not None)
244 if st.changeset_comment_id is not None)
246
245
247 inlines = ChangesetCommentsModel()\
246 inlines = ChangesetCommentsModel()\
248 .get_inline_comments(c.db_repo.repo_id,
247 .get_inline_comments(c.db_repo.repo_id,
249 revision=changeset.raw_id)
248 revision=changeset.raw_id)
250 c.inline_comments.extend(inlines)
249 c.inline_comments.extend(inlines)
251
250
252 c.changes[changeset.raw_id] = []
251 c.changes[changeset.raw_id] = []
253
252
254 cs2 = changeset.raw_id
253 cs2 = changeset.raw_id
255 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
254 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
256 context_lcl = get_line_ctx('', request.GET)
255 context_lcl = get_line_ctx('', request.GET)
257 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
256 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
258
257
259 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
258 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
260 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
259 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
261 diff_limit = self.cut_off_limit if not fulldiff else None
260 diff_limit = self.cut_off_limit if not fulldiff else None
262 diff_processor = diffs.DiffProcessor(_diff,
261 diff_processor = diffs.DiffProcessor(_diff,
263 vcs=c.db_repo_scm_instance.alias,
262 vcs=c.db_repo_scm_instance.alias,
264 format='gitdiff',
263 format='gitdiff',
265 diff_limit=diff_limit)
264 diff_limit=diff_limit)
266 cs_changes = OrderedDict()
265 cs_changes = OrderedDict()
267 if method == 'show':
266 if method == 'show':
268 _parsed = diff_processor.prepare()
267 _parsed = diff_processor.prepare()
269 c.limited_diff = False
268 c.limited_diff = False
270 if isinstance(_parsed, LimitedDiffContainer):
269 if isinstance(_parsed, LimitedDiffContainer):
271 c.limited_diff = True
270 c.limited_diff = True
272 for f in _parsed:
271 for f in _parsed:
273 st = f['stats']
272 st = f['stats']
274 c.lines_added += st['added']
273 c.lines_added += st['added']
275 c.lines_deleted += st['deleted']
274 c.lines_deleted += st['deleted']
276 fid = h.FID(changeset.raw_id, f['filename'])
275 fid = h.FID(changeset.raw_id, f['filename'])
277 diff = diff_processor.as_html(enable_comments=enable_comments,
276 diff = diff_processor.as_html(enable_comments=enable_comments,
278 parsed_lines=[f])
277 parsed_lines=[f])
279 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
278 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
280 diff, st]
279 diff, st]
281 else:
280 else:
282 # downloads/raw we only need RAW diff nothing else
281 # downloads/raw we only need RAW diff nothing else
283 diff = diff_processor.as_raw()
282 diff = diff_processor.as_raw()
284 cs_changes[''] = [None, None, None, None, diff, None]
283 cs_changes[''] = [None, None, None, None, diff, None]
285 c.changes[changeset.raw_id] = cs_changes
284 c.changes[changeset.raw_id] = cs_changes
286
285
287 #sort comments in creation order
286 #sort comments in creation order
288 c.comments = [com for com_id, com in sorted(comments.items())]
287 c.comments = [com for com_id, com in sorted(comments.items())]
289
288
290 # count inline comments
289 # count inline comments
291 for __, lines in c.inline_comments:
290 for __, lines in c.inline_comments:
292 for comments in lines.values():
291 for comments in lines.values():
293 c.inline_cnt += len(comments)
292 c.inline_cnt += len(comments)
294
293
295 if len(c.cs_ranges) == 1:
294 if len(c.cs_ranges) == 1:
296 c.changeset = c.cs_ranges[0]
295 c.changeset = c.cs_ranges[0]
297 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
296 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
298 for x in c.changeset.parents])
297 for x in c.changeset.parents])
299 if method == 'download':
298 if method == 'download':
300 response.content_type = 'text/plain'
299 response.content_type = 'text/plain'
301 response.content_disposition = 'attachment; filename=%s.diff' \
300 response.content_disposition = 'attachment; filename=%s.diff' \
302 % revision[:12]
301 % revision[:12]
303 return diff
302 return diff
304 elif method == 'patch':
303 elif method == 'patch':
305 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
306 c.diff = safe_unicode(diff)
305 c.diff = safe_unicode(diff)
307 return render('changeset/patch_changeset.html')
306 return render('changeset/patch_changeset.html')
308 elif method == 'raw':
307 elif method == 'raw':
309 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
310 return diff
309 return diff
311 elif method == 'show':
310 elif method == 'show':
312 self.__load_data()
311 self.__load_data()
313 if len(c.cs_ranges) == 1:
312 if len(c.cs_ranges) == 1:
314 return render('changeset/changeset.html')
313 return render('changeset/changeset.html')
315 else:
314 else:
316 c.cs_ranges_org = None
315 c.cs_ranges_org = None
317 c.cs_comments = {}
316 c.cs_comments = {}
318 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
317 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
319 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
318 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
320 return render('changeset/changeset_range.html')
319 return render('changeset/changeset_range.html')
321
320
322 @LoginRequired()
321 @LoginRequired()
323 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
324 'repository.admin')
323 'repository.admin')
325 def index(self, revision, method='show'):
324 def index(self, revision, method='show'):
326 return self._index(revision, method=method)
325 return self._index(revision, method=method)
327
326
328 @LoginRequired()
327 @LoginRequired()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
328 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
329 'repository.admin')
331 def changeset_raw(self, revision):
330 def changeset_raw(self, revision):
332 return self._index(revision, method='raw')
331 return self._index(revision, method='raw')
333
332
334 @LoginRequired()
333 @LoginRequired()
335 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
336 'repository.admin')
335 'repository.admin')
337 def changeset_patch(self, revision):
336 def changeset_patch(self, revision):
338 return self._index(revision, method='patch')
337 return self._index(revision, method='patch')
339
338
340 @LoginRequired()
339 @LoginRequired()
341 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
342 'repository.admin')
341 'repository.admin')
343 def changeset_download(self, revision):
342 def changeset_download(self, revision):
344 return self._index(revision, method='download')
343 return self._index(revision, method='download')
345
344
346 @LoginRequired()
345 @LoginRequired()
347 @NotAnonymous()
346 @NotAnonymous()
348 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
349 'repository.admin')
348 'repository.admin')
350 @jsonify
349 @jsonify
351 def comment(self, repo_name, revision):
350 def comment(self, repo_name, revision):
352 status = request.POST.get('changeset_status')
351 status = request.POST.get('changeset_status')
353 text = request.POST.get('text', '').strip()
352 text = request.POST.get('text', '').strip()
354
353
355 c.comment = comment = ChangesetCommentsModel().create(
354 c.comment = comment = ChangesetCommentsModel().create(
356 text=text,
355 text=text,
357 repo=c.db_repo.repo_id,
356 repo=c.db_repo.repo_id,
358 user=c.authuser.user_id,
357 user=c.authuser.user_id,
359 revision=revision,
358 revision=revision,
360 f_path=request.POST.get('f_path'),
359 f_path=request.POST.get('f_path'),
361 line_no=request.POST.get('line'),
360 line_no=request.POST.get('line'),
362 status_change=(ChangesetStatus.get_status_lbl(status)
361 status_change=(ChangesetStatus.get_status_lbl(status)
363 if status else None)
362 if status else None)
364 )
363 )
365
364
366 # get status if set !
365 # get status if set !
367 if status:
366 if status:
368 # if latest status was from pull request and it's closed
367 # if latest status was from pull request and it's closed
369 # disallow changing status !
368 # disallow changing status !
370 # dont_allow_on_closed_pull_request = True !
369 # dont_allow_on_closed_pull_request = True !
371
370
372 try:
371 try:
373 ChangesetStatusModel().set_status(
372 ChangesetStatusModel().set_status(
374 c.db_repo.repo_id,
373 c.db_repo.repo_id,
375 status,
374 status,
376 c.authuser.user_id,
375 c.authuser.user_id,
377 comment,
376 comment,
378 revision=revision,
377 revision=revision,
379 dont_allow_on_closed_pull_request=True
378 dont_allow_on_closed_pull_request=True
380 )
379 )
381 except StatusChangeOnClosedPullRequestError:
380 except StatusChangeOnClosedPullRequestError:
382 log.debug(traceback.format_exc())
381 log.debug(traceback.format_exc())
383 msg = _('Changing status on a changeset associated with '
382 msg = _('Changing status on a changeset associated with '
384 'a closed pull request is not allowed')
383 'a closed pull request is not allowed')
385 h.flash(msg, category='warning')
384 h.flash(msg, category='warning')
386 return redirect(h.url('changeset_home', repo_name=repo_name,
385 raise HTTPFound(location=h.url('changeset_home', repo_name=repo_name,
387 revision=revision))
386 revision=revision))
388 action_logger(self.authuser,
387 action_logger(self.authuser,
389 'user_commented_revision:%s' % revision,
388 'user_commented_revision:%s' % revision,
390 c.db_repo, self.ip_addr, self.sa)
389 c.db_repo, self.ip_addr, self.sa)
391
390
392 Session().commit()
391 Session().commit()
393
392
394 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
393 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
395 return redirect(h.url('changeset_home', repo_name=repo_name,
394 raise HTTPFound(location=h.url('changeset_home', repo_name=repo_name,
396 revision=revision))
395 revision=revision))
397 #only ajax below
396 #only ajax below
398 data = {
397 data = {
399 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
398 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 }
399 }
401 if comment is not None:
400 if comment is not None:
402 data.update(comment.get_dict())
401 data.update(comment.get_dict())
403 data.update({'rendered_text':
402 data.update({'rendered_text':
404 render('changeset/changeset_comment_block.html')})
403 render('changeset/changeset_comment_block.html')})
405
404
406 return data
405 return data
407
406
408 @LoginRequired()
407 @LoginRequired()
409 @NotAnonymous()
408 @NotAnonymous()
410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 'repository.admin')
410 'repository.admin')
412 def preview_comment(self):
411 def preview_comment(self):
413 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
412 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
414 raise HTTPBadRequest()
413 raise HTTPBadRequest()
415 text = request.POST.get('text')
414 text = request.POST.get('text')
416 if text:
415 if text:
417 return h.rst_w_mentions(text)
416 return h.rst_w_mentions(text)
418 return ''
417 return ''
419
418
420 @LoginRequired()
419 @LoginRequired()
421 @NotAnonymous()
420 @NotAnonymous()
422 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
423 'repository.admin')
422 'repository.admin')
424 @jsonify
423 @jsonify
425 def delete_comment(self, repo_name, comment_id):
424 def delete_comment(self, repo_name, comment_id):
426 co = ChangesetComment.get(comment_id)
425 co = ChangesetComment.get(comment_id)
427 if not co:
426 if not co:
428 raise HTTPBadRequest()
427 raise HTTPBadRequest()
429 owner = co.author.user_id == c.authuser.user_id
428 owner = co.author.user_id == c.authuser.user_id
430 repo_admin = h.HasRepoPermissionAny('repository.admin')
429 repo_admin = h.HasRepoPermissionAny('repository.admin')
431 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
430 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
432 ChangesetCommentsModel().delete(comment=co)
431 ChangesetCommentsModel().delete(comment=co)
433 Session().commit()
432 Session().commit()
434 return True
433 return True
435 else:
434 else:
436 raise HTTPForbidden()
435 raise HTTPForbidden()
437
436
438 @LoginRequired()
437 @LoginRequired()
439 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
438 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
440 'repository.admin')
439 'repository.admin')
441 @jsonify
440 @jsonify
442 def changeset_info(self, repo_name, revision):
441 def changeset_info(self, repo_name, revision):
443 if request.is_xhr:
442 if request.is_xhr:
444 try:
443 try:
445 return c.db_repo_scm_instance.get_changeset(revision)
444 return c.db_repo_scm_instance.get_changeset(revision)
446 except ChangesetDoesNotExistError as e:
445 except ChangesetDoesNotExistError as e:
447 return EmptyChangeset(message=str(e))
446 return EmptyChangeset(message=str(e))
448 else:
447 else:
449 raise HTTPBadRequest()
448 raise HTTPBadRequest()
450
449
451 @LoginRequired()
450 @LoginRequired()
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
451 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
453 'repository.admin')
452 'repository.admin')
454 @jsonify
453 @jsonify
455 def changeset_children(self, repo_name, revision):
454 def changeset_children(self, repo_name, revision):
456 if request.is_xhr:
455 if request.is_xhr:
457 changeset = c.db_repo_scm_instance.get_changeset(revision)
456 changeset = c.db_repo_scm_instance.get_changeset(revision)
458 result = {"results": []}
457 result = {"results": []}
459 if changeset.children:
458 if changeset.children:
460 result = {"results": changeset.children}
459 result = {"results": changeset.children}
461 return result
460 return result
462 else:
461 else:
463 raise HTTPBadRequest()
462 raise HTTPBadRequest()
464
463
465 @LoginRequired()
464 @LoginRequired()
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 'repository.admin')
466 'repository.admin')
468 @jsonify
467 @jsonify
469 def changeset_parents(self, repo_name, revision):
468 def changeset_parents(self, repo_name, revision):
470 if request.is_xhr:
469 if request.is_xhr:
471 changeset = c.db_repo_scm_instance.get_changeset(revision)
470 changeset = c.db_repo_scm_instance.get_changeset(revision)
472 result = {"results": []}
471 result = {"results": []}
473 if changeset.parents:
472 if changeset.parents:
474 result = {"results": changeset.parents}
473 result = {"results": changeset.parents}
475 return result
474 return result
476 else:
475 else:
477 raise HTTPBadRequest()
476 raise HTTPBadRequest()
@@ -1,296 +1,295 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.compare
15 kallithea.controllers.compare
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 compare controller for pylons showing differences between two
18 compare controller for pylons showing differences between two
19 repos, branches, bookmarks or tips
19 repos, branches, bookmarks or tips
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: May 6, 2012
23 :created_on: May 6, 2012
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29
29
30 import logging
30 import logging
31 import re
31 import re
32
32
33 from webob.exc import HTTPBadRequest
34 from pylons import request, tmpl_context as c, url
33 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPBadRequest
37
36
38 from kallithea.lib.utils2 import safe_str
37 from kallithea.lib.utils2 import safe_str
39 from kallithea.lib.vcs.utils.hgcompat import unionrepo
38 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 from kallithea.lib import helpers as h
39 from kallithea.lib import helpers as h
41 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 from kallithea.lib import diffs
42 from kallithea.lib import diffs
44 from kallithea.model.db import Repository
43 from kallithea.model.db import Repository
45 from kallithea.lib.diffs import LimitedDiffContainer
44 from kallithea.lib.diffs import LimitedDiffContainer
46 from kallithea.controllers.changeset import _ignorews_url, _context_url
45 from kallithea.controllers.changeset import _ignorews_url, _context_url
47 from kallithea.lib.graphmod import graph_data
46 from kallithea.lib.graphmod import graph_data
48 from kallithea.lib.compat import json
47 from kallithea.lib.compat import json
49
48
50 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
51
50
52
51
53 class CompareController(BaseRepoController):
52 class CompareController(BaseRepoController):
54
53
55 def __before__(self):
54 def __before__(self):
56 super(CompareController, self).__before__()
55 super(CompareController, self).__before__()
57
56
58 @staticmethod
57 @staticmethod
59 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
58 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
60 """
59 """
61 Returns lists of changesets that can be merged from org_repo@org_rev
60 Returns lists of changesets that can be merged from org_repo@org_rev
62 to other_repo@other_rev
61 to other_repo@other_rev
63 ... and the other way
62 ... and the other way
64 ... and the ancestor that would be used for merge
63 ... and the ancestor that would be used for merge
65
64
66 :param org_repo: repo object, that is most likely the original repo we forked from
65 :param org_repo: repo object, that is most likely the original repo we forked from
67 :param org_rev: the revision we want our compare to be made
66 :param org_rev: the revision we want our compare to be made
68 :param other_repo: repo object, most likely the fork of org_repo. It has
67 :param other_repo: repo object, most likely the fork of org_repo. It has
69 all changesets that we need to obtain
68 all changesets that we need to obtain
70 :param other_rev: revision we want out compare to be made on other_repo
69 :param other_rev: revision we want out compare to be made on other_repo
71 """
70 """
72 ancestor = None
71 ancestor = None
73 if org_rev == other_rev:
72 if org_rev == other_rev:
74 org_changesets = []
73 org_changesets = []
75 other_changesets = []
74 other_changesets = []
76 ancestor = org_rev
75 ancestor = org_rev
77
76
78 elif alias == 'hg':
77 elif alias == 'hg':
79 #case two independent repos
78 #case two independent repos
80 if org_repo != other_repo:
79 if org_repo != other_repo:
81 hgrepo = unionrepo.unionrepository(other_repo.baseui,
80 hgrepo = unionrepo.unionrepository(other_repo.baseui,
82 other_repo.path,
81 other_repo.path,
83 org_repo.path)
82 org_repo.path)
84 # all ancestors of other_rev will be in other_repo and
83 # all ancestors of other_rev will be in other_repo and
85 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
84 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
86
85
87 #no remote compare do it on the same repository
86 #no remote compare do it on the same repository
88 else:
87 else:
89 hgrepo = other_repo._repo
88 hgrepo = other_repo._repo
90
89
91 if org_repo.EMPTY_CHANGESET in (org_rev, other_rev):
90 if org_repo.EMPTY_CHANGESET in (org_rev, other_rev):
92 # work around unexpected behaviour in Mercurial < 3.4
91 # work around unexpected behaviour in Mercurial < 3.4
93 ancestor = org_repo.EMPTY_CHANGESET
92 ancestor = org_repo.EMPTY_CHANGESET
94 else:
93 else:
95 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
94 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
96 if ancestors:
95 if ancestors:
97 # FIXME: picks arbitrary ancestor - but there is usually only one
96 # FIXME: picks arbitrary ancestor - but there is usually only one
98 try:
97 try:
99 ancestor = hgrepo[ancestors.first()].hex()
98 ancestor = hgrepo[ancestors.first()].hex()
100 except AttributeError:
99 except AttributeError:
101 # removed in hg 3.2
100 # removed in hg 3.2
102 ancestor = hgrepo[ancestors[0]].hex()
101 ancestor = hgrepo[ancestors[0]].hex()
103
102
104 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
103 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
105 other_rev, org_rev, org_rev)
104 other_rev, org_rev, org_rev)
106 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
105 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
107 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
106 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
108 org_rev, other_rev, other_rev)
107 org_rev, other_rev, other_rev)
109
108
110 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
109 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
111
110
112 elif alias == 'git':
111 elif alias == 'git':
113 if org_repo != other_repo:
112 if org_repo != other_repo:
114 from dulwich.repo import Repo
113 from dulwich.repo import Repo
115 from dulwich.client import SubprocessGitClient
114 from dulwich.client import SubprocessGitClient
116
115
117 gitrepo = Repo(org_repo.path)
116 gitrepo = Repo(org_repo.path)
118 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
117 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
119
118
120 gitrepo_remote = Repo(other_repo.path)
119 gitrepo_remote = Repo(other_repo.path)
121 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
120 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
122
121
123 revs = []
122 revs = []
124 for x in gitrepo_remote.get_walker(include=[other_rev],
123 for x in gitrepo_remote.get_walker(include=[other_rev],
125 exclude=[org_rev]):
124 exclude=[org_rev]):
126 revs.append(x.commit.id)
125 revs.append(x.commit.id)
127
126
128 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
127 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
129 if other_changesets:
128 if other_changesets:
130 ancestor = other_changesets[0].parents[0].raw_id
129 ancestor = other_changesets[0].parents[0].raw_id
131 else:
130 else:
132 # no changesets from other repo, ancestor is the other_rev
131 # no changesets from other repo, ancestor is the other_rev
133 ancestor = other_rev
132 ancestor = other_rev
134
133
135 else:
134 else:
136 so, se = org_repo.run_git_command(
135 so, se = org_repo.run_git_command(
137 ['log', '--reverse', '--pretty=format:%H',
136 ['log', '--reverse', '--pretty=format:%H',
138 '-s', '%s..%s' % (org_rev, other_rev)]
137 '-s', '%s..%s' % (org_rev, other_rev)]
139 )
138 )
140 other_changesets = [org_repo.get_changeset(cs)
139 other_changesets = [org_repo.get_changeset(cs)
141 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
140 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
142 so, se = org_repo.run_git_command(
141 so, se = org_repo.run_git_command(
143 ['merge-base', org_rev, other_rev]
142 ['merge-base', org_rev, other_rev]
144 )
143 )
145 ancestor = re.findall(r'[0-9a-fA-F]{40}', so)[0]
144 ancestor = re.findall(r'[0-9a-fA-F]{40}', so)[0]
146 org_changesets = []
145 org_changesets = []
147
146
148 else:
147 else:
149 raise Exception('Bad alias only git and hg is allowed')
148 raise Exception('Bad alias only git and hg is allowed')
150
149
151 return other_changesets, org_changesets, ancestor
150 return other_changesets, org_changesets, ancestor
152
151
153 @LoginRequired()
152 @LoginRequired()
154 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
155 'repository.admin')
154 'repository.admin')
156 def index(self, repo_name):
155 def index(self, repo_name):
157 c.compare_home = True
156 c.compare_home = True
158 org_repo = c.db_repo.repo_name
157 org_repo = c.db_repo.repo_name
159 other_repo = request.GET.get('other_repo', org_repo)
158 other_repo = request.GET.get('other_repo', org_repo)
160 c.a_repo = Repository.get_by_repo_name(org_repo)
159 c.a_repo = Repository.get_by_repo_name(org_repo)
161 c.cs_repo = Repository.get_by_repo_name(other_repo)
160 c.cs_repo = Repository.get_by_repo_name(other_repo)
162 c.a_ref_name = c.cs_ref_name = _('Select changeset')
161 c.a_ref_name = c.cs_ref_name = _('Select changeset')
163 return render('compare/compare_diff.html')
162 return render('compare/compare_diff.html')
164
163
165 @LoginRequired()
164 @LoginRequired()
166 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
167 'repository.admin')
166 'repository.admin')
168 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
167 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
169 org_ref_name = org_ref_name.strip()
168 org_ref_name = org_ref_name.strip()
170 other_ref_name = other_ref_name.strip()
169 other_ref_name = other_ref_name.strip()
171
170
172 org_repo = c.db_repo.repo_name
171 org_repo = c.db_repo.repo_name
173 other_repo = request.GET.get('other_repo', org_repo)
172 other_repo = request.GET.get('other_repo', org_repo)
174 # If merge is True:
173 # If merge is True:
175 # Show what org would get if merged with other:
174 # Show what org would get if merged with other:
176 # List changesets that are ancestors of other but not of org.
175 # List changesets that are ancestors of other but not of org.
177 # New changesets in org is thus ignored.
176 # New changesets in org is thus ignored.
178 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
177 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
179 # If merge is False:
178 # If merge is False:
180 # Make a raw diff from org to other, no matter if related or not.
179 # Make a raw diff from org to other, no matter if related or not.
181 # Changesets in one and not in the other will be ignored
180 # Changesets in one and not in the other will be ignored
182 merge = bool(request.GET.get('merge'))
181 merge = bool(request.GET.get('merge'))
183 # fulldiff disables cut_off_limit
182 # fulldiff disables cut_off_limit
184 c.fulldiff = request.GET.get('fulldiff')
183 c.fulldiff = request.GET.get('fulldiff')
185 # partial uses compare_cs.html template directly
184 # partial uses compare_cs.html template directly
186 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
185 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
187 # as_form puts hidden input field with changeset revisions
186 # as_form puts hidden input field with changeset revisions
188 c.as_form = partial and request.GET.get('as_form')
187 c.as_form = partial and request.GET.get('as_form')
189 # swap url for compare_diff page - never partial and never as_form
188 # swap url for compare_diff page - never partial and never as_form
190 c.swap_url = h.url('compare_url',
189 c.swap_url = h.url('compare_url',
191 repo_name=other_repo,
190 repo_name=other_repo,
192 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
191 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
193 other_repo=org_repo,
192 other_repo=org_repo,
194 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
193 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
195 merge=merge or '')
194 merge=merge or '')
196
195
197 # set callbacks for generating markup for icons
196 # set callbacks for generating markup for icons
198 c.ignorews_url = _ignorews_url
197 c.ignorews_url = _ignorews_url
199 c.context_url = _context_url
198 c.context_url = _context_url
200 ignore_whitespace = request.GET.get('ignorews') == '1'
199 ignore_whitespace = request.GET.get('ignorews') == '1'
201 line_context = request.GET.get('context', 3)
200 line_context = request.GET.get('context', 3)
202
201
203 org_repo = Repository.get_by_repo_name(org_repo)
202 org_repo = Repository.get_by_repo_name(org_repo)
204 other_repo = Repository.get_by_repo_name(other_repo)
203 other_repo = Repository.get_by_repo_name(other_repo)
205
204
206 if org_repo is None:
205 if org_repo is None:
207 msg = 'Could not find org repo %s' % org_repo
206 msg = 'Could not find org repo %s' % org_repo
208 log.error(msg)
207 log.error(msg)
209 h.flash(msg, category='error')
208 h.flash(msg, category='error')
210 return redirect(url('compare_home', repo_name=c.repo_name))
209 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
211
210
212 if other_repo is None:
211 if other_repo is None:
213 msg = 'Could not find other repo %s' % other_repo
212 msg = 'Could not find other repo %s' % other_repo
214 log.error(msg)
213 log.error(msg)
215 h.flash(msg, category='error')
214 h.flash(msg, category='error')
216 return redirect(url('compare_home', repo_name=c.repo_name))
215 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
217
216
218 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
217 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
219 msg = 'compare of two different kind of remote repos not available'
218 msg = 'compare of two different kind of remote repos not available'
220 log.error(msg)
219 log.error(msg)
221 h.flash(msg, category='error')
220 h.flash(msg, category='error')
222 return redirect(url('compare_home', repo_name=c.repo_name))
221 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
223
222
224 c.a_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name,
223 c.a_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name,
225 returnempty=True)
224 returnempty=True)
226 c.cs_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
225 c.cs_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
227
226
228 c.compare_home = False
227 c.compare_home = False
229 c.a_repo = org_repo
228 c.a_repo = org_repo
230 c.a_ref_name = org_ref_name
229 c.a_ref_name = org_ref_name
231 c.a_ref_type = org_ref_type
230 c.a_ref_type = org_ref_type
232 c.cs_repo = other_repo
231 c.cs_repo = other_repo
233 c.cs_ref_name = other_ref_name
232 c.cs_ref_name = other_ref_name
234 c.cs_ref_type = other_ref_type
233 c.cs_ref_type = other_ref_type
235
234
236 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
235 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
237 org_repo.scm_instance.alias, org_repo.scm_instance, c.a_rev,
236 org_repo.scm_instance.alias, org_repo.scm_instance, c.a_rev,
238 other_repo.scm_instance, c.cs_rev)
237 other_repo.scm_instance, c.cs_rev)
239 raw_ids = [x.raw_id for x in c.cs_ranges]
238 raw_ids = [x.raw_id for x in c.cs_ranges]
240 c.cs_comments = other_repo.get_comments(raw_ids)
239 c.cs_comments = other_repo.get_comments(raw_ids)
241 c.statuses = other_repo.statuses(raw_ids)
240 c.statuses = other_repo.statuses(raw_ids)
242
241
243 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
242 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
244 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
243 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
245
244
246 if partial:
245 if partial:
247 return render('compare/compare_cs.html')
246 return render('compare/compare_cs.html')
248 if merge and c.ancestor:
247 if merge and c.ancestor:
249 # case we want a simple diff without incoming changesets,
248 # case we want a simple diff without incoming changesets,
250 # previewing what will be merged.
249 # previewing what will be merged.
251 # Make the diff on the other repo (which is known to have other_rev)
250 # Make the diff on the other repo (which is known to have other_rev)
252 log.debug('Using ancestor %s as rev1 instead of %s',
251 log.debug('Using ancestor %s as rev1 instead of %s',
253 c.ancestor, c.a_rev)
252 c.ancestor, c.a_rev)
254 rev1 = c.ancestor
253 rev1 = c.ancestor
255 org_repo = other_repo
254 org_repo = other_repo
256 else: # comparing tips, not necessarily linearly related
255 else: # comparing tips, not necessarily linearly related
257 if merge:
256 if merge:
258 log.error('Unable to find ancestor revision')
257 log.error('Unable to find ancestor revision')
259 if org_repo != other_repo:
258 if org_repo != other_repo:
260 # TODO: we could do this by using hg unionrepo
259 # TODO: we could do this by using hg unionrepo
261 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
260 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
262 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
261 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
263 raise HTTPBadRequest
262 raise HTTPBadRequest
264 rev1 = c.a_rev
263 rev1 = c.a_rev
265
264
266 diff_limit = self.cut_off_limit if not c.fulldiff else None
265 diff_limit = self.cut_off_limit if not c.fulldiff else None
267
266
268 log.debug('running diff between %s and %s in %s',
267 log.debug('running diff between %s and %s in %s',
269 rev1, c.cs_rev, org_repo.scm_instance.path)
268 rev1, c.cs_rev, org_repo.scm_instance.path)
270 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
269 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
271 ignore_whitespace=ignore_whitespace,
270 ignore_whitespace=ignore_whitespace,
272 context=line_context)
271 context=line_context)
273
272
274 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
273 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
275 diff_limit=diff_limit)
274 diff_limit=diff_limit)
276 _parsed = diff_processor.prepare()
275 _parsed = diff_processor.prepare()
277
276
278 c.limited_diff = False
277 c.limited_diff = False
279 if isinstance(_parsed, LimitedDiffContainer):
278 if isinstance(_parsed, LimitedDiffContainer):
280 c.limited_diff = True
279 c.limited_diff = True
281
280
282 c.files = []
281 c.files = []
283 c.changes = {}
282 c.changes = {}
284 c.lines_added = 0
283 c.lines_added = 0
285 c.lines_deleted = 0
284 c.lines_deleted = 0
286 for f in _parsed:
285 for f in _parsed:
287 st = f['stats']
286 st = f['stats']
288 if not st['binary']:
287 if not st['binary']:
289 c.lines_added += st['added']
288 c.lines_added += st['added']
290 c.lines_deleted += st['deleted']
289 c.lines_deleted += st['deleted']
291 fid = h.FID('', f['filename'])
290 fid = h.FID('', f['filename'])
292 c.files.append([fid, f['operation'], f['filename'], f['stats']])
291 c.files.append([fid, f['operation'], f['filename'], f['stats']])
293 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
292 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
294 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
293 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
295
294
296 return render('compare/compare_diff.html')
295 return render('compare/compare_diff.html')
@@ -1,800 +1,800 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.files
15 kallithea.controllers.files
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Files controller for Kallithea
18 Files controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import os
28 import os
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import tempfile
31 import tempfile
32 import shutil
32 import shutil
33
33
34 from pylons import request, response, tmpl_context as c, url
34 from pylons import request, response, tmpl_context as c, url
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
36 from webob.exc import HTTPFound
37
37 from kallithea.lib.utils import jsonify, action_logger
38 from kallithea.lib.utils import jsonify, action_logger
38
39 from kallithea.lib import diffs
39 from kallithea.lib import diffs
40 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
41
41
42 from kallithea.lib.compat import OrderedDict
42 from kallithea.lib.compat import OrderedDict
43 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
43 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
44 str2bool
44 str2bool
45 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
46 from kallithea.lib.base import BaseRepoController, render
46 from kallithea.lib.base import BaseRepoController, render
47 from kallithea.lib.vcs.backends.base import EmptyChangeset
47 from kallithea.lib.vcs.backends.base import EmptyChangeset
48 from kallithea.lib.vcs.conf import settings
48 from kallithea.lib.vcs.conf import settings
49 from kallithea.lib.vcs.exceptions import RepositoryError, \
49 from kallithea.lib.vcs.exceptions import RepositoryError, \
50 ChangesetDoesNotExistError, EmptyRepositoryError, \
50 ChangesetDoesNotExistError, EmptyRepositoryError, \
51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
52 NodeDoesNotExistError, ChangesetError, NodeError
52 NodeDoesNotExistError, ChangesetError, NodeError
53 from kallithea.lib.vcs.nodes import FileNode
53 from kallithea.lib.vcs.nodes import FileNode
54
54
55 from kallithea.model.repo import RepoModel
55 from kallithea.model.repo import RepoModel
56 from kallithea.model.scm import ScmModel
56 from kallithea.model.scm import ScmModel
57 from kallithea.model.db import Repository
57 from kallithea.model.db import Repository
58
58
59 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
59 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
60 _context_url, get_line_ctx, get_ignore_ws
60 _context_url, get_line_ctx, get_ignore_ws
61 from webob.exc import HTTPNotFound
61 from webob.exc import HTTPNotFound
62 from kallithea.lib.exceptions import NonRelativePathError
62 from kallithea.lib.exceptions import NonRelativePathError
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class FilesController(BaseRepoController):
68 class FilesController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(FilesController, self).__before__()
71 super(FilesController, self).__before__()
72 c.cut_off_limit = self.cut_off_limit
72 c.cut_off_limit = self.cut_off_limit
73
73
74 def __get_cs(self, rev, silent_empty=False):
74 def __get_cs(self, rev, silent_empty=False):
75 """
75 """
76 Safe way to get changeset if error occur it redirects to tip with
76 Safe way to get changeset if error occur it redirects to tip with
77 proper message
77 proper message
78
78
79 :param rev: revision to fetch
79 :param rev: revision to fetch
80 :silent_empty: return None if repository is empty
80 :silent_empty: return None if repository is empty
81 """
81 """
82
82
83 try:
83 try:
84 return c.db_repo_scm_instance.get_changeset(rev)
84 return c.db_repo_scm_instance.get_changeset(rev)
85 except EmptyRepositoryError as e:
85 except EmptyRepositoryError as e:
86 if silent_empty:
86 if silent_empty:
87 return None
87 return None
88 url_ = url('files_add_home',
88 url_ = url('files_add_home',
89 repo_name=c.repo_name,
89 repo_name=c.repo_name,
90 revision=0, f_path='', anchor='edit')
90 revision=0, f_path='', anchor='edit')
91 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
91 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
92 h.flash(h.literal(_('There are no files yet. %s') % add_new),
92 h.flash(h.literal(_('There are no files yet. %s') % add_new),
93 category='warning')
93 category='warning')
94 raise HTTPNotFound()
94 raise HTTPNotFound()
95 except(ChangesetDoesNotExistError, LookupError), e:
95 except(ChangesetDoesNotExistError, LookupError), e:
96 msg = _('Such revision does not exist for this repository')
96 msg = _('Such revision does not exist for this repository')
97 h.flash(msg, category='error')
97 h.flash(msg, category='error')
98 raise HTTPNotFound()
98 raise HTTPNotFound()
99 except RepositoryError as e:
99 except RepositoryError as e:
100 h.flash(safe_str(e), category='error')
100 h.flash(safe_str(e), category='error')
101 raise HTTPNotFound()
101 raise HTTPNotFound()
102
102
103 def __get_filenode(self, cs, path):
103 def __get_filenode(self, cs, path):
104 """
104 """
105 Returns file_node or raise HTTP error.
105 Returns file_node or raise HTTP error.
106
106
107 :param cs: given changeset
107 :param cs: given changeset
108 :param path: path to lookup
108 :param path: path to lookup
109 """
109 """
110
110
111 try:
111 try:
112 file_node = cs.get_node(path)
112 file_node = cs.get_node(path)
113 if file_node.is_dir():
113 if file_node.is_dir():
114 raise RepositoryError('given path is a directory')
114 raise RepositoryError('given path is a directory')
115 except(ChangesetDoesNotExistError,), e:
115 except(ChangesetDoesNotExistError,), e:
116 msg = _('Such revision does not exist for this repository')
116 msg = _('Such revision does not exist for this repository')
117 h.flash(msg, category='error')
117 h.flash(msg, category='error')
118 raise HTTPNotFound()
118 raise HTTPNotFound()
119 except RepositoryError as e:
119 except RepositoryError as e:
120 h.flash(safe_str(e), category='error')
120 h.flash(safe_str(e), category='error')
121 raise HTTPNotFound()
121 raise HTTPNotFound()
122
122
123 return file_node
123 return file_node
124
124
125 @LoginRequired()
125 @LoginRequired()
126 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
126 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
127 'repository.admin')
127 'repository.admin')
128 def index(self, repo_name, revision, f_path, annotate=False):
128 def index(self, repo_name, revision, f_path, annotate=False):
129 # redirect to given revision from form if given
129 # redirect to given revision from form if given
130 post_revision = request.POST.get('at_rev', None)
130 post_revision = request.POST.get('at_rev', None)
131 if post_revision:
131 if post_revision:
132 cs = self.__get_cs(post_revision) # FIXME - unused!
132 cs = self.__get_cs(post_revision) # FIXME - unused!
133
133
134 c.revision = revision
134 c.revision = revision
135 c.changeset = self.__get_cs(revision)
135 c.changeset = self.__get_cs(revision)
136 c.branch = request.GET.get('branch', None)
136 c.branch = request.GET.get('branch', None)
137 c.f_path = f_path
137 c.f_path = f_path
138 c.annotate = annotate
138 c.annotate = annotate
139 cur_rev = c.changeset.revision
139 cur_rev = c.changeset.revision
140
140
141 # prev link
141 # prev link
142 try:
142 try:
143 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
143 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
144 c.url_prev = url('files_home', repo_name=c.repo_name,
144 c.url_prev = url('files_home', repo_name=c.repo_name,
145 revision=prev_rev.raw_id, f_path=f_path)
145 revision=prev_rev.raw_id, f_path=f_path)
146 if c.branch:
146 if c.branch:
147 c.url_prev += '?branch=%s' % c.branch
147 c.url_prev += '?branch=%s' % c.branch
148 except (ChangesetDoesNotExistError, VCSError):
148 except (ChangesetDoesNotExistError, VCSError):
149 c.url_prev = '#'
149 c.url_prev = '#'
150
150
151 # next link
151 # next link
152 try:
152 try:
153 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
153 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
154 c.url_next = url('files_home', repo_name=c.repo_name,
154 c.url_next = url('files_home', repo_name=c.repo_name,
155 revision=next_rev.raw_id, f_path=f_path)
155 revision=next_rev.raw_id, f_path=f_path)
156 if c.branch:
156 if c.branch:
157 c.url_next += '?branch=%s' % c.branch
157 c.url_next += '?branch=%s' % c.branch
158 except (ChangesetDoesNotExistError, VCSError):
158 except (ChangesetDoesNotExistError, VCSError):
159 c.url_next = '#'
159 c.url_next = '#'
160
160
161 # files or dirs
161 # files or dirs
162 try:
162 try:
163 c.file = c.changeset.get_node(f_path)
163 c.file = c.changeset.get_node(f_path)
164
164
165 if c.file.is_file():
165 if c.file.is_file():
166 c.load_full_history = False
166 c.load_full_history = False
167 file_last_cs = c.file.last_changeset
167 file_last_cs = c.file.last_changeset
168 c.file_changeset = (c.changeset
168 c.file_changeset = (c.changeset
169 if c.changeset.revision < file_last_cs.revision
169 if c.changeset.revision < file_last_cs.revision
170 else file_last_cs)
170 else file_last_cs)
171 #determine if we're on branch head
171 #determine if we're on branch head
172 _branches = c.db_repo_scm_instance.branches
172 _branches = c.db_repo_scm_instance.branches
173 c.on_branch_head = revision in _branches.keys() + _branches.values()
173 c.on_branch_head = revision in _branches.keys() + _branches.values()
174 _hist = []
174 _hist = []
175 c.file_history = []
175 c.file_history = []
176 if c.load_full_history:
176 if c.load_full_history:
177 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
177 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
178
178
179 c.authors = []
179 c.authors = []
180 for a in set([x.author for x in _hist]):
180 for a in set([x.author for x in _hist]):
181 c.authors.append((h.email(a), h.person(a)))
181 c.authors.append((h.email(a), h.person(a)))
182 else:
182 else:
183 c.authors = c.file_history = []
183 c.authors = c.file_history = []
184 except RepositoryError as e:
184 except RepositoryError as e:
185 h.flash(safe_str(e), category='error')
185 h.flash(safe_str(e), category='error')
186 raise HTTPNotFound()
186 raise HTTPNotFound()
187
187
188 if request.environ.get('HTTP_X_PARTIAL_XHR'):
188 if request.environ.get('HTTP_X_PARTIAL_XHR'):
189 return render('files/files_ypjax.html')
189 return render('files/files_ypjax.html')
190
190
191 # TODO: tags and bookmarks?
191 # TODO: tags and bookmarks?
192 c.revision_options = [(c.changeset.raw_id,
192 c.revision_options = [(c.changeset.raw_id,
193 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
193 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
194 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
194 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
195 if c.db_repo_scm_instance.closed_branches:
195 if c.db_repo_scm_instance.closed_branches:
196 prefix = _('(closed)') + ' '
196 prefix = _('(closed)') + ' '
197 c.revision_options += [('-', '-')] + \
197 c.revision_options += [('-', '-')] + \
198 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
198 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
199
199
200 return render('files/files.html')
200 return render('files/files.html')
201
201
202 @LoginRequired()
202 @LoginRequired()
203 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
203 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
204 'repository.admin')
204 'repository.admin')
205 @jsonify
205 @jsonify
206 def history(self, repo_name, revision, f_path):
206 def history(self, repo_name, revision, f_path):
207 changeset = self.__get_cs(revision)
207 changeset = self.__get_cs(revision)
208 f_path = f_path
208 f_path = f_path
209 _file = changeset.get_node(f_path)
209 _file = changeset.get_node(f_path)
210 if _file.is_file():
210 if _file.is_file():
211 file_history, _hist = self._get_node_history(changeset, f_path)
211 file_history, _hist = self._get_node_history(changeset, f_path)
212
212
213 res = []
213 res = []
214 for obj in file_history:
214 for obj in file_history:
215 res.append({
215 res.append({
216 'text': obj[1],
216 'text': obj[1],
217 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
217 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
218 })
218 })
219
219
220 data = {
220 data = {
221 'more': False,
221 'more': False,
222 'results': res
222 'results': res
223 }
223 }
224 return data
224 return data
225
225
226 @LoginRequired()
226 @LoginRequired()
227 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
227 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
228 'repository.admin')
228 'repository.admin')
229 def authors(self, repo_name, revision, f_path):
229 def authors(self, repo_name, revision, f_path):
230 changeset = self.__get_cs(revision)
230 changeset = self.__get_cs(revision)
231 f_path = f_path
231 f_path = f_path
232 _file = changeset.get_node(f_path)
232 _file = changeset.get_node(f_path)
233 if _file.is_file():
233 if _file.is_file():
234 file_history, _hist = self._get_node_history(changeset, f_path)
234 file_history, _hist = self._get_node_history(changeset, f_path)
235 c.authors = []
235 c.authors = []
236 for a in set([x.author for x in _hist]):
236 for a in set([x.author for x in _hist]):
237 c.authors.append((h.email(a), h.person(a)))
237 c.authors.append((h.email(a), h.person(a)))
238 return render('files/files_history_box.html')
238 return render('files/files_history_box.html')
239
239
240 @LoginRequired()
240 @LoginRequired()
241 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
241 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
242 'repository.admin')
242 'repository.admin')
243 def rawfile(self, repo_name, revision, f_path):
243 def rawfile(self, repo_name, revision, f_path):
244 cs = self.__get_cs(revision)
244 cs = self.__get_cs(revision)
245 file_node = self.__get_filenode(cs, f_path)
245 file_node = self.__get_filenode(cs, f_path)
246
246
247 response.content_disposition = 'attachment; filename=%s' % \
247 response.content_disposition = 'attachment; filename=%s' % \
248 safe_str(f_path.split(Repository.url_sep())[-1])
248 safe_str(f_path.split(Repository.url_sep())[-1])
249
249
250 response.content_type = file_node.mimetype
250 response.content_type = file_node.mimetype
251 return file_node.content
251 return file_node.content
252
252
253 @LoginRequired()
253 @LoginRequired()
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 'repository.admin')
255 'repository.admin')
256 def raw(self, repo_name, revision, f_path):
256 def raw(self, repo_name, revision, f_path):
257 cs = self.__get_cs(revision)
257 cs = self.__get_cs(revision)
258 file_node = self.__get_filenode(cs, f_path)
258 file_node = self.__get_filenode(cs, f_path)
259
259
260 raw_mimetype_mapping = {
260 raw_mimetype_mapping = {
261 # map original mimetype to a mimetype used for "show as raw"
261 # map original mimetype to a mimetype used for "show as raw"
262 # you can also provide a content-disposition to override the
262 # you can also provide a content-disposition to override the
263 # default "attachment" disposition.
263 # default "attachment" disposition.
264 # orig_type: (new_type, new_dispo)
264 # orig_type: (new_type, new_dispo)
265
265
266 # show images inline:
266 # show images inline:
267 'image/x-icon': ('image/x-icon', 'inline'),
267 'image/x-icon': ('image/x-icon', 'inline'),
268 'image/png': ('image/png', 'inline'),
268 'image/png': ('image/png', 'inline'),
269 'image/gif': ('image/gif', 'inline'),
269 'image/gif': ('image/gif', 'inline'),
270 'image/jpeg': ('image/jpeg', 'inline'),
270 'image/jpeg': ('image/jpeg', 'inline'),
271 'image/svg+xml': ('image/svg+xml', 'inline'),
271 'image/svg+xml': ('image/svg+xml', 'inline'),
272 }
272 }
273
273
274 mimetype = file_node.mimetype
274 mimetype = file_node.mimetype
275 try:
275 try:
276 mimetype, dispo = raw_mimetype_mapping[mimetype]
276 mimetype, dispo = raw_mimetype_mapping[mimetype]
277 except KeyError:
277 except KeyError:
278 # we don't know anything special about this, handle it safely
278 # we don't know anything special about this, handle it safely
279 if file_node.is_binary:
279 if file_node.is_binary:
280 # do same as download raw for binary files
280 # do same as download raw for binary files
281 mimetype, dispo = 'application/octet-stream', 'attachment'
281 mimetype, dispo = 'application/octet-stream', 'attachment'
282 else:
282 else:
283 # do not just use the original mimetype, but force text/plain,
283 # do not just use the original mimetype, but force text/plain,
284 # otherwise it would serve text/html and that might be unsafe.
284 # otherwise it would serve text/html and that might be unsafe.
285 # Note: underlying vcs library fakes text/plain mimetype if the
285 # Note: underlying vcs library fakes text/plain mimetype if the
286 # mimetype can not be determined and it thinks it is not
286 # mimetype can not be determined and it thinks it is not
287 # binary.This might lead to erroneous text display in some
287 # binary.This might lead to erroneous text display in some
288 # cases, but helps in other cases, like with text files
288 # cases, but helps in other cases, like with text files
289 # without extension.
289 # without extension.
290 mimetype, dispo = 'text/plain', 'inline'
290 mimetype, dispo = 'text/plain', 'inline'
291
291
292 if dispo == 'attachment':
292 if dispo == 'attachment':
293 dispo = 'attachment; filename=%s' % \
293 dispo = 'attachment; filename=%s' % \
294 safe_str(f_path.split(os.sep)[-1])
294 safe_str(f_path.split(os.sep)[-1])
295
295
296 response.content_disposition = dispo
296 response.content_disposition = dispo
297 response.content_type = mimetype
297 response.content_type = mimetype
298 return file_node.content
298 return file_node.content
299
299
300 @LoginRequired()
300 @LoginRequired()
301 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
301 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
302 def delete(self, repo_name, revision, f_path):
302 def delete(self, repo_name, revision, f_path):
303 repo = c.db_repo
303 repo = c.db_repo
304 if repo.enable_locking and repo.locked[0]:
304 if repo.enable_locking and repo.locked[0]:
305 h.flash(_('This repository has been locked by %s on %s')
305 h.flash(_('This repository has been locked by %s on %s')
306 % (h.person_by_id(repo.locked[0]),
306 % (h.person_by_id(repo.locked[0]),
307 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
307 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
308 'warning')
308 'warning')
309 return redirect(h.url('files_home',
309 raise HTTPFound(location=h.url('files_home',
310 repo_name=repo_name, revision='tip'))
310 repo_name=repo_name, revision='tip'))
311
311
312 # check if revision is a branch identifier- basically we cannot
312 # check if revision is a branch identifier- basically we cannot
313 # create multiple heads via file editing
313 # create multiple heads via file editing
314 _branches = repo.scm_instance.branches
314 _branches = repo.scm_instance.branches
315 # check if revision is a branch name or branch hash
315 # check if revision is a branch name or branch hash
316 if revision not in _branches.keys() + _branches.values():
316 if revision not in _branches.keys() + _branches.values():
317 h.flash(_('You can only delete files with revision '
317 h.flash(_('You can only delete files with revision '
318 'being a valid branch '), category='warning')
318 'being a valid branch '), category='warning')
319 return redirect(h.url('files_home',
319 raise HTTPFound(location=h.url('files_home',
320 repo_name=repo_name, revision='tip',
320 repo_name=repo_name, revision='tip',
321 f_path=f_path))
321 f_path=f_path))
322
322
323 r_post = request.POST
323 r_post = request.POST
324
324
325 c.cs = self.__get_cs(revision)
325 c.cs = self.__get_cs(revision)
326 c.file = self.__get_filenode(c.cs, f_path)
326 c.file = self.__get_filenode(c.cs, f_path)
327
327
328 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
328 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
329 c.f_path = f_path
329 c.f_path = f_path
330 node_path = f_path
330 node_path = f_path
331 author = self.authuser.full_contact
331 author = self.authuser.full_contact
332
332
333 if r_post:
333 if r_post:
334 message = r_post.get('message') or c.default_message
334 message = r_post.get('message') or c.default_message
335
335
336 try:
336 try:
337 nodes = {
337 nodes = {
338 node_path: {
338 node_path: {
339 'content': ''
339 'content': ''
340 }
340 }
341 }
341 }
342 self.scm_model.delete_nodes(
342 self.scm_model.delete_nodes(
343 user=c.authuser.user_id, repo=c.db_repo,
343 user=c.authuser.user_id, repo=c.db_repo,
344 message=message,
344 message=message,
345 nodes=nodes,
345 nodes=nodes,
346 parent_cs=c.cs,
346 parent_cs=c.cs,
347 author=author,
347 author=author,
348 )
348 )
349
349
350 h.flash(_('Successfully deleted file %s') % f_path,
350 h.flash(_('Successfully deleted file %s') % f_path,
351 category='success')
351 category='success')
352 except Exception:
352 except Exception:
353 log.error(traceback.format_exc())
353 log.error(traceback.format_exc())
354 h.flash(_('Error occurred during commit'), category='error')
354 h.flash(_('Error occurred during commit'), category='error')
355 return redirect(url('changeset_home',
355 raise HTTPFound(location=url('changeset_home',
356 repo_name=c.repo_name, revision='tip'))
356 repo_name=c.repo_name, revision='tip'))
357
357
358 return render('files/files_delete.html')
358 return render('files/files_delete.html')
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
361 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
362 def edit(self, repo_name, revision, f_path):
362 def edit(self, repo_name, revision, f_path):
363 repo = c.db_repo
363 repo = c.db_repo
364 if repo.enable_locking and repo.locked[0]:
364 if repo.enable_locking and repo.locked[0]:
365 h.flash(_('This repository has been locked by %s on %s')
365 h.flash(_('This repository has been locked by %s on %s')
366 % (h.person_by_id(repo.locked[0]),
366 % (h.person_by_id(repo.locked[0]),
367 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
367 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
368 'warning')
368 'warning')
369 return redirect(h.url('files_home',
369 raise HTTPFound(location=h.url('files_home',
370 repo_name=repo_name, revision='tip'))
370 repo_name=repo_name, revision='tip'))
371
371
372 # check if revision is a branch identifier- basically we cannot
372 # check if revision is a branch identifier- basically we cannot
373 # create multiple heads via file editing
373 # create multiple heads via file editing
374 _branches = repo.scm_instance.branches
374 _branches = repo.scm_instance.branches
375 # check if revision is a branch name or branch hash
375 # check if revision is a branch name or branch hash
376 if revision not in _branches.keys() + _branches.values():
376 if revision not in _branches.keys() + _branches.values():
377 h.flash(_('You can only edit files with revision '
377 h.flash(_('You can only edit files with revision '
378 'being a valid branch '), category='warning')
378 'being a valid branch '), category='warning')
379 return redirect(h.url('files_home',
379 raise HTTPFound(location=h.url('files_home',
380 repo_name=repo_name, revision='tip',
380 repo_name=repo_name, revision='tip',
381 f_path=f_path))
381 f_path=f_path))
382
382
383 r_post = request.POST
383 r_post = request.POST
384
384
385 c.cs = self.__get_cs(revision)
385 c.cs = self.__get_cs(revision)
386 c.file = self.__get_filenode(c.cs, f_path)
386 c.file = self.__get_filenode(c.cs, f_path)
387
387
388 if c.file.is_binary:
388 if c.file.is_binary:
389 return redirect(url('files_home', repo_name=c.repo_name,
389 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
390 revision=c.cs.raw_id, f_path=f_path))
390 revision=c.cs.raw_id, f_path=f_path))
391 c.default_message = _('Edited file %s via Kallithea') % (f_path)
391 c.default_message = _('Edited file %s via Kallithea') % (f_path)
392 c.f_path = f_path
392 c.f_path = f_path
393
393
394 if r_post:
394 if r_post:
395
395
396 old_content = c.file.content
396 old_content = c.file.content
397 sl = old_content.splitlines(1)
397 sl = old_content.splitlines(1)
398 first_line = sl[0] if sl else ''
398 first_line = sl[0] if sl else ''
399 # modes: 0 - Unix, 1 - Mac, 2 - DOS
399 # modes: 0 - Unix, 1 - Mac, 2 - DOS
400 mode = detect_mode(first_line, 0)
400 mode = detect_mode(first_line, 0)
401 content = convert_line_endings(r_post.get('content', ''), mode)
401 content = convert_line_endings(r_post.get('content', ''), mode)
402
402
403 message = r_post.get('message') or c.default_message
403 message = r_post.get('message') or c.default_message
404 author = self.authuser.full_contact
404 author = self.authuser.full_contact
405
405
406 if content == old_content:
406 if content == old_content:
407 h.flash(_('No changes'), category='warning')
407 h.flash(_('No changes'), category='warning')
408 return redirect(url('changeset_home', repo_name=c.repo_name,
408 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
409 revision='tip'))
409 revision='tip'))
410 try:
410 try:
411 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
411 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
412 repo_name=repo_name, cs=c.cs,
412 repo_name=repo_name, cs=c.cs,
413 user=self.authuser.user_id,
413 user=self.authuser.user_id,
414 author=author, message=message,
414 author=author, message=message,
415 content=content, f_path=f_path)
415 content=content, f_path=f_path)
416 h.flash(_('Successfully committed to %s') % f_path,
416 h.flash(_('Successfully committed to %s') % f_path,
417 category='success')
417 category='success')
418 except Exception:
418 except Exception:
419 log.error(traceback.format_exc())
419 log.error(traceback.format_exc())
420 h.flash(_('Error occurred during commit'), category='error')
420 h.flash(_('Error occurred during commit'), category='error')
421 return redirect(url('changeset_home',
421 raise HTTPFound(location=url('changeset_home',
422 repo_name=c.repo_name, revision='tip'))
422 repo_name=c.repo_name, revision='tip'))
423
423
424 return render('files/files_edit.html')
424 return render('files/files_edit.html')
425
425
426 @LoginRequired()
426 @LoginRequired()
427 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
427 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
428 def add(self, repo_name, revision, f_path):
428 def add(self, repo_name, revision, f_path):
429
429
430 repo = Repository.get_by_repo_name(repo_name)
430 repo = Repository.get_by_repo_name(repo_name)
431 if repo.enable_locking and repo.locked[0]:
431 if repo.enable_locking and repo.locked[0]:
432 h.flash(_('This repository has been locked by %s on %s')
432 h.flash(_('This repository has been locked by %s on %s')
433 % (h.person_by_id(repo.locked[0]),
433 % (h.person_by_id(repo.locked[0]),
434 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
434 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
435 'warning')
435 'warning')
436 return redirect(h.url('files_home',
436 raise HTTPFound(location=h.url('files_home',
437 repo_name=repo_name, revision='tip'))
437 repo_name=repo_name, revision='tip'))
438
438
439 r_post = request.POST
439 r_post = request.POST
440 c.cs = self.__get_cs(revision, silent_empty=True)
440 c.cs = self.__get_cs(revision, silent_empty=True)
441 if c.cs is None:
441 if c.cs is None:
442 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
442 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
443 c.default_message = (_('Added file via Kallithea'))
443 c.default_message = (_('Added file via Kallithea'))
444 c.f_path = f_path
444 c.f_path = f_path
445
445
446 if r_post:
446 if r_post:
447 unix_mode = 0
447 unix_mode = 0
448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
449
449
450 message = r_post.get('message') or c.default_message
450 message = r_post.get('message') or c.default_message
451 filename = r_post.get('filename')
451 filename = r_post.get('filename')
452 location = r_post.get('location', '')
452 location = r_post.get('location', '')
453 file_obj = r_post.get('upload_file', None)
453 file_obj = r_post.get('upload_file', None)
454
454
455 if file_obj is not None and hasattr(file_obj, 'filename'):
455 if file_obj is not None and hasattr(file_obj, 'filename'):
456 filename = file_obj.filename
456 filename = file_obj.filename
457 content = file_obj.file
457 content = file_obj.file
458
458
459 if hasattr(content, 'file'):
459 if hasattr(content, 'file'):
460 # non posix systems store real file under file attr
460 # non posix systems store real file under file attr
461 content = content.file
461 content = content.file
462
462
463 if not content:
463 if not content:
464 h.flash(_('No content'), category='warning')
464 h.flash(_('No content'), category='warning')
465 return redirect(url('changeset_home', repo_name=c.repo_name,
465 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
466 revision='tip'))
466 revision='tip'))
467 if not filename:
467 if not filename:
468 h.flash(_('No filename'), category='warning')
468 h.flash(_('No filename'), category='warning')
469 return redirect(url('changeset_home', repo_name=c.repo_name,
469 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
470 revision='tip'))
470 revision='tip'))
471 #strip all crap out of file, just leave the basename
471 #strip all crap out of file, just leave the basename
472 filename = os.path.basename(filename)
472 filename = os.path.basename(filename)
473 node_path = os.path.join(location, filename)
473 node_path = os.path.join(location, filename)
474 author = self.authuser.full_contact
474 author = self.authuser.full_contact
475
475
476 try:
476 try:
477 nodes = {
477 nodes = {
478 node_path: {
478 node_path: {
479 'content': content
479 'content': content
480 }
480 }
481 }
481 }
482 self.scm_model.create_nodes(
482 self.scm_model.create_nodes(
483 user=c.authuser.user_id, repo=c.db_repo,
483 user=c.authuser.user_id, repo=c.db_repo,
484 message=message,
484 message=message,
485 nodes=nodes,
485 nodes=nodes,
486 parent_cs=c.cs,
486 parent_cs=c.cs,
487 author=author,
487 author=author,
488 )
488 )
489
489
490 h.flash(_('Successfully committed to %s') % node_path,
490 h.flash(_('Successfully committed to %s') % node_path,
491 category='success')
491 category='success')
492 except NonRelativePathError as e:
492 except NonRelativePathError as e:
493 h.flash(_('Location must be relative path and must not '
493 h.flash(_('Location must be relative path and must not '
494 'contain .. in path'), category='warning')
494 'contain .. in path'), category='warning')
495 return redirect(url('changeset_home', repo_name=c.repo_name,
495 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
496 revision='tip'))
496 revision='tip'))
497 except (NodeError, NodeAlreadyExistsError) as e:
497 except (NodeError, NodeAlreadyExistsError) as e:
498 h.flash(_(e), category='error')
498 h.flash(_(e), category='error')
499 except Exception:
499 except Exception:
500 log.error(traceback.format_exc())
500 log.error(traceback.format_exc())
501 h.flash(_('Error occurred during commit'), category='error')
501 h.flash(_('Error occurred during commit'), category='error')
502 return redirect(url('changeset_home',
502 raise HTTPFound(location=url('changeset_home',
503 repo_name=c.repo_name, revision='tip'))
503 repo_name=c.repo_name, revision='tip'))
504
504
505 return render('files/files_add.html')
505 return render('files/files_add.html')
506
506
507 @LoginRequired()
507 @LoginRequired()
508 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
508 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
509 'repository.admin')
509 'repository.admin')
510 def archivefile(self, repo_name, fname):
510 def archivefile(self, repo_name, fname):
511 fileformat = None
511 fileformat = None
512 revision = None
512 revision = None
513 ext = None
513 ext = None
514 subrepos = request.GET.get('subrepos') == 'true'
514 subrepos = request.GET.get('subrepos') == 'true'
515
515
516 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
516 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
517 archive_spec = fname.split(ext_data[1])
517 archive_spec = fname.split(ext_data[1])
518 if len(archive_spec) == 2 and archive_spec[1] == '':
518 if len(archive_spec) == 2 and archive_spec[1] == '':
519 fileformat = a_type or ext_data[1]
519 fileformat = a_type or ext_data[1]
520 revision = archive_spec[0]
520 revision = archive_spec[0]
521 ext = ext_data[1]
521 ext = ext_data[1]
522
522
523 try:
523 try:
524 dbrepo = RepoModel().get_by_repo_name(repo_name)
524 dbrepo = RepoModel().get_by_repo_name(repo_name)
525 if not dbrepo.enable_downloads:
525 if not dbrepo.enable_downloads:
526 return _('Downloads disabled') # TODO: do something else?
526 return _('Downloads disabled') # TODO: do something else?
527
527
528 if c.db_repo_scm_instance.alias == 'hg':
528 if c.db_repo_scm_instance.alias == 'hg':
529 # patch and reset hooks section of UI config to not run any
529 # patch and reset hooks section of UI config to not run any
530 # hooks on fetching archives with subrepos
530 # hooks on fetching archives with subrepos
531 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
531 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
532 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
532 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
533
533
534 cs = c.db_repo_scm_instance.get_changeset(revision)
534 cs = c.db_repo_scm_instance.get_changeset(revision)
535 content_type = settings.ARCHIVE_SPECS[fileformat][0]
535 content_type = settings.ARCHIVE_SPECS[fileformat][0]
536 except ChangesetDoesNotExistError:
536 except ChangesetDoesNotExistError:
537 return _('Unknown revision %s') % revision
537 return _('Unknown revision %s') % revision
538 except EmptyRepositoryError:
538 except EmptyRepositoryError:
539 return _('Empty repository')
539 return _('Empty repository')
540 except (ImproperArchiveTypeError, KeyError):
540 except (ImproperArchiveTypeError, KeyError):
541 return _('Unknown archive type')
541 return _('Unknown archive type')
542
542
543 from kallithea import CONFIG
543 from kallithea import CONFIG
544 rev_name = cs.raw_id[:12]
544 rev_name = cs.raw_id[:12]
545 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
545 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
546 safe_str(rev_name), ext)
546 safe_str(rev_name), ext)
547
547
548 archive_path = None
548 archive_path = None
549 cached_archive_path = None
549 cached_archive_path = None
550 archive_cache_dir = CONFIG.get('archive_cache_dir')
550 archive_cache_dir = CONFIG.get('archive_cache_dir')
551 if archive_cache_dir and not subrepos: # TOOD: subrepo caching?
551 if archive_cache_dir and not subrepos: # TOOD: subrepo caching?
552 if not os.path.isdir(archive_cache_dir):
552 if not os.path.isdir(archive_cache_dir):
553 os.makedirs(archive_cache_dir)
553 os.makedirs(archive_cache_dir)
554 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
554 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
555 if os.path.isfile(cached_archive_path):
555 if os.path.isfile(cached_archive_path):
556 log.debug('Found cached archive in %s', cached_archive_path)
556 log.debug('Found cached archive in %s', cached_archive_path)
557 archive_path = cached_archive_path
557 archive_path = cached_archive_path
558 else:
558 else:
559 log.debug('Archive %s is not yet cached', archive_name)
559 log.debug('Archive %s is not yet cached', archive_name)
560
560
561 if archive_path is None:
561 if archive_path is None:
562 # generate new archive
562 # generate new archive
563 fd, archive_path = tempfile.mkstemp()
563 fd, archive_path = tempfile.mkstemp()
564 log.debug('Creating new temp archive in %s', archive_path)
564 log.debug('Creating new temp archive in %s', archive_path)
565 with os.fdopen(fd, 'wb') as stream:
565 with os.fdopen(fd, 'wb') as stream:
566 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
566 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
567 # stream (and thus fd) has been closed by cs.fill_archive
567 # stream (and thus fd) has been closed by cs.fill_archive
568 if cached_archive_path is not None:
568 if cached_archive_path is not None:
569 # we generated the archive - move it to cache
569 # we generated the archive - move it to cache
570 log.debug('Storing new archive in %s', cached_archive_path)
570 log.debug('Storing new archive in %s', cached_archive_path)
571 shutil.move(archive_path, cached_archive_path)
571 shutil.move(archive_path, cached_archive_path)
572 archive_path = cached_archive_path
572 archive_path = cached_archive_path
573
573
574 def get_chunked_archive(archive_path):
574 def get_chunked_archive(archive_path):
575 stream = open(archive_path, 'rb')
575 stream = open(archive_path, 'rb')
576 while True:
576 while True:
577 data = stream.read(16 * 1024)
577 data = stream.read(16 * 1024)
578 if not data:
578 if not data:
579 break
579 break
580 yield data
580 yield data
581 stream.close()
581 stream.close()
582 if archive_path != cached_archive_path:
582 if archive_path != cached_archive_path:
583 log.debug('Destroying temp archive %s', archive_path)
583 log.debug('Destroying temp archive %s', archive_path)
584 os.remove(archive_path)
584 os.remove(archive_path)
585
585
586 action_logger(user=c.authuser,
586 action_logger(user=c.authuser,
587 action='user_downloaded_archive:%s' % (archive_name),
587 action='user_downloaded_archive:%s' % (archive_name),
588 repo=repo_name, ipaddr=self.ip_addr, commit=True)
588 repo=repo_name, ipaddr=self.ip_addr, commit=True)
589
589
590 response.content_disposition = str('attachment; filename=%s' % (archive_name))
590 response.content_disposition = str('attachment; filename=%s' % (archive_name))
591 response.content_type = str(content_type)
591 response.content_type = str(content_type)
592 return get_chunked_archive(archive_path)
592 return get_chunked_archive(archive_path)
593
593
594 @LoginRequired()
594 @LoginRequired()
595 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
595 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
596 'repository.admin')
596 'repository.admin')
597 def diff(self, repo_name, f_path):
597 def diff(self, repo_name, f_path):
598 ignore_whitespace = request.GET.get('ignorews') == '1'
598 ignore_whitespace = request.GET.get('ignorews') == '1'
599 line_context = request.GET.get('context', 3)
599 line_context = request.GET.get('context', 3)
600 diff2 = request.GET.get('diff2', '')
600 diff2 = request.GET.get('diff2', '')
601 diff1 = request.GET.get('diff1', '') or diff2
601 diff1 = request.GET.get('diff1', '') or diff2
602 c.action = request.GET.get('diff')
602 c.action = request.GET.get('diff')
603 c.no_changes = diff1 == diff2
603 c.no_changes = diff1 == diff2
604 c.f_path = f_path
604 c.f_path = f_path
605 c.big_diff = False
605 c.big_diff = False
606 c.anchor_url = anchor_url
606 c.anchor_url = anchor_url
607 c.ignorews_url = _ignorews_url
607 c.ignorews_url = _ignorews_url
608 c.context_url = _context_url
608 c.context_url = _context_url
609 c.changes = OrderedDict()
609 c.changes = OrderedDict()
610 c.changes[diff2] = []
610 c.changes[diff2] = []
611
611
612 #special case if we want a show rev only, it's impl here
612 #special case if we want a show rev only, it's impl here
613 #to reduce JS and callbacks
613 #to reduce JS and callbacks
614
614
615 if request.GET.get('show_rev'):
615 if request.GET.get('show_rev'):
616 if str2bool(request.GET.get('annotate', 'False')):
616 if str2bool(request.GET.get('annotate', 'False')):
617 _url = url('files_annotate_home', repo_name=c.repo_name,
617 _url = url('files_annotate_home', repo_name=c.repo_name,
618 revision=diff1, f_path=c.f_path)
618 revision=diff1, f_path=c.f_path)
619 else:
619 else:
620 _url = url('files_home', repo_name=c.repo_name,
620 _url = url('files_home', repo_name=c.repo_name,
621 revision=diff1, f_path=c.f_path)
621 revision=diff1, f_path=c.f_path)
622
622
623 return redirect(_url)
623 raise HTTPFound(location=_url)
624 try:
624 try:
625 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
625 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
626 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
626 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
627 try:
627 try:
628 node1 = c.changeset_1.get_node(f_path)
628 node1 = c.changeset_1.get_node(f_path)
629 if node1.is_dir():
629 if node1.is_dir():
630 raise NodeError('%s path is a %s not a file'
630 raise NodeError('%s path is a %s not a file'
631 % (node1, type(node1)))
631 % (node1, type(node1)))
632 except NodeDoesNotExistError:
632 except NodeDoesNotExistError:
633 c.changeset_1 = EmptyChangeset(cs=diff1,
633 c.changeset_1 = EmptyChangeset(cs=diff1,
634 revision=c.changeset_1.revision,
634 revision=c.changeset_1.revision,
635 repo=c.db_repo_scm_instance)
635 repo=c.db_repo_scm_instance)
636 node1 = FileNode(f_path, '', changeset=c.changeset_1)
636 node1 = FileNode(f_path, '', changeset=c.changeset_1)
637 else:
637 else:
638 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
638 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
639 node1 = FileNode(f_path, '', changeset=c.changeset_1)
639 node1 = FileNode(f_path, '', changeset=c.changeset_1)
640
640
641 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
641 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
642 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
642 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
643 try:
643 try:
644 node2 = c.changeset_2.get_node(f_path)
644 node2 = c.changeset_2.get_node(f_path)
645 if node2.is_dir():
645 if node2.is_dir():
646 raise NodeError('%s path is a %s not a file'
646 raise NodeError('%s path is a %s not a file'
647 % (node2, type(node2)))
647 % (node2, type(node2)))
648 except NodeDoesNotExistError:
648 except NodeDoesNotExistError:
649 c.changeset_2 = EmptyChangeset(cs=diff2,
649 c.changeset_2 = EmptyChangeset(cs=diff2,
650 revision=c.changeset_2.revision,
650 revision=c.changeset_2.revision,
651 repo=c.db_repo_scm_instance)
651 repo=c.db_repo_scm_instance)
652 node2 = FileNode(f_path, '', changeset=c.changeset_2)
652 node2 = FileNode(f_path, '', changeset=c.changeset_2)
653 else:
653 else:
654 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
654 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
655 node2 = FileNode(f_path, '', changeset=c.changeset_2)
655 node2 = FileNode(f_path, '', changeset=c.changeset_2)
656 except (RepositoryError, NodeError):
656 except (RepositoryError, NodeError):
657 log.error(traceback.format_exc())
657 log.error(traceback.format_exc())
658 return redirect(url('files_home', repo_name=c.repo_name,
658 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
659 f_path=f_path))
659 f_path=f_path))
660
660
661 if c.action == 'download':
661 if c.action == 'download':
662 _diff = diffs.get_gitdiff(node1, node2,
662 _diff = diffs.get_gitdiff(node1, node2,
663 ignore_whitespace=ignore_whitespace,
663 ignore_whitespace=ignore_whitespace,
664 context=line_context)
664 context=line_context)
665 diff = diffs.DiffProcessor(_diff, format='gitdiff')
665 diff = diffs.DiffProcessor(_diff, format='gitdiff')
666
666
667 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
667 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
668 response.content_type = 'text/plain'
668 response.content_type = 'text/plain'
669 response.content_disposition = (
669 response.content_disposition = (
670 'attachment; filename=%s' % diff_name
670 'attachment; filename=%s' % diff_name
671 )
671 )
672 return diff.as_raw()
672 return diff.as_raw()
673
673
674 elif c.action == 'raw':
674 elif c.action == 'raw':
675 _diff = diffs.get_gitdiff(node1, node2,
675 _diff = diffs.get_gitdiff(node1, node2,
676 ignore_whitespace=ignore_whitespace,
676 ignore_whitespace=ignore_whitespace,
677 context=line_context)
677 context=line_context)
678 diff = diffs.DiffProcessor(_diff, format='gitdiff')
678 diff = diffs.DiffProcessor(_diff, format='gitdiff')
679 response.content_type = 'text/plain'
679 response.content_type = 'text/plain'
680 return diff.as_raw()
680 return diff.as_raw()
681
681
682 else:
682 else:
683 fid = h.FID(diff2, node2.path)
683 fid = h.FID(diff2, node2.path)
684 line_context_lcl = get_line_ctx(fid, request.GET)
684 line_context_lcl = get_line_ctx(fid, request.GET)
685 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
685 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
686
686
687 lim = request.GET.get('fulldiff') or self.cut_off_limit
687 lim = request.GET.get('fulldiff') or self.cut_off_limit
688 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
688 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
689 filenode_new=node2,
689 filenode_new=node2,
690 cut_off_limit=lim,
690 cut_off_limit=lim,
691 ignore_whitespace=ign_whitespace_lcl,
691 ignore_whitespace=ign_whitespace_lcl,
692 line_context=line_context_lcl,
692 line_context=line_context_lcl,
693 enable_comments=False)
693 enable_comments=False)
694 op = ''
694 op = ''
695 filename = node1.path
695 filename = node1.path
696 cs_changes = {
696 cs_changes = {
697 'fid': [cs1, cs2, op, filename, diff, st]
697 'fid': [cs1, cs2, op, filename, diff, st]
698 }
698 }
699 c.changes = cs_changes
699 c.changes = cs_changes
700
700
701 return render('files/file_diff.html')
701 return render('files/file_diff.html')
702
702
703 @LoginRequired()
703 @LoginRequired()
704 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
704 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
705 'repository.admin')
705 'repository.admin')
706 def diff_2way(self, repo_name, f_path):
706 def diff_2way(self, repo_name, f_path):
707 diff1 = request.GET.get('diff1', '')
707 diff1 = request.GET.get('diff1', '')
708 diff2 = request.GET.get('diff2', '')
708 diff2 = request.GET.get('diff2', '')
709 try:
709 try:
710 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
710 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
711 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
711 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
712 try:
712 try:
713 node1 = c.changeset_1.get_node(f_path)
713 node1 = c.changeset_1.get_node(f_path)
714 if node1.is_dir():
714 if node1.is_dir():
715 raise NodeError('%s path is a %s not a file'
715 raise NodeError('%s path is a %s not a file'
716 % (node1, type(node1)))
716 % (node1, type(node1)))
717 except NodeDoesNotExistError:
717 except NodeDoesNotExistError:
718 c.changeset_1 = EmptyChangeset(cs=diff1,
718 c.changeset_1 = EmptyChangeset(cs=diff1,
719 revision=c.changeset_1.revision,
719 revision=c.changeset_1.revision,
720 repo=c.db_repo_scm_instance)
720 repo=c.db_repo_scm_instance)
721 node1 = FileNode(f_path, '', changeset=c.changeset_1)
721 node1 = FileNode(f_path, '', changeset=c.changeset_1)
722 else:
722 else:
723 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
723 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
724 node1 = FileNode(f_path, '', changeset=c.changeset_1)
724 node1 = FileNode(f_path, '', changeset=c.changeset_1)
725
725
726 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
726 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
727 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
727 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
728 try:
728 try:
729 node2 = c.changeset_2.get_node(f_path)
729 node2 = c.changeset_2.get_node(f_path)
730 if node2.is_dir():
730 if node2.is_dir():
731 raise NodeError('%s path is a %s not a file'
731 raise NodeError('%s path is a %s not a file'
732 % (node2, type(node2)))
732 % (node2, type(node2)))
733 except NodeDoesNotExistError:
733 except NodeDoesNotExistError:
734 c.changeset_2 = EmptyChangeset(cs=diff2,
734 c.changeset_2 = EmptyChangeset(cs=diff2,
735 revision=c.changeset_2.revision,
735 revision=c.changeset_2.revision,
736 repo=c.db_repo_scm_instance)
736 repo=c.db_repo_scm_instance)
737 node2 = FileNode(f_path, '', changeset=c.changeset_2)
737 node2 = FileNode(f_path, '', changeset=c.changeset_2)
738 else:
738 else:
739 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
739 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
740 node2 = FileNode(f_path, '', changeset=c.changeset_2)
740 node2 = FileNode(f_path, '', changeset=c.changeset_2)
741 except ChangesetDoesNotExistError as e:
741 except ChangesetDoesNotExistError as e:
742 msg = _('Such revision does not exist for this repository')
742 msg = _('Such revision does not exist for this repository')
743 h.flash(msg, category='error')
743 h.flash(msg, category='error')
744 raise HTTPNotFound()
744 raise HTTPNotFound()
745 c.node1 = node1
745 c.node1 = node1
746 c.node2 = node2
746 c.node2 = node2
747 c.cs1 = c.changeset_1
747 c.cs1 = c.changeset_1
748 c.cs2 = c.changeset_2
748 c.cs2 = c.changeset_2
749
749
750 return render('files/diff_2way.html')
750 return render('files/diff_2way.html')
751
751
752 def _get_node_history(self, cs, f_path, changesets=None):
752 def _get_node_history(self, cs, f_path, changesets=None):
753 """
753 """
754 get changesets history for given node
754 get changesets history for given node
755
755
756 :param cs: changeset to calculate history
756 :param cs: changeset to calculate history
757 :param f_path: path for node to calculate history for
757 :param f_path: path for node to calculate history for
758 :param changesets: if passed don't calculate history and take
758 :param changesets: if passed don't calculate history and take
759 changesets defined in this list
759 changesets defined in this list
760 """
760 """
761 # calculate history based on tip
761 # calculate history based on tip
762 tip_cs = c.db_repo_scm_instance.get_changeset()
762 tip_cs = c.db_repo_scm_instance.get_changeset()
763 if changesets is None:
763 if changesets is None:
764 try:
764 try:
765 changesets = tip_cs.get_file_history(f_path)
765 changesets = tip_cs.get_file_history(f_path)
766 except (NodeDoesNotExistError, ChangesetError):
766 except (NodeDoesNotExistError, ChangesetError):
767 #this node is not present at tip !
767 #this node is not present at tip !
768 changesets = cs.get_file_history(f_path)
768 changesets = cs.get_file_history(f_path)
769 hist_l = []
769 hist_l = []
770
770
771 changesets_group = ([], _("Changesets"))
771 changesets_group = ([], _("Changesets"))
772 branches_group = ([], _("Branches"))
772 branches_group = ([], _("Branches"))
773 tags_group = ([], _("Tags"))
773 tags_group = ([], _("Tags"))
774 for chs in changesets:
774 for chs in changesets:
775 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
775 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
776 _branch = chs.branch
776 _branch = chs.branch
777 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
777 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
778 changesets_group[0].append((chs.raw_id, n_desc,))
778 changesets_group[0].append((chs.raw_id, n_desc,))
779 hist_l.append(changesets_group)
779 hist_l.append(changesets_group)
780
780
781 for name, chs in c.db_repo_scm_instance.branches.items():
781 for name, chs in c.db_repo_scm_instance.branches.items():
782 branches_group[0].append((chs, name),)
782 branches_group[0].append((chs, name),)
783 hist_l.append(branches_group)
783 hist_l.append(branches_group)
784
784
785 for name, chs in c.db_repo_scm_instance.tags.items():
785 for name, chs in c.db_repo_scm_instance.tags.items():
786 tags_group[0].append((chs, name),)
786 tags_group[0].append((chs, name),)
787 hist_l.append(tags_group)
787 hist_l.append(tags_group)
788
788
789 return hist_l, changesets
789 return hist_l, changesets
790
790
791 @LoginRequired()
791 @LoginRequired()
792 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
792 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
793 'repository.admin')
793 'repository.admin')
794 @jsonify
794 @jsonify
795 def nodelist(self, repo_name, revision, f_path):
795 def nodelist(self, repo_name, revision, f_path):
796 if request.environ.get('HTTP_X_PARTIAL_XHR'):
796 if request.environ.get('HTTP_X_PARTIAL_XHR'):
797 cs = self.__get_cs(revision)
797 cs = self.__get_cs(revision)
798 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
798 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
799 flat=False)
799 flat=False)
800 return {'nodes': _d + _f}
800 return {'nodes': _d + _f}
@@ -1,191 +1,191 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.forks
15 kallithea.controllers.forks
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 forks controller for Kallithea
18 forks controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 23, 2011
22 :created_on: Apr 23, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import formencode
29 import formencode
30 import traceback
30 import traceback
31 from formencode import htmlfill
31 from formencode import htmlfill
32
32
33 from pylons import tmpl_context as c, request, url
33 from pylons import tmpl_context as c, request, url
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 import kallithea.lib.helpers as h
37 import kallithea.lib.helpers as h
38
38
39 from kallithea.lib.helpers import Page
39 from kallithea.lib.helpers import Page
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
42 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.model.db import Repository, UserFollowing, User, Ui
43 from kallithea.model.db import Repository, UserFollowing, User, Ui
44 from kallithea.model.repo import RepoModel
44 from kallithea.model.repo import RepoModel
45 from kallithea.model.forms import RepoForkForm
45 from kallithea.model.forms import RepoForkForm
46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
47 from kallithea.lib.utils2 import safe_int
47 from kallithea.lib.utils2 import safe_int
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class ForksController(BaseRepoController):
52 class ForksController(BaseRepoController):
53
53
54 def __before__(self):
54 def __before__(self):
55 super(ForksController, self).__before__()
55 super(ForksController, self).__before__()
56
56
57 def __load_defaults(self):
57 def __load_defaults(self):
58 repo_group_perms = ['group.admin']
58 repo_group_perms = ['group.admin']
59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
60 repo_group_perms.append('group.write')
60 repo_group_perms.append('group.write')
61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
62
62
63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64
64
65 c.can_update = Ui.get_by_key(Ui.HOOK_UPDATE).ui_active
65 c.can_update = Ui.get_by_key(Ui.HOOK_UPDATE).ui_active
66
66
67 def __load_data(self, repo_name=None):
67 def __load_data(self, repo_name=None):
68 """
68 """
69 Load defaults settings for edit, and update
69 Load defaults settings for edit, and update
70
70
71 :param repo_name:
71 :param repo_name:
72 """
72 """
73 self.__load_defaults()
73 self.__load_defaults()
74
74
75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
76 repo = db_repo.scm_instance
76 repo = db_repo.scm_instance
77
77
78 if c.repo_info is None:
78 if c.repo_info is None:
79 h.not_mapped_error(repo_name)
79 h.not_mapped_error(repo_name)
80 return redirect(url('repos'))
80 raise HTTPFound(location=url('repos'))
81
81
82 c.default_user_id = User.get_default_user().user_id
82 c.default_user_id = User.get_default_user().user_id
83 c.in_public_journal = UserFollowing.query()\
83 c.in_public_journal = UserFollowing.query()\
84 .filter(UserFollowing.user_id == c.default_user_id)\
84 .filter(UserFollowing.user_id == c.default_user_id)\
85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
86
86
87 if c.repo_info.stats:
87 if c.repo_info.stats:
88 last_rev = c.repo_info.stats.stat_on_revision+1
88 last_rev = c.repo_info.stats.stat_on_revision+1
89 else:
89 else:
90 last_rev = 0
90 last_rev = 0
91 c.stats_revision = last_rev
91 c.stats_revision = last_rev
92
92
93 c.repo_last_rev = repo.count() if repo.revisions else 0
93 c.repo_last_rev = repo.count() if repo.revisions else 0
94
94
95 if last_rev == 0 or c.repo_last_rev == 0:
95 if last_rev == 0 or c.repo_last_rev == 0:
96 c.stats_percentage = 0
96 c.stats_percentage = 0
97 else:
97 else:
98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
99 c.repo_last_rev) * 100)
99 c.repo_last_rev) * 100)
100
100
101 defaults = RepoModel()._get_defaults(repo_name)
101 defaults = RepoModel()._get_defaults(repo_name)
102 # alter the description to indicate a fork
102 # alter the description to indicate a fork
103 defaults['description'] = ('fork of repository: %s \n%s'
103 defaults['description'] = ('fork of repository: %s \n%s'
104 % (defaults['repo_name'],
104 % (defaults['repo_name'],
105 defaults['description']))
105 defaults['description']))
106 # add suffix to fork
106 # add suffix to fork
107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
108
108
109 return defaults
109 return defaults
110
110
111 @LoginRequired()
111 @LoginRequired()
112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
113 'repository.admin')
113 'repository.admin')
114 def forks(self, repo_name):
114 def forks(self, repo_name):
115 p = safe_int(request.GET.get('page', 1), 1)
115 p = safe_int(request.GET.get('page', 1), 1)
116 repo_id = c.db_repo.repo_id
116 repo_id = c.db_repo.repo_id
117 d = []
117 d = []
118 for r in Repository.get_repo_forks(repo_id):
118 for r in Repository.get_repo_forks(repo_id):
119 if not HasRepoPermissionAny(
119 if not HasRepoPermissionAny(
120 'repository.read', 'repository.write', 'repository.admin'
120 'repository.read', 'repository.write', 'repository.admin'
121 )(r.repo_name, 'get forks check'):
121 )(r.repo_name, 'get forks check'):
122 continue
122 continue
123 d.append(r)
123 d.append(r)
124 c.forks_pager = Page(d, page=p, items_per_page=20)
124 c.forks_pager = Page(d, page=p, items_per_page=20)
125
125
126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
127 return render('/forks/forks_data.html')
127 return render('/forks/forks_data.html')
128
128
129 return render('/forks/forks.html')
129 return render('/forks/forks.html')
130
130
131 @LoginRequired()
131 @LoginRequired()
132 @NotAnonymous()
132 @NotAnonymous()
133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 'repository.admin')
135 'repository.admin')
136 def fork(self, repo_name):
136 def fork(self, repo_name):
137 c.repo_info = Repository.get_by_repo_name(repo_name)
137 c.repo_info = Repository.get_by_repo_name(repo_name)
138 if not c.repo_info:
138 if not c.repo_info:
139 h.not_mapped_error(repo_name)
139 h.not_mapped_error(repo_name)
140 return redirect(url('home'))
140 raise HTTPFound(location=url('home'))
141
141
142 defaults = self.__load_data(repo_name)
142 defaults = self.__load_data(repo_name)
143
143
144 return htmlfill.render(
144 return htmlfill.render(
145 render('forks/fork.html'),
145 render('forks/fork.html'),
146 defaults=defaults,
146 defaults=defaults,
147 encoding="UTF-8",
147 encoding="UTF-8",
148 force_defaults=False)
148 force_defaults=False)
149
149
150 @LoginRequired()
150 @LoginRequired()
151 @NotAnonymous()
151 @NotAnonymous()
152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 'repository.admin')
154 'repository.admin')
155 def fork_create(self, repo_name):
155 def fork_create(self, repo_name):
156 self.__load_defaults()
156 self.__load_defaults()
157 c.repo_info = Repository.get_by_repo_name(repo_name)
157 c.repo_info = Repository.get_by_repo_name(repo_name)
158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 repo_groups=c.repo_groups,
159 repo_groups=c.repo_groups,
160 landing_revs=c.landing_revs_choices)()
160 landing_revs=c.landing_revs_choices)()
161 form_result = {}
161 form_result = {}
162 task_id = None
162 task_id = None
163 try:
163 try:
164 form_result = _form.to_python(dict(request.POST))
164 form_result = _form.to_python(dict(request.POST))
165
165
166 # an approximation that is better than nothing
166 # an approximation that is better than nothing
167 if not Ui.get_by_key(Ui.HOOK_UPDATE).ui_active:
167 if not Ui.get_by_key(Ui.HOOK_UPDATE).ui_active:
168 form_result['update_after_clone'] = False
168 form_result['update_after_clone'] = False
169
169
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(form_result, self.authuser.user_id)
172 task = RepoModel().create_fork(form_result, self.authuser.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('forks/fork.html'),
178 render('forks/fork.html'),
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 except Exception:
184 except Exception:
185 log.error(traceback.format_exc())
185 log.error(traceback.format_exc())
186 h.flash(_('An error occurred during repository forking %s') %
186 h.flash(_('An error occurred during repository forking %s') %
187 repo_name, category='error')
187 repo_name, category='error')
188
188
189 return redirect(h.url('repo_creating_home',
189 raise HTTPFound(location=h.url('repo_creating_home',
190 repo_name=form_result['repo_name_full'],
190 repo_name=form_result['repo_name_full'],
191 task_id=task_id))
191 task_id=task_id))
@@ -1,265 +1,264 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.login
15 kallithea.controllers.login
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Login controller for Kallithea
18 Login controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 22, 2010
22 :created_on: Apr 22, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import re
30 import re
31 import formencode
31 import formencode
32
32
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from pylons.i18n.translation import _
35 from pylons import request, session, tmpl_context as c, url
34 from webob.exc import HTTPFound, HTTPBadRequest
36 from webob.exc import HTTPFound, HTTPBadRequest
35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
37 from pylons import request, session, tmpl_context as c, url
38
37
39 import kallithea.lib.helpers as h
38 import kallithea.lib.helpers as h
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
39 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
41 from kallithea.lib.base import BaseController, log_in_user, render
40 from kallithea.lib.base import BaseController, log_in_user, render
42 from kallithea.lib.exceptions import UserCreationError
41 from kallithea.lib.exceptions import UserCreationError
43 from kallithea.lib.utils2 import safe_str
42 from kallithea.lib.utils2 import safe_str
44 from kallithea.model.db import User, Setting
43 from kallithea.model.db import User, Setting
45 from kallithea.model.forms import \
44 from kallithea.model.forms import \
46 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
45 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
47 from kallithea.model.user import UserModel
46 from kallithea.model.user import UserModel
48 from kallithea.model.meta import Session
47 from kallithea.model.meta import Session
49
48
50
49
51 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
52
51
53
52
54 class LoginController(BaseController):
53 class LoginController(BaseController):
55
54
56 def __before__(self):
55 def __before__(self):
57 super(LoginController, self).__before__()
56 super(LoginController, self).__before__()
58
57
59 def _validate_came_from(self, came_from,
58 def _validate_came_from(self, came_from,
60 _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
59 _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
61 """Return True if came_from is valid and can and should be used.
60 """Return True if came_from is valid and can and should be used.
62
61
63 Determines if a URI reference is valid and relative to the origin;
62 Determines if a URI reference is valid and relative to the origin;
64 or in RFC 3986 terms, whether it matches this production:
63 or in RFC 3986 terms, whether it matches this production:
65
64
66 origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ]
65 origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ]
67
66
68 with the exception that '%' escapes are not validated and '#' is
67 with the exception that '%' escapes are not validated and '#' is
69 allowed inside the fragment part.
68 allowed inside the fragment part.
70 """
69 """
71 return _re.match(came_from) is not None
70 return _re.match(came_from) is not None
72
71
73 def index(self):
72 def index(self):
74 c.came_from = safe_str(request.GET.get('came_from', ''))
73 c.came_from = safe_str(request.GET.get('came_from', ''))
75 if c.came_from:
74 if c.came_from:
76 if not self._validate_came_from(c.came_from):
75 if not self._validate_came_from(c.came_from):
77 log.error('Invalid came_from (not server-relative): %r', c.came_from)
76 log.error('Invalid came_from (not server-relative): %r', c.came_from)
78 raise HTTPBadRequest()
77 raise HTTPBadRequest()
79 else:
78 else:
80 c.came_from = url('home')
79 c.came_from = url('home')
81
80
82 not_default = self.authuser.username != User.DEFAULT_USER
81 not_default = self.authuser.username != User.DEFAULT_USER
83 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
82 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
84
83
85 # redirect if already logged in
84 # redirect if already logged in
86 if self.authuser.is_authenticated and not_default and ip_allowed:
85 if self.authuser.is_authenticated and not_default and ip_allowed:
87 raise HTTPFound(location=c.came_from)
86 raise HTTPFound(location=c.came_from)
88
87
89 if request.POST:
88 if request.POST:
90 # import Login Form validator class
89 # import Login Form validator class
91 login_form = LoginForm()
90 login_form = LoginForm()
92 try:
91 try:
93 c.form_result = login_form.to_python(dict(request.POST))
92 c.form_result = login_form.to_python(dict(request.POST))
94 # form checks for username/password, now we're authenticated
93 # form checks for username/password, now we're authenticated
95 username = c.form_result['username']
94 username = c.form_result['username']
96 user = User.get_by_username(username, case_insensitive=True)
95 user = User.get_by_username(username, case_insensitive=True)
97 except formencode.Invalid as errors:
96 except formencode.Invalid as errors:
98 defaults = errors.value
97 defaults = errors.value
99 # remove password from filling in form again
98 # remove password from filling in form again
100 del defaults['password']
99 del defaults['password']
101 return htmlfill.render(
100 return htmlfill.render(
102 render('/login.html'),
101 render('/login.html'),
103 defaults=errors.value,
102 defaults=errors.value,
104 errors=errors.error_dict or {},
103 errors=errors.error_dict or {},
105 prefix_error=False,
104 prefix_error=False,
106 encoding="UTF-8",
105 encoding="UTF-8",
107 force_defaults=False)
106 force_defaults=False)
108 except UserCreationError as e:
107 except UserCreationError as e:
109 # container auth or other auth functions that create users on
108 # container auth or other auth functions that create users on
110 # the fly can throw this exception signaling that there's issue
109 # the fly can throw this exception signaling that there's issue
111 # with user creation, explanation should be provided in
110 # with user creation, explanation should be provided in
112 # Exception itself
111 # Exception itself
113 h.flash(e, 'error')
112 h.flash(e, 'error')
114 else:
113 else:
115 log_in_user(user, c.form_result['remember'],
114 log_in_user(user, c.form_result['remember'],
116 is_external_auth=False)
115 is_external_auth=False)
117 raise HTTPFound(location=c.came_from)
116 raise HTTPFound(location=c.came_from)
118
117
119 return render('/login.html')
118 return render('/login.html')
120
119
121 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
120 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
122 'hg.register.manual_activate')
121 'hg.register.manual_activate')
123 def register(self):
122 def register(self):
124 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
123 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
125 .AuthUser.permissions['global']
124 .AuthUser.permissions['global']
126
125
127 settings = Setting.get_app_settings()
126 settings = Setting.get_app_settings()
128 captcha_private_key = settings.get('captcha_private_key')
127 captcha_private_key = settings.get('captcha_private_key')
129 c.captcha_active = bool(captcha_private_key)
128 c.captcha_active = bool(captcha_private_key)
130 c.captcha_public_key = settings.get('captcha_public_key')
129 c.captcha_public_key = settings.get('captcha_public_key')
131
130
132 if request.POST:
131 if request.POST:
133 register_form = RegisterForm()()
132 register_form = RegisterForm()()
134 try:
133 try:
135 form_result = register_form.to_python(dict(request.POST))
134 form_result = register_form.to_python(dict(request.POST))
136 form_result['active'] = c.auto_active
135 form_result['active'] = c.auto_active
137
136
138 if c.captcha_active:
137 if c.captcha_active:
139 from kallithea.lib.recaptcha import submit
138 from kallithea.lib.recaptcha import submit
140 response = submit(request.POST.get('recaptcha_challenge_field'),
139 response = submit(request.POST.get('recaptcha_challenge_field'),
141 request.POST.get('recaptcha_response_field'),
140 request.POST.get('recaptcha_response_field'),
142 private_key=captcha_private_key,
141 private_key=captcha_private_key,
143 remoteip=self.ip_addr)
142 remoteip=self.ip_addr)
144 if c.captcha_active and not response.is_valid:
143 if c.captcha_active and not response.is_valid:
145 _value = form_result
144 _value = form_result
146 _msg = _('Bad captcha')
145 _msg = _('Bad captcha')
147 error_dict = {'recaptcha_field': _msg}
146 error_dict = {'recaptcha_field': _msg}
148 raise formencode.Invalid(_msg, _value, None,
147 raise formencode.Invalid(_msg, _value, None,
149 error_dict=error_dict)
148 error_dict=error_dict)
150
149
151 UserModel().create_registration(form_result)
150 UserModel().create_registration(form_result)
152 h.flash(_('You have successfully registered into Kallithea'),
151 h.flash(_('You have successfully registered into Kallithea'),
153 category='success')
152 category='success')
154 Session().commit()
153 Session().commit()
155 return redirect(url('login_home'))
154 raise HTTPFound(location=url('login_home'))
156
155
157 except formencode.Invalid as errors:
156 except formencode.Invalid as errors:
158 return htmlfill.render(
157 return htmlfill.render(
159 render('/register.html'),
158 render('/register.html'),
160 defaults=errors.value,
159 defaults=errors.value,
161 errors=errors.error_dict or {},
160 errors=errors.error_dict or {},
162 prefix_error=False,
161 prefix_error=False,
163 encoding="UTF-8",
162 encoding="UTF-8",
164 force_defaults=False)
163 force_defaults=False)
165 except UserCreationError as e:
164 except UserCreationError as e:
166 # container auth or other auth functions that create users on
165 # container auth or other auth functions that create users on
167 # the fly can throw this exception signaling that there's issue
166 # the fly can throw this exception signaling that there's issue
168 # with user creation, explanation should be provided in
167 # with user creation, explanation should be provided in
169 # Exception itself
168 # Exception itself
170 h.flash(e, 'error')
169 h.flash(e, 'error')
171
170
172 return render('/register.html')
171 return render('/register.html')
173
172
174 def password_reset(self):
173 def password_reset(self):
175 settings = Setting.get_app_settings()
174 settings = Setting.get_app_settings()
176 captcha_private_key = settings.get('captcha_private_key')
175 captcha_private_key = settings.get('captcha_private_key')
177 c.captcha_active = bool(captcha_private_key)
176 c.captcha_active = bool(captcha_private_key)
178 c.captcha_public_key = settings.get('captcha_public_key')
177 c.captcha_public_key = settings.get('captcha_public_key')
179
178
180 if request.POST:
179 if request.POST:
181 password_reset_form = PasswordResetRequestForm()()
180 password_reset_form = PasswordResetRequestForm()()
182 try:
181 try:
183 form_result = password_reset_form.to_python(dict(request.POST))
182 form_result = password_reset_form.to_python(dict(request.POST))
184 if c.captcha_active:
183 if c.captcha_active:
185 from kallithea.lib.recaptcha import submit
184 from kallithea.lib.recaptcha import submit
186 response = submit(request.POST.get('recaptcha_challenge_field'),
185 response = submit(request.POST.get('recaptcha_challenge_field'),
187 request.POST.get('recaptcha_response_field'),
186 request.POST.get('recaptcha_response_field'),
188 private_key=captcha_private_key,
187 private_key=captcha_private_key,
189 remoteip=self.ip_addr)
188 remoteip=self.ip_addr)
190 if c.captcha_active and not response.is_valid:
189 if c.captcha_active and not response.is_valid:
191 _value = form_result
190 _value = form_result
192 _msg = _('Bad captcha')
191 _msg = _('Bad captcha')
193 error_dict = {'recaptcha_field': _msg}
192 error_dict = {'recaptcha_field': _msg}
194 raise formencode.Invalid(_msg, _value, None,
193 raise formencode.Invalid(_msg, _value, None,
195 error_dict=error_dict)
194 error_dict=error_dict)
196 redirect_link = UserModel().send_reset_password_email(form_result)
195 redirect_link = UserModel().send_reset_password_email(form_result)
197 h.flash(_('A password reset confirmation code has been sent'),
196 h.flash(_('A password reset confirmation code has been sent'),
198 category='success')
197 category='success')
199 return redirect(redirect_link)
198 raise HTTPFound(location=redirect_link)
200
199
201 except formencode.Invalid as errors:
200 except formencode.Invalid as errors:
202 return htmlfill.render(
201 return htmlfill.render(
203 render('/password_reset.html'),
202 render('/password_reset.html'),
204 defaults=errors.value,
203 defaults=errors.value,
205 errors=errors.error_dict or {},
204 errors=errors.error_dict or {},
206 prefix_error=False,
205 prefix_error=False,
207 encoding="UTF-8",
206 encoding="UTF-8",
208 force_defaults=False)
207 force_defaults=False)
209
208
210 return render('/password_reset.html')
209 return render('/password_reset.html')
211
210
212 def password_reset_confirmation(self):
211 def password_reset_confirmation(self):
213 # This controller handles both GET and POST requests, though we
212 # This controller handles both GET and POST requests, though we
214 # only ever perform the actual password change on POST (since
213 # only ever perform the actual password change on POST (since
215 # GET requests are not allowed to have side effects, and do not
214 # GET requests are not allowed to have side effects, and do not
216 # receive automatic CSRF protection).
215 # receive automatic CSRF protection).
217
216
218 # The template needs the email address outside of the form.
217 # The template needs the email address outside of the form.
219 c.email = request.params.get('email')
218 c.email = request.params.get('email')
220
219
221 if not request.POST:
220 if not request.POST:
222 return htmlfill.render(
221 return htmlfill.render(
223 render('/password_reset_confirmation.html'),
222 render('/password_reset_confirmation.html'),
224 defaults=dict(request.params),
223 defaults=dict(request.params),
225 encoding='UTF-8')
224 encoding='UTF-8')
226
225
227 form = PasswordResetConfirmationForm()()
226 form = PasswordResetConfirmationForm()()
228 try:
227 try:
229 form_result = form.to_python(dict(request.POST))
228 form_result = form.to_python(dict(request.POST))
230 except formencode.Invalid as errors:
229 except formencode.Invalid as errors:
231 return htmlfill.render(
230 return htmlfill.render(
232 render('/password_reset_confirmation.html'),
231 render('/password_reset_confirmation.html'),
233 defaults=errors.value,
232 defaults=errors.value,
234 errors=errors.error_dict or {},
233 errors=errors.error_dict or {},
235 prefix_error=False,
234 prefix_error=False,
236 encoding='UTF-8')
235 encoding='UTF-8')
237
236
238 if not UserModel().verify_reset_password_token(
237 if not UserModel().verify_reset_password_token(
239 form_result['email'],
238 form_result['email'],
240 form_result['timestamp'],
239 form_result['timestamp'],
241 form_result['token'],
240 form_result['token'],
242 ):
241 ):
243 return htmlfill.render(
242 return htmlfill.render(
244 render('/password_reset_confirmation.html'),
243 render('/password_reset_confirmation.html'),
245 defaults=form_result,
244 defaults=form_result,
246 errors={'token': _('Invalid password reset token')},
245 errors={'token': _('Invalid password reset token')},
247 prefix_error=False,
246 prefix_error=False,
248 encoding='UTF-8')
247 encoding='UTF-8')
249
248
250 UserModel().reset_password(form_result['email'], form_result['password'])
249 UserModel().reset_password(form_result['email'], form_result['password'])
251 h.flash(_('Successfully updated password'), category='success')
250 h.flash(_('Successfully updated password'), category='success')
252 return redirect(url('login_home'))
251 raise HTTPFound(location=url('login_home'))
253
252
254 def logout(self):
253 def logout(self):
255 session.delete()
254 session.delete()
256 log.info('Logging out and deleting session for user')
255 log.info('Logging out and deleting session for user')
257 redirect(url('home'))
256 raise HTTPFound(location=url('home'))
258
257
259 def authentication_token(self):
258 def authentication_token(self):
260 """Return the CSRF protection token for the session - just like it
259 """Return the CSRF protection token for the session - just like it
261 could have been screen scraped from a page with a form.
260 could have been screen scraped from a page with a form.
262 Only intended for testing but might also be useful for other kinds
261 Only intended for testing but might also be useful for other kinds
263 of automation.
262 of automation.
264 """
263 """
265 return h.authentication_token()
264 return h.authentication_token()
@@ -1,796 +1,794 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: May 7, 2012
22 :created_on: May 7, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31 import re
31 import re
32
32
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
34
35 from pylons import request, tmpl_context as c, url
33 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
37 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
38
36
39 from kallithea.lib.vcs.utils.hgcompat import unionrepo
37 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 from kallithea.lib.compat import json
38 from kallithea.lib.compat import json
41 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
43 NotAnonymous
41 NotAnonymous
44 from kallithea.lib.helpers import Page
42 from kallithea.lib.helpers import Page
45 from kallithea.lib import helpers as h
43 from kallithea.lib import helpers as h
46 from kallithea.lib import diffs
44 from kallithea.lib import diffs
47 from kallithea.lib.exceptions import UserInvalidException
45 from kallithea.lib.exceptions import UserInvalidException
48 from kallithea.lib.utils import action_logger, jsonify
46 from kallithea.lib.utils import action_logger, jsonify
49 from kallithea.lib.vcs.utils import safe_str
47 from kallithea.lib.vcs.utils import safe_str
50 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
51 from kallithea.lib.diffs import LimitedDiffContainer
49 from kallithea.lib.diffs import LimitedDiffContainer
52 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
53 PullRequestReviewers, User
51 PullRequestReviewers, User
54 from kallithea.model.pull_request import PullRequestModel
52 from kallithea.model.pull_request import PullRequestModel
55 from kallithea.model.meta import Session
53 from kallithea.model.meta import Session
56 from kallithea.model.repo import RepoModel
54 from kallithea.model.repo import RepoModel
57 from kallithea.model.comment import ChangesetCommentsModel
55 from kallithea.model.comment import ChangesetCommentsModel
58 from kallithea.model.changeset_status import ChangesetStatusModel
56 from kallithea.model.changeset_status import ChangesetStatusModel
59 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
60 from kallithea.lib.utils2 import safe_int
58 from kallithea.lib.utils2 import safe_int
61 from kallithea.controllers.changeset import _ignorews_url, _context_url
59 from kallithea.controllers.changeset import _ignorews_url, _context_url
62 from kallithea.controllers.compare import CompareController
60 from kallithea.controllers.compare import CompareController
63 from kallithea.lib.graphmod import graph_data
61 from kallithea.lib.graphmod import graph_data
64
62
65 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
66
64
67
65
68 class PullrequestsController(BaseRepoController):
66 class PullrequestsController(BaseRepoController):
69
67
70 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
71 """return a structure with repo's interesting changesets, suitable for
69 """return a structure with repo's interesting changesets, suitable for
72 the selectors in pullrequest.html
70 the selectors in pullrequest.html
73
71
74 rev: a revision that must be in the list somehow and selected by default
72 rev: a revision that must be in the list somehow and selected by default
75 branch: a branch that must be in the list and selected by default - even if closed
73 branch: a branch that must be in the list and selected by default - even if closed
76 branch_rev: a revision of which peers should be preferred and available."""
74 branch_rev: a revision of which peers should be preferred and available."""
77 # list named branches that has been merged to this named branch - it should probably merge back
75 # list named branches that has been merged to this named branch - it should probably merge back
78 peers = []
76 peers = []
79
77
80 if rev:
78 if rev:
81 rev = safe_str(rev)
79 rev = safe_str(rev)
82
80
83 if branch:
81 if branch:
84 branch = safe_str(branch)
82 branch = safe_str(branch)
85
83
86 if branch_rev:
84 if branch_rev:
87 branch_rev = safe_str(branch_rev)
85 branch_rev = safe_str(branch_rev)
88 # a revset not restricting to merge() would be better
86 # a revset not restricting to merge() would be better
89 # (especially because it would get the branch point)
87 # (especially because it would get the branch point)
90 # ... but is currently too expensive
88 # ... but is currently too expensive
91 # including branches of children could be nice too
89 # including branches of children could be nice too
92 peerbranches = set()
90 peerbranches = set()
93 for i in repo._repo.revs(
91 for i in repo._repo.revs(
94 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
92 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
95 branch_rev, branch_rev):
93 branch_rev, branch_rev):
96 abranch = repo.get_changeset(i).branch
94 abranch = repo.get_changeset(i).branch
97 if abranch not in peerbranches:
95 if abranch not in peerbranches:
98 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
96 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
99 peers.append((n, abranch))
97 peers.append((n, abranch))
100 peerbranches.add(abranch)
98 peerbranches.add(abranch)
101
99
102 selected = None
100 selected = None
103 tiprev = repo.tags.get('tip')
101 tiprev = repo.tags.get('tip')
104 tipbranch = None
102 tipbranch = None
105
103
106 branches = []
104 branches = []
107 for abranch, branchrev in repo.branches.iteritems():
105 for abranch, branchrev in repo.branches.iteritems():
108 n = 'branch:%s:%s' % (abranch, branchrev)
106 n = 'branch:%s:%s' % (abranch, branchrev)
109 desc = abranch
107 desc = abranch
110 if branchrev == tiprev:
108 if branchrev == tiprev:
111 tipbranch = abranch
109 tipbranch = abranch
112 desc = '%s (current tip)' % desc
110 desc = '%s (current tip)' % desc
113 branches.append((n, desc))
111 branches.append((n, desc))
114 if rev == branchrev:
112 if rev == branchrev:
115 selected = n
113 selected = n
116 if branch == abranch:
114 if branch == abranch:
117 if not rev:
115 if not rev:
118 selected = n
116 selected = n
119 branch = None
117 branch = None
120 if branch: # branch not in list - it is probably closed
118 if branch: # branch not in list - it is probably closed
121 branchrev = repo.closed_branches.get(branch)
119 branchrev = repo.closed_branches.get(branch)
122 if branchrev:
120 if branchrev:
123 n = 'branch:%s:%s' % (branch, branchrev)
121 n = 'branch:%s:%s' % (branch, branchrev)
124 branches.append((n, _('%s (closed)') % branch))
122 branches.append((n, _('%s (closed)') % branch))
125 selected = n
123 selected = n
126 branch = None
124 branch = None
127 if branch:
125 if branch:
128 log.debug('branch %r not found in %s', branch, repo)
126 log.debug('branch %r not found in %s', branch, repo)
129
127
130 bookmarks = []
128 bookmarks = []
131 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
129 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
132 n = 'book:%s:%s' % (bookmark, bookmarkrev)
130 n = 'book:%s:%s' % (bookmark, bookmarkrev)
133 bookmarks.append((n, bookmark))
131 bookmarks.append((n, bookmark))
134 if rev == bookmarkrev:
132 if rev == bookmarkrev:
135 selected = n
133 selected = n
136
134
137 tags = []
135 tags = []
138 for tag, tagrev in repo.tags.iteritems():
136 for tag, tagrev in repo.tags.iteritems():
139 if tag == 'tip':
137 if tag == 'tip':
140 continue
138 continue
141 n = 'tag:%s:%s' % (tag, tagrev)
139 n = 'tag:%s:%s' % (tag, tagrev)
142 tags.append((n, tag))
140 tags.append((n, tag))
143 if rev == tagrev:
141 if rev == tagrev:
144 selected = n
142 selected = n
145
143
146 # prio 1: rev was selected as existing entry above
144 # prio 1: rev was selected as existing entry above
147
145
148 # prio 2: create special entry for rev; rev _must_ be used
146 # prio 2: create special entry for rev; rev _must_ be used
149 specials = []
147 specials = []
150 if rev and selected is None:
148 if rev and selected is None:
151 selected = 'rev:%s:%s' % (rev, rev)
149 selected = 'rev:%s:%s' % (rev, rev)
152 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
150 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
153
151
154 # prio 3: most recent peer branch
152 # prio 3: most recent peer branch
155 if peers and not selected:
153 if peers and not selected:
156 selected = peers[0][0]
154 selected = peers[0][0]
157
155
158 # prio 4: tip revision
156 # prio 4: tip revision
159 if not selected:
157 if not selected:
160 if h.is_hg(repo):
158 if h.is_hg(repo):
161 if tipbranch:
159 if tipbranch:
162 selected = 'branch:%s:%s' % (tipbranch, tiprev)
160 selected = 'branch:%s:%s' % (tipbranch, tiprev)
163 else:
161 else:
164 selected = 'tag:null:' + repo.EMPTY_CHANGESET
162 selected = 'tag:null:' + repo.EMPTY_CHANGESET
165 tags.append((selected, 'null'))
163 tags.append((selected, 'null'))
166 else:
164 else:
167 if 'master' in repo.branches:
165 if 'master' in repo.branches:
168 selected = 'branch:master:%s' % repo.branches['master']
166 selected = 'branch:master:%s' % repo.branches['master']
169 else:
167 else:
170 k, v = repo.branches.items()[0]
168 k, v = repo.branches.items()[0]
171 selected = 'branch:%s:%s' % (k, v)
169 selected = 'branch:%s:%s' % (k, v)
172
170
173 groups = [(specials, _("Special")),
171 groups = [(specials, _("Special")),
174 (peers, _("Peer branches")),
172 (peers, _("Peer branches")),
175 (bookmarks, _("Bookmarks")),
173 (bookmarks, _("Bookmarks")),
176 (branches, _("Branches")),
174 (branches, _("Branches")),
177 (tags, _("Tags")),
175 (tags, _("Tags")),
178 ]
176 ]
179 return [g for g in groups if g[0]], selected
177 return [g for g in groups if g[0]], selected
180
178
181 def _get_is_allowed_change_status(self, pull_request):
179 def _get_is_allowed_change_status(self, pull_request):
182 if pull_request.is_closed():
180 if pull_request.is_closed():
183 return False
181 return False
184
182
185 owner = self.authuser.user_id == pull_request.user_id
183 owner = self.authuser.user_id == pull_request.user_id
186 reviewer = self.authuser.user_id in [x.user_id for x in
184 reviewer = self.authuser.user_id in [x.user_id for x in
187 pull_request.reviewers]
185 pull_request.reviewers]
188 return self.authuser.admin or owner or reviewer
186 return self.authuser.admin or owner or reviewer
189
187
190 @LoginRequired()
188 @LoginRequired()
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 'repository.admin')
190 'repository.admin')
193 def show_all(self, repo_name):
191 def show_all(self, repo_name):
194 c.from_ = request.GET.get('from_') or ''
192 c.from_ = request.GET.get('from_') or ''
195 c.closed = request.GET.get('closed') or ''
193 c.closed = request.GET.get('closed') or ''
196 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
194 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
197 c.repo_name = repo_name
195 c.repo_name = repo_name
198 p = safe_int(request.GET.get('page', 1), 1)
196 p = safe_int(request.GET.get('page', 1), 1)
199
197
200 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
198 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
201
199
202 return render('/pullrequests/pullrequest_show_all.html')
200 return render('/pullrequests/pullrequest_show_all.html')
203
201
204 @LoginRequired()
202 @LoginRequired()
205 @NotAnonymous()
203 @NotAnonymous()
206 def show_my(self):
204 def show_my(self):
207 c.closed = request.GET.get('closed') or ''
205 c.closed = request.GET.get('closed') or ''
208
206
209 def _filter(pr):
207 def _filter(pr):
210 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
208 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
211 if not c.closed:
209 if not c.closed:
212 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
210 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
213 return s
211 return s
214
212
215 c.my_pull_requests = _filter(PullRequest.query()\
213 c.my_pull_requests = _filter(PullRequest.query()\
216 .filter(PullRequest.user_id ==
214 .filter(PullRequest.user_id ==
217 self.authuser.user_id)\
215 self.authuser.user_id)\
218 .all())
216 .all())
219
217
220 c.participate_in_pull_requests = _filter(PullRequest.query()\
218 c.participate_in_pull_requests = _filter(PullRequest.query()\
221 .join(PullRequestReviewers)\
219 .join(PullRequestReviewers)\
222 .filter(PullRequestReviewers.user_id ==
220 .filter(PullRequestReviewers.user_id ==
223 self.authuser.user_id)\
221 self.authuser.user_id)\
224 )
222 )
225
223
226 return render('/pullrequests/pullrequest_show_my.html')
224 return render('/pullrequests/pullrequest_show_my.html')
227
225
228 @LoginRequired()
226 @LoginRequired()
229 @NotAnonymous()
227 @NotAnonymous()
230 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
228 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
231 'repository.admin')
229 'repository.admin')
232 def index(self):
230 def index(self):
233 org_repo = c.db_repo
231 org_repo = c.db_repo
234 org_scm_instance = org_repo.scm_instance
232 org_scm_instance = org_repo.scm_instance
235 try:
233 try:
236 org_scm_instance.get_changeset()
234 org_scm_instance.get_changeset()
237 except EmptyRepositoryError as e:
235 except EmptyRepositoryError as e:
238 h.flash(h.literal(_('There are no changesets yet')),
236 h.flash(h.literal(_('There are no changesets yet')),
239 category='warning')
237 category='warning')
240 redirect(url('summary_home', repo_name=org_repo.repo_name))
238 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
241
239
242 org_rev = request.GET.get('rev_end')
240 org_rev = request.GET.get('rev_end')
243 # rev_start is not directly useful - its parent could however be used
241 # rev_start is not directly useful - its parent could however be used
244 # as default for other and thus give a simple compare view
242 # as default for other and thus give a simple compare view
245 rev_start = request.GET.get('rev_start')
243 rev_start = request.GET.get('rev_start')
246 other_rev = None
244 other_rev = None
247 if rev_start:
245 if rev_start:
248 starters = org_repo.get_changeset(rev_start).parents
246 starters = org_repo.get_changeset(rev_start).parents
249 if starters:
247 if starters:
250 other_rev = starters[0].raw_id
248 other_rev = starters[0].raw_id
251 else:
249 else:
252 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
250 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
253 branch = request.GET.get('branch')
251 branch = request.GET.get('branch')
254
252
255 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
253 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
256 c.default_cs_repo = org_repo.repo_name
254 c.default_cs_repo = org_repo.repo_name
257 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
255 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
258
256
259 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
257 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
260 if default_cs_ref_type != 'branch':
258 if default_cs_ref_type != 'branch':
261 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
259 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
262
260
263 # add org repo to other so we can open pull request against peer branches on itself
261 # add org repo to other so we can open pull request against peer branches on itself
264 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
262 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
265
263
266 if org_repo.parent:
264 if org_repo.parent:
267 # add parent of this fork also and select it.
265 # add parent of this fork also and select it.
268 # use the same branch on destination as on source, if available.
266 # use the same branch on destination as on source, if available.
269 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
267 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
270 c.a_repo = org_repo.parent
268 c.a_repo = org_repo.parent
271 c.a_refs, c.default_a_ref = self._get_repo_refs(
269 c.a_refs, c.default_a_ref = self._get_repo_refs(
272 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
270 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
273
271
274 else:
272 else:
275 c.a_repo = org_repo
273 c.a_repo = org_repo
276 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
274 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
277
275
278 # gather forks and add to this list ... even though it is rare to
276 # gather forks and add to this list ... even though it is rare to
279 # request forks to pull from their parent
277 # request forks to pull from their parent
280 for fork in org_repo.forks:
278 for fork in org_repo.forks:
281 c.a_repos.append((fork.repo_name, fork.repo_name))
279 c.a_repos.append((fork.repo_name, fork.repo_name))
282
280
283 return render('/pullrequests/pullrequest.html')
281 return render('/pullrequests/pullrequest.html')
284
282
285 @LoginRequired()
283 @LoginRequired()
286 @NotAnonymous()
284 @NotAnonymous()
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 'repository.admin')
286 'repository.admin')
289 @jsonify
287 @jsonify
290 def repo_info(self, repo_name):
288 def repo_info(self, repo_name):
291 repo = RepoModel()._get_repo(repo_name)
289 repo = RepoModel()._get_repo(repo_name)
292 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
290 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
293 return {
291 return {
294 'description': repo.description.split('\n', 1)[0],
292 'description': repo.description.split('\n', 1)[0],
295 'selected_ref': selected_ref,
293 'selected_ref': selected_ref,
296 'refs': refs,
294 'refs': refs,
297 }
295 }
298
296
299 @LoginRequired()
297 @LoginRequired()
300 @NotAnonymous()
298 @NotAnonymous()
301 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
299 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
302 'repository.admin')
300 'repository.admin')
303 def create(self, repo_name):
301 def create(self, repo_name):
304 repo = RepoModel()._get_repo(repo_name)
302 repo = RepoModel()._get_repo(repo_name)
305 try:
303 try:
306 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
304 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
307 except formencode.Invalid as errors:
305 except formencode.Invalid as errors:
308 log.error(traceback.format_exc())
306 log.error(traceback.format_exc())
309 log.error(str(errors))
307 log.error(str(errors))
310 msg = _('Error creating pull request: %s') % errors.msg
308 msg = _('Error creating pull request: %s') % errors.msg
311 h.flash(msg, 'error')
309 h.flash(msg, 'error')
312 raise HTTPBadRequest
310 raise HTTPBadRequest
313
311
314 # heads up: org and other might seem backward here ...
312 # heads up: org and other might seem backward here ...
315 org_repo_name = _form['org_repo']
313 org_repo_name = _form['org_repo']
316 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
314 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
317 org_repo = RepoModel()._get_repo(org_repo_name)
315 org_repo = RepoModel()._get_repo(org_repo_name)
318 (org_ref_type,
316 (org_ref_type,
319 org_ref_name,
317 org_ref_name,
320 org_rev) = org_ref.split(':')
318 org_rev) = org_ref.split(':')
321 if org_ref_type == 'rev':
319 if org_ref_type == 'rev':
322 org_ref_type = 'branch'
320 org_ref_type = 'branch'
323 cs = org_repo.scm_instance.get_changeset(org_rev)
321 cs = org_repo.scm_instance.get_changeset(org_rev)
324 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
322 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
325
323
326 other_repo_name = _form['other_repo']
324 other_repo_name = _form['other_repo']
327 other_ref = _form['other_ref'] # will have symbolic name and head revision
325 other_ref = _form['other_ref'] # will have symbolic name and head revision
328 other_repo = RepoModel()._get_repo(other_repo_name)
326 other_repo = RepoModel()._get_repo(other_repo_name)
329 (other_ref_type,
327 (other_ref_type,
330 other_ref_name,
328 other_ref_name,
331 other_rev) = other_ref.split(':')
329 other_rev) = other_ref.split(':')
332
330
333 cs_ranges, _cs_ranges_not, ancestor_rev = \
331 cs_ranges, _cs_ranges_not, ancestor_rev = \
334 CompareController._get_changesets(org_repo.scm_instance.alias,
332 CompareController._get_changesets(org_repo.scm_instance.alias,
335 other_repo.scm_instance, other_rev, # org and other "swapped"
333 other_repo.scm_instance, other_rev, # org and other "swapped"
336 org_repo.scm_instance, org_rev,
334 org_repo.scm_instance, org_rev,
337 )
335 )
338 if ancestor_rev is None:
336 if ancestor_rev is None:
339 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
337 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
340 revisions = [cs_.raw_id for cs_ in cs_ranges]
338 revisions = [cs_.raw_id for cs_ in cs_ranges]
341
339
342 # hack: ancestor_rev is not an other_rev but we want to show the
340 # hack: ancestor_rev is not an other_rev but we want to show the
343 # requested destination and have the exact ancestor
341 # requested destination and have the exact ancestor
344 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
342 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
345
343
346 reviewers = _form['review_members']
344 reviewers = _form['review_members']
347
345
348 title = _form['pullrequest_title']
346 title = _form['pullrequest_title']
349 if not title:
347 if not title:
350 if org_repo_name == other_repo_name:
348 if org_repo_name == other_repo_name:
351 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
349 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
352 h.short_ref(other_ref_type, other_ref_name))
350 h.short_ref(other_ref_type, other_ref_name))
353 else:
351 else:
354 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
352 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
355 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
353 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
356 description = _form['pullrequest_desc'].strip() or _('No description')
354 description = _form['pullrequest_desc'].strip() or _('No description')
357 try:
355 try:
358 pull_request = PullRequestModel().create(
356 pull_request = PullRequestModel().create(
359 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
357 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
360 other_ref, revisions, reviewers, title, description
358 other_ref, revisions, reviewers, title, description
361 )
359 )
362 Session().commit()
360 Session().commit()
363 h.flash(_('Successfully opened new pull request'),
361 h.flash(_('Successfully opened new pull request'),
364 category='success')
362 category='success')
365 except UserInvalidException as u:
363 except UserInvalidException as u:
366 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
364 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
367 raise HTTPBadRequest()
365 raise HTTPBadRequest()
368 except Exception:
366 except Exception:
369 h.flash(_('Error occurred while creating pull request'),
367 h.flash(_('Error occurred while creating pull request'),
370 category='error')
368 category='error')
371 log.error(traceback.format_exc())
369 log.error(traceback.format_exc())
372 return redirect(url('pullrequest_home', repo_name=repo_name))
370 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
373
371
374 return redirect(pull_request.url())
372 raise HTTPFound(location=pull_request.url())
375
373
376 def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
374 def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
377 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
375 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
378 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
376 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
379 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
377 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
380
378
381 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
379 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
382 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
380 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
383 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
381 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
384 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
382 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
385
383
386 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
384 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
387 other_repo.scm_instance, new_other_rev, # org and other "swapped"
385 other_repo.scm_instance, new_other_rev, # org and other "swapped"
388 org_repo.scm_instance, new_org_rev)
386 org_repo.scm_instance, new_org_rev)
389
387
390 old_revisions = set(old_pull_request.revisions)
388 old_revisions = set(old_pull_request.revisions)
391 revisions = [cs.raw_id for cs in cs_ranges]
389 revisions = [cs.raw_id for cs in cs_ranges]
392 new_revisions = [r for r in revisions if r not in old_revisions]
390 new_revisions = [r for r in revisions if r not in old_revisions]
393 lost = old_revisions.difference(revisions)
391 lost = old_revisions.difference(revisions)
394
392
395 infos = ['This is an update of %s "%s".' %
393 infos = ['This is an update of %s "%s".' %
396 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
394 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
397 pull_request_id=old_pull_request.pull_request_id),
395 pull_request_id=old_pull_request.pull_request_id),
398 old_pull_request.title)]
396 old_pull_request.title)]
399
397
400 if lost:
398 if lost:
401 infos.append(_('Missing changesets since the previous pull request:'))
399 infos.append(_('Missing changesets since the previous pull request:'))
402 for r in old_pull_request.revisions:
400 for r in old_pull_request.revisions:
403 if r in lost:
401 if r in lost:
404 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
402 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
405 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
403 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
406
404
407 if new_revisions:
405 if new_revisions:
408 infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
406 infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
409 for r in reversed(revisions):
407 for r in reversed(revisions):
410 if r in new_revisions:
408 if r in new_revisions:
411 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
409 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
412 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
410 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
413
411
414 if ancestor_rev == other_rev:
412 if ancestor_rev == other_rev:
415 infos.append(_("Ancestor didn't change - show diff since previous version:"))
413 infos.append(_("Ancestor didn't change - show diff since previous version:"))
416 infos.append(h.canonical_url('compare_url',
414 infos.append(h.canonical_url('compare_url',
417 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
415 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
418 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
416 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
419 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
417 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
420 )) # note: linear diff, merge or not doesn't matter
418 )) # note: linear diff, merge or not doesn't matter
421 else:
419 else:
422 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
420 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
423 else:
421 else:
424 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
422 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
425 # TODO: fail?
423 # TODO: fail?
426
424
427 # hack: ancestor_rev is not an other_ref but we want to show the
425 # hack: ancestor_rev is not an other_ref but we want to show the
428 # requested destination and have the exact ancestor
426 # requested destination and have the exact ancestor
429 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
427 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
430 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
428 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
431
429
432 try:
430 try:
433 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
431 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
434 v = int(old_v) + 1
432 v = int(old_v) + 1
435 except (AttributeError, ValueError):
433 except (AttributeError, ValueError):
436 v = 2
434 v = 2
437 title = '%s (v%s)' % (title.strip(), v)
435 title = '%s (v%s)' % (title.strip(), v)
438
436
439 # using a mail-like separator, insert new update info at the top of the list
437 # using a mail-like separator, insert new update info at the top of the list
440 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
438 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
441 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
439 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
442 if len(descriptions) > 1:
440 if len(descriptions) > 1:
443 description += '\n\n' + descriptions[1].strip()
441 description += '\n\n' + descriptions[1].strip()
444
442
445 try:
443 try:
446 pull_request = PullRequestModel().create(
444 pull_request = PullRequestModel().create(
447 self.authuser.user_id,
445 self.authuser.user_id,
448 old_pull_request.org_repo.repo_name, new_org_ref,
446 old_pull_request.org_repo.repo_name, new_org_ref,
449 old_pull_request.other_repo.repo_name, new_other_ref,
447 old_pull_request.other_repo.repo_name, new_other_ref,
450 revisions, reviewers_ids, title, description
448 revisions, reviewers_ids, title, description
451 )
449 )
452 except UserInvalidException as u:
450 except UserInvalidException as u:
453 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
451 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
454 raise HTTPBadRequest()
452 raise HTTPBadRequest()
455 except Exception:
453 except Exception:
456 h.flash(_('Error occurred while creating pull request'),
454 h.flash(_('Error occurred while creating pull request'),
457 category='error')
455 category='error')
458 log.error(traceback.format_exc())
456 log.error(traceback.format_exc())
459 return redirect(old_pull_request.url())
457 raise HTTPFound(location=old_pull_request.url())
460
458
461 ChangesetCommentsModel().create(
459 ChangesetCommentsModel().create(
462 text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
460 text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
463 repo=old_pull_request.other_repo.repo_id,
461 repo=old_pull_request.other_repo.repo_id,
464 user=c.authuser.user_id,
462 user=c.authuser.user_id,
465 pull_request=old_pull_request.pull_request_id,
463 pull_request=old_pull_request.pull_request_id,
466 closing_pr=True)
464 closing_pr=True)
467 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
465 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
468
466
469 Session().commit()
467 Session().commit()
470 h.flash(_('Pull request update created'),
468 h.flash(_('Pull request update created'),
471 category='success')
469 category='success')
472
470
473 return redirect(pull_request.url())
471 raise HTTPFound(location=pull_request.url())
474
472
475 # pullrequest_post for PR editing
473 # pullrequest_post for PR editing
476 @LoginRequired()
474 @LoginRequired()
477 @NotAnonymous()
475 @NotAnonymous()
478 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
479 'repository.admin')
477 'repository.admin')
480 def post(self, repo_name, pull_request_id):
478 def post(self, repo_name, pull_request_id):
481 pull_request = PullRequest.get_or_404(pull_request_id)
479 pull_request = PullRequest.get_or_404(pull_request_id)
482 if pull_request.is_closed():
480 if pull_request.is_closed():
483 raise HTTPForbidden()
481 raise HTTPForbidden()
484 assert pull_request.other_repo.repo_name == repo_name
482 assert pull_request.other_repo.repo_name == repo_name
485 #only owner or admin can update it
483 #only owner or admin can update it
486 owner = pull_request.owner.user_id == c.authuser.user_id
484 owner = pull_request.owner.user_id == c.authuser.user_id
487 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
485 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
488 if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
486 if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
489 raise HTTPForbidden()
487 raise HTTPForbidden()
490
488
491 _form = PullRequestPostForm()().to_python(request.POST)
489 _form = PullRequestPostForm()().to_python(request.POST)
492 reviewers_ids = [int(s) for s in _form['review_members']]
490 reviewers_ids = [int(s) for s in _form['review_members']]
493
491
494 if _form['updaterev']:
492 if _form['updaterev']:
495 return self.create_update(pull_request,
493 return self.create_update(pull_request,
496 _form['updaterev'],
494 _form['updaterev'],
497 _form['pullrequest_title'],
495 _form['pullrequest_title'],
498 _form['pullrequest_desc'],
496 _form['pullrequest_desc'],
499 reviewers_ids)
497 reviewers_ids)
500
498
501 old_description = pull_request.description
499 old_description = pull_request.description
502 pull_request.title = _form['pullrequest_title']
500 pull_request.title = _form['pullrequest_title']
503 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
501 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
504 pull_request.owner = User.get_by_username(_form['owner'])
502 pull_request.owner = User.get_by_username(_form['owner'])
505 user = User.get(c.authuser.user_id)
503 user = User.get(c.authuser.user_id)
506 try:
504 try:
507 PullRequestModel().mention_from_description(user, pull_request, old_description)
505 PullRequestModel().mention_from_description(user, pull_request, old_description)
508 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
506 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
509 except UserInvalidException as u:
507 except UserInvalidException as u:
510 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
508 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
511 raise HTTPBadRequest()
509 raise HTTPBadRequest()
512
510
513 Session().commit()
511 Session().commit()
514 h.flash(_('Pull request updated'), category='success')
512 h.flash(_('Pull request updated'), category='success')
515
513
516 return redirect(pull_request.url())
514 raise HTTPFound(location=pull_request.url())
517
515
518 @LoginRequired()
516 @LoginRequired()
519 @NotAnonymous()
517 @NotAnonymous()
520 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
518 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
521 'repository.admin')
519 'repository.admin')
522 @jsonify
520 @jsonify
523 def delete(self, repo_name, pull_request_id):
521 def delete(self, repo_name, pull_request_id):
524 pull_request = PullRequest.get_or_404(pull_request_id)
522 pull_request = PullRequest.get_or_404(pull_request_id)
525 #only owner can delete it !
523 #only owner can delete it !
526 if pull_request.owner.user_id == c.authuser.user_id:
524 if pull_request.owner.user_id == c.authuser.user_id:
527 PullRequestModel().delete(pull_request)
525 PullRequestModel().delete(pull_request)
528 Session().commit()
526 Session().commit()
529 h.flash(_('Successfully deleted pull request'),
527 h.flash(_('Successfully deleted pull request'),
530 category='success')
528 category='success')
531 return redirect(url('my_pullrequests'))
529 raise HTTPFound(location=url('my_pullrequests'))
532 raise HTTPForbidden()
530 raise HTTPForbidden()
533
531
534 @LoginRequired()
532 @LoginRequired()
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
533 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 'repository.admin')
534 'repository.admin')
537 def show(self, repo_name, pull_request_id, extra=None):
535 def show(self, repo_name, pull_request_id, extra=None):
538 repo_model = RepoModel()
536 repo_model = RepoModel()
539 c.users_array = repo_model.get_users_js()
537 c.users_array = repo_model.get_users_js()
540 c.user_groups_array = repo_model.get_user_groups_js()
538 c.user_groups_array = repo_model.get_user_groups_js()
541 c.pull_request = PullRequest.get_or_404(pull_request_id)
539 c.pull_request = PullRequest.get_or_404(pull_request_id)
542 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
540 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
543 cc_model = ChangesetCommentsModel()
541 cc_model = ChangesetCommentsModel()
544 cs_model = ChangesetStatusModel()
542 cs_model = ChangesetStatusModel()
545
543
546 # pull_requests repo_name we opened it against
544 # pull_requests repo_name we opened it against
547 # ie. other_repo must match
545 # ie. other_repo must match
548 if repo_name != c.pull_request.other_repo.repo_name:
546 if repo_name != c.pull_request.other_repo.repo_name:
549 raise HTTPNotFound
547 raise HTTPNotFound
550
548
551 # load compare data into template context
549 # load compare data into template context
552 c.cs_repo = c.pull_request.org_repo
550 c.cs_repo = c.pull_request.org_repo
553 (c.cs_ref_type,
551 (c.cs_ref_type,
554 c.cs_ref_name,
552 c.cs_ref_name,
555 c.cs_rev) = c.pull_request.org_ref.split(':')
553 c.cs_rev) = c.pull_request.org_ref.split(':')
556
554
557 c.a_repo = c.pull_request.other_repo
555 c.a_repo = c.pull_request.other_repo
558 (c.a_ref_type,
556 (c.a_ref_type,
559 c.a_ref_name,
557 c.a_ref_name,
560 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
558 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
561
559
562 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
560 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
563 c.cs_repo = c.cs_repo
561 c.cs_repo = c.cs_repo
564 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
562 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
565 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
563 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
566 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
564 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
567 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
565 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
568
566
569 c.is_range = False
567 c.is_range = False
570 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
568 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
571 cs_a = org_scm_instance.get_changeset(c.a_rev)
569 cs_a = org_scm_instance.get_changeset(c.a_rev)
572 root_parents = c.cs_ranges[0].parents
570 root_parents = c.cs_ranges[0].parents
573 c.is_range = cs_a in root_parents
571 c.is_range = cs_a in root_parents
574 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
572 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
575
573
576 avail_revs = set()
574 avail_revs = set()
577 avail_show = []
575 avail_show = []
578 c.cs_branch_name = c.cs_ref_name
576 c.cs_branch_name = c.cs_ref_name
579 other_scm_instance = c.a_repo.scm_instance
577 other_scm_instance = c.a_repo.scm_instance
580 c.update_msg = ""
578 c.update_msg = ""
581 c.update_msg_other = ""
579 c.update_msg_other = ""
582 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
580 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
583 if c.cs_ref_type != 'branch':
581 if c.cs_ref_type != 'branch':
584 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
582 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
585 c.a_branch_name = c.a_ref_name
583 c.a_branch_name = c.a_ref_name
586 if c.a_ref_type != 'branch':
584 if c.a_ref_type != 'branch':
587 try:
585 try:
588 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
586 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
589 except EmptyRepositoryError:
587 except EmptyRepositoryError:
590 c.a_branch_name = 'null' # not a branch name ... but close enough
588 c.a_branch_name = 'null' # not a branch name ... but close enough
591 # candidates: descendants of old head that are on the right branch
589 # candidates: descendants of old head that are on the right branch
592 # and not are the old head itself ...
590 # and not are the old head itself ...
593 # and nothing at all if old head is a descendant of target ref name
591 # and nothing at all if old head is a descendant of target ref name
594 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
592 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
595 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
593 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
596 elif c.pull_request.is_closed():
594 elif c.pull_request.is_closed():
597 c.update_msg = _('This pull request has been closed and can not be updated.')
595 c.update_msg = _('This pull request has been closed and can not be updated.')
598 else: # look for descendants of PR head on source branch in org repo
596 else: # look for descendants of PR head on source branch in org repo
599 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
597 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
600 revs[0], c.cs_branch_name)
598 revs[0], c.cs_branch_name)
601 if len(avail_revs) > 1: # more than just revs[0]
599 if len(avail_revs) > 1: # more than just revs[0]
602 # also show changesets that not are descendants but would be merged in
600 # also show changesets that not are descendants but would be merged in
603 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
601 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
604 if org_scm_instance.path != other_scm_instance.path:
602 if org_scm_instance.path != other_scm_instance.path:
605 # Note: org_scm_instance.path must come first so all
603 # Note: org_scm_instance.path must come first so all
606 # valid revision numbers are 100% org_scm compatible
604 # valid revision numbers are 100% org_scm compatible
607 # - both for avail_revs and for revset results
605 # - both for avail_revs and for revset results
608 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
606 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
609 org_scm_instance.path,
607 org_scm_instance.path,
610 other_scm_instance.path)
608 other_scm_instance.path)
611 else:
609 else:
612 hgrepo = org_scm_instance._repo
610 hgrepo = org_scm_instance._repo
613 show = set(hgrepo.revs('::%ld & !::%s & !::%s',
611 show = set(hgrepo.revs('::%ld & !::%s & !::%s',
614 avail_revs, revs[0], targethead))
612 avail_revs, revs[0], targethead))
615 c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
613 c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
616 else:
614 else:
617 show = set()
615 show = set()
618 avail_revs = set() # drop revs[0]
616 avail_revs = set() # drop revs[0]
619 c.update_msg = _('No changesets found for updating this pull request.')
617 c.update_msg = _('No changesets found for updating this pull request.')
620
618
621 # TODO: handle branch heads that not are tip-most
619 # TODO: handle branch heads that not are tip-most
622 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
620 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
623 if brevs:
621 if brevs:
624 # also show changesets that are on branch but neither ancestors nor descendants
622 # also show changesets that are on branch but neither ancestors nor descendants
625 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
623 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
626 show.add(revs[0]) # make sure graph shows this so we can see how they relate
624 show.add(revs[0]) # make sure graph shows this so we can see how they relate
627 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
625 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
628 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
626 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
629
627
630 avail_show = sorted(show, reverse=True)
628 avail_show = sorted(show, reverse=True)
631
629
632 elif org_scm_instance.alias == 'git':
630 elif org_scm_instance.alias == 'git':
633 c.update_msg = _("Git pull requests don't support updates yet.")
631 c.update_msg = _("Git pull requests don't support updates yet.")
634
632
635 c.avail_revs = avail_revs
633 c.avail_revs = avail_revs
636 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
634 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
637 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
635 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
638
636
639 raw_ids = [x.raw_id for x in c.cs_ranges]
637 raw_ids = [x.raw_id for x in c.cs_ranges]
640 c.cs_comments = c.cs_repo.get_comments(raw_ids)
638 c.cs_comments = c.cs_repo.get_comments(raw_ids)
641 c.statuses = c.cs_repo.statuses(raw_ids)
639 c.statuses = c.cs_repo.statuses(raw_ids)
642
640
643 ignore_whitespace = request.GET.get('ignorews') == '1'
641 ignore_whitespace = request.GET.get('ignorews') == '1'
644 line_context = request.GET.get('context', 3)
642 line_context = request.GET.get('context', 3)
645 c.ignorews_url = _ignorews_url
643 c.ignorews_url = _ignorews_url
646 c.context_url = _context_url
644 c.context_url = _context_url
647 c.fulldiff = request.GET.get('fulldiff')
645 c.fulldiff = request.GET.get('fulldiff')
648 diff_limit = self.cut_off_limit if not c.fulldiff else None
646 diff_limit = self.cut_off_limit if not c.fulldiff else None
649
647
650 # we swap org/other ref since we run a simple diff on one repo
648 # we swap org/other ref since we run a simple diff on one repo
651 log.debug('running diff between %s and %s in %s',
649 log.debug('running diff between %s and %s in %s',
652 c.a_rev, c.cs_rev, org_scm_instance.path)
650 c.a_rev, c.cs_rev, org_scm_instance.path)
653 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
651 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
654 ignore_whitespace=ignore_whitespace,
652 ignore_whitespace=ignore_whitespace,
655 context=line_context)
653 context=line_context)
656
654
657 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
655 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
658 diff_limit=diff_limit)
656 diff_limit=diff_limit)
659 _parsed = diff_processor.prepare()
657 _parsed = diff_processor.prepare()
660
658
661 c.limited_diff = False
659 c.limited_diff = False
662 if isinstance(_parsed, LimitedDiffContainer):
660 if isinstance(_parsed, LimitedDiffContainer):
663 c.limited_diff = True
661 c.limited_diff = True
664
662
665 c.files = []
663 c.files = []
666 c.changes = {}
664 c.changes = {}
667 c.lines_added = 0
665 c.lines_added = 0
668 c.lines_deleted = 0
666 c.lines_deleted = 0
669
667
670 for f in _parsed:
668 for f in _parsed:
671 st = f['stats']
669 st = f['stats']
672 c.lines_added += st['added']
670 c.lines_added += st['added']
673 c.lines_deleted += st['deleted']
671 c.lines_deleted += st['deleted']
674 fid = h.FID('', f['filename'])
672 fid = h.FID('', f['filename'])
675 c.files.append([fid, f['operation'], f['filename'], f['stats']])
673 c.files.append([fid, f['operation'], f['filename'], f['stats']])
676 htmldiff = diff_processor.as_html(enable_comments=True,
674 htmldiff = diff_processor.as_html(enable_comments=True,
677 parsed_lines=[f])
675 parsed_lines=[f])
678 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
676 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
679
677
680 # inline comments
678 # inline comments
681 c.inline_cnt = 0
679 c.inline_cnt = 0
682 c.inline_comments = cc_model.get_inline_comments(
680 c.inline_comments = cc_model.get_inline_comments(
683 c.db_repo.repo_id,
681 c.db_repo.repo_id,
684 pull_request=pull_request_id)
682 pull_request=pull_request_id)
685 # count inline comments
683 # count inline comments
686 for __, lines in c.inline_comments:
684 for __, lines in c.inline_comments:
687 for comments in lines.values():
685 for comments in lines.values():
688 c.inline_cnt += len(comments)
686 c.inline_cnt += len(comments)
689 # comments
687 # comments
690 c.comments = cc_model.get_comments(c.db_repo.repo_id,
688 c.comments = cc_model.get_comments(c.db_repo.repo_id,
691 pull_request=pull_request_id)
689 pull_request=pull_request_id)
692
690
693 # (badly named) pull-request status calculation based on reviewer votes
691 # (badly named) pull-request status calculation based on reviewer votes
694 (c.pull_request_reviewers,
692 (c.pull_request_reviewers,
695 c.pull_request_pending_reviewers,
693 c.pull_request_pending_reviewers,
696 c.current_voting_result,
694 c.current_voting_result,
697 ) = cs_model.calculate_pull_request_result(c.pull_request)
695 ) = cs_model.calculate_pull_request_result(c.pull_request)
698 c.changeset_statuses = ChangesetStatus.STATUSES
696 c.changeset_statuses = ChangesetStatus.STATUSES
699
697
700 c.as_form = False
698 c.as_form = False
701 c.ancestor = None # there is one - but right here we don't know which
699 c.ancestor = None # there is one - but right here we don't know which
702 return render('/pullrequests/pullrequest_show.html')
700 return render('/pullrequests/pullrequest_show.html')
703
701
704 @LoginRequired()
702 @LoginRequired()
705 @NotAnonymous()
703 @NotAnonymous()
706 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
704 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
707 'repository.admin')
705 'repository.admin')
708 @jsonify
706 @jsonify
709 def comment(self, repo_name, pull_request_id):
707 def comment(self, repo_name, pull_request_id):
710 pull_request = PullRequest.get_or_404(pull_request_id)
708 pull_request = PullRequest.get_or_404(pull_request_id)
711
709
712 status = request.POST.get('changeset_status')
710 status = request.POST.get('changeset_status')
713 close_pr = request.POST.get('save_close')
711 close_pr = request.POST.get('save_close')
714 f_path = request.POST.get('f_path')
712 f_path = request.POST.get('f_path')
715 line_no = request.POST.get('line')
713 line_no = request.POST.get('line')
716
714
717 if (status or close_pr) and (f_path or line_no):
715 if (status or close_pr) and (f_path or line_no):
718 # status votes and closing is only possible in general comments
716 # status votes and closing is only possible in general comments
719 raise HTTPBadRequest()
717 raise HTTPBadRequest()
720
718
721 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
719 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
722 if not allowed_to_change_status:
720 if not allowed_to_change_status:
723 if status or close_pr:
721 if status or close_pr:
724 h.flash(_('No permission to change pull request status'), 'error')
722 h.flash(_('No permission to change pull request status'), 'error')
725 raise HTTPForbidden()
723 raise HTTPForbidden()
726
724
727 text = request.POST.get('text', '').strip()
725 text = request.POST.get('text', '').strip()
728 if close_pr:
726 if close_pr:
729 text = _('Closing.') + '\n' + text
727 text = _('Closing.') + '\n' + text
730
728
731 comment = ChangesetCommentsModel().create(
729 comment = ChangesetCommentsModel().create(
732 text=text,
730 text=text,
733 repo=c.db_repo.repo_id,
731 repo=c.db_repo.repo_id,
734 user=c.authuser.user_id,
732 user=c.authuser.user_id,
735 pull_request=pull_request_id,
733 pull_request=pull_request_id,
736 f_path=f_path,
734 f_path=f_path,
737 line_no=line_no,
735 line_no=line_no,
738 status_change=(ChangesetStatus.get_status_lbl(status)
736 status_change=(ChangesetStatus.get_status_lbl(status)
739 if status and allowed_to_change_status else None),
737 if status and allowed_to_change_status else None),
740 closing_pr=close_pr
738 closing_pr=close_pr
741 )
739 )
742
740
743 action_logger(self.authuser,
741 action_logger(self.authuser,
744 'user_commented_pull_request:%s' % pull_request_id,
742 'user_commented_pull_request:%s' % pull_request_id,
745 c.db_repo, self.ip_addr, self.sa)
743 c.db_repo, self.ip_addr, self.sa)
746
744
747 if status:
745 if status:
748 ChangesetStatusModel().set_status(
746 ChangesetStatusModel().set_status(
749 c.db_repo.repo_id,
747 c.db_repo.repo_id,
750 status,
748 status,
751 c.authuser.user_id,
749 c.authuser.user_id,
752 comment,
750 comment,
753 pull_request=pull_request_id
751 pull_request=pull_request_id
754 )
752 )
755
753
756 if close_pr:
754 if close_pr:
757 PullRequestModel().close_pull_request(pull_request_id)
755 PullRequestModel().close_pull_request(pull_request_id)
758 action_logger(self.authuser,
756 action_logger(self.authuser,
759 'user_closed_pull_request:%s' % pull_request_id,
757 'user_closed_pull_request:%s' % pull_request_id,
760 c.db_repo, self.ip_addr, self.sa)
758 c.db_repo, self.ip_addr, self.sa)
761
759
762 Session().commit()
760 Session().commit()
763
761
764 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
762 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
765 return redirect(pull_request.url())
763 raise HTTPFound(location=pull_request.url())
766
764
767 data = {
765 data = {
768 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
766 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
769 }
767 }
770 if comment is not None:
768 if comment is not None:
771 c.comment = comment
769 c.comment = comment
772 data.update(comment.get_dict())
770 data.update(comment.get_dict())
773 data.update({'rendered_text':
771 data.update({'rendered_text':
774 render('changeset/changeset_comment_block.html')})
772 render('changeset/changeset_comment_block.html')})
775
773
776 return data
774 return data
777
775
778 @LoginRequired()
776 @LoginRequired()
779 @NotAnonymous()
777 @NotAnonymous()
780 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
778 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
781 'repository.admin')
779 'repository.admin')
782 @jsonify
780 @jsonify
783 def delete_comment(self, repo_name, comment_id):
781 def delete_comment(self, repo_name, comment_id):
784 co = ChangesetComment.get(comment_id)
782 co = ChangesetComment.get(comment_id)
785 if co.pull_request.is_closed():
783 if co.pull_request.is_closed():
786 #don't allow deleting comments on closed pull request
784 #don't allow deleting comments on closed pull request
787 raise HTTPForbidden()
785 raise HTTPForbidden()
788
786
789 owner = co.author.user_id == c.authuser.user_id
787 owner = co.author.user_id == c.authuser.user_id
790 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
788 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
791 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
789 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
792 ChangesetCommentsModel().delete(comment=co)
790 ChangesetCommentsModel().delete(comment=co)
793 Session().commit()
791 Session().commit()
794 return True
792 return True
795 else:
793 else:
796 raise HTTPForbidden()
794 raise HTTPForbidden()
@@ -1,1325 +1,1324 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.auth
15 kallithea.lib.auth
16 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
17
17
18 authentication and permission libraries
18 authentication and permission libraries
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 4, 2010
22 :created_on: Apr 4, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27 import time
27 import time
28 import os
28 import os
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import hashlib
31 import hashlib
32 import itertools
32 import itertools
33 import collections
33 import collections
34
34
35 from decorator import decorator
35 from decorator import decorator
36
36
37 from pylons import url, request, session
37 from pylons import url, request, session
38 from pylons.controllers.util import redirect
39 from pylons.i18n.translation import _
38 from pylons.i18n.translation import _
40 from webhelpers.pylonslib import secure_form
39 from webhelpers.pylonslib import secure_form
41 from sqlalchemy import or_
40 from sqlalchemy import or_
42 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm.exc import ObjectDeletedError
43 from sqlalchemy.orm import joinedload
42 from sqlalchemy.orm import joinedload
44 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
45
44
46 from kallithea import __platform__, is_windows, is_unix
45 from kallithea import __platform__, is_windows, is_unix
47 from kallithea.lib.vcs.utils.lazy import LazyProperty
46 from kallithea.lib.vcs.utils.lazy import LazyProperty
48 from kallithea.model import meta
47 from kallithea.model import meta
49 from kallithea.model.meta import Session
48 from kallithea.model.meta import Session
50 from kallithea.model.user import UserModel
49 from kallithea.model.user import UserModel
51 from kallithea.model.db import User, Repository, Permission, \
50 from kallithea.model.db import User, Repository, Permission, \
52 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
51 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
53 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
52 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
54 UserGroup, UserApiKeys
53 UserGroup, UserApiKeys
55
54
56 from kallithea.lib.utils2 import safe_unicode, aslist
55 from kallithea.lib.utils2 import safe_unicode, aslist
57 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
56 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
58 get_user_group_slug, conditional_cache
57 get_user_group_slug, conditional_cache
59 from kallithea.lib.caching_query import FromCache
58 from kallithea.lib.caching_query import FromCache
60
59
61
60
62 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
63
62
64
63
65 class PasswordGenerator(object):
64 class PasswordGenerator(object):
66 """
65 """
67 This is a simple class for generating password from different sets of
66 This is a simple class for generating password from different sets of
68 characters
67 characters
69 usage::
68 usage::
70
69
71 passwd_gen = PasswordGenerator()
70 passwd_gen = PasswordGenerator()
72 #print 8-letter password containing only big and small letters
71 #print 8-letter password containing only big and small letters
73 of alphabet
72 of alphabet
74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 """
74 """
76 ALPHABETS_NUM = r'''1234567890'''
75 ALPHABETS_NUM = r'''1234567890'''
77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86
85
87 def gen_password(self, length, alphabet=ALPHABETS_FULL):
86 def gen_password(self, length, alphabet=ALPHABETS_FULL):
88 assert len(alphabet) <= 256, alphabet
87 assert len(alphabet) <= 256, alphabet
89 l = []
88 l = []
90 while len(l) < length:
89 while len(l) < length:
91 i = ord(os.urandom(1))
90 i = ord(os.urandom(1))
92 if i < len(alphabet):
91 if i < len(alphabet):
93 l.append(alphabet[i])
92 l.append(alphabet[i])
94 return ''.join(l)
93 return ''.join(l)
95
94
96
95
97 class KallitheaCrypto(object):
96 class KallitheaCrypto(object):
98
97
99 @classmethod
98 @classmethod
100 def hash_string(cls, str_):
99 def hash_string(cls, str_):
101 """
100 """
102 Cryptographic function used for password hashing based on pybcrypt
101 Cryptographic function used for password hashing based on pybcrypt
103 or Python's own OpenSSL wrapper on windows
102 or Python's own OpenSSL wrapper on windows
104
103
105 :param password: password to hash
104 :param password: password to hash
106 """
105 """
107 if is_windows:
106 if is_windows:
108 return hashlib.sha256(str_).hexdigest()
107 return hashlib.sha256(str_).hexdigest()
109 elif is_unix:
108 elif is_unix:
110 import bcrypt
109 import bcrypt
111 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
110 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
112 else:
111 else:
113 raise Exception('Unknown or unsupported platform %s' \
112 raise Exception('Unknown or unsupported platform %s' \
114 % __platform__)
113 % __platform__)
115
114
116 @classmethod
115 @classmethod
117 def hash_check(cls, password, hashed):
116 def hash_check(cls, password, hashed):
118 """
117 """
119 Checks matching password with it's hashed value, runs different
118 Checks matching password with it's hashed value, runs different
120 implementation based on platform it runs on
119 implementation based on platform it runs on
121
120
122 :param password: password
121 :param password: password
123 :param hashed: password in hashed form
122 :param hashed: password in hashed form
124 """
123 """
125
124
126 if is_windows:
125 if is_windows:
127 return hashlib.sha256(password).hexdigest() == hashed
126 return hashlib.sha256(password).hexdigest() == hashed
128 elif is_unix:
127 elif is_unix:
129 import bcrypt
128 import bcrypt
130 return bcrypt.hashpw(password, hashed) == hashed
129 return bcrypt.hashpw(password, hashed) == hashed
131 else:
130 else:
132 raise Exception('Unknown or unsupported platform %s' \
131 raise Exception('Unknown or unsupported platform %s' \
133 % __platform__)
132 % __platform__)
134
133
135
134
136 def get_crypt_password(password):
135 def get_crypt_password(password):
137 return KallitheaCrypto.hash_string(password)
136 return KallitheaCrypto.hash_string(password)
138
137
139
138
140 def check_password(password, hashed):
139 def check_password(password, hashed):
141 return KallitheaCrypto.hash_check(password, hashed)
140 return KallitheaCrypto.hash_check(password, hashed)
142
141
143
142
144
143
145 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
144 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
146 explicit, algo):
145 explicit, algo):
147 RK = 'repositories'
146 RK = 'repositories'
148 GK = 'repositories_groups'
147 GK = 'repositories_groups'
149 UK = 'user_groups'
148 UK = 'user_groups'
150 GLOBAL = 'global'
149 GLOBAL = 'global'
151 PERM_WEIGHTS = Permission.PERM_WEIGHTS
150 PERM_WEIGHTS = Permission.PERM_WEIGHTS
152 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
151 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
153
152
154 def _choose_perm(new_perm, cur_perm):
153 def _choose_perm(new_perm, cur_perm):
155 new_perm_val = PERM_WEIGHTS[new_perm]
154 new_perm_val = PERM_WEIGHTS[new_perm]
156 cur_perm_val = PERM_WEIGHTS[cur_perm]
155 cur_perm_val = PERM_WEIGHTS[cur_perm]
157 if algo == 'higherwin':
156 if algo == 'higherwin':
158 if new_perm_val > cur_perm_val:
157 if new_perm_val > cur_perm_val:
159 return new_perm
158 return new_perm
160 return cur_perm
159 return cur_perm
161 elif algo == 'lowerwin':
160 elif algo == 'lowerwin':
162 if new_perm_val < cur_perm_val:
161 if new_perm_val < cur_perm_val:
163 return new_perm
162 return new_perm
164 return cur_perm
163 return cur_perm
165
164
166 #======================================================================
165 #======================================================================
167 # fetch default permissions
166 # fetch default permissions
168 #======================================================================
167 #======================================================================
169 default_user = User.get_by_username('default', cache=True)
168 default_user = User.get_by_username('default', cache=True)
170 default_user_id = default_user.user_id
169 default_user_id = default_user.user_id
171
170
172 default_repo_perms = Permission.get_default_perms(default_user_id)
171 default_repo_perms = Permission.get_default_perms(default_user_id)
173 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
172 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
174 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
173 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
175
174
176 if user_is_admin:
175 if user_is_admin:
177 #==================================================================
176 #==================================================================
178 # admin users have all rights;
177 # admin users have all rights;
179 # based on default permissions, just set everything to admin
178 # based on default permissions, just set everything to admin
180 #==================================================================
179 #==================================================================
181 permissions[GLOBAL].add('hg.admin')
180 permissions[GLOBAL].add('hg.admin')
182 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
181 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
183
182
184 # repositories
183 # repositories
185 for perm in default_repo_perms:
184 for perm in default_repo_perms:
186 r_k = perm.UserRepoToPerm.repository.repo_name
185 r_k = perm.UserRepoToPerm.repository.repo_name
187 p = 'repository.admin'
186 p = 'repository.admin'
188 permissions[RK][r_k] = p
187 permissions[RK][r_k] = p
189
188
190 # repository groups
189 # repository groups
191 for perm in default_repo_groups_perms:
190 for perm in default_repo_groups_perms:
192 rg_k = perm.UserRepoGroupToPerm.group.group_name
191 rg_k = perm.UserRepoGroupToPerm.group.group_name
193 p = 'group.admin'
192 p = 'group.admin'
194 permissions[GK][rg_k] = p
193 permissions[GK][rg_k] = p
195
194
196 # user groups
195 # user groups
197 for perm in default_user_group_perms:
196 for perm in default_user_group_perms:
198 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
197 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
199 p = 'usergroup.admin'
198 p = 'usergroup.admin'
200 permissions[UK][u_k] = p
199 permissions[UK][u_k] = p
201 return permissions
200 return permissions
202
201
203 #==================================================================
202 #==================================================================
204 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
203 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
205 #==================================================================
204 #==================================================================
206
205
207 # default global permissions taken from the default user
206 # default global permissions taken from the default user
208 default_global_perms = UserToPerm.query()\
207 default_global_perms = UserToPerm.query()\
209 .filter(UserToPerm.user_id == default_user_id)\
208 .filter(UserToPerm.user_id == default_user_id)\
210 .options(joinedload(UserToPerm.permission))
209 .options(joinedload(UserToPerm.permission))
211
210
212 for perm in default_global_perms:
211 for perm in default_global_perms:
213 permissions[GLOBAL].add(perm.permission.permission_name)
212 permissions[GLOBAL].add(perm.permission.permission_name)
214
213
215 # defaults for repositories, taken from default user
214 # defaults for repositories, taken from default user
216 for perm in default_repo_perms:
215 for perm in default_repo_perms:
217 r_k = perm.UserRepoToPerm.repository.repo_name
216 r_k = perm.UserRepoToPerm.repository.repo_name
218 if perm.Repository.private and not (perm.Repository.user_id == user_id):
217 if perm.Repository.private and not (perm.Repository.user_id == user_id):
219 # disable defaults for private repos,
218 # disable defaults for private repos,
220 p = 'repository.none'
219 p = 'repository.none'
221 elif perm.Repository.user_id == user_id:
220 elif perm.Repository.user_id == user_id:
222 # set admin if owner
221 # set admin if owner
223 p = 'repository.admin'
222 p = 'repository.admin'
224 else:
223 else:
225 p = perm.Permission.permission_name
224 p = perm.Permission.permission_name
226
225
227 permissions[RK][r_k] = p
226 permissions[RK][r_k] = p
228
227
229 # defaults for repository groups taken from default user permission
228 # defaults for repository groups taken from default user permission
230 # on given group
229 # on given group
231 for perm in default_repo_groups_perms:
230 for perm in default_repo_groups_perms:
232 rg_k = perm.UserRepoGroupToPerm.group.group_name
231 rg_k = perm.UserRepoGroupToPerm.group.group_name
233 p = perm.Permission.permission_name
232 p = perm.Permission.permission_name
234 permissions[GK][rg_k] = p
233 permissions[GK][rg_k] = p
235
234
236 # defaults for user groups taken from default user permission
235 # defaults for user groups taken from default user permission
237 # on given user group
236 # on given user group
238 for perm in default_user_group_perms:
237 for perm in default_user_group_perms:
239 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
238 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
240 p = perm.Permission.permission_name
239 p = perm.Permission.permission_name
241 permissions[UK][u_k] = p
240 permissions[UK][u_k] = p
242
241
243 #======================================================================
242 #======================================================================
244 # !! OVERRIDE GLOBALS !! with user permissions if any found
243 # !! OVERRIDE GLOBALS !! with user permissions if any found
245 #======================================================================
244 #======================================================================
246 # those can be configured from groups or users explicitly
245 # those can be configured from groups or users explicitly
247 _configurable = set([
246 _configurable = set([
248 'hg.fork.none', 'hg.fork.repository',
247 'hg.fork.none', 'hg.fork.repository',
249 'hg.create.none', 'hg.create.repository',
248 'hg.create.none', 'hg.create.repository',
250 'hg.usergroup.create.false', 'hg.usergroup.create.true'
249 'hg.usergroup.create.false', 'hg.usergroup.create.true'
251 ])
250 ])
252
251
253 # USER GROUPS comes first
252 # USER GROUPS comes first
254 # user group global permissions
253 # user group global permissions
255 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
254 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
256 .options(joinedload(UserGroupToPerm.permission))\
255 .options(joinedload(UserGroupToPerm.permission))\
257 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
256 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
258 UserGroupMember.users_group_id))\
257 UserGroupMember.users_group_id))\
259 .filter(UserGroupMember.user_id == user_id)\
258 .filter(UserGroupMember.user_id == user_id)\
260 .join((UserGroup, UserGroupMember.users_group_id ==
259 .join((UserGroup, UserGroupMember.users_group_id ==
261 UserGroup.users_group_id))\
260 UserGroup.users_group_id))\
262 .filter(UserGroup.users_group_active == True)\
261 .filter(UserGroup.users_group_active == True)\
263 .order_by(UserGroupToPerm.users_group_id)\
262 .order_by(UserGroupToPerm.users_group_id)\
264 .all()
263 .all()
265 # need to group here by groups since user can be in more than
264 # need to group here by groups since user can be in more than
266 # one group
265 # one group
267 _grouped = [[x, list(y)] for x, y in
266 _grouped = [[x, list(y)] for x, y in
268 itertools.groupby(user_perms_from_users_groups,
267 itertools.groupby(user_perms_from_users_groups,
269 lambda x:x.users_group)]
268 lambda x:x.users_group)]
270 for gr, perms in _grouped:
269 for gr, perms in _grouped:
271 # since user can be in multiple groups iterate over them and
270 # since user can be in multiple groups iterate over them and
272 # select the lowest permissions first (more explicit)
271 # select the lowest permissions first (more explicit)
273 ##TODO: do this^^
272 ##TODO: do this^^
274 if not gr.inherit_default_permissions:
273 if not gr.inherit_default_permissions:
275 # NEED TO IGNORE all configurable permissions and
274 # NEED TO IGNORE all configurable permissions and
276 # replace them with explicitly set
275 # replace them with explicitly set
277 permissions[GLOBAL] = permissions[GLOBAL]\
276 permissions[GLOBAL] = permissions[GLOBAL]\
278 .difference(_configurable)
277 .difference(_configurable)
279 for perm in perms:
278 for perm in perms:
280 permissions[GLOBAL].add(perm.permission.permission_name)
279 permissions[GLOBAL].add(perm.permission.permission_name)
281
280
282 # user specific global permissions
281 # user specific global permissions
283 user_perms = Session().query(UserToPerm)\
282 user_perms = Session().query(UserToPerm)\
284 .options(joinedload(UserToPerm.permission))\
283 .options(joinedload(UserToPerm.permission))\
285 .filter(UserToPerm.user_id == user_id).all()
284 .filter(UserToPerm.user_id == user_id).all()
286
285
287 if not user_inherit_default_permissions:
286 if not user_inherit_default_permissions:
288 # NEED TO IGNORE all configurable permissions and
287 # NEED TO IGNORE all configurable permissions and
289 # replace them with explicitly set
288 # replace them with explicitly set
290 permissions[GLOBAL] = permissions[GLOBAL]\
289 permissions[GLOBAL] = permissions[GLOBAL]\
291 .difference(_configurable)
290 .difference(_configurable)
292
291
293 for perm in user_perms:
292 for perm in user_perms:
294 permissions[GLOBAL].add(perm.permission.permission_name)
293 permissions[GLOBAL].add(perm.permission.permission_name)
295 ## END GLOBAL PERMISSIONS
294 ## END GLOBAL PERMISSIONS
296
295
297 #======================================================================
296 #======================================================================
298 # !! PERMISSIONS FOR REPOSITORIES !!
297 # !! PERMISSIONS FOR REPOSITORIES !!
299 #======================================================================
298 #======================================================================
300 #======================================================================
299 #======================================================================
301 # check if user is part of user groups for this repository and
300 # check if user is part of user groups for this repository and
302 # fill in his permission from it. _choose_perm decides of which
301 # fill in his permission from it. _choose_perm decides of which
303 # permission should be selected based on selected method
302 # permission should be selected based on selected method
304 #======================================================================
303 #======================================================================
305
304
306 # user group for repositories permissions
305 # user group for repositories permissions
307 user_repo_perms_from_users_groups = \
306 user_repo_perms_from_users_groups = \
308 Session().query(UserGroupRepoToPerm, Permission, Repository,)\
307 Session().query(UserGroupRepoToPerm, Permission, Repository,)\
309 .join((Repository, UserGroupRepoToPerm.repository_id ==
308 .join((Repository, UserGroupRepoToPerm.repository_id ==
310 Repository.repo_id))\
309 Repository.repo_id))\
311 .join((Permission, UserGroupRepoToPerm.permission_id ==
310 .join((Permission, UserGroupRepoToPerm.permission_id ==
312 Permission.permission_id))\
311 Permission.permission_id))\
313 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
312 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
314 UserGroup.users_group_id))\
313 UserGroup.users_group_id))\
315 .filter(UserGroup.users_group_active == True)\
314 .filter(UserGroup.users_group_active == True)\
316 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
315 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
317 UserGroupMember.users_group_id))\
316 UserGroupMember.users_group_id))\
318 .filter(UserGroupMember.user_id == user_id)\
317 .filter(UserGroupMember.user_id == user_id)\
319 .all()
318 .all()
320
319
321 multiple_counter = collections.defaultdict(int)
320 multiple_counter = collections.defaultdict(int)
322 for perm in user_repo_perms_from_users_groups:
321 for perm in user_repo_perms_from_users_groups:
323 r_k = perm.UserGroupRepoToPerm.repository.repo_name
322 r_k = perm.UserGroupRepoToPerm.repository.repo_name
324 multiple_counter[r_k] += 1
323 multiple_counter[r_k] += 1
325 p = perm.Permission.permission_name
324 p = perm.Permission.permission_name
326 cur_perm = permissions[RK][r_k]
325 cur_perm = permissions[RK][r_k]
327
326
328 if perm.Repository.user_id == user_id:
327 if perm.Repository.user_id == user_id:
329 # set admin if owner
328 # set admin if owner
330 p = 'repository.admin'
329 p = 'repository.admin'
331 else:
330 else:
332 if multiple_counter[r_k] > 1:
331 if multiple_counter[r_k] > 1:
333 p = _choose_perm(p, cur_perm)
332 p = _choose_perm(p, cur_perm)
334 permissions[RK][r_k] = p
333 permissions[RK][r_k] = p
335
334
336 # user explicit permissions for repositories, overrides any specified
335 # user explicit permissions for repositories, overrides any specified
337 # by the group permission
336 # by the group permission
338 user_repo_perms = Permission.get_default_perms(user_id)
337 user_repo_perms = Permission.get_default_perms(user_id)
339 for perm in user_repo_perms:
338 for perm in user_repo_perms:
340 r_k = perm.UserRepoToPerm.repository.repo_name
339 r_k = perm.UserRepoToPerm.repository.repo_name
341 cur_perm = permissions[RK][r_k]
340 cur_perm = permissions[RK][r_k]
342 # set admin if owner
341 # set admin if owner
343 if perm.Repository.user_id == user_id:
342 if perm.Repository.user_id == user_id:
344 p = 'repository.admin'
343 p = 'repository.admin'
345 else:
344 else:
346 p = perm.Permission.permission_name
345 p = perm.Permission.permission_name
347 if not explicit:
346 if not explicit:
348 p = _choose_perm(p, cur_perm)
347 p = _choose_perm(p, cur_perm)
349 permissions[RK][r_k] = p
348 permissions[RK][r_k] = p
350
349
351 #======================================================================
350 #======================================================================
352 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
351 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
353 #======================================================================
352 #======================================================================
354 #======================================================================
353 #======================================================================
355 # check if user is part of user groups for this repository groups and
354 # check if user is part of user groups for this repository groups and
356 # fill in his permission from it. _choose_perm decides of which
355 # fill in his permission from it. _choose_perm decides of which
357 # permission should be selected based on selected method
356 # permission should be selected based on selected method
358 #======================================================================
357 #======================================================================
359 # user group for repo groups permissions
358 # user group for repo groups permissions
360 user_repo_group_perms_from_users_groups = \
359 user_repo_group_perms_from_users_groups = \
361 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup)\
360 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup)\
362 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
361 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
363 .join((Permission, UserGroupRepoGroupToPerm.permission_id
362 .join((Permission, UserGroupRepoGroupToPerm.permission_id
364 == Permission.permission_id))\
363 == Permission.permission_id))\
365 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
364 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
366 UserGroup.users_group_id))\
365 UserGroup.users_group_id))\
367 .filter(UserGroup.users_group_active == True)\
366 .filter(UserGroup.users_group_active == True)\
368 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
367 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
369 == UserGroupMember.users_group_id))\
368 == UserGroupMember.users_group_id))\
370 .filter(UserGroupMember.user_id == user_id)\
369 .filter(UserGroupMember.user_id == user_id)\
371 .all()
370 .all()
372
371
373 multiple_counter = collections.defaultdict(int)
372 multiple_counter = collections.defaultdict(int)
374 for perm in user_repo_group_perms_from_users_groups:
373 for perm in user_repo_group_perms_from_users_groups:
375 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
374 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
376 multiple_counter[g_k] += 1
375 multiple_counter[g_k] += 1
377 p = perm.Permission.permission_name
376 p = perm.Permission.permission_name
378 cur_perm = permissions[GK][g_k]
377 cur_perm = permissions[GK][g_k]
379 if multiple_counter[g_k] > 1:
378 if multiple_counter[g_k] > 1:
380 p = _choose_perm(p, cur_perm)
379 p = _choose_perm(p, cur_perm)
381 permissions[GK][g_k] = p
380 permissions[GK][g_k] = p
382
381
383 # user explicit permissions for repository groups
382 # user explicit permissions for repository groups
384 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
383 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
385 for perm in user_repo_groups_perms:
384 for perm in user_repo_groups_perms:
386 rg_k = perm.UserRepoGroupToPerm.group.group_name
385 rg_k = perm.UserRepoGroupToPerm.group.group_name
387 p = perm.Permission.permission_name
386 p = perm.Permission.permission_name
388 cur_perm = permissions[GK][rg_k]
387 cur_perm = permissions[GK][rg_k]
389 if not explicit:
388 if not explicit:
390 p = _choose_perm(p, cur_perm)
389 p = _choose_perm(p, cur_perm)
391 permissions[GK][rg_k] = p
390 permissions[GK][rg_k] = p
392
391
393 #======================================================================
392 #======================================================================
394 # !! PERMISSIONS FOR USER GROUPS !!
393 # !! PERMISSIONS FOR USER GROUPS !!
395 #======================================================================
394 #======================================================================
396 # user group for user group permissions
395 # user group for user group permissions
397 user_group_user_groups_perms = \
396 user_group_user_groups_perms = \
398 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup)\
397 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup)\
399 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
398 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
400 == UserGroup.users_group_id))\
399 == UserGroup.users_group_id))\
401 .join((Permission, UserGroupUserGroupToPerm.permission_id
400 .join((Permission, UserGroupUserGroupToPerm.permission_id
402 == Permission.permission_id))\
401 == Permission.permission_id))\
403 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
402 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
404 == UserGroupMember.users_group_id))\
403 == UserGroupMember.users_group_id))\
405 .filter(UserGroupMember.user_id == user_id)\
404 .filter(UserGroupMember.user_id == user_id)\
406 .join((UserGroup, UserGroupMember.users_group_id ==
405 .join((UserGroup, UserGroupMember.users_group_id ==
407 UserGroup.users_group_id), aliased=True, from_joinpoint=True)\
406 UserGroup.users_group_id), aliased=True, from_joinpoint=True)\
408 .filter(UserGroup.users_group_active == True)\
407 .filter(UserGroup.users_group_active == True)\
409 .all()
408 .all()
410
409
411 multiple_counter = collections.defaultdict(int)
410 multiple_counter = collections.defaultdict(int)
412 for perm in user_group_user_groups_perms:
411 for perm in user_group_user_groups_perms:
413 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
412 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
414 multiple_counter[g_k] += 1
413 multiple_counter[g_k] += 1
415 p = perm.Permission.permission_name
414 p = perm.Permission.permission_name
416 cur_perm = permissions[UK][g_k]
415 cur_perm = permissions[UK][g_k]
417 if multiple_counter[g_k] > 1:
416 if multiple_counter[g_k] > 1:
418 p = _choose_perm(p, cur_perm)
417 p = _choose_perm(p, cur_perm)
419 permissions[UK][g_k] = p
418 permissions[UK][g_k] = p
420
419
421 #user explicit permission for user groups
420 #user explicit permission for user groups
422 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
421 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
423 for perm in user_user_groups_perms:
422 for perm in user_user_groups_perms:
424 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
423 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
425 p = perm.Permission.permission_name
424 p = perm.Permission.permission_name
426 cur_perm = permissions[UK][u_k]
425 cur_perm = permissions[UK][u_k]
427 if not explicit:
426 if not explicit:
428 p = _choose_perm(p, cur_perm)
427 p = _choose_perm(p, cur_perm)
429 permissions[UK][u_k] = p
428 permissions[UK][u_k] = p
430
429
431 return permissions
430 return permissions
432
431
433
432
434 def allowed_api_access(controller_name, whitelist=None, api_key=None):
433 def allowed_api_access(controller_name, whitelist=None, api_key=None):
435 """
434 """
436 Check if given controller_name is in whitelist API access
435 Check if given controller_name is in whitelist API access
437 """
436 """
438 if not whitelist:
437 if not whitelist:
439 from kallithea import CONFIG
438 from kallithea import CONFIG
440 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
439 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
441 sep=',')
440 sep=',')
442 log.debug('whitelist of API access is: %s', whitelist)
441 log.debug('whitelist of API access is: %s', whitelist)
443 api_access_valid = controller_name in whitelist
442 api_access_valid = controller_name in whitelist
444 if api_access_valid:
443 if api_access_valid:
445 log.debug('controller:%s is in API whitelist', controller_name)
444 log.debug('controller:%s is in API whitelist', controller_name)
446 else:
445 else:
447 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
446 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
448 if api_key:
447 if api_key:
449 #if we use API key and don't have access it's a warning
448 #if we use API key and don't have access it's a warning
450 log.warning(msg)
449 log.warning(msg)
451 else:
450 else:
452 log.debug(msg)
451 log.debug(msg)
453 return api_access_valid
452 return api_access_valid
454
453
455
454
456 class AuthUser(object):
455 class AuthUser(object):
457 """
456 """
458 Represents a Kallithea user, including various authentication and
457 Represents a Kallithea user, including various authentication and
459 authorization information. Typically used to store the current user,
458 authorization information. Typically used to store the current user,
460 but is also used as a generic user information data structure in
459 but is also used as a generic user information data structure in
461 parts of the code, e.g. user management.
460 parts of the code, e.g. user management.
462
461
463 Constructed from a database `User` object, a user ID or cookie dict,
462 Constructed from a database `User` object, a user ID or cookie dict,
464 it looks up the user (if needed) and copies all attributes to itself,
463 it looks up the user (if needed) and copies all attributes to itself,
465 adding various non-persistent data. If lookup fails but anonymous
464 adding various non-persistent data. If lookup fails but anonymous
466 access to Kallithea is enabled, the default user is loaded instead.
465 access to Kallithea is enabled, the default user is loaded instead.
467
466
468 `AuthUser` does not by itself authenticate users and the constructor
467 `AuthUser` does not by itself authenticate users and the constructor
469 sets the `is_authenticated` field to False, except when falling back
468 sets the `is_authenticated` field to False, except when falling back
470 to the default anonymous user (if enabled). It's up to other parts
469 to the default anonymous user (if enabled). It's up to other parts
471 of the code to check e.g. if a supplied password is correct, and if
470 of the code to check e.g. if a supplied password is correct, and if
472 so, set `is_authenticated` to True.
471 so, set `is_authenticated` to True.
473
472
474 However, `AuthUser` does refuse to load a user that is not `active`.
473 However, `AuthUser` does refuse to load a user that is not `active`.
475 """
474 """
476
475
477 def __init__(self, user_id=None, dbuser=None,
476 def __init__(self, user_id=None, dbuser=None,
478 is_external_auth=False):
477 is_external_auth=False):
479
478
480 self.is_authenticated = False
479 self.is_authenticated = False
481 self.is_external_auth = is_external_auth
480 self.is_external_auth = is_external_auth
482
481
483 user_model = UserModel()
482 user_model = UserModel()
484 self.anonymous_user = User.get_default_user(cache=True)
483 self.anonymous_user = User.get_default_user(cache=True)
485
484
486 # These attributes will be overriden by fill_data, below, unless the
485 # These attributes will be overriden by fill_data, below, unless the
487 # requested user cannot be found and the default anonymous user is
486 # requested user cannot be found and the default anonymous user is
488 # not enabled.
487 # not enabled.
489 self.user_id = None
488 self.user_id = None
490 self.username = None
489 self.username = None
491 self.api_key = None
490 self.api_key = None
492 self.name = ''
491 self.name = ''
493 self.lastname = ''
492 self.lastname = ''
494 self.email = ''
493 self.email = ''
495 self.admin = False
494 self.admin = False
496 self.inherit_default_permissions = False
495 self.inherit_default_permissions = False
497
496
498 # Look up database user, if necessary.
497 # Look up database user, if necessary.
499 if user_id is not None:
498 if user_id is not None:
500 log.debug('Auth User lookup by USER ID %s', user_id)
499 log.debug('Auth User lookup by USER ID %s', user_id)
501 dbuser = user_model.get(user_id)
500 dbuser = user_model.get(user_id)
502 else:
501 else:
503 # Note: dbuser is allowed to be None.
502 # Note: dbuser is allowed to be None.
504 log.debug('Auth User lookup by database user %s', dbuser)
503 log.debug('Auth User lookup by database user %s', dbuser)
505
504
506 is_user_loaded = self._fill_data(dbuser)
505 is_user_loaded = self._fill_data(dbuser)
507
506
508 # If user cannot be found, try falling back to anonymous.
507 # If user cannot be found, try falling back to anonymous.
509 if not is_user_loaded:
508 if not is_user_loaded:
510 is_user_loaded = self._fill_data(self.anonymous_user)
509 is_user_loaded = self._fill_data(self.anonymous_user)
511
510
512 # The anonymous user is always "logged in".
511 # The anonymous user is always "logged in".
513 if self.user_id == self.anonymous_user.user_id:
512 if self.user_id == self.anonymous_user.user_id:
514 self.is_authenticated = True
513 self.is_authenticated = True
515
514
516 if not self.username:
515 if not self.username:
517 self.username = 'None'
516 self.username = 'None'
518
517
519 log.debug('Auth User is now %s', self)
518 log.debug('Auth User is now %s', self)
520
519
521 def _fill_data(self, dbuser):
520 def _fill_data(self, dbuser):
522 """
521 """
523 Copies database fields from a `db.User` to this `AuthUser`. Does
522 Copies database fields from a `db.User` to this `AuthUser`. Does
524 not copy `api_keys` and `permissions` attributes.
523 not copy `api_keys` and `permissions` attributes.
525
524
526 Checks that `dbuser` is `active` (and not None) before copying;
525 Checks that `dbuser` is `active` (and not None) before copying;
527 returns True on success.
526 returns True on success.
528 """
527 """
529 if dbuser is not None and dbuser.active:
528 if dbuser is not None and dbuser.active:
530 log.debug('filling %s data', dbuser)
529 log.debug('filling %s data', dbuser)
531 for k, v in dbuser.get_dict().iteritems():
530 for k, v in dbuser.get_dict().iteritems():
532 assert k not in ['api_keys', 'permissions']
531 assert k not in ['api_keys', 'permissions']
533 setattr(self, k, v)
532 setattr(self, k, v)
534 return True
533 return True
535 return False
534 return False
536
535
537 @LazyProperty
536 @LazyProperty
538 def permissions(self):
537 def permissions(self):
539 return self.__get_perms(user=self, cache=False)
538 return self.__get_perms(user=self, cache=False)
540
539
541 @property
540 @property
542 def api_keys(self):
541 def api_keys(self):
543 return self._get_api_keys()
542 return self._get_api_keys()
544
543
545 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
544 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
546 """
545 """
547 Fills user permission attribute with permissions taken from database
546 Fills user permission attribute with permissions taken from database
548 works for permissions given for repositories, and for permissions that
547 works for permissions given for repositories, and for permissions that
549 are granted to groups
548 are granted to groups
550
549
551 :param user: `AuthUser` instance
550 :param user: `AuthUser` instance
552 :param explicit: In case there are permissions both for user and a group
551 :param explicit: In case there are permissions both for user and a group
553 that user is part of, explicit flag will define if user will
552 that user is part of, explicit flag will define if user will
554 explicitly override permissions from group, if it's False it will
553 explicitly override permissions from group, if it's False it will
555 make decision based on the algo
554 make decision based on the algo
556 :param algo: algorithm to decide what permission should be choose if
555 :param algo: algorithm to decide what permission should be choose if
557 it's multiple defined, eg user in two different groups. It also
556 it's multiple defined, eg user in two different groups. It also
558 decides if explicit flag is turned off how to specify the permission
557 decides if explicit flag is turned off how to specify the permission
559 for case when user is in a group + have defined separate permission
558 for case when user is in a group + have defined separate permission
560 """
559 """
561 user_id = user.user_id
560 user_id = user.user_id
562 user_is_admin = user.is_admin
561 user_is_admin = user.is_admin
563 user_inherit_default_permissions = user.inherit_default_permissions
562 user_inherit_default_permissions = user.inherit_default_permissions
564
563
565 log.debug('Getting PERMISSION tree')
564 log.debug('Getting PERMISSION tree')
566 compute = conditional_cache('short_term', 'cache_desc',
565 compute = conditional_cache('short_term', 'cache_desc',
567 condition=cache, func=_cached_perms_data)
566 condition=cache, func=_cached_perms_data)
568 return compute(user_id, user_is_admin,
567 return compute(user_id, user_is_admin,
569 user_inherit_default_permissions, explicit, algo)
568 user_inherit_default_permissions, explicit, algo)
570
569
571 def _get_api_keys(self):
570 def _get_api_keys(self):
572 api_keys = [self.api_key]
571 api_keys = [self.api_key]
573 for api_key in UserApiKeys.query()\
572 for api_key in UserApiKeys.query()\
574 .filter(UserApiKeys.user_id == self.user_id)\
573 .filter(UserApiKeys.user_id == self.user_id)\
575 .filter(or_(UserApiKeys.expires == -1,
574 .filter(or_(UserApiKeys.expires == -1,
576 UserApiKeys.expires >= time.time())).all():
575 UserApiKeys.expires >= time.time())).all():
577 api_keys.append(api_key.api_key)
576 api_keys.append(api_key.api_key)
578
577
579 return api_keys
578 return api_keys
580
579
581 @property
580 @property
582 def is_admin(self):
581 def is_admin(self):
583 return self.admin
582 return self.admin
584
583
585 @property
584 @property
586 def repositories_admin(self):
585 def repositories_admin(self):
587 """
586 """
588 Returns list of repositories you're an admin of
587 Returns list of repositories you're an admin of
589 """
588 """
590 return [x[0] for x in self.permissions['repositories'].iteritems()
589 return [x[0] for x in self.permissions['repositories'].iteritems()
591 if x[1] == 'repository.admin']
590 if x[1] == 'repository.admin']
592
591
593 @property
592 @property
594 def repository_groups_admin(self):
593 def repository_groups_admin(self):
595 """
594 """
596 Returns list of repository groups you're an admin of
595 Returns list of repository groups you're an admin of
597 """
596 """
598 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
597 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
599 if x[1] == 'group.admin']
598 if x[1] == 'group.admin']
600
599
601 @property
600 @property
602 def user_groups_admin(self):
601 def user_groups_admin(self):
603 """
602 """
604 Returns list of user groups you're an admin of
603 Returns list of user groups you're an admin of
605 """
604 """
606 return [x[0] for x in self.permissions['user_groups'].iteritems()
605 return [x[0] for x in self.permissions['user_groups'].iteritems()
607 if x[1] == 'usergroup.admin']
606 if x[1] == 'usergroup.admin']
608
607
609 @staticmethod
608 @staticmethod
610 def check_ip_allowed(user, ip_addr):
609 def check_ip_allowed(user, ip_addr):
611 """
610 """
612 Check if the given IP address (a `str`) is allowed for the given
611 Check if the given IP address (a `str`) is allowed for the given
613 user (an `AuthUser` or `db.User`).
612 user (an `AuthUser` or `db.User`).
614 """
613 """
615 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
614 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
616 inherit_from_default=user.inherit_default_permissions)
615 inherit_from_default=user.inherit_default_permissions)
617 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
616 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
618 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
617 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
619 return True
618 return True
620 else:
619 else:
621 log.info('Access for IP:%s forbidden, '
620 log.info('Access for IP:%s forbidden, '
622 'not in %s' % (ip_addr, allowed_ips))
621 'not in %s' % (ip_addr, allowed_ips))
623 return False
622 return False
624
623
625 def __repr__(self):
624 def __repr__(self):
626 return "<AuthUser('id:%s[%s] auth:%s')>"\
625 return "<AuthUser('id:%s[%s] auth:%s')>"\
627 % (self.user_id, self.username, self.is_authenticated)
626 % (self.user_id, self.username, self.is_authenticated)
628
627
629 def set_authenticated(self, authenticated=True):
628 def set_authenticated(self, authenticated=True):
630 if self.user_id != self.anonymous_user.user_id:
629 if self.user_id != self.anonymous_user.user_id:
631 self.is_authenticated = authenticated
630 self.is_authenticated = authenticated
632
631
633 def to_cookie(self):
632 def to_cookie(self):
634 """ Serializes this login session to a cookie `dict`. """
633 """ Serializes this login session to a cookie `dict`. """
635 return {
634 return {
636 'user_id': self.user_id,
635 'user_id': self.user_id,
637 'is_authenticated': self.is_authenticated,
636 'is_authenticated': self.is_authenticated,
638 'is_external_auth': self.is_external_auth,
637 'is_external_auth': self.is_external_auth,
639 }
638 }
640
639
641 @staticmethod
640 @staticmethod
642 def from_cookie(cookie):
641 def from_cookie(cookie):
643 """
642 """
644 Deserializes an `AuthUser` from a cookie `dict`.
643 Deserializes an `AuthUser` from a cookie `dict`.
645 """
644 """
646
645
647 au = AuthUser(
646 au = AuthUser(
648 user_id=cookie.get('user_id'),
647 user_id=cookie.get('user_id'),
649 is_external_auth=cookie.get('is_external_auth', False),
648 is_external_auth=cookie.get('is_external_auth', False),
650 )
649 )
651 if not au.is_authenticated and au.user_id is not None:
650 if not au.is_authenticated and au.user_id is not None:
652 # user is not authenticated and not empty
651 # user is not authenticated and not empty
653 au.set_authenticated(cookie.get('is_authenticated'))
652 au.set_authenticated(cookie.get('is_authenticated'))
654 return au
653 return au
655
654
656 @classmethod
655 @classmethod
657 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
656 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
658 _set = set()
657 _set = set()
659
658
660 if inherit_from_default:
659 if inherit_from_default:
661 default_ips = UserIpMap.query().filter(UserIpMap.user ==
660 default_ips = UserIpMap.query().filter(UserIpMap.user ==
662 User.get_default_user(cache=True))
661 User.get_default_user(cache=True))
663 if cache:
662 if cache:
664 default_ips = default_ips.options(FromCache("sql_cache_short",
663 default_ips = default_ips.options(FromCache("sql_cache_short",
665 "get_user_ips_default"))
664 "get_user_ips_default"))
666
665
667 # populate from default user
666 # populate from default user
668 for ip in default_ips:
667 for ip in default_ips:
669 try:
668 try:
670 _set.add(ip.ip_addr)
669 _set.add(ip.ip_addr)
671 except ObjectDeletedError:
670 except ObjectDeletedError:
672 # since we use heavy caching sometimes it happens that we get
671 # since we use heavy caching sometimes it happens that we get
673 # deleted objects here, we just skip them
672 # deleted objects here, we just skip them
674 pass
673 pass
675
674
676 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
675 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
677 if cache:
676 if cache:
678 user_ips = user_ips.options(FromCache("sql_cache_short",
677 user_ips = user_ips.options(FromCache("sql_cache_short",
679 "get_user_ips_%s" % user_id))
678 "get_user_ips_%s" % user_id))
680
679
681 for ip in user_ips:
680 for ip in user_ips:
682 try:
681 try:
683 _set.add(ip.ip_addr)
682 _set.add(ip.ip_addr)
684 except ObjectDeletedError:
683 except ObjectDeletedError:
685 # since we use heavy caching sometimes it happens that we get
684 # since we use heavy caching sometimes it happens that we get
686 # deleted objects here, we just skip them
685 # deleted objects here, we just skip them
687 pass
686 pass
688 return _set or set(['0.0.0.0/0', '::/0'])
687 return _set or set(['0.0.0.0/0', '::/0'])
689
688
690
689
691 def set_available_permissions(config):
690 def set_available_permissions(config):
692 """
691 """
693 This function will propagate pylons globals with all available defined
692 This function will propagate pylons globals with all available defined
694 permission given in db. We don't want to check each time from db for new
693 permission given in db. We don't want to check each time from db for new
695 permissions since adding a new permission also requires application restart
694 permissions since adding a new permission also requires application restart
696 ie. to decorate new views with the newly created permission
695 ie. to decorate new views with the newly created permission
697
696
698 :param config: current pylons config instance
697 :param config: current pylons config instance
699
698
700 """
699 """
701 log.info('getting information about all available permissions')
700 log.info('getting information about all available permissions')
702 try:
701 try:
703 sa = meta.Session
702 sa = meta.Session
704 all_perms = sa.query(Permission).all()
703 all_perms = sa.query(Permission).all()
705 config['available_permissions'] = [x.permission_name for x in all_perms]
704 config['available_permissions'] = [x.permission_name for x in all_perms]
706 finally:
705 finally:
707 meta.Session.remove()
706 meta.Session.remove()
708
707
709
708
710 #==============================================================================
709 #==============================================================================
711 # CHECK DECORATORS
710 # CHECK DECORATORS
712 #==============================================================================
711 #==============================================================================
713
712
714 def redirect_to_login(message=None):
713 def redirect_to_login(message=None):
715 from kallithea.lib import helpers as h
714 from kallithea.lib import helpers as h
716 p = request.path_qs
715 p = request.path_qs
717 if message:
716 if message:
718 h.flash(h.literal(message), category='warning')
717 h.flash(h.literal(message), category='warning')
719 log.debug('Redirecting to login page, origin: %s', p)
718 log.debug('Redirecting to login page, origin: %s', p)
720 return redirect(url('login_home', came_from=p))
719 raise HTTPFound(location=url('login_home', came_from=p))
721
720
722
721
723 class LoginRequired(object):
722 class LoginRequired(object):
724 """
723 """
725 Must be logged in to execute this function else
724 Must be logged in to execute this function else
726 redirect to login page
725 redirect to login page
727
726
728 :param api_access: if enabled this checks only for valid auth token
727 :param api_access: if enabled this checks only for valid auth token
729 and grants access based on valid token
728 and grants access based on valid token
730 """
729 """
731
730
732 def __init__(self, api_access=False):
731 def __init__(self, api_access=False):
733 self.api_access = api_access
732 self.api_access = api_access
734
733
735 def __call__(self, func):
734 def __call__(self, func):
736 return decorator(self.__wrapper, func)
735 return decorator(self.__wrapper, func)
737
736
738 def __wrapper(self, func, *fargs, **fkwargs):
737 def __wrapper(self, func, *fargs, **fkwargs):
739 controller = fargs[0]
738 controller = fargs[0]
740 user = controller.authuser
739 user = controller.authuser
741 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
740 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
742 log.debug('Checking access for user %s @ %s', user, loc)
741 log.debug('Checking access for user %s @ %s', user, loc)
743
742
744 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
743 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
745 return redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
744 return redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
746
745
747 # check if we used an API key and it's a valid one
746 # check if we used an API key and it's a valid one
748 api_key = request.GET.get('api_key')
747 api_key = request.GET.get('api_key')
749 if api_key is not None:
748 if api_key is not None:
750 # explicit controller is enabled or API is in our whitelist
749 # explicit controller is enabled or API is in our whitelist
751 if self.api_access or allowed_api_access(loc, api_key=api_key):
750 if self.api_access or allowed_api_access(loc, api_key=api_key):
752 if api_key in user.api_keys:
751 if api_key in user.api_keys:
753 log.info('user %s authenticated with API key ****%s @ %s',
752 log.info('user %s authenticated with API key ****%s @ %s',
754 user, api_key[-4:], loc)
753 user, api_key[-4:], loc)
755 return func(*fargs, **fkwargs)
754 return func(*fargs, **fkwargs)
756 else:
755 else:
757 log.warning('API key ****%s is NOT valid', api_key[-4:])
756 log.warning('API key ****%s is NOT valid', api_key[-4:])
758 return redirect_to_login(_('Invalid API key'))
757 return redirect_to_login(_('Invalid API key'))
759 else:
758 else:
760 # controller does not allow API access
759 # controller does not allow API access
761 log.warning('API access to %s is not allowed', loc)
760 log.warning('API access to %s is not allowed', loc)
762 raise HTTPForbidden()
761 raise HTTPForbidden()
763
762
764 # Only allow the following HTTP request methods. (We sometimes use POST
763 # Only allow the following HTTP request methods. (We sometimes use POST
765 # requests with a '_method' set to 'PUT' or 'DELETE'; but that is only
764 # requests with a '_method' set to 'PUT' or 'DELETE'; but that is only
766 # used for the route lookup, and does not affect request.method.)
765 # used for the route lookup, and does not affect request.method.)
767 if request.method not in ['GET', 'HEAD', 'POST', 'PUT']:
766 if request.method not in ['GET', 'HEAD', 'POST', 'PUT']:
768 raise HTTPMethodNotAllowed()
767 raise HTTPMethodNotAllowed()
769
768
770 # Make sure CSRF token never appears in the URL. If so, invalidate it.
769 # Make sure CSRF token never appears in the URL. If so, invalidate it.
771 if secure_form.token_key in request.GET:
770 if secure_form.token_key in request.GET:
772 log.error('CSRF key leak detected')
771 log.error('CSRF key leak detected')
773 session.pop(secure_form.token_key, None)
772 session.pop(secure_form.token_key, None)
774 session.save()
773 session.save()
775 from kallithea.lib import helpers as h
774 from kallithea.lib import helpers as h
776 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
775 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
777 category='error')
776 category='error')
778
777
779 # CSRF protection: Whenever a request has ambient authority (whether
778 # CSRF protection: Whenever a request has ambient authority (whether
780 # through a session cookie or its origin IP address), it must include
779 # through a session cookie or its origin IP address), it must include
781 # the correct token, unless the HTTP method is GET or HEAD (and thus
780 # the correct token, unless the HTTP method is GET or HEAD (and thus
782 # guaranteed to be side effect free. In practice, the only situation
781 # guaranteed to be side effect free. In practice, the only situation
783 # where we allow side effects without ambient authority is when the
782 # where we allow side effects without ambient authority is when the
784 # authority comes from an API key; and that is handled above.
783 # authority comes from an API key; and that is handled above.
785 if request.method not in ['GET', 'HEAD']:
784 if request.method not in ['GET', 'HEAD']:
786 token = request.POST.get(secure_form.token_key)
785 token = request.POST.get(secure_form.token_key)
787 if not token or token != secure_form.authentication_token():
786 if not token or token != secure_form.authentication_token():
788 log.error('CSRF check failed')
787 log.error('CSRF check failed')
789 raise HTTPForbidden()
788 raise HTTPForbidden()
790
789
791 # WebOb already ignores request payload parameters for anything other
790 # WebOb already ignores request payload parameters for anything other
792 # than POST/PUT, but double-check since other Kallithea code relies on
791 # than POST/PUT, but double-check since other Kallithea code relies on
793 # this assumption.
792 # this assumption.
794 if request.method not in ['POST', 'PUT'] and request.POST:
793 if request.method not in ['POST', 'PUT'] and request.POST:
795 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
794 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
796 raise HTTPBadRequest()
795 raise HTTPBadRequest()
797
796
798 # regular user authentication
797 # regular user authentication
799 if user.is_authenticated:
798 if user.is_authenticated:
800 log.info('user %s authenticated with regular auth @ %s', user, loc)
799 log.info('user %s authenticated with regular auth @ %s', user, loc)
801 return func(*fargs, **fkwargs)
800 return func(*fargs, **fkwargs)
802 else:
801 else:
803 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
802 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
804 return redirect_to_login()
803 return redirect_to_login()
805
804
806 class NotAnonymous(object):
805 class NotAnonymous(object):
807 """
806 """
808 Must be logged in to execute this function else
807 Must be logged in to execute this function else
809 redirect to login page"""
808 redirect to login page"""
810
809
811 def __call__(self, func):
810 def __call__(self, func):
812 return decorator(self.__wrapper, func)
811 return decorator(self.__wrapper, func)
813
812
814 def __wrapper(self, func, *fargs, **fkwargs):
813 def __wrapper(self, func, *fargs, **fkwargs):
815 cls = fargs[0]
814 cls = fargs[0]
816 self.user = cls.authuser
815 self.user = cls.authuser
817
816
818 log.debug('Checking if user is not anonymous @%s', cls)
817 log.debug('Checking if user is not anonymous @%s', cls)
819
818
820 anonymous = self.user.username == User.DEFAULT_USER
819 anonymous = self.user.username == User.DEFAULT_USER
821
820
822 if anonymous:
821 if anonymous:
823 return redirect_to_login(_('You need to be a registered user to '
822 return redirect_to_login(_('You need to be a registered user to '
824 'perform this action'))
823 'perform this action'))
825 else:
824 else:
826 return func(*fargs, **fkwargs)
825 return func(*fargs, **fkwargs)
827
826
828
827
829 class PermsDecorator(object):
828 class PermsDecorator(object):
830 """Base class for controller decorators"""
829 """Base class for controller decorators"""
831
830
832 def __init__(self, *required_perms):
831 def __init__(self, *required_perms):
833 self.required_perms = set(required_perms)
832 self.required_perms = set(required_perms)
834 self.user_perms = None
833 self.user_perms = None
835
834
836 def __call__(self, func):
835 def __call__(self, func):
837 return decorator(self.__wrapper, func)
836 return decorator(self.__wrapper, func)
838
837
839 def __wrapper(self, func, *fargs, **fkwargs):
838 def __wrapper(self, func, *fargs, **fkwargs):
840 cls = fargs[0]
839 cls = fargs[0]
841 self.user = cls.authuser
840 self.user = cls.authuser
842 self.user_perms = self.user.permissions
841 self.user_perms = self.user.permissions
843 log.debug('checking %s permissions %s for %s %s',
842 log.debug('checking %s permissions %s for %s %s',
844 self.__class__.__name__, self.required_perms, cls, self.user)
843 self.__class__.__name__, self.required_perms, cls, self.user)
845
844
846 if self.check_permissions():
845 if self.check_permissions():
847 log.debug('Permission granted for %s %s', cls, self.user)
846 log.debug('Permission granted for %s %s', cls, self.user)
848 return func(*fargs, **fkwargs)
847 return func(*fargs, **fkwargs)
849
848
850 else:
849 else:
851 log.debug('Permission denied for %s %s', cls, self.user)
850 log.debug('Permission denied for %s %s', cls, self.user)
852 anonymous = self.user.username == User.DEFAULT_USER
851 anonymous = self.user.username == User.DEFAULT_USER
853
852
854 if anonymous:
853 if anonymous:
855 return redirect_to_login(_('You need to be signed in to view this page'))
854 return redirect_to_login(_('You need to be signed in to view this page'))
856 else:
855 else:
857 raise HTTPForbidden()
856 raise HTTPForbidden()
858
857
859 def check_permissions(self):
858 def check_permissions(self):
860 """Dummy function for overriding"""
859 """Dummy function for overriding"""
861 raise Exception('You have to write this function in child class')
860 raise Exception('You have to write this function in child class')
862
861
863
862
864 class HasPermissionAllDecorator(PermsDecorator):
863 class HasPermissionAllDecorator(PermsDecorator):
865 """
864 """
866 Checks for access permission for all given predicates. All of them
865 Checks for access permission for all given predicates. All of them
867 have to be meet in order to fulfill the request
866 have to be meet in order to fulfill the request
868 """
867 """
869
868
870 def check_permissions(self):
869 def check_permissions(self):
871 if self.required_perms.issubset(self.user_perms.get('global')):
870 if self.required_perms.issubset(self.user_perms.get('global')):
872 return True
871 return True
873 return False
872 return False
874
873
875
874
876 class HasPermissionAnyDecorator(PermsDecorator):
875 class HasPermissionAnyDecorator(PermsDecorator):
877 """
876 """
878 Checks for access permission for any of given predicates. In order to
877 Checks for access permission for any of given predicates. In order to
879 fulfill the request any of predicates must be meet
878 fulfill the request any of predicates must be meet
880 """
879 """
881
880
882 def check_permissions(self):
881 def check_permissions(self):
883 if self.required_perms.intersection(self.user_perms.get('global')):
882 if self.required_perms.intersection(self.user_perms.get('global')):
884 return True
883 return True
885 return False
884 return False
886
885
887
886
888 class HasRepoPermissionAllDecorator(PermsDecorator):
887 class HasRepoPermissionAllDecorator(PermsDecorator):
889 """
888 """
890 Checks for access permission for all given predicates for specific
889 Checks for access permission for all given predicates for specific
891 repository. All of them have to be meet in order to fulfill the request
890 repository. All of them have to be meet in order to fulfill the request
892 """
891 """
893
892
894 def check_permissions(self):
893 def check_permissions(self):
895 repo_name = get_repo_slug(request)
894 repo_name = get_repo_slug(request)
896 try:
895 try:
897 user_perms = set([self.user_perms['repositories'][repo_name]])
896 user_perms = set([self.user_perms['repositories'][repo_name]])
898 except KeyError:
897 except KeyError:
899 return False
898 return False
900 if self.required_perms.issubset(user_perms):
899 if self.required_perms.issubset(user_perms):
901 return True
900 return True
902 return False
901 return False
903
902
904
903
905 class HasRepoPermissionAnyDecorator(PermsDecorator):
904 class HasRepoPermissionAnyDecorator(PermsDecorator):
906 """
905 """
907 Checks for access permission for any of given predicates for specific
906 Checks for access permission for any of given predicates for specific
908 repository. In order to fulfill the request any of predicates must be meet
907 repository. In order to fulfill the request any of predicates must be meet
909 """
908 """
910
909
911 def check_permissions(self):
910 def check_permissions(self):
912 repo_name = get_repo_slug(request)
911 repo_name = get_repo_slug(request)
913 try:
912 try:
914 user_perms = set([self.user_perms['repositories'][repo_name]])
913 user_perms = set([self.user_perms['repositories'][repo_name]])
915 except KeyError:
914 except KeyError:
916 return False
915 return False
917
916
918 if self.required_perms.intersection(user_perms):
917 if self.required_perms.intersection(user_perms):
919 return True
918 return True
920 return False
919 return False
921
920
922
921
923 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
922 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
924 """
923 """
925 Checks for access permission for all given predicates for specific
924 Checks for access permission for all given predicates for specific
926 repository group. All of them have to be meet in order to fulfill the request
925 repository group. All of them have to be meet in order to fulfill the request
927 """
926 """
928
927
929 def check_permissions(self):
928 def check_permissions(self):
930 group_name = get_repo_group_slug(request)
929 group_name = get_repo_group_slug(request)
931 try:
930 try:
932 user_perms = set([self.user_perms['repositories_groups'][group_name]])
931 user_perms = set([self.user_perms['repositories_groups'][group_name]])
933 except KeyError:
932 except KeyError:
934 return False
933 return False
935
934
936 if self.required_perms.issubset(user_perms):
935 if self.required_perms.issubset(user_perms):
937 return True
936 return True
938 return False
937 return False
939
938
940
939
941 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
940 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
942 """
941 """
943 Checks for access permission for any of given predicates for specific
942 Checks for access permission for any of given predicates for specific
944 repository group. In order to fulfill the request any of predicates must be meet
943 repository group. In order to fulfill the request any of predicates must be meet
945 """
944 """
946
945
947 def check_permissions(self):
946 def check_permissions(self):
948 group_name = get_repo_group_slug(request)
947 group_name = get_repo_group_slug(request)
949 try:
948 try:
950 user_perms = set([self.user_perms['repositories_groups'][group_name]])
949 user_perms = set([self.user_perms['repositories_groups'][group_name]])
951 except KeyError:
950 except KeyError:
952 return False
951 return False
953
952
954 if self.required_perms.intersection(user_perms):
953 if self.required_perms.intersection(user_perms):
955 return True
954 return True
956 return False
955 return False
957
956
958
957
959 class HasUserGroupPermissionAllDecorator(PermsDecorator):
958 class HasUserGroupPermissionAllDecorator(PermsDecorator):
960 """
959 """
961 Checks for access permission for all given predicates for specific
960 Checks for access permission for all given predicates for specific
962 user group. All of them have to be meet in order to fulfill the request
961 user group. All of them have to be meet in order to fulfill the request
963 """
962 """
964
963
965 def check_permissions(self):
964 def check_permissions(self):
966 group_name = get_user_group_slug(request)
965 group_name = get_user_group_slug(request)
967 try:
966 try:
968 user_perms = set([self.user_perms['user_groups'][group_name]])
967 user_perms = set([self.user_perms['user_groups'][group_name]])
969 except KeyError:
968 except KeyError:
970 return False
969 return False
971
970
972 if self.required_perms.issubset(user_perms):
971 if self.required_perms.issubset(user_perms):
973 return True
972 return True
974 return False
973 return False
975
974
976
975
977 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
976 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
978 """
977 """
979 Checks for access permission for any of given predicates for specific
978 Checks for access permission for any of given predicates for specific
980 user group. In order to fulfill the request any of predicates must be meet
979 user group. In order to fulfill the request any of predicates must be meet
981 """
980 """
982
981
983 def check_permissions(self):
982 def check_permissions(self):
984 group_name = get_user_group_slug(request)
983 group_name = get_user_group_slug(request)
985 try:
984 try:
986 user_perms = set([self.user_perms['user_groups'][group_name]])
985 user_perms = set([self.user_perms['user_groups'][group_name]])
987 except KeyError:
986 except KeyError:
988 return False
987 return False
989
988
990 if self.required_perms.intersection(user_perms):
989 if self.required_perms.intersection(user_perms):
991 return True
990 return True
992 return False
991 return False
993
992
994
993
995 #==============================================================================
994 #==============================================================================
996 # CHECK FUNCTIONS
995 # CHECK FUNCTIONS
997 #==============================================================================
996 #==============================================================================
998 class PermsFunction(object):
997 class PermsFunction(object):
999 """Base function for other check functions"""
998 """Base function for other check functions"""
1000
999
1001 def __init__(self, *perms):
1000 def __init__(self, *perms):
1002 self.required_perms = set(perms)
1001 self.required_perms = set(perms)
1003 self.user_perms = None
1002 self.user_perms = None
1004 self.repo_name = None
1003 self.repo_name = None
1005 self.group_name = None
1004 self.group_name = None
1006
1005
1007 def __call__(self, check_location='', user=None):
1006 def __call__(self, check_location='', user=None):
1008 if not user:
1007 if not user:
1009 #TODO: remove this someday,put as user as attribute here
1008 #TODO: remove this someday,put as user as attribute here
1010 user = request.user
1009 user = request.user
1011
1010
1012 # init auth user if not already given
1011 # init auth user if not already given
1013 if not isinstance(user, AuthUser):
1012 if not isinstance(user, AuthUser):
1014 user = AuthUser(user.user_id)
1013 user = AuthUser(user.user_id)
1015
1014
1016 cls_name = self.__class__.__name__
1015 cls_name = self.__class__.__name__
1017 check_scope = {
1016 check_scope = {
1018 'HasPermissionAll': '',
1017 'HasPermissionAll': '',
1019 'HasPermissionAny': '',
1018 'HasPermissionAny': '',
1020 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1019 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1021 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1020 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1022 'HasRepoGroupPermissionAll': 'group:%s' % self.group_name,
1021 'HasRepoGroupPermissionAll': 'group:%s' % self.group_name,
1023 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
1022 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
1024 }.get(cls_name, '?')
1023 }.get(cls_name, '?')
1025 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1024 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1026 self.required_perms, user, check_scope,
1025 self.required_perms, user, check_scope,
1027 check_location or 'unspecified location')
1026 check_location or 'unspecified location')
1028 if not user:
1027 if not user:
1029 log.debug('Empty request user')
1028 log.debug('Empty request user')
1030 return False
1029 return False
1031 self.user_perms = user.permissions
1030 self.user_perms = user.permissions
1032 if self.check_permissions():
1031 if self.check_permissions():
1033 log.debug('Permission to %s granted for user: %s @ %s',
1032 log.debug('Permission to %s granted for user: %s @ %s',
1034 check_scope, user,
1033 check_scope, user,
1035 check_location or 'unspecified location')
1034 check_location or 'unspecified location')
1036 return True
1035 return True
1037
1036
1038 else:
1037 else:
1039 log.debug('Permission to %s denied for user: %s @ %s',
1038 log.debug('Permission to %s denied for user: %s @ %s',
1040 check_scope, user,
1039 check_scope, user,
1041 check_location or 'unspecified location')
1040 check_location or 'unspecified location')
1042 return False
1041 return False
1043
1042
1044 def check_permissions(self):
1043 def check_permissions(self):
1045 """Dummy function for overriding"""
1044 """Dummy function for overriding"""
1046 raise Exception('You have to write this function in child class')
1045 raise Exception('You have to write this function in child class')
1047
1046
1048
1047
1049 class HasPermissionAll(PermsFunction):
1048 class HasPermissionAll(PermsFunction):
1050 def check_permissions(self):
1049 def check_permissions(self):
1051 if self.required_perms.issubset(self.user_perms.get('global')):
1050 if self.required_perms.issubset(self.user_perms.get('global')):
1052 return True
1051 return True
1053 return False
1052 return False
1054
1053
1055
1054
1056 class HasPermissionAny(PermsFunction):
1055 class HasPermissionAny(PermsFunction):
1057 def check_permissions(self):
1056 def check_permissions(self):
1058 if self.required_perms.intersection(self.user_perms.get('global')):
1057 if self.required_perms.intersection(self.user_perms.get('global')):
1059 return True
1058 return True
1060 return False
1059 return False
1061
1060
1062
1061
1063 class HasRepoPermissionAll(PermsFunction):
1062 class HasRepoPermissionAll(PermsFunction):
1064 def __call__(self, repo_name=None, check_location='', user=None):
1063 def __call__(self, repo_name=None, check_location='', user=None):
1065 self.repo_name = repo_name
1064 self.repo_name = repo_name
1066 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1065 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1067
1066
1068 def check_permissions(self):
1067 def check_permissions(self):
1069 if not self.repo_name:
1068 if not self.repo_name:
1070 self.repo_name = get_repo_slug(request)
1069 self.repo_name = get_repo_slug(request)
1071
1070
1072 try:
1071 try:
1073 self._user_perms = set(
1072 self._user_perms = set(
1074 [self.user_perms['repositories'][self.repo_name]]
1073 [self.user_perms['repositories'][self.repo_name]]
1075 )
1074 )
1076 except KeyError:
1075 except KeyError:
1077 return False
1076 return False
1078 if self.required_perms.issubset(self._user_perms):
1077 if self.required_perms.issubset(self._user_perms):
1079 return True
1078 return True
1080 return False
1079 return False
1081
1080
1082
1081
1083 class HasRepoPermissionAny(PermsFunction):
1082 class HasRepoPermissionAny(PermsFunction):
1084 def __call__(self, repo_name=None, check_location='', user=None):
1083 def __call__(self, repo_name=None, check_location='', user=None):
1085 self.repo_name = repo_name
1084 self.repo_name = repo_name
1086 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1085 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1087
1086
1088 def check_permissions(self):
1087 def check_permissions(self):
1089 if not self.repo_name:
1088 if not self.repo_name:
1090 self.repo_name = get_repo_slug(request)
1089 self.repo_name = get_repo_slug(request)
1091
1090
1092 try:
1091 try:
1093 self._user_perms = set(
1092 self._user_perms = set(
1094 [self.user_perms['repositories'][self.repo_name]]
1093 [self.user_perms['repositories'][self.repo_name]]
1095 )
1094 )
1096 except KeyError:
1095 except KeyError:
1097 return False
1096 return False
1098 if self.required_perms.intersection(self._user_perms):
1097 if self.required_perms.intersection(self._user_perms):
1099 return True
1098 return True
1100 return False
1099 return False
1101
1100
1102
1101
1103 class HasRepoGroupPermissionAny(PermsFunction):
1102 class HasRepoGroupPermissionAny(PermsFunction):
1104 def __call__(self, group_name=None, check_location='', user=None):
1103 def __call__(self, group_name=None, check_location='', user=None):
1105 self.group_name = group_name
1104 self.group_name = group_name
1106 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1105 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1107
1106
1108 def check_permissions(self):
1107 def check_permissions(self):
1109 try:
1108 try:
1110 self._user_perms = set(
1109 self._user_perms = set(
1111 [self.user_perms['repositories_groups'][self.group_name]]
1110 [self.user_perms['repositories_groups'][self.group_name]]
1112 )
1111 )
1113 except KeyError:
1112 except KeyError:
1114 return False
1113 return False
1115 if self.required_perms.intersection(self._user_perms):
1114 if self.required_perms.intersection(self._user_perms):
1116 return True
1115 return True
1117 return False
1116 return False
1118
1117
1119
1118
1120 class HasRepoGroupPermissionAll(PermsFunction):
1119 class HasRepoGroupPermissionAll(PermsFunction):
1121 def __call__(self, group_name=None, check_location='', user=None):
1120 def __call__(self, group_name=None, check_location='', user=None):
1122 self.group_name = group_name
1121 self.group_name = group_name
1123 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
1122 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
1124
1123
1125 def check_permissions(self):
1124 def check_permissions(self):
1126 try:
1125 try:
1127 self._user_perms = set(
1126 self._user_perms = set(
1128 [self.user_perms['repositories_groups'][self.group_name]]
1127 [self.user_perms['repositories_groups'][self.group_name]]
1129 )
1128 )
1130 except KeyError:
1129 except KeyError:
1131 return False
1130 return False
1132 if self.required_perms.issubset(self._user_perms):
1131 if self.required_perms.issubset(self._user_perms):
1133 return True
1132 return True
1134 return False
1133 return False
1135
1134
1136
1135
1137 class HasUserGroupPermissionAny(PermsFunction):
1136 class HasUserGroupPermissionAny(PermsFunction):
1138 def __call__(self, user_group_name=None, check_location='', user=None):
1137 def __call__(self, user_group_name=None, check_location='', user=None):
1139 self.user_group_name = user_group_name
1138 self.user_group_name = user_group_name
1140 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1139 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1141
1140
1142 def check_permissions(self):
1141 def check_permissions(self):
1143 try:
1142 try:
1144 self._user_perms = set(
1143 self._user_perms = set(
1145 [self.user_perms['user_groups'][self.user_group_name]]
1144 [self.user_perms['user_groups'][self.user_group_name]]
1146 )
1145 )
1147 except KeyError:
1146 except KeyError:
1148 return False
1147 return False
1149 if self.required_perms.intersection(self._user_perms):
1148 if self.required_perms.intersection(self._user_perms):
1150 return True
1149 return True
1151 return False
1150 return False
1152
1151
1153
1152
1154 class HasUserGroupPermissionAll(PermsFunction):
1153 class HasUserGroupPermissionAll(PermsFunction):
1155 def __call__(self, user_group_name=None, check_location='', user=None):
1154 def __call__(self, user_group_name=None, check_location='', user=None):
1156 self.user_group_name = user_group_name
1155 self.user_group_name = user_group_name
1157 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
1156 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
1158
1157
1159 def check_permissions(self):
1158 def check_permissions(self):
1160 try:
1159 try:
1161 self._user_perms = set(
1160 self._user_perms = set(
1162 [self.user_perms['user_groups'][self.user_group_name]]
1161 [self.user_perms['user_groups'][self.user_group_name]]
1163 )
1162 )
1164 except KeyError:
1163 except KeyError:
1165 return False
1164 return False
1166 if self.required_perms.issubset(self._user_perms):
1165 if self.required_perms.issubset(self._user_perms):
1167 return True
1166 return True
1168 return False
1167 return False
1169
1168
1170
1169
1171 #==============================================================================
1170 #==============================================================================
1172 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1171 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1173 #==============================================================================
1172 #==============================================================================
1174 class HasPermissionAnyMiddleware(object):
1173 class HasPermissionAnyMiddleware(object):
1175 def __init__(self, *perms):
1174 def __init__(self, *perms):
1176 self.required_perms = set(perms)
1175 self.required_perms = set(perms)
1177
1176
1178 def __call__(self, user, repo_name):
1177 def __call__(self, user, repo_name):
1179 # repo_name MUST be unicode, since we handle keys in permission
1178 # repo_name MUST be unicode, since we handle keys in permission
1180 # dict by unicode
1179 # dict by unicode
1181 repo_name = safe_unicode(repo_name)
1180 repo_name = safe_unicode(repo_name)
1182 usr = AuthUser(user.user_id)
1181 usr = AuthUser(user.user_id)
1183 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1182 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1184 self.username = user.username
1183 self.username = user.username
1185 self.repo_name = repo_name
1184 self.repo_name = repo_name
1186 return self.check_permissions()
1185 return self.check_permissions()
1187
1186
1188 def check_permissions(self):
1187 def check_permissions(self):
1189 log.debug('checking VCS protocol '
1188 log.debug('checking VCS protocol '
1190 'permissions %s for user:%s repository:%s', self.user_perms,
1189 'permissions %s for user:%s repository:%s', self.user_perms,
1191 self.username, self.repo_name)
1190 self.username, self.repo_name)
1192 if self.required_perms.intersection(self.user_perms):
1191 if self.required_perms.intersection(self.user_perms):
1193 log.debug('Permission to repo: %s granted for user: %s @ %s',
1192 log.debug('Permission to repo: %s granted for user: %s @ %s',
1194 self.repo_name, self.username, 'PermissionMiddleware')
1193 self.repo_name, self.username, 'PermissionMiddleware')
1195 return True
1194 return True
1196 log.debug('Permission to repo: %s denied for user: %s @ %s',
1195 log.debug('Permission to repo: %s denied for user: %s @ %s',
1197 self.repo_name, self.username, 'PermissionMiddleware')
1196 self.repo_name, self.username, 'PermissionMiddleware')
1198 return False
1197 return False
1199
1198
1200
1199
1201 #==============================================================================
1200 #==============================================================================
1202 # SPECIAL VERSION TO HANDLE API AUTH
1201 # SPECIAL VERSION TO HANDLE API AUTH
1203 #==============================================================================
1202 #==============================================================================
1204 class _BaseApiPerm(object):
1203 class _BaseApiPerm(object):
1205 def __init__(self, *perms):
1204 def __init__(self, *perms):
1206 self.required_perms = set(perms)
1205 self.required_perms = set(perms)
1207
1206
1208 def __call__(self, check_location=None, user=None, repo_name=None,
1207 def __call__(self, check_location=None, user=None, repo_name=None,
1209 group_name=None):
1208 group_name=None):
1210 cls_name = self.__class__.__name__
1209 cls_name = self.__class__.__name__
1211 check_scope = 'user:%s' % (user)
1210 check_scope = 'user:%s' % (user)
1212 if repo_name:
1211 if repo_name:
1213 check_scope += ', repo:%s' % (repo_name)
1212 check_scope += ', repo:%s' % (repo_name)
1214
1213
1215 if group_name:
1214 if group_name:
1216 check_scope += ', repo group:%s' % (group_name)
1215 check_scope += ', repo group:%s' % (group_name)
1217
1216
1218 log.debug('checking cls:%s %s %s @ %s',
1217 log.debug('checking cls:%s %s %s @ %s',
1219 cls_name, self.required_perms, check_scope, check_location)
1218 cls_name, self.required_perms, check_scope, check_location)
1220 if not user:
1219 if not user:
1221 log.debug('Empty User passed into arguments')
1220 log.debug('Empty User passed into arguments')
1222 return False
1221 return False
1223
1222
1224 ## process user
1223 ## process user
1225 if not isinstance(user, AuthUser):
1224 if not isinstance(user, AuthUser):
1226 user = AuthUser(user.user_id)
1225 user = AuthUser(user.user_id)
1227 if not check_location:
1226 if not check_location:
1228 check_location = 'unspecified'
1227 check_location = 'unspecified'
1229 if self.check_permissions(user.permissions, repo_name, group_name):
1228 if self.check_permissions(user.permissions, repo_name, group_name):
1230 log.debug('Permission to %s granted for user: %s @ %s',
1229 log.debug('Permission to %s granted for user: %s @ %s',
1231 check_scope, user, check_location)
1230 check_scope, user, check_location)
1232 return True
1231 return True
1233
1232
1234 else:
1233 else:
1235 log.debug('Permission to %s denied for user: %s @ %s',
1234 log.debug('Permission to %s denied for user: %s @ %s',
1236 check_scope, user, check_location)
1235 check_scope, user, check_location)
1237 return False
1236 return False
1238
1237
1239 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1238 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1240 """
1239 """
1241 implement in child class should return True if permissions are ok,
1240 implement in child class should return True if permissions are ok,
1242 False otherwise
1241 False otherwise
1243
1242
1244 :param perm_defs: dict with permission definitions
1243 :param perm_defs: dict with permission definitions
1245 :param repo_name: repo name
1244 :param repo_name: repo name
1246 """
1245 """
1247 raise NotImplementedError()
1246 raise NotImplementedError()
1248
1247
1249
1248
1250 class HasPermissionAllApi(_BaseApiPerm):
1249 class HasPermissionAllApi(_BaseApiPerm):
1251 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1250 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1252 if self.required_perms.issubset(perm_defs.get('global')):
1251 if self.required_perms.issubset(perm_defs.get('global')):
1253 return True
1252 return True
1254 return False
1253 return False
1255
1254
1256
1255
1257 class HasPermissionAnyApi(_BaseApiPerm):
1256 class HasPermissionAnyApi(_BaseApiPerm):
1258 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1257 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1259 if self.required_perms.intersection(perm_defs.get('global')):
1258 if self.required_perms.intersection(perm_defs.get('global')):
1260 return True
1259 return True
1261 return False
1260 return False
1262
1261
1263
1262
1264 class HasRepoPermissionAllApi(_BaseApiPerm):
1263 class HasRepoPermissionAllApi(_BaseApiPerm):
1265 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1264 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1266 try:
1265 try:
1267 _user_perms = set([perm_defs['repositories'][repo_name]])
1266 _user_perms = set([perm_defs['repositories'][repo_name]])
1268 except KeyError:
1267 except KeyError:
1269 log.warning(traceback.format_exc())
1268 log.warning(traceback.format_exc())
1270 return False
1269 return False
1271 if self.required_perms.issubset(_user_perms):
1270 if self.required_perms.issubset(_user_perms):
1272 return True
1271 return True
1273 return False
1272 return False
1274
1273
1275
1274
1276 class HasRepoPermissionAnyApi(_BaseApiPerm):
1275 class HasRepoPermissionAnyApi(_BaseApiPerm):
1277 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1276 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1278 try:
1277 try:
1279 _user_perms = set([perm_defs['repositories'][repo_name]])
1278 _user_perms = set([perm_defs['repositories'][repo_name]])
1280 except KeyError:
1279 except KeyError:
1281 log.warning(traceback.format_exc())
1280 log.warning(traceback.format_exc())
1282 return False
1281 return False
1283 if self.required_perms.intersection(_user_perms):
1282 if self.required_perms.intersection(_user_perms):
1284 return True
1283 return True
1285 return False
1284 return False
1286
1285
1287
1286
1288 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1287 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1289 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1288 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1290 try:
1289 try:
1291 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1290 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1292 except KeyError:
1291 except KeyError:
1293 log.warning(traceback.format_exc())
1292 log.warning(traceback.format_exc())
1294 return False
1293 return False
1295 if self.required_perms.intersection(_user_perms):
1294 if self.required_perms.intersection(_user_perms):
1296 return True
1295 return True
1297 return False
1296 return False
1298
1297
1299 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1298 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1300 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1299 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1301 try:
1300 try:
1302 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1301 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1303 except KeyError:
1302 except KeyError:
1304 log.warning(traceback.format_exc())
1303 log.warning(traceback.format_exc())
1305 return False
1304 return False
1306 if self.required_perms.issubset(_user_perms):
1305 if self.required_perms.issubset(_user_perms):
1307 return True
1306 return True
1308 return False
1307 return False
1309
1308
1310 def check_ip_access(source_ip, allowed_ips=None):
1309 def check_ip_access(source_ip, allowed_ips=None):
1311 """
1310 """
1312 Checks if source_ip is a subnet of any of allowed_ips.
1311 Checks if source_ip is a subnet of any of allowed_ips.
1313
1312
1314 :param source_ip:
1313 :param source_ip:
1315 :param allowed_ips: list of allowed ips together with mask
1314 :param allowed_ips: list of allowed ips together with mask
1316 """
1315 """
1317 from kallithea.lib import ipaddr
1316 from kallithea.lib import ipaddr
1318 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1317 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1319 if isinstance(allowed_ips, (tuple, list, set)):
1318 if isinstance(allowed_ips, (tuple, list, set)):
1320 for ip in allowed_ips:
1319 for ip in allowed_ips:
1321 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1320 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1322 log.debug('IP %s is network %s',
1321 log.debug('IP %s is network %s',
1323 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1322 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1324 return True
1323 return True
1325 return False
1324 return False
@@ -1,539 +1,538 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 """
15 """
16 kallithea.lib.base
16 kallithea.lib.base
17 ~~~~~~~~~~~~~~~~~~
17 ~~~~~~~~~~~~~~~~~~
18
18
19 The base Controller API
19 The base Controller API
20 Provides the BaseController class for subclassing. And usage in different
20 Provides the BaseController class for subclassing. And usage in different
21 controllers
21 controllers
22
22
23 This file was forked by the Kallithea project in July 2014.
23 This file was forked by the Kallithea project in July 2014.
24 Original author and date, and relevant copyright and licensing information is below:
24 Original author and date, and relevant copyright and licensing information is below:
25 :created_on: Oct 06, 2010
25 :created_on: Oct 06, 2010
26 :author: marcink
26 :author: marcink
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 :license: GPLv3, see LICENSE.md for more details.
28 :license: GPLv3, see LICENSE.md for more details.
29 """
29 """
30
30
31 import datetime
31 import datetime
32 import logging
32 import logging
33 import time
33 import time
34 import traceback
34 import traceback
35
35
36 import webob.exc
36 import webob.exc
37 import paste.httpexceptions
37 import paste.httpexceptions
38 import paste.auth.basic
38 import paste.auth.basic
39 import paste.httpheaders
39 import paste.httpheaders
40
40
41 from pylons import config, tmpl_context as c, request, session, url
41 from pylons import config, tmpl_context as c, request, session, url
42 from pylons.controllers import WSGIController
42 from pylons.controllers import WSGIController
43 from pylons.controllers.util import redirect
44 from pylons.templating import render_mako as render # don't remove this import
43 from pylons.templating import render_mako as render # don't remove this import
45 from pylons.i18n.translation import _
44 from pylons.i18n.translation import _
46
45
47 from kallithea import __version__, BACKENDS
46 from kallithea import __version__, BACKENDS
48
47
49 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
48 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
50 safe_str, safe_int
49 safe_str, safe_int
51 from kallithea.lib import auth_modules
50 from kallithea.lib import auth_modules
52 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
51 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
53 from kallithea.lib.utils import get_repo_slug
52 from kallithea.lib.utils import get_repo_slug
54 from kallithea.lib.exceptions import UserCreationError
53 from kallithea.lib.exceptions import UserCreationError
55 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
54 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
56 from kallithea.model import meta
55 from kallithea.model import meta
57
56
58 from kallithea.model.db import Repository, Ui, User, Setting
57 from kallithea.model.db import Repository, Ui, User, Setting
59 from kallithea.model.notification import NotificationModel
58 from kallithea.model.notification import NotificationModel
60 from kallithea.model.scm import ScmModel
59 from kallithea.model.scm import ScmModel
61 from kallithea.model.pull_request import PullRequestModel
60 from kallithea.model.pull_request import PullRequestModel
62
61
63 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
64
63
65
64
66 def _filter_proxy(ip):
65 def _filter_proxy(ip):
67 """
66 """
68 HEADERS can have multiple ips inside the left-most being the original
67 HEADERS can have multiple ips inside the left-most being the original
69 client, and each successive proxy that passed the request adding the IP
68 client, and each successive proxy that passed the request adding the IP
70 address where it received the request from.
69 address where it received the request from.
71
70
72 :param ip:
71 :param ip:
73 """
72 """
74 if ',' in ip:
73 if ',' in ip:
75 _ips = ip.split(',')
74 _ips = ip.split(',')
76 _first_ip = _ips[0].strip()
75 _first_ip = _ips[0].strip()
77 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
76 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
78 return _first_ip
77 return _first_ip
79 return ip
78 return ip
80
79
81
80
82 def _get_ip_addr(environ):
81 def _get_ip_addr(environ):
83 proxy_key = 'HTTP_X_REAL_IP'
82 proxy_key = 'HTTP_X_REAL_IP'
84 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
83 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
85 def_key = 'REMOTE_ADDR'
84 def_key = 'REMOTE_ADDR'
86
85
87 ip = environ.get(proxy_key)
86 ip = environ.get(proxy_key)
88 if ip:
87 if ip:
89 return _filter_proxy(ip)
88 return _filter_proxy(ip)
90
89
91 ip = environ.get(proxy_key2)
90 ip = environ.get(proxy_key2)
92 if ip:
91 if ip:
93 return _filter_proxy(ip)
92 return _filter_proxy(ip)
94
93
95 ip = environ.get(def_key, '0.0.0.0')
94 ip = environ.get(def_key, '0.0.0.0')
96 return _filter_proxy(ip)
95 return _filter_proxy(ip)
97
96
98
97
99 def _get_access_path(environ):
98 def _get_access_path(environ):
100 path = environ.get('PATH_INFO')
99 path = environ.get('PATH_INFO')
101 org_req = environ.get('pylons.original_request')
100 org_req = environ.get('pylons.original_request')
102 if org_req:
101 if org_req:
103 path = org_req.environ.get('PATH_INFO')
102 path = org_req.environ.get('PATH_INFO')
104 return path
103 return path
105
104
106
105
107 def log_in_user(user, remember, is_external_auth):
106 def log_in_user(user, remember, is_external_auth):
108 """
107 """
109 Log a `User` in and update session and cookies. If `remember` is True,
108 Log a `User` in and update session and cookies. If `remember` is True,
110 the session cookie is set to expire in a year; otherwise, it expires at
109 the session cookie is set to expire in a year; otherwise, it expires at
111 the end of the browser session.
110 the end of the browser session.
112
111
113 Returns populated `AuthUser` object.
112 Returns populated `AuthUser` object.
114 """
113 """
115 user.update_lastlogin()
114 user.update_lastlogin()
116 meta.Session().commit()
115 meta.Session().commit()
117
116
118 auth_user = AuthUser(dbuser=user,
117 auth_user = AuthUser(dbuser=user,
119 is_external_auth=is_external_auth)
118 is_external_auth=is_external_auth)
120 auth_user.set_authenticated()
119 auth_user.set_authenticated()
121
120
122 # Start new session to prevent session fixation attacks.
121 # Start new session to prevent session fixation attacks.
123 session.invalidate()
122 session.invalidate()
124 session['authuser'] = cookie = auth_user.to_cookie()
123 session['authuser'] = cookie = auth_user.to_cookie()
125
124
126 # If they want to be remembered, update the cookie.
125 # If they want to be remembered, update the cookie.
127 # NOTE: Assumes that beaker defaults to browser session cookie.
126 # NOTE: Assumes that beaker defaults to browser session cookie.
128 if remember:
127 if remember:
129 t = datetime.datetime.now() + datetime.timedelta(days=365)
128 t = datetime.datetime.now() + datetime.timedelta(days=365)
130 session._set_cookie_expires(t)
129 session._set_cookie_expires(t)
131
130
132 session.save()
131 session.save()
133
132
134 log.info('user %s is now authenticated and stored in '
133 log.info('user %s is now authenticated and stored in '
135 'session, session attrs %s', user.username, cookie)
134 'session, session attrs %s', user.username, cookie)
136
135
137 # dumps session attrs back to cookie
136 # dumps session attrs back to cookie
138 session._update_cookie_out()
137 session._update_cookie_out()
139
138
140 return auth_user
139 return auth_user
141
140
142
141
143 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
142 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
144
143
145 def __init__(self, realm, authfunc, auth_http_code=None):
144 def __init__(self, realm, authfunc, auth_http_code=None):
146 self.realm = realm
145 self.realm = realm
147 self.authfunc = authfunc
146 self.authfunc = authfunc
148 self._rc_auth_http_code = auth_http_code
147 self._rc_auth_http_code = auth_http_code
149
148
150 def build_authentication(self):
149 def build_authentication(self):
151 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
150 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
152 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
151 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
153 # return 403 if alternative http return code is specified in
152 # return 403 if alternative http return code is specified in
154 # Kallithea config
153 # Kallithea config
155 return paste.httpexceptions.HTTPForbidden(headers=head)
154 return paste.httpexceptions.HTTPForbidden(headers=head)
156 return paste.httpexceptions.HTTPUnauthorized(headers=head)
155 return paste.httpexceptions.HTTPUnauthorized(headers=head)
157
156
158 def authenticate(self, environ):
157 def authenticate(self, environ):
159 authorization = paste.httpheaders.AUTHORIZATION(environ)
158 authorization = paste.httpheaders.AUTHORIZATION(environ)
160 if not authorization:
159 if not authorization:
161 return self.build_authentication()
160 return self.build_authentication()
162 (authmeth, auth) = authorization.split(' ', 1)
161 (authmeth, auth) = authorization.split(' ', 1)
163 if 'basic' != authmeth.lower():
162 if 'basic' != authmeth.lower():
164 return self.build_authentication()
163 return self.build_authentication()
165 auth = auth.strip().decode('base64')
164 auth = auth.strip().decode('base64')
166 _parts = auth.split(':', 1)
165 _parts = auth.split(':', 1)
167 if len(_parts) == 2:
166 if len(_parts) == 2:
168 username, password = _parts
167 username, password = _parts
169 if self.authfunc(username, password, environ) is not None:
168 if self.authfunc(username, password, environ) is not None:
170 return username
169 return username
171 return self.build_authentication()
170 return self.build_authentication()
172
171
173 __call__ = authenticate
172 __call__ = authenticate
174
173
175
174
176 class BaseVCSController(object):
175 class BaseVCSController(object):
177
176
178 def __init__(self, application, config):
177 def __init__(self, application, config):
179 self.application = application
178 self.application = application
180 self.config = config
179 self.config = config
181 # base path of repo locations
180 # base path of repo locations
182 self.basepath = self.config['base_path']
181 self.basepath = self.config['base_path']
183 # authenticate this VCS request using the authentication modules
182 # authenticate this VCS request using the authentication modules
184 self.authenticate = BasicAuth('', auth_modules.authenticate,
183 self.authenticate = BasicAuth('', auth_modules.authenticate,
185 config.get('auth_ret_code'))
184 config.get('auth_ret_code'))
186 self.ip_addr = '0.0.0.0'
185 self.ip_addr = '0.0.0.0'
187
186
188 def _handle_request(self, environ, start_response):
187 def _handle_request(self, environ, start_response):
189 raise NotImplementedError()
188 raise NotImplementedError()
190
189
191 def _get_by_id(self, repo_name):
190 def _get_by_id(self, repo_name):
192 """
191 """
193 Gets a special pattern _<ID> from clone url and tries to replace it
192 Gets a special pattern _<ID> from clone url and tries to replace it
194 with a repository_name for support of _<ID> permanent URLs
193 with a repository_name for support of _<ID> permanent URLs
195
194
196 :param repo_name:
195 :param repo_name:
197 """
196 """
198
197
199 data = repo_name.split('/')
198 data = repo_name.split('/')
200 if len(data) >= 2:
199 if len(data) >= 2:
201 from kallithea.lib.utils import get_repo_by_id
200 from kallithea.lib.utils import get_repo_by_id
202 by_id_match = get_repo_by_id(repo_name)
201 by_id_match = get_repo_by_id(repo_name)
203 if by_id_match:
202 if by_id_match:
204 data[1] = by_id_match
203 data[1] = by_id_match
205
204
206 return '/'.join(data)
205 return '/'.join(data)
207
206
208 def _invalidate_cache(self, repo_name):
207 def _invalidate_cache(self, repo_name):
209 """
208 """
210 Sets cache for this repository for invalidation on next access
209 Sets cache for this repository for invalidation on next access
211
210
212 :param repo_name: full repo name, also a cache key
211 :param repo_name: full repo name, also a cache key
213 """
212 """
214 ScmModel().mark_for_invalidation(repo_name)
213 ScmModel().mark_for_invalidation(repo_name)
215
214
216 def _check_permission(self, action, user, repo_name, ip_addr=None):
215 def _check_permission(self, action, user, repo_name, ip_addr=None):
217 """
216 """
218 Checks permissions using action (push/pull) user and repository
217 Checks permissions using action (push/pull) user and repository
219 name
218 name
220
219
221 :param action: push or pull action
220 :param action: push or pull action
222 :param user: `User` instance
221 :param user: `User` instance
223 :param repo_name: repository name
222 :param repo_name: repository name
224 """
223 """
225 # check IP
224 # check IP
226 ip_allowed = AuthUser.check_ip_allowed(user, ip_addr)
225 ip_allowed = AuthUser.check_ip_allowed(user, ip_addr)
227 if ip_allowed:
226 if ip_allowed:
228 log.info('Access for IP:%s allowed', ip_addr)
227 log.info('Access for IP:%s allowed', ip_addr)
229 else:
228 else:
230 return False
229 return False
231
230
232 if action == 'push':
231 if action == 'push':
233 if not HasPermissionAnyMiddleware('repository.write',
232 if not HasPermissionAnyMiddleware('repository.write',
234 'repository.admin')(user,
233 'repository.admin')(user,
235 repo_name):
234 repo_name):
236 return False
235 return False
237
236
238 else:
237 else:
239 #any other action need at least read permission
238 #any other action need at least read permission
240 if not HasPermissionAnyMiddleware('repository.read',
239 if not HasPermissionAnyMiddleware('repository.read',
241 'repository.write',
240 'repository.write',
242 'repository.admin')(user,
241 'repository.admin')(user,
243 repo_name):
242 repo_name):
244 return False
243 return False
245
244
246 return True
245 return True
247
246
248 def _get_ip_addr(self, environ):
247 def _get_ip_addr(self, environ):
249 return _get_ip_addr(environ)
248 return _get_ip_addr(environ)
250
249
251 def _check_ssl(self, environ):
250 def _check_ssl(self, environ):
252 """
251 """
253 Checks the SSL check flag and returns False if SSL is not present
252 Checks the SSL check flag and returns False if SSL is not present
254 and required True otherwise
253 and required True otherwise
255 """
254 """
256 #check if we have SSL required ! if not it's a bad request !
255 #check if we have SSL required ! if not it's a bad request !
257 if str2bool(Ui.get_by_key('push_ssl').ui_value):
256 if str2bool(Ui.get_by_key('push_ssl').ui_value):
258 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
257 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
259 if org_proto != 'https':
258 if org_proto != 'https':
260 log.debug('proto is %s and SSL is required BAD REQUEST !',
259 log.debug('proto is %s and SSL is required BAD REQUEST !',
261 org_proto)
260 org_proto)
262 return False
261 return False
263 return True
262 return True
264
263
265 def _check_locking_state(self, environ, action, repo, user_id):
264 def _check_locking_state(self, environ, action, repo, user_id):
266 """
265 """
267 Checks locking on this repository, if locking is enabled and lock is
266 Checks locking on this repository, if locking is enabled and lock is
268 present returns a tuple of make_lock, locked, locked_by.
267 present returns a tuple of make_lock, locked, locked_by.
269 make_lock can have 3 states None (do nothing) True, make lock
268 make_lock can have 3 states None (do nothing) True, make lock
270 False release lock, This value is later propagated to hooks, which
269 False release lock, This value is later propagated to hooks, which
271 do the locking. Think about this as signals passed to hooks what to do.
270 do the locking. Think about this as signals passed to hooks what to do.
272
271
273 """
272 """
274 locked = False # defines that locked error should be thrown to user
273 locked = False # defines that locked error should be thrown to user
275 make_lock = None
274 make_lock = None
276 repo = Repository.get_by_repo_name(repo)
275 repo = Repository.get_by_repo_name(repo)
277 user = User.get(user_id)
276 user = User.get(user_id)
278
277
279 # this is kind of hacky, but due to how mercurial handles client-server
278 # this is kind of hacky, but due to how mercurial handles client-server
280 # server see all operation on changeset; bookmarks, phases and
279 # server see all operation on changeset; bookmarks, phases and
281 # obsolescence marker in different transaction, we don't want to check
280 # obsolescence marker in different transaction, we don't want to check
282 # locking on those
281 # locking on those
283 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
282 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
284 locked_by = repo.locked
283 locked_by = repo.locked
285 if repo and repo.enable_locking and not obsolete_call:
284 if repo and repo.enable_locking and not obsolete_call:
286 if action == 'push':
285 if action == 'push':
287 #check if it's already locked !, if it is compare users
286 #check if it's already locked !, if it is compare users
288 user_id, _date = repo.locked
287 user_id, _date = repo.locked
289 if user.user_id == user_id:
288 if user.user_id == user_id:
290 log.debug('Got push from user %s, now unlocking', user)
289 log.debug('Got push from user %s, now unlocking', user)
291 # unlock if we have push from user who locked
290 # unlock if we have push from user who locked
292 make_lock = False
291 make_lock = False
293 else:
292 else:
294 # we're not the same user who locked, ban with 423 !
293 # we're not the same user who locked, ban with 423 !
295 locked = True
294 locked = True
296 if action == 'pull':
295 if action == 'pull':
297 if repo.locked[0] and repo.locked[1]:
296 if repo.locked[0] and repo.locked[1]:
298 locked = True
297 locked = True
299 else:
298 else:
300 log.debug('Setting lock on repo %s by %s', repo, user)
299 log.debug('Setting lock on repo %s by %s', repo, user)
301 make_lock = True
300 make_lock = True
302
301
303 else:
302 else:
304 log.debug('Repository %s do not have locking enabled', repo)
303 log.debug('Repository %s do not have locking enabled', repo)
305 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
304 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
306 make_lock, locked, locked_by)
305 make_lock, locked, locked_by)
307 return make_lock, locked, locked_by
306 return make_lock, locked, locked_by
308
307
309 def __call__(self, environ, start_response):
308 def __call__(self, environ, start_response):
310 start = time.time()
309 start = time.time()
311 try:
310 try:
312 return self._handle_request(environ, start_response)
311 return self._handle_request(environ, start_response)
313 finally:
312 finally:
314 log = logging.getLogger('kallithea.' + self.__class__.__name__)
313 log = logging.getLogger('kallithea.' + self.__class__.__name__)
315 log.debug('Request time: %.3fs', time.time() - start)
314 log.debug('Request time: %.3fs', time.time() - start)
316 meta.Session.remove()
315 meta.Session.remove()
317
316
318
317
319 class BaseController(WSGIController):
318 class BaseController(WSGIController):
320
319
321 def __before__(self):
320 def __before__(self):
322 """
321 """
323 __before__ is called before controller methods and after __call__
322 __before__ is called before controller methods and after __call__
324 """
323 """
325 c.kallithea_version = __version__
324 c.kallithea_version = __version__
326 rc_config = Setting.get_app_settings()
325 rc_config = Setting.get_app_settings()
327
326
328 # Visual options
327 # Visual options
329 c.visual = AttributeDict({})
328 c.visual = AttributeDict({})
330
329
331 ## DB stored
330 ## DB stored
332 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
331 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
333 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
332 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
334 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
333 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
335 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
334 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
336 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
335 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
337 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
336 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
338 c.visual.show_version = str2bool(rc_config.get('show_version'))
337 c.visual.show_version = str2bool(rc_config.get('show_version'))
339 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
338 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
340 c.visual.gravatar_url = rc_config.get('gravatar_url')
339 c.visual.gravatar_url = rc_config.get('gravatar_url')
341
340
342 c.ga_code = rc_config.get('ga_code')
341 c.ga_code = rc_config.get('ga_code')
343 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
342 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
344 if c.ga_code and '<' not in c.ga_code:
343 if c.ga_code and '<' not in c.ga_code:
345 c.ga_code = '''<script type="text/javascript">
344 c.ga_code = '''<script type="text/javascript">
346 var _gaq = _gaq || [];
345 var _gaq = _gaq || [];
347 _gaq.push(['_setAccount', '%s']);
346 _gaq.push(['_setAccount', '%s']);
348 _gaq.push(['_trackPageview']);
347 _gaq.push(['_trackPageview']);
349
348
350 (function() {
349 (function() {
351 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
350 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
352 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
351 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
353 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
352 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
354 })();
353 })();
355 </script>''' % c.ga_code
354 </script>''' % c.ga_code
356 c.site_name = rc_config.get('title')
355 c.site_name = rc_config.get('title')
357 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
356 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
358
357
359 ## INI stored
358 ## INI stored
360 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
359 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
361 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
360 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
362
361
363 c.instance_id = config.get('instance_id')
362 c.instance_id = config.get('instance_id')
364 c.issues_url = config.get('bugtracker', url('issues_url'))
363 c.issues_url = config.get('bugtracker', url('issues_url'))
365 # END CONFIG VARS
364 # END CONFIG VARS
366
365
367 c.repo_name = get_repo_slug(request) # can be empty
366 c.repo_name = get_repo_slug(request) # can be empty
368 c.backends = BACKENDS.keys()
367 c.backends = BACKENDS.keys()
369 c.unread_notifications = NotificationModel()\
368 c.unread_notifications = NotificationModel()\
370 .get_unread_cnt_for_user(c.authuser.user_id)
369 .get_unread_cnt_for_user(c.authuser.user_id)
371
370
372 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
371 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
373
372
374 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
373 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
375
374
376 self.sa = meta.Session
375 self.sa = meta.Session
377 self.scm_model = ScmModel(self.sa)
376 self.scm_model = ScmModel(self.sa)
378
377
379 @staticmethod
378 @staticmethod
380 def _determine_auth_user(api_key, session_authuser):
379 def _determine_auth_user(api_key, session_authuser):
381 """
380 """
382 Create an `AuthUser` object given the API key (if any) and the
381 Create an `AuthUser` object given the API key (if any) and the
383 value of the authuser session cookie.
382 value of the authuser session cookie.
384 """
383 """
385
384
386 # Authenticate by API key
385 # Authenticate by API key
387 if api_key:
386 if api_key:
388 # when using API_KEY we are sure user exists.
387 # when using API_KEY we are sure user exists.
389 return AuthUser(dbuser=User.get_by_api_key(api_key),
388 return AuthUser(dbuser=User.get_by_api_key(api_key),
390 is_external_auth=True)
389 is_external_auth=True)
391
390
392 # Authenticate by session cookie
391 # Authenticate by session cookie
393 # In ancient login sessions, 'authuser' may not be a dict.
392 # In ancient login sessions, 'authuser' may not be a dict.
394 # In that case, the user will have to log in again.
393 # In that case, the user will have to log in again.
395 if isinstance(session_authuser, dict):
394 if isinstance(session_authuser, dict):
396 try:
395 try:
397 return AuthUser.from_cookie(session_authuser)
396 return AuthUser.from_cookie(session_authuser)
398 except UserCreationError as e:
397 except UserCreationError as e:
399 # container auth or other auth functions that create users on
398 # container auth or other auth functions that create users on
400 # the fly can throw UserCreationError to signal issues with
399 # the fly can throw UserCreationError to signal issues with
401 # user creation. Explanation should be provided in the
400 # user creation. Explanation should be provided in the
402 # exception object.
401 # exception object.
403 from kallithea.lib import helpers as h
402 from kallithea.lib import helpers as h
404 h.flash(e, 'error', logf=log.error)
403 h.flash(e, 'error', logf=log.error)
405
404
406 # Authenticate by auth_container plugin (if enabled)
405 # Authenticate by auth_container plugin (if enabled)
407 if any(
406 if any(
408 auth_modules.importplugin(name).is_container_auth
407 auth_modules.importplugin(name).is_container_auth
409 for name in Setting.get_auth_plugins()
408 for name in Setting.get_auth_plugins()
410 ):
409 ):
411 try:
410 try:
412 user_info = auth_modules.authenticate('', '', request.environ)
411 user_info = auth_modules.authenticate('', '', request.environ)
413 except UserCreationError as e:
412 except UserCreationError as e:
414 from kallithea.lib import helpers as h
413 from kallithea.lib import helpers as h
415 h.flash(e, 'error', logf=log.error)
414 h.flash(e, 'error', logf=log.error)
416 else:
415 else:
417 if user_info is not None:
416 if user_info is not None:
418 username = user_info['username']
417 username = user_info['username']
419 user = User.get_by_username(username, case_insensitive=True)
418 user = User.get_by_username(username, case_insensitive=True)
420 return log_in_user(user, remember=False,
419 return log_in_user(user, remember=False,
421 is_external_auth=True)
420 is_external_auth=True)
422
421
423 # User is anonymous
422 # User is anonymous
424 return AuthUser()
423 return AuthUser()
425
424
426 def __call__(self, environ, start_response):
425 def __call__(self, environ, start_response):
427 """Invoke the Controller"""
426 """Invoke the Controller"""
428
427
429 # WSGIController.__call__ dispatches to the Controller method
428 # WSGIController.__call__ dispatches to the Controller method
430 # the request is routed to. This routing information is
429 # the request is routed to. This routing information is
431 # available in environ['pylons.routes_dict']
430 # available in environ['pylons.routes_dict']
432 try:
431 try:
433 self.ip_addr = _get_ip_addr(environ)
432 self.ip_addr = _get_ip_addr(environ)
434 # make sure that we update permissions each time we call controller
433 # make sure that we update permissions each time we call controller
435
434
436 #set globals for auth user
435 #set globals for auth user
437 self.authuser = c.authuser = request.user = self._determine_auth_user(
436 self.authuser = c.authuser = request.user = self._determine_auth_user(
438 request.GET.get('api_key'),
437 request.GET.get('api_key'),
439 session.get('authuser'),
438 session.get('authuser'),
440 )
439 )
441
440
442 log.info('IP: %s User: %s accessed %s',
441 log.info('IP: %s User: %s accessed %s',
443 self.ip_addr, self.authuser,
442 self.ip_addr, self.authuser,
444 safe_unicode(_get_access_path(environ)),
443 safe_unicode(_get_access_path(environ)),
445 )
444 )
446 return WSGIController.__call__(self, environ, start_response)
445 return WSGIController.__call__(self, environ, start_response)
447 finally:
446 finally:
448 meta.Session.remove()
447 meta.Session.remove()
449
448
450
449
451 class BaseRepoController(BaseController):
450 class BaseRepoController(BaseController):
452 """
451 """
453 Base class for controllers responsible for loading all needed data for
452 Base class for controllers responsible for loading all needed data for
454 repository loaded items are
453 repository loaded items are
455
454
456 c.db_repo_scm_instance: instance of scm repository
455 c.db_repo_scm_instance: instance of scm repository
457 c.db_repo: instance of db
456 c.db_repo: instance of db
458 c.repository_followers: number of followers
457 c.repository_followers: number of followers
459 c.repository_forks: number of forks
458 c.repository_forks: number of forks
460 c.repository_following: weather the current user is following the current repo
459 c.repository_following: weather the current user is following the current repo
461 """
460 """
462
461
463 def __before__(self):
462 def __before__(self):
464 super(BaseRepoController, self).__before__()
463 super(BaseRepoController, self).__before__()
465 if c.repo_name: # extracted from routes
464 if c.repo_name: # extracted from routes
466 _dbr = Repository.get_by_repo_name(c.repo_name)
465 _dbr = Repository.get_by_repo_name(c.repo_name)
467 if not _dbr:
466 if not _dbr:
468 return
467 return
469
468
470 log.debug('Found repository in database %s with state `%s`',
469 log.debug('Found repository in database %s with state `%s`',
471 safe_unicode(_dbr), safe_unicode(_dbr.repo_state))
470 safe_unicode(_dbr), safe_unicode(_dbr.repo_state))
472 route = getattr(request.environ.get('routes.route'), 'name', '')
471 route = getattr(request.environ.get('routes.route'), 'name', '')
473
472
474 # allow to delete repos that are somehow damages in filesystem
473 # allow to delete repos that are somehow damages in filesystem
475 if route in ['delete_repo']:
474 if route in ['delete_repo']:
476 return
475 return
477
476
478 if _dbr.repo_state in [Repository.STATE_PENDING]:
477 if _dbr.repo_state in [Repository.STATE_PENDING]:
479 if route in ['repo_creating_home']:
478 if route in ['repo_creating_home']:
480 return
479 return
481 check_url = url('repo_creating_home', repo_name=c.repo_name)
480 check_url = url('repo_creating_home', repo_name=c.repo_name)
482 return redirect(check_url)
481 raise webob.exc.HTTPFound(location=check_url)
483
482
484 dbr = c.db_repo = _dbr
483 dbr = c.db_repo = _dbr
485 c.db_repo_scm_instance = c.db_repo.scm_instance
484 c.db_repo_scm_instance = c.db_repo.scm_instance
486 if c.db_repo_scm_instance is None:
485 if c.db_repo_scm_instance is None:
487 log.error('%s this repository is present in database but it '
486 log.error('%s this repository is present in database but it '
488 'cannot be created as an scm instance', c.repo_name)
487 'cannot be created as an scm instance', c.repo_name)
489 from kallithea.lib import helpers as h
488 from kallithea.lib import helpers as h
490 h.flash(h.literal(_('Repository not found in the filesystem')),
489 h.flash(h.literal(_('Repository not found in the filesystem')),
491 category='error')
490 category='error')
492 raise paste.httpexceptions.HTTPNotFound()
491 raise paste.httpexceptions.HTTPNotFound()
493
492
494 # some globals counter for menu
493 # some globals counter for menu
495 c.repository_followers = self.scm_model.get_followers(dbr)
494 c.repository_followers = self.scm_model.get_followers(dbr)
496 c.repository_forks = self.scm_model.get_forks(dbr)
495 c.repository_forks = self.scm_model.get_forks(dbr)
497 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
496 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
498 c.repository_following = self.scm_model.is_following_repo(
497 c.repository_following = self.scm_model.is_following_repo(
499 c.repo_name, self.authuser.user_id)
498 c.repo_name, self.authuser.user_id)
500
499
501 @staticmethod
500 @staticmethod
502 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
501 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
503 """
502 """
504 Safe way to get changeset. If error occurs show error.
503 Safe way to get changeset. If error occurs show error.
505 """
504 """
506 from kallithea.lib import helpers as h
505 from kallithea.lib import helpers as h
507 try:
506 try:
508 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
507 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
509 except EmptyRepositoryError as e:
508 except EmptyRepositoryError as e:
510 if returnempty:
509 if returnempty:
511 return repo.scm_instance.EMPTY_CHANGESET
510 return repo.scm_instance.EMPTY_CHANGESET
512 h.flash(h.literal(_('There are no changesets yet')),
511 h.flash(h.literal(_('There are no changesets yet')),
513 category='error')
512 category='error')
514 raise webob.exc.HTTPNotFound()
513 raise webob.exc.HTTPNotFound()
515 except ChangesetDoesNotExistError as e:
514 except ChangesetDoesNotExistError as e:
516 h.flash(h.literal(_('Changeset not found')),
515 h.flash(h.literal(_('Changeset not found')),
517 category='error')
516 category='error')
518 raise webob.exc.HTTPNotFound()
517 raise webob.exc.HTTPNotFound()
519 except RepositoryError as e:
518 except RepositoryError as e:
520 log.error(traceback.format_exc())
519 log.error(traceback.format_exc())
521 h.flash(safe_str(e), category='error')
520 h.flash(safe_str(e), category='error')
522 raise webob.exc.HTTPBadRequest()
521 raise webob.exc.HTTPBadRequest()
523
522
524
523
525 class WSGIResultCloseCallback(object):
524 class WSGIResultCloseCallback(object):
526 """Wrap a WSGI result and let close call close after calling the
525 """Wrap a WSGI result and let close call close after calling the
527 close method on the result.
526 close method on the result.
528 """
527 """
529 def __init__(self, result, close):
528 def __init__(self, result, close):
530 self._result = result
529 self._result = result
531 self._close = close
530 self._close = close
532
531
533 def __iter__(self):
532 def __iter__(self):
534 return iter(self._result)
533 return iter(self._result)
535
534
536 def close(self):
535 def close(self):
537 if hasattr(self._result, 'close'):
536 if hasattr(self._result, 'close'):
538 self._result.close()
537 self._result.close()
539 self._close()
538 self._close()
General Comments 0
You need to be logged in to leave comments. Login now