##// END OF EJS Templates
auth: remove HasPermissionAll and variants...
Søren Løvborg -
r6026:09bcde0e default
parent child Browse files
Show More
@@ -1,148 +1,148 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.admin
15 kallithea.controllers.admin.admin
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Controller for Admin panel of Kallithea
18 Controller for Admin panel of 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
28
29 import logging
29 import logging
30
30
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from sqlalchemy.orm import joinedload
32 from sqlalchemy.orm import joinedload
33 from whoosh.qparser.default import QueryParser
33 from whoosh.qparser.default import QueryParser
34 from whoosh.qparser.dateparse import DateParserPlugin
34 from whoosh.qparser.dateparse import DateParserPlugin
35 from whoosh import query
35 from whoosh import query
36 from sqlalchemy.sql.expression import or_, and_, func
36 from sqlalchemy.sql.expression import or_, and_, func
37
37
38 from kallithea.model.db import UserLog
38 from kallithea.model.db import UserLog
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator
40 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
42 from kallithea.lib.indexers import JOURNAL_SCHEMA
42 from kallithea.lib.indexers import JOURNAL_SCHEMA
43 from kallithea.lib.helpers import Page
43 from kallithea.lib.helpers import Page
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def _journal_filter(user_log, search_term):
49 def _journal_filter(user_log, search_term):
50 """
50 """
51 Filters sqlalchemy user_log based on search_term with whoosh Query language
51 Filters sqlalchemy user_log based on search_term with whoosh Query language
52 http://packages.python.org/Whoosh/querylang.html
52 http://packages.python.org/Whoosh/querylang.html
53
53
54 :param user_log:
54 :param user_log:
55 :param search_term:
55 :param search_term:
56 """
56 """
57 log.debug('Initial search term: %r', search_term)
57 log.debug('Initial search term: %r', search_term)
58 qry = None
58 qry = None
59 if search_term:
59 if search_term:
60 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
60 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
61 qp.add_plugin(DateParserPlugin())
61 qp.add_plugin(DateParserPlugin())
62 qry = qp.parse(unicode(search_term))
62 qry = qp.parse(unicode(search_term))
63 log.debug('Filtering using parsed query %r', qry)
63 log.debug('Filtering using parsed query %r', qry)
64
64
65 def wildcard_handler(col, wc_term):
65 def wildcard_handler(col, wc_term):
66 if wc_term.startswith('*') and not wc_term.endswith('*'):
66 if wc_term.startswith('*') and not wc_term.endswith('*'):
67 #postfix == endswith
67 #postfix == endswith
68 wc_term = remove_prefix(wc_term, prefix='*')
68 wc_term = remove_prefix(wc_term, prefix='*')
69 return func.lower(col).endswith(wc_term)
69 return func.lower(col).endswith(wc_term)
70 elif wc_term.startswith('*') and wc_term.endswith('*'):
70 elif wc_term.startswith('*') and wc_term.endswith('*'):
71 #wildcard == ilike
71 #wildcard == ilike
72 wc_term = remove_prefix(wc_term, prefix='*')
72 wc_term = remove_prefix(wc_term, prefix='*')
73 wc_term = remove_suffix(wc_term, suffix='*')
73 wc_term = remove_suffix(wc_term, suffix='*')
74 return func.lower(col).contains(wc_term)
74 return func.lower(col).contains(wc_term)
75
75
76 def get_filterion(field, val, term):
76 def get_filterion(field, val, term):
77
77
78 if field == 'repository':
78 if field == 'repository':
79 field = getattr(UserLog, 'repository_name')
79 field = getattr(UserLog, 'repository_name')
80 elif field == 'ip':
80 elif field == 'ip':
81 field = getattr(UserLog, 'user_ip')
81 field = getattr(UserLog, 'user_ip')
82 elif field == 'date':
82 elif field == 'date':
83 field = getattr(UserLog, 'action_date')
83 field = getattr(UserLog, 'action_date')
84 elif field == 'username':
84 elif field == 'username':
85 field = getattr(UserLog, 'username')
85 field = getattr(UserLog, 'username')
86 else:
86 else:
87 field = getattr(UserLog, field)
87 field = getattr(UserLog, field)
88 log.debug('filter field: %s val=>%s', field, val)
88 log.debug('filter field: %s val=>%s', field, val)
89
89
90 #sql filtering
90 #sql filtering
91 if isinstance(term, query.Wildcard):
91 if isinstance(term, query.Wildcard):
92 return wildcard_handler(field, val)
92 return wildcard_handler(field, val)
93 elif isinstance(term, query.Prefix):
93 elif isinstance(term, query.Prefix):
94 return func.lower(field).startswith(func.lower(val))
94 return func.lower(field).startswith(func.lower(val))
95 elif isinstance(term, query.DateRange):
95 elif isinstance(term, query.DateRange):
96 return and_(field >= val[0], field <= val[1])
96 return and_(field >= val[0], field <= val[1])
97 return func.lower(field) == func.lower(val)
97 return func.lower(field) == func.lower(val)
98
98
99 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
99 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
100 query.DateRange)):
100 query.DateRange)):
101 if not isinstance(qry, query.And):
101 if not isinstance(qry, query.And):
102 qry = [qry]
102 qry = [qry]
103 for term in qry:
103 for term in qry:
104 field = term.fieldname
104 field = term.fieldname
105 val = (term.text if not isinstance(term, query.DateRange)
105 val = (term.text if not isinstance(term, query.DateRange)
106 else [term.startdate, term.enddate])
106 else [term.startdate, term.enddate])
107 user_log = user_log.filter(get_filterion(field, val, term))
107 user_log = user_log.filter(get_filterion(field, val, term))
108 elif isinstance(qry, query.Or):
108 elif isinstance(qry, query.Or):
109 filters = []
109 filters = []
110 for term in qry:
110 for term in qry:
111 field = term.fieldname
111 field = term.fieldname
112 val = (term.text if not isinstance(term, query.DateRange)
112 val = (term.text if not isinstance(term, query.DateRange)
113 else [term.startdate, term.enddate])
113 else [term.startdate, term.enddate])
114 filters.append(get_filterion(field, val, term))
114 filters.append(get_filterion(field, val, term))
115 user_log = user_log.filter(or_(*filters))
115 user_log = user_log.filter(or_(*filters))
116
116
117 return user_log
117 return user_log
118
118
119
119
120 class AdminController(BaseController):
120 class AdminController(BaseController):
121
121
122 @LoginRequired()
122 @LoginRequired()
123 def __before__(self):
123 def __before__(self):
124 super(AdminController, self).__before__()
124 super(AdminController, self).__before__()
125
125
126 @HasPermissionAllDecorator('hg.admin')
126 @HasPermissionAnyDecorator('hg.admin')
127 def index(self):
127 def index(self):
128 users_log = UserLog.query() \
128 users_log = UserLog.query() \
129 .options(joinedload(UserLog.user)) \
129 .options(joinedload(UserLog.user)) \
130 .options(joinedload(UserLog.repository))
130 .options(joinedload(UserLog.repository))
131
131
132 #FILTERING
132 #FILTERING
133 c.search_term = request.GET.get('filter')
133 c.search_term = request.GET.get('filter')
134 users_log = _journal_filter(users_log, c.search_term)
134 users_log = _journal_filter(users_log, c.search_term)
135
135
136 users_log = users_log.order_by(UserLog.action_date.desc())
136 users_log = users_log.order_by(UserLog.action_date.desc())
137
137
138 p = safe_int(request.GET.get('page'), 1)
138 p = safe_int(request.GET.get('page'), 1)
139
139
140 def url_generator(**kw):
140 def url_generator(**kw):
141 return url.current(filter=c.search_term, **kw)
141 return url.current(filter=c.search_term, **kw)
142
142
143 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
143 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
144
144
145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
146 return render('admin/admin_log.html')
146 return render('admin/admin_log.html')
147
147
148 return render('admin/admin.html')
148 return render('admin/admin.html')
@@ -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.i18n.translation import _
31 from pylons.i18n.translation import _
32 from webob.exc import HTTPFound
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, HasPermissionAnyDecorator
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 @HasPermissionAnyDecorator('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 raise HTTPFound(location=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.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
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, HasPermissionAnyDecorator
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 @HasPermissionAnyDecorator('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 raise HTTPFound(location=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,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.i18n.translation import _
35 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
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, HasPermissionAnyDecorator
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 @HasPermissionAnyDecorator('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 raise HTTPFound(location=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,458 +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.i18n.translation import _, ungettext
36 from pylons.i18n.translation import _, ungettext
37 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
37 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
38
38
39 import kallithea
39 import kallithea
40 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
41 from kallithea.lib.compat import json
41 from kallithea.lib.compat import json
42 from kallithea.lib.auth import LoginRequired, \
42 from kallithea.lib.auth import LoginRequired, \
43 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll, \
43 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAny, \
44 HasPermissionAll
44 HasPermissionAny
45 from kallithea.lib.base import BaseController, render
45 from kallithea.lib.base import BaseController, render
46 from kallithea.model.db import RepoGroup, Repository
46 from kallithea.model.db import RepoGroup, Repository
47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
48 from kallithea.model.repo_group import RepoGroupModel
48 from kallithea.model.repo_group import RepoGroupModel
49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51 from kallithea.model.repo import RepoModel
51 from kallithea.model.repo import RepoModel
52 from kallithea.lib.utils2 import safe_int
52 from kallithea.lib.utils2 import safe_int
53 from sqlalchemy.sql.expression import func
53 from sqlalchemy.sql.expression import func
54
54
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class RepoGroupsController(BaseController):
59 class RepoGroupsController(BaseController):
60
60
61 @LoginRequired()
61 @LoginRequired()
62 def __before__(self):
62 def __before__(self):
63 super(RepoGroupsController, self).__before__()
63 super(RepoGroupsController, self).__before__()
64
64
65 def __load_defaults(self, extras=(), exclude=()):
65 def __load_defaults(self, extras=(), exclude=()):
66 """extras is used for keeping current parent ignoring permissions
66 """extras is used for keeping current parent ignoring permissions
67 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
68 Note: only admin can create top level groups
68 Note: only admin can create top level groups
69 """
69 """
70 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
70 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
71 exclude_group_ids = set(rg.group_id for rg in exclude)
71 exclude_group_ids = set(rg.group_id for rg in exclude)
72 c.repo_groups = [rg for rg in repo_groups
72 c.repo_groups = [rg for rg in repo_groups
73 if rg[0] not in exclude_group_ids]
73 if rg[0] not in exclude_group_ids]
74
74
75 repo_model = RepoModel()
75 repo_model = RepoModel()
76 c.users_array = repo_model.get_users_js()
76 c.users_array = repo_model.get_users_js()
77 c.user_groups_array = repo_model.get_user_groups_js()
77 c.user_groups_array = repo_model.get_user_groups_js()
78
78
79 def __load_data(self, group_id):
79 def __load_data(self, group_id):
80 """
80 """
81 Load defaults settings for edit, and update
81 Load defaults settings for edit, and update
82
82
83 :param group_id:
83 :param group_id:
84 """
84 """
85 repo_group = RepoGroup.get_or_404(group_id)
85 repo_group = RepoGroup.get_or_404(group_id)
86 data = repo_group.get_dict()
86 data = repo_group.get_dict()
87 data['group_name'] = repo_group.name
87 data['group_name'] = repo_group.name
88
88
89 # fill repository group users
89 # fill repository group users
90 for p in repo_group.repo_group_to_perm:
90 for p in repo_group.repo_group_to_perm:
91 data.update({'u_perm_%s' % p.user.username:
91 data.update({'u_perm_%s' % p.user.username:
92 p.permission.permission_name})
92 p.permission.permission_name})
93
93
94 # fill repository group groups
94 # fill repository group groups
95 for p in repo_group.users_group_to_perm:
95 for p in repo_group.users_group_to_perm:
96 data.update({'g_perm_%s' % p.users_group.users_group_name:
96 data.update({'g_perm_%s' % p.users_group.users_group_name:
97 p.permission.permission_name})
97 p.permission.permission_name})
98
98
99 return data
99 return data
100
100
101 def _revoke_perms_on_yourself(self, form_result):
101 def _revoke_perms_on_yourself(self, form_result):
102 _up = filter(lambda u: c.authuser.username == u[0],
102 _up = filter(lambda u: c.authuser.username == u[0],
103 form_result['perms_updates'])
103 form_result['perms_updates'])
104 _new = filter(lambda u: c.authuser.username == u[0],
104 _new = filter(lambda u: c.authuser.username == u[0],
105 form_result['perms_new'])
105 form_result['perms_new'])
106 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':
107 return True
107 return True
108 return False
108 return False
109
109
110 def index(self, format='html'):
110 def index(self, format='html'):
111 """GET /repo_groups: All items in the collection"""
111 """GET /repo_groups: All items in the collection"""
112 # url('repos_groups')
112 # url('repos_groups')
113 _list = RepoGroup.query() \
113 _list = RepoGroup.query() \
114 .order_by(func.lower(RepoGroup.group_name)) \
114 .order_by(func.lower(RepoGroup.group_name)) \
115 .all()
115 .all()
116 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
116 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
117 repo_groups_data = []
117 repo_groups_data = []
118 total_records = len(group_iter)
118 total_records = len(group_iter)
119 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
119 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
120 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
120 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
121
121
122 repo_group_name = lambda repo_group_name, children_groups: (
122 repo_group_name = lambda repo_group_name, children_groups: (
123 template.get_def("repo_group_name")
123 template.get_def("repo_group_name")
124 .render(repo_group_name, children_groups, _=_, h=h, c=c)
124 .render(repo_group_name, children_groups, _=_, h=h, c=c)
125 )
125 )
126 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: (
127 template.get_def("repo_group_actions")
127 template.get_def("repo_group_actions")
128 .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,
129 ungettext=ungettext)
129 ungettext=ungettext)
130 )
130 )
131
131
132 for repo_gr in group_iter:
132 for repo_gr in group_iter:
133 children_groups = map(h.safe_unicode,
133 children_groups = map(h.safe_unicode,
134 itertools.chain((g.name for g in repo_gr.parents),
134 itertools.chain((g.name for g in repo_gr.parents),
135 (x.name for x in [repo_gr])))
135 (x.name for x in [repo_gr])))
136 repo_count = repo_gr.repositories.count()
136 repo_count = repo_gr.repositories.count()
137 repo_groups_data.append({
137 repo_groups_data.append({
138 "raw_name": repo_gr.group_name,
138 "raw_name": repo_gr.group_name,
139 "group_name": repo_group_name(repo_gr.group_name, children_groups),
139 "group_name": repo_group_name(repo_gr.group_name, children_groups),
140 "desc": h.escape(repo_gr.group_description),
140 "desc": h.escape(repo_gr.group_description),
141 "repos": repo_count,
141 "repos": repo_count,
142 "owner": h.person(repo_gr.user),
142 "owner": h.person(repo_gr.user),
143 "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,
144 repo_count)
144 repo_count)
145 })
145 })
146
146
147 c.data = json.dumps({
147 c.data = json.dumps({
148 "totalRecords": total_records,
148 "totalRecords": total_records,
149 "startIndex": 0,
149 "startIndex": 0,
150 "sort": None,
150 "sort": None,
151 "dir": "asc",
151 "dir": "asc",
152 "records": repo_groups_data
152 "records": repo_groups_data
153 })
153 })
154
154
155 return render('admin/repo_groups/repo_groups.html')
155 return render('admin/repo_groups/repo_groups.html')
156
156
157 def create(self):
157 def create(self):
158 """POST /repo_groups: Create a new item"""
158 """POST /repo_groups: Create a new item"""
159 # url('repos_groups')
159 # url('repos_groups')
160
160
161 self.__load_defaults()
161 self.__load_defaults()
162
162
163 # permissions for can create group based on parent_id are checked
163 # permissions for can create group based on parent_id are checked
164 # here in the Form
164 # here in the Form
165 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
165 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
166 try:
166 try:
167 form_result = repo_group_form.to_python(dict(request.POST))
167 form_result = repo_group_form.to_python(dict(request.POST))
168 gr = RepoGroupModel().create(
168 gr = RepoGroupModel().create(
169 group_name=form_result['group_name'],
169 group_name=form_result['group_name'],
170 group_description=form_result['group_description'],
170 group_description=form_result['group_description'],
171 parent=form_result['group_parent_id'],
171 parent=form_result['group_parent_id'],
172 owner=self.authuser.user_id, # TODO: make editable
172 owner=self.authuser.user_id, # TODO: make editable
173 copy_permissions=form_result['group_copy_permissions']
173 copy_permissions=form_result['group_copy_permissions']
174 )
174 )
175 Session().commit()
175 Session().commit()
176 #TODO: in futureaction_logger(, '', '', '', self.sa)
176 #TODO: in futureaction_logger(, '', '', '', self.sa)
177 except formencode.Invalid as errors:
177 except formencode.Invalid as errors:
178 return htmlfill.render(
178 return htmlfill.render(
179 render('admin/repo_groups/repo_group_add.html'),
179 render('admin/repo_groups/repo_group_add.html'),
180 defaults=errors.value,
180 defaults=errors.value,
181 errors=errors.error_dict or {},
181 errors=errors.error_dict or {},
182 prefix_error=False,
182 prefix_error=False,
183 encoding="UTF-8",
183 encoding="UTF-8",
184 force_defaults=False)
184 force_defaults=False)
185 except Exception:
185 except Exception:
186 log.error(traceback.format_exc())
186 log.error(traceback.format_exc())
187 h.flash(_('Error occurred during creation of repository group %s') \
187 h.flash(_('Error occurred during creation of repository group %s') \
188 % request.POST.get('group_name'), category='error')
188 % request.POST.get('group_name'), category='error')
189 parent_group_id = form_result['group_parent_id']
189 parent_group_id = form_result['group_parent_id']
190 #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
191 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
191 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
192 h.flash(_('Created repository group %s') % gr.group_name,
192 h.flash(_('Created repository group %s') % gr.group_name,
193 category='success')
193 category='success')
194 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
194 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
195
195
196 def new(self):
196 def new(self):
197 """GET /repo_groups/new: Form to create a new item"""
197 """GET /repo_groups/new: Form to create a new item"""
198 # url('new_repos_group')
198 # url('new_repos_group')
199 if HasPermissionAll('hg.admin')('group create'):
199 if HasPermissionAny('hg.admin')('group create'):
200 #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
201 pass
201 pass
202 else:
202 else:
203 # we pass in parent group into creation form, thus we know
203 # we pass in parent group into creation form, thus we know
204 # what would be the group, we can check perms here !
204 # what would be the group, we can check perms here !
205 group_id = safe_int(request.GET.get('parent_group'))
205 group_id = safe_int(request.GET.get('parent_group'))
206 group = RepoGroup.get(group_id) if group_id else None
206 group = RepoGroup.get(group_id) if group_id else None
207 group_name = group.group_name if group else None
207 group_name = group.group_name if group else None
208 if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
208 if HasRepoGroupPermissionAny('group.admin')(group_name, 'group create'):
209 pass
209 pass
210 else:
210 else:
211 raise HTTPForbidden()
211 raise HTTPForbidden()
212
212
213 self.__load_defaults()
213 self.__load_defaults()
214 return render('admin/repo_groups/repo_group_add.html')
214 return render('admin/repo_groups/repo_group_add.html')
215
215
216 @HasRepoGroupPermissionAnyDecorator('group.admin')
216 @HasRepoGroupPermissionAnyDecorator('group.admin')
217 def update(self, group_name):
217 def update(self, group_name):
218 """PUT /repo_groups/group_name: Update an existing item"""
218 """PUT /repo_groups/group_name: Update an existing item"""
219 # Forms posted to this method should contain a hidden field:
219 # Forms posted to this method should contain a hidden field:
220 # <input type="hidden" name="_method" value="PUT" />
220 # <input type="hidden" name="_method" value="PUT" />
221 # Or using helpers:
221 # Or using helpers:
222 # h.form(url('repos_group', group_name=GROUP_NAME),
222 # h.form(url('repos_group', group_name=GROUP_NAME),
223 # method='put')
223 # method='put')
224 # url('repos_group', group_name=GROUP_NAME)
224 # url('repos_group', group_name=GROUP_NAME)
225
225
226 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
226 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
227 self.__load_defaults(extras=[c.repo_group.parent_group],
227 self.__load_defaults(extras=[c.repo_group.parent_group],
228 exclude=[c.repo_group])
228 exclude=[c.repo_group])
229
229
230 # 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!
231 if HasPermissionAll('hg.admin')('group edit'):
231 if HasPermissionAny('hg.admin')('group edit'):
232 #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
233 allow_empty_group = True
233 allow_empty_group = True
234 elif not c.repo_group.parent_group:
234 elif not c.repo_group.parent_group:
235 allow_empty_group = True
235 allow_empty_group = True
236 else:
236 else:
237 allow_empty_group = False
237 allow_empty_group = False
238 repo_group_form = RepoGroupForm(
238 repo_group_form = RepoGroupForm(
239 edit=True,
239 edit=True,
240 old_data=c.repo_group.get_dict(),
240 old_data=c.repo_group.get_dict(),
241 repo_groups=c.repo_groups,
241 repo_groups=c.repo_groups,
242 can_create_in_root=allow_empty_group,
242 can_create_in_root=allow_empty_group,
243 )()
243 )()
244 try:
244 try:
245 form_result = repo_group_form.to_python(dict(request.POST))
245 form_result = repo_group_form.to_python(dict(request.POST))
246
246
247 new_gr = RepoGroupModel().update(group_name, form_result)
247 new_gr = RepoGroupModel().update(group_name, form_result)
248 Session().commit()
248 Session().commit()
249 h.flash(_('Updated repository group %s') \
249 h.flash(_('Updated repository group %s') \
250 % form_result['group_name'], category='success')
250 % form_result['group_name'], category='success')
251 # we now have new name !
251 # we now have new name !
252 group_name = new_gr.group_name
252 group_name = new_gr.group_name
253 #TODO: in future action_logger(, '', '', '', self.sa)
253 #TODO: in future action_logger(, '', '', '', self.sa)
254 except formencode.Invalid as errors:
254 except formencode.Invalid as errors:
255
255
256 return htmlfill.render(
256 return htmlfill.render(
257 render('admin/repo_groups/repo_group_edit.html'),
257 render('admin/repo_groups/repo_group_edit.html'),
258 defaults=errors.value,
258 defaults=errors.value,
259 errors=errors.error_dict or {},
259 errors=errors.error_dict or {},
260 prefix_error=False,
260 prefix_error=False,
261 encoding="UTF-8",
261 encoding="UTF-8",
262 force_defaults=False)
262 force_defaults=False)
263 except Exception:
263 except Exception:
264 log.error(traceback.format_exc())
264 log.error(traceback.format_exc())
265 h.flash(_('Error occurred during update of repository group %s') \
265 h.flash(_('Error occurred during update of repository group %s') \
266 % request.POST.get('group_name'), category='error')
266 % request.POST.get('group_name'), category='error')
267
267
268 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
268 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
269
269
270 @HasRepoGroupPermissionAnyDecorator('group.admin')
270 @HasRepoGroupPermissionAnyDecorator('group.admin')
271 def delete(self, group_name):
271 def delete(self, group_name):
272 """DELETE /repo_groups/group_name: Delete an existing item"""
272 """DELETE /repo_groups/group_name: Delete an existing item"""
273 # Forms posted to this method should contain a hidden field:
273 # Forms posted to this method should contain a hidden field:
274 # <input type="hidden" name="_method" value="DELETE" />
274 # <input type="hidden" name="_method" value="DELETE" />
275 # Or using helpers:
275 # Or using helpers:
276 # h.form(url('repos_group', group_name=GROUP_NAME),
276 # h.form(url('repos_group', group_name=GROUP_NAME),
277 # method='delete')
277 # method='delete')
278 # url('repos_group', group_name=GROUP_NAME)
278 # url('repos_group', group_name=GROUP_NAME)
279
279
280 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
280 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
281 repos = gr.repositories.all()
281 repos = gr.repositories.all()
282 if repos:
282 if repos:
283 h.flash(_('This group contains %s repositories and cannot be '
283 h.flash(_('This group contains %s repositories and cannot be '
284 'deleted') % len(repos), category='warning')
284 'deleted') % len(repos), category='warning')
285 raise HTTPFound(location=url('repos_groups'))
285 raise HTTPFound(location=url('repos_groups'))
286
286
287 children = gr.children.all()
287 children = gr.children.all()
288 if children:
288 if children:
289 h.flash(_('This group contains %s subgroups and cannot be deleted'
289 h.flash(_('This group contains %s subgroups and cannot be deleted'
290 % (len(children))), category='warning')
290 % (len(children))), category='warning')
291 raise HTTPFound(location=url('repos_groups'))
291 raise HTTPFound(location=url('repos_groups'))
292
292
293 try:
293 try:
294 RepoGroupModel().delete(group_name)
294 RepoGroupModel().delete(group_name)
295 Session().commit()
295 Session().commit()
296 h.flash(_('Removed repository group %s') % group_name,
296 h.flash(_('Removed repository group %s') % group_name,
297 category='success')
297 category='success')
298 #TODO: in future action_logger(, '', '', '', self.sa)
298 #TODO: in future action_logger(, '', '', '', self.sa)
299 except Exception:
299 except Exception:
300 log.error(traceback.format_exc())
300 log.error(traceback.format_exc())
301 h.flash(_('Error occurred during deletion of repository group %s')
301 h.flash(_('Error occurred during deletion of repository group %s')
302 % group_name, category='error')
302 % group_name, category='error')
303
303
304 if gr.parent_group:
304 if gr.parent_group:
305 raise HTTPFound(location=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))
306 raise HTTPFound(location=url('repos_groups'))
306 raise HTTPFound(location=url('repos_groups'))
307
307
308 def show_by_name(self, group_name):
308 def show_by_name(self, group_name):
309 """
309 """
310 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
311 the group by id view instead
311 the group by id view instead
312 """
312 """
313 group_name = group_name.rstrip('/')
313 group_name = group_name.rstrip('/')
314 id_ = RepoGroup.get_by_group_name(group_name)
314 id_ = RepoGroup.get_by_group_name(group_name)
315 if id_:
315 if id_:
316 return self.show(group_name)
316 return self.show(group_name)
317 raise HTTPNotFound
317 raise HTTPNotFound
318
318
319 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
319 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
320 'group.admin')
320 'group.admin')
321 def show(self, group_name):
321 def show(self, group_name):
322 """GET /repo_groups/group_name: Show a specific item"""
322 """GET /repo_groups/group_name: Show a specific item"""
323 # url('repos_group', group_name=GROUP_NAME)
323 # url('repos_group', group_name=GROUP_NAME)
324 c.active = 'settings'
324 c.active = 'settings'
325
325
326 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
326 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
327 c.group_repos = c.group.repositories.all()
327 c.group_repos = c.group.repositories.all()
328
328
329 #overwrite our cached list with current filter
329 #overwrite our cached list with current filter
330 c.repo_cnt = 0
330 c.repo_cnt = 0
331
331
332 groups = RepoGroup.query().order_by(RepoGroup.group_name) \
332 groups = RepoGroup.query().order_by(RepoGroup.group_name) \
333 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
333 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
334 c.groups = self.scm_model.get_repo_groups(groups)
334 c.groups = self.scm_model.get_repo_groups(groups)
335
335
336 c.repos_list = Repository.query() \
336 c.repos_list = Repository.query() \
337 .filter(Repository.group_id == c.group.group_id) \
337 .filter(Repository.group_id == c.group.group_id) \
338 .order_by(func.lower(Repository.repo_name)) \
338 .order_by(func.lower(Repository.repo_name)) \
339 .all()
339 .all()
340
340
341 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,
342 admin=False, short_name=True)
342 admin=False, short_name=True)
343 #json used to render the grid
343 #json used to render the grid
344 c.data = json.dumps(repos_data)
344 c.data = json.dumps(repos_data)
345
345
346 return render('admin/repo_groups/repo_group_show.html')
346 return render('admin/repo_groups/repo_group_show.html')
347
347
348 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 @HasRepoGroupPermissionAnyDecorator('group.admin')
349 def edit(self, group_name):
349 def edit(self, group_name):
350 """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"""
351 # url('edit_repo_group', group_name=GROUP_NAME)
351 # url('edit_repo_group', group_name=GROUP_NAME)
352 c.active = 'settings'
352 c.active = 'settings'
353
353
354 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
354 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
355 self.__load_defaults(extras=[c.repo_group.parent_group],
355 self.__load_defaults(extras=[c.repo_group.parent_group],
356 exclude=[c.repo_group])
356 exclude=[c.repo_group])
357 defaults = self.__load_data(c.repo_group.group_id)
357 defaults = self.__load_data(c.repo_group.group_id)
358
358
359 return htmlfill.render(
359 return htmlfill.render(
360 render('admin/repo_groups/repo_group_edit.html'),
360 render('admin/repo_groups/repo_group_edit.html'),
361 defaults=defaults,
361 defaults=defaults,
362 encoding="UTF-8",
362 encoding="UTF-8",
363 force_defaults=False
363 force_defaults=False
364 )
364 )
365
365
366 @HasRepoGroupPermissionAnyDecorator('group.admin')
366 @HasRepoGroupPermissionAnyDecorator('group.admin')
367 def edit_repo_group_advanced(self, group_name):
367 def edit_repo_group_advanced(self, group_name):
368 """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"""
369 # url('edit_repo_group', group_name=GROUP_NAME)
369 # url('edit_repo_group', group_name=GROUP_NAME)
370 c.active = 'advanced'
370 c.active = 'advanced'
371 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
371 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
372
372
373 return render('admin/repo_groups/repo_group_edit.html')
373 return render('admin/repo_groups/repo_group_edit.html')
374
374
375 @HasRepoGroupPermissionAnyDecorator('group.admin')
375 @HasRepoGroupPermissionAnyDecorator('group.admin')
376 def edit_repo_group_perms(self, group_name):
376 def edit_repo_group_perms(self, group_name):
377 """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"""
378 # url('edit_repo_group', group_name=GROUP_NAME)
378 # url('edit_repo_group', group_name=GROUP_NAME)
379 c.active = 'perms'
379 c.active = 'perms'
380 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
380 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
381 self.__load_defaults()
381 self.__load_defaults()
382 defaults = self.__load_data(c.repo_group.group_id)
382 defaults = self.__load_data(c.repo_group.group_id)
383
383
384 return htmlfill.render(
384 return htmlfill.render(
385 render('admin/repo_groups/repo_group_edit.html'),
385 render('admin/repo_groups/repo_group_edit.html'),
386 defaults=defaults,
386 defaults=defaults,
387 encoding="UTF-8",
387 encoding="UTF-8",
388 force_defaults=False
388 force_defaults=False
389 )
389 )
390
390
391 @HasRepoGroupPermissionAnyDecorator('group.admin')
391 @HasRepoGroupPermissionAnyDecorator('group.admin')
392 def update_perms(self, group_name):
392 def update_perms(self, group_name):
393 """
393 """
394 Update permissions for given repository group
394 Update permissions for given repository group
395
395
396 :param group_name:
396 :param group_name:
397 """
397 """
398
398
399 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
399 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
400 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
400 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
401 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
401 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
402 if not c.authuser.is_admin:
402 if not c.authuser.is_admin:
403 if self._revoke_perms_on_yourself(form_result):
403 if self._revoke_perms_on_yourself(form_result):
404 msg = _('Cannot revoke permission for yourself as admin')
404 msg = _('Cannot revoke permission for yourself as admin')
405 h.flash(msg, category='warning')
405 h.flash(msg, category='warning')
406 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
406 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
407 recursive = form_result['recursive']
407 recursive = form_result['recursive']
408 # 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
409 # set the permissions !
409 # set the permissions !
410 # this can be potentially heavy operation
410 # this can be potentially heavy operation
411 RepoGroupModel()._update_permissions(c.repo_group,
411 RepoGroupModel()._update_permissions(c.repo_group,
412 form_result['perms_new'],
412 form_result['perms_new'],
413 form_result['perms_updates'],
413 form_result['perms_updates'],
414 recursive)
414 recursive)
415 #TODO: implement this
415 #TODO: implement this
416 #action_logger(self.authuser, 'admin_changed_repo_permissions',
416 #action_logger(self.authuser, 'admin_changed_repo_permissions',
417 # repo_name, self.ip_addr, self.sa)
417 # repo_name, self.ip_addr, self.sa)
418 Session().commit()
418 Session().commit()
419 h.flash(_('Repository group permissions updated'), category='success')
419 h.flash(_('Repository group permissions updated'), category='success')
420 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
420 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
421
421
422 @HasRepoGroupPermissionAnyDecorator('group.admin')
422 @HasRepoGroupPermissionAnyDecorator('group.admin')
423 def delete_perms(self, group_name):
423 def delete_perms(self, group_name):
424 """
424 """
425 DELETE an existing repository group permission user
425 DELETE an existing repository group permission user
426
426
427 :param group_name:
427 :param group_name:
428 """
428 """
429 try:
429 try:
430 obj_type = request.POST.get('obj_type')
430 obj_type = request.POST.get('obj_type')
431 obj_id = None
431 obj_id = None
432 if obj_type == 'user':
432 if obj_type == 'user':
433 obj_id = safe_int(request.POST.get('user_id'))
433 obj_id = safe_int(request.POST.get('user_id'))
434 elif obj_type == 'user_group':
434 elif obj_type == 'user_group':
435 obj_id = safe_int(request.POST.get('user_group_id'))
435 obj_id = safe_int(request.POST.get('user_group_id'))
436
436
437 if not c.authuser.is_admin:
437 if not c.authuser.is_admin:
438 if obj_type == 'user' and c.authuser.user_id == obj_id:
438 if obj_type == 'user' and c.authuser.user_id == obj_id:
439 msg = _('Cannot revoke permission for yourself as admin')
439 msg = _('Cannot revoke permission for yourself as admin')
440 h.flash(msg, category='warning')
440 h.flash(msg, category='warning')
441 raise Exception('revoke admin permission on self')
441 raise Exception('revoke admin permission on self')
442 recursive = request.POST.get('recursive', 'none')
442 recursive = request.POST.get('recursive', 'none')
443 if obj_type == 'user':
443 if obj_type == 'user':
444 RepoGroupModel().delete_permission(repo_group=group_name,
444 RepoGroupModel().delete_permission(repo_group=group_name,
445 obj=obj_id, obj_type='user',
445 obj=obj_id, obj_type='user',
446 recursive=recursive)
446 recursive=recursive)
447 elif obj_type == 'user_group':
447 elif obj_type == 'user_group':
448 RepoGroupModel().delete_permission(repo_group=group_name,
448 RepoGroupModel().delete_permission(repo_group=group_name,
449 obj=obj_id,
449 obj=obj_id,
450 obj_type='user_group',
450 obj_type='user_group',
451 recursive=recursive)
451 recursive=recursive)
452
452
453 Session().commit()
453 Session().commit()
454 except Exception:
454 except Exception:
455 log.error(traceback.format_exc())
455 log.error(traceback.format_exc())
456 h.flash(_('An error occurred during revoking of permission'),
456 h.flash(_('An error occurred during revoking of permission'),
457 category='error')
457 category='error')
458 raise HTTPInternalServerError()
458 raise HTTPInternalServerError()
@@ -1,640 +1,639 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 pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
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, \
38 from kallithea.lib.auth import LoginRequired, \
39 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, \
39 HasRepoPermissionAnyDecorator, NotAnonymous, HasPermissionAny
40 HasRepoPermissionAnyDecorator
41 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.utils import action_logger, jsonify
41 from kallithea.lib.utils import action_logger, jsonify
43 from kallithea.lib.vcs import RepositoryError
42 from kallithea.lib.vcs import RepositoryError
44 from kallithea.model.meta import Session
43 from kallithea.model.meta import Session
45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
44 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
46 Setting, RepositoryField
45 Setting, RepositoryField
47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
46 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
47 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
49 from kallithea.model.repo import RepoModel
48 from kallithea.model.repo import RepoModel
50 from kallithea.lib.compat import json
49 from kallithea.lib.compat import json
51 from kallithea.lib.exceptions import AttachedForksError
50 from kallithea.lib.exceptions import AttachedForksError
52 from kallithea.lib.utils2 import safe_int
51 from kallithea.lib.utils2 import safe_int
53
52
54 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
55
54
56
55
57 class ReposController(BaseRepoController):
56 class ReposController(BaseRepoController):
58 """
57 """
59 REST Controller styled on the Atom Publishing Protocol"""
58 REST Controller styled on the Atom Publishing Protocol"""
60 # To properly map this controller, ensure your config/routing.py
59 # To properly map this controller, ensure your config/routing.py
61 # file has a resource setup:
60 # file has a resource setup:
62 # map.resource('repo', 'repos')
61 # map.resource('repo', 'repos')
63
62
64 @LoginRequired()
63 @LoginRequired()
65 def __before__(self):
64 def __before__(self):
66 super(ReposController, self).__before__()
65 super(ReposController, self).__before__()
67
66
68 def _load_repo(self, repo_name):
67 def _load_repo(self, repo_name):
69 repo_obj = Repository.get_by_repo_name(repo_name)
68 repo_obj = Repository.get_by_repo_name(repo_name)
70
69
71 if repo_obj is None:
70 if repo_obj is None:
72 h.not_mapped_error(repo_name)
71 h.not_mapped_error(repo_name)
73 raise HTTPFound(location=url('repos'))
72 raise HTTPFound(location=url('repos'))
74
73
75 return repo_obj
74 return repo_obj
76
75
77 def __load_defaults(self, repo=None):
76 def __load_defaults(self, repo=None):
78 top_perms = ['hg.create.repository']
77 top_perms = ['hg.create.repository']
79 repo_group_perms = ['group.admin']
78 repo_group_perms = ['group.admin']
80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
79 if HasPermissionAny('hg.create.write_on_repogroup.true')():
81 repo_group_perms.append('group.write')
80 repo_group_perms.append('group.write')
82 extras = [] if repo is None else [repo.group]
81 extras = [] if repo is None else [repo.group]
83
82
84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
83 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
85
84
86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
85 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
87
86
88 def __load_data(self, repo_name=None):
87 def __load_data(self, repo_name=None):
89 """
88 """
90 Load defaults settings for edit, and update
89 Load defaults settings for edit, and update
91
90
92 :param repo_name:
91 :param repo_name:
93 """
92 """
94 c.repo_info = self._load_repo(repo_name)
93 c.repo_info = self._load_repo(repo_name)
95 self.__load_defaults(c.repo_info)
94 self.__load_defaults(c.repo_info)
96
95
97 defaults = RepoModel()._get_defaults(repo_name)
96 defaults = RepoModel()._get_defaults(repo_name)
98 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
97 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
99
98
100 return defaults
99 return defaults
101
100
102 def index(self, format='html'):
101 def index(self, format='html'):
103 """GET /repos: All items in the collection"""
102 """GET /repos: All items in the collection"""
104 # url('repos')
103 # url('repos')
105 _list = Repository.query() \
104 _list = Repository.query() \
106 .order_by(func.lower(Repository.repo_name)) \
105 .order_by(func.lower(Repository.repo_name)) \
107 .all()
106 .all()
108
107
109 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
108 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
110 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
109 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
111 admin=True,
110 admin=True,
112 super_user_actions=True)
111 super_user_actions=True)
113 #json used to render the grid
112 #json used to render the grid
114 c.data = json.dumps(repos_data)
113 c.data = json.dumps(repos_data)
115
114
116 return render('admin/repos/repos.html')
115 return render('admin/repos/repos.html')
117
116
118 @NotAnonymous()
117 @NotAnonymous()
119 def create(self):
118 def create(self):
120 """
119 """
121 POST /repos: Create a new item"""
120 POST /repos: Create a new item"""
122 # url('repos')
121 # url('repos')
123
122
124 self.__load_defaults()
123 self.__load_defaults()
125 form_result = {}
124 form_result = {}
126 task_id = None
125 task_id = None
127 try:
126 try:
128 # CanWriteGroup validators checks permissions of this POST
127 # CanWriteGroup validators checks permissions of this POST
129 form_result = RepoForm(repo_groups=c.repo_groups,
128 form_result = RepoForm(repo_groups=c.repo_groups,
130 landing_revs=c.landing_revs_choices)() \
129 landing_revs=c.landing_revs_choices)() \
131 .to_python(dict(request.POST))
130 .to_python(dict(request.POST))
132
131
133 # create is done sometimes async on celery, db transaction
132 # create is done sometimes async on celery, db transaction
134 # management is handled there.
133 # management is handled there.
135 task = RepoModel().create(form_result, self.authuser.user_id)
134 task = RepoModel().create(form_result, self.authuser.user_id)
136 from celery.result import BaseAsyncResult
135 from celery.result import BaseAsyncResult
137 if isinstance(task, BaseAsyncResult):
136 if isinstance(task, BaseAsyncResult):
138 task_id = task.task_id
137 task_id = task.task_id
139 except formencode.Invalid as errors:
138 except formencode.Invalid as errors:
140 log.info(errors)
139 log.info(errors)
141 return htmlfill.render(
140 return htmlfill.render(
142 render('admin/repos/repo_add.html'),
141 render('admin/repos/repo_add.html'),
143 defaults=errors.value,
142 defaults=errors.value,
144 errors=errors.error_dict or {},
143 errors=errors.error_dict or {},
145 prefix_error=False,
144 prefix_error=False,
146 force_defaults=False,
145 force_defaults=False,
147 encoding="UTF-8")
146 encoding="UTF-8")
148
147
149 except Exception:
148 except Exception:
150 log.error(traceback.format_exc())
149 log.error(traceback.format_exc())
151 msg = (_('Error creating repository %s')
150 msg = (_('Error creating repository %s')
152 % form_result.get('repo_name'))
151 % form_result.get('repo_name'))
153 h.flash(msg, category='error')
152 h.flash(msg, category='error')
154 raise HTTPFound(location=url('home'))
153 raise HTTPFound(location=url('home'))
155
154
156 raise HTTPFound(location=h.url('repo_creating_home',
155 raise HTTPFound(location=h.url('repo_creating_home',
157 repo_name=form_result['repo_name_full'],
156 repo_name=form_result['repo_name_full'],
158 task_id=task_id))
157 task_id=task_id))
159
158
160 @NotAnonymous()
159 @NotAnonymous()
161 def create_repository(self):
160 def create_repository(self):
162 """GET /_admin/create_repository: Form to create a new item"""
161 """GET /_admin/create_repository: Form to create a new item"""
163 self.__load_defaults()
162 self.__load_defaults()
164 if not c.repo_groups:
163 if not c.repo_groups:
165 raise HTTPForbidden
164 raise HTTPForbidden
166 parent_group = request.GET.get('parent_group')
165 parent_group = request.GET.get('parent_group')
167
166
168 ## apply the defaults from defaults page
167 ## apply the defaults from defaults page
169 defaults = Setting.get_default_repo_settings(strip_prefix=True)
168 defaults = Setting.get_default_repo_settings(strip_prefix=True)
170 if parent_group:
169 if parent_group:
171 prg = RepoGroup.get(parent_group)
170 prg = RepoGroup.get(parent_group)
172 if prg is None or not any(rgc[0] == prg.group_id
171 if prg is None or not any(rgc[0] == prg.group_id
173 for rgc in c.repo_groups):
172 for rgc in c.repo_groups):
174 raise HTTPForbidden
173 raise HTTPForbidden
175 defaults.update({'repo_group': parent_group})
174 defaults.update({'repo_group': parent_group})
176
175
177 return htmlfill.render(
176 return htmlfill.render(
178 render('admin/repos/repo_add.html'),
177 render('admin/repos/repo_add.html'),
179 defaults=defaults,
178 defaults=defaults,
180 errors={},
179 errors={},
181 prefix_error=False,
180 prefix_error=False,
182 encoding="UTF-8",
181 encoding="UTF-8",
183 force_defaults=False)
182 force_defaults=False)
184
183
185 @LoginRequired()
184 @LoginRequired()
186 @NotAnonymous()
185 @NotAnonymous()
187 def repo_creating(self, repo_name):
186 def repo_creating(self, repo_name):
188 c.repo = repo_name
187 c.repo = repo_name
189 c.task_id = request.GET.get('task_id')
188 c.task_id = request.GET.get('task_id')
190 if not c.repo:
189 if not c.repo:
191 raise HTTPNotFound()
190 raise HTTPNotFound()
192 return render('admin/repos/repo_creating.html')
191 return render('admin/repos/repo_creating.html')
193
192
194 @LoginRequired()
193 @LoginRequired()
195 @NotAnonymous()
194 @NotAnonymous()
196 @jsonify
195 @jsonify
197 def repo_check(self, repo_name):
196 def repo_check(self, repo_name):
198 c.repo = repo_name
197 c.repo = repo_name
199 task_id = request.GET.get('task_id')
198 task_id = request.GET.get('task_id')
200
199
201 if task_id and task_id not in ['None']:
200 if task_id and task_id not in ['None']:
202 from kallithea import CELERY_ON
201 from kallithea import CELERY_ON
203 from celery.result import AsyncResult
202 from celery.result import AsyncResult
204 if CELERY_ON:
203 if CELERY_ON:
205 task = AsyncResult(task_id)
204 task = AsyncResult(task_id)
206 if task.failed():
205 if task.failed():
207 raise HTTPInternalServerError(task.traceback)
206 raise HTTPInternalServerError(task.traceback)
208
207
209 repo = Repository.get_by_repo_name(repo_name)
208 repo = Repository.get_by_repo_name(repo_name)
210 if repo and repo.repo_state == Repository.STATE_CREATED:
209 if repo and repo.repo_state == Repository.STATE_CREATED:
211 if repo.clone_uri:
210 if repo.clone_uri:
212 h.flash(_('Created repository %s from %s')
211 h.flash(_('Created repository %s from %s')
213 % (repo.repo_name, repo.clone_uri_hidden), category='success')
212 % (repo.repo_name, repo.clone_uri_hidden), category='success')
214 else:
213 else:
215 repo_url = h.link_to(repo.repo_name,
214 repo_url = h.link_to(repo.repo_name,
216 h.url('summary_home',
215 h.url('summary_home',
217 repo_name=repo.repo_name))
216 repo_name=repo.repo_name))
218 fork = repo.fork
217 fork = repo.fork
219 if fork is not None:
218 if fork is not None:
220 fork_name = fork.repo_name
219 fork_name = fork.repo_name
221 h.flash(h.literal(_('Forked repository %s as %s')
220 h.flash(h.literal(_('Forked repository %s as %s')
222 % (fork_name, repo_url)), category='success')
221 % (fork_name, repo_url)), category='success')
223 else:
222 else:
224 h.flash(h.literal(_('Created repository %s') % repo_url),
223 h.flash(h.literal(_('Created repository %s') % repo_url),
225 category='success')
224 category='success')
226 return {'result': True}
225 return {'result': True}
227 return {'result': False}
226 return {'result': False}
228
227
229 @HasRepoPermissionAllDecorator('repository.admin')
228 @HasRepoPermissionAnyDecorator('repository.admin')
230 def update(self, repo_name):
229 def update(self, repo_name):
231 """
230 """
232 PUT /repos/repo_name: Update an existing item"""
231 PUT /repos/repo_name: Update an existing item"""
233 # Forms posted to this method should contain a hidden field:
232 # Forms posted to this method should contain a hidden field:
234 # <input type="hidden" name="_method" value="PUT" />
233 # <input type="hidden" name="_method" value="PUT" />
235 # Or using helpers:
234 # Or using helpers:
236 # h.form(url('put_repo', repo_name=ID),
235 # h.form(url('put_repo', repo_name=ID),
237 # method='put')
236 # method='put')
238 # url('put_repo', repo_name=ID)
237 # url('put_repo', repo_name=ID)
239 c.repo_info = self._load_repo(repo_name)
238 c.repo_info = self._load_repo(repo_name)
240 self.__load_defaults(c.repo_info)
239 self.__load_defaults(c.repo_info)
241 c.active = 'settings'
240 c.active = 'settings'
242 c.repo_fields = RepositoryField.query() \
241 c.repo_fields = RepositoryField.query() \
243 .filter(RepositoryField.repository == c.repo_info).all()
242 .filter(RepositoryField.repository == c.repo_info).all()
244
243
245 repo_model = RepoModel()
244 repo_model = RepoModel()
246 changed_name = repo_name
245 changed_name = repo_name
247 repo = Repository.get_by_repo_name(repo_name)
246 repo = Repository.get_by_repo_name(repo_name)
248 old_data = {
247 old_data = {
249 'repo_name': repo_name,
248 'repo_name': repo_name,
250 'repo_group': repo.group.get_dict() if repo.group else {},
249 'repo_group': repo.group.get_dict() if repo.group else {},
251 'repo_type': repo.repo_type,
250 'repo_type': repo.repo_type,
252 }
251 }
253 _form = RepoForm(edit=True, old_data=old_data,
252 _form = RepoForm(edit=True, old_data=old_data,
254 repo_groups=c.repo_groups,
253 repo_groups=c.repo_groups,
255 landing_revs=c.landing_revs_choices)()
254 landing_revs=c.landing_revs_choices)()
256
255
257 try:
256 try:
258 form_result = _form.to_python(dict(request.POST))
257 form_result = _form.to_python(dict(request.POST))
259 repo = repo_model.update(repo_name, **form_result)
258 repo = repo_model.update(repo_name, **form_result)
260 ScmModel().mark_for_invalidation(repo_name)
259 ScmModel().mark_for_invalidation(repo_name)
261 h.flash(_('Repository %s updated successfully') % repo_name,
260 h.flash(_('Repository %s updated successfully') % repo_name,
262 category='success')
261 category='success')
263 changed_name = repo.repo_name
262 changed_name = repo.repo_name
264 action_logger(self.authuser, 'admin_updated_repo',
263 action_logger(self.authuser, 'admin_updated_repo',
265 changed_name, self.ip_addr, self.sa)
264 changed_name, self.ip_addr, self.sa)
266 Session().commit()
265 Session().commit()
267 except formencode.Invalid as errors:
266 except formencode.Invalid as errors:
268 log.info(errors)
267 log.info(errors)
269 defaults = self.__load_data(repo_name)
268 defaults = self.__load_data(repo_name)
270 defaults.update(errors.value)
269 defaults.update(errors.value)
271 c.users_array = repo_model.get_users_js()
270 c.users_array = repo_model.get_users_js()
272 return htmlfill.render(
271 return htmlfill.render(
273 render('admin/repos/repo_edit.html'),
272 render('admin/repos/repo_edit.html'),
274 defaults=defaults,
273 defaults=defaults,
275 errors=errors.error_dict or {},
274 errors=errors.error_dict or {},
276 prefix_error=False,
275 prefix_error=False,
277 encoding="UTF-8",
276 encoding="UTF-8",
278 force_defaults=False)
277 force_defaults=False)
279
278
280 except Exception:
279 except Exception:
281 log.error(traceback.format_exc())
280 log.error(traceback.format_exc())
282 h.flash(_('Error occurred during update of repository %s') \
281 h.flash(_('Error occurred during update of repository %s') \
283 % repo_name, category='error')
282 % repo_name, category='error')
284 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
283 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
285
284
286 @HasRepoPermissionAllDecorator('repository.admin')
285 @HasRepoPermissionAnyDecorator('repository.admin')
287 def delete(self, repo_name):
286 def delete(self, repo_name):
288 """
287 """
289 DELETE /repos/repo_name: Delete an existing item"""
288 DELETE /repos/repo_name: Delete an existing item"""
290 # Forms posted to this method should contain a hidden field:
289 # Forms posted to this method should contain a hidden field:
291 # <input type="hidden" name="_method" value="DELETE" />
290 # <input type="hidden" name="_method" value="DELETE" />
292 # Or using helpers:
291 # Or using helpers:
293 # h.form(url('delete_repo', repo_name=ID),
292 # h.form(url('delete_repo', repo_name=ID),
294 # method='delete')
293 # method='delete')
295 # url('delete_repo', repo_name=ID)
294 # url('delete_repo', repo_name=ID)
296
295
297 repo_model = RepoModel()
296 repo_model = RepoModel()
298 repo = repo_model.get_by_repo_name(repo_name)
297 repo = repo_model.get_by_repo_name(repo_name)
299 if not repo:
298 if not repo:
300 h.not_mapped_error(repo_name)
299 h.not_mapped_error(repo_name)
301 raise HTTPFound(location=url('repos'))
300 raise HTTPFound(location=url('repos'))
302 try:
301 try:
303 _forks = repo.forks.count()
302 _forks = repo.forks.count()
304 handle_forks = None
303 handle_forks = None
305 if _forks and request.POST.get('forks'):
304 if _forks and request.POST.get('forks'):
306 do = request.POST['forks']
305 do = request.POST['forks']
307 if do == 'detach_forks':
306 if do == 'detach_forks':
308 handle_forks = 'detach'
307 handle_forks = 'detach'
309 h.flash(_('Detached %s forks') % _forks, category='success')
308 h.flash(_('Detached %s forks') % _forks, category='success')
310 elif do == 'delete_forks':
309 elif do == 'delete_forks':
311 handle_forks = 'delete'
310 handle_forks = 'delete'
312 h.flash(_('Deleted %s forks') % _forks, category='success')
311 h.flash(_('Deleted %s forks') % _forks, category='success')
313 repo_model.delete(repo, forks=handle_forks)
312 repo_model.delete(repo, forks=handle_forks)
314 action_logger(self.authuser, 'admin_deleted_repo',
313 action_logger(self.authuser, 'admin_deleted_repo',
315 repo_name, self.ip_addr, self.sa)
314 repo_name, self.ip_addr, self.sa)
316 ScmModel().mark_for_invalidation(repo_name)
315 ScmModel().mark_for_invalidation(repo_name)
317 h.flash(_('Deleted repository %s') % repo_name, category='success')
316 h.flash(_('Deleted repository %s') % repo_name, category='success')
318 Session().commit()
317 Session().commit()
319 except AttachedForksError:
318 except AttachedForksError:
320 h.flash(_('Cannot delete repository %s which still has forks')
319 h.flash(_('Cannot delete repository %s which still has forks')
321 % repo_name, category='warning')
320 % repo_name, category='warning')
322
321
323 except Exception:
322 except Exception:
324 log.error(traceback.format_exc())
323 log.error(traceback.format_exc())
325 h.flash(_('An error occurred during deletion of %s') % repo_name,
324 h.flash(_('An error occurred during deletion of %s') % repo_name,
326 category='error')
325 category='error')
327
326
328 if repo.group:
327 if repo.group:
329 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
328 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
330 raise HTTPFound(location=url('repos'))
329 raise HTTPFound(location=url('repos'))
331
330
332 @HasRepoPermissionAllDecorator('repository.admin')
331 @HasRepoPermissionAnyDecorator('repository.admin')
333 def edit(self, repo_name):
332 def edit(self, repo_name):
334 """GET /repo_name/settings: Form to edit an existing item"""
333 """GET /repo_name/settings: Form to edit an existing item"""
335 # url('edit_repo', repo_name=ID)
334 # url('edit_repo', repo_name=ID)
336 defaults = self.__load_data(repo_name)
335 defaults = self.__load_data(repo_name)
337 c.repo_fields = RepositoryField.query() \
336 c.repo_fields = RepositoryField.query() \
338 .filter(RepositoryField.repository == c.repo_info).all()
337 .filter(RepositoryField.repository == c.repo_info).all()
339 repo_model = RepoModel()
338 repo_model = RepoModel()
340 c.users_array = repo_model.get_users_js()
339 c.users_array = repo_model.get_users_js()
341 c.active = 'settings'
340 c.active = 'settings'
342 return htmlfill.render(
341 return htmlfill.render(
343 render('admin/repos/repo_edit.html'),
342 render('admin/repos/repo_edit.html'),
344 defaults=defaults,
343 defaults=defaults,
345 encoding="UTF-8",
344 encoding="UTF-8",
346 force_defaults=False)
345 force_defaults=False)
347
346
348 @HasRepoPermissionAllDecorator('repository.admin')
347 @HasRepoPermissionAnyDecorator('repository.admin')
349 def edit_permissions(self, repo_name):
348 def edit_permissions(self, repo_name):
350 """GET /repo_name/settings: Form to edit an existing item"""
349 """GET /repo_name/settings: Form to edit an existing item"""
351 # url('edit_repo', repo_name=ID)
350 # url('edit_repo', repo_name=ID)
352 c.repo_info = self._load_repo(repo_name)
351 c.repo_info = self._load_repo(repo_name)
353 repo_model = RepoModel()
352 repo_model = RepoModel()
354 c.users_array = repo_model.get_users_js()
353 c.users_array = repo_model.get_users_js()
355 c.user_groups_array = repo_model.get_user_groups_js()
354 c.user_groups_array = repo_model.get_user_groups_js()
356 c.active = 'permissions'
355 c.active = 'permissions'
357 defaults = RepoModel()._get_defaults(repo_name)
356 defaults = RepoModel()._get_defaults(repo_name)
358
357
359 return htmlfill.render(
358 return htmlfill.render(
360 render('admin/repos/repo_edit.html'),
359 render('admin/repos/repo_edit.html'),
361 defaults=defaults,
360 defaults=defaults,
362 encoding="UTF-8",
361 encoding="UTF-8",
363 force_defaults=False)
362 force_defaults=False)
364
363
365 def edit_permissions_update(self, repo_name):
364 def edit_permissions_update(self, repo_name):
366 form = RepoPermsForm()().to_python(request.POST)
365 form = RepoPermsForm()().to_python(request.POST)
367 RepoModel()._update_permissions(repo_name, form['perms_new'],
366 RepoModel()._update_permissions(repo_name, form['perms_new'],
368 form['perms_updates'])
367 form['perms_updates'])
369 #TODO: implement this
368 #TODO: implement this
370 #action_logger(self.authuser, 'admin_changed_repo_permissions',
369 #action_logger(self.authuser, 'admin_changed_repo_permissions',
371 # repo_name, self.ip_addr, self.sa)
370 # repo_name, self.ip_addr, self.sa)
372 Session().commit()
371 Session().commit()
373 h.flash(_('Repository permissions updated'), category='success')
372 h.flash(_('Repository permissions updated'), category='success')
374 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
373 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
375
374
376 def edit_permissions_revoke(self, repo_name):
375 def edit_permissions_revoke(self, repo_name):
377 try:
376 try:
378 obj_type = request.POST.get('obj_type')
377 obj_type = request.POST.get('obj_type')
379 obj_id = None
378 obj_id = None
380 if obj_type == 'user':
379 if obj_type == 'user':
381 obj_id = safe_int(request.POST.get('user_id'))
380 obj_id = safe_int(request.POST.get('user_id'))
382 elif obj_type == 'user_group':
381 elif obj_type == 'user_group':
383 obj_id = safe_int(request.POST.get('user_group_id'))
382 obj_id = safe_int(request.POST.get('user_group_id'))
384
383
385 if obj_type == 'user':
384 if obj_type == 'user':
386 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
385 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
387 elif obj_type == 'user_group':
386 elif obj_type == 'user_group':
388 RepoModel().revoke_user_group_permission(
387 RepoModel().revoke_user_group_permission(
389 repo=repo_name, group_name=obj_id
388 repo=repo_name, group_name=obj_id
390 )
389 )
391 #TODO: implement this
390 #TODO: implement this
392 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
391 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
393 # repo_name, self.ip_addr, self.sa)
392 # repo_name, self.ip_addr, self.sa)
394 Session().commit()
393 Session().commit()
395 except Exception:
394 except Exception:
396 log.error(traceback.format_exc())
395 log.error(traceback.format_exc())
397 h.flash(_('An error occurred during revoking of permission'),
396 h.flash(_('An error occurred during revoking of permission'),
398 category='error')
397 category='error')
399 raise HTTPInternalServerError()
398 raise HTTPInternalServerError()
400
399
401 @HasRepoPermissionAllDecorator('repository.admin')
400 @HasRepoPermissionAnyDecorator('repository.admin')
402 def edit_fields(self, repo_name):
401 def edit_fields(self, repo_name):
403 """GET /repo_name/settings: Form to edit an existing item"""
402 """GET /repo_name/settings: Form to edit an existing item"""
404 # url('edit_repo', repo_name=ID)
403 # url('edit_repo', repo_name=ID)
405 c.repo_info = self._load_repo(repo_name)
404 c.repo_info = self._load_repo(repo_name)
406 c.repo_fields = RepositoryField.query() \
405 c.repo_fields = RepositoryField.query() \
407 .filter(RepositoryField.repository == c.repo_info).all()
406 .filter(RepositoryField.repository == c.repo_info).all()
408 c.active = 'fields'
407 c.active = 'fields'
409 if request.POST:
408 if request.POST:
410
409
411 raise HTTPFound(location=url('repo_edit_fields'))
410 raise HTTPFound(location=url('repo_edit_fields'))
412 return render('admin/repos/repo_edit.html')
411 return render('admin/repos/repo_edit.html')
413
412
414 @HasRepoPermissionAllDecorator('repository.admin')
413 @HasRepoPermissionAnyDecorator('repository.admin')
415 def create_repo_field(self, repo_name):
414 def create_repo_field(self, repo_name):
416 try:
415 try:
417 form_result = RepoFieldForm()().to_python(dict(request.POST))
416 form_result = RepoFieldForm()().to_python(dict(request.POST))
418 new_field = RepositoryField()
417 new_field = RepositoryField()
419 new_field.repository = Repository.get_by_repo_name(repo_name)
418 new_field.repository = Repository.get_by_repo_name(repo_name)
420 new_field.field_key = form_result['new_field_key']
419 new_field.field_key = form_result['new_field_key']
421 new_field.field_type = form_result['new_field_type'] # python type
420 new_field.field_type = form_result['new_field_type'] # python type
422 new_field.field_value = form_result['new_field_value'] # set initial blank value
421 new_field.field_value = form_result['new_field_value'] # set initial blank value
423 new_field.field_desc = form_result['new_field_desc']
422 new_field.field_desc = form_result['new_field_desc']
424 new_field.field_label = form_result['new_field_label']
423 new_field.field_label = form_result['new_field_label']
425 Session().add(new_field)
424 Session().add(new_field)
426 Session().commit()
425 Session().commit()
427 except Exception as e:
426 except Exception as e:
428 log.error(traceback.format_exc())
427 log.error(traceback.format_exc())
429 msg = _('An error occurred during creation of field')
428 msg = _('An error occurred during creation of field')
430 if isinstance(e, formencode.Invalid):
429 if isinstance(e, formencode.Invalid):
431 msg += ". " + e.msg
430 msg += ". " + e.msg
432 h.flash(msg, category='error')
431 h.flash(msg, category='error')
433 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
432 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
434
433
435 @HasRepoPermissionAllDecorator('repository.admin')
434 @HasRepoPermissionAnyDecorator('repository.admin')
436 def delete_repo_field(self, repo_name, field_id):
435 def delete_repo_field(self, repo_name, field_id):
437 field = RepositoryField.get_or_404(field_id)
436 field = RepositoryField.get_or_404(field_id)
438 try:
437 try:
439 Session().delete(field)
438 Session().delete(field)
440 Session().commit()
439 Session().commit()
441 except Exception as e:
440 except Exception as e:
442 log.error(traceback.format_exc())
441 log.error(traceback.format_exc())
443 msg = _('An error occurred during removal of field')
442 msg = _('An error occurred during removal of field')
444 h.flash(msg, category='error')
443 h.flash(msg, category='error')
445 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
444 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
446
445
447 @HasRepoPermissionAllDecorator('repository.admin')
446 @HasRepoPermissionAnyDecorator('repository.admin')
448 def edit_advanced(self, repo_name):
447 def edit_advanced(self, repo_name):
449 """GET /repo_name/settings: Form to edit an existing item"""
448 """GET /repo_name/settings: Form to edit an existing item"""
450 # url('edit_repo', repo_name=ID)
449 # url('edit_repo', repo_name=ID)
451 c.repo_info = self._load_repo(repo_name)
450 c.repo_info = self._load_repo(repo_name)
452 c.default_user_id = User.get_default_user().user_id
451 c.default_user_id = User.get_default_user().user_id
453 c.in_public_journal = UserFollowing.query() \
452 c.in_public_journal = UserFollowing.query() \
454 .filter(UserFollowing.user_id == c.default_user_id) \
453 .filter(UserFollowing.user_id == c.default_user_id) \
455 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
454 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
456
455
457 _repos = Repository.query().order_by(Repository.repo_name).all()
456 _repos = Repository.query().order_by(Repository.repo_name).all()
458 read_access_repos = RepoList(_repos)
457 read_access_repos = RepoList(_repos)
459 c.repos_list = [(None, _('-- Not a fork --'))]
458 c.repos_list = [(None, _('-- Not a fork --'))]
460 c.repos_list += [(x.repo_id, x.repo_name)
459 c.repos_list += [(x.repo_id, x.repo_name)
461 for x in read_access_repos
460 for x in read_access_repos
462 if x.repo_id != c.repo_info.repo_id]
461 if x.repo_id != c.repo_info.repo_id]
463
462
464 defaults = {
463 defaults = {
465 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
464 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
466 }
465 }
467
466
468 c.active = 'advanced'
467 c.active = 'advanced'
469 if request.POST:
468 if request.POST:
470 raise HTTPFound(location=url('repo_edit_advanced'))
469 raise HTTPFound(location=url('repo_edit_advanced'))
471 return htmlfill.render(
470 return htmlfill.render(
472 render('admin/repos/repo_edit.html'),
471 render('admin/repos/repo_edit.html'),
473 defaults=defaults,
472 defaults=defaults,
474 encoding="UTF-8",
473 encoding="UTF-8",
475 force_defaults=False)
474 force_defaults=False)
476
475
477 @HasRepoPermissionAllDecorator('repository.admin')
476 @HasRepoPermissionAnyDecorator('repository.admin')
478 def edit_advanced_journal(self, repo_name):
477 def edit_advanced_journal(self, repo_name):
479 """
478 """
480 Sets this repository to be visible in public journal,
479 Sets this repository to be visible in public journal,
481 in other words asking default user to follow this repo
480 in other words asking default user to follow this repo
482
481
483 :param repo_name:
482 :param repo_name:
484 """
483 """
485
484
486 try:
485 try:
487 repo_id = Repository.get_by_repo_name(repo_name).repo_id
486 repo_id = Repository.get_by_repo_name(repo_name).repo_id
488 user_id = User.get_default_user().user_id
487 user_id = User.get_default_user().user_id
489 self.scm_model.toggle_following_repo(repo_id, user_id)
488 self.scm_model.toggle_following_repo(repo_id, user_id)
490 h.flash(_('Updated repository visibility in public journal'),
489 h.flash(_('Updated repository visibility in public journal'),
491 category='success')
490 category='success')
492 Session().commit()
491 Session().commit()
493 except Exception:
492 except Exception:
494 h.flash(_('An error occurred during setting this'
493 h.flash(_('An error occurred during setting this'
495 ' repository in public journal'),
494 ' repository in public journal'),
496 category='error')
495 category='error')
497 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
496 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
498
497
499
498
500 @HasRepoPermissionAllDecorator('repository.admin')
499 @HasRepoPermissionAnyDecorator('repository.admin')
501 def edit_advanced_fork(self, repo_name):
500 def edit_advanced_fork(self, repo_name):
502 """
501 """
503 Mark given repository as a fork of another
502 Mark given repository as a fork of another
504
503
505 :param repo_name:
504 :param repo_name:
506 """
505 """
507 try:
506 try:
508 fork_id = request.POST.get('id_fork_of')
507 fork_id = request.POST.get('id_fork_of')
509 repo = ScmModel().mark_as_fork(repo_name, fork_id,
508 repo = ScmModel().mark_as_fork(repo_name, fork_id,
510 self.authuser.username)
509 self.authuser.username)
511 fork = repo.fork.repo_name if repo.fork else _('Nothing')
510 fork = repo.fork.repo_name if repo.fork else _('Nothing')
512 Session().commit()
511 Session().commit()
513 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
512 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
514 category='success')
513 category='success')
515 except RepositoryError as e:
514 except RepositoryError as e:
516 log.error(traceback.format_exc())
515 log.error(traceback.format_exc())
517 h.flash(str(e), category='error')
516 h.flash(str(e), category='error')
518 except Exception as e:
517 except Exception as e:
519 log.error(traceback.format_exc())
518 log.error(traceback.format_exc())
520 h.flash(_('An error occurred during this operation'),
519 h.flash(_('An error occurred during this operation'),
521 category='error')
520 category='error')
522
521
523 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
522 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
524
523
525 @HasRepoPermissionAllDecorator('repository.admin')
524 @HasRepoPermissionAnyDecorator('repository.admin')
526 def edit_advanced_locking(self, repo_name):
525 def edit_advanced_locking(self, repo_name):
527 """
526 """
528 Unlock repository when it is locked !
527 Unlock repository when it is locked !
529
528
530 :param repo_name:
529 :param repo_name:
531 """
530 """
532 try:
531 try:
533 repo = Repository.get_by_repo_name(repo_name)
532 repo = Repository.get_by_repo_name(repo_name)
534 if request.POST.get('set_lock'):
533 if request.POST.get('set_lock'):
535 Repository.lock(repo, c.authuser.user_id)
534 Repository.lock(repo, c.authuser.user_id)
536 h.flash(_('Repository has been locked'), category='success')
535 h.flash(_('Repository has been locked'), category='success')
537 elif request.POST.get('set_unlock'):
536 elif request.POST.get('set_unlock'):
538 Repository.unlock(repo)
537 Repository.unlock(repo)
539 h.flash(_('Repository has been unlocked'), category='success')
538 h.flash(_('Repository has been unlocked'), category='success')
540 except Exception as e:
539 except Exception as e:
541 log.error(traceback.format_exc())
540 log.error(traceback.format_exc())
542 h.flash(_('An error occurred during unlocking'),
541 h.flash(_('An error occurred during unlocking'),
543 category='error')
542 category='error')
544 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
543 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
545
544
546 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
545 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
547 def toggle_locking(self, repo_name):
546 def toggle_locking(self, repo_name):
548 """
547 """
549 Toggle locking of repository by simple GET call to url
548 Toggle locking of repository by simple GET call to url
550
549
551 :param repo_name:
550 :param repo_name:
552 """
551 """
553
552
554 try:
553 try:
555 repo = Repository.get_by_repo_name(repo_name)
554 repo = Repository.get_by_repo_name(repo_name)
556
555
557 if repo.enable_locking:
556 if repo.enable_locking:
558 if repo.locked[0]:
557 if repo.locked[0]:
559 Repository.unlock(repo)
558 Repository.unlock(repo)
560 h.flash(_('Repository has been unlocked'), category='success')
559 h.flash(_('Repository has been unlocked'), category='success')
561 else:
560 else:
562 Repository.lock(repo, c.authuser.user_id)
561 Repository.lock(repo, c.authuser.user_id)
563 h.flash(_('Repository has been locked'), category='success')
562 h.flash(_('Repository has been locked'), category='success')
564
563
565 except Exception as e:
564 except Exception as e:
566 log.error(traceback.format_exc())
565 log.error(traceback.format_exc())
567 h.flash(_('An error occurred during unlocking'),
566 h.flash(_('An error occurred during unlocking'),
568 category='error')
567 category='error')
569 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
568 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
570
569
571 @HasRepoPermissionAllDecorator('repository.admin')
570 @HasRepoPermissionAnyDecorator('repository.admin')
572 def edit_caches(self, repo_name):
571 def edit_caches(self, repo_name):
573 """GET /repo_name/settings: Form to edit an existing item"""
572 """GET /repo_name/settings: Form to edit an existing item"""
574 # url('edit_repo', repo_name=ID)
573 # url('edit_repo', repo_name=ID)
575 c.repo_info = self._load_repo(repo_name)
574 c.repo_info = self._load_repo(repo_name)
576 c.active = 'caches'
575 c.active = 'caches'
577 if request.POST:
576 if request.POST:
578 try:
577 try:
579 ScmModel().mark_for_invalidation(repo_name)
578 ScmModel().mark_for_invalidation(repo_name)
580 Session().commit()
579 Session().commit()
581 h.flash(_('Cache invalidation successful'),
580 h.flash(_('Cache invalidation successful'),
582 category='success')
581 category='success')
583 except Exception as e:
582 except Exception as e:
584 log.error(traceback.format_exc())
583 log.error(traceback.format_exc())
585 h.flash(_('An error occurred during cache invalidation'),
584 h.flash(_('An error occurred during cache invalidation'),
586 category='error')
585 category='error')
587
586
588 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
587 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
589 return render('admin/repos/repo_edit.html')
588 return render('admin/repos/repo_edit.html')
590
589
591 @HasRepoPermissionAllDecorator('repository.admin')
590 @HasRepoPermissionAnyDecorator('repository.admin')
592 def edit_remote(self, repo_name):
591 def edit_remote(self, repo_name):
593 """GET /repo_name/settings: Form to edit an existing item"""
592 """GET /repo_name/settings: Form to edit an existing item"""
594 # url('edit_repo', repo_name=ID)
593 # url('edit_repo', repo_name=ID)
595 c.repo_info = self._load_repo(repo_name)
594 c.repo_info = self._load_repo(repo_name)
596 c.active = 'remote'
595 c.active = 'remote'
597 if request.POST:
596 if request.POST:
598 try:
597 try:
599 ScmModel().pull_changes(repo_name, self.authuser.username)
598 ScmModel().pull_changes(repo_name, self.authuser.username)
600 h.flash(_('Pulled from remote location'), category='success')
599 h.flash(_('Pulled from remote location'), category='success')
601 except Exception as e:
600 except Exception as e:
602 log.error(traceback.format_exc())
601 log.error(traceback.format_exc())
603 h.flash(_('An error occurred during pull from remote location'),
602 h.flash(_('An error occurred during pull from remote location'),
604 category='error')
603 category='error')
605 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
604 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
606 return render('admin/repos/repo_edit.html')
605 return render('admin/repos/repo_edit.html')
607
606
608 @HasRepoPermissionAllDecorator('repository.admin')
607 @HasRepoPermissionAnyDecorator('repository.admin')
609 def edit_statistics(self, repo_name):
608 def edit_statistics(self, repo_name):
610 """GET /repo_name/settings: Form to edit an existing item"""
609 """GET /repo_name/settings: Form to edit an existing item"""
611 # url('edit_repo', repo_name=ID)
610 # url('edit_repo', repo_name=ID)
612 c.repo_info = self._load_repo(repo_name)
611 c.repo_info = self._load_repo(repo_name)
613 repo = c.repo_info.scm_instance
612 repo = c.repo_info.scm_instance
614
613
615 if c.repo_info.stats:
614 if c.repo_info.stats:
616 # this is on what revision we ended up so we add +1 for count
615 # this is on what revision we ended up so we add +1 for count
617 last_rev = c.repo_info.stats.stat_on_revision + 1
616 last_rev = c.repo_info.stats.stat_on_revision + 1
618 else:
617 else:
619 last_rev = 0
618 last_rev = 0
620 c.stats_revision = last_rev
619 c.stats_revision = last_rev
621
620
622 c.repo_last_rev = repo.count() if repo.revisions else 0
621 c.repo_last_rev = repo.count() if repo.revisions else 0
623
622
624 if last_rev == 0 or c.repo_last_rev == 0:
623 if last_rev == 0 or c.repo_last_rev == 0:
625 c.stats_percentage = 0
624 c.stats_percentage = 0
626 else:
625 else:
627 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
626 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
628
627
629 c.active = 'statistics'
628 c.active = 'statistics'
630 if request.POST:
629 if request.POST:
631 try:
630 try:
632 RepoModel().delete_stats(repo_name)
631 RepoModel().delete_stats(repo_name)
633 Session().commit()
632 Session().commit()
634 except Exception as e:
633 except Exception as e:
635 log.error(traceback.format_exc())
634 log.error(traceback.format_exc())
636 h.flash(_('An error occurred during deletion of repository stats'),
635 h.flash(_('An error occurred during deletion of repository stats'),
637 category='error')
636 category='error')
638 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
637 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
639
638
640 return render('admin/repos/repo_edit.html')
639 return render('admin/repos/repo_edit.html')
@@ -1,499 +1,499 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.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
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, HasPermissionAnyDecorator
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 settings = {}
67 settings = {}
68 for each in ret:
68 for each in ret:
69 k = each.ui_section + '_' + each.ui_key
69 k = each.ui_section + '_' + each.ui_key
70 v = each.ui_value
70 v = each.ui_value
71 if k == 'paths_/':
71 if k == 'paths_/':
72 k = 'paths_root_path'
72 k = 'paths_root_path'
73
73
74 if k == 'web_push_ssl':
74 if k == 'web_push_ssl':
75 v = str2bool(v)
75 v = str2bool(v)
76
76
77 k = k.replace('.', '_')
77 k = k.replace('.', '_')
78
78
79 if each.ui_section in ['hooks', 'extensions']:
79 if each.ui_section in ['hooks', 'extensions']:
80 v = each.ui_active
80 v = each.ui_active
81
81
82 settings[k] = v
82 settings[k] = v
83 return settings
83 return settings
84
84
85 @HasPermissionAllDecorator('hg.admin')
85 @HasPermissionAnyDecorator('hg.admin')
86 def settings_vcs(self):
86 def settings_vcs(self):
87 """GET /admin/settings: All items in the collection"""
87 """GET /admin/settings: All items in the collection"""
88 # url('admin_settings')
88 # url('admin_settings')
89 c.active = 'vcs'
89 c.active = 'vcs'
90 if request.POST:
90 if request.POST:
91 application_form = ApplicationUiSettingsForm()()
91 application_form = ApplicationUiSettingsForm()()
92 try:
92 try:
93 form_result = application_form.to_python(dict(request.POST))
93 form_result = application_form.to_python(dict(request.POST))
94 except formencode.Invalid as errors:
94 except formencode.Invalid as errors:
95 return htmlfill.render(
95 return htmlfill.render(
96 render('admin/settings/settings.html'),
96 render('admin/settings/settings.html'),
97 defaults=errors.value,
97 defaults=errors.value,
98 errors=errors.error_dict or {},
98 errors=errors.error_dict or {},
99 prefix_error=False,
99 prefix_error=False,
100 encoding="UTF-8",
100 encoding="UTF-8",
101 force_defaults=False)
101 force_defaults=False)
102
102
103 try:
103 try:
104 sett = Ui.get_by_key('web', 'push_ssl')
104 sett = Ui.get_by_key('web', 'push_ssl')
105 sett.ui_value = form_result['web_push_ssl']
105 sett.ui_value = form_result['web_push_ssl']
106
106
107 if c.visual.allow_repo_location_change:
107 if c.visual.allow_repo_location_change:
108 sett = Ui.get_by_key('paths', '/')
108 sett = Ui.get_by_key('paths', '/')
109 sett.ui_value = form_result['paths_root_path']
109 sett.ui_value = form_result['paths_root_path']
110
110
111 #HOOKS
111 #HOOKS
112 sett = Ui.get_by_key('hooks', Ui.HOOK_UPDATE)
112 sett = Ui.get_by_key('hooks', Ui.HOOK_UPDATE)
113 sett.ui_active = form_result['hooks_changegroup_update']
113 sett.ui_active = form_result['hooks_changegroup_update']
114
114
115 sett = Ui.get_by_key('hooks', Ui.HOOK_REPO_SIZE)
115 sett = Ui.get_by_key('hooks', Ui.HOOK_REPO_SIZE)
116 sett.ui_active = form_result['hooks_changegroup_repo_size']
116 sett.ui_active = form_result['hooks_changegroup_repo_size']
117
117
118 sett = Ui.get_by_key('hooks', Ui.HOOK_PUSH)
118 sett = Ui.get_by_key('hooks', Ui.HOOK_PUSH)
119 sett.ui_active = form_result['hooks_changegroup_push_logger']
119 sett.ui_active = form_result['hooks_changegroup_push_logger']
120
120
121 sett = Ui.get_by_key('hooks', Ui.HOOK_PULL)
121 sett = Ui.get_by_key('hooks', Ui.HOOK_PULL)
122 sett.ui_active = form_result['hooks_outgoing_pull_logger']
122 sett.ui_active = form_result['hooks_outgoing_pull_logger']
123
123
124 ## EXTENSIONS
124 ## EXTENSIONS
125 sett = Ui.get_or_create('extensions', 'largefiles')
125 sett = Ui.get_or_create('extensions', 'largefiles')
126 sett.ui_active = form_result['extensions_largefiles']
126 sett.ui_active = form_result['extensions_largefiles']
127
127
128 sett = Ui.get_or_create('extensions', 'hgsubversion')
128 sett = Ui.get_or_create('extensions', 'hgsubversion')
129 sett.ui_active = form_result['extensions_hgsubversion']
129 sett.ui_active = form_result['extensions_hgsubversion']
130 if sett.ui_active:
130 if sett.ui_active:
131 try:
131 try:
132 import hgsubversion # pragma: no cover
132 import hgsubversion # pragma: no cover
133 except ImportError:
133 except ImportError:
134 raise HgsubversionImportError
134 raise HgsubversionImportError
135
135
136 # sett = Ui.get_or_create('extensions', 'hggit')
136 # sett = Ui.get_or_create('extensions', 'hggit')
137 # sett.ui_active = form_result['extensions_hggit']
137 # sett.ui_active = form_result['extensions_hggit']
138
138
139 Session().commit()
139 Session().commit()
140
140
141 h.flash(_('Updated VCS settings'), category='success')
141 h.flash(_('Updated VCS settings'), category='success')
142
142
143 except HgsubversionImportError:
143 except HgsubversionImportError:
144 log.error(traceback.format_exc())
144 log.error(traceback.format_exc())
145 h.flash(_('Unable to activate hgsubversion support. '
145 h.flash(_('Unable to activate hgsubversion support. '
146 'The "hgsubversion" library is missing'),
146 'The "hgsubversion" library is missing'),
147 category='error')
147 category='error')
148
148
149 except Exception:
149 except Exception:
150 log.error(traceback.format_exc())
150 log.error(traceback.format_exc())
151 h.flash(_('Error occurred while updating '
151 h.flash(_('Error occurred while updating '
152 'application settings'), category='error')
152 'application settings'), category='error')
153
153
154 defaults = Setting.get_app_settings()
154 defaults = Setting.get_app_settings()
155 defaults.update(self._get_hg_ui_settings())
155 defaults.update(self._get_hg_ui_settings())
156
156
157 return htmlfill.render(
157 return htmlfill.render(
158 render('admin/settings/settings.html'),
158 render('admin/settings/settings.html'),
159 defaults=defaults,
159 defaults=defaults,
160 encoding="UTF-8",
160 encoding="UTF-8",
161 force_defaults=False)
161 force_defaults=False)
162
162
163 @HasPermissionAllDecorator('hg.admin')
163 @HasPermissionAnyDecorator('hg.admin')
164 def settings_mapping(self):
164 def settings_mapping(self):
165 """GET /admin/settings/mapping: All items in the collection"""
165 """GET /admin/settings/mapping: All items in the collection"""
166 # url('admin_settings_mapping')
166 # url('admin_settings_mapping')
167 c.active = 'mapping'
167 c.active = 'mapping'
168 if request.POST:
168 if request.POST:
169 rm_obsolete = request.POST.get('destroy', False)
169 rm_obsolete = request.POST.get('destroy', False)
170 install_git_hooks = request.POST.get('hooks', False)
170 install_git_hooks = request.POST.get('hooks', False)
171 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
171 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
172 invalidate_cache = request.POST.get('invalidate', False)
172 invalidate_cache = request.POST.get('invalidate', False)
173 log.debug('rescanning repo location with destroy obsolete=%s, '
173 log.debug('rescanning repo location with destroy obsolete=%s, '
174 'install git hooks=%s and '
174 'install git hooks=%s and '
175 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
175 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
176
176
177 if invalidate_cache:
177 if invalidate_cache:
178 log.debug('invalidating all repositories cache')
178 log.debug('invalidating all repositories cache')
179 for repo in Repository.get_all():
179 for repo in Repository.get_all():
180 ScmModel().mark_for_invalidation(repo.repo_name)
180 ScmModel().mark_for_invalidation(repo.repo_name)
181
181
182 filesystem_repos = ScmModel().repo_scan()
182 filesystem_repos = ScmModel().repo_scan()
183 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
183 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
184 install_git_hooks=install_git_hooks,
184 install_git_hooks=install_git_hooks,
185 user=c.authuser.username,
185 user=c.authuser.username,
186 overwrite_git_hooks=overwrite_git_hooks)
186 overwrite_git_hooks=overwrite_git_hooks)
187 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
187 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
188 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
188 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
189 for repo_name in added) or '-',
189 for repo_name in added) or '-',
190 ', '.join(h.escape(safe_unicode(repo_name)) for repo_name in removed) or '-')),
190 ', '.join(h.escape(safe_unicode(repo_name)) for repo_name in removed) or '-')),
191 category='success')
191 category='success')
192 raise HTTPFound(location=url('admin_settings_mapping'))
192 raise HTTPFound(location=url('admin_settings_mapping'))
193
193
194 defaults = Setting.get_app_settings()
194 defaults = Setting.get_app_settings()
195 defaults.update(self._get_hg_ui_settings())
195 defaults.update(self._get_hg_ui_settings())
196
196
197 return htmlfill.render(
197 return htmlfill.render(
198 render('admin/settings/settings.html'),
198 render('admin/settings/settings.html'),
199 defaults=defaults,
199 defaults=defaults,
200 encoding="UTF-8",
200 encoding="UTF-8",
201 force_defaults=False)
201 force_defaults=False)
202
202
203 @HasPermissionAllDecorator('hg.admin')
203 @HasPermissionAnyDecorator('hg.admin')
204 def settings_global(self):
204 def settings_global(self):
205 """GET /admin/settings/global: All items in the collection"""
205 """GET /admin/settings/global: All items in the collection"""
206 # url('admin_settings_global')
206 # url('admin_settings_global')
207 c.active = 'global'
207 c.active = 'global'
208 if request.POST:
208 if request.POST:
209 application_form = ApplicationSettingsForm()()
209 application_form = ApplicationSettingsForm()()
210 try:
210 try:
211 form_result = application_form.to_python(dict(request.POST))
211 form_result = application_form.to_python(dict(request.POST))
212 except formencode.Invalid as errors:
212 except formencode.Invalid as errors:
213 return htmlfill.render(
213 return htmlfill.render(
214 render('admin/settings/settings.html'),
214 render('admin/settings/settings.html'),
215 defaults=errors.value,
215 defaults=errors.value,
216 errors=errors.error_dict or {},
216 errors=errors.error_dict or {},
217 prefix_error=False,
217 prefix_error=False,
218 encoding="UTF-8",
218 encoding="UTF-8",
219 force_defaults=False)
219 force_defaults=False)
220
220
221 try:
221 try:
222 sett1 = Setting.create_or_update('title',
222 sett1 = Setting.create_or_update('title',
223 form_result['title'])
223 form_result['title'])
224 Session().add(sett1)
224 Session().add(sett1)
225
225
226 sett2 = Setting.create_or_update('realm',
226 sett2 = Setting.create_or_update('realm',
227 form_result['realm'])
227 form_result['realm'])
228 Session().add(sett2)
228 Session().add(sett2)
229
229
230 sett3 = Setting.create_or_update('ga_code',
230 sett3 = Setting.create_or_update('ga_code',
231 form_result['ga_code'])
231 form_result['ga_code'])
232 Session().add(sett3)
232 Session().add(sett3)
233
233
234 sett4 = Setting.create_or_update('captcha_public_key',
234 sett4 = Setting.create_or_update('captcha_public_key',
235 form_result['captcha_public_key'])
235 form_result['captcha_public_key'])
236 Session().add(sett4)
236 Session().add(sett4)
237
237
238 sett5 = Setting.create_or_update('captcha_private_key',
238 sett5 = Setting.create_or_update('captcha_private_key',
239 form_result['captcha_private_key'])
239 form_result['captcha_private_key'])
240 Session().add(sett5)
240 Session().add(sett5)
241
241
242 Session().commit()
242 Session().commit()
243 set_app_settings(config)
243 set_app_settings(config)
244 h.flash(_('Updated application settings'), category='success')
244 h.flash(_('Updated application settings'), category='success')
245
245
246 except Exception:
246 except Exception:
247 log.error(traceback.format_exc())
247 log.error(traceback.format_exc())
248 h.flash(_('Error occurred while updating '
248 h.flash(_('Error occurred while updating '
249 'application settings'),
249 'application settings'),
250 category='error')
250 category='error')
251
251
252 raise HTTPFound(location=url('admin_settings_global'))
252 raise HTTPFound(location=url('admin_settings_global'))
253
253
254 defaults = Setting.get_app_settings()
254 defaults = Setting.get_app_settings()
255 defaults.update(self._get_hg_ui_settings())
255 defaults.update(self._get_hg_ui_settings())
256
256
257 return htmlfill.render(
257 return htmlfill.render(
258 render('admin/settings/settings.html'),
258 render('admin/settings/settings.html'),
259 defaults=defaults,
259 defaults=defaults,
260 encoding="UTF-8",
260 encoding="UTF-8",
261 force_defaults=False)
261 force_defaults=False)
262
262
263 @HasPermissionAllDecorator('hg.admin')
263 @HasPermissionAnyDecorator('hg.admin')
264 def settings_visual(self):
264 def settings_visual(self):
265 """GET /admin/settings/visual: All items in the collection"""
265 """GET /admin/settings/visual: All items in the collection"""
266 # url('admin_settings_visual')
266 # url('admin_settings_visual')
267 c.active = 'visual'
267 c.active = 'visual'
268 if request.POST:
268 if request.POST:
269 application_form = ApplicationVisualisationForm()()
269 application_form = ApplicationVisualisationForm()()
270 try:
270 try:
271 form_result = application_form.to_python(dict(request.POST))
271 form_result = application_form.to_python(dict(request.POST))
272 except formencode.Invalid as errors:
272 except formencode.Invalid as errors:
273 return htmlfill.render(
273 return htmlfill.render(
274 render('admin/settings/settings.html'),
274 render('admin/settings/settings.html'),
275 defaults=errors.value,
275 defaults=errors.value,
276 errors=errors.error_dict or {},
276 errors=errors.error_dict or {},
277 prefix_error=False,
277 prefix_error=False,
278 encoding="UTF-8",
278 encoding="UTF-8",
279 force_defaults=False)
279 force_defaults=False)
280
280
281 try:
281 try:
282 settings = [
282 settings = [
283 ('show_public_icon', 'show_public_icon', 'bool'),
283 ('show_public_icon', 'show_public_icon', 'bool'),
284 ('show_private_icon', 'show_private_icon', 'bool'),
284 ('show_private_icon', 'show_private_icon', 'bool'),
285 ('stylify_metatags', 'stylify_metatags', 'bool'),
285 ('stylify_metatags', 'stylify_metatags', 'bool'),
286 ('repository_fields', 'repository_fields', 'bool'),
286 ('repository_fields', 'repository_fields', 'bool'),
287 ('dashboard_items', 'dashboard_items', 'int'),
287 ('dashboard_items', 'dashboard_items', 'int'),
288 ('admin_grid_items', 'admin_grid_items', 'int'),
288 ('admin_grid_items', 'admin_grid_items', 'int'),
289 ('show_version', 'show_version', 'bool'),
289 ('show_version', 'show_version', 'bool'),
290 ('use_gravatar', 'use_gravatar', 'bool'),
290 ('use_gravatar', 'use_gravatar', 'bool'),
291 ('gravatar_url', 'gravatar_url', 'unicode'),
291 ('gravatar_url', 'gravatar_url', 'unicode'),
292 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
292 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
293 ]
293 ]
294 for setting, form_key, type_ in settings:
294 for setting, form_key, type_ in settings:
295 sett = Setting.create_or_update(setting,
295 sett = Setting.create_or_update(setting,
296 form_result[form_key], type_)
296 form_result[form_key], type_)
297 Session().add(sett)
297 Session().add(sett)
298
298
299 Session().commit()
299 Session().commit()
300 set_app_settings(config)
300 set_app_settings(config)
301 h.flash(_('Updated visualisation settings'),
301 h.flash(_('Updated visualisation settings'),
302 category='success')
302 category='success')
303
303
304 except Exception:
304 except Exception:
305 log.error(traceback.format_exc())
305 log.error(traceback.format_exc())
306 h.flash(_('Error occurred during updating '
306 h.flash(_('Error occurred during updating '
307 'visualisation settings'),
307 'visualisation settings'),
308 category='error')
308 category='error')
309
309
310 raise HTTPFound(location=url('admin_settings_visual'))
310 raise HTTPFound(location=url('admin_settings_visual'))
311
311
312 defaults = Setting.get_app_settings()
312 defaults = Setting.get_app_settings()
313 defaults.update(self._get_hg_ui_settings())
313 defaults.update(self._get_hg_ui_settings())
314
314
315 return htmlfill.render(
315 return htmlfill.render(
316 render('admin/settings/settings.html'),
316 render('admin/settings/settings.html'),
317 defaults=defaults,
317 defaults=defaults,
318 encoding="UTF-8",
318 encoding="UTF-8",
319 force_defaults=False)
319 force_defaults=False)
320
320
321 @HasPermissionAllDecorator('hg.admin')
321 @HasPermissionAnyDecorator('hg.admin')
322 def settings_email(self):
322 def settings_email(self):
323 """GET /admin/settings/email: All items in the collection"""
323 """GET /admin/settings/email: All items in the collection"""
324 # url('admin_settings_email')
324 # url('admin_settings_email')
325 c.active = 'email'
325 c.active = 'email'
326 if request.POST:
326 if request.POST:
327 test_email = request.POST.get('test_email')
327 test_email = request.POST.get('test_email')
328 test_email_subj = 'Kallithea test email'
328 test_email_subj = 'Kallithea test email'
329 test_body = ('Kallithea Email test, '
329 test_body = ('Kallithea Email test, '
330 'Kallithea version: %s' % c.kallithea_version)
330 'Kallithea version: %s' % c.kallithea_version)
331 if not test_email:
331 if not test_email:
332 h.flash(_('Please enter email address'), category='error')
332 h.flash(_('Please enter email address'), category='error')
333 raise HTTPFound(location=url('admin_settings_email'))
333 raise HTTPFound(location=url('admin_settings_email'))
334
334
335 test_email_txt_body = EmailNotificationModel() \
335 test_email_txt_body = EmailNotificationModel() \
336 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
336 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
337 'txt', body=test_body)
337 'txt', body=test_body)
338 test_email_html_body = EmailNotificationModel() \
338 test_email_html_body = EmailNotificationModel() \
339 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
339 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
340 'html', body=test_body)
340 'html', body=test_body)
341
341
342 recipients = [test_email] if test_email else None
342 recipients = [test_email] if test_email else None
343
343
344 run_task(tasks.send_email, recipients, test_email_subj,
344 run_task(tasks.send_email, recipients, test_email_subj,
345 test_email_txt_body, test_email_html_body)
345 test_email_txt_body, test_email_html_body)
346
346
347 h.flash(_('Send email task created'), category='success')
347 h.flash(_('Send email task created'), category='success')
348 raise HTTPFound(location=url('admin_settings_email'))
348 raise HTTPFound(location=url('admin_settings_email'))
349
349
350 defaults = Setting.get_app_settings()
350 defaults = Setting.get_app_settings()
351 defaults.update(self._get_hg_ui_settings())
351 defaults.update(self._get_hg_ui_settings())
352
352
353 import kallithea
353 import kallithea
354 c.ini = kallithea.CONFIG
354 c.ini = kallithea.CONFIG
355
355
356 return htmlfill.render(
356 return htmlfill.render(
357 render('admin/settings/settings.html'),
357 render('admin/settings/settings.html'),
358 defaults=defaults,
358 defaults=defaults,
359 encoding="UTF-8",
359 encoding="UTF-8",
360 force_defaults=False)
360 force_defaults=False)
361
361
362 @HasPermissionAllDecorator('hg.admin')
362 @HasPermissionAnyDecorator('hg.admin')
363 def settings_hooks(self):
363 def settings_hooks(self):
364 """GET /admin/settings/hooks: All items in the collection"""
364 """GET /admin/settings/hooks: All items in the collection"""
365 # url('admin_settings_hooks')
365 # url('admin_settings_hooks')
366 c.active = 'hooks'
366 c.active = 'hooks'
367 if request.POST:
367 if request.POST:
368 if c.visual.allow_custom_hooks_settings:
368 if c.visual.allow_custom_hooks_settings:
369 ui_key = request.POST.get('new_hook_ui_key')
369 ui_key = request.POST.get('new_hook_ui_key')
370 ui_value = request.POST.get('new_hook_ui_value')
370 ui_value = request.POST.get('new_hook_ui_value')
371
371
372 hook_id = request.POST.get('hook_id')
372 hook_id = request.POST.get('hook_id')
373
373
374 try:
374 try:
375 ui_key = ui_key and ui_key.strip()
375 ui_key = ui_key and ui_key.strip()
376 if ui_value and ui_key:
376 if ui_value and ui_key:
377 Ui.create_or_update_hook(ui_key, ui_value)
377 Ui.create_or_update_hook(ui_key, ui_value)
378 h.flash(_('Added new hook'), category='success')
378 h.flash(_('Added new hook'), category='success')
379 elif hook_id:
379 elif hook_id:
380 Ui.delete(hook_id)
380 Ui.delete(hook_id)
381 Session().commit()
381 Session().commit()
382
382
383 # check for edits
383 # check for edits
384 update = False
384 update = False
385 _d = request.POST.dict_of_lists()
385 _d = request.POST.dict_of_lists()
386 for k, v in zip(_d.get('hook_ui_key', []),
386 for k, v in zip(_d.get('hook_ui_key', []),
387 _d.get('hook_ui_value_new', [])):
387 _d.get('hook_ui_value_new', [])):
388 Ui.create_or_update_hook(k, v)
388 Ui.create_or_update_hook(k, v)
389 update = True
389 update = True
390
390
391 if update:
391 if update:
392 h.flash(_('Updated hooks'), category='success')
392 h.flash(_('Updated hooks'), category='success')
393 Session().commit()
393 Session().commit()
394 except Exception:
394 except Exception:
395 log.error(traceback.format_exc())
395 log.error(traceback.format_exc())
396 h.flash(_('Error occurred during hook creation'),
396 h.flash(_('Error occurred during hook creation'),
397 category='error')
397 category='error')
398
398
399 raise HTTPFound(location=url('admin_settings_hooks'))
399 raise HTTPFound(location=url('admin_settings_hooks'))
400
400
401 defaults = Setting.get_app_settings()
401 defaults = Setting.get_app_settings()
402 defaults.update(self._get_hg_ui_settings())
402 defaults.update(self._get_hg_ui_settings())
403
403
404 c.hooks = Ui.get_builtin_hooks()
404 c.hooks = Ui.get_builtin_hooks()
405 c.custom_hooks = Ui.get_custom_hooks()
405 c.custom_hooks = Ui.get_custom_hooks()
406
406
407 return htmlfill.render(
407 return htmlfill.render(
408 render('admin/settings/settings.html'),
408 render('admin/settings/settings.html'),
409 defaults=defaults,
409 defaults=defaults,
410 encoding="UTF-8",
410 encoding="UTF-8",
411 force_defaults=False)
411 force_defaults=False)
412
412
413 @HasPermissionAllDecorator('hg.admin')
413 @HasPermissionAnyDecorator('hg.admin')
414 def settings_search(self):
414 def settings_search(self):
415 """GET /admin/settings/search: All items in the collection"""
415 """GET /admin/settings/search: All items in the collection"""
416 # url('admin_settings_search')
416 # url('admin_settings_search')
417 c.active = 'search'
417 c.active = 'search'
418 if request.POST:
418 if request.POST:
419 repo_location = self._get_hg_ui_settings()['paths_root_path']
419 repo_location = self._get_hg_ui_settings()['paths_root_path']
420 full_index = request.POST.get('full_index', False)
420 full_index = request.POST.get('full_index', False)
421 run_task(tasks.whoosh_index, repo_location, full_index)
421 run_task(tasks.whoosh_index, repo_location, full_index)
422 h.flash(_('Whoosh reindex task scheduled'), category='success')
422 h.flash(_('Whoosh reindex task scheduled'), category='success')
423 raise HTTPFound(location=url('admin_settings_search'))
423 raise HTTPFound(location=url('admin_settings_search'))
424
424
425 defaults = Setting.get_app_settings()
425 defaults = Setting.get_app_settings()
426 defaults.update(self._get_hg_ui_settings())
426 defaults.update(self._get_hg_ui_settings())
427
427
428 return htmlfill.render(
428 return htmlfill.render(
429 render('admin/settings/settings.html'),
429 render('admin/settings/settings.html'),
430 defaults=defaults,
430 defaults=defaults,
431 encoding="UTF-8",
431 encoding="UTF-8",
432 force_defaults=False)
432 force_defaults=False)
433
433
434 @HasPermissionAllDecorator('hg.admin')
434 @HasPermissionAnyDecorator('hg.admin')
435 def settings_system(self):
435 def settings_system(self):
436 """GET /admin/settings/system: All items in the collection"""
436 """GET /admin/settings/system: All items in the collection"""
437 # url('admin_settings_system')
437 # url('admin_settings_system')
438 c.active = 'system'
438 c.active = 'system'
439
439
440 defaults = Setting.get_app_settings()
440 defaults = Setting.get_app_settings()
441 defaults.update(self._get_hg_ui_settings())
441 defaults.update(self._get_hg_ui_settings())
442
442
443 import kallithea
443 import kallithea
444 c.ini = kallithea.CONFIG
444 c.ini = kallithea.CONFIG
445 c.update_url = defaults.get('update_url')
445 c.update_url = defaults.get('update_url')
446 server_info = Setting.get_server_info()
446 server_info = Setting.get_server_info()
447 for key, val in server_info.iteritems():
447 for key, val in server_info.iteritems():
448 setattr(c, key, val)
448 setattr(c, key, val)
449
449
450 return htmlfill.render(
450 return htmlfill.render(
451 render('admin/settings/settings.html'),
451 render('admin/settings/settings.html'),
452 defaults=defaults,
452 defaults=defaults,
453 encoding="UTF-8",
453 encoding="UTF-8",
454 force_defaults=False)
454 force_defaults=False)
455
455
456 @HasPermissionAllDecorator('hg.admin')
456 @HasPermissionAnyDecorator('hg.admin')
457 def settings_system_update(self):
457 def settings_system_update(self):
458 """GET /admin/settings/system/updates: All items in the collection"""
458 """GET /admin/settings/system/updates: All items in the collection"""
459 # url('admin_settings_system_update')
459 # url('admin_settings_system_update')
460 import json
460 import json
461 import urllib2
461 import urllib2
462 from kallithea.lib.verlib import NormalizedVersion
462 from kallithea.lib.verlib import NormalizedVersion
463 from kallithea import __version__
463 from kallithea import __version__
464
464
465 defaults = Setting.get_app_settings()
465 defaults = Setting.get_app_settings()
466 defaults.update(self._get_hg_ui_settings())
466 defaults.update(self._get_hg_ui_settings())
467 _update_url = defaults.get('update_url', '')
467 _update_url = defaults.get('update_url', '')
468 _update_url = "" # FIXME: disabled
468 _update_url = "" # FIXME: disabled
469
469
470 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
470 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
471 try:
471 try:
472 import kallithea
472 import kallithea
473 ver = kallithea.__version__
473 ver = kallithea.__version__
474 log.debug('Checking for upgrade on `%s` server', _update_url)
474 log.debug('Checking for upgrade on `%s` server', _update_url)
475 opener = urllib2.build_opener()
475 opener = urllib2.build_opener()
476 opener.addheaders = [('User-agent', 'Kallithea-SCM/%s' % ver)]
476 opener.addheaders = [('User-agent', 'Kallithea-SCM/%s' % ver)]
477 response = opener.open(_update_url)
477 response = opener.open(_update_url)
478 response_data = response.read()
478 response_data = response.read()
479 data = json.loads(response_data)
479 data = json.loads(response_data)
480 except urllib2.URLError as e:
480 except urllib2.URLError as e:
481 log.error(traceback.format_exc())
481 log.error(traceback.format_exc())
482 return _err('Failed to contact upgrade server: %r' % e)
482 return _err('Failed to contact upgrade server: %r' % e)
483 except ValueError as e:
483 except ValueError as e:
484 log.error(traceback.format_exc())
484 log.error(traceback.format_exc())
485 return _err('Bad data sent from update server')
485 return _err('Bad data sent from update server')
486
486
487 latest = data['versions'][0]
487 latest = data['versions'][0]
488
488
489 c.update_url = _update_url
489 c.update_url = _update_url
490 c.latest_data = latest
490 c.latest_data = latest
491 c.latest_ver = latest['version']
491 c.latest_ver = latest['version']
492 c.cur_ver = __version__
492 c.cur_ver = __version__
493 c.should_upgrade = False
493 c.should_upgrade = False
494
494
495 if NormalizedVersion(c.latest_ver) > NormalizedVersion(c.cur_ver):
495 if NormalizedVersion(c.latest_ver) > NormalizedVersion(c.cur_ver):
496 c.should_upgrade = True
496 c.should_upgrade = True
497 c.important_notices = latest['general']
497 c.important_notices = latest['general']
498
498
499 return render('admin/settings/settings_system_update.html'),
499 return render('admin/settings/settings_system_update.html'),
@@ -1,486 +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.i18n.translation import _
34 from pylons.i18n.translation import _
35 from sqlalchemy.sql.expression import func
35 from sqlalchemy.sql.expression import func
36 from webob.exc import HTTPFound, HTTPNotFound
36 from webob.exc import HTTPFound, HTTPNotFound
37
37
38 import kallithea
38 import kallithea
39 from kallithea.lib.exceptions import DefaultUserException, \
39 from kallithea.lib.exceptions import DefaultUserException, \
40 UserOwnsReposException, UserCreationError
40 UserOwnsReposException, UserCreationError
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
42 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator, \
43 AuthUser
43 AuthUser
44 from kallithea.lib import auth_modules
44 from kallithea.lib import auth_modules
45 from kallithea.lib.auth_modules import auth_internal
45 from kallithea.lib.auth_modules import auth_internal
46 from kallithea.lib.base import BaseController, render
46 from kallithea.lib.base import BaseController, render
47 from kallithea.model.api_key import ApiKeyModel
47 from kallithea.model.api_key import ApiKeyModel
48
48
49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
51 from kallithea.model.user import UserModel
51 from kallithea.model.user import UserModel
52 from kallithea.model.meta import Session
52 from kallithea.model.meta import Session
53 from kallithea.lib.utils import action_logger
53 from kallithea.lib.utils import action_logger
54 from kallithea.lib.compat import json
54 from kallithea.lib.compat import json
55 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
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class UsersController(BaseController):
60 class UsersController(BaseController):
61 """REST Controller styled on the Atom Publishing Protocol"""
61 """REST Controller styled on the Atom Publishing Protocol"""
62
62
63 @LoginRequired()
63 @LoginRequired()
64 @HasPermissionAllDecorator('hg.admin')
64 @HasPermissionAnyDecorator('hg.admin')
65 def __before__(self):
65 def __before__(self):
66 super(UsersController, self).__before__()
66 super(UsersController, self).__before__()
67 c.available_permissions = config['available_permissions']
67 c.available_permissions = config['available_permissions']
68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
69
69
70 def index(self, format='html'):
70 def index(self, format='html'):
71 """GET /users: All items in the collection"""
71 """GET /users: All items in the collection"""
72 # url('users')
72 # url('users')
73
73
74 c.users_list = User.query().order_by(User.username) \
74 c.users_list = User.query().order_by(User.username) \
75 .filter(User.username != User.DEFAULT_USER) \
75 .filter(User.username != User.DEFAULT_USER) \
76 .order_by(func.lower(User.username)) \
76 .order_by(func.lower(User.username)) \
77 .all()
77 .all()
78
78
79 users_data = []
79 users_data = []
80 total_records = len(c.users_list)
80 total_records = len(c.users_list)
81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
83
83
84 grav_tmpl = '<div class="gravatar">%s</div>'
84 grav_tmpl = '<div class="gravatar">%s</div>'
85
85
86 username = lambda user_id, username: (
86 username = lambda user_id, username: (
87 template.get_def("user_name")
87 template.get_def("user_name")
88 .render(user_id, username, _=_, h=h, c=c))
88 .render(user_id, username, _=_, h=h, c=c))
89
89
90 user_actions = lambda user_id, username: (
90 user_actions = lambda user_id, username: (
91 template.get_def("user_actions")
91 template.get_def("user_actions")
92 .render(user_id, username, _=_, h=h, c=c))
92 .render(user_id, username, _=_, h=h, c=c))
93
93
94 for user in c.users_list:
94 for user in c.users_list:
95 users_data.append({
95 users_data.append({
96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
97 "raw_name": user.username,
97 "raw_name": user.username,
98 "username": username(user.user_id, user.username),
98 "username": username(user.user_id, user.username),
99 "firstname": h.escape(user.name),
99 "firstname": h.escape(user.name),
100 "lastname": h.escape(user.lastname),
100 "lastname": h.escape(user.lastname),
101 "last_login": h.fmt_date(user.last_login),
101 "last_login": h.fmt_date(user.last_login),
102 "last_login_raw": datetime_to_time(user.last_login),
102 "last_login_raw": datetime_to_time(user.last_login),
103 "active": h.boolicon(user.active),
103 "active": h.boolicon(user.active),
104 "admin": h.boolicon(user.admin),
104 "admin": h.boolicon(user.admin),
105 "extern_type": user.extern_type,
105 "extern_type": user.extern_type,
106 "extern_name": user.extern_name,
106 "extern_name": user.extern_name,
107 "action": user_actions(user.user_id, user.username),
107 "action": user_actions(user.user_id, user.username),
108 })
108 })
109
109
110 c.data = json.dumps({
110 c.data = json.dumps({
111 "totalRecords": total_records,
111 "totalRecords": total_records,
112 "startIndex": 0,
112 "startIndex": 0,
113 "sort": None,
113 "sort": None,
114 "dir": "asc",
114 "dir": "asc",
115 "records": users_data
115 "records": users_data
116 })
116 })
117
117
118 return render('admin/users/users.html')
118 return render('admin/users/users.html')
119
119
120 def create(self):
120 def create(self):
121 """POST /users: Create a new item"""
121 """POST /users: Create a new item"""
122 # url('users')
122 # url('users')
123 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
123 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
124 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
124 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
125 user_model = UserModel()
125 user_model = UserModel()
126 user_form = UserForm()()
126 user_form = UserForm()()
127 try:
127 try:
128 form_result = user_form.to_python(dict(request.POST))
128 form_result = user_form.to_python(dict(request.POST))
129 user = user_model.create(form_result)
129 user = user_model.create(form_result)
130 usr = form_result['username']
130 usr = form_result['username']
131 action_logger(self.authuser, 'admin_created_user:%s' % usr,
131 action_logger(self.authuser, 'admin_created_user:%s' % usr,
132 None, self.ip_addr, self.sa)
132 None, self.ip_addr, self.sa)
133 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))),
134 category='success')
134 category='success')
135 Session().commit()
135 Session().commit()
136 except formencode.Invalid as errors:
136 except formencode.Invalid as errors:
137 return htmlfill.render(
137 return htmlfill.render(
138 render('admin/users/user_add.html'),
138 render('admin/users/user_add.html'),
139 defaults=errors.value,
139 defaults=errors.value,
140 errors=errors.error_dict or {},
140 errors=errors.error_dict or {},
141 prefix_error=False,
141 prefix_error=False,
142 encoding="UTF-8",
142 encoding="UTF-8",
143 force_defaults=False)
143 force_defaults=False)
144 except UserCreationError as e:
144 except UserCreationError as e:
145 h.flash(e, 'error')
145 h.flash(e, 'error')
146 except Exception:
146 except Exception:
147 log.error(traceback.format_exc())
147 log.error(traceback.format_exc())
148 h.flash(_('Error occurred during creation of user %s') \
148 h.flash(_('Error occurred during creation of user %s') \
149 % request.POST.get('username'), category='error')
149 % request.POST.get('username'), category='error')
150 raise HTTPFound(location=url('users'))
150 raise HTTPFound(location=url('users'))
151
151
152 def new(self, format='html'):
152 def new(self, format='html'):
153 """GET /users/new: Form to create a new item"""
153 """GET /users/new: Form to create a new item"""
154 # url('new_user')
154 # url('new_user')
155 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
155 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
156 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
156 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
157 return render('admin/users/user_add.html')
157 return render('admin/users/user_add.html')
158
158
159 def update(self, id):
159 def update(self, id):
160 """PUT /users/id: Update an existing item"""
160 """PUT /users/id: Update an existing item"""
161 # Forms posted to this method should contain a hidden field:
161 # Forms posted to this method should contain a hidden field:
162 # <input type="hidden" name="_method" value="PUT" />
162 # <input type="hidden" name="_method" value="PUT" />
163 # Or using helpers:
163 # Or using helpers:
164 # h.form(url('update_user', id=ID),
164 # h.form(url('update_user', id=ID),
165 # method='put')
165 # method='put')
166 # url('user', id=ID)
166 # url('user', id=ID)
167 user_model = UserModel()
167 user_model = UserModel()
168 user = user_model.get(id)
168 user = user_model.get(id)
169 _form = UserForm(edit=True, old_data={'user_id': id,
169 _form = UserForm(edit=True, old_data={'user_id': id,
170 'email': user.email})()
170 'email': user.email})()
171 form_result = {}
171 form_result = {}
172 try:
172 try:
173 form_result = _form.to_python(dict(request.POST))
173 form_result = _form.to_python(dict(request.POST))
174 skip_attrs = ['extern_type', 'extern_name',
174 skip_attrs = ['extern_type', 'extern_name',
175 ] + auth_modules.get_managed_fields(user)
175 ] + auth_modules.get_managed_fields(user)
176
176
177 user_model.update(id, form_result, skip_attrs=skip_attrs)
177 user_model.update(id, form_result, skip_attrs=skip_attrs)
178 usr = form_result['username']
178 usr = form_result['username']
179 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
179 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
180 None, self.ip_addr, self.sa)
180 None, self.ip_addr, self.sa)
181 h.flash(_('User updated successfully'), category='success')
181 h.flash(_('User updated successfully'), category='success')
182 Session().commit()
182 Session().commit()
183 except formencode.Invalid as errors:
183 except formencode.Invalid as errors:
184 defaults = errors.value
184 defaults = errors.value
185 e = errors.error_dict or {}
185 e = errors.error_dict or {}
186 defaults.update({
186 defaults.update({
187 'create_repo_perm': user_model.has_perm(id,
187 'create_repo_perm': user_model.has_perm(id,
188 'hg.create.repository'),
188 'hg.create.repository'),
189 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
189 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
190 '_method': 'put'
190 '_method': 'put'
191 })
191 })
192 return htmlfill.render(
192 return htmlfill.render(
193 self._render_edit_profile(user),
193 self._render_edit_profile(user),
194 defaults=defaults,
194 defaults=defaults,
195 errors=e,
195 errors=e,
196 prefix_error=False,
196 prefix_error=False,
197 encoding="UTF-8",
197 encoding="UTF-8",
198 force_defaults=False)
198 force_defaults=False)
199 except Exception:
199 except Exception:
200 log.error(traceback.format_exc())
200 log.error(traceback.format_exc())
201 h.flash(_('Error occurred during update of user %s') \
201 h.flash(_('Error occurred during update of user %s') \
202 % form_result.get('username'), category='error')
202 % form_result.get('username'), category='error')
203 raise HTTPFound(location=url('edit_user', id=id))
203 raise HTTPFound(location=url('edit_user', id=id))
204
204
205 def delete(self, id):
205 def delete(self, id):
206 """DELETE /users/id: Delete an existing item"""
206 """DELETE /users/id: Delete an existing item"""
207 # Forms posted to this method should contain a hidden field:
207 # Forms posted to this method should contain a hidden field:
208 # <input type="hidden" name="_method" value="DELETE" />
208 # <input type="hidden" name="_method" value="DELETE" />
209 # Or using helpers:
209 # Or using helpers:
210 # h.form(url('delete_user', id=ID),
210 # h.form(url('delete_user', id=ID),
211 # method='delete')
211 # method='delete')
212 # url('user', id=ID)
212 # url('user', id=ID)
213 usr = User.get_or_404(id)
213 usr = User.get_or_404(id)
214 try:
214 try:
215 UserModel().delete(usr)
215 UserModel().delete(usr)
216 Session().commit()
216 Session().commit()
217 h.flash(_('Successfully deleted user'), category='success')
217 h.flash(_('Successfully deleted user'), category='success')
218 except (UserOwnsReposException, DefaultUserException) as e:
218 except (UserOwnsReposException, DefaultUserException) as e:
219 h.flash(e, category='warning')
219 h.flash(e, category='warning')
220 except Exception:
220 except Exception:
221 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
222 h.flash(_('An error occurred during deletion of user'),
222 h.flash(_('An error occurred during deletion of user'),
223 category='error')
223 category='error')
224 raise HTTPFound(location=url('users'))
224 raise HTTPFound(location=url('users'))
225
225
226 def show(self, id, format='html'):
226 def show(self, id, format='html'):
227 """GET /users/id: Show a specific item"""
227 """GET /users/id: Show a specific item"""
228 # url('user', id=ID)
228 # url('user', id=ID)
229 User.get_or_404(-1)
229 User.get_or_404(-1)
230
230
231 def _get_user_or_raise_if_default(self, id):
231 def _get_user_or_raise_if_default(self, id):
232 try:
232 try:
233 return User.get_or_404(id, allow_default=False)
233 return User.get_or_404(id, allow_default=False)
234 except DefaultUserException:
234 except DefaultUserException:
235 h.flash(_("The default user cannot be edited"), category='warning')
235 h.flash(_("The default user cannot be edited"), category='warning')
236 raise HTTPNotFound
236 raise HTTPNotFound
237
237
238 def _render_edit_profile(self, user):
238 def _render_edit_profile(self, user):
239 c.user = user
239 c.user = user
240 c.active = 'profile'
240 c.active = 'profile'
241 c.perm_user = AuthUser(dbuser=user)
241 c.perm_user = AuthUser(dbuser=user)
242 c.ip_addr = self.ip_addr
242 c.ip_addr = self.ip_addr
243 managed_fields = auth_modules.get_managed_fields(user)
243 managed_fields = auth_modules.get_managed_fields(user)
244 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
245 return render('admin/users/user_edit.html')
245 return render('admin/users/user_edit.html')
246
246
247 def edit(self, id, format='html'):
247 def edit(self, id, format='html'):
248 """GET /users/id/edit: Form to edit an existing item"""
248 """GET /users/id/edit: Form to edit an existing item"""
249 # url('edit_user', id=ID)
249 # url('edit_user', id=ID)
250 user = self._get_user_or_raise_if_default(id)
250 user = self._get_user_or_raise_if_default(id)
251 defaults = user.get_dict()
251 defaults = user.get_dict()
252
252
253 return htmlfill.render(
253 return htmlfill.render(
254 self._render_edit_profile(user),
254 self._render_edit_profile(user),
255 defaults=defaults,
255 defaults=defaults,
256 encoding="UTF-8",
256 encoding="UTF-8",
257 force_defaults=False)
257 force_defaults=False)
258
258
259 def edit_advanced(self, id):
259 def edit_advanced(self, id):
260 c.user = self._get_user_or_raise_if_default(id)
260 c.user = self._get_user_or_raise_if_default(id)
261 c.active = 'advanced'
261 c.active = 'advanced'
262 c.perm_user = AuthUser(user_id=id)
262 c.perm_user = AuthUser(user_id=id)
263 c.ip_addr = self.ip_addr
263 c.ip_addr = self.ip_addr
264
264
265 umodel = UserModel()
265 umodel = UserModel()
266 defaults = c.user.get_dict()
266 defaults = c.user.get_dict()
267 defaults.update({
267 defaults.update({
268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
269 'create_user_group_perm': umodel.has_perm(c.user,
269 'create_user_group_perm': umodel.has_perm(c.user,
270 'hg.usergroup.create.true'),
270 'hg.usergroup.create.true'),
271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
272 })
272 })
273 return htmlfill.render(
273 return htmlfill.render(
274 render('admin/users/user_edit.html'),
274 render('admin/users/user_edit.html'),
275 defaults=defaults,
275 defaults=defaults,
276 encoding="UTF-8",
276 encoding="UTF-8",
277 force_defaults=False)
277 force_defaults=False)
278
278
279 def edit_api_keys(self, id):
279 def edit_api_keys(self, id):
280 c.user = self._get_user_or_raise_if_default(id)
280 c.user = self._get_user_or_raise_if_default(id)
281 c.active = 'api_keys'
281 c.active = 'api_keys'
282 show_expired = True
282 show_expired = True
283 c.lifetime_values = [
283 c.lifetime_values = [
284 (str(-1), _('Forever')),
284 (str(-1), _('Forever')),
285 (str(5), _('5 minutes')),
285 (str(5), _('5 minutes')),
286 (str(60), _('1 hour')),
286 (str(60), _('1 hour')),
287 (str(60 * 24), _('1 day')),
287 (str(60 * 24), _('1 day')),
288 (str(60 * 24 * 30), _('1 month')),
288 (str(60 * 24 * 30), _('1 month')),
289 ]
289 ]
290 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
290 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
291 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,
292 show_expired=show_expired)
292 show_expired=show_expired)
293 defaults = c.user.get_dict()
293 defaults = c.user.get_dict()
294 return htmlfill.render(
294 return htmlfill.render(
295 render('admin/users/user_edit.html'),
295 render('admin/users/user_edit.html'),
296 defaults=defaults,
296 defaults=defaults,
297 encoding="UTF-8",
297 encoding="UTF-8",
298 force_defaults=False)
298 force_defaults=False)
299
299
300 def add_api_key(self, id):
300 def add_api_key(self, id):
301 c.user = self._get_user_or_raise_if_default(id)
301 c.user = self._get_user_or_raise_if_default(id)
302
302
303 lifetime = safe_int(request.POST.get('lifetime'), -1)
303 lifetime = safe_int(request.POST.get('lifetime'), -1)
304 description = request.POST.get('description')
304 description = request.POST.get('description')
305 ApiKeyModel().create(c.user.user_id, description, lifetime)
305 ApiKeyModel().create(c.user.user_id, description, lifetime)
306 Session().commit()
306 Session().commit()
307 h.flash(_("API key successfully created"), category='success')
307 h.flash(_("API key successfully created"), category='success')
308 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
308 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
309
309
310 def delete_api_key(self, id):
310 def delete_api_key(self, id):
311 c.user = self._get_user_or_raise_if_default(id)
311 c.user = self._get_user_or_raise_if_default(id)
312
312
313 api_key = request.POST.get('del_api_key')
313 api_key = request.POST.get('del_api_key')
314 if request.POST.get('del_api_key_builtin'):
314 if request.POST.get('del_api_key_builtin'):
315 user = User.get(c.user.user_id)
315 user = User.get(c.user.user_id)
316 if user is not None:
316 if user is not None:
317 user.api_key = generate_api_key()
317 user.api_key = generate_api_key()
318 Session().add(user)
318 Session().add(user)
319 Session().commit()
319 Session().commit()
320 h.flash(_("API key successfully reset"), category='success')
320 h.flash(_("API key successfully reset"), category='success')
321 elif api_key:
321 elif api_key:
322 ApiKeyModel().delete(api_key, c.user.user_id)
322 ApiKeyModel().delete(api_key, c.user.user_id)
323 Session().commit()
323 Session().commit()
324 h.flash(_("API key successfully deleted"), category='success')
324 h.flash(_("API key successfully deleted"), category='success')
325
325
326 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
326 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
327
327
328 def update_account(self, id):
328 def update_account(self, id):
329 pass
329 pass
330
330
331 def edit_perms(self, id):
331 def edit_perms(self, id):
332 c.user = self._get_user_or_raise_if_default(id)
332 c.user = self._get_user_or_raise_if_default(id)
333 c.active = 'perms'
333 c.active = 'perms'
334 c.perm_user = AuthUser(user_id=id)
334 c.perm_user = AuthUser(user_id=id)
335 c.ip_addr = self.ip_addr
335 c.ip_addr = self.ip_addr
336
336
337 umodel = UserModel()
337 umodel = UserModel()
338 defaults = c.user.get_dict()
338 defaults = c.user.get_dict()
339 defaults.update({
339 defaults.update({
340 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
340 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
341 'create_user_group_perm': umodel.has_perm(c.user,
341 'create_user_group_perm': umodel.has_perm(c.user,
342 'hg.usergroup.create.true'),
342 'hg.usergroup.create.true'),
343 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
343 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
344 })
344 })
345 return htmlfill.render(
345 return htmlfill.render(
346 render('admin/users/user_edit.html'),
346 render('admin/users/user_edit.html'),
347 defaults=defaults,
347 defaults=defaults,
348 encoding="UTF-8",
348 encoding="UTF-8",
349 force_defaults=False)
349 force_defaults=False)
350
350
351 def update_perms(self, id):
351 def update_perms(self, id):
352 """PUT /users_perm/id: Update an existing item"""
352 """PUT /users_perm/id: Update an existing item"""
353 # url('user_perm', id=ID, method='put')
353 # url('user_perm', id=ID, method='put')
354 user = self._get_user_or_raise_if_default(id)
354 user = self._get_user_or_raise_if_default(id)
355
355
356 try:
356 try:
357 form = CustomDefaultPermissionsForm()()
357 form = CustomDefaultPermissionsForm()()
358 form_result = form.to_python(request.POST)
358 form_result = form.to_python(request.POST)
359
359
360 inherit_perms = form_result['inherit_default_permissions']
360 inherit_perms = form_result['inherit_default_permissions']
361 user.inherit_default_permissions = inherit_perms
361 user.inherit_default_permissions = inherit_perms
362 Session().add(user)
362 Session().add(user)
363 user_model = UserModel()
363 user_model = UserModel()
364
364
365 defs = UserToPerm.query() \
365 defs = UserToPerm.query() \
366 .filter(UserToPerm.user == user) \
366 .filter(UserToPerm.user == user) \
367 .all()
367 .all()
368 for ug in defs:
368 for ug in defs:
369 Session().delete(ug)
369 Session().delete(ug)
370
370
371 if form_result['create_repo_perm']:
371 if form_result['create_repo_perm']:
372 user_model.grant_perm(id, 'hg.create.repository')
372 user_model.grant_perm(id, 'hg.create.repository')
373 else:
373 else:
374 user_model.grant_perm(id, 'hg.create.none')
374 user_model.grant_perm(id, 'hg.create.none')
375 if form_result['create_user_group_perm']:
375 if form_result['create_user_group_perm']:
376 user_model.grant_perm(id, 'hg.usergroup.create.true')
376 user_model.grant_perm(id, 'hg.usergroup.create.true')
377 else:
377 else:
378 user_model.grant_perm(id, 'hg.usergroup.create.false')
378 user_model.grant_perm(id, 'hg.usergroup.create.false')
379 if form_result['fork_repo_perm']:
379 if form_result['fork_repo_perm']:
380 user_model.grant_perm(id, 'hg.fork.repository')
380 user_model.grant_perm(id, 'hg.fork.repository')
381 else:
381 else:
382 user_model.grant_perm(id, 'hg.fork.none')
382 user_model.grant_perm(id, 'hg.fork.none')
383 h.flash(_("Updated permissions"), category='success')
383 h.flash(_("Updated permissions"), category='success')
384 Session().commit()
384 Session().commit()
385 except Exception:
385 except Exception:
386 log.error(traceback.format_exc())
386 log.error(traceback.format_exc())
387 h.flash(_('An error occurred during permissions saving'),
387 h.flash(_('An error occurred during permissions saving'),
388 category='error')
388 category='error')
389 raise HTTPFound(location=url('edit_user_perms', id=id))
389 raise HTTPFound(location=url('edit_user_perms', id=id))
390
390
391 def edit_emails(self, id):
391 def edit_emails(self, id):
392 c.user = self._get_user_or_raise_if_default(id)
392 c.user = self._get_user_or_raise_if_default(id)
393 c.active = 'emails'
393 c.active = 'emails'
394 c.user_email_map = UserEmailMap.query() \
394 c.user_email_map = UserEmailMap.query() \
395 .filter(UserEmailMap.user == c.user).all()
395 .filter(UserEmailMap.user == c.user).all()
396
396
397 defaults = c.user.get_dict()
397 defaults = c.user.get_dict()
398 return htmlfill.render(
398 return htmlfill.render(
399 render('admin/users/user_edit.html'),
399 render('admin/users/user_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 def add_email(self, id):
404 def add_email(self, id):
405 """POST /user_emails:Add an existing item"""
405 """POST /user_emails:Add an existing item"""
406 # url('user_emails', id=ID, method='put')
406 # url('user_emails', id=ID, method='put')
407 user = self._get_user_or_raise_if_default(id)
407 user = self._get_user_or_raise_if_default(id)
408 email = request.POST.get('new_email')
408 email = request.POST.get('new_email')
409 user_model = UserModel()
409 user_model = UserModel()
410
410
411 try:
411 try:
412 user_model.add_extra_email(id, email)
412 user_model.add_extra_email(id, email)
413 Session().commit()
413 Session().commit()
414 h.flash(_("Added email %s to user") % email, category='success')
414 h.flash(_("Added email %s to user") % email, category='success')
415 except formencode.Invalid as error:
415 except formencode.Invalid as error:
416 msg = error.error_dict['email']
416 msg = error.error_dict['email']
417 h.flash(msg, category='error')
417 h.flash(msg, category='error')
418 except Exception:
418 except Exception:
419 log.error(traceback.format_exc())
419 log.error(traceback.format_exc())
420 h.flash(_('An error occurred during email saving'),
420 h.flash(_('An error occurred during email saving'),
421 category='error')
421 category='error')
422 raise HTTPFound(location=url('edit_user_emails', id=id))
422 raise HTTPFound(location=url('edit_user_emails', id=id))
423
423
424 def delete_email(self, id):
424 def delete_email(self, id):
425 """DELETE /user_emails_delete/id: Delete an existing item"""
425 """DELETE /user_emails_delete/id: Delete an existing item"""
426 # url('user_emails_delete', id=ID, method='delete')
426 # url('user_emails_delete', id=ID, method='delete')
427 user = self._get_user_or_raise_if_default(id)
427 user = self._get_user_or_raise_if_default(id)
428 email_id = request.POST.get('del_email_id')
428 email_id = request.POST.get('del_email_id')
429 user_model = UserModel()
429 user_model = UserModel()
430 user_model.delete_extra_email(id, email_id)
430 user_model.delete_extra_email(id, email_id)
431 Session().commit()
431 Session().commit()
432 h.flash(_("Removed email from user"), category='success')
432 h.flash(_("Removed email from user"), category='success')
433 raise HTTPFound(location=url('edit_user_emails', id=id))
433 raise HTTPFound(location=url('edit_user_emails', id=id))
434
434
435 def edit_ips(self, id):
435 def edit_ips(self, id):
436 c.user = self._get_user_or_raise_if_default(id)
436 c.user = self._get_user_or_raise_if_default(id)
437 c.active = 'ips'
437 c.active = 'ips'
438 c.user_ip_map = UserIpMap.query() \
438 c.user_ip_map = UserIpMap.query() \
439 .filter(UserIpMap.user == c.user).all()
439 .filter(UserIpMap.user == c.user).all()
440
440
441 c.inherit_default_ips = c.user.inherit_default_permissions
441 c.inherit_default_ips = c.user.inherit_default_permissions
442 c.default_user_ip_map = UserIpMap.query() \
442 c.default_user_ip_map = UserIpMap.query() \
443 .filter(UserIpMap.user == User.get_default_user()).all()
443 .filter(UserIpMap.user == User.get_default_user()).all()
444
444
445 defaults = c.user.get_dict()
445 defaults = c.user.get_dict()
446 return htmlfill.render(
446 return htmlfill.render(
447 render('admin/users/user_edit.html'),
447 render('admin/users/user_edit.html'),
448 defaults=defaults,
448 defaults=defaults,
449 encoding="UTF-8",
449 encoding="UTF-8",
450 force_defaults=False)
450 force_defaults=False)
451
451
452 def add_ip(self, id):
452 def add_ip(self, id):
453 """POST /user_ips:Add an existing item"""
453 """POST /user_ips:Add an existing item"""
454 # url('user_ips', id=ID, method='put')
454 # url('user_ips', id=ID, method='put')
455
455
456 ip = request.POST.get('new_ip')
456 ip = request.POST.get('new_ip')
457 user_model = UserModel()
457 user_model = UserModel()
458
458
459 try:
459 try:
460 user_model.add_extra_ip(id, ip)
460 user_model.add_extra_ip(id, ip)
461 Session().commit()
461 Session().commit()
462 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')
463 except formencode.Invalid as error:
463 except formencode.Invalid as error:
464 msg = error.error_dict['ip']
464 msg = error.error_dict['ip']
465 h.flash(msg, category='error')
465 h.flash(msg, category='error')
466 except Exception:
466 except Exception:
467 log.error(traceback.format_exc())
467 log.error(traceback.format_exc())
468 h.flash(_('An error occurred while adding IP address'),
468 h.flash(_('An error occurred while adding IP address'),
469 category='error')
469 category='error')
470
470
471 if 'default_user' in request.POST:
471 if 'default_user' in request.POST:
472 raise HTTPFound(location=url('admin_permissions_ips'))
472 raise HTTPFound(location=url('admin_permissions_ips'))
473 raise HTTPFound(location=url('edit_user_ips', id=id))
473 raise HTTPFound(location=url('edit_user_ips', id=id))
474
474
475 def delete_ip(self, id):
475 def delete_ip(self, id):
476 """DELETE /user_ips_delete/id: Delete an existing item"""
476 """DELETE /user_ips_delete/id: Delete an existing item"""
477 # url('user_ips_delete', id=ID, method='delete')
477 # url('user_ips_delete', id=ID, method='delete')
478 ip_id = request.POST.get('del_ip_id')
478 ip_id = request.POST.get('del_ip_id')
479 user_model = UserModel()
479 user_model = UserModel()
480 user_model.delete_extra_ip(id, ip_id)
480 user_model.delete_extra_ip(id, ip_id)
481 Session().commit()
481 Session().commit()
482 h.flash(_("Removed IP address from user whitelist"), category='success')
482 h.flash(_("Removed IP address from user whitelist"), category='success')
483
483
484 if 'default_user' in request.POST:
484 if 'default_user' in request.POST:
485 raise HTTPFound(location=url('admin_permissions_ips'))
485 raise HTTPFound(location=url('admin_permissions_ips'))
486 raise HTTPFound(location=url('edit_user_ips', id=id))
486 raise HTTPFound(location=url('edit_user_ips', id=id))
@@ -1,2629 +1,2629 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.api.api
15 kallithea.controllers.api.api
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 API controller for Kallithea
18 API 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: Aug 20, 2011
22 :created_on: Aug 20, 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 time
28 import time
29 import traceback
29 import traceback
30 import logging
30 import logging
31 from sqlalchemy import or_
31 from sqlalchemy import or_
32
32
33 from kallithea import EXTERN_TYPE_INTERNAL
33 from kallithea import EXTERN_TYPE_INTERNAL
34 from kallithea.controllers.api import JSONRPCController, JSONRPCError
34 from kallithea.controllers.api import JSONRPCController, JSONRPCError
35 from kallithea.lib.auth import (
35 from kallithea.lib.auth import (
36 PasswordGenerator, AuthUser, HasPermissionAllDecorator,
36 PasswordGenerator, AuthUser, HasPermissionAnyDecorator,
37 HasPermissionAnyDecorator, HasPermissionAnyApi, HasRepoPermissionAnyApi,
37 HasPermissionAnyDecorator, HasPermissionAnyApi, HasRepoPermissionAnyApi,
38 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAny)
38 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAny)
39 from kallithea.lib.utils import map_groups, repo2db_mapper
39 from kallithea.lib.utils import map_groups, repo2db_mapper
40 from kallithea.lib.utils2 import (
40 from kallithea.lib.utils2 import (
41 str2bool, time_to_datetime, safe_int, Optional, OAttr)
41 str2bool, time_to_datetime, safe_int, Optional, OAttr)
42 from kallithea.model.meta import Session
42 from kallithea.model.meta import Session
43 from kallithea.model.repo_group import RepoGroupModel
43 from kallithea.model.repo_group import RepoGroupModel
44 from kallithea.model.scm import ScmModel, UserGroupList
44 from kallithea.model.scm import ScmModel, UserGroupList
45 from kallithea.model.repo import RepoModel
45 from kallithea.model.repo import RepoModel
46 from kallithea.model.user import UserModel
46 from kallithea.model.user import UserModel
47 from kallithea.model.user_group import UserGroupModel
47 from kallithea.model.user_group import UserGroupModel
48 from kallithea.model.gist import GistModel
48 from kallithea.model.gist import GistModel
49 from kallithea.model.db import (
49 from kallithea.model.db import (
50 Repository, Setting, UserIpMap, Permission, User, Gist,
50 Repository, Setting, UserIpMap, Permission, User, Gist,
51 RepoGroup)
51 RepoGroup)
52 from kallithea.lib.compat import json
52 from kallithea.lib.compat import json
53 from kallithea.lib.exceptions import (
53 from kallithea.lib.exceptions import (
54 DefaultUserException, UserGroupsAssignedException)
54 DefaultUserException, UserGroupsAssignedException)
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def store_update(updates, attr, name):
59 def store_update(updates, attr, name):
60 """
60 """
61 Stores param in updates dict if it's not instance of Optional
61 Stores param in updates dict if it's not instance of Optional
62 allows easy updates of passed in params
62 allows easy updates of passed in params
63 """
63 """
64 if not isinstance(attr, Optional):
64 if not isinstance(attr, Optional):
65 updates[name] = attr
65 updates[name] = attr
66
66
67
67
68 def get_user_or_error(userid):
68 def get_user_or_error(userid):
69 """
69 """
70 Get user by id or name or return JsonRPCError if not found
70 Get user by id or name or return JsonRPCError if not found
71
71
72 :param userid:
72 :param userid:
73 """
73 """
74 user = UserModel().get_user(userid)
74 user = UserModel().get_user(userid)
75 if user is None:
75 if user is None:
76 raise JSONRPCError("user `%s` does not exist" % (userid,))
76 raise JSONRPCError("user `%s` does not exist" % (userid,))
77 return user
77 return user
78
78
79
79
80 def get_repo_or_error(repoid):
80 def get_repo_or_error(repoid):
81 """
81 """
82 Get repo by id or name or return JsonRPCError if not found
82 Get repo by id or name or return JsonRPCError if not found
83
83
84 :param repoid:
84 :param repoid:
85 """
85 """
86 repo = RepoModel().get_repo(repoid)
86 repo = RepoModel().get_repo(repoid)
87 if repo is None:
87 if repo is None:
88 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
88 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
89 return repo
89 return repo
90
90
91
91
92 def get_repo_group_or_error(repogroupid):
92 def get_repo_group_or_error(repogroupid):
93 """
93 """
94 Get repo group by id or name or return JsonRPCError if not found
94 Get repo group by id or name or return JsonRPCError if not found
95
95
96 :param repogroupid:
96 :param repogroupid:
97 """
97 """
98 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
98 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
99 if repo_group is None:
99 if repo_group is None:
100 raise JSONRPCError(
100 raise JSONRPCError(
101 'repository group `%s` does not exist' % (repogroupid,))
101 'repository group `%s` does not exist' % (repogroupid,))
102 return repo_group
102 return repo_group
103
103
104
104
105 def get_user_group_or_error(usergroupid):
105 def get_user_group_or_error(usergroupid):
106 """
106 """
107 Get user group by id or name or return JsonRPCError if not found
107 Get user group by id or name or return JsonRPCError if not found
108
108
109 :param usergroupid:
109 :param usergroupid:
110 """
110 """
111 user_group = UserGroupModel().get_group(usergroupid)
111 user_group = UserGroupModel().get_group(usergroupid)
112 if user_group is None:
112 if user_group is None:
113 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
113 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
114 return user_group
114 return user_group
115
115
116
116
117 def get_perm_or_error(permid, prefix=None):
117 def get_perm_or_error(permid, prefix=None):
118 """
118 """
119 Get permission by id or name or return JsonRPCError if not found
119 Get permission by id or name or return JsonRPCError if not found
120
120
121 :param permid:
121 :param permid:
122 """
122 """
123 perm = Permission.get_by_key(permid)
123 perm = Permission.get_by_key(permid)
124 if perm is None:
124 if perm is None:
125 raise JSONRPCError('permission `%s` does not exist' % (permid,))
125 raise JSONRPCError('permission `%s` does not exist' % (permid,))
126 if prefix:
126 if prefix:
127 if not perm.permission_name.startswith(prefix):
127 if not perm.permission_name.startswith(prefix):
128 raise JSONRPCError('permission `%s` is invalid, '
128 raise JSONRPCError('permission `%s` is invalid, '
129 'should start with %s' % (permid, prefix))
129 'should start with %s' % (permid, prefix))
130 return perm
130 return perm
131
131
132
132
133 def get_gist_or_error(gistid):
133 def get_gist_or_error(gistid):
134 """
134 """
135 Get gist by id or gist_access_id or return JsonRPCError if not found
135 Get gist by id or gist_access_id or return JsonRPCError if not found
136
136
137 :param gistid:
137 :param gistid:
138 """
138 """
139 gist = GistModel().get_gist(gistid)
139 gist = GistModel().get_gist(gistid)
140 if gist is None:
140 if gist is None:
141 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
141 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
142 return gist
142 return gist
143
143
144
144
145 class ApiController(JSONRPCController):
145 class ApiController(JSONRPCController):
146 """
146 """
147 API Controller
147 API Controller
148
148
149 Each method takes USER as first argument. This is then, based on given
149 Each method takes USER as first argument. This is then, based on given
150 API_KEY propagated as instance of user object who's making the call.
150 API_KEY propagated as instance of user object who's making the call.
151
151
152 example function::
152 example function::
153
153
154 def func(apiuser,arg1, arg2,...):
154 def func(apiuser,arg1, arg2,...):
155 pass
155 pass
156
156
157 Each function should also **raise** JSONRPCError for any
157 Each function should also **raise** JSONRPCError for any
158 errors that happens.
158 errors that happens.
159
159
160 """
160 """
161
161
162 @HasPermissionAllDecorator('hg.admin')
162 @HasPermissionAnyDecorator('hg.admin')
163 def test(self, apiuser, args):
163 def test(self, apiuser, args):
164 return args
164 return args
165
165
166 @HasPermissionAllDecorator('hg.admin')
166 @HasPermissionAnyDecorator('hg.admin')
167 def pull(self, apiuser, repoid):
167 def pull(self, apiuser, repoid):
168 """
168 """
169 Triggers a pull from remote location on given repo. Can be used to
169 Triggers a pull from remote location on given repo. Can be used to
170 automatically keep remote repos up to date. This command can be executed
170 automatically keep remote repos up to date. This command can be executed
171 only using api_key belonging to user with admin rights
171 only using api_key belonging to user with admin rights
172
172
173 :param apiuser: filled automatically from apikey
173 :param apiuser: filled automatically from apikey
174 :type apiuser: AuthUser
174 :type apiuser: AuthUser
175 :param repoid: repository name or repository id
175 :param repoid: repository name or repository id
176 :type repoid: str or int
176 :type repoid: str or int
177
177
178 OUTPUT::
178 OUTPUT::
179
179
180 id : <id_given_in_input>
180 id : <id_given_in_input>
181 result : {
181 result : {
182 "msg": "Pulled from `<repository name>`"
182 "msg": "Pulled from `<repository name>`"
183 "repository": "<repository name>"
183 "repository": "<repository name>"
184 }
184 }
185 error : null
185 error : null
186
186
187 ERROR OUTPUT::
187 ERROR OUTPUT::
188
188
189 id : <id_given_in_input>
189 id : <id_given_in_input>
190 result : null
190 result : null
191 error : {
191 error : {
192 "Unable to pull changes from `<reponame>`"
192 "Unable to pull changes from `<reponame>`"
193 }
193 }
194
194
195 """
195 """
196
196
197 repo = get_repo_or_error(repoid)
197 repo = get_repo_or_error(repoid)
198
198
199 try:
199 try:
200 ScmModel().pull_changes(repo.repo_name,
200 ScmModel().pull_changes(repo.repo_name,
201 self.authuser.username)
201 self.authuser.username)
202 return dict(
202 return dict(
203 msg='Pulled from `%s`' % repo.repo_name,
203 msg='Pulled from `%s`' % repo.repo_name,
204 repository=repo.repo_name
204 repository=repo.repo_name
205 )
205 )
206 except Exception:
206 except Exception:
207 log.error(traceback.format_exc())
207 log.error(traceback.format_exc())
208 raise JSONRPCError(
208 raise JSONRPCError(
209 'Unable to pull changes from `%s`' % repo.repo_name
209 'Unable to pull changes from `%s`' % repo.repo_name
210 )
210 )
211
211
212 @HasPermissionAllDecorator('hg.admin')
212 @HasPermissionAnyDecorator('hg.admin')
213 def rescan_repos(self, apiuser, remove_obsolete=Optional(False)):
213 def rescan_repos(self, apiuser, remove_obsolete=Optional(False)):
214 """
214 """
215 Triggers rescan repositories action. If remove_obsolete is set
215 Triggers rescan repositories action. If remove_obsolete is set
216 than also delete repos that are in database but not in the filesystem.
216 than also delete repos that are in database but not in the filesystem.
217 aka "clean zombies". This command can be executed only using api_key
217 aka "clean zombies". This command can be executed only using api_key
218 belonging to user with admin rights.
218 belonging to user with admin rights.
219
219
220 :param apiuser: filled automatically from apikey
220 :param apiuser: filled automatically from apikey
221 :type apiuser: AuthUser
221 :type apiuser: AuthUser
222 :param remove_obsolete: deletes repositories from
222 :param remove_obsolete: deletes repositories from
223 database that are not found on the filesystem
223 database that are not found on the filesystem
224 :type remove_obsolete: Optional(bool)
224 :type remove_obsolete: Optional(bool)
225
225
226 OUTPUT::
226 OUTPUT::
227
227
228 id : <id_given_in_input>
228 id : <id_given_in_input>
229 result : {
229 result : {
230 'added': [<added repository name>,...]
230 'added': [<added repository name>,...]
231 'removed': [<removed repository name>,...]
231 'removed': [<removed repository name>,...]
232 }
232 }
233 error : null
233 error : null
234
234
235 ERROR OUTPUT::
235 ERROR OUTPUT::
236
236
237 id : <id_given_in_input>
237 id : <id_given_in_input>
238 result : null
238 result : null
239 error : {
239 error : {
240 'Error occurred during rescan repositories action'
240 'Error occurred during rescan repositories action'
241 }
241 }
242
242
243 """
243 """
244
244
245 try:
245 try:
246 rm_obsolete = Optional.extract(remove_obsolete)
246 rm_obsolete = Optional.extract(remove_obsolete)
247 added, removed = repo2db_mapper(ScmModel().repo_scan(),
247 added, removed = repo2db_mapper(ScmModel().repo_scan(),
248 remove_obsolete=rm_obsolete)
248 remove_obsolete=rm_obsolete)
249 return {'added': added, 'removed': removed}
249 return {'added': added, 'removed': removed}
250 except Exception:
250 except Exception:
251 log.error(traceback.format_exc())
251 log.error(traceback.format_exc())
252 raise JSONRPCError(
252 raise JSONRPCError(
253 'Error occurred during rescan repositories action'
253 'Error occurred during rescan repositories action'
254 )
254 )
255
255
256 def invalidate_cache(self, apiuser, repoid):
256 def invalidate_cache(self, apiuser, repoid):
257 """
257 """
258 Invalidate cache for repository.
258 Invalidate cache for repository.
259 This command can be executed only using api_key belonging to user with admin
259 This command can be executed only using api_key belonging to user with admin
260 rights or regular user that have write or admin or write access to repository.
260 rights or regular user that have write or admin or write access to repository.
261
261
262 :param apiuser: filled automatically from apikey
262 :param apiuser: filled automatically from apikey
263 :type apiuser: AuthUser
263 :type apiuser: AuthUser
264 :param repoid: repository name or repository id
264 :param repoid: repository name or repository id
265 :type repoid: str or int
265 :type repoid: str or int
266
266
267 OUTPUT::
267 OUTPUT::
268
268
269 id : <id_given_in_input>
269 id : <id_given_in_input>
270 result : {
270 result : {
271 'msg': Cache for repository `<repository name>` was invalidated,
271 'msg': Cache for repository `<repository name>` was invalidated,
272 'repository': <repository name>
272 'repository': <repository name>
273 }
273 }
274 error : null
274 error : null
275
275
276 ERROR OUTPUT::
276 ERROR OUTPUT::
277
277
278 id : <id_given_in_input>
278 id : <id_given_in_input>
279 result : null
279 result : null
280 error : {
280 error : {
281 'Error occurred during cache invalidation action'
281 'Error occurred during cache invalidation action'
282 }
282 }
283
283
284 """
284 """
285 repo = get_repo_or_error(repoid)
285 repo = get_repo_or_error(repoid)
286 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
286 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
287 # check if we have admin permission for this repo !
287 # check if we have admin permission for this repo !
288 if not HasRepoPermissionAnyApi('repository.admin',
288 if not HasRepoPermissionAnyApi('repository.admin',
289 'repository.write')(
289 'repository.write')(
290 user=apiuser, repo_name=repo.repo_name):
290 user=apiuser, repo_name=repo.repo_name):
291 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
291 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
292
292
293 try:
293 try:
294 ScmModel().mark_for_invalidation(repo.repo_name)
294 ScmModel().mark_for_invalidation(repo.repo_name)
295 return dict(
295 return dict(
296 msg='Cache for repository `%s` was invalidated' % (repoid,),
296 msg='Cache for repository `%s` was invalidated' % (repoid,),
297 repository=repo.repo_name
297 repository=repo.repo_name
298 )
298 )
299 except Exception:
299 except Exception:
300 log.error(traceback.format_exc())
300 log.error(traceback.format_exc())
301 raise JSONRPCError(
301 raise JSONRPCError(
302 'Error occurred during cache invalidation action'
302 'Error occurred during cache invalidation action'
303 )
303 )
304
304
305 # permission check inside
305 # permission check inside
306 def lock(self, apiuser, repoid, locked=Optional(None),
306 def lock(self, apiuser, repoid, locked=Optional(None),
307 userid=Optional(OAttr('apiuser'))):
307 userid=Optional(OAttr('apiuser'))):
308 """
308 """
309 Set locking state on given repository by given user. If userid param
309 Set locking state on given repository by given user. If userid param
310 is skipped, then it is set to id of user who is calling this method.
310 is skipped, then it is set to id of user who is calling this method.
311 If locked param is skipped then function shows current lock state of
311 If locked param is skipped then function shows current lock state of
312 given repo. This command can be executed only using api_key belonging
312 given repo. This command can be executed only using api_key belonging
313 to user with admin rights or regular user that have admin or write
313 to user with admin rights or regular user that have admin or write
314 access to repository.
314 access to repository.
315
315
316 :param apiuser: filled automatically from apikey
316 :param apiuser: filled automatically from apikey
317 :type apiuser: AuthUser
317 :type apiuser: AuthUser
318 :param repoid: repository name or repository id
318 :param repoid: repository name or repository id
319 :type repoid: str or int
319 :type repoid: str or int
320 :param locked: lock state to be set
320 :param locked: lock state to be set
321 :type locked: Optional(bool)
321 :type locked: Optional(bool)
322 :param userid: set lock as user
322 :param userid: set lock as user
323 :type userid: Optional(str or int)
323 :type userid: Optional(str or int)
324
324
325 OUTPUT::
325 OUTPUT::
326
326
327 id : <id_given_in_input>
327 id : <id_given_in_input>
328 result : {
328 result : {
329 'repo': '<reponame>',
329 'repo': '<reponame>',
330 'locked': <bool: lock state>,
330 'locked': <bool: lock state>,
331 'locked_since': <int: lock timestamp>,
331 'locked_since': <int: lock timestamp>,
332 'locked_by': <username of person who made the lock>,
332 'locked_by': <username of person who made the lock>,
333 'lock_state_changed': <bool: True if lock state has been changed in this request>,
333 'lock_state_changed': <bool: True if lock state has been changed in this request>,
334 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
334 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
335 or
335 or
336 'msg': 'Repo `<repository name>` not locked.'
336 'msg': 'Repo `<repository name>` not locked.'
337 or
337 or
338 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
338 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
339 }
339 }
340 error : null
340 error : null
341
341
342 ERROR OUTPUT::
342 ERROR OUTPUT::
343
343
344 id : <id_given_in_input>
344 id : <id_given_in_input>
345 result : null
345 result : null
346 error : {
346 error : {
347 'Error occurred locking repository `<reponame>`
347 'Error occurred locking repository `<reponame>`
348 }
348 }
349
349
350 """
350 """
351 repo = get_repo_or_error(repoid)
351 repo = get_repo_or_error(repoid)
352 if HasPermissionAnyApi('hg.admin')(user=apiuser):
352 if HasPermissionAnyApi('hg.admin')(user=apiuser):
353 pass
353 pass
354 elif HasRepoPermissionAnyApi('repository.admin',
354 elif HasRepoPermissionAnyApi('repository.admin',
355 'repository.write')(user=apiuser,
355 'repository.write')(user=apiuser,
356 repo_name=repo.repo_name):
356 repo_name=repo.repo_name):
357 # make sure normal user does not pass someone else userid,
357 # make sure normal user does not pass someone else userid,
358 # he is not allowed to do that
358 # he is not allowed to do that
359 if not isinstance(userid, Optional) and userid != apiuser.user_id:
359 if not isinstance(userid, Optional) and userid != apiuser.user_id:
360 raise JSONRPCError(
360 raise JSONRPCError(
361 'userid is not the same as your user'
361 'userid is not the same as your user'
362 )
362 )
363 else:
363 else:
364 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
364 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
365
365
366 if isinstance(userid, Optional):
366 if isinstance(userid, Optional):
367 userid = apiuser.user_id
367 userid = apiuser.user_id
368
368
369 user = get_user_or_error(userid)
369 user = get_user_or_error(userid)
370
370
371 if isinstance(locked, Optional):
371 if isinstance(locked, Optional):
372 lockobj = Repository.getlock(repo)
372 lockobj = Repository.getlock(repo)
373
373
374 if lockobj[0] is None:
374 if lockobj[0] is None:
375 _d = {
375 _d = {
376 'repo': repo.repo_name,
376 'repo': repo.repo_name,
377 'locked': False,
377 'locked': False,
378 'locked_since': None,
378 'locked_since': None,
379 'locked_by': None,
379 'locked_by': None,
380 'lock_state_changed': False,
380 'lock_state_changed': False,
381 'msg': 'Repo `%s` not locked.' % repo.repo_name
381 'msg': 'Repo `%s` not locked.' % repo.repo_name
382 }
382 }
383 return _d
383 return _d
384 else:
384 else:
385 userid, time_ = lockobj
385 userid, time_ = lockobj
386 lock_user = get_user_or_error(userid)
386 lock_user = get_user_or_error(userid)
387 _d = {
387 _d = {
388 'repo': repo.repo_name,
388 'repo': repo.repo_name,
389 'locked': True,
389 'locked': True,
390 'locked_since': time_,
390 'locked_since': time_,
391 'locked_by': lock_user.username,
391 'locked_by': lock_user.username,
392 'lock_state_changed': False,
392 'lock_state_changed': False,
393 'msg': ('Repo `%s` locked by `%s` on `%s`.'
393 'msg': ('Repo `%s` locked by `%s` on `%s`.'
394 % (repo.repo_name, lock_user.username,
394 % (repo.repo_name, lock_user.username,
395 json.dumps(time_to_datetime(time_))))
395 json.dumps(time_to_datetime(time_))))
396 }
396 }
397 return _d
397 return _d
398
398
399 # force locked state through a flag
399 # force locked state through a flag
400 else:
400 else:
401 locked = str2bool(locked)
401 locked = str2bool(locked)
402 try:
402 try:
403 if locked:
403 if locked:
404 lock_time = time.time()
404 lock_time = time.time()
405 Repository.lock(repo, user.user_id, lock_time)
405 Repository.lock(repo, user.user_id, lock_time)
406 else:
406 else:
407 lock_time = None
407 lock_time = None
408 Repository.unlock(repo)
408 Repository.unlock(repo)
409 _d = {
409 _d = {
410 'repo': repo.repo_name,
410 'repo': repo.repo_name,
411 'locked': locked,
411 'locked': locked,
412 'locked_since': lock_time,
412 'locked_since': lock_time,
413 'locked_by': user.username,
413 'locked_by': user.username,
414 'lock_state_changed': True,
414 'lock_state_changed': True,
415 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
415 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
416 % (user.username, repo.repo_name, locked))
416 % (user.username, repo.repo_name, locked))
417 }
417 }
418 return _d
418 return _d
419 except Exception:
419 except Exception:
420 log.error(traceback.format_exc())
420 log.error(traceback.format_exc())
421 raise JSONRPCError(
421 raise JSONRPCError(
422 'Error occurred locking repository `%s`' % repo.repo_name
422 'Error occurred locking repository `%s`' % repo.repo_name
423 )
423 )
424
424
425 def get_locks(self, apiuser, userid=Optional(OAttr('apiuser'))):
425 def get_locks(self, apiuser, userid=Optional(OAttr('apiuser'))):
426 """
426 """
427 Get all repositories with locks for given userid, if
427 Get all repositories with locks for given userid, if
428 this command is run by non-admin account userid is set to user
428 this command is run by non-admin account userid is set to user
429 who is calling this method, thus returning locks for himself.
429 who is calling this method, thus returning locks for himself.
430
430
431 :param apiuser: filled automatically from apikey
431 :param apiuser: filled automatically from apikey
432 :type apiuser: AuthUser
432 :type apiuser: AuthUser
433 :param userid: User to get locks for
433 :param userid: User to get locks for
434 :type userid: Optional(str or int)
434 :type userid: Optional(str or int)
435
435
436 OUTPUT::
436 OUTPUT::
437
437
438 id : <id_given_in_input>
438 id : <id_given_in_input>
439 result : {
439 result : {
440 [repo_object, repo_object,...]
440 [repo_object, repo_object,...]
441 }
441 }
442 error : null
442 error : null
443 """
443 """
444
444
445 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
445 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
446 # make sure normal user does not pass someone else userid,
446 # make sure normal user does not pass someone else userid,
447 # he is not allowed to do that
447 # he is not allowed to do that
448 if not isinstance(userid, Optional) and userid != apiuser.user_id:
448 if not isinstance(userid, Optional) and userid != apiuser.user_id:
449 raise JSONRPCError(
449 raise JSONRPCError(
450 'userid is not the same as your user'
450 'userid is not the same as your user'
451 )
451 )
452
452
453 ret = []
453 ret = []
454 if isinstance(userid, Optional):
454 if isinstance(userid, Optional):
455 user = None
455 user = None
456 else:
456 else:
457 user = get_user_or_error(userid)
457 user = get_user_or_error(userid)
458
458
459 # show all locks
459 # show all locks
460 for r in Repository.getAll():
460 for r in Repository.getAll():
461 userid, time_ = r.locked
461 userid, time_ = r.locked
462 if time_:
462 if time_:
463 _api_data = r.get_api_data()
463 _api_data = r.get_api_data()
464 # if we use userfilter just show the locks for this user
464 # if we use userfilter just show the locks for this user
465 if user is not None:
465 if user is not None:
466 if safe_int(userid) == user.user_id:
466 if safe_int(userid) == user.user_id:
467 ret.append(_api_data)
467 ret.append(_api_data)
468 else:
468 else:
469 ret.append(_api_data)
469 ret.append(_api_data)
470
470
471 return ret
471 return ret
472
472
473 @HasPermissionAllDecorator('hg.admin')
473 @HasPermissionAnyDecorator('hg.admin')
474 def get_ip(self, apiuser, userid=Optional(OAttr('apiuser'))):
474 def get_ip(self, apiuser, userid=Optional(OAttr('apiuser'))):
475 """
475 """
476 Shows IP address as seen from Kallithea server, together with all
476 Shows IP address as seen from Kallithea server, together with all
477 defined IP addresses for given user. If userid is not passed data is
477 defined IP addresses for given user. If userid is not passed data is
478 returned for user who's calling this function.
478 returned for user who's calling this function.
479 This command can be executed only using api_key belonging to user with
479 This command can be executed only using api_key belonging to user with
480 admin rights.
480 admin rights.
481
481
482 :param apiuser: filled automatically from apikey
482 :param apiuser: filled automatically from apikey
483 :type apiuser: AuthUser
483 :type apiuser: AuthUser
484 :param userid: username to show ips for
484 :param userid: username to show ips for
485 :type userid: Optional(str or int)
485 :type userid: Optional(str or int)
486
486
487 OUTPUT::
487 OUTPUT::
488
488
489 id : <id_given_in_input>
489 id : <id_given_in_input>
490 result : {
490 result : {
491 "server_ip_addr": "<ip_from_clien>",
491 "server_ip_addr": "<ip_from_clien>",
492 "user_ips": [
492 "user_ips": [
493 {
493 {
494 "ip_addr": "<ip_with_mask>",
494 "ip_addr": "<ip_with_mask>",
495 "ip_range": ["<start_ip>", "<end_ip>"],
495 "ip_range": ["<start_ip>", "<end_ip>"],
496 },
496 },
497 ...
497 ...
498 ]
498 ]
499 }
499 }
500
500
501 """
501 """
502 if isinstance(userid, Optional):
502 if isinstance(userid, Optional):
503 userid = apiuser.user_id
503 userid = apiuser.user_id
504 user = get_user_or_error(userid)
504 user = get_user_or_error(userid)
505 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
505 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
506 return dict(
506 return dict(
507 server_ip_addr=self.ip_addr,
507 server_ip_addr=self.ip_addr,
508 user_ips=ips
508 user_ips=ips
509 )
509 )
510
510
511 # alias for old
511 # alias for old
512 show_ip = get_ip
512 show_ip = get_ip
513
513
514 @HasPermissionAllDecorator('hg.admin')
514 @HasPermissionAnyDecorator('hg.admin')
515 def get_server_info(self, apiuser):
515 def get_server_info(self, apiuser):
516 """
516 """
517 return server info, including Kallithea version and installed packages
517 return server info, including Kallithea version and installed packages
518
518
519 :param apiuser: filled automatically from apikey
519 :param apiuser: filled automatically from apikey
520 :type apiuser: AuthUser
520 :type apiuser: AuthUser
521
521
522 OUTPUT::
522 OUTPUT::
523
523
524 id : <id_given_in_input>
524 id : <id_given_in_input>
525 result : {
525 result : {
526 'modules': [<module name>,...]
526 'modules': [<module name>,...]
527 'py_version': <python version>,
527 'py_version': <python version>,
528 'platform': <platform type>,
528 'platform': <platform type>,
529 'kallithea_version': <kallithea version>
529 'kallithea_version': <kallithea version>
530 }
530 }
531 error : null
531 error : null
532 """
532 """
533 return Setting.get_server_info()
533 return Setting.get_server_info()
534
534
535 def get_user(self, apiuser, userid=Optional(OAttr('apiuser'))):
535 def get_user(self, apiuser, userid=Optional(OAttr('apiuser'))):
536 """
536 """
537 Gets a user by username or user_id, Returns empty result if user is
537 Gets a user by username or user_id, Returns empty result if user is
538 not found. If userid param is skipped it is set to id of user who is
538 not found. If userid param is skipped it is set to id of user who is
539 calling this method. This command can be executed only using api_key
539 calling this method. This command can be executed only using api_key
540 belonging to user with admin rights, or regular users that cannot
540 belonging to user with admin rights, or regular users that cannot
541 specify different userid than theirs
541 specify different userid than theirs
542
542
543 :param apiuser: filled automatically from apikey
543 :param apiuser: filled automatically from apikey
544 :type apiuser: AuthUser
544 :type apiuser: AuthUser
545 :param userid: user to get data for
545 :param userid: user to get data for
546 :type userid: Optional(str or int)
546 :type userid: Optional(str or int)
547
547
548 OUTPUT::
548 OUTPUT::
549
549
550 id : <id_given_in_input>
550 id : <id_given_in_input>
551 result: None if user does not exist or
551 result: None if user does not exist or
552 {
552 {
553 "user_id" : "<user_id>",
553 "user_id" : "<user_id>",
554 "api_key" : "<api_key>",
554 "api_key" : "<api_key>",
555 "api_keys": "[<list of all API keys including additional ones>]"
555 "api_keys": "[<list of all API keys including additional ones>]"
556 "username" : "<username>",
556 "username" : "<username>",
557 "firstname": "<firstname>",
557 "firstname": "<firstname>",
558 "lastname" : "<lastname>",
558 "lastname" : "<lastname>",
559 "email" : "<email>",
559 "email" : "<email>",
560 "emails": "[<list of all emails including additional ones>]",
560 "emails": "[<list of all emails including additional ones>]",
561 "ip_addresses": "[<ip_address_for_user>,...]",
561 "ip_addresses": "[<ip_address_for_user>,...]",
562 "active" : "<bool: user active>",
562 "active" : "<bool: user active>",
563 "admin" :  "<bool: user is admin>",
563 "admin" :  "<bool: user is admin>",
564 "extern_name" : "<extern_name>",
564 "extern_name" : "<extern_name>",
565 "extern_type" : "<extern type>
565 "extern_type" : "<extern type>
566 "last_login": "<last_login>",
566 "last_login": "<last_login>",
567 "permissions": {
567 "permissions": {
568 "global": ["hg.create.repository",
568 "global": ["hg.create.repository",
569 "repository.read",
569 "repository.read",
570 "hg.register.manual_activate"],
570 "hg.register.manual_activate"],
571 "repositories": {"repo1": "repository.none"},
571 "repositories": {"repo1": "repository.none"},
572 "repositories_groups": {"Group1": "group.read"}
572 "repositories_groups": {"Group1": "group.read"}
573 },
573 },
574 }
574 }
575
575
576 error: null
576 error: null
577
577
578 """
578 """
579 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
579 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
580 # make sure normal user does not pass someone else userid,
580 # make sure normal user does not pass someone else userid,
581 # he is not allowed to do that
581 # he is not allowed to do that
582 if not isinstance(userid, Optional) and userid != apiuser.user_id:
582 if not isinstance(userid, Optional) and userid != apiuser.user_id:
583 raise JSONRPCError(
583 raise JSONRPCError(
584 'userid is not the same as your user'
584 'userid is not the same as your user'
585 )
585 )
586
586
587 if isinstance(userid, Optional):
587 if isinstance(userid, Optional):
588 userid = apiuser.user_id
588 userid = apiuser.user_id
589
589
590 user = get_user_or_error(userid)
590 user = get_user_or_error(userid)
591 data = user.get_api_data()
591 data = user.get_api_data()
592 data['permissions'] = AuthUser(user_id=user.user_id).permissions
592 data['permissions'] = AuthUser(user_id=user.user_id).permissions
593 return data
593 return data
594
594
595 @HasPermissionAllDecorator('hg.admin')
595 @HasPermissionAnyDecorator('hg.admin')
596 def get_users(self, apiuser):
596 def get_users(self, apiuser):
597 """
597 """
598 Lists all existing users. This command can be executed only using api_key
598 Lists all existing users. This command can be executed only using api_key
599 belonging to user with admin rights.
599 belonging to user with admin rights.
600
600
601 :param apiuser: filled automatically from apikey
601 :param apiuser: filled automatically from apikey
602 :type apiuser: AuthUser
602 :type apiuser: AuthUser
603
603
604 OUTPUT::
604 OUTPUT::
605
605
606 id : <id_given_in_input>
606 id : <id_given_in_input>
607 result: [<user_object>, ...]
607 result: [<user_object>, ...]
608 error: null
608 error: null
609 """
609 """
610
610
611 result = []
611 result = []
612 users_list = User.query().order_by(User.username) \
612 users_list = User.query().order_by(User.username) \
613 .filter(User.username != User.DEFAULT_USER) \
613 .filter(User.username != User.DEFAULT_USER) \
614 .all()
614 .all()
615 for user in users_list:
615 for user in users_list:
616 result.append(user.get_api_data())
616 result.append(user.get_api_data())
617 return result
617 return result
618
618
619 @HasPermissionAllDecorator('hg.admin')
619 @HasPermissionAnyDecorator('hg.admin')
620 def create_user(self, apiuser, username, email, password=Optional(''),
620 def create_user(self, apiuser, username, email, password=Optional(''),
621 firstname=Optional(''), lastname=Optional(''),
621 firstname=Optional(''), lastname=Optional(''),
622 active=Optional(True), admin=Optional(False),
622 active=Optional(True), admin=Optional(False),
623 extern_name=Optional(EXTERN_TYPE_INTERNAL),
623 extern_name=Optional(EXTERN_TYPE_INTERNAL),
624 extern_type=Optional(EXTERN_TYPE_INTERNAL)):
624 extern_type=Optional(EXTERN_TYPE_INTERNAL)):
625 """
625 """
626 Creates new user. Returns new user object. This command can
626 Creates new user. Returns new user object. This command can
627 be executed only using api_key belonging to user with admin rights.
627 be executed only using api_key belonging to user with admin rights.
628
628
629 :param apiuser: filled automatically from apikey
629 :param apiuser: filled automatically from apikey
630 :type apiuser: AuthUser
630 :type apiuser: AuthUser
631 :param username: new username
631 :param username: new username
632 :type username: str or int
632 :type username: str or int
633 :param email: email
633 :param email: email
634 :type email: str
634 :type email: str
635 :param password: password
635 :param password: password
636 :type password: Optional(str)
636 :type password: Optional(str)
637 :param firstname: firstname
637 :param firstname: firstname
638 :type firstname: Optional(str)
638 :type firstname: Optional(str)
639 :param lastname: lastname
639 :param lastname: lastname
640 :type lastname: Optional(str)
640 :type lastname: Optional(str)
641 :param active: active
641 :param active: active
642 :type active: Optional(bool)
642 :type active: Optional(bool)
643 :param admin: admin
643 :param admin: admin
644 :type admin: Optional(bool)
644 :type admin: Optional(bool)
645 :param extern_name: name of extern
645 :param extern_name: name of extern
646 :type extern_name: Optional(str)
646 :type extern_name: Optional(str)
647 :param extern_type: extern_type
647 :param extern_type: extern_type
648 :type extern_type: Optional(str)
648 :type extern_type: Optional(str)
649
649
650
650
651 OUTPUT::
651 OUTPUT::
652
652
653 id : <id_given_in_input>
653 id : <id_given_in_input>
654 result: {
654 result: {
655 "msg" : "created new user `<username>`",
655 "msg" : "created new user `<username>`",
656 "user": <user_obj>
656 "user": <user_obj>
657 }
657 }
658 error: null
658 error: null
659
659
660 ERROR OUTPUT::
660 ERROR OUTPUT::
661
661
662 id : <id_given_in_input>
662 id : <id_given_in_input>
663 result : null
663 result : null
664 error : {
664 error : {
665 "user `<username>` already exist"
665 "user `<username>` already exist"
666 or
666 or
667 "email `<email>` already exist"
667 "email `<email>` already exist"
668 or
668 or
669 "failed to create user `<username>`"
669 "failed to create user `<username>`"
670 }
670 }
671
671
672 """
672 """
673
673
674 if User.get_by_username(username):
674 if User.get_by_username(username):
675 raise JSONRPCError("user `%s` already exist" % (username,))
675 raise JSONRPCError("user `%s` already exist" % (username,))
676
676
677 if User.get_by_email(email):
677 if User.get_by_email(email):
678 raise JSONRPCError("email `%s` already exist" % (email,))
678 raise JSONRPCError("email `%s` already exist" % (email,))
679
679
680 if Optional.extract(extern_name):
680 if Optional.extract(extern_name):
681 # generate temporary password if user is external
681 # generate temporary password if user is external
682 password = PasswordGenerator().gen_password(length=8)
682 password = PasswordGenerator().gen_password(length=8)
683
683
684 try:
684 try:
685 user = UserModel().create_or_update(
685 user = UserModel().create_or_update(
686 username=Optional.extract(username),
686 username=Optional.extract(username),
687 password=Optional.extract(password),
687 password=Optional.extract(password),
688 email=Optional.extract(email),
688 email=Optional.extract(email),
689 firstname=Optional.extract(firstname),
689 firstname=Optional.extract(firstname),
690 lastname=Optional.extract(lastname),
690 lastname=Optional.extract(lastname),
691 active=Optional.extract(active),
691 active=Optional.extract(active),
692 admin=Optional.extract(admin),
692 admin=Optional.extract(admin),
693 extern_type=Optional.extract(extern_type),
693 extern_type=Optional.extract(extern_type),
694 extern_name=Optional.extract(extern_name)
694 extern_name=Optional.extract(extern_name)
695 )
695 )
696 Session().commit()
696 Session().commit()
697 return dict(
697 return dict(
698 msg='created new user `%s`' % username,
698 msg='created new user `%s`' % username,
699 user=user.get_api_data()
699 user=user.get_api_data()
700 )
700 )
701 except Exception:
701 except Exception:
702 log.error(traceback.format_exc())
702 log.error(traceback.format_exc())
703 raise JSONRPCError('failed to create user `%s`' % (username,))
703 raise JSONRPCError('failed to create user `%s`' % (username,))
704
704
705 @HasPermissionAllDecorator('hg.admin')
705 @HasPermissionAnyDecorator('hg.admin')
706 def update_user(self, apiuser, userid, username=Optional(None),
706 def update_user(self, apiuser, userid, username=Optional(None),
707 email=Optional(None), password=Optional(None),
707 email=Optional(None), password=Optional(None),
708 firstname=Optional(None), lastname=Optional(None),
708 firstname=Optional(None), lastname=Optional(None),
709 active=Optional(None), admin=Optional(None),
709 active=Optional(None), admin=Optional(None),
710 extern_type=Optional(None), extern_name=Optional(None)):
710 extern_type=Optional(None), extern_name=Optional(None)):
711 """
711 """
712 updates given user if such user exists. This command can
712 updates given user if such user exists. This command can
713 be executed only using api_key belonging to user with admin rights.
713 be executed only using api_key belonging to user with admin rights.
714
714
715 :param apiuser: filled automatically from apikey
715 :param apiuser: filled automatically from apikey
716 :type apiuser: AuthUser
716 :type apiuser: AuthUser
717 :param userid: userid to update
717 :param userid: userid to update
718 :type userid: str or int
718 :type userid: str or int
719 :param username: new username
719 :param username: new username
720 :type username: str or int
720 :type username: str or int
721 :param email: email
721 :param email: email
722 :type email: str
722 :type email: str
723 :param password: password
723 :param password: password
724 :type password: Optional(str)
724 :type password: Optional(str)
725 :param firstname: firstname
725 :param firstname: firstname
726 :type firstname: Optional(str)
726 :type firstname: Optional(str)
727 :param lastname: lastname
727 :param lastname: lastname
728 :type lastname: Optional(str)
728 :type lastname: Optional(str)
729 :param active: active
729 :param active: active
730 :type active: Optional(bool)
730 :type active: Optional(bool)
731 :param admin: admin
731 :param admin: admin
732 :type admin: Optional(bool)
732 :type admin: Optional(bool)
733 :param extern_name:
733 :param extern_name:
734 :type extern_name: Optional(str)
734 :type extern_name: Optional(str)
735 :param extern_type:
735 :param extern_type:
736 :type extern_type: Optional(str)
736 :type extern_type: Optional(str)
737
737
738
738
739 OUTPUT::
739 OUTPUT::
740
740
741 id : <id_given_in_input>
741 id : <id_given_in_input>
742 result: {
742 result: {
743 "msg" : "updated user ID:<userid> <username>",
743 "msg" : "updated user ID:<userid> <username>",
744 "user": <user_object>,
744 "user": <user_object>,
745 }
745 }
746 error: null
746 error: null
747
747
748 ERROR OUTPUT::
748 ERROR OUTPUT::
749
749
750 id : <id_given_in_input>
750 id : <id_given_in_input>
751 result : null
751 result : null
752 error : {
752 error : {
753 "failed to update user `<username>`"
753 "failed to update user `<username>`"
754 }
754 }
755
755
756 """
756 """
757
757
758 user = get_user_or_error(userid)
758 user = get_user_or_error(userid)
759
759
760 # only non optional arguments will be stored in updates
760 # only non optional arguments will be stored in updates
761 updates = {}
761 updates = {}
762
762
763 try:
763 try:
764
764
765 store_update(updates, username, 'username')
765 store_update(updates, username, 'username')
766 store_update(updates, password, 'password')
766 store_update(updates, password, 'password')
767 store_update(updates, email, 'email')
767 store_update(updates, email, 'email')
768 store_update(updates, firstname, 'name')
768 store_update(updates, firstname, 'name')
769 store_update(updates, lastname, 'lastname')
769 store_update(updates, lastname, 'lastname')
770 store_update(updates, active, 'active')
770 store_update(updates, active, 'active')
771 store_update(updates, admin, 'admin')
771 store_update(updates, admin, 'admin')
772 store_update(updates, extern_name, 'extern_name')
772 store_update(updates, extern_name, 'extern_name')
773 store_update(updates, extern_type, 'extern_type')
773 store_update(updates, extern_type, 'extern_type')
774
774
775 user = UserModel().update_user(user, **updates)
775 user = UserModel().update_user(user, **updates)
776 Session().commit()
776 Session().commit()
777 return dict(
777 return dict(
778 msg='updated user ID:%s %s' % (user.user_id, user.username),
778 msg='updated user ID:%s %s' % (user.user_id, user.username),
779 user=user.get_api_data()
779 user=user.get_api_data()
780 )
780 )
781 except DefaultUserException:
781 except DefaultUserException:
782 log.error(traceback.format_exc())
782 log.error(traceback.format_exc())
783 raise JSONRPCError('editing default user is forbidden')
783 raise JSONRPCError('editing default user is forbidden')
784 except Exception:
784 except Exception:
785 log.error(traceback.format_exc())
785 log.error(traceback.format_exc())
786 raise JSONRPCError('failed to update user `%s`' % (userid,))
786 raise JSONRPCError('failed to update user `%s`' % (userid,))
787
787
788 @HasPermissionAllDecorator('hg.admin')
788 @HasPermissionAnyDecorator('hg.admin')
789 def delete_user(self, apiuser, userid):
789 def delete_user(self, apiuser, userid):
790 """
790 """
791 deletes given user if such user exists. This command can
791 deletes given user if such user exists. This command can
792 be executed only using api_key belonging to user with admin rights.
792 be executed only using api_key belonging to user with admin rights.
793
793
794 :param apiuser: filled automatically from apikey
794 :param apiuser: filled automatically from apikey
795 :type apiuser: AuthUser
795 :type apiuser: AuthUser
796 :param userid: user to delete
796 :param userid: user to delete
797 :type userid: str or int
797 :type userid: str or int
798
798
799 OUTPUT::
799 OUTPUT::
800
800
801 id : <id_given_in_input>
801 id : <id_given_in_input>
802 result: {
802 result: {
803 "msg" : "deleted user ID:<userid> <username>",
803 "msg" : "deleted user ID:<userid> <username>",
804 "user": null
804 "user": null
805 }
805 }
806 error: null
806 error: null
807
807
808 ERROR OUTPUT::
808 ERROR OUTPUT::
809
809
810 id : <id_given_in_input>
810 id : <id_given_in_input>
811 result : null
811 result : null
812 error : {
812 error : {
813 "failed to delete user ID:<userid> <username>"
813 "failed to delete user ID:<userid> <username>"
814 }
814 }
815
815
816 """
816 """
817 user = get_user_or_error(userid)
817 user = get_user_or_error(userid)
818
818
819 try:
819 try:
820 UserModel().delete(userid)
820 UserModel().delete(userid)
821 Session().commit()
821 Session().commit()
822 return dict(
822 return dict(
823 msg='deleted user ID:%s %s' % (user.user_id, user.username),
823 msg='deleted user ID:%s %s' % (user.user_id, user.username),
824 user=None
824 user=None
825 )
825 )
826 except Exception:
826 except Exception:
827
827
828 log.error(traceback.format_exc())
828 log.error(traceback.format_exc())
829 raise JSONRPCError('failed to delete user ID:%s %s'
829 raise JSONRPCError('failed to delete user ID:%s %s'
830 % (user.user_id, user.username))
830 % (user.user_id, user.username))
831
831
832 # permission check inside
832 # permission check inside
833 def get_user_group(self, apiuser, usergroupid):
833 def get_user_group(self, apiuser, usergroupid):
834 """
834 """
835 Gets an existing user group. This command can be executed only using api_key
835 Gets an existing user group. This command can be executed only using api_key
836 belonging to user with admin rights or user who has at least
836 belonging to user with admin rights or user who has at least
837 read access to user group.
837 read access to user group.
838
838
839 :param apiuser: filled automatically from apikey
839 :param apiuser: filled automatically from apikey
840 :type apiuser: AuthUser
840 :type apiuser: AuthUser
841 :param usergroupid: id of user_group to edit
841 :param usergroupid: id of user_group to edit
842 :type usergroupid: str or int
842 :type usergroupid: str or int
843
843
844 OUTPUT::
844 OUTPUT::
845
845
846 id : <id_given_in_input>
846 id : <id_given_in_input>
847 result : None if group not exist
847 result : None if group not exist
848 {
848 {
849 "users_group_id" : "<id>",
849 "users_group_id" : "<id>",
850 "group_name" : "<groupname>",
850 "group_name" : "<groupname>",
851 "active": "<bool>",
851 "active": "<bool>",
852 "members" : [<user_obj>,...]
852 "members" : [<user_obj>,...]
853 }
853 }
854 error : null
854 error : null
855
855
856 """
856 """
857 user_group = get_user_group_or_error(usergroupid)
857 user_group = get_user_group_or_error(usergroupid)
858 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
858 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
859 # check if we have at least read permission for this user group !
859 # check if we have at least read permission for this user group !
860 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
860 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
861 if not HasUserGroupPermissionAny(*_perms)(
861 if not HasUserGroupPermissionAny(*_perms)(
862 user=apiuser, user_group_name=user_group.users_group_name):
862 user=apiuser, user_group_name=user_group.users_group_name):
863 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
863 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
864
864
865 data = user_group.get_api_data()
865 data = user_group.get_api_data()
866 return data
866 return data
867
867
868 # permission check inside
868 # permission check inside
869 def get_user_groups(self, apiuser):
869 def get_user_groups(self, apiuser):
870 """
870 """
871 Lists all existing user groups. This command can be executed only using
871 Lists all existing user groups. This command can be executed only using
872 api_key belonging to user with admin rights or user who has at least
872 api_key belonging to user with admin rights or user who has at least
873 read access to user group.
873 read access to user group.
874
874
875 :param apiuser: filled automatically from apikey
875 :param apiuser: filled automatically from apikey
876 :type apiuser: AuthUser
876 :type apiuser: AuthUser
877
877
878 OUTPUT::
878 OUTPUT::
879
879
880 id : <id_given_in_input>
880 id : <id_given_in_input>
881 result : [<user_group_obj>,...]
881 result : [<user_group_obj>,...]
882 error : null
882 error : null
883 """
883 """
884
884
885 result = []
885 result = []
886 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
886 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
887 extras = {'user': apiuser}
887 extras = {'user': apiuser}
888 for user_group in UserGroupList(UserGroupModel().get_all(),
888 for user_group in UserGroupList(UserGroupModel().get_all(),
889 perm_set=_perms, extra_kwargs=extras):
889 perm_set=_perms, extra_kwargs=extras):
890 result.append(user_group.get_api_data())
890 result.append(user_group.get_api_data())
891 return result
891 return result
892
892
893 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
893 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
894 def create_user_group(self, apiuser, group_name, description=Optional(''),
894 def create_user_group(self, apiuser, group_name, description=Optional(''),
895 owner=Optional(OAttr('apiuser')), active=Optional(True)):
895 owner=Optional(OAttr('apiuser')), active=Optional(True)):
896 """
896 """
897 Creates new user group. This command can be executed only using api_key
897 Creates new user group. This command can be executed only using api_key
898 belonging to user with admin rights or an user who has create user group
898 belonging to user with admin rights or an user who has create user group
899 permission
899 permission
900
900
901 :param apiuser: filled automatically from apikey
901 :param apiuser: filled automatically from apikey
902 :type apiuser: AuthUser
902 :type apiuser: AuthUser
903 :param group_name: name of new user group
903 :param group_name: name of new user group
904 :type group_name: str
904 :type group_name: str
905 :param description: group description
905 :param description: group description
906 :type description: str
906 :type description: str
907 :param owner: owner of group. If not passed apiuser is the owner
907 :param owner: owner of group. If not passed apiuser is the owner
908 :type owner: Optional(str or int)
908 :type owner: Optional(str or int)
909 :param active: group is active
909 :param active: group is active
910 :type active: Optional(bool)
910 :type active: Optional(bool)
911
911
912 OUTPUT::
912 OUTPUT::
913
913
914 id : <id_given_in_input>
914 id : <id_given_in_input>
915 result: {
915 result: {
916 "msg": "created new user group `<groupname>`",
916 "msg": "created new user group `<groupname>`",
917 "user_group": <user_group_object>
917 "user_group": <user_group_object>
918 }
918 }
919 error: null
919 error: null
920
920
921 ERROR OUTPUT::
921 ERROR OUTPUT::
922
922
923 id : <id_given_in_input>
923 id : <id_given_in_input>
924 result : null
924 result : null
925 error : {
925 error : {
926 "user group `<group name>` already exist"
926 "user group `<group name>` already exist"
927 or
927 or
928 "failed to create group `<group name>`"
928 "failed to create group `<group name>`"
929 }
929 }
930
930
931 """
931 """
932
932
933 if UserGroupModel().get_by_name(group_name):
933 if UserGroupModel().get_by_name(group_name):
934 raise JSONRPCError("user group `%s` already exist" % (group_name,))
934 raise JSONRPCError("user group `%s` already exist" % (group_name,))
935
935
936 try:
936 try:
937 if isinstance(owner, Optional):
937 if isinstance(owner, Optional):
938 owner = apiuser.user_id
938 owner = apiuser.user_id
939
939
940 owner = get_user_or_error(owner)
940 owner = get_user_or_error(owner)
941 active = Optional.extract(active)
941 active = Optional.extract(active)
942 description = Optional.extract(description)
942 description = Optional.extract(description)
943 ug = UserGroupModel().create(name=group_name, description=description,
943 ug = UserGroupModel().create(name=group_name, description=description,
944 owner=owner, active=active)
944 owner=owner, active=active)
945 Session().commit()
945 Session().commit()
946 return dict(
946 return dict(
947 msg='created new user group `%s`' % group_name,
947 msg='created new user group `%s`' % group_name,
948 user_group=ug.get_api_data()
948 user_group=ug.get_api_data()
949 )
949 )
950 except Exception:
950 except Exception:
951 log.error(traceback.format_exc())
951 log.error(traceback.format_exc())
952 raise JSONRPCError('failed to create group `%s`' % (group_name,))
952 raise JSONRPCError('failed to create group `%s`' % (group_name,))
953
953
954 # permission check inside
954 # permission check inside
955 def update_user_group(self, apiuser, usergroupid, group_name=Optional(''),
955 def update_user_group(self, apiuser, usergroupid, group_name=Optional(''),
956 description=Optional(''), owner=Optional(None),
956 description=Optional(''), owner=Optional(None),
957 active=Optional(True)):
957 active=Optional(True)):
958 """
958 """
959 Updates given usergroup. This command can be executed only using api_key
959 Updates given usergroup. This command can be executed only using api_key
960 belonging to user with admin rights or an admin of given user group
960 belonging to user with admin rights or an admin of given user group
961
961
962 :param apiuser: filled automatically from apikey
962 :param apiuser: filled automatically from apikey
963 :type apiuser: AuthUser
963 :type apiuser: AuthUser
964 :param usergroupid: id of user group to update
964 :param usergroupid: id of user group to update
965 :type usergroupid: str or int
965 :type usergroupid: str or int
966 :param group_name: name of new user group
966 :param group_name: name of new user group
967 :type group_name: str
967 :type group_name: str
968 :param description: group description
968 :param description: group description
969 :type description: str
969 :type description: str
970 :param owner: owner of group.
970 :param owner: owner of group.
971 :type owner: Optional(str or int)
971 :type owner: Optional(str or int)
972 :param active: group is active
972 :param active: group is active
973 :type active: Optional(bool)
973 :type active: Optional(bool)
974
974
975 OUTPUT::
975 OUTPUT::
976
976
977 id : <id_given_in_input>
977 id : <id_given_in_input>
978 result : {
978 result : {
979 "msg": 'updated user group ID:<user group id> <user group name>',
979 "msg": 'updated user group ID:<user group id> <user group name>',
980 "user_group": <user_group_object>
980 "user_group": <user_group_object>
981 }
981 }
982 error : null
982 error : null
983
983
984 ERROR OUTPUT::
984 ERROR OUTPUT::
985
985
986 id : <id_given_in_input>
986 id : <id_given_in_input>
987 result : null
987 result : null
988 error : {
988 error : {
989 "failed to update user group `<user group name>`"
989 "failed to update user group `<user group name>`"
990 }
990 }
991
991
992 """
992 """
993 user_group = get_user_group_or_error(usergroupid)
993 user_group = get_user_group_or_error(usergroupid)
994 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
994 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
995 # check if we have admin permission for this user group !
995 # check if we have admin permission for this user group !
996 _perms = ('usergroup.admin',)
996 _perms = ('usergroup.admin',)
997 if not HasUserGroupPermissionAny(*_perms)(
997 if not HasUserGroupPermissionAny(*_perms)(
998 user=apiuser, user_group_name=user_group.users_group_name):
998 user=apiuser, user_group_name=user_group.users_group_name):
999 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
999 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1000
1000
1001 if not isinstance(owner, Optional):
1001 if not isinstance(owner, Optional):
1002 owner = get_user_or_error(owner)
1002 owner = get_user_or_error(owner)
1003
1003
1004 updates = {}
1004 updates = {}
1005 store_update(updates, group_name, 'users_group_name')
1005 store_update(updates, group_name, 'users_group_name')
1006 store_update(updates, description, 'user_group_description')
1006 store_update(updates, description, 'user_group_description')
1007 store_update(updates, owner, 'user')
1007 store_update(updates, owner, 'user')
1008 store_update(updates, active, 'users_group_active')
1008 store_update(updates, active, 'users_group_active')
1009 try:
1009 try:
1010 UserGroupModel().update(user_group, updates)
1010 UserGroupModel().update(user_group, updates)
1011 Session().commit()
1011 Session().commit()
1012 return dict(
1012 return dict(
1013 msg='updated user group ID:%s %s' % (user_group.users_group_id,
1013 msg='updated user group ID:%s %s' % (user_group.users_group_id,
1014 user_group.users_group_name),
1014 user_group.users_group_name),
1015 user_group=user_group.get_api_data()
1015 user_group=user_group.get_api_data()
1016 )
1016 )
1017 except Exception:
1017 except Exception:
1018 log.error(traceback.format_exc())
1018 log.error(traceback.format_exc())
1019 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
1019 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
1020
1020
1021 # permission check inside
1021 # permission check inside
1022 def delete_user_group(self, apiuser, usergroupid):
1022 def delete_user_group(self, apiuser, usergroupid):
1023 """
1023 """
1024 Delete given user group by user group id or name.
1024 Delete given user group by user group id or name.
1025 This command can be executed only using api_key
1025 This command can be executed only using api_key
1026 belonging to user with admin rights or an admin of given user group
1026 belonging to user with admin rights or an admin of given user group
1027
1027
1028 :param apiuser: filled automatically from apikey
1028 :param apiuser: filled automatically from apikey
1029 :type apiuser: AuthUser
1029 :type apiuser: AuthUser
1030 :param usergroupid:
1030 :param usergroupid:
1031 :type usergroupid: int
1031 :type usergroupid: int
1032
1032
1033 OUTPUT::
1033 OUTPUT::
1034
1034
1035 id : <id_given_in_input>
1035 id : <id_given_in_input>
1036 result : {
1036 result : {
1037 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
1037 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
1038 }
1038 }
1039 error : null
1039 error : null
1040
1040
1041 ERROR OUTPUT::
1041 ERROR OUTPUT::
1042
1042
1043 id : <id_given_in_input>
1043 id : <id_given_in_input>
1044 result : null
1044 result : null
1045 error : {
1045 error : {
1046 "failed to delete user group ID:<user_group_id> <user_group_name>"
1046 "failed to delete user group ID:<user_group_id> <user_group_name>"
1047 or
1047 or
1048 "RepoGroup assigned to <repo_groups_list>"
1048 "RepoGroup assigned to <repo_groups_list>"
1049 }
1049 }
1050
1050
1051 """
1051 """
1052 user_group = get_user_group_or_error(usergroupid)
1052 user_group = get_user_group_or_error(usergroupid)
1053 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1053 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1054 # check if we have admin permission for this user group !
1054 # check if we have admin permission for this user group !
1055 _perms = ('usergroup.admin',)
1055 _perms = ('usergroup.admin',)
1056 if not HasUserGroupPermissionAny(*_perms)(
1056 if not HasUserGroupPermissionAny(*_perms)(
1057 user=apiuser, user_group_name=user_group.users_group_name):
1057 user=apiuser, user_group_name=user_group.users_group_name):
1058 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1058 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1059
1059
1060 try:
1060 try:
1061 UserGroupModel().delete(user_group)
1061 UserGroupModel().delete(user_group)
1062 Session().commit()
1062 Session().commit()
1063 return dict(
1063 return dict(
1064 msg='deleted user group ID:%s %s' %
1064 msg='deleted user group ID:%s %s' %
1065 (user_group.users_group_id, user_group.users_group_name),
1065 (user_group.users_group_id, user_group.users_group_name),
1066 user_group=None
1066 user_group=None
1067 )
1067 )
1068 except UserGroupsAssignedException as e:
1068 except UserGroupsAssignedException as e:
1069 log.error(traceback.format_exc())
1069 log.error(traceback.format_exc())
1070 raise JSONRPCError(str(e))
1070 raise JSONRPCError(str(e))
1071 except Exception:
1071 except Exception:
1072 log.error(traceback.format_exc())
1072 log.error(traceback.format_exc())
1073 raise JSONRPCError('failed to delete user group ID:%s %s' %
1073 raise JSONRPCError('failed to delete user group ID:%s %s' %
1074 (user_group.users_group_id,
1074 (user_group.users_group_id,
1075 user_group.users_group_name)
1075 user_group.users_group_name)
1076 )
1076 )
1077
1077
1078 # permission check inside
1078 # permission check inside
1079 def add_user_to_user_group(self, apiuser, usergroupid, userid):
1079 def add_user_to_user_group(self, apiuser, usergroupid, userid):
1080 """
1080 """
1081 Adds a user to a user group. If user exists in that group success will be
1081 Adds a user to a user group. If user exists in that group success will be
1082 `false`. This command can be executed only using api_key
1082 `false`. This command can be executed only using api_key
1083 belonging to user with admin rights or an admin of given user group
1083 belonging to user with admin rights or an admin of given user group
1084
1084
1085 :param apiuser: filled automatically from apikey
1085 :param apiuser: filled automatically from apikey
1086 :type apiuser: AuthUser
1086 :type apiuser: AuthUser
1087 :param usergroupid:
1087 :param usergroupid:
1088 :type usergroupid: int
1088 :type usergroupid: int
1089 :param userid:
1089 :param userid:
1090 :type userid: int
1090 :type userid: int
1091
1091
1092 OUTPUT::
1092 OUTPUT::
1093
1093
1094 id : <id_given_in_input>
1094 id : <id_given_in_input>
1095 result : {
1095 result : {
1096 "success": True|False # depends on if member is in group
1096 "success": True|False # depends on if member is in group
1097 "msg": "added member `<username>` to user group `<groupname>` |
1097 "msg": "added member `<username>` to user group `<groupname>` |
1098 User is already in that group"
1098 User is already in that group"
1099
1099
1100 }
1100 }
1101 error : null
1101 error : null
1102
1102
1103 ERROR OUTPUT::
1103 ERROR OUTPUT::
1104
1104
1105 id : <id_given_in_input>
1105 id : <id_given_in_input>
1106 result : null
1106 result : null
1107 error : {
1107 error : {
1108 "failed to add member to user group `<user_group_name>`"
1108 "failed to add member to user group `<user_group_name>`"
1109 }
1109 }
1110
1110
1111 """
1111 """
1112 user = get_user_or_error(userid)
1112 user = get_user_or_error(userid)
1113 user_group = get_user_group_or_error(usergroupid)
1113 user_group = get_user_group_or_error(usergroupid)
1114 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1114 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1115 # check if we have admin permission for this user group !
1115 # check if we have admin permission for this user group !
1116 _perms = ('usergroup.admin',)
1116 _perms = ('usergroup.admin',)
1117 if not HasUserGroupPermissionAny(*_perms)(
1117 if not HasUserGroupPermissionAny(*_perms)(
1118 user=apiuser, user_group_name=user_group.users_group_name):
1118 user=apiuser, user_group_name=user_group.users_group_name):
1119 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1119 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1120
1120
1121 try:
1121 try:
1122 ugm = UserGroupModel().add_user_to_group(user_group, user)
1122 ugm = UserGroupModel().add_user_to_group(user_group, user)
1123 success = True if ugm != True else False
1123 success = True if ugm != True else False
1124 msg = 'added member `%s` to user group `%s`' % (
1124 msg = 'added member `%s` to user group `%s`' % (
1125 user.username, user_group.users_group_name
1125 user.username, user_group.users_group_name
1126 )
1126 )
1127 msg = msg if success else 'User is already in that group'
1127 msg = msg if success else 'User is already in that group'
1128 Session().commit()
1128 Session().commit()
1129
1129
1130 return dict(
1130 return dict(
1131 success=success,
1131 success=success,
1132 msg=msg
1132 msg=msg
1133 )
1133 )
1134 except Exception:
1134 except Exception:
1135 log.error(traceback.format_exc())
1135 log.error(traceback.format_exc())
1136 raise JSONRPCError(
1136 raise JSONRPCError(
1137 'failed to add member to user group `%s`' % (
1137 'failed to add member to user group `%s`' % (
1138 user_group.users_group_name,
1138 user_group.users_group_name,
1139 )
1139 )
1140 )
1140 )
1141
1141
1142 # permission check inside
1142 # permission check inside
1143 def remove_user_from_user_group(self, apiuser, usergroupid, userid):
1143 def remove_user_from_user_group(self, apiuser, usergroupid, userid):
1144 """
1144 """
1145 Removes a user from a user group. If user is not in given group success will
1145 Removes a user from a user group. If user is not in given group success will
1146 be `false`. This command can be executed only
1146 be `false`. This command can be executed only
1147 using api_key belonging to user with admin rights or an admin of given user group
1147 using api_key belonging to user with admin rights or an admin of given user group
1148
1148
1149 :param apiuser: filled automatically from apikey
1149 :param apiuser: filled automatically from apikey
1150 :type apiuser: AuthUser
1150 :type apiuser: AuthUser
1151 :param usergroupid:
1151 :param usergroupid:
1152 :param userid:
1152 :param userid:
1153
1153
1154
1154
1155 OUTPUT::
1155 OUTPUT::
1156
1156
1157 id : <id_given_in_input>
1157 id : <id_given_in_input>
1158 result: {
1158 result: {
1159 "success": True|False, # depends on if member is in group
1159 "success": True|False, # depends on if member is in group
1160 "msg": "removed member <username> from user group <groupname> |
1160 "msg": "removed member <username> from user group <groupname> |
1161 User wasn't in group"
1161 User wasn't in group"
1162 }
1162 }
1163 error: null
1163 error: null
1164
1164
1165 """
1165 """
1166 user = get_user_or_error(userid)
1166 user = get_user_or_error(userid)
1167 user_group = get_user_group_or_error(usergroupid)
1167 user_group = get_user_group_or_error(usergroupid)
1168 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1168 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1169 # check if we have admin permission for this user group !
1169 # check if we have admin permission for this user group !
1170 _perms = ('usergroup.admin',)
1170 _perms = ('usergroup.admin',)
1171 if not HasUserGroupPermissionAny(*_perms)(
1171 if not HasUserGroupPermissionAny(*_perms)(
1172 user=apiuser, user_group_name=user_group.users_group_name):
1172 user=apiuser, user_group_name=user_group.users_group_name):
1173 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1173 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1174
1174
1175 try:
1175 try:
1176 success = UserGroupModel().remove_user_from_group(user_group, user)
1176 success = UserGroupModel().remove_user_from_group(user_group, user)
1177 msg = 'removed member `%s` from user group `%s`' % (
1177 msg = 'removed member `%s` from user group `%s`' % (
1178 user.username, user_group.users_group_name
1178 user.username, user_group.users_group_name
1179 )
1179 )
1180 msg = msg if success else "User wasn't in group"
1180 msg = msg if success else "User wasn't in group"
1181 Session().commit()
1181 Session().commit()
1182 return dict(success=success, msg=msg)
1182 return dict(success=success, msg=msg)
1183 except Exception:
1183 except Exception:
1184 log.error(traceback.format_exc())
1184 log.error(traceback.format_exc())
1185 raise JSONRPCError(
1185 raise JSONRPCError(
1186 'failed to remove member from user group `%s`' % (
1186 'failed to remove member from user group `%s`' % (
1187 user_group.users_group_name,
1187 user_group.users_group_name,
1188 )
1188 )
1189 )
1189 )
1190
1190
1191 # permission check inside
1191 # permission check inside
1192 def get_repo(self, apiuser, repoid):
1192 def get_repo(self, apiuser, repoid):
1193 """
1193 """
1194 Gets an existing repository by it's name or repository_id. Members will return
1194 Gets an existing repository by it's name or repository_id. Members will return
1195 either users_group or user associated to that repository. This command can be
1195 either users_group or user associated to that repository. This command can be
1196 executed only using api_key belonging to user with admin
1196 executed only using api_key belonging to user with admin
1197 rights or regular user that have at least read access to repository.
1197 rights or regular user that have at least read access to repository.
1198
1198
1199 :param apiuser: filled automatically from apikey
1199 :param apiuser: filled automatically from apikey
1200 :type apiuser: AuthUser
1200 :type apiuser: AuthUser
1201 :param repoid: repository name or repository id
1201 :param repoid: repository name or repository id
1202 :type repoid: str or int
1202 :type repoid: str or int
1203
1203
1204 OUTPUT::
1204 OUTPUT::
1205
1205
1206 id : <id_given_in_input>
1206 id : <id_given_in_input>
1207 result : {
1207 result : {
1208 {
1208 {
1209 "repo_id" : "<repo_id>",
1209 "repo_id" : "<repo_id>",
1210 "repo_name" : "<reponame>"
1210 "repo_name" : "<reponame>"
1211 "repo_type" : "<repo_type>",
1211 "repo_type" : "<repo_type>",
1212 "clone_uri" : "<clone_uri>",
1212 "clone_uri" : "<clone_uri>",
1213 "enable_downloads": "<bool>",
1213 "enable_downloads": "<bool>",
1214 "enable_locking": "<bool>",
1214 "enable_locking": "<bool>",
1215 "enable_statistics": "<bool>",
1215 "enable_statistics": "<bool>",
1216 "private": "<bool>",
1216 "private": "<bool>",
1217 "created_on" : "<date_time_created>",
1217 "created_on" : "<date_time_created>",
1218 "description" : "<description>",
1218 "description" : "<description>",
1219 "landing_rev": "<landing_rev>",
1219 "landing_rev": "<landing_rev>",
1220 "last_changeset": {
1220 "last_changeset": {
1221 "author": "<full_author>",
1221 "author": "<full_author>",
1222 "date": "<date_time_of_commit>",
1222 "date": "<date_time_of_commit>",
1223 "message": "<commit_message>",
1223 "message": "<commit_message>",
1224 "raw_id": "<raw_id>",
1224 "raw_id": "<raw_id>",
1225 "revision": "<numeric_revision>",
1225 "revision": "<numeric_revision>",
1226 "short_id": "<short_id>"
1226 "short_id": "<short_id>"
1227 }
1227 }
1228 "owner": "<repo_owner>",
1228 "owner": "<repo_owner>",
1229 "fork_of": "<name_of_fork_parent>",
1229 "fork_of": "<name_of_fork_parent>",
1230 "members" : [
1230 "members" : [
1231 {
1231 {
1232 "name": "<username>",
1232 "name": "<username>",
1233 "type" : "user",
1233 "type" : "user",
1234 "permission" : "repository.(read|write|admin)"
1234 "permission" : "repository.(read|write|admin)"
1235 },
1235 },
1236
1236
1237 {
1237 {
1238 "name": "<usergroup name>",
1238 "name": "<usergroup name>",
1239 "type" : "user_group",
1239 "type" : "user_group",
1240 "permission" : "usergroup.(read|write|admin)"
1240 "permission" : "usergroup.(read|write|admin)"
1241 },
1241 },
1242
1242
1243 ]
1243 ]
1244 "followers": [<user_obj>, ...]
1244 "followers": [<user_obj>, ...]
1245 ]
1245 ]
1246 }
1246 }
1247 }
1247 }
1248 error : null
1248 error : null
1249
1249
1250 """
1250 """
1251 repo = get_repo_or_error(repoid)
1251 repo = get_repo_or_error(repoid)
1252
1252
1253 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1253 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1254 # check if we have admin permission for this repo !
1254 # check if we have admin permission for this repo !
1255 perms = ('repository.admin', 'repository.write', 'repository.read')
1255 perms = ('repository.admin', 'repository.write', 'repository.read')
1256 if not HasRepoPermissionAnyApi(*perms)(user=apiuser, repo_name=repo.repo_name):
1256 if not HasRepoPermissionAnyApi(*perms)(user=apiuser, repo_name=repo.repo_name):
1257 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1257 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1258
1258
1259 members = []
1259 members = []
1260 followers = []
1260 followers = []
1261 for user in repo.repo_to_perm:
1261 for user in repo.repo_to_perm:
1262 perm = user.permission.permission_name
1262 perm = user.permission.permission_name
1263 user = user.user
1263 user = user.user
1264 user_data = {
1264 user_data = {
1265 'name': user.username,
1265 'name': user.username,
1266 'type': "user",
1266 'type': "user",
1267 'permission': perm
1267 'permission': perm
1268 }
1268 }
1269 members.append(user_data)
1269 members.append(user_data)
1270
1270
1271 for user_group in repo.users_group_to_perm:
1271 for user_group in repo.users_group_to_perm:
1272 perm = user_group.permission.permission_name
1272 perm = user_group.permission.permission_name
1273 user_group = user_group.users_group
1273 user_group = user_group.users_group
1274 user_group_data = {
1274 user_group_data = {
1275 'name': user_group.users_group_name,
1275 'name': user_group.users_group_name,
1276 'type': "user_group",
1276 'type': "user_group",
1277 'permission': perm
1277 'permission': perm
1278 }
1278 }
1279 members.append(user_group_data)
1279 members.append(user_group_data)
1280
1280
1281 for user in repo.followers:
1281 for user in repo.followers:
1282 followers.append(user.user.get_api_data())
1282 followers.append(user.user.get_api_data())
1283
1283
1284 data = repo.get_api_data()
1284 data = repo.get_api_data()
1285 data['members'] = members
1285 data['members'] = members
1286 data['followers'] = followers
1286 data['followers'] = followers
1287 return data
1287 return data
1288
1288
1289 # permission check inside
1289 # permission check inside
1290 def get_repos(self, apiuser):
1290 def get_repos(self, apiuser):
1291 """
1291 """
1292 Lists all existing repositories. This command can be executed only using
1292 Lists all existing repositories. This command can be executed only using
1293 api_key belonging to user with admin rights or regular user that have
1293 api_key belonging to user with admin rights or regular user that have
1294 admin, write or read access to repository.
1294 admin, write or read access to repository.
1295
1295
1296 :param apiuser: filled automatically from apikey
1296 :param apiuser: filled automatically from apikey
1297 :type apiuser: AuthUser
1297 :type apiuser: AuthUser
1298
1298
1299 OUTPUT::
1299 OUTPUT::
1300
1300
1301 id : <id_given_in_input>
1301 id : <id_given_in_input>
1302 result: [
1302 result: [
1303 {
1303 {
1304 "repo_id" : "<repo_id>",
1304 "repo_id" : "<repo_id>",
1305 "repo_name" : "<reponame>"
1305 "repo_name" : "<reponame>"
1306 "repo_type" : "<repo_type>",
1306 "repo_type" : "<repo_type>",
1307 "clone_uri" : "<clone_uri>",
1307 "clone_uri" : "<clone_uri>",
1308 "private": : "<bool>",
1308 "private": : "<bool>",
1309 "created_on" : "<datetimecreated>",
1309 "created_on" : "<datetimecreated>",
1310 "description" : "<description>",
1310 "description" : "<description>",
1311 "landing_rev": "<landing_rev>",
1311 "landing_rev": "<landing_rev>",
1312 "owner": "<repo_owner>",
1312 "owner": "<repo_owner>",
1313 "fork_of": "<name_of_fork_parent>",
1313 "fork_of": "<name_of_fork_parent>",
1314 "enable_downloads": "<bool>",
1314 "enable_downloads": "<bool>",
1315 "enable_locking": "<bool>",
1315 "enable_locking": "<bool>",
1316 "enable_statistics": "<bool>",
1316 "enable_statistics": "<bool>",
1317 },
1317 },
1318
1318
1319 ]
1319 ]
1320 error: null
1320 error: null
1321 """
1321 """
1322 result = []
1322 result = []
1323 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1323 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1324 repos = RepoModel().get_all_user_repos(user=apiuser)
1324 repos = RepoModel().get_all_user_repos(user=apiuser)
1325 else:
1325 else:
1326 repos = RepoModel().get_all()
1326 repos = RepoModel().get_all()
1327
1327
1328 for repo in repos:
1328 for repo in repos:
1329 result.append(repo.get_api_data())
1329 result.append(repo.get_api_data())
1330 return result
1330 return result
1331
1331
1332 # permission check inside
1332 # permission check inside
1333 def get_repo_nodes(self, apiuser, repoid, revision, root_path,
1333 def get_repo_nodes(self, apiuser, repoid, revision, root_path,
1334 ret_type=Optional('all')):
1334 ret_type=Optional('all')):
1335 """
1335 """
1336 returns a list of nodes and it's children in a flat list for a given path
1336 returns a list of nodes and it's children in a flat list for a given path
1337 at given revision. It's possible to specify ret_type to show only `files` or
1337 at given revision. It's possible to specify ret_type to show only `files` or
1338 `dirs`. This command can be executed only using api_key belonging to
1338 `dirs`. This command can be executed only using api_key belonging to
1339 user with admin rights or regular user that have at least read access to repository.
1339 user with admin rights or regular user that have at least read access to repository.
1340
1340
1341 :param apiuser: filled automatically from apikey
1341 :param apiuser: filled automatically from apikey
1342 :type apiuser: AuthUser
1342 :type apiuser: AuthUser
1343 :param repoid: repository name or repository id
1343 :param repoid: repository name or repository id
1344 :type repoid: str or int
1344 :type repoid: str or int
1345 :param revision: revision for which listing should be done
1345 :param revision: revision for which listing should be done
1346 :type revision: str
1346 :type revision: str
1347 :param root_path: path from which start displaying
1347 :param root_path: path from which start displaying
1348 :type root_path: str
1348 :type root_path: str
1349 :param ret_type: return type 'all|files|dirs' nodes
1349 :param ret_type: return type 'all|files|dirs' nodes
1350 :type ret_type: Optional(str)
1350 :type ret_type: Optional(str)
1351
1351
1352
1352
1353 OUTPUT::
1353 OUTPUT::
1354
1354
1355 id : <id_given_in_input>
1355 id : <id_given_in_input>
1356 result: [
1356 result: [
1357 {
1357 {
1358 "name" : "<name>"
1358 "name" : "<name>"
1359 "type" : "<type>",
1359 "type" : "<type>",
1360 },
1360 },
1361
1361
1362 ]
1362 ]
1363 error: null
1363 error: null
1364 """
1364 """
1365 repo = get_repo_or_error(repoid)
1365 repo = get_repo_or_error(repoid)
1366
1366
1367 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1367 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1368 # check if we have admin permission for this repo !
1368 # check if we have admin permission for this repo !
1369 perms = ('repository.admin', 'repository.write', 'repository.read')
1369 perms = ('repository.admin', 'repository.write', 'repository.read')
1370 if not HasRepoPermissionAnyApi(*perms)(user=apiuser, repo_name=repo.repo_name):
1370 if not HasRepoPermissionAnyApi(*perms)(user=apiuser, repo_name=repo.repo_name):
1371 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1371 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1372
1372
1373 ret_type = Optional.extract(ret_type)
1373 ret_type = Optional.extract(ret_type)
1374 _map = {}
1374 _map = {}
1375 try:
1375 try:
1376 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1376 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1377 flat=False)
1377 flat=False)
1378 _map = {
1378 _map = {
1379 'all': _d + _f,
1379 'all': _d + _f,
1380 'files': _f,
1380 'files': _f,
1381 'dirs': _d,
1381 'dirs': _d,
1382 }
1382 }
1383 return _map[ret_type]
1383 return _map[ret_type]
1384 except KeyError:
1384 except KeyError:
1385 raise JSONRPCError('ret_type must be one of %s'
1385 raise JSONRPCError('ret_type must be one of %s'
1386 % (','.join(_map.keys())))
1386 % (','.join(_map.keys())))
1387 except Exception:
1387 except Exception:
1388 log.error(traceback.format_exc())
1388 log.error(traceback.format_exc())
1389 raise JSONRPCError(
1389 raise JSONRPCError(
1390 'failed to get repo: `%s` nodes' % repo.repo_name
1390 'failed to get repo: `%s` nodes' % repo.repo_name
1391 )
1391 )
1392
1392
1393 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1393 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1394 def create_repo(self, apiuser, repo_name, owner=Optional(OAttr('apiuser')),
1394 def create_repo(self, apiuser, repo_name, owner=Optional(OAttr('apiuser')),
1395 repo_type=Optional('hg'), description=Optional(''),
1395 repo_type=Optional('hg'), description=Optional(''),
1396 private=Optional(False), clone_uri=Optional(None),
1396 private=Optional(False), clone_uri=Optional(None),
1397 landing_rev=Optional('rev:tip'),
1397 landing_rev=Optional('rev:tip'),
1398 enable_statistics=Optional(False),
1398 enable_statistics=Optional(False),
1399 enable_locking=Optional(False),
1399 enable_locking=Optional(False),
1400 enable_downloads=Optional(False),
1400 enable_downloads=Optional(False),
1401 copy_permissions=Optional(False)):
1401 copy_permissions=Optional(False)):
1402 """
1402 """
1403 Creates a repository. If repository name contains "/", all needed repository
1403 Creates a repository. If repository name contains "/", all needed repository
1404 groups will be created. For example "foo/bar/baz" will create groups
1404 groups will be created. For example "foo/bar/baz" will create groups
1405 "foo", "bar" (with "foo" as parent), and create "baz" repository with
1405 "foo", "bar" (with "foo" as parent), and create "baz" repository with
1406 "bar" as group. This command can be executed only using api_key
1406 "bar" as group. This command can be executed only using api_key
1407 belonging to user with admin rights or regular user that have create
1407 belonging to user with admin rights or regular user that have create
1408 repository permission. Regular users cannot specify owner parameter
1408 repository permission. Regular users cannot specify owner parameter
1409
1409
1410 :param apiuser: filled automatically from apikey
1410 :param apiuser: filled automatically from apikey
1411 :type apiuser: AuthUser
1411 :type apiuser: AuthUser
1412 :param repo_name: repository name
1412 :param repo_name: repository name
1413 :type repo_name: str
1413 :type repo_name: str
1414 :param owner: user_id or username
1414 :param owner: user_id or username
1415 :type owner: Optional(str)
1415 :type owner: Optional(str)
1416 :param repo_type: 'hg' or 'git'
1416 :param repo_type: 'hg' or 'git'
1417 :type repo_type: Optional(str)
1417 :type repo_type: Optional(str)
1418 :param description: repository description
1418 :param description: repository description
1419 :type description: Optional(str)
1419 :type description: Optional(str)
1420 :param private:
1420 :param private:
1421 :type private: bool
1421 :type private: bool
1422 :param clone_uri:
1422 :param clone_uri:
1423 :type clone_uri: str
1423 :type clone_uri: str
1424 :param landing_rev: <rev_type>:<rev>
1424 :param landing_rev: <rev_type>:<rev>
1425 :type landing_rev: str
1425 :type landing_rev: str
1426 :param enable_locking:
1426 :param enable_locking:
1427 :type enable_locking: bool
1427 :type enable_locking: bool
1428 :param enable_downloads:
1428 :param enable_downloads:
1429 :type enable_downloads: bool
1429 :type enable_downloads: bool
1430 :param enable_statistics:
1430 :param enable_statistics:
1431 :type enable_statistics: bool
1431 :type enable_statistics: bool
1432 :param copy_permissions: Copy permission from group that repository is
1432 :param copy_permissions: Copy permission from group that repository is
1433 being created.
1433 being created.
1434 :type copy_permissions: bool
1434 :type copy_permissions: bool
1435
1435
1436 OUTPUT::
1436 OUTPUT::
1437
1437
1438 id : <id_given_in_input>
1438 id : <id_given_in_input>
1439 result: {
1439 result: {
1440 "msg": "Created new repository `<reponame>`",
1440 "msg": "Created new repository `<reponame>`",
1441 "success": true,
1441 "success": true,
1442 "task": "<celery task id or None if done sync>"
1442 "task": "<celery task id or None if done sync>"
1443 }
1443 }
1444 error: null
1444 error: null
1445
1445
1446 ERROR OUTPUT::
1446 ERROR OUTPUT::
1447
1447
1448 id : <id_given_in_input>
1448 id : <id_given_in_input>
1449 result : null
1449 result : null
1450 error : {
1450 error : {
1451 'failed to create repository `<repo_name>`
1451 'failed to create repository `<repo_name>`
1452 }
1452 }
1453
1453
1454 """
1454 """
1455 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1455 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1456 if not isinstance(owner, Optional):
1456 if not isinstance(owner, Optional):
1457 # forbid setting owner for non-admins
1457 # forbid setting owner for non-admins
1458 raise JSONRPCError(
1458 raise JSONRPCError(
1459 'Only Kallithea admin can specify `owner` param'
1459 'Only Kallithea admin can specify `owner` param'
1460 )
1460 )
1461 if isinstance(owner, Optional):
1461 if isinstance(owner, Optional):
1462 owner = apiuser.user_id
1462 owner = apiuser.user_id
1463
1463
1464 owner = get_user_or_error(owner)
1464 owner = get_user_or_error(owner)
1465
1465
1466 if RepoModel().get_by_repo_name(repo_name):
1466 if RepoModel().get_by_repo_name(repo_name):
1467 raise JSONRPCError("repo `%s` already exist" % repo_name)
1467 raise JSONRPCError("repo `%s` already exist" % repo_name)
1468
1468
1469 defs = Setting.get_default_repo_settings(strip_prefix=True)
1469 defs = Setting.get_default_repo_settings(strip_prefix=True)
1470 if isinstance(private, Optional):
1470 if isinstance(private, Optional):
1471 private = defs.get('repo_private') or Optional.extract(private)
1471 private = defs.get('repo_private') or Optional.extract(private)
1472 if isinstance(repo_type, Optional):
1472 if isinstance(repo_type, Optional):
1473 repo_type = defs.get('repo_type')
1473 repo_type = defs.get('repo_type')
1474 if isinstance(enable_statistics, Optional):
1474 if isinstance(enable_statistics, Optional):
1475 enable_statistics = defs.get('repo_enable_statistics')
1475 enable_statistics = defs.get('repo_enable_statistics')
1476 if isinstance(enable_locking, Optional):
1476 if isinstance(enable_locking, Optional):
1477 enable_locking = defs.get('repo_enable_locking')
1477 enable_locking = defs.get('repo_enable_locking')
1478 if isinstance(enable_downloads, Optional):
1478 if isinstance(enable_downloads, Optional):
1479 enable_downloads = defs.get('repo_enable_downloads')
1479 enable_downloads = defs.get('repo_enable_downloads')
1480
1480
1481 clone_uri = Optional.extract(clone_uri)
1481 clone_uri = Optional.extract(clone_uri)
1482 description = Optional.extract(description)
1482 description = Optional.extract(description)
1483 landing_rev = Optional.extract(landing_rev)
1483 landing_rev = Optional.extract(landing_rev)
1484 copy_permissions = Optional.extract(copy_permissions)
1484 copy_permissions = Optional.extract(copy_permissions)
1485
1485
1486 try:
1486 try:
1487 repo_name_cleaned = repo_name.split('/')[-1]
1487 repo_name_cleaned = repo_name.split('/')[-1]
1488 # create structure of groups and return the last group
1488 # create structure of groups and return the last group
1489 repo_group = map_groups(repo_name)
1489 repo_group = map_groups(repo_name)
1490 data = dict(
1490 data = dict(
1491 repo_name=repo_name_cleaned,
1491 repo_name=repo_name_cleaned,
1492 repo_name_full=repo_name,
1492 repo_name_full=repo_name,
1493 repo_type=repo_type,
1493 repo_type=repo_type,
1494 repo_description=description,
1494 repo_description=description,
1495 owner=owner,
1495 owner=owner,
1496 repo_private=private,
1496 repo_private=private,
1497 clone_uri=clone_uri,
1497 clone_uri=clone_uri,
1498 repo_group=repo_group,
1498 repo_group=repo_group,
1499 repo_landing_rev=landing_rev,
1499 repo_landing_rev=landing_rev,
1500 enable_statistics=enable_statistics,
1500 enable_statistics=enable_statistics,
1501 enable_locking=enable_locking,
1501 enable_locking=enable_locking,
1502 enable_downloads=enable_downloads,
1502 enable_downloads=enable_downloads,
1503 repo_copy_permissions=copy_permissions,
1503 repo_copy_permissions=copy_permissions,
1504 )
1504 )
1505
1505
1506 task = RepoModel().create(form_data=data, cur_user=owner)
1506 task = RepoModel().create(form_data=data, cur_user=owner)
1507 from celery.result import BaseAsyncResult
1507 from celery.result import BaseAsyncResult
1508 task_id = None
1508 task_id = None
1509 if isinstance(task, BaseAsyncResult):
1509 if isinstance(task, BaseAsyncResult):
1510 task_id = task.task_id
1510 task_id = task.task_id
1511 # no commit, it's done in RepoModel, or async via celery
1511 # no commit, it's done in RepoModel, or async via celery
1512 return dict(
1512 return dict(
1513 msg="Created new repository `%s`" % (repo_name,),
1513 msg="Created new repository `%s`" % (repo_name,),
1514 success=True, # cannot return the repo data here since fork
1514 success=True, # cannot return the repo data here since fork
1515 # can be done async
1515 # can be done async
1516 task=task_id
1516 task=task_id
1517 )
1517 )
1518 except Exception:
1518 except Exception:
1519 log.error(traceback.format_exc())
1519 log.error(traceback.format_exc())
1520 raise JSONRPCError(
1520 raise JSONRPCError(
1521 'failed to create repository `%s`' % (repo_name,))
1521 'failed to create repository `%s`' % (repo_name,))
1522
1522
1523 # permission check inside
1523 # permission check inside
1524 def update_repo(self, apiuser, repoid, name=Optional(None),
1524 def update_repo(self, apiuser, repoid, name=Optional(None),
1525 owner=Optional(OAttr('apiuser')),
1525 owner=Optional(OAttr('apiuser')),
1526 group=Optional(None),
1526 group=Optional(None),
1527 description=Optional(''), private=Optional(False),
1527 description=Optional(''), private=Optional(False),
1528 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1528 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1529 enable_statistics=Optional(False),
1529 enable_statistics=Optional(False),
1530 enable_locking=Optional(False),
1530 enable_locking=Optional(False),
1531 enable_downloads=Optional(False)):
1531 enable_downloads=Optional(False)):
1532
1532
1533 """
1533 """
1534 Updates repo
1534 Updates repo
1535
1535
1536 :param apiuser: filled automatically from apikey
1536 :param apiuser: filled automatically from apikey
1537 :type apiuser: AuthUser
1537 :type apiuser: AuthUser
1538 :param repoid: repository name or repository id
1538 :param repoid: repository name or repository id
1539 :type repoid: str or int
1539 :type repoid: str or int
1540 :param name:
1540 :param name:
1541 :param owner:
1541 :param owner:
1542 :param group:
1542 :param group:
1543 :param description:
1543 :param description:
1544 :param private:
1544 :param private:
1545 :param clone_uri:
1545 :param clone_uri:
1546 :param landing_rev:
1546 :param landing_rev:
1547 :param enable_statistics:
1547 :param enable_statistics:
1548 :param enable_locking:
1548 :param enable_locking:
1549 :param enable_downloads:
1549 :param enable_downloads:
1550 """
1550 """
1551 repo = get_repo_or_error(repoid)
1551 repo = get_repo_or_error(repoid)
1552 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1552 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1553 # check if we have admin permission for this repo !
1553 # check if we have admin permission for this repo !
1554 if not HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
1554 if not HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
1555 repo_name=repo.repo_name):
1555 repo_name=repo.repo_name):
1556 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1556 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1557
1557
1558 if (name != repo.repo_name and
1558 if (name != repo.repo_name and
1559 not HasPermissionAnyApi('hg.create.repository')(user=apiuser)
1559 not HasPermissionAnyApi('hg.create.repository')(user=apiuser)
1560 ):
1560 ):
1561 raise JSONRPCError('no permission to create (or move) repositories')
1561 raise JSONRPCError('no permission to create (or move) repositories')
1562
1562
1563 if not isinstance(owner, Optional):
1563 if not isinstance(owner, Optional):
1564 # forbid setting owner for non-admins
1564 # forbid setting owner for non-admins
1565 raise JSONRPCError(
1565 raise JSONRPCError(
1566 'Only Kallithea admin can specify `owner` param'
1566 'Only Kallithea admin can specify `owner` param'
1567 )
1567 )
1568
1568
1569 updates = {}
1569 updates = {}
1570 repo_group = group
1570 repo_group = group
1571 if not isinstance(repo_group, Optional):
1571 if not isinstance(repo_group, Optional):
1572 repo_group = get_repo_group_or_error(repo_group)
1572 repo_group = get_repo_group_or_error(repo_group)
1573 repo_group = repo_group.group_id
1573 repo_group = repo_group.group_id
1574 try:
1574 try:
1575 store_update(updates, name, 'repo_name')
1575 store_update(updates, name, 'repo_name')
1576 store_update(updates, repo_group, 'repo_group')
1576 store_update(updates, repo_group, 'repo_group')
1577 store_update(updates, owner, 'user')
1577 store_update(updates, owner, 'user')
1578 store_update(updates, description, 'repo_description')
1578 store_update(updates, description, 'repo_description')
1579 store_update(updates, private, 'repo_private')
1579 store_update(updates, private, 'repo_private')
1580 store_update(updates, clone_uri, 'clone_uri')
1580 store_update(updates, clone_uri, 'clone_uri')
1581 store_update(updates, landing_rev, 'repo_landing_rev')
1581 store_update(updates, landing_rev, 'repo_landing_rev')
1582 store_update(updates, enable_statistics, 'repo_enable_statistics')
1582 store_update(updates, enable_statistics, 'repo_enable_statistics')
1583 store_update(updates, enable_locking, 'repo_enable_locking')
1583 store_update(updates, enable_locking, 'repo_enable_locking')
1584 store_update(updates, enable_downloads, 'repo_enable_downloads')
1584 store_update(updates, enable_downloads, 'repo_enable_downloads')
1585
1585
1586 RepoModel().update(repo, **updates)
1586 RepoModel().update(repo, **updates)
1587 Session().commit()
1587 Session().commit()
1588 return dict(
1588 return dict(
1589 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1589 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1590 repository=repo.get_api_data()
1590 repository=repo.get_api_data()
1591 )
1591 )
1592 except Exception:
1592 except Exception:
1593 log.error(traceback.format_exc())
1593 log.error(traceback.format_exc())
1594 raise JSONRPCError('failed to update repo `%s`' % repoid)
1594 raise JSONRPCError('failed to update repo `%s`' % repoid)
1595
1595
1596 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1596 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1597 def fork_repo(self, apiuser, repoid, fork_name,
1597 def fork_repo(self, apiuser, repoid, fork_name,
1598 owner=Optional(OAttr('apiuser')),
1598 owner=Optional(OAttr('apiuser')),
1599 description=Optional(''), copy_permissions=Optional(False),
1599 description=Optional(''), copy_permissions=Optional(False),
1600 private=Optional(False), landing_rev=Optional('rev:tip')):
1600 private=Optional(False), landing_rev=Optional('rev:tip')):
1601 """
1601 """
1602 Creates a fork of given repo. In case of using celery this will
1602 Creates a fork of given repo. In case of using celery this will
1603 immediately return success message, while fork is going to be created
1603 immediately return success message, while fork is going to be created
1604 asynchronous. This command can be executed only using api_key belonging to
1604 asynchronous. This command can be executed only using api_key belonging to
1605 user with admin rights or regular user that have fork permission, and at least
1605 user with admin rights or regular user that have fork permission, and at least
1606 read access to forking repository. Regular users cannot specify owner parameter.
1606 read access to forking repository. Regular users cannot specify owner parameter.
1607
1607
1608 :param apiuser: filled automatically from apikey
1608 :param apiuser: filled automatically from apikey
1609 :type apiuser: AuthUser
1609 :type apiuser: AuthUser
1610 :param repoid: repository name or repository id
1610 :param repoid: repository name or repository id
1611 :type repoid: str or int
1611 :type repoid: str or int
1612 :param fork_name:
1612 :param fork_name:
1613 :param owner:
1613 :param owner:
1614 :param description:
1614 :param description:
1615 :param copy_permissions:
1615 :param copy_permissions:
1616 :param private:
1616 :param private:
1617 :param landing_rev:
1617 :param landing_rev:
1618
1618
1619 INPUT::
1619 INPUT::
1620
1620
1621 id : <id_for_response>
1621 id : <id_for_response>
1622 api_key : "<api_key>"
1622 api_key : "<api_key>"
1623 args: {
1623 args: {
1624 "repoid" : "<reponame or repo_id>",
1624 "repoid" : "<reponame or repo_id>",
1625 "fork_name": "<forkname>",
1625 "fork_name": "<forkname>",
1626 "owner": "<username or user_id = Optional(=apiuser)>",
1626 "owner": "<username or user_id = Optional(=apiuser)>",
1627 "description": "<description>",
1627 "description": "<description>",
1628 "copy_permissions": "<bool>",
1628 "copy_permissions": "<bool>",
1629 "private": "<bool>",
1629 "private": "<bool>",
1630 "landing_rev": "<landing_rev>"
1630 "landing_rev": "<landing_rev>"
1631 }
1631 }
1632
1632
1633 OUTPUT::
1633 OUTPUT::
1634
1634
1635 id : <id_given_in_input>
1635 id : <id_given_in_input>
1636 result: {
1636 result: {
1637 "msg": "Created fork of `<reponame>` as `<forkname>`",
1637 "msg": "Created fork of `<reponame>` as `<forkname>`",
1638 "success": true,
1638 "success": true,
1639 "task": "<celery task id or None if done sync>"
1639 "task": "<celery task id or None if done sync>"
1640 }
1640 }
1641 error: null
1641 error: null
1642
1642
1643 """
1643 """
1644 repo = get_repo_or_error(repoid)
1644 repo = get_repo_or_error(repoid)
1645 repo_name = repo.repo_name
1645 repo_name = repo.repo_name
1646
1646
1647 _repo = RepoModel().get_by_repo_name(fork_name)
1647 _repo = RepoModel().get_by_repo_name(fork_name)
1648 if _repo:
1648 if _repo:
1649 type_ = 'fork' if _repo.fork else 'repo'
1649 type_ = 'fork' if _repo.fork else 'repo'
1650 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1650 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1651
1651
1652 if HasPermissionAnyApi('hg.admin')(user=apiuser):
1652 if HasPermissionAnyApi('hg.admin')(user=apiuser):
1653 pass
1653 pass
1654 elif HasRepoPermissionAnyApi('repository.admin',
1654 elif HasRepoPermissionAnyApi('repository.admin',
1655 'repository.write',
1655 'repository.write',
1656 'repository.read')(user=apiuser,
1656 'repository.read')(user=apiuser,
1657 repo_name=repo.repo_name):
1657 repo_name=repo.repo_name):
1658 if not isinstance(owner, Optional):
1658 if not isinstance(owner, Optional):
1659 # forbid setting owner for non-admins
1659 # forbid setting owner for non-admins
1660 raise JSONRPCError(
1660 raise JSONRPCError(
1661 'Only Kallithea admin can specify `owner` param'
1661 'Only Kallithea admin can specify `owner` param'
1662 )
1662 )
1663
1663
1664 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
1664 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
1665 raise JSONRPCError('no permission to create repositories')
1665 raise JSONRPCError('no permission to create repositories')
1666 else:
1666 else:
1667 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1667 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1668
1668
1669 if isinstance(owner, Optional):
1669 if isinstance(owner, Optional):
1670 owner = apiuser.user_id
1670 owner = apiuser.user_id
1671
1671
1672 owner = get_user_or_error(owner)
1672 owner = get_user_or_error(owner)
1673
1673
1674 try:
1674 try:
1675 # create structure of groups and return the last group
1675 # create structure of groups and return the last group
1676 group = map_groups(fork_name)
1676 group = map_groups(fork_name)
1677 fork_base_name = fork_name.rsplit('/', 1)[-1]
1677 fork_base_name = fork_name.rsplit('/', 1)[-1]
1678
1678
1679 form_data = dict(
1679 form_data = dict(
1680 repo_name=fork_base_name,
1680 repo_name=fork_base_name,
1681 repo_name_full=fork_name,
1681 repo_name_full=fork_name,
1682 repo_group=group,
1682 repo_group=group,
1683 repo_type=repo.repo_type,
1683 repo_type=repo.repo_type,
1684 description=Optional.extract(description),
1684 description=Optional.extract(description),
1685 private=Optional.extract(private),
1685 private=Optional.extract(private),
1686 copy_permissions=Optional.extract(copy_permissions),
1686 copy_permissions=Optional.extract(copy_permissions),
1687 landing_rev=Optional.extract(landing_rev),
1687 landing_rev=Optional.extract(landing_rev),
1688 update_after_clone=False,
1688 update_after_clone=False,
1689 fork_parent_id=repo.repo_id,
1689 fork_parent_id=repo.repo_id,
1690 )
1690 )
1691 task = RepoModel().create_fork(form_data, cur_user=owner)
1691 task = RepoModel().create_fork(form_data, cur_user=owner)
1692 # no commit, it's done in RepoModel, or async via celery
1692 # no commit, it's done in RepoModel, or async via celery
1693 from celery.result import BaseAsyncResult
1693 from celery.result import BaseAsyncResult
1694 task_id = None
1694 task_id = None
1695 if isinstance(task, BaseAsyncResult):
1695 if isinstance(task, BaseAsyncResult):
1696 task_id = task.task_id
1696 task_id = task.task_id
1697 return dict(
1697 return dict(
1698 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1698 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1699 fork_name),
1699 fork_name),
1700 success=True, # cannot return the repo data here since fork
1700 success=True, # cannot return the repo data here since fork
1701 # can be done async
1701 # can be done async
1702 task=task_id
1702 task=task_id
1703 )
1703 )
1704 except Exception:
1704 except Exception:
1705 log.error(traceback.format_exc())
1705 log.error(traceback.format_exc())
1706 raise JSONRPCError(
1706 raise JSONRPCError(
1707 'failed to fork repository `%s` as `%s`' % (repo_name,
1707 'failed to fork repository `%s` as `%s`' % (repo_name,
1708 fork_name)
1708 fork_name)
1709 )
1709 )
1710
1710
1711 # permission check inside
1711 # permission check inside
1712 def delete_repo(self, apiuser, repoid, forks=Optional('')):
1712 def delete_repo(self, apiuser, repoid, forks=Optional('')):
1713 """
1713 """
1714 Deletes a repository. This command can be executed only using api_key belonging
1714 Deletes a repository. This command can be executed only using api_key belonging
1715 to user with admin rights or regular user that have admin access to repository.
1715 to user with admin rights or regular user that have admin access to repository.
1716 When `forks` param is set it's possible to detach or delete forks of deleting
1716 When `forks` param is set it's possible to detach or delete forks of deleting
1717 repository
1717 repository
1718
1718
1719 :param apiuser: filled automatically from apikey
1719 :param apiuser: filled automatically from apikey
1720 :type apiuser: AuthUser
1720 :type apiuser: AuthUser
1721 :param repoid: repository name or repository id
1721 :param repoid: repository name or repository id
1722 :type repoid: str or int
1722 :type repoid: str or int
1723 :param forks: `detach` or `delete`, what do do with attached forks for repo
1723 :param forks: `detach` or `delete`, what do do with attached forks for repo
1724 :type forks: Optional(str)
1724 :type forks: Optional(str)
1725
1725
1726 OUTPUT::
1726 OUTPUT::
1727
1727
1728 id : <id_given_in_input>
1728 id : <id_given_in_input>
1729 result: {
1729 result: {
1730 "msg": "Deleted repository `<reponame>`",
1730 "msg": "Deleted repository `<reponame>`",
1731 "success": true
1731 "success": true
1732 }
1732 }
1733 error: null
1733 error: null
1734
1734
1735 """
1735 """
1736 repo = get_repo_or_error(repoid)
1736 repo = get_repo_or_error(repoid)
1737
1737
1738 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1738 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1739 # check if we have admin permission for this repo !
1739 # check if we have admin permission for this repo !
1740 if not HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
1740 if not HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
1741 repo_name=repo.repo_name):
1741 repo_name=repo.repo_name):
1742 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1742 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1743
1743
1744 try:
1744 try:
1745 handle_forks = Optional.extract(forks)
1745 handle_forks = Optional.extract(forks)
1746 _forks_msg = ''
1746 _forks_msg = ''
1747 _forks = [f for f in repo.forks]
1747 _forks = [f for f in repo.forks]
1748 if handle_forks == 'detach':
1748 if handle_forks == 'detach':
1749 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1749 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1750 elif handle_forks == 'delete':
1750 elif handle_forks == 'delete':
1751 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1751 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1752 elif _forks:
1752 elif _forks:
1753 raise JSONRPCError(
1753 raise JSONRPCError(
1754 'Cannot delete `%s` it still contains attached forks' %
1754 'Cannot delete `%s` it still contains attached forks' %
1755 (repo.repo_name,)
1755 (repo.repo_name,)
1756 )
1756 )
1757
1757
1758 RepoModel().delete(repo, forks=forks)
1758 RepoModel().delete(repo, forks=forks)
1759 Session().commit()
1759 Session().commit()
1760 return dict(
1760 return dict(
1761 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1761 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1762 success=True
1762 success=True
1763 )
1763 )
1764 except Exception:
1764 except Exception:
1765 log.error(traceback.format_exc())
1765 log.error(traceback.format_exc())
1766 raise JSONRPCError(
1766 raise JSONRPCError(
1767 'failed to delete repository `%s`' % (repo.repo_name,)
1767 'failed to delete repository `%s`' % (repo.repo_name,)
1768 )
1768 )
1769
1769
1770 @HasPermissionAllDecorator('hg.admin')
1770 @HasPermissionAnyDecorator('hg.admin')
1771 def grant_user_permission(self, apiuser, repoid, userid, perm):
1771 def grant_user_permission(self, apiuser, repoid, userid, perm):
1772 """
1772 """
1773 Grant permission for user on given repository, or update existing one
1773 Grant permission for user on given repository, or update existing one
1774 if found. This command can be executed only using api_key belonging to user
1774 if found. This command can be executed only using api_key belonging to user
1775 with admin rights.
1775 with admin rights.
1776
1776
1777 :param apiuser: filled automatically from apikey
1777 :param apiuser: filled automatically from apikey
1778 :type apiuser: AuthUser
1778 :type apiuser: AuthUser
1779 :param repoid: repository name or repository id
1779 :param repoid: repository name or repository id
1780 :type repoid: str or int
1780 :type repoid: str or int
1781 :param userid:
1781 :param userid:
1782 :param perm: (repository.(none|read|write|admin))
1782 :param perm: (repository.(none|read|write|admin))
1783 :type perm: str
1783 :type perm: str
1784
1784
1785 OUTPUT::
1785 OUTPUT::
1786
1786
1787 id : <id_given_in_input>
1787 id : <id_given_in_input>
1788 result: {
1788 result: {
1789 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1789 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1790 "success": true
1790 "success": true
1791 }
1791 }
1792 error: null
1792 error: null
1793 """
1793 """
1794 repo = get_repo_or_error(repoid)
1794 repo = get_repo_or_error(repoid)
1795 user = get_user_or_error(userid)
1795 user = get_user_or_error(userid)
1796 perm = get_perm_or_error(perm)
1796 perm = get_perm_or_error(perm)
1797
1797
1798 try:
1798 try:
1799
1799
1800 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1800 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1801
1801
1802 Session().commit()
1802 Session().commit()
1803 return dict(
1803 return dict(
1804 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1804 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1805 perm.permission_name, user.username, repo.repo_name
1805 perm.permission_name, user.username, repo.repo_name
1806 ),
1806 ),
1807 success=True
1807 success=True
1808 )
1808 )
1809 except Exception:
1809 except Exception:
1810 log.error(traceback.format_exc())
1810 log.error(traceback.format_exc())
1811 raise JSONRPCError(
1811 raise JSONRPCError(
1812 'failed to edit permission for user: `%s` in repo: `%s`' % (
1812 'failed to edit permission for user: `%s` in repo: `%s`' % (
1813 userid, repoid
1813 userid, repoid
1814 )
1814 )
1815 )
1815 )
1816
1816
1817 @HasPermissionAllDecorator('hg.admin')
1817 @HasPermissionAnyDecorator('hg.admin')
1818 def revoke_user_permission(self, apiuser, repoid, userid):
1818 def revoke_user_permission(self, apiuser, repoid, userid):
1819 """
1819 """
1820 Revoke permission for user on given repository. This command can be executed
1820 Revoke permission for user on given repository. This command can be executed
1821 only using api_key belonging to user with admin rights.
1821 only using api_key belonging to user with admin rights.
1822
1822
1823 :param apiuser: filled automatically from apikey
1823 :param apiuser: filled automatically from apikey
1824 :type apiuser: AuthUser
1824 :type apiuser: AuthUser
1825 :param repoid: repository name or repository id
1825 :param repoid: repository name or repository id
1826 :type repoid: str or int
1826 :type repoid: str or int
1827 :param userid:
1827 :param userid:
1828
1828
1829 OUTPUT::
1829 OUTPUT::
1830
1830
1831 id : <id_given_in_input>
1831 id : <id_given_in_input>
1832 result: {
1832 result: {
1833 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1833 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1834 "success": true
1834 "success": true
1835 }
1835 }
1836 error: null
1836 error: null
1837
1837
1838 """
1838 """
1839
1839
1840 repo = get_repo_or_error(repoid)
1840 repo = get_repo_or_error(repoid)
1841 user = get_user_or_error(userid)
1841 user = get_user_or_error(userid)
1842 try:
1842 try:
1843 RepoModel().revoke_user_permission(repo=repo, user=user)
1843 RepoModel().revoke_user_permission(repo=repo, user=user)
1844 Session().commit()
1844 Session().commit()
1845 return dict(
1845 return dict(
1846 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1846 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1847 user.username, repo.repo_name
1847 user.username, repo.repo_name
1848 ),
1848 ),
1849 success=True
1849 success=True
1850 )
1850 )
1851 except Exception:
1851 except Exception:
1852 log.error(traceback.format_exc())
1852 log.error(traceback.format_exc())
1853 raise JSONRPCError(
1853 raise JSONRPCError(
1854 'failed to edit permission for user: `%s` in repo: `%s`' % (
1854 'failed to edit permission for user: `%s` in repo: `%s`' % (
1855 userid, repoid
1855 userid, repoid
1856 )
1856 )
1857 )
1857 )
1858
1858
1859 # permission check inside
1859 # permission check inside
1860 def grant_user_group_permission(self, apiuser, repoid, usergroupid, perm):
1860 def grant_user_group_permission(self, apiuser, repoid, usergroupid, perm):
1861 """
1861 """
1862 Grant permission for user group on given repository, or update
1862 Grant permission for user group on given repository, or update
1863 existing one if found. This command can be executed only using
1863 existing one if found. This command can be executed only using
1864 api_key belonging to user with admin rights.
1864 api_key belonging to user with admin rights.
1865
1865
1866 :param apiuser: filled automatically from apikey
1866 :param apiuser: filled automatically from apikey
1867 :type apiuser: AuthUser
1867 :type apiuser: AuthUser
1868 :param repoid: repository name or repository id
1868 :param repoid: repository name or repository id
1869 :type repoid: str or int
1869 :type repoid: str or int
1870 :param usergroupid: id of usergroup
1870 :param usergroupid: id of usergroup
1871 :type usergroupid: str or int
1871 :type usergroupid: str or int
1872 :param perm: (repository.(none|read|write|admin))
1872 :param perm: (repository.(none|read|write|admin))
1873 :type perm: str
1873 :type perm: str
1874
1874
1875 OUTPUT::
1875 OUTPUT::
1876
1876
1877 id : <id_given_in_input>
1877 id : <id_given_in_input>
1878 result : {
1878 result : {
1879 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1879 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1880 "success": true
1880 "success": true
1881
1881
1882 }
1882 }
1883 error : null
1883 error : null
1884
1884
1885 ERROR OUTPUT::
1885 ERROR OUTPUT::
1886
1886
1887 id : <id_given_in_input>
1887 id : <id_given_in_input>
1888 result : null
1888 result : null
1889 error : {
1889 error : {
1890 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1890 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1891 }
1891 }
1892
1892
1893 """
1893 """
1894 repo = get_repo_or_error(repoid)
1894 repo = get_repo_or_error(repoid)
1895 perm = get_perm_or_error(perm)
1895 perm = get_perm_or_error(perm)
1896 user_group = get_user_group_or_error(usergroupid)
1896 user_group = get_user_group_or_error(usergroupid)
1897 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1897 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1898 # check if we have admin permission for this repo !
1898 # check if we have admin permission for this repo !
1899 _perms = ('repository.admin',)
1899 _perms = ('repository.admin',)
1900 if not HasRepoPermissionAnyApi(*_perms)(
1900 if not HasRepoPermissionAnyApi(*_perms)(
1901 user=apiuser, repo_name=repo.repo_name):
1901 user=apiuser, repo_name=repo.repo_name):
1902 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1902 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1903
1903
1904 # check if we have at least read permission for this user group !
1904 # check if we have at least read permission for this user group !
1905 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1905 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1906 if not HasUserGroupPermissionAny(*_perms)(
1906 if not HasUserGroupPermissionAny(*_perms)(
1907 user=apiuser, user_group_name=user_group.users_group_name):
1907 user=apiuser, user_group_name=user_group.users_group_name):
1908 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1908 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1909
1909
1910 try:
1910 try:
1911 RepoModel().grant_user_group_permission(
1911 RepoModel().grant_user_group_permission(
1912 repo=repo, group_name=user_group, perm=perm)
1912 repo=repo, group_name=user_group, perm=perm)
1913
1913
1914 Session().commit()
1914 Session().commit()
1915 return dict(
1915 return dict(
1916 msg='Granted perm: `%s` for user group: `%s` in '
1916 msg='Granted perm: `%s` for user group: `%s` in '
1917 'repo: `%s`' % (
1917 'repo: `%s`' % (
1918 perm.permission_name, user_group.users_group_name,
1918 perm.permission_name, user_group.users_group_name,
1919 repo.repo_name
1919 repo.repo_name
1920 ),
1920 ),
1921 success=True
1921 success=True
1922 )
1922 )
1923 except Exception:
1923 except Exception:
1924 log.error(traceback.format_exc())
1924 log.error(traceback.format_exc())
1925 raise JSONRPCError(
1925 raise JSONRPCError(
1926 'failed to edit permission for user group: `%s` in '
1926 'failed to edit permission for user group: `%s` in '
1927 'repo: `%s`' % (
1927 'repo: `%s`' % (
1928 usergroupid, repo.repo_name
1928 usergroupid, repo.repo_name
1929 )
1929 )
1930 )
1930 )
1931
1931
1932 # permission check inside
1932 # permission check inside
1933 def revoke_user_group_permission(self, apiuser, repoid, usergroupid):
1933 def revoke_user_group_permission(self, apiuser, repoid, usergroupid):
1934 """
1934 """
1935 Revoke permission for user group on given repository. This command can be
1935 Revoke permission for user group on given repository. This command can be
1936 executed only using api_key belonging to user with admin rights.
1936 executed only using api_key belonging to user with admin rights.
1937
1937
1938 :param apiuser: filled automatically from apikey
1938 :param apiuser: filled automatically from apikey
1939 :type apiuser: AuthUser
1939 :type apiuser: AuthUser
1940 :param repoid: repository name or repository id
1940 :param repoid: repository name or repository id
1941 :type repoid: str or int
1941 :type repoid: str or int
1942 :param usergroupid:
1942 :param usergroupid:
1943
1943
1944 OUTPUT::
1944 OUTPUT::
1945
1945
1946 id : <id_given_in_input>
1946 id : <id_given_in_input>
1947 result: {
1947 result: {
1948 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1948 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1949 "success": true
1949 "success": true
1950 }
1950 }
1951 error: null
1951 error: null
1952 """
1952 """
1953 repo = get_repo_or_error(repoid)
1953 repo = get_repo_or_error(repoid)
1954 user_group = get_user_group_or_error(usergroupid)
1954 user_group = get_user_group_or_error(usergroupid)
1955 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1955 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
1956 # check if we have admin permission for this repo !
1956 # check if we have admin permission for this repo !
1957 _perms = ('repository.admin',)
1957 _perms = ('repository.admin',)
1958 if not HasRepoPermissionAnyApi(*_perms)(
1958 if not HasRepoPermissionAnyApi(*_perms)(
1959 user=apiuser, repo_name=repo.repo_name):
1959 user=apiuser, repo_name=repo.repo_name):
1960 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1960 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1961
1961
1962 # check if we have at least read permission for this user group !
1962 # check if we have at least read permission for this user group !
1963 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1963 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1964 if not HasUserGroupPermissionAny(*_perms)(
1964 if not HasUserGroupPermissionAny(*_perms)(
1965 user=apiuser, user_group_name=user_group.users_group_name):
1965 user=apiuser, user_group_name=user_group.users_group_name):
1966 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1966 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1967
1967
1968 try:
1968 try:
1969 RepoModel().revoke_user_group_permission(
1969 RepoModel().revoke_user_group_permission(
1970 repo=repo, group_name=user_group)
1970 repo=repo, group_name=user_group)
1971
1971
1972 Session().commit()
1972 Session().commit()
1973 return dict(
1973 return dict(
1974 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1974 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1975 user_group.users_group_name, repo.repo_name
1975 user_group.users_group_name, repo.repo_name
1976 ),
1976 ),
1977 success=True
1977 success=True
1978 )
1978 )
1979 except Exception:
1979 except Exception:
1980 log.error(traceback.format_exc())
1980 log.error(traceback.format_exc())
1981 raise JSONRPCError(
1981 raise JSONRPCError(
1982 'failed to edit permission for user group: `%s` in '
1982 'failed to edit permission for user group: `%s` in '
1983 'repo: `%s`' % (
1983 'repo: `%s`' % (
1984 user_group.users_group_name, repo.repo_name
1984 user_group.users_group_name, repo.repo_name
1985 )
1985 )
1986 )
1986 )
1987
1987
1988 @HasPermissionAllDecorator('hg.admin')
1988 @HasPermissionAnyDecorator('hg.admin')
1989 def get_repo_group(self, apiuser, repogroupid):
1989 def get_repo_group(self, apiuser, repogroupid):
1990 """
1990 """
1991 Returns given repo group together with permissions, and repositories
1991 Returns given repo group together with permissions, and repositories
1992 inside the group
1992 inside the group
1993
1993
1994 :param apiuser: filled automatically from apikey
1994 :param apiuser: filled automatically from apikey
1995 :type apiuser: AuthUser
1995 :type apiuser: AuthUser
1996 :param repogroupid: id/name of repository group
1996 :param repogroupid: id/name of repository group
1997 :type repogroupid: str or int
1997 :type repogroupid: str or int
1998 """
1998 """
1999 repo_group = get_repo_group_or_error(repogroupid)
1999 repo_group = get_repo_group_or_error(repogroupid)
2000
2000
2001 members = []
2001 members = []
2002 for user in repo_group.repo_group_to_perm:
2002 for user in repo_group.repo_group_to_perm:
2003 perm = user.permission.permission_name
2003 perm = user.permission.permission_name
2004 user = user.user
2004 user = user.user
2005 user_data = {
2005 user_data = {
2006 'name': user.username,
2006 'name': user.username,
2007 'type': "user",
2007 'type': "user",
2008 'permission': perm
2008 'permission': perm
2009 }
2009 }
2010 members.append(user_data)
2010 members.append(user_data)
2011
2011
2012 for user_group in repo_group.users_group_to_perm:
2012 for user_group in repo_group.users_group_to_perm:
2013 perm = user_group.permission.permission_name
2013 perm = user_group.permission.permission_name
2014 user_group = user_group.users_group
2014 user_group = user_group.users_group
2015 user_group_data = {
2015 user_group_data = {
2016 'name': user_group.users_group_name,
2016 'name': user_group.users_group_name,
2017 'type': "user_group",
2017 'type': "user_group",
2018 'permission': perm
2018 'permission': perm
2019 }
2019 }
2020 members.append(user_group_data)
2020 members.append(user_group_data)
2021
2021
2022 data = repo_group.get_api_data()
2022 data = repo_group.get_api_data()
2023 data["members"] = members
2023 data["members"] = members
2024 return data
2024 return data
2025
2025
2026 @HasPermissionAllDecorator('hg.admin')
2026 @HasPermissionAnyDecorator('hg.admin')
2027 def get_repo_groups(self, apiuser):
2027 def get_repo_groups(self, apiuser):
2028 """
2028 """
2029 Returns all repository groups
2029 Returns all repository groups
2030
2030
2031 :param apiuser: filled automatically from apikey
2031 :param apiuser: filled automatically from apikey
2032 :type apiuser: AuthUser
2032 :type apiuser: AuthUser
2033 """
2033 """
2034 result = []
2034 result = []
2035 for repo_group in RepoGroupModel().get_all():
2035 for repo_group in RepoGroupModel().get_all():
2036 result.append(repo_group.get_api_data())
2036 result.append(repo_group.get_api_data())
2037 return result
2037 return result
2038
2038
2039 @HasPermissionAllDecorator('hg.admin')
2039 @HasPermissionAnyDecorator('hg.admin')
2040 def create_repo_group(self, apiuser, group_name, description=Optional(''),
2040 def create_repo_group(self, apiuser, group_name, description=Optional(''),
2041 owner=Optional(OAttr('apiuser')),
2041 owner=Optional(OAttr('apiuser')),
2042 parent=Optional(None),
2042 parent=Optional(None),
2043 copy_permissions=Optional(False)):
2043 copy_permissions=Optional(False)):
2044 """
2044 """
2045 Creates a repository group. This command can be executed only using
2045 Creates a repository group. This command can be executed only using
2046 api_key belonging to user with admin rights.
2046 api_key belonging to user with admin rights.
2047
2047
2048 :param apiuser: filled automatically from apikey
2048 :param apiuser: filled automatically from apikey
2049 :type apiuser: AuthUser
2049 :type apiuser: AuthUser
2050 :param group_name:
2050 :param group_name:
2051 :type group_name:
2051 :type group_name:
2052 :param description:
2052 :param description:
2053 :type description:
2053 :type description:
2054 :param owner:
2054 :param owner:
2055 :type owner:
2055 :type owner:
2056 :param parent:
2056 :param parent:
2057 :type parent:
2057 :type parent:
2058 :param copy_permissions:
2058 :param copy_permissions:
2059 :type copy_permissions:
2059 :type copy_permissions:
2060
2060
2061 OUTPUT::
2061 OUTPUT::
2062
2062
2063 id : <id_given_in_input>
2063 id : <id_given_in_input>
2064 result : {
2064 result : {
2065 "msg": "created new repo group `<repo_group_name>`"
2065 "msg": "created new repo group `<repo_group_name>`"
2066 "repo_group": <repogroup_object>
2066 "repo_group": <repogroup_object>
2067 }
2067 }
2068 error : null
2068 error : null
2069
2069
2070 ERROR OUTPUT::
2070 ERROR OUTPUT::
2071
2071
2072 id : <id_given_in_input>
2072 id : <id_given_in_input>
2073 result : null
2073 result : null
2074 error : {
2074 error : {
2075 failed to create repo group `<repogroupid>`
2075 failed to create repo group `<repogroupid>`
2076 }
2076 }
2077
2077
2078 """
2078 """
2079 if RepoGroup.get_by_group_name(group_name):
2079 if RepoGroup.get_by_group_name(group_name):
2080 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
2080 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
2081
2081
2082 if isinstance(owner, Optional):
2082 if isinstance(owner, Optional):
2083 owner = apiuser.user_id
2083 owner = apiuser.user_id
2084 group_description = Optional.extract(description)
2084 group_description = Optional.extract(description)
2085 parent_group = Optional.extract(parent)
2085 parent_group = Optional.extract(parent)
2086 if not isinstance(parent, Optional):
2086 if not isinstance(parent, Optional):
2087 parent_group = get_repo_group_or_error(parent_group)
2087 parent_group = get_repo_group_or_error(parent_group)
2088
2088
2089 copy_permissions = Optional.extract(copy_permissions)
2089 copy_permissions = Optional.extract(copy_permissions)
2090 try:
2090 try:
2091 repo_group = RepoGroupModel().create(
2091 repo_group = RepoGroupModel().create(
2092 group_name=group_name,
2092 group_name=group_name,
2093 group_description=group_description,
2093 group_description=group_description,
2094 owner=owner,
2094 owner=owner,
2095 parent=parent_group,
2095 parent=parent_group,
2096 copy_permissions=copy_permissions
2096 copy_permissions=copy_permissions
2097 )
2097 )
2098 Session().commit()
2098 Session().commit()
2099 return dict(
2099 return dict(
2100 msg='created new repo group `%s`' % group_name,
2100 msg='created new repo group `%s`' % group_name,
2101 repo_group=repo_group.get_api_data()
2101 repo_group=repo_group.get_api_data()
2102 )
2102 )
2103 except Exception:
2103 except Exception:
2104
2104
2105 log.error(traceback.format_exc())
2105 log.error(traceback.format_exc())
2106 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
2106 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
2107
2107
2108 @HasPermissionAllDecorator('hg.admin')
2108 @HasPermissionAnyDecorator('hg.admin')
2109 def update_repo_group(self, apiuser, repogroupid, group_name=Optional(''),
2109 def update_repo_group(self, apiuser, repogroupid, group_name=Optional(''),
2110 description=Optional(''),
2110 description=Optional(''),
2111 owner=Optional(OAttr('apiuser')),
2111 owner=Optional(OAttr('apiuser')),
2112 parent=Optional(None), enable_locking=Optional(False)):
2112 parent=Optional(None), enable_locking=Optional(False)):
2113 repo_group = get_repo_group_or_error(repogroupid)
2113 repo_group = get_repo_group_or_error(repogroupid)
2114
2114
2115 updates = {}
2115 updates = {}
2116 try:
2116 try:
2117 store_update(updates, group_name, 'group_name')
2117 store_update(updates, group_name, 'group_name')
2118 store_update(updates, description, 'group_description')
2118 store_update(updates, description, 'group_description')
2119 store_update(updates, owner, 'owner')
2119 store_update(updates, owner, 'owner')
2120 store_update(updates, parent, 'parent_group')
2120 store_update(updates, parent, 'parent_group')
2121 store_update(updates, enable_locking, 'enable_locking')
2121 store_update(updates, enable_locking, 'enable_locking')
2122 repo_group = RepoGroupModel().update(repo_group, updates)
2122 repo_group = RepoGroupModel().update(repo_group, updates)
2123 Session().commit()
2123 Session().commit()
2124 return dict(
2124 return dict(
2125 msg='updated repository group ID:%s %s' % (repo_group.group_id,
2125 msg='updated repository group ID:%s %s' % (repo_group.group_id,
2126 repo_group.group_name),
2126 repo_group.group_name),
2127 repo_group=repo_group.get_api_data()
2127 repo_group=repo_group.get_api_data()
2128 )
2128 )
2129 except Exception:
2129 except Exception:
2130 log.error(traceback.format_exc())
2130 log.error(traceback.format_exc())
2131 raise JSONRPCError('failed to update repository group `%s`'
2131 raise JSONRPCError('failed to update repository group `%s`'
2132 % (repogroupid,))
2132 % (repogroupid,))
2133
2133
2134 @HasPermissionAllDecorator('hg.admin')
2134 @HasPermissionAnyDecorator('hg.admin')
2135 def delete_repo_group(self, apiuser, repogroupid):
2135 def delete_repo_group(self, apiuser, repogroupid):
2136 """
2136 """
2137
2137
2138 :param apiuser: filled automatically from apikey
2138 :param apiuser: filled automatically from apikey
2139 :type apiuser: AuthUser
2139 :type apiuser: AuthUser
2140 :param repogroupid: name or id of repository group
2140 :param repogroupid: name or id of repository group
2141 :type repogroupid: str or int
2141 :type repogroupid: str or int
2142
2142
2143 OUTPUT::
2143 OUTPUT::
2144
2144
2145 id : <id_given_in_input>
2145 id : <id_given_in_input>
2146 result : {
2146 result : {
2147 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
2147 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
2148 'repo_group': null
2148 'repo_group': null
2149 }
2149 }
2150 error : null
2150 error : null
2151
2151
2152 ERROR OUTPUT::
2152 ERROR OUTPUT::
2153
2153
2154 id : <id_given_in_input>
2154 id : <id_given_in_input>
2155 result : null
2155 result : null
2156 error : {
2156 error : {
2157 "failed to delete repo group ID:<repogroupid> <repogroupname>"
2157 "failed to delete repo group ID:<repogroupid> <repogroupname>"
2158 }
2158 }
2159
2159
2160 """
2160 """
2161 repo_group = get_repo_group_or_error(repogroupid)
2161 repo_group = get_repo_group_or_error(repogroupid)
2162
2162
2163 try:
2163 try:
2164 RepoGroupModel().delete(repo_group)
2164 RepoGroupModel().delete(repo_group)
2165 Session().commit()
2165 Session().commit()
2166 return dict(
2166 return dict(
2167 msg='deleted repo group ID:%s %s' %
2167 msg='deleted repo group ID:%s %s' %
2168 (repo_group.group_id, repo_group.group_name),
2168 (repo_group.group_id, repo_group.group_name),
2169 repo_group=None
2169 repo_group=None
2170 )
2170 )
2171 except Exception:
2171 except Exception:
2172 log.error(traceback.format_exc())
2172 log.error(traceback.format_exc())
2173 raise JSONRPCError('failed to delete repo group ID:%s %s' %
2173 raise JSONRPCError('failed to delete repo group ID:%s %s' %
2174 (repo_group.group_id, repo_group.group_name)
2174 (repo_group.group_id, repo_group.group_name)
2175 )
2175 )
2176
2176
2177 # permission check inside
2177 # permission check inside
2178 def grant_user_permission_to_repo_group(self, apiuser, repogroupid, userid,
2178 def grant_user_permission_to_repo_group(self, apiuser, repogroupid, userid,
2179 perm, apply_to_children=Optional('none')):
2179 perm, apply_to_children=Optional('none')):
2180 """
2180 """
2181 Grant permission for user on given repository group, or update existing
2181 Grant permission for user on given repository group, or update existing
2182 one if found. This command can be executed only using api_key belonging
2182 one if found. This command can be executed only using api_key belonging
2183 to user with admin rights, or user who has admin right to given repository
2183 to user with admin rights, or user who has admin right to given repository
2184 group.
2184 group.
2185
2185
2186 :param apiuser: filled automatically from apikey
2186 :param apiuser: filled automatically from apikey
2187 :type apiuser: AuthUser
2187 :type apiuser: AuthUser
2188 :param repogroupid: name or id of repository group
2188 :param repogroupid: name or id of repository group
2189 :type repogroupid: str or int
2189 :type repogroupid: str or int
2190 :param userid:
2190 :param userid:
2191 :param perm: (group.(none|read|write|admin))
2191 :param perm: (group.(none|read|write|admin))
2192 :type perm: str
2192 :type perm: str
2193 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2193 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2194 :type apply_to_children: str
2194 :type apply_to_children: str
2195
2195
2196 OUTPUT::
2196 OUTPUT::
2197
2197
2198 id : <id_given_in_input>
2198 id : <id_given_in_input>
2199 result: {
2199 result: {
2200 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2200 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2201 "success": true
2201 "success": true
2202 }
2202 }
2203 error: null
2203 error: null
2204
2204
2205 ERROR OUTPUT::
2205 ERROR OUTPUT::
2206
2206
2207 id : <id_given_in_input>
2207 id : <id_given_in_input>
2208 result : null
2208 result : null
2209 error : {
2209 error : {
2210 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2210 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2211 }
2211 }
2212
2212
2213 """
2213 """
2214
2214
2215 repo_group = get_repo_group_or_error(repogroupid)
2215 repo_group = get_repo_group_or_error(repogroupid)
2216
2216
2217 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2217 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2218 # check if we have admin permission for this repo group !
2218 # check if we have admin permission for this repo group !
2219 if not HasRepoGroupPermissionAnyApi('group.admin')(user=apiuser,
2219 if not HasRepoGroupPermissionAnyApi('group.admin')(user=apiuser,
2220 group_name=repo_group.group_name):
2220 group_name=repo_group.group_name):
2221 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2221 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2222
2222
2223 user = get_user_or_error(userid)
2223 user = get_user_or_error(userid)
2224 perm = get_perm_or_error(perm, prefix='group.')
2224 perm = get_perm_or_error(perm, prefix='group.')
2225 apply_to_children = Optional.extract(apply_to_children)
2225 apply_to_children = Optional.extract(apply_to_children)
2226
2226
2227 try:
2227 try:
2228 RepoGroupModel().add_permission(repo_group=repo_group,
2228 RepoGroupModel().add_permission(repo_group=repo_group,
2229 obj=user,
2229 obj=user,
2230 obj_type="user",
2230 obj_type="user",
2231 perm=perm,
2231 perm=perm,
2232 recursive=apply_to_children)
2232 recursive=apply_to_children)
2233 Session().commit()
2233 Session().commit()
2234 return dict(
2234 return dict(
2235 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
2235 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
2236 perm.permission_name, apply_to_children, user.username, repo_group.name
2236 perm.permission_name, apply_to_children, user.username, repo_group.name
2237 ),
2237 ),
2238 success=True
2238 success=True
2239 )
2239 )
2240 except Exception:
2240 except Exception:
2241 log.error(traceback.format_exc())
2241 log.error(traceback.format_exc())
2242 raise JSONRPCError(
2242 raise JSONRPCError(
2243 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2243 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2244 userid, repo_group.name))
2244 userid, repo_group.name))
2245
2245
2246 # permission check inside
2246 # permission check inside
2247 def revoke_user_permission_from_repo_group(self, apiuser, repogroupid, userid,
2247 def revoke_user_permission_from_repo_group(self, apiuser, repogroupid, userid,
2248 apply_to_children=Optional('none')):
2248 apply_to_children=Optional('none')):
2249 """
2249 """
2250 Revoke permission for user on given repository group. This command can
2250 Revoke permission for user on given repository group. This command can
2251 be executed only using api_key belonging to user with admin rights, or
2251 be executed only using api_key belonging to user with admin rights, or
2252 user who has admin right to given repository group.
2252 user who has admin right to given repository group.
2253
2253
2254 :param apiuser: filled automatically from apikey
2254 :param apiuser: filled automatically from apikey
2255 :type apiuser: AuthUser
2255 :type apiuser: AuthUser
2256 :param repogroupid: name or id of repository group
2256 :param repogroupid: name or id of repository group
2257 :type repogroupid: str or int
2257 :type repogroupid: str or int
2258 :param userid:
2258 :param userid:
2259 :type userid:
2259 :type userid:
2260 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2260 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2261 :type apply_to_children: str
2261 :type apply_to_children: str
2262
2262
2263 OUTPUT::
2263 OUTPUT::
2264
2264
2265 id : <id_given_in_input>
2265 id : <id_given_in_input>
2266 result: {
2266 result: {
2267 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2267 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2268 "success": true
2268 "success": true
2269 }
2269 }
2270 error: null
2270 error: null
2271
2271
2272 ERROR OUTPUT::
2272 ERROR OUTPUT::
2273
2273
2274 id : <id_given_in_input>
2274 id : <id_given_in_input>
2275 result : null
2275 result : null
2276 error : {
2276 error : {
2277 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2277 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2278 }
2278 }
2279
2279
2280 """
2280 """
2281
2281
2282 repo_group = get_repo_group_or_error(repogroupid)
2282 repo_group = get_repo_group_or_error(repogroupid)
2283
2283
2284 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2284 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2285 # check if we have admin permission for this repo group !
2285 # check if we have admin permission for this repo group !
2286 if not HasRepoGroupPermissionAnyApi('group.admin')(user=apiuser,
2286 if not HasRepoGroupPermissionAnyApi('group.admin')(user=apiuser,
2287 group_name=repo_group.group_name):
2287 group_name=repo_group.group_name):
2288 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2288 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2289
2289
2290 user = get_user_or_error(userid)
2290 user = get_user_or_error(userid)
2291 apply_to_children = Optional.extract(apply_to_children)
2291 apply_to_children = Optional.extract(apply_to_children)
2292
2292
2293 try:
2293 try:
2294 RepoGroupModel().delete_permission(repo_group=repo_group,
2294 RepoGroupModel().delete_permission(repo_group=repo_group,
2295 obj=user,
2295 obj=user,
2296 obj_type="user",
2296 obj_type="user",
2297 recursive=apply_to_children)
2297 recursive=apply_to_children)
2298
2298
2299 Session().commit()
2299 Session().commit()
2300 return dict(
2300 return dict(
2301 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2301 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2302 apply_to_children, user.username, repo_group.name
2302 apply_to_children, user.username, repo_group.name
2303 ),
2303 ),
2304 success=True
2304 success=True
2305 )
2305 )
2306 except Exception:
2306 except Exception:
2307 log.error(traceback.format_exc())
2307 log.error(traceback.format_exc())
2308 raise JSONRPCError(
2308 raise JSONRPCError(
2309 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2309 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2310 userid, repo_group.name))
2310 userid, repo_group.name))
2311
2311
2312 # permission check inside
2312 # permission check inside
2313 def grant_user_group_permission_to_repo_group(
2313 def grant_user_group_permission_to_repo_group(
2314 self, apiuser, repogroupid, usergroupid, perm,
2314 self, apiuser, repogroupid, usergroupid, perm,
2315 apply_to_children=Optional('none')):
2315 apply_to_children=Optional('none')):
2316 """
2316 """
2317 Grant permission for user group on given repository group, or update
2317 Grant permission for user group on given repository group, or update
2318 existing one if found. This command can be executed only using
2318 existing one if found. This command can be executed only using
2319 api_key belonging to user with admin rights, or user who has admin
2319 api_key belonging to user with admin rights, or user who has admin
2320 right to given repository group.
2320 right to given repository group.
2321
2321
2322 :param apiuser: filled automatically from apikey
2322 :param apiuser: filled automatically from apikey
2323 :type apiuser: AuthUser
2323 :type apiuser: AuthUser
2324 :param repogroupid: name or id of repository group
2324 :param repogroupid: name or id of repository group
2325 :type repogroupid: str or int
2325 :type repogroupid: str or int
2326 :param usergroupid: id of usergroup
2326 :param usergroupid: id of usergroup
2327 :type usergroupid: str or int
2327 :type usergroupid: str or int
2328 :param perm: (group.(none|read|write|admin))
2328 :param perm: (group.(none|read|write|admin))
2329 :type perm: str
2329 :type perm: str
2330 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2330 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2331 :type apply_to_children: str
2331 :type apply_to_children: str
2332
2332
2333 OUTPUT::
2333 OUTPUT::
2334
2334
2335 id : <id_given_in_input>
2335 id : <id_given_in_input>
2336 result : {
2336 result : {
2337 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2337 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2338 "success": true
2338 "success": true
2339
2339
2340 }
2340 }
2341 error : null
2341 error : null
2342
2342
2343 ERROR OUTPUT::
2343 ERROR OUTPUT::
2344
2344
2345 id : <id_given_in_input>
2345 id : <id_given_in_input>
2346 result : null
2346 result : null
2347 error : {
2347 error : {
2348 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2348 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2349 }
2349 }
2350
2350
2351 """
2351 """
2352 repo_group = get_repo_group_or_error(repogroupid)
2352 repo_group = get_repo_group_or_error(repogroupid)
2353 perm = get_perm_or_error(perm, prefix='group.')
2353 perm = get_perm_or_error(perm, prefix='group.')
2354 user_group = get_user_group_or_error(usergroupid)
2354 user_group = get_user_group_or_error(usergroupid)
2355 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2355 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2356 # check if we have admin permission for this repo group !
2356 # check if we have admin permission for this repo group !
2357 _perms = ('group.admin',)
2357 _perms = ('group.admin',)
2358 if not HasRepoGroupPermissionAnyApi(*_perms)(
2358 if not HasRepoGroupPermissionAnyApi(*_perms)(
2359 user=apiuser, group_name=repo_group.group_name):
2359 user=apiuser, group_name=repo_group.group_name):
2360 raise JSONRPCError(
2360 raise JSONRPCError(
2361 'repository group `%s` does not exist' % (repogroupid,))
2361 'repository group `%s` does not exist' % (repogroupid,))
2362
2362
2363 # check if we have at least read permission for this user group !
2363 # check if we have at least read permission for this user group !
2364 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2364 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2365 if not HasUserGroupPermissionAny(*_perms)(
2365 if not HasUserGroupPermissionAny(*_perms)(
2366 user=apiuser, user_group_name=user_group.users_group_name):
2366 user=apiuser, user_group_name=user_group.users_group_name):
2367 raise JSONRPCError(
2367 raise JSONRPCError(
2368 'user group `%s` does not exist' % (usergroupid,))
2368 'user group `%s` does not exist' % (usergroupid,))
2369
2369
2370 apply_to_children = Optional.extract(apply_to_children)
2370 apply_to_children = Optional.extract(apply_to_children)
2371
2371
2372 try:
2372 try:
2373 RepoGroupModel().add_permission(repo_group=repo_group,
2373 RepoGroupModel().add_permission(repo_group=repo_group,
2374 obj=user_group,
2374 obj=user_group,
2375 obj_type="user_group",
2375 obj_type="user_group",
2376 perm=perm,
2376 perm=perm,
2377 recursive=apply_to_children)
2377 recursive=apply_to_children)
2378 Session().commit()
2378 Session().commit()
2379 return dict(
2379 return dict(
2380 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2380 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2381 perm.permission_name, apply_to_children,
2381 perm.permission_name, apply_to_children,
2382 user_group.users_group_name, repo_group.name
2382 user_group.users_group_name, repo_group.name
2383 ),
2383 ),
2384 success=True
2384 success=True
2385 )
2385 )
2386 except Exception:
2386 except Exception:
2387 log.error(traceback.format_exc())
2387 log.error(traceback.format_exc())
2388 raise JSONRPCError(
2388 raise JSONRPCError(
2389 'failed to edit permission for user group: `%s` in '
2389 'failed to edit permission for user group: `%s` in '
2390 'repo group: `%s`' % (
2390 'repo group: `%s`' % (
2391 usergroupid, repo_group.name
2391 usergroupid, repo_group.name
2392 )
2392 )
2393 )
2393 )
2394
2394
2395 # permission check inside
2395 # permission check inside
2396 def revoke_user_group_permission_from_repo_group(
2396 def revoke_user_group_permission_from_repo_group(
2397 self, apiuser, repogroupid, usergroupid,
2397 self, apiuser, repogroupid, usergroupid,
2398 apply_to_children=Optional('none')):
2398 apply_to_children=Optional('none')):
2399 """
2399 """
2400 Revoke permission for user group on given repository. This command can be
2400 Revoke permission for user group on given repository. This command can be
2401 executed only using api_key belonging to user with admin rights, or
2401 executed only using api_key belonging to user with admin rights, or
2402 user who has admin right to given repository group.
2402 user who has admin right to given repository group.
2403
2403
2404 :param apiuser: filled automatically from apikey
2404 :param apiuser: filled automatically from apikey
2405 :type apiuser: AuthUser
2405 :type apiuser: AuthUser
2406 :param repogroupid: name or id of repository group
2406 :param repogroupid: name or id of repository group
2407 :type repogroupid: str or int
2407 :type repogroupid: str or int
2408 :param usergroupid:
2408 :param usergroupid:
2409 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2409 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2410 :type apply_to_children: str
2410 :type apply_to_children: str
2411
2411
2412 OUTPUT::
2412 OUTPUT::
2413
2413
2414 id : <id_given_in_input>
2414 id : <id_given_in_input>
2415 result: {
2415 result: {
2416 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2416 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2417 "success": true
2417 "success": true
2418 }
2418 }
2419 error: null
2419 error: null
2420
2420
2421 ERROR OUTPUT::
2421 ERROR OUTPUT::
2422
2422
2423 id : <id_given_in_input>
2423 id : <id_given_in_input>
2424 result : null
2424 result : null
2425 error : {
2425 error : {
2426 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2426 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2427 }
2427 }
2428
2428
2429
2429
2430 """
2430 """
2431 repo_group = get_repo_group_or_error(repogroupid)
2431 repo_group = get_repo_group_or_error(repogroupid)
2432 user_group = get_user_group_or_error(usergroupid)
2432 user_group = get_user_group_or_error(usergroupid)
2433 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2433 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2434 # check if we have admin permission for this repo group !
2434 # check if we have admin permission for this repo group !
2435 _perms = ('group.admin',)
2435 _perms = ('group.admin',)
2436 if not HasRepoGroupPermissionAnyApi(*_perms)(
2436 if not HasRepoGroupPermissionAnyApi(*_perms)(
2437 user=apiuser, group_name=repo_group.group_name):
2437 user=apiuser, group_name=repo_group.group_name):
2438 raise JSONRPCError(
2438 raise JSONRPCError(
2439 'repository group `%s` does not exist' % (repogroupid,))
2439 'repository group `%s` does not exist' % (repogroupid,))
2440
2440
2441 # check if we have at least read permission for this user group !
2441 # check if we have at least read permission for this user group !
2442 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2442 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2443 if not HasUserGroupPermissionAny(*_perms)(
2443 if not HasUserGroupPermissionAny(*_perms)(
2444 user=apiuser, user_group_name=user_group.users_group_name):
2444 user=apiuser, user_group_name=user_group.users_group_name):
2445 raise JSONRPCError(
2445 raise JSONRPCError(
2446 'user group `%s` does not exist' % (usergroupid,))
2446 'user group `%s` does not exist' % (usergroupid,))
2447
2447
2448 apply_to_children = Optional.extract(apply_to_children)
2448 apply_to_children = Optional.extract(apply_to_children)
2449
2449
2450 try:
2450 try:
2451 RepoGroupModel().delete_permission(repo_group=repo_group,
2451 RepoGroupModel().delete_permission(repo_group=repo_group,
2452 obj=user_group,
2452 obj=user_group,
2453 obj_type="user_group",
2453 obj_type="user_group",
2454 recursive=apply_to_children)
2454 recursive=apply_to_children)
2455 Session().commit()
2455 Session().commit()
2456 return dict(
2456 return dict(
2457 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2457 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2458 apply_to_children, user_group.users_group_name, repo_group.name
2458 apply_to_children, user_group.users_group_name, repo_group.name
2459 ),
2459 ),
2460 success=True
2460 success=True
2461 )
2461 )
2462 except Exception:
2462 except Exception:
2463 log.error(traceback.format_exc())
2463 log.error(traceback.format_exc())
2464 raise JSONRPCError(
2464 raise JSONRPCError(
2465 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2465 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2466 user_group.users_group_name, repo_group.name
2466 user_group.users_group_name, repo_group.name
2467 )
2467 )
2468 )
2468 )
2469
2469
2470 def get_gist(self, apiuser, gistid):
2470 def get_gist(self, apiuser, gistid):
2471 """
2471 """
2472 Get given gist by id
2472 Get given gist by id
2473
2473
2474 :param apiuser: filled automatically from apikey
2474 :param apiuser: filled automatically from apikey
2475 :type apiuser: AuthUser
2475 :type apiuser: AuthUser
2476 :param gistid: id of private or public gist
2476 :param gistid: id of private or public gist
2477 :type gistid: str
2477 :type gistid: str
2478 """
2478 """
2479 gist = get_gist_or_error(gistid)
2479 gist = get_gist_or_error(gistid)
2480 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2480 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2481 if gist.gist_owner != apiuser.user_id:
2481 if gist.gist_owner != apiuser.user_id:
2482 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2482 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2483 return gist.get_api_data()
2483 return gist.get_api_data()
2484
2484
2485 def get_gists(self, apiuser, userid=Optional(OAttr('apiuser'))):
2485 def get_gists(self, apiuser, userid=Optional(OAttr('apiuser'))):
2486 """
2486 """
2487 Get all gists for given user. If userid is empty returned gists
2487 Get all gists for given user. If userid is empty returned gists
2488 are for user who called the api
2488 are for user who called the api
2489
2489
2490 :param apiuser: filled automatically from apikey
2490 :param apiuser: filled automatically from apikey
2491 :type apiuser: AuthUser
2491 :type apiuser: AuthUser
2492 :param userid: user to get gists for
2492 :param userid: user to get gists for
2493 :type userid: Optional(str or int)
2493 :type userid: Optional(str or int)
2494 """
2494 """
2495 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2495 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2496 # make sure normal user does not pass someone else userid,
2496 # make sure normal user does not pass someone else userid,
2497 # he is not allowed to do that
2497 # he is not allowed to do that
2498 if not isinstance(userid, Optional) and userid != apiuser.user_id:
2498 if not isinstance(userid, Optional) and userid != apiuser.user_id:
2499 raise JSONRPCError(
2499 raise JSONRPCError(
2500 'userid is not the same as your user'
2500 'userid is not the same as your user'
2501 )
2501 )
2502
2502
2503 if isinstance(userid, Optional):
2503 if isinstance(userid, Optional):
2504 user_id = apiuser.user_id
2504 user_id = apiuser.user_id
2505 else:
2505 else:
2506 user_id = get_user_or_error(userid).user_id
2506 user_id = get_user_or_error(userid).user_id
2507
2507
2508 gists = []
2508 gists = []
2509 _gists = Gist().query() \
2509 _gists = Gist().query() \
2510 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
2510 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
2511 .filter(Gist.gist_owner == user_id) \
2511 .filter(Gist.gist_owner == user_id) \
2512 .order_by(Gist.created_on.desc())
2512 .order_by(Gist.created_on.desc())
2513 for gist in _gists:
2513 for gist in _gists:
2514 gists.append(gist.get_api_data())
2514 gists.append(gist.get_api_data())
2515 return gists
2515 return gists
2516
2516
2517 def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
2517 def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
2518 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2518 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2519 description=Optional('')):
2519 description=Optional('')):
2520
2520
2521 """
2521 """
2522 Creates new Gist
2522 Creates new Gist
2523
2523
2524 :param apiuser: filled automatically from apikey
2524 :param apiuser: filled automatically from apikey
2525 :type apiuser: AuthUser
2525 :type apiuser: AuthUser
2526 :param files: files to be added to gist
2526 :param files: files to be added to gist
2527 {'filename': {'content':'...', 'lexer': null},
2527 {'filename': {'content':'...', 'lexer': null},
2528 'filename2': {'content':'...', 'lexer': null}}
2528 'filename2': {'content':'...', 'lexer': null}}
2529 :type files: dict
2529 :type files: dict
2530 :param owner: gist owner, defaults to api method caller
2530 :param owner: gist owner, defaults to api method caller
2531 :type owner: Optional(str or int)
2531 :type owner: Optional(str or int)
2532 :param gist_type: type of gist 'public' or 'private'
2532 :param gist_type: type of gist 'public' or 'private'
2533 :type gist_type: Optional(str)
2533 :type gist_type: Optional(str)
2534 :param lifetime: time in minutes of gist lifetime
2534 :param lifetime: time in minutes of gist lifetime
2535 :type lifetime: Optional(int)
2535 :type lifetime: Optional(int)
2536 :param description: gist description
2536 :param description: gist description
2537 :type description: Optional(str)
2537 :type description: Optional(str)
2538
2538
2539 OUTPUT::
2539 OUTPUT::
2540
2540
2541 id : <id_given_in_input>
2541 id : <id_given_in_input>
2542 result : {
2542 result : {
2543 "msg": "created new gist",
2543 "msg": "created new gist",
2544 "gist": {}
2544 "gist": {}
2545 }
2545 }
2546 error : null
2546 error : null
2547
2547
2548 ERROR OUTPUT::
2548 ERROR OUTPUT::
2549
2549
2550 id : <id_given_in_input>
2550 id : <id_given_in_input>
2551 result : null
2551 result : null
2552 error : {
2552 error : {
2553 "failed to create gist"
2553 "failed to create gist"
2554 }
2554 }
2555
2555
2556 """
2556 """
2557 try:
2557 try:
2558 if isinstance(owner, Optional):
2558 if isinstance(owner, Optional):
2559 owner = apiuser.user_id
2559 owner = apiuser.user_id
2560
2560
2561 owner = get_user_or_error(owner)
2561 owner = get_user_or_error(owner)
2562 description = Optional.extract(description)
2562 description = Optional.extract(description)
2563 gist_type = Optional.extract(gist_type)
2563 gist_type = Optional.extract(gist_type)
2564 lifetime = Optional.extract(lifetime)
2564 lifetime = Optional.extract(lifetime)
2565
2565
2566 gist = GistModel().create(description=description,
2566 gist = GistModel().create(description=description,
2567 owner=owner,
2567 owner=owner,
2568 gist_mapping=files,
2568 gist_mapping=files,
2569 gist_type=gist_type,
2569 gist_type=gist_type,
2570 lifetime=lifetime)
2570 lifetime=lifetime)
2571 Session().commit()
2571 Session().commit()
2572 return dict(
2572 return dict(
2573 msg='created new gist',
2573 msg='created new gist',
2574 gist=gist.get_api_data()
2574 gist=gist.get_api_data()
2575 )
2575 )
2576 except Exception:
2576 except Exception:
2577 log.error(traceback.format_exc())
2577 log.error(traceback.format_exc())
2578 raise JSONRPCError('failed to create gist')
2578 raise JSONRPCError('failed to create gist')
2579
2579
2580 # def update_gist(self, apiuser, gistid, files, owner=Optional(OAttr('apiuser')),
2580 # def update_gist(self, apiuser, gistid, files, owner=Optional(OAttr('apiuser')),
2581 # gist_type=Optional(Gist.GIST_PUBLIC),
2581 # gist_type=Optional(Gist.GIST_PUBLIC),
2582 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2582 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2583 # gist = get_gist_or_error(gistid)
2583 # gist = get_gist_or_error(gistid)
2584 # updates = {}
2584 # updates = {}
2585
2585
2586 # permission check inside
2586 # permission check inside
2587 def delete_gist(self, apiuser, gistid):
2587 def delete_gist(self, apiuser, gistid):
2588 """
2588 """
2589 Deletes existing gist
2589 Deletes existing gist
2590
2590
2591 :param apiuser: filled automatically from apikey
2591 :param apiuser: filled automatically from apikey
2592 :type apiuser: AuthUser
2592 :type apiuser: AuthUser
2593 :param gistid: id of gist to delete
2593 :param gistid: id of gist to delete
2594 :type gistid: str
2594 :type gistid: str
2595
2595
2596 OUTPUT::
2596 OUTPUT::
2597
2597
2598 id : <id_given_in_input>
2598 id : <id_given_in_input>
2599 result : {
2599 result : {
2600 "deleted gist ID: <gist_id>",
2600 "deleted gist ID: <gist_id>",
2601 "gist": null
2601 "gist": null
2602 }
2602 }
2603 error : null
2603 error : null
2604
2604
2605 ERROR OUTPUT::
2605 ERROR OUTPUT::
2606
2606
2607 id : <id_given_in_input>
2607 id : <id_given_in_input>
2608 result : null
2608 result : null
2609 error : {
2609 error : {
2610 "failed to delete gist ID:<gist_id>"
2610 "failed to delete gist ID:<gist_id>"
2611 }
2611 }
2612
2612
2613 """
2613 """
2614 gist = get_gist_or_error(gistid)
2614 gist = get_gist_or_error(gistid)
2615 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2615 if not HasPermissionAnyApi('hg.admin')(user=apiuser):
2616 if gist.gist_owner != apiuser.user_id:
2616 if gist.gist_owner != apiuser.user_id:
2617 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2617 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2618
2618
2619 try:
2619 try:
2620 GistModel().delete(gist)
2620 GistModel().delete(gist)
2621 Session().commit()
2621 Session().commit()
2622 return dict(
2622 return dict(
2623 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2623 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2624 gist=None
2624 gist=None
2625 )
2625 )
2626 except Exception:
2626 except Exception:
2627 log.error(traceback.format_exc())
2627 log.error(traceback.format_exc())
2628 raise JSONRPCError('failed to delete gist ID:%s'
2628 raise JSONRPCError('failed to delete gist ID:%s'
2629 % (gist.gist_access_id,))
2629 % (gist.gist_access_id,))
@@ -1,1330 +1,1172 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.i18n.translation import _
38 from pylons.i18n.translation import _
39 from webhelpers.pylonslib import secure_form
39 from webhelpers.pylonslib import secure_form
40 from sqlalchemy import or_
40 from sqlalchemy import or_
41 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm.exc import ObjectDeletedError
42 from sqlalchemy.orm import joinedload
42 from sqlalchemy.orm import joinedload
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
44
44
45 from kallithea import __platform__, is_windows, is_unix
45 from kallithea import __platform__, is_windows, is_unix
46 from kallithea.lib.vcs.utils.lazy import LazyProperty
46 from kallithea.lib.vcs.utils.lazy import LazyProperty
47 from kallithea.model import meta
47 from kallithea.model import meta
48 from kallithea.model.meta import Session
48 from kallithea.model.meta import Session
49 from kallithea.model.user import UserModel
49 from kallithea.model.user import UserModel
50 from kallithea.model.db import User, Repository, Permission, \
50 from kallithea.model.db import User, Repository, Permission, \
51 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
51 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
52 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
52 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
53 UserGroup, UserApiKeys
53 UserGroup, UserApiKeys
54
54
55 from kallithea.lib.utils2 import safe_str, safe_unicode, aslist
55 from kallithea.lib.utils2 import safe_str, safe_unicode, aslist
56 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, \
57 get_user_group_slug, conditional_cache
57 get_user_group_slug, conditional_cache
58 from kallithea.lib.caching_query import FromCache
58 from kallithea.lib.caching_query import FromCache
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 class PasswordGenerator(object):
64 class PasswordGenerator(object):
65 """
65 """
66 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
67 characters
67 characters
68 usage::
68 usage::
69
69
70 passwd_gen = PasswordGenerator()
70 passwd_gen = PasswordGenerator()
71 #print 8-letter password containing only big and small letters
71 #print 8-letter password containing only big and small letters
72 of alphabet
72 of alphabet
73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
74 """
74 """
75 ALPHABETS_NUM = r'''1234567890'''
75 ALPHABETS_NUM = r'''1234567890'''
76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
85
85
86 def gen_password(self, length, alphabet=ALPHABETS_FULL):
86 def gen_password(self, length, alphabet=ALPHABETS_FULL):
87 assert len(alphabet) <= 256, alphabet
87 assert len(alphabet) <= 256, alphabet
88 l = []
88 l = []
89 while len(l) < length:
89 while len(l) < length:
90 i = ord(os.urandom(1))
90 i = ord(os.urandom(1))
91 if i < len(alphabet):
91 if i < len(alphabet):
92 l.append(alphabet[i])
92 l.append(alphabet[i])
93 return ''.join(l)
93 return ''.join(l)
94
94
95
95
96 class KallitheaCrypto(object):
96 class KallitheaCrypto(object):
97
97
98 @classmethod
98 @classmethod
99 def hash_string(cls, str_):
99 def hash_string(cls, str_):
100 """
100 """
101 Cryptographic function used for password hashing based on pybcrypt
101 Cryptographic function used for password hashing based on pybcrypt
102 or Python's own OpenSSL wrapper on windows
102 or Python's own OpenSSL wrapper on windows
103
103
104 :param password: password to hash
104 :param password: password to hash
105 """
105 """
106 if is_windows:
106 if is_windows:
107 return hashlib.sha256(str_).hexdigest()
107 return hashlib.sha256(str_).hexdigest()
108 elif is_unix:
108 elif is_unix:
109 import bcrypt
109 import bcrypt
110 return bcrypt.hashpw(safe_str(str_), bcrypt.gensalt(10))
110 return bcrypt.hashpw(safe_str(str_), bcrypt.gensalt(10))
111 else:
111 else:
112 raise Exception('Unknown or unsupported platform %s' \
112 raise Exception('Unknown or unsupported platform %s' \
113 % __platform__)
113 % __platform__)
114
114
115 @classmethod
115 @classmethod
116 def hash_check(cls, password, hashed):
116 def hash_check(cls, password, hashed):
117 """
117 """
118 Checks matching password with it's hashed value, runs different
118 Checks matching password with it's hashed value, runs different
119 implementation based on platform it runs on
119 implementation based on platform it runs on
120
120
121 :param password: password
121 :param password: password
122 :param hashed: password in hashed form
122 :param hashed: password in hashed form
123 """
123 """
124
124
125 if is_windows:
125 if is_windows:
126 return hashlib.sha256(password).hexdigest() == hashed
126 return hashlib.sha256(password).hexdigest() == hashed
127 elif is_unix:
127 elif is_unix:
128 import bcrypt
128 import bcrypt
129 return bcrypt.checkpw(safe_str(password), safe_str(hashed))
129 return bcrypt.checkpw(safe_str(password), safe_str(hashed))
130 else:
130 else:
131 raise Exception('Unknown or unsupported platform %s' \
131 raise Exception('Unknown or unsupported platform %s' \
132 % __platform__)
132 % __platform__)
133
133
134
134
135 def get_crypt_password(password):
135 def get_crypt_password(password):
136 return KallitheaCrypto.hash_string(password)
136 return KallitheaCrypto.hash_string(password)
137
137
138
138
139 def check_password(password, hashed):
139 def check_password(password, hashed):
140 return KallitheaCrypto.hash_check(password, hashed)
140 return KallitheaCrypto.hash_check(password, hashed)
141
141
142
142
143
143
144 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,
145 explicit, algo):
145 explicit, algo):
146 RK = 'repositories'
146 RK = 'repositories'
147 GK = 'repositories_groups'
147 GK = 'repositories_groups'
148 UK = 'user_groups'
148 UK = 'user_groups'
149 GLOBAL = 'global'
149 GLOBAL = 'global'
150 PERM_WEIGHTS = Permission.PERM_WEIGHTS
150 PERM_WEIGHTS = Permission.PERM_WEIGHTS
151 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
151 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
152
152
153 def _choose_perm(new_perm, cur_perm):
153 def _choose_perm(new_perm, cur_perm):
154 new_perm_val = PERM_WEIGHTS[new_perm]
154 new_perm_val = PERM_WEIGHTS[new_perm]
155 cur_perm_val = PERM_WEIGHTS[cur_perm]
155 cur_perm_val = PERM_WEIGHTS[cur_perm]
156 if algo == 'higherwin':
156 if algo == 'higherwin':
157 if new_perm_val > cur_perm_val:
157 if new_perm_val > cur_perm_val:
158 return new_perm
158 return new_perm
159 return cur_perm
159 return cur_perm
160 elif algo == 'lowerwin':
160 elif algo == 'lowerwin':
161 if new_perm_val < cur_perm_val:
161 if new_perm_val < cur_perm_val:
162 return new_perm
162 return new_perm
163 return cur_perm
163 return cur_perm
164
164
165 #======================================================================
165 #======================================================================
166 # fetch default permissions
166 # fetch default permissions
167 #======================================================================
167 #======================================================================
168 default_user = User.get_by_username('default', cache=True)
168 default_user = User.get_by_username('default', cache=True)
169 default_user_id = default_user.user_id
169 default_user_id = default_user.user_id
170
170
171 default_repo_perms = Permission.get_default_perms(default_user_id)
171 default_repo_perms = Permission.get_default_perms(default_user_id)
172 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)
173 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)
174
174
175 if user_is_admin:
175 if user_is_admin:
176 #==================================================================
176 #==================================================================
177 # admin users have all rights;
177 # admin users have all rights;
178 # based on default permissions, just set everything to admin
178 # based on default permissions, just set everything to admin
179 #==================================================================
179 #==================================================================
180 permissions[GLOBAL].add('hg.admin')
180 permissions[GLOBAL].add('hg.admin')
181 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
181 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
182
182
183 # repositories
183 # repositories
184 for perm in default_repo_perms:
184 for perm in default_repo_perms:
185 r_k = perm.UserRepoToPerm.repository.repo_name
185 r_k = perm.UserRepoToPerm.repository.repo_name
186 p = 'repository.admin'
186 p = 'repository.admin'
187 permissions[RK][r_k] = p
187 permissions[RK][r_k] = p
188
188
189 # repository groups
189 # repository groups
190 for perm in default_repo_groups_perms:
190 for perm in default_repo_groups_perms:
191 rg_k = perm.UserRepoGroupToPerm.group.group_name
191 rg_k = perm.UserRepoGroupToPerm.group.group_name
192 p = 'group.admin'
192 p = 'group.admin'
193 permissions[GK][rg_k] = p
193 permissions[GK][rg_k] = p
194
194
195 # user groups
195 # user groups
196 for perm in default_user_group_perms:
196 for perm in default_user_group_perms:
197 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
197 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
198 p = 'usergroup.admin'
198 p = 'usergroup.admin'
199 permissions[UK][u_k] = p
199 permissions[UK][u_k] = p
200 return permissions
200 return permissions
201
201
202 #==================================================================
202 #==================================================================
203 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
203 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
204 #==================================================================
204 #==================================================================
205
205
206 # default global permissions taken from the default user
206 # default global permissions taken from the default user
207 default_global_perms = UserToPerm.query() \
207 default_global_perms = UserToPerm.query() \
208 .filter(UserToPerm.user_id == default_user_id) \
208 .filter(UserToPerm.user_id == default_user_id) \
209 .options(joinedload(UserToPerm.permission))
209 .options(joinedload(UserToPerm.permission))
210
210
211 for perm in default_global_perms:
211 for perm in default_global_perms:
212 permissions[GLOBAL].add(perm.permission.permission_name)
212 permissions[GLOBAL].add(perm.permission.permission_name)
213
213
214 # defaults for repositories, taken from default user
214 # defaults for repositories, taken from default user
215 for perm in default_repo_perms:
215 for perm in default_repo_perms:
216 r_k = perm.UserRepoToPerm.repository.repo_name
216 r_k = perm.UserRepoToPerm.repository.repo_name
217 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):
218 # disable defaults for private repos,
218 # disable defaults for private repos,
219 p = 'repository.none'
219 p = 'repository.none'
220 elif perm.Repository.user_id == user_id:
220 elif perm.Repository.user_id == user_id:
221 # set admin if owner
221 # set admin if owner
222 p = 'repository.admin'
222 p = 'repository.admin'
223 else:
223 else:
224 p = perm.Permission.permission_name
224 p = perm.Permission.permission_name
225
225
226 permissions[RK][r_k] = p
226 permissions[RK][r_k] = p
227
227
228 # defaults for repository groups taken from default user permission
228 # defaults for repository groups taken from default user permission
229 # on given group
229 # on given group
230 for perm in default_repo_groups_perms:
230 for perm in default_repo_groups_perms:
231 rg_k = perm.UserRepoGroupToPerm.group.group_name
231 rg_k = perm.UserRepoGroupToPerm.group.group_name
232 p = perm.Permission.permission_name
232 p = perm.Permission.permission_name
233 permissions[GK][rg_k] = p
233 permissions[GK][rg_k] = p
234
234
235 # defaults for user groups taken from default user permission
235 # defaults for user groups taken from default user permission
236 # on given user group
236 # on given user group
237 for perm in default_user_group_perms:
237 for perm in default_user_group_perms:
238 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
238 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
239 p = perm.Permission.permission_name
239 p = perm.Permission.permission_name
240 permissions[UK][u_k] = p
240 permissions[UK][u_k] = p
241
241
242 #======================================================================
242 #======================================================================
243 # !! OVERRIDE GLOBALS !! with user permissions if any found
243 # !! OVERRIDE GLOBALS !! with user permissions if any found
244 #======================================================================
244 #======================================================================
245 # those can be configured from groups or users explicitly
245 # those can be configured from groups or users explicitly
246 _configurable = set([
246 _configurable = set([
247 'hg.fork.none', 'hg.fork.repository',
247 'hg.fork.none', 'hg.fork.repository',
248 'hg.create.none', 'hg.create.repository',
248 'hg.create.none', 'hg.create.repository',
249 'hg.usergroup.create.false', 'hg.usergroup.create.true'
249 'hg.usergroup.create.false', 'hg.usergroup.create.true'
250 ])
250 ])
251
251
252 # USER GROUPS comes first
252 # USER GROUPS comes first
253 # user group global permissions
253 # user group global permissions
254 user_perms_from_users_groups = Session().query(UserGroupToPerm) \
254 user_perms_from_users_groups = Session().query(UserGroupToPerm) \
255 .options(joinedload(UserGroupToPerm.permission)) \
255 .options(joinedload(UserGroupToPerm.permission)) \
256 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
256 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
257 UserGroupMember.users_group_id)) \
257 UserGroupMember.users_group_id)) \
258 .filter(UserGroupMember.user_id == user_id) \
258 .filter(UserGroupMember.user_id == user_id) \
259 .join((UserGroup, UserGroupMember.users_group_id ==
259 .join((UserGroup, UserGroupMember.users_group_id ==
260 UserGroup.users_group_id)) \
260 UserGroup.users_group_id)) \
261 .filter(UserGroup.users_group_active == True) \
261 .filter(UserGroup.users_group_active == True) \
262 .order_by(UserGroupToPerm.users_group_id) \
262 .order_by(UserGroupToPerm.users_group_id) \
263 .all()
263 .all()
264 # 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
265 # one group
265 # one group
266 _grouped = [[x, list(y)] for x, y in
266 _grouped = [[x, list(y)] for x, y in
267 itertools.groupby(user_perms_from_users_groups,
267 itertools.groupby(user_perms_from_users_groups,
268 lambda x:x.users_group)]
268 lambda x:x.users_group)]
269 for gr, perms in _grouped:
269 for gr, perms in _grouped:
270 # since user can be in multiple groups iterate over them and
270 # since user can be in multiple groups iterate over them and
271 # select the lowest permissions first (more explicit)
271 # select the lowest permissions first (more explicit)
272 ##TODO: do this^^
272 ##TODO: do this^^
273 if not gr.inherit_default_permissions:
273 if not gr.inherit_default_permissions:
274 # NEED TO IGNORE all configurable permissions and
274 # NEED TO IGNORE all configurable permissions and
275 # replace them with explicitly set
275 # replace them with explicitly set
276 permissions[GLOBAL] = permissions[GLOBAL] \
276 permissions[GLOBAL] = permissions[GLOBAL] \
277 .difference(_configurable)
277 .difference(_configurable)
278 for perm in perms:
278 for perm in perms:
279 permissions[GLOBAL].add(perm.permission.permission_name)
279 permissions[GLOBAL].add(perm.permission.permission_name)
280
280
281 # user specific global permissions
281 # user specific global permissions
282 user_perms = Session().query(UserToPerm) \
282 user_perms = Session().query(UserToPerm) \
283 .options(joinedload(UserToPerm.permission)) \
283 .options(joinedload(UserToPerm.permission)) \
284 .filter(UserToPerm.user_id == user_id).all()
284 .filter(UserToPerm.user_id == user_id).all()
285
285
286 if not user_inherit_default_permissions:
286 if not user_inherit_default_permissions:
287 # NEED TO IGNORE all configurable permissions and
287 # NEED TO IGNORE all configurable permissions and
288 # replace them with explicitly set
288 # replace them with explicitly set
289 permissions[GLOBAL] = permissions[GLOBAL] \
289 permissions[GLOBAL] = permissions[GLOBAL] \
290 .difference(_configurable)
290 .difference(_configurable)
291
291
292 for perm in user_perms:
292 for perm in user_perms:
293 permissions[GLOBAL].add(perm.permission.permission_name)
293 permissions[GLOBAL].add(perm.permission.permission_name)
294 ## END GLOBAL PERMISSIONS
294 ## END GLOBAL PERMISSIONS
295
295
296 #======================================================================
296 #======================================================================
297 # !! PERMISSIONS FOR REPOSITORIES !!
297 # !! PERMISSIONS FOR REPOSITORIES !!
298 #======================================================================
298 #======================================================================
299 #======================================================================
299 #======================================================================
300 # 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
301 # fill in his permission from it. _choose_perm decides of which
301 # fill in his permission from it. _choose_perm decides of which
302 # permission should be selected based on selected method
302 # permission should be selected based on selected method
303 #======================================================================
303 #======================================================================
304
304
305 # user group for repositories permissions
305 # user group for repositories permissions
306 user_repo_perms_from_users_groups = \
306 user_repo_perms_from_users_groups = \
307 Session().query(UserGroupRepoToPerm, Permission, Repository,) \
307 Session().query(UserGroupRepoToPerm, Permission, Repository,) \
308 .join((Repository, UserGroupRepoToPerm.repository_id ==
308 .join((Repository, UserGroupRepoToPerm.repository_id ==
309 Repository.repo_id)) \
309 Repository.repo_id)) \
310 .join((Permission, UserGroupRepoToPerm.permission_id ==
310 .join((Permission, UserGroupRepoToPerm.permission_id ==
311 Permission.permission_id)) \
311 Permission.permission_id)) \
312 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
312 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
313 UserGroup.users_group_id)) \
313 UserGroup.users_group_id)) \
314 .filter(UserGroup.users_group_active == True) \
314 .filter(UserGroup.users_group_active == True) \
315 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
315 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
316 UserGroupMember.users_group_id)) \
316 UserGroupMember.users_group_id)) \
317 .filter(UserGroupMember.user_id == user_id) \
317 .filter(UserGroupMember.user_id == user_id) \
318 .all()
318 .all()
319
319
320 multiple_counter = collections.defaultdict(int)
320 multiple_counter = collections.defaultdict(int)
321 for perm in user_repo_perms_from_users_groups:
321 for perm in user_repo_perms_from_users_groups:
322 r_k = perm.UserGroupRepoToPerm.repository.repo_name
322 r_k = perm.UserGroupRepoToPerm.repository.repo_name
323 multiple_counter[r_k] += 1
323 multiple_counter[r_k] += 1
324 p = perm.Permission.permission_name
324 p = perm.Permission.permission_name
325 cur_perm = permissions[RK][r_k]
325 cur_perm = permissions[RK][r_k]
326
326
327 if perm.Repository.user_id == user_id:
327 if perm.Repository.user_id == user_id:
328 # set admin if owner
328 # set admin if owner
329 p = 'repository.admin'
329 p = 'repository.admin'
330 else:
330 else:
331 if multiple_counter[r_k] > 1:
331 if multiple_counter[r_k] > 1:
332 p = _choose_perm(p, cur_perm)
332 p = _choose_perm(p, cur_perm)
333 permissions[RK][r_k] = p
333 permissions[RK][r_k] = p
334
334
335 # user explicit permissions for repositories, overrides any specified
335 # user explicit permissions for repositories, overrides any specified
336 # by the group permission
336 # by the group permission
337 user_repo_perms = Permission.get_default_perms(user_id)
337 user_repo_perms = Permission.get_default_perms(user_id)
338 for perm in user_repo_perms:
338 for perm in user_repo_perms:
339 r_k = perm.UserRepoToPerm.repository.repo_name
339 r_k = perm.UserRepoToPerm.repository.repo_name
340 cur_perm = permissions[RK][r_k]
340 cur_perm = permissions[RK][r_k]
341 # set admin if owner
341 # set admin if owner
342 if perm.Repository.user_id == user_id:
342 if perm.Repository.user_id == user_id:
343 p = 'repository.admin'
343 p = 'repository.admin'
344 else:
344 else:
345 p = perm.Permission.permission_name
345 p = perm.Permission.permission_name
346 if not explicit:
346 if not explicit:
347 p = _choose_perm(p, cur_perm)
347 p = _choose_perm(p, cur_perm)
348 permissions[RK][r_k] = p
348 permissions[RK][r_k] = p
349
349
350 #======================================================================
350 #======================================================================
351 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
351 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
352 #======================================================================
352 #======================================================================
353 #======================================================================
353 #======================================================================
354 # 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
355 # fill in his permission from it. _choose_perm decides of which
355 # fill in his permission from it. _choose_perm decides of which
356 # permission should be selected based on selected method
356 # permission should be selected based on selected method
357 #======================================================================
357 #======================================================================
358 # user group for repo groups permissions
358 # user group for repo groups permissions
359 user_repo_group_perms_from_users_groups = \
359 user_repo_group_perms_from_users_groups = \
360 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
360 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
361 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
361 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
362 .join((Permission, UserGroupRepoGroupToPerm.permission_id
362 .join((Permission, UserGroupRepoGroupToPerm.permission_id
363 == Permission.permission_id)) \
363 == Permission.permission_id)) \
364 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
364 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
365 UserGroup.users_group_id)) \
365 UserGroup.users_group_id)) \
366 .filter(UserGroup.users_group_active == True) \
366 .filter(UserGroup.users_group_active == True) \
367 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
367 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
368 == UserGroupMember.users_group_id)) \
368 == UserGroupMember.users_group_id)) \
369 .filter(UserGroupMember.user_id == user_id) \
369 .filter(UserGroupMember.user_id == user_id) \
370 .all()
370 .all()
371
371
372 multiple_counter = collections.defaultdict(int)
372 multiple_counter = collections.defaultdict(int)
373 for perm in user_repo_group_perms_from_users_groups:
373 for perm in user_repo_group_perms_from_users_groups:
374 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
374 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
375 multiple_counter[g_k] += 1
375 multiple_counter[g_k] += 1
376 p = perm.Permission.permission_name
376 p = perm.Permission.permission_name
377 cur_perm = permissions[GK][g_k]
377 cur_perm = permissions[GK][g_k]
378 if multiple_counter[g_k] > 1:
378 if multiple_counter[g_k] > 1:
379 p = _choose_perm(p, cur_perm)
379 p = _choose_perm(p, cur_perm)
380 permissions[GK][g_k] = p
380 permissions[GK][g_k] = p
381
381
382 # user explicit permissions for repository groups
382 # user explicit permissions for repository groups
383 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
383 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
384 for perm in user_repo_groups_perms:
384 for perm in user_repo_groups_perms:
385 rg_k = perm.UserRepoGroupToPerm.group.group_name
385 rg_k = perm.UserRepoGroupToPerm.group.group_name
386 p = perm.Permission.permission_name
386 p = perm.Permission.permission_name
387 cur_perm = permissions[GK][rg_k]
387 cur_perm = permissions[GK][rg_k]
388 if not explicit:
388 if not explicit:
389 p = _choose_perm(p, cur_perm)
389 p = _choose_perm(p, cur_perm)
390 permissions[GK][rg_k] = p
390 permissions[GK][rg_k] = p
391
391
392 #======================================================================
392 #======================================================================
393 # !! PERMISSIONS FOR USER GROUPS !!
393 # !! PERMISSIONS FOR USER GROUPS !!
394 #======================================================================
394 #======================================================================
395 # user group for user group permissions
395 # user group for user group permissions
396 user_group_user_groups_perms = \
396 user_group_user_groups_perms = \
397 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
397 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
398 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
398 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
399 == UserGroup.users_group_id)) \
399 == UserGroup.users_group_id)) \
400 .join((Permission, UserGroupUserGroupToPerm.permission_id
400 .join((Permission, UserGroupUserGroupToPerm.permission_id
401 == Permission.permission_id)) \
401 == Permission.permission_id)) \
402 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
402 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
403 == UserGroupMember.users_group_id)) \
403 == UserGroupMember.users_group_id)) \
404 .filter(UserGroupMember.user_id == user_id) \
404 .filter(UserGroupMember.user_id == user_id) \
405 .join((UserGroup, UserGroupMember.users_group_id ==
405 .join((UserGroup, UserGroupMember.users_group_id ==
406 UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
406 UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
407 .filter(UserGroup.users_group_active == True) \
407 .filter(UserGroup.users_group_active == True) \
408 .all()
408 .all()
409
409
410 multiple_counter = collections.defaultdict(int)
410 multiple_counter = collections.defaultdict(int)
411 for perm in user_group_user_groups_perms:
411 for perm in user_group_user_groups_perms:
412 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
412 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
413 multiple_counter[g_k] += 1
413 multiple_counter[g_k] += 1
414 p = perm.Permission.permission_name
414 p = perm.Permission.permission_name
415 cur_perm = permissions[UK][g_k]
415 cur_perm = permissions[UK][g_k]
416 if multiple_counter[g_k] > 1:
416 if multiple_counter[g_k] > 1:
417 p = _choose_perm(p, cur_perm)
417 p = _choose_perm(p, cur_perm)
418 permissions[UK][g_k] = p
418 permissions[UK][g_k] = p
419
419
420 #user explicit permission for user groups
420 #user explicit permission for user groups
421 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)
422 for perm in user_user_groups_perms:
422 for perm in user_user_groups_perms:
423 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
423 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
424 p = perm.Permission.permission_name
424 p = perm.Permission.permission_name
425 cur_perm = permissions[UK][u_k]
425 cur_perm = permissions[UK][u_k]
426 if not explicit:
426 if not explicit:
427 p = _choose_perm(p, cur_perm)
427 p = _choose_perm(p, cur_perm)
428 permissions[UK][u_k] = p
428 permissions[UK][u_k] = p
429
429
430 return permissions
430 return permissions
431
431
432
432
433 def allowed_api_access(controller_name, whitelist=None, api_key=None):
433 def allowed_api_access(controller_name, whitelist=None, api_key=None):
434 """
434 """
435 Check if given controller_name is in whitelist API access
435 Check if given controller_name is in whitelist API access
436 """
436 """
437 if not whitelist:
437 if not whitelist:
438 from kallithea import CONFIG
438 from kallithea import CONFIG
439 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
439 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
440 sep=',')
440 sep=',')
441 log.debug('whitelist of API access is: %s', whitelist)
441 log.debug('whitelist of API access is: %s', whitelist)
442 api_access_valid = controller_name in whitelist
442 api_access_valid = controller_name in whitelist
443 if api_access_valid:
443 if api_access_valid:
444 log.debug('controller:%s is in API whitelist', controller_name)
444 log.debug('controller:%s is in API whitelist', controller_name)
445 else:
445 else:
446 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
446 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
447 if api_key:
447 if api_key:
448 #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
449 log.warning(msg)
449 log.warning(msg)
450 else:
450 else:
451 log.debug(msg)
451 log.debug(msg)
452 return api_access_valid
452 return api_access_valid
453
453
454
454
455 class AuthUser(object):
455 class AuthUser(object):
456 """
456 """
457 Represents a Kallithea user, including various authentication and
457 Represents a Kallithea user, including various authentication and
458 authorization information. Typically used to store the current user,
458 authorization information. Typically used to store the current user,
459 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
460 parts of the code, e.g. user management.
460 parts of the code, e.g. user management.
461
461
462 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,
463 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,
464 adding various non-persistent data. If lookup fails but anonymous
464 adding various non-persistent data. If lookup fails but anonymous
465 access to Kallithea is enabled, the default user is loaded instead.
465 access to Kallithea is enabled, the default user is loaded instead.
466
466
467 `AuthUser` does not by itself authenticate users and the constructor
467 `AuthUser` does not by itself authenticate users and the constructor
468 sets the `is_authenticated` field to False. It's up to other parts
468 sets the `is_authenticated` field to False. It's up to other parts
469 of the code to check e.g. if a supplied password is correct, and if
469 of the code to check e.g. if a supplied password is correct, and if
470 so, set `is_authenticated` to True.
470 so, set `is_authenticated` to True.
471
471
472 However, `AuthUser` does refuse to load a user that is not `active`.
472 However, `AuthUser` does refuse to load a user that is not `active`.
473 """
473 """
474
474
475 def __init__(self, user_id=None, dbuser=None,
475 def __init__(self, user_id=None, dbuser=None,
476 is_external_auth=False):
476 is_external_auth=False):
477
477
478 self.is_authenticated = False
478 self.is_authenticated = False
479 self.is_external_auth = is_external_auth
479 self.is_external_auth = is_external_auth
480
480
481 user_model = UserModel()
481 user_model = UserModel()
482 self.anonymous_user = User.get_default_user(cache=True)
482 self.anonymous_user = User.get_default_user(cache=True)
483
483
484 # These attributes will be overridden by fill_data, below, unless the
484 # These attributes will be overridden by fill_data, below, unless the
485 # requested user cannot be found and the default anonymous user is
485 # requested user cannot be found and the default anonymous user is
486 # not enabled.
486 # not enabled.
487 self.user_id = None
487 self.user_id = None
488 self.username = None
488 self.username = None
489 self.api_key = None
489 self.api_key = None
490 self.name = ''
490 self.name = ''
491 self.lastname = ''
491 self.lastname = ''
492 self.email = ''
492 self.email = ''
493 self.admin = False
493 self.admin = False
494 self.inherit_default_permissions = False
494 self.inherit_default_permissions = False
495
495
496 # Look up database user, if necessary.
496 # Look up database user, if necessary.
497 if user_id is not None:
497 if user_id is not None:
498 log.debug('Auth User lookup by USER ID %s', user_id)
498 log.debug('Auth User lookup by USER ID %s', user_id)
499 dbuser = user_model.get(user_id)
499 dbuser = user_model.get(user_id)
500 else:
500 else:
501 # Note: dbuser is allowed to be None.
501 # Note: dbuser is allowed to be None.
502 log.debug('Auth User lookup by database user %s', dbuser)
502 log.debug('Auth User lookup by database user %s', dbuser)
503
503
504 is_user_loaded = self._fill_data(dbuser)
504 is_user_loaded = self._fill_data(dbuser)
505
505
506 # If user cannot be found, try falling back to anonymous.
506 # If user cannot be found, try falling back to anonymous.
507 if not is_user_loaded:
507 if not is_user_loaded:
508 is_user_loaded = self._fill_data(self.anonymous_user)
508 is_user_loaded = self._fill_data(self.anonymous_user)
509
509
510 self.is_default_user = (self.user_id == self.anonymous_user.user_id)
510 self.is_default_user = (self.user_id == self.anonymous_user.user_id)
511
511
512 if not self.username:
512 if not self.username:
513 self.username = 'None'
513 self.username = 'None'
514
514
515 log.debug('Auth User is now %s', self)
515 log.debug('Auth User is now %s', self)
516
516
517 def _fill_data(self, dbuser):
517 def _fill_data(self, dbuser):
518 """
518 """
519 Copies database fields from a `db.User` to this `AuthUser`. Does
519 Copies database fields from a `db.User` to this `AuthUser`. Does
520 not copy `api_keys` and `permissions` attributes.
520 not copy `api_keys` and `permissions` attributes.
521
521
522 Checks that `dbuser` is `active` (and not None) before copying;
522 Checks that `dbuser` is `active` (and not None) before copying;
523 returns True on success.
523 returns True on success.
524 """
524 """
525 if dbuser is not None and dbuser.active:
525 if dbuser is not None and dbuser.active:
526 log.debug('filling %s data', dbuser)
526 log.debug('filling %s data', dbuser)
527 for k, v in dbuser.get_dict().iteritems():
527 for k, v in dbuser.get_dict().iteritems():
528 assert k not in ['api_keys', 'permissions']
528 assert k not in ['api_keys', 'permissions']
529 setattr(self, k, v)
529 setattr(self, k, v)
530 return True
530 return True
531 return False
531 return False
532
532
533 @LazyProperty
533 @LazyProperty
534 def permissions(self):
534 def permissions(self):
535 return self.__get_perms(user=self, cache=False)
535 return self.__get_perms(user=self, cache=False)
536
536
537 @property
537 @property
538 def api_keys(self):
538 def api_keys(self):
539 return self._get_api_keys()
539 return self._get_api_keys()
540
540
541 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
541 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
542 """
542 """
543 Fills user permission attribute with permissions taken from database
543 Fills user permission attribute with permissions taken from database
544 works for permissions given for repositories, and for permissions that
544 works for permissions given for repositories, and for permissions that
545 are granted to groups
545 are granted to groups
546
546
547 :param user: `AuthUser` instance
547 :param user: `AuthUser` instance
548 :param explicit: In case there are permissions both for user and a group
548 :param explicit: In case there are permissions both for user and a group
549 that user is part of, explicit flag will define if user will
549 that user is part of, explicit flag will define if user will
550 explicitly override permissions from group, if it's False it will
550 explicitly override permissions from group, if it's False it will
551 make decision based on the algo
551 make decision based on the algo
552 :param algo: algorithm to decide what permission should be choose if
552 :param algo: algorithm to decide what permission should be choose if
553 it's multiple defined, eg user in two different groups. It also
553 it's multiple defined, eg user in two different groups. It also
554 decides if explicit flag is turned off how to specify the permission
554 decides if explicit flag is turned off how to specify the permission
555 for case when user is in a group + have defined separate permission
555 for case when user is in a group + have defined separate permission
556 """
556 """
557 user_id = user.user_id
557 user_id = user.user_id
558 user_is_admin = user.is_admin
558 user_is_admin = user.is_admin
559 user_inherit_default_permissions = user.inherit_default_permissions
559 user_inherit_default_permissions = user.inherit_default_permissions
560
560
561 log.debug('Getting PERMISSION tree')
561 log.debug('Getting PERMISSION tree')
562 compute = conditional_cache('short_term', 'cache_desc',
562 compute = conditional_cache('short_term', 'cache_desc',
563 condition=cache, func=_cached_perms_data)
563 condition=cache, func=_cached_perms_data)
564 return compute(user_id, user_is_admin,
564 return compute(user_id, user_is_admin,
565 user_inherit_default_permissions, explicit, algo)
565 user_inherit_default_permissions, explicit, algo)
566
566
567 def _get_api_keys(self):
567 def _get_api_keys(self):
568 api_keys = [self.api_key]
568 api_keys = [self.api_key]
569 for api_key in UserApiKeys.query() \
569 for api_key in UserApiKeys.query() \
570 .filter(UserApiKeys.user_id == self.user_id) \
570 .filter(UserApiKeys.user_id == self.user_id) \
571 .filter(or_(UserApiKeys.expires == -1,
571 .filter(or_(UserApiKeys.expires == -1,
572 UserApiKeys.expires >= time.time())).all():
572 UserApiKeys.expires >= time.time())).all():
573 api_keys.append(api_key.api_key)
573 api_keys.append(api_key.api_key)
574
574
575 return api_keys
575 return api_keys
576
576
577 @property
577 @property
578 def is_admin(self):
578 def is_admin(self):
579 return self.admin
579 return self.admin
580
580
581 @property
581 @property
582 def repositories_admin(self):
582 def repositories_admin(self):
583 """
583 """
584 Returns list of repositories you're an admin of
584 Returns list of repositories you're an admin of
585 """
585 """
586 return [x[0] for x in self.permissions['repositories'].iteritems()
586 return [x[0] for x in self.permissions['repositories'].iteritems()
587 if x[1] == 'repository.admin']
587 if x[1] == 'repository.admin']
588
588
589 @property
589 @property
590 def repository_groups_admin(self):
590 def repository_groups_admin(self):
591 """
591 """
592 Returns list of repository groups you're an admin of
592 Returns list of repository groups you're an admin of
593 """
593 """
594 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
594 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
595 if x[1] == 'group.admin']
595 if x[1] == 'group.admin']
596
596
597 @property
597 @property
598 def user_groups_admin(self):
598 def user_groups_admin(self):
599 """
599 """
600 Returns list of user groups you're an admin of
600 Returns list of user groups you're an admin of
601 """
601 """
602 return [x[0] for x in self.permissions['user_groups'].iteritems()
602 return [x[0] for x in self.permissions['user_groups'].iteritems()
603 if x[1] == 'usergroup.admin']
603 if x[1] == 'usergroup.admin']
604
604
605 @staticmethod
605 @staticmethod
606 def check_ip_allowed(user, ip_addr):
606 def check_ip_allowed(user, ip_addr):
607 """
607 """
608 Check if the given IP address (a `str`) is allowed for the given
608 Check if the given IP address (a `str`) is allowed for the given
609 user (an `AuthUser` or `db.User`).
609 user (an `AuthUser` or `db.User`).
610 """
610 """
611 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
611 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
612 inherit_from_default=user.inherit_default_permissions)
612 inherit_from_default=user.inherit_default_permissions)
613 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
613 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
614 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
614 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
615 return True
615 return True
616 else:
616 else:
617 log.info('Access for IP:%s forbidden, '
617 log.info('Access for IP:%s forbidden, '
618 'not in %s' % (ip_addr, allowed_ips))
618 'not in %s' % (ip_addr, allowed_ips))
619 return False
619 return False
620
620
621 def __repr__(self):
621 def __repr__(self):
622 return "<AuthUser('id:%s[%s] auth:%s')>" \
622 return "<AuthUser('id:%s[%s] auth:%s')>" \
623 % (self.user_id, self.username, (self.is_authenticated or self.is_default_user))
623 % (self.user_id, self.username, (self.is_authenticated or self.is_default_user))
624
624
625 def to_cookie(self):
625 def to_cookie(self):
626 """ Serializes this login session to a cookie `dict`. """
626 """ Serializes this login session to a cookie `dict`. """
627 return {
627 return {
628 'user_id': self.user_id,
628 'user_id': self.user_id,
629 'is_external_auth': self.is_external_auth,
629 'is_external_auth': self.is_external_auth,
630 }
630 }
631
631
632 @staticmethod
632 @staticmethod
633 def from_cookie(cookie):
633 def from_cookie(cookie):
634 """
634 """
635 Deserializes an `AuthUser` from a cookie `dict`.
635 Deserializes an `AuthUser` from a cookie `dict`.
636 """
636 """
637
637
638 au = AuthUser(
638 au = AuthUser(
639 user_id=cookie.get('user_id'),
639 user_id=cookie.get('user_id'),
640 is_external_auth=cookie.get('is_external_auth', False),
640 is_external_auth=cookie.get('is_external_auth', False),
641 )
641 )
642 au.is_authenticated = True
642 au.is_authenticated = True
643 return au
643 return au
644
644
645 @classmethod
645 @classmethod
646 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
646 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
647 _set = set()
647 _set = set()
648
648
649 if inherit_from_default:
649 if inherit_from_default:
650 default_ips = UserIpMap.query().filter(UserIpMap.user ==
650 default_ips = UserIpMap.query().filter(UserIpMap.user ==
651 User.get_default_user(cache=True))
651 User.get_default_user(cache=True))
652 if cache:
652 if cache:
653 default_ips = default_ips.options(FromCache("sql_cache_short",
653 default_ips = default_ips.options(FromCache("sql_cache_short",
654 "get_user_ips_default"))
654 "get_user_ips_default"))
655
655
656 # populate from default user
656 # populate from default user
657 for ip in default_ips:
657 for ip in default_ips:
658 try:
658 try:
659 _set.add(ip.ip_addr)
659 _set.add(ip.ip_addr)
660 except ObjectDeletedError:
660 except ObjectDeletedError:
661 # since we use heavy caching sometimes it happens that we get
661 # since we use heavy caching sometimes it happens that we get
662 # deleted objects here, we just skip them
662 # deleted objects here, we just skip them
663 pass
663 pass
664
664
665 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
665 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
666 if cache:
666 if cache:
667 user_ips = user_ips.options(FromCache("sql_cache_short",
667 user_ips = user_ips.options(FromCache("sql_cache_short",
668 "get_user_ips_%s" % user_id))
668 "get_user_ips_%s" % user_id))
669
669
670 for ip in user_ips:
670 for ip in user_ips:
671 try:
671 try:
672 _set.add(ip.ip_addr)
672 _set.add(ip.ip_addr)
673 except ObjectDeletedError:
673 except ObjectDeletedError:
674 # since we use heavy caching sometimes it happens that we get
674 # since we use heavy caching sometimes it happens that we get
675 # deleted objects here, we just skip them
675 # deleted objects here, we just skip them
676 pass
676 pass
677 return _set or set(['0.0.0.0/0', '::/0'])
677 return _set or set(['0.0.0.0/0', '::/0'])
678
678
679
679
680 def set_available_permissions(config):
680 def set_available_permissions(config):
681 """
681 """
682 This function will propagate pylons globals with all available defined
682 This function will propagate pylons globals with all available defined
683 permission given in db. We don't want to check each time from db for new
683 permission given in db. We don't want to check each time from db for new
684 permissions since adding a new permission also requires application restart
684 permissions since adding a new permission also requires application restart
685 ie. to decorate new views with the newly created permission
685 ie. to decorate new views with the newly created permission
686
686
687 :param config: current pylons config instance
687 :param config: current pylons config instance
688
688
689 """
689 """
690 log.info('getting information about all available permissions')
690 log.info('getting information about all available permissions')
691 try:
691 try:
692 sa = meta.Session
692 sa = meta.Session
693 all_perms = sa.query(Permission).all()
693 all_perms = sa.query(Permission).all()
694 config['available_permissions'] = [x.permission_name for x in all_perms]
694 config['available_permissions'] = [x.permission_name for x in all_perms]
695 finally:
695 finally:
696 meta.Session.remove()
696 meta.Session.remove()
697
697
698
698
699 #==============================================================================
699 #==============================================================================
700 # CHECK DECORATORS
700 # CHECK DECORATORS
701 #==============================================================================
701 #==============================================================================
702
702
703 def _redirect_to_login(message=None):
703 def _redirect_to_login(message=None):
704 """Return an exception that must be raised. It will redirect to the login
704 """Return an exception that must be raised. It will redirect to the login
705 page which will redirect back to the current URL after authentication.
705 page which will redirect back to the current URL after authentication.
706 The optional message will be shown in a flash message."""
706 The optional message will be shown in a flash message."""
707 from kallithea.lib import helpers as h
707 from kallithea.lib import helpers as h
708 if message:
708 if message:
709 h.flash(h.literal(message), category='warning')
709 h.flash(h.literal(message), category='warning')
710 p = request.path_qs
710 p = request.path_qs
711 log.debug('Redirecting to login page, origin: %s', p)
711 log.debug('Redirecting to login page, origin: %s', p)
712 return HTTPFound(location=url('login_home', came_from=p))
712 return HTTPFound(location=url('login_home', came_from=p))
713
713
714
714
715 class LoginRequired(object):
715 class LoginRequired(object):
716 """
716 """
717 Must be logged in to execute this function else
717 Must be logged in to execute this function else
718 redirect to login page
718 redirect to login page
719
719
720 :param api_access: if enabled this checks only for valid auth token
720 :param api_access: if enabled this checks only for valid auth token
721 and grants access based on valid token
721 and grants access based on valid token
722 """
722 """
723
723
724 def __init__(self, api_access=False):
724 def __init__(self, api_access=False):
725 self.api_access = api_access
725 self.api_access = api_access
726
726
727 def __call__(self, func):
727 def __call__(self, func):
728 return decorator(self.__wrapper, func)
728 return decorator(self.__wrapper, func)
729
729
730 def __wrapper(self, func, *fargs, **fkwargs):
730 def __wrapper(self, func, *fargs, **fkwargs):
731 controller = fargs[0]
731 controller = fargs[0]
732 user = controller.authuser
732 user = controller.authuser
733 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
733 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
734 log.debug('Checking access for user %s @ %s', user, loc)
734 log.debug('Checking access for user %s @ %s', user, loc)
735
735
736 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
736 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
737 raise _redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
737 raise _redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
738
738
739 # check if we used an API key and it's a valid one
739 # check if we used an API key and it's a valid one
740 api_key = request.GET.get('api_key')
740 api_key = request.GET.get('api_key')
741 if api_key is not None:
741 if api_key is not None:
742 # explicit controller is enabled or API is in our whitelist
742 # explicit controller is enabled or API is in our whitelist
743 if self.api_access or allowed_api_access(loc, api_key=api_key):
743 if self.api_access or allowed_api_access(loc, api_key=api_key):
744 if api_key in user.api_keys:
744 if api_key in user.api_keys:
745 log.info('user %s authenticated with API key ****%s @ %s',
745 log.info('user %s authenticated with API key ****%s @ %s',
746 user, api_key[-4:], loc)
746 user, api_key[-4:], loc)
747 return func(*fargs, **fkwargs)
747 return func(*fargs, **fkwargs)
748 else:
748 else:
749 log.warning('API key ****%s is NOT valid', api_key[-4:])
749 log.warning('API key ****%s is NOT valid', api_key[-4:])
750 raise _redirect_to_login(_('Invalid API key'))
750 raise _redirect_to_login(_('Invalid API key'))
751 else:
751 else:
752 # controller does not allow API access
752 # controller does not allow API access
753 log.warning('API access to %s is not allowed', loc)
753 log.warning('API access to %s is not allowed', loc)
754 raise HTTPForbidden()
754 raise HTTPForbidden()
755
755
756 # Only allow the following HTTP request methods. (We sometimes use POST
756 # Only allow the following HTTP request methods. (We sometimes use POST
757 # requests with a '_method' set to 'PUT' or 'DELETE'; but that is only
757 # requests with a '_method' set to 'PUT' or 'DELETE'; but that is only
758 # used for the route lookup, and does not affect request.method.)
758 # used for the route lookup, and does not affect request.method.)
759 if request.method not in ['GET', 'HEAD', 'POST', 'PUT']:
759 if request.method not in ['GET', 'HEAD', 'POST', 'PUT']:
760 raise HTTPMethodNotAllowed()
760 raise HTTPMethodNotAllowed()
761
761
762 # Also verify the _method override. This is only permitted in POST
762 # Also verify the _method override. This is only permitted in POST
763 # requests, and can specify PUT or DELETE.
763 # requests, and can specify PUT or DELETE.
764 _method = request.params.get('_method')
764 _method = request.params.get('_method')
765 if _method is None:
765 if _method is None:
766 pass # no override, no problem
766 pass # no override, no problem
767 elif request.method == 'POST' and _method.upper() in ['PUT', 'DELETE']:
767 elif request.method == 'POST' and _method.upper() in ['PUT', 'DELETE']:
768 pass # permitted override
768 pass # permitted override
769 else:
769 else:
770 raise HTTPMethodNotAllowed()
770 raise HTTPMethodNotAllowed()
771
771
772 # Make sure CSRF token never appears in the URL. If so, invalidate it.
772 # Make sure CSRF token never appears in the URL. If so, invalidate it.
773 if secure_form.token_key in request.GET:
773 if secure_form.token_key in request.GET:
774 log.error('CSRF key leak detected')
774 log.error('CSRF key leak detected')
775 session.pop(secure_form.token_key, None)
775 session.pop(secure_form.token_key, None)
776 session.save()
776 session.save()
777 from kallithea.lib import helpers as h
777 from kallithea.lib import helpers as h
778 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
778 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
779 category='error')
779 category='error')
780
780
781 # CSRF protection: Whenever a request has ambient authority (whether
781 # CSRF protection: Whenever a request has ambient authority (whether
782 # through a session cookie or its origin IP address), it must include
782 # through a session cookie or its origin IP address), it must include
783 # the correct token, unless the HTTP method is GET or HEAD (and thus
783 # the correct token, unless the HTTP method is GET or HEAD (and thus
784 # guaranteed to be side effect free. In practice, the only situation
784 # guaranteed to be side effect free. In practice, the only situation
785 # where we allow side effects without ambient authority is when the
785 # where we allow side effects without ambient authority is when the
786 # authority comes from an API key; and that is handled above.
786 # authority comes from an API key; and that is handled above.
787 if request.method not in ['GET', 'HEAD']:
787 if request.method not in ['GET', 'HEAD']:
788 token = request.POST.get(secure_form.token_key)
788 token = request.POST.get(secure_form.token_key)
789 if not token or token != secure_form.authentication_token():
789 if not token or token != secure_form.authentication_token():
790 log.error('CSRF check failed')
790 log.error('CSRF check failed')
791 raise HTTPForbidden()
791 raise HTTPForbidden()
792
792
793 # WebOb already ignores request payload parameters for anything other
793 # WebOb already ignores request payload parameters for anything other
794 # than POST/PUT, but double-check since other Kallithea code relies on
794 # than POST/PUT, but double-check since other Kallithea code relies on
795 # this assumption.
795 # this assumption.
796 if request.method not in ['POST', 'PUT'] and request.POST:
796 if request.method not in ['POST', 'PUT'] and request.POST:
797 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
797 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
798 raise HTTPBadRequest()
798 raise HTTPBadRequest()
799
799
800 # regular user authentication
800 # regular user authentication
801 if user.is_authenticated or user.is_default_user:
801 if user.is_authenticated or user.is_default_user:
802 log.info('user %s authenticated with regular auth @ %s', user, loc)
802 log.info('user %s authenticated with regular auth @ %s', user, loc)
803 return func(*fargs, **fkwargs)
803 return func(*fargs, **fkwargs)
804 else:
804 else:
805 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
805 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
806 raise _redirect_to_login()
806 raise _redirect_to_login()
807
807
808 class NotAnonymous(object):
808 class NotAnonymous(object):
809 """
809 """
810 Must be logged in to execute this function else
810 Must be logged in to execute this function else
811 redirect to login page"""
811 redirect to login page"""
812
812
813 def __call__(self, func):
813 def __call__(self, func):
814 return decorator(self.__wrapper, func)
814 return decorator(self.__wrapper, func)
815
815
816 def __wrapper(self, func, *fargs, **fkwargs):
816 def __wrapper(self, func, *fargs, **fkwargs):
817 cls = fargs[0]
817 cls = fargs[0]
818 self.user = cls.authuser
818 self.user = cls.authuser
819
819
820 log.debug('Checking if user is not anonymous @%s', cls)
820 log.debug('Checking if user is not anonymous @%s', cls)
821
821
822 if self.user.is_default_user:
822 if self.user.is_default_user:
823 raise _redirect_to_login(_('You need to be a registered user to '
823 raise _redirect_to_login(_('You need to be a registered user to '
824 'perform this action'))
824 'perform this action'))
825 else:
825 else:
826 return func(*fargs, **fkwargs)
826 return func(*fargs, **fkwargs)
827
827
828
828
829 class PermsDecorator(object):
829 class PermsDecorator(object):
830 """Base class for controller decorators"""
830 """Base class for controller decorators"""
831
831
832 def __init__(self, *required_perms):
832 def __init__(self, *required_perms):
833 self.required_perms = set(required_perms)
833 self.required_perms = set(required_perms)
834 self.user_perms = None
834 self.user_perms = None
835
835
836 def __call__(self, func):
836 def __call__(self, func):
837 return decorator(self.__wrapper, func)
837 return decorator(self.__wrapper, func)
838
838
839 def __wrapper(self, func, *fargs, **fkwargs):
839 def __wrapper(self, func, *fargs, **fkwargs):
840 cls = fargs[0]
840 cls = fargs[0]
841 self.user = cls.authuser
841 self.user = cls.authuser
842 self.user_perms = self.user.permissions
842 self.user_perms = self.user.permissions
843 log.debug('checking %s permissions %s for %s %s',
843 log.debug('checking %s permissions %s for %s %s',
844 self.__class__.__name__, self.required_perms, cls, self.user)
844 self.__class__.__name__, self.required_perms, cls, self.user)
845
845
846 if self.check_permissions():
846 if self.check_permissions():
847 log.debug('Permission granted for %s %s', cls, self.user)
847 log.debug('Permission granted for %s %s', cls, self.user)
848 return func(*fargs, **fkwargs)
848 return func(*fargs, **fkwargs)
849
849
850 else:
850 else:
851 log.debug('Permission denied for %s %s', cls, self.user)
851 log.debug('Permission denied for %s %s', cls, self.user)
852 if self.user.is_default_user:
852 if self.user.is_default_user:
853 raise _redirect_to_login(_('You need to be signed in to view this page'))
853 raise _redirect_to_login(_('You need to be signed in to view this page'))
854 else:
854 else:
855 raise HTTPForbidden()
855 raise HTTPForbidden()
856
856
857 def check_permissions(self):
857 def check_permissions(self):
858 """Dummy function for overriding"""
858 """Dummy function for overriding"""
859 raise Exception('You have to write this function in child class')
859 raise Exception('You have to write this function in child class')
860
860
861
861
862 class HasPermissionAllDecorator(PermsDecorator):
863 """
864 Checks for access permission for all given predicates. All of them
865 have to be meet in order to fulfill the request
866 """
867
868 def check_permissions(self):
869 if self.required_perms.issubset(self.user_perms.get('global')):
870 return True
871 return False
872
873
874 class HasPermissionAnyDecorator(PermsDecorator):
862 class HasPermissionAnyDecorator(PermsDecorator):
875 """
863 """
876 Checks for access permission for any of given predicates. In order to
864 Checks for access permission for any of given predicates. In order to
877 fulfill the request any of predicates must be meet
865 fulfill the request any of predicates must be meet
878 """
866 """
879
867
880 def check_permissions(self):
868 def check_permissions(self):
881 if self.required_perms.intersection(self.user_perms.get('global')):
869 if self.required_perms.intersection(self.user_perms.get('global')):
882 return True
870 return True
883 return False
871 return False
884
872
885
873
886 class HasRepoPermissionAllDecorator(PermsDecorator):
887 """
888 Checks for access permission for all given predicates for specific
889 repository. All of them have to be meet in order to fulfill the request
890 """
891
892 def check_permissions(self):
893 repo_name = get_repo_slug(request)
894 try:
895 user_perms = set([self.user_perms['repositories'][repo_name]])
896 except KeyError:
897 return False
898 if self.required_perms.issubset(user_perms):
899 return True
900 return False
901
902
903 class HasRepoPermissionAnyDecorator(PermsDecorator):
874 class HasRepoPermissionAnyDecorator(PermsDecorator):
904 """
875 """
905 Checks for access permission for any of given predicates for specific
876 Checks for access permission for any of given predicates for specific
906 repository. In order to fulfill the request any of predicates must be meet
877 repository. In order to fulfill the request any of predicates must be meet
907 """
878 """
908
879
909 def check_permissions(self):
880 def check_permissions(self):
910 repo_name = get_repo_slug(request)
881 repo_name = get_repo_slug(request)
911 try:
882 try:
912 user_perms = set([self.user_perms['repositories'][repo_name]])
883 user_perms = set([self.user_perms['repositories'][repo_name]])
913 except KeyError:
884 except KeyError:
914 return False
885 return False
915
886
916 if self.required_perms.intersection(user_perms):
887 if self.required_perms.intersection(user_perms):
917 return True
888 return True
918 return False
889 return False
919
890
920
891
921 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
922 """
923 Checks for access permission for all given predicates for specific
924 repository group. All of them have to be meet in order to fulfill the request
925 """
926
927 def check_permissions(self):
928 group_name = get_repo_group_slug(request)
929 try:
930 user_perms = set([self.user_perms['repositories_groups'][group_name]])
931 except KeyError:
932 return False
933
934 if self.required_perms.issubset(user_perms):
935 return True
936 return False
937
938
939 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
892 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
940 """
893 """
941 Checks for access permission for any of given predicates for specific
894 Checks for access permission for any of given predicates for specific
942 repository group. In order to fulfill the request any of predicates must be meet
895 repository group. In order to fulfill the request any of predicates must be meet
943 """
896 """
944
897
945 def check_permissions(self):
898 def check_permissions(self):
946 group_name = get_repo_group_slug(request)
899 group_name = get_repo_group_slug(request)
947 try:
900 try:
948 user_perms = set([self.user_perms['repositories_groups'][group_name]])
901 user_perms = set([self.user_perms['repositories_groups'][group_name]])
949 except KeyError:
902 except KeyError:
950 return False
903 return False
951
904
952 if self.required_perms.intersection(user_perms):
905 if self.required_perms.intersection(user_perms):
953 return True
906 return True
954 return False
907 return False
955
908
956
909
957 class HasUserGroupPermissionAllDecorator(PermsDecorator):
958 """
959 Checks for access permission for all given predicates for specific
960 user group. All of them have to be meet in order to fulfill the request
961 """
962
963 def check_permissions(self):
964 group_name = get_user_group_slug(request)
965 try:
966 user_perms = set([self.user_perms['user_groups'][group_name]])
967 except KeyError:
968 return False
969
970 if self.required_perms.issubset(user_perms):
971 return True
972 return False
973
974
975 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
910 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
976 """
911 """
977 Checks for access permission for any of given predicates for specific
912 Checks for access permission for any of given predicates for specific
978 user group. In order to fulfill the request any of predicates must be meet
913 user group. In order to fulfill the request any of predicates must be meet
979 """
914 """
980
915
981 def check_permissions(self):
916 def check_permissions(self):
982 group_name = get_user_group_slug(request)
917 group_name = get_user_group_slug(request)
983 try:
918 try:
984 user_perms = set([self.user_perms['user_groups'][group_name]])
919 user_perms = set([self.user_perms['user_groups'][group_name]])
985 except KeyError:
920 except KeyError:
986 return False
921 return False
987
922
988 if self.required_perms.intersection(user_perms):
923 if self.required_perms.intersection(user_perms):
989 return True
924 return True
990 return False
925 return False
991
926
992
927
993 #==============================================================================
928 #==============================================================================
994 # CHECK FUNCTIONS
929 # CHECK FUNCTIONS
995 #==============================================================================
930 #==============================================================================
996 class PermsFunction(object):
931 class PermsFunction(object):
997 """Base function for other check functions"""
932 """Base function for other check functions"""
998
933
999 def __init__(self, *perms):
934 def __init__(self, *perms):
1000 self.required_perms = set(perms)
935 self.required_perms = set(perms)
1001 self.user_perms = None
936 self.user_perms = None
1002 self.repo_name = None
937 self.repo_name = None
1003 self.group_name = None
938 self.group_name = None
1004
939
1005 def __nonzero__(self):
940 def __nonzero__(self):
1006 """ Defend against accidentally forgetting to call the object
941 """ Defend against accidentally forgetting to call the object
1007 and instead evaluating it directly in a boolean context,
942 and instead evaluating it directly in a boolean context,
1008 which could have security implications.
943 which could have security implications.
1009 """
944 """
1010 raise AssertionError(self.__class__.__name__ + ' is not a bool and must be called!')
945 raise AssertionError(self.__class__.__name__ + ' is not a bool and must be called!')
1011
946
1012 def __call__(self, check_location='', user=None):
947 def __call__(self, check_location='', user=None):
1013 if not user:
948 if not user:
1014 #TODO: remove this someday,put as user as attribute here
949 #TODO: remove this someday,put as user as attribute here
1015 user = request.user
950 user = request.user
1016
951
1017 # init auth user if not already given
952 # init auth user if not already given
1018 if not isinstance(user, AuthUser):
953 if not isinstance(user, AuthUser):
1019 user = AuthUser(user.user_id)
954 user = AuthUser(user.user_id)
1020
955
1021 cls_name = self.__class__.__name__
956 cls_name = self.__class__.__name__
1022 check_scope = {
957 check_scope = {
1023 'HasPermissionAll': '',
1024 'HasPermissionAny': '',
958 'HasPermissionAny': '',
1025 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1026 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
959 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1027 'HasRepoGroupPermissionAll': 'group:%s' % self.group_name,
1028 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
960 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
1029 }.get(cls_name, '?')
961 }.get(cls_name, '?')
1030 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
962 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1031 self.required_perms, user, check_scope,
963 self.required_perms, user, check_scope,
1032 check_location or 'unspecified location')
964 check_location or 'unspecified location')
1033 if not user:
965 if not user:
1034 log.debug('Empty request user')
966 log.debug('Empty request user')
1035 return False
967 return False
1036 self.user_perms = user.permissions
968 self.user_perms = user.permissions
1037 if self.check_permissions():
969 if self.check_permissions():
1038 log.debug('Permission to %s granted for user: %s @ %s',
970 log.debug('Permission to %s granted for user: %s @ %s',
1039 check_scope, user,
971 check_scope, user,
1040 check_location or 'unspecified location')
972 check_location or 'unspecified location')
1041 return True
973 return True
1042
974
1043 else:
975 else:
1044 log.debug('Permission to %s denied for user: %s @ %s',
976 log.debug('Permission to %s denied for user: %s @ %s',
1045 check_scope, user,
977 check_scope, user,
1046 check_location or 'unspecified location')
978 check_location or 'unspecified location')
1047 return False
979 return False
1048
980
1049 def check_permissions(self):
981 def check_permissions(self):
1050 """Dummy function for overriding"""
982 """Dummy function for overriding"""
1051 raise Exception('You have to write this function in child class')
983 raise Exception('You have to write this function in child class')
1052
984
1053
985
1054 class HasPermissionAll(PermsFunction):
1055 def check_permissions(self):
1056 if self.required_perms.issubset(self.user_perms.get('global')):
1057 return True
1058 return False
1059
1060
1061 class HasPermissionAny(PermsFunction):
986 class HasPermissionAny(PermsFunction):
1062 def check_permissions(self):
987 def check_permissions(self):
1063 if self.required_perms.intersection(self.user_perms.get('global')):
988 if self.required_perms.intersection(self.user_perms.get('global')):
1064 return True
989 return True
1065 return False
990 return False
1066
991
1067
992
1068 class HasRepoPermissionAll(PermsFunction):
1069 def __call__(self, repo_name=None, check_location='', user=None):
1070 self.repo_name = repo_name
1071 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1072
1073 def check_permissions(self):
1074 if not self.repo_name:
1075 self.repo_name = get_repo_slug(request)
1076
1077 try:
1078 self._user_perms = set(
1079 [self.user_perms['repositories'][self.repo_name]]
1080 )
1081 except KeyError:
1082 return False
1083 if self.required_perms.issubset(self._user_perms):
1084 return True
1085 return False
1086
1087
1088 class HasRepoPermissionAny(PermsFunction):
993 class HasRepoPermissionAny(PermsFunction):
1089 def __call__(self, repo_name=None, check_location='', user=None):
994 def __call__(self, repo_name=None, check_location='', user=None):
1090 self.repo_name = repo_name
995 self.repo_name = repo_name
1091 return super(HasRepoPermissionAny, self).__call__(check_location, user)
996 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1092
997
1093 def check_permissions(self):
998 def check_permissions(self):
1094 if not self.repo_name:
999 if not self.repo_name:
1095 self.repo_name = get_repo_slug(request)
1000 self.repo_name = get_repo_slug(request)
1096
1001
1097 try:
1002 try:
1098 self._user_perms = set(
1003 self._user_perms = set(
1099 [self.user_perms['repositories'][self.repo_name]]
1004 [self.user_perms['repositories'][self.repo_name]]
1100 )
1005 )
1101 except KeyError:
1006 except KeyError:
1102 return False
1007 return False
1103 if self.required_perms.intersection(self._user_perms):
1008 if self.required_perms.intersection(self._user_perms):
1104 return True
1009 return True
1105 return False
1010 return False
1106
1011
1107
1012
1108 class HasRepoGroupPermissionAny(PermsFunction):
1013 class HasRepoGroupPermissionAny(PermsFunction):
1109 def __call__(self, group_name=None, check_location='', user=None):
1014 def __call__(self, group_name=None, check_location='', user=None):
1110 self.group_name = group_name
1015 self.group_name = group_name
1111 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1016 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1112
1017
1113 def check_permissions(self):
1018 def check_permissions(self):
1114 try:
1019 try:
1115 self._user_perms = set(
1020 self._user_perms = set(
1116 [self.user_perms['repositories_groups'][self.group_name]]
1021 [self.user_perms['repositories_groups'][self.group_name]]
1117 )
1022 )
1118 except KeyError:
1023 except KeyError:
1119 return False
1024 return False
1120 if self.required_perms.intersection(self._user_perms):
1025 if self.required_perms.intersection(self._user_perms):
1121 return True
1026 return True
1122 return False
1027 return False
1123
1028
1124
1029
1125 class HasRepoGroupPermissionAll(PermsFunction):
1126 def __call__(self, group_name=None, check_location='', user=None):
1127 self.group_name = group_name
1128 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
1129
1130 def check_permissions(self):
1131 try:
1132 self._user_perms = set(
1133 [self.user_perms['repositories_groups'][self.group_name]]
1134 )
1135 except KeyError:
1136 return False
1137 if self.required_perms.issubset(self._user_perms):
1138 return True
1139 return False
1140
1141
1142 class HasUserGroupPermissionAny(PermsFunction):
1030 class HasUserGroupPermissionAny(PermsFunction):
1143 def __call__(self, user_group_name=None, check_location='', user=None):
1031 def __call__(self, user_group_name=None, check_location='', user=None):
1144 self.user_group_name = user_group_name
1032 self.user_group_name = user_group_name
1145 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1033 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1146
1034
1147 def check_permissions(self):
1035 def check_permissions(self):
1148 try:
1036 try:
1149 self._user_perms = set(
1037 self._user_perms = set(
1150 [self.user_perms['user_groups'][self.user_group_name]]
1038 [self.user_perms['user_groups'][self.user_group_name]]
1151 )
1039 )
1152 except KeyError:
1040 except KeyError:
1153 return False
1041 return False
1154 if self.required_perms.intersection(self._user_perms):
1042 if self.required_perms.intersection(self._user_perms):
1155 return True
1043 return True
1156 return False
1044 return False
1157
1045
1158
1046
1159 class HasUserGroupPermissionAll(PermsFunction):
1160 def __call__(self, user_group_name=None, check_location='', user=None):
1161 self.user_group_name = user_group_name
1162 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
1163
1164 def check_permissions(self):
1165 try:
1166 self._user_perms = set(
1167 [self.user_perms['user_groups'][self.user_group_name]]
1168 )
1169 except KeyError:
1170 return False
1171 if self.required_perms.issubset(self._user_perms):
1172 return True
1173 return False
1174
1175
1176 #==============================================================================
1047 #==============================================================================
1177 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1048 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1178 #==============================================================================
1049 #==============================================================================
1179 class HasPermissionAnyMiddleware(object):
1050 class HasPermissionAnyMiddleware(object):
1180 def __init__(self, *perms):
1051 def __init__(self, *perms):
1181 self.required_perms = set(perms)
1052 self.required_perms = set(perms)
1182
1053
1183 def __call__(self, user, repo_name):
1054 def __call__(self, user, repo_name):
1184 # repo_name MUST be unicode, since we handle keys in permission
1055 # repo_name MUST be unicode, since we handle keys in permission
1185 # dict by unicode
1056 # dict by unicode
1186 repo_name = safe_unicode(repo_name)
1057 repo_name = safe_unicode(repo_name)
1187 usr = AuthUser(user.user_id)
1058 usr = AuthUser(user.user_id)
1188 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1059 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1189 self.username = user.username
1060 self.username = user.username
1190 self.repo_name = repo_name
1061 self.repo_name = repo_name
1191 return self.check_permissions()
1062 return self.check_permissions()
1192
1063
1193 def check_permissions(self):
1064 def check_permissions(self):
1194 log.debug('checking VCS protocol '
1065 log.debug('checking VCS protocol '
1195 'permissions %s for user:%s repository:%s', self.user_perms,
1066 'permissions %s for user:%s repository:%s', self.user_perms,
1196 self.username, self.repo_name)
1067 self.username, self.repo_name)
1197 if self.required_perms.intersection(self.user_perms):
1068 if self.required_perms.intersection(self.user_perms):
1198 log.debug('Permission to repo: %s granted for user: %s @ %s',
1069 log.debug('Permission to repo: %s granted for user: %s @ %s',
1199 self.repo_name, self.username, 'PermissionMiddleware')
1070 self.repo_name, self.username, 'PermissionMiddleware')
1200 return True
1071 return True
1201 log.debug('Permission to repo: %s denied for user: %s @ %s',
1072 log.debug('Permission to repo: %s denied for user: %s @ %s',
1202 self.repo_name, self.username, 'PermissionMiddleware')
1073 self.repo_name, self.username, 'PermissionMiddleware')
1203 return False
1074 return False
1204
1075
1205
1076
1206 #==============================================================================
1077 #==============================================================================
1207 # SPECIAL VERSION TO HANDLE API AUTH
1078 # SPECIAL VERSION TO HANDLE API AUTH
1208 #==============================================================================
1079 #==============================================================================
1209 class _BaseApiPerm(object):
1080 class _BaseApiPerm(object):
1210 def __init__(self, *perms):
1081 def __init__(self, *perms):
1211 self.required_perms = set(perms)
1082 self.required_perms = set(perms)
1212
1083
1213 def __call__(self, check_location=None, user=None, repo_name=None,
1084 def __call__(self, check_location=None, user=None, repo_name=None,
1214 group_name=None):
1085 group_name=None):
1215 cls_name = self.__class__.__name__
1086 cls_name = self.__class__.__name__
1216 check_scope = 'user:%s' % (user)
1087 check_scope = 'user:%s' % (user)
1217 if repo_name:
1088 if repo_name:
1218 check_scope += ', repo:%s' % (repo_name)
1089 check_scope += ', repo:%s' % (repo_name)
1219
1090
1220 if group_name:
1091 if group_name:
1221 check_scope += ', repo group:%s' % (group_name)
1092 check_scope += ', repo group:%s' % (group_name)
1222
1093
1223 log.debug('checking cls:%s %s %s @ %s',
1094 log.debug('checking cls:%s %s %s @ %s',
1224 cls_name, self.required_perms, check_scope, check_location)
1095 cls_name, self.required_perms, check_scope, check_location)
1225 if not user:
1096 if not user:
1226 log.debug('Empty User passed into arguments')
1097 log.debug('Empty User passed into arguments')
1227 return False
1098 return False
1228
1099
1229 ## process user
1100 ## process user
1230 if not isinstance(user, AuthUser):
1101 if not isinstance(user, AuthUser):
1231 user = AuthUser(user.user_id)
1102 user = AuthUser(user.user_id)
1232 if not check_location:
1103 if not check_location:
1233 check_location = 'unspecified'
1104 check_location = 'unspecified'
1234 if self.check_permissions(user.permissions, repo_name, group_name):
1105 if self.check_permissions(user.permissions, repo_name, group_name):
1235 log.debug('Permission to %s granted for user: %s @ %s',
1106 log.debug('Permission to %s granted for user: %s @ %s',
1236 check_scope, user, check_location)
1107 check_scope, user, check_location)
1237 return True
1108 return True
1238
1109
1239 else:
1110 else:
1240 log.debug('Permission to %s denied for user: %s @ %s',
1111 log.debug('Permission to %s denied for user: %s @ %s',
1241 check_scope, user, check_location)
1112 check_scope, user, check_location)
1242 return False
1113 return False
1243
1114
1244 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1115 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1245 """
1116 """
1246 implement in child class should return True if permissions are ok,
1117 implement in child class should return True if permissions are ok,
1247 False otherwise
1118 False otherwise
1248
1119
1249 :param perm_defs: dict with permission definitions
1120 :param perm_defs: dict with permission definitions
1250 :param repo_name: repo name
1121 :param repo_name: repo name
1251 """
1122 """
1252 raise NotImplementedError()
1123 raise NotImplementedError()
1253
1124
1254
1125
1255 class HasPermissionAllApi(_BaseApiPerm):
1256 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1257 if self.required_perms.issubset(perm_defs.get('global')):
1258 return True
1259 return False
1260
1261
1262 class HasPermissionAnyApi(_BaseApiPerm):
1126 class HasPermissionAnyApi(_BaseApiPerm):
1263 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1127 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1264 if self.required_perms.intersection(perm_defs.get('global')):
1128 if self.required_perms.intersection(perm_defs.get('global')):
1265 return True
1129 return True
1266 return False
1130 return False
1267
1131
1268
1132
1269 class HasRepoPermissionAllApi(_BaseApiPerm):
1270 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1271 try:
1272 _user_perms = set([perm_defs['repositories'][repo_name]])
1273 except KeyError:
1274 log.warning(traceback.format_exc())
1275 return False
1276 if self.required_perms.issubset(_user_perms):
1277 return True
1278 return False
1279
1280
1281 class HasRepoPermissionAnyApi(_BaseApiPerm):
1133 class HasRepoPermissionAnyApi(_BaseApiPerm):
1282 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1134 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1283 try:
1135 try:
1284 _user_perms = set([perm_defs['repositories'][repo_name]])
1136 _user_perms = set([perm_defs['repositories'][repo_name]])
1285 except KeyError:
1137 except KeyError:
1286 log.warning(traceback.format_exc())
1138 log.warning(traceback.format_exc())
1287 return False
1139 return False
1288 if self.required_perms.intersection(_user_perms):
1140 if self.required_perms.intersection(_user_perms):
1289 return True
1141 return True
1290 return False
1142 return False
1291
1143
1292
1144
1293 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1145 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1294 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1146 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1295 try:
1147 try:
1296 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1148 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1297 except KeyError:
1149 except KeyError:
1298 log.warning(traceback.format_exc())
1150 log.warning(traceback.format_exc())
1299 return False
1151 return False
1300 if self.required_perms.intersection(_user_perms):
1152 if self.required_perms.intersection(_user_perms):
1301 return True
1153 return True
1302 return False
1154 return False
1303
1155
1304 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1305 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1306 try:
1307 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1308 except KeyError:
1309 log.warning(traceback.format_exc())
1310 return False
1311 if self.required_perms.issubset(_user_perms):
1312 return True
1313 return False
1314
1156
1315 def check_ip_access(source_ip, allowed_ips=None):
1157 def check_ip_access(source_ip, allowed_ips=None):
1316 """
1158 """
1317 Checks if source_ip is a subnet of any of allowed_ips.
1159 Checks if source_ip is a subnet of any of allowed_ips.
1318
1160
1319 :param source_ip:
1161 :param source_ip:
1320 :param allowed_ips: list of allowed ips together with mask
1162 :param allowed_ips: list of allowed ips together with mask
1321 """
1163 """
1322 from kallithea.lib import ipaddr
1164 from kallithea.lib import ipaddr
1323 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1165 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1324 if isinstance(allowed_ips, (tuple, list, set)):
1166 if isinstance(allowed_ips, (tuple, list, set)):
1325 for ip in allowed_ips:
1167 for ip in allowed_ips:
1326 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1168 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1327 log.debug('IP %s is network %s',
1169 log.debug('IP %s is network %s',
1328 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1170 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1329 return True
1171 return True
1330 return False
1172 return False
@@ -1,1517 +1,1516 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 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import StringIO
21 import StringIO
22 import math
22 import math
23 import logging
23 import logging
24 import re
24 import re
25 import urlparse
25 import urlparse
26 import textwrap
26 import textwrap
27
27
28 from beaker.cache import cache_region
28 from beaker.cache import cache_region
29 from pygments.formatters.html import HtmlFormatter
29 from pygments.formatters.html import HtmlFormatter
30 from pygments import highlight as code_highlight
30 from pygments import highlight as code_highlight
31 from pylons import url
31 from pylons import url
32 from pylons.i18n.translation import _, ungettext
32 from pylons.i18n.translation import _, ungettext
33
33
34 from webhelpers.html import literal, HTML, escape
34 from webhelpers.html import literal, HTML, escape
35 from webhelpers.html.tools import *
35 from webhelpers.html.tools import *
36 from webhelpers.html.builder import make_tag
36 from webhelpers.html.builder import make_tag
37 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
37 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
38 end_form, file, hidden, image, javascript_link, link_to, \
38 end_form, file, hidden, image, javascript_link, link_to, \
39 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
39 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
40 submit, text, password, textarea, title, ul, xml_declaration, radio, \
40 submit, text, password, textarea, title, ul, xml_declaration, radio, \
41 form as insecure_form
41 form as insecure_form
42 from webhelpers.html.tools import auto_link, button_to, highlight, \
42 from webhelpers.html.tools import auto_link, button_to, highlight, \
43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
44 from webhelpers.number import format_byte_size, format_bit_size
44 from webhelpers.number import format_byte_size, format_bit_size
45 from webhelpers.pylonslib import Flash as _Flash
45 from webhelpers.pylonslib import Flash as _Flash
46 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
46 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
49 replace_whitespace, urlify, truncate, wrap_paragraphs
49 replace_whitespace, urlify, truncate, wrap_paragraphs
50 from webhelpers.date import time_ago_in_words
50 from webhelpers.date import time_ago_in_words
51 from webhelpers.paginate import Page as _Page
51 from webhelpers.paginate import Page as _Page
52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
53 convert_boolean_attrs, NotGiven, _make_safe_id_component
53 convert_boolean_attrs, NotGiven, _make_safe_id_component
54
54
55 from kallithea.lib.annotate import annotate_highlight
55 from kallithea.lib.annotate import annotate_highlight
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
59 safe_int, MENTIONS_REGEX
59 safe_int, MENTIONS_REGEX
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
64 from kallithea.model.changeset_status import ChangesetStatusModel
64 from kallithea.model.changeset_status import ChangesetStatusModel
65 from kallithea.model.db import URL_SEP, Permission
65 from kallithea.model.db import URL_SEP, Permission
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 def canonical_url(*args, **kargs):
70 def canonical_url(*args, **kargs):
71 '''Like url(x, qualified=True), but returns url that not only is qualified
71 '''Like url(x, qualified=True), but returns url that not only is qualified
72 but also canonical, as configured in canonical_url'''
72 but also canonical, as configured in canonical_url'''
73 from kallithea import CONFIG
73 from kallithea import CONFIG
74 try:
74 try:
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
76 kargs['host'] = parts[1].split('/', 1)[0]
76 kargs['host'] = parts[1].split('/', 1)[0]
77 kargs['protocol'] = parts[0]
77 kargs['protocol'] = parts[0]
78 except IndexError:
78 except IndexError:
79 kargs['qualified'] = True
79 kargs['qualified'] = True
80 return url(*args, **kargs)
80 return url(*args, **kargs)
81
81
82 def canonical_hostname():
82 def canonical_hostname():
83 '''Return canonical hostname of system'''
83 '''Return canonical hostname of system'''
84 from kallithea import CONFIG
84 from kallithea import CONFIG
85 try:
85 try:
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
87 return parts[1].split('/', 1)[0]
87 return parts[1].split('/', 1)[0]
88 except IndexError:
88 except IndexError:
89 parts = url('home', qualified=True).split('://', 1)
89 parts = url('home', qualified=True).split('://', 1)
90 return parts[1].split('/', 1)[0]
90 return parts[1].split('/', 1)[0]
91
91
92 def html_escape(s):
92 def html_escape(s):
93 """Return string with all html escaped.
93 """Return string with all html escaped.
94 This is also safe for javascript in html but not necessarily correct.
94 This is also safe for javascript in html but not necessarily correct.
95 """
95 """
96 return (s
96 return (s
97 .replace('&', '&amp;')
97 .replace('&', '&amp;')
98 .replace(">", "&gt;")
98 .replace(">", "&gt;")
99 .replace("<", "&lt;")
99 .replace("<", "&lt;")
100 .replace('"', "&quot;")
100 .replace('"', "&quot;")
101 .replace("'", "&apos;")
101 .replace("'", "&apos;")
102 )
102 )
103
103
104 def shorter(s, size=20, firstline=False, postfix='...'):
104 def shorter(s, size=20, firstline=False, postfix='...'):
105 """Truncate s to size, including the postfix string if truncating.
105 """Truncate s to size, including the postfix string if truncating.
106 If firstline, truncate at newline.
106 If firstline, truncate at newline.
107 """
107 """
108 if firstline:
108 if firstline:
109 s = s.split('\n', 1)[0].rstrip()
109 s = s.split('\n', 1)[0].rstrip()
110 if len(s) > size:
110 if len(s) > size:
111 return s[:size - len(postfix)] + postfix
111 return s[:size - len(postfix)] + postfix
112 return s
112 return s
113
113
114
114
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
116 """
116 """
117 Reset button
117 Reset button
118 """
118 """
119 _set_input_attrs(attrs, type, name, value)
119 _set_input_attrs(attrs, type, name, value)
120 _set_id_attr(attrs, id, name)
120 _set_id_attr(attrs, id, name)
121 convert_boolean_attrs(attrs, ["disabled"])
121 convert_boolean_attrs(attrs, ["disabled"])
122 return HTML.input(**attrs)
122 return HTML.input(**attrs)
123
123
124 reset = _reset
124 reset = _reset
125 safeid = _make_safe_id_component
125 safeid = _make_safe_id_component
126
126
127
127
128 def FID(raw_id, path):
128 def FID(raw_id, path):
129 """
129 """
130 Creates a unique ID for filenode based on it's hash of path and revision
130 Creates a unique ID for filenode based on it's hash of path and revision
131 it's safe to use in urls
131 it's safe to use in urls
132
132
133 :param raw_id:
133 :param raw_id:
134 :param path:
134 :param path:
135 """
135 """
136
136
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
138
138
139
139
140 class _GetError(object):
140 class _GetError(object):
141 """Get error from form_errors, and represent it as span wrapped error
141 """Get error from form_errors, and represent it as span wrapped error
142 message
142 message
143
143
144 :param field_name: field to fetch errors for
144 :param field_name: field to fetch errors for
145 :param form_errors: form errors dict
145 :param form_errors: form errors dict
146 """
146 """
147
147
148 def __call__(self, field_name, form_errors):
148 def __call__(self, field_name, form_errors):
149 tmpl = """<span class="error_msg">%s</span>"""
149 tmpl = """<span class="error_msg">%s</span>"""
150 if form_errors and field_name in form_errors:
150 if form_errors and field_name in form_errors:
151 return literal(tmpl % form_errors.get(field_name))
151 return literal(tmpl % form_errors.get(field_name))
152
152
153 get_error = _GetError()
153 get_error = _GetError()
154
154
155
155
156 class _FilesBreadCrumbs(object):
156 class _FilesBreadCrumbs(object):
157
157
158 def __call__(self, repo_name, rev, paths):
158 def __call__(self, repo_name, rev, paths):
159 if isinstance(paths, str):
159 if isinstance(paths, str):
160 paths = safe_unicode(paths)
160 paths = safe_unicode(paths)
161 url_l = [link_to(repo_name, url('files_home',
161 url_l = [link_to(repo_name, url('files_home',
162 repo_name=repo_name,
162 repo_name=repo_name,
163 revision=rev, f_path=''),
163 revision=rev, f_path=''),
164 class_='ypjax-link')]
164 class_='ypjax-link')]
165 paths_l = paths.split('/')
165 paths_l = paths.split('/')
166 for cnt, p in enumerate(paths_l):
166 for cnt, p in enumerate(paths_l):
167 if p != '':
167 if p != '':
168 url_l.append(link_to(p,
168 url_l.append(link_to(p,
169 url('files_home',
169 url('files_home',
170 repo_name=repo_name,
170 repo_name=repo_name,
171 revision=rev,
171 revision=rev,
172 f_path='/'.join(paths_l[:cnt + 1])
172 f_path='/'.join(paths_l[:cnt + 1])
173 ),
173 ),
174 class_='ypjax-link'
174 class_='ypjax-link'
175 )
175 )
176 )
176 )
177
177
178 return literal('/'.join(url_l))
178 return literal('/'.join(url_l))
179
179
180 files_breadcrumbs = _FilesBreadCrumbs()
180 files_breadcrumbs = _FilesBreadCrumbs()
181
181
182
182
183 class CodeHtmlFormatter(HtmlFormatter):
183 class CodeHtmlFormatter(HtmlFormatter):
184 """
184 """
185 My code Html Formatter for source codes
185 My code Html Formatter for source codes
186 """
186 """
187
187
188 def wrap(self, source, outfile):
188 def wrap(self, source, outfile):
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
190
190
191 def _wrap_code(self, source):
191 def _wrap_code(self, source):
192 for cnt, it in enumerate(source):
192 for cnt, it in enumerate(source):
193 i, t = it
193 i, t = it
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
195 yield i, t
195 yield i, t
196
196
197 def _wrap_tablelinenos(self, inner):
197 def _wrap_tablelinenos(self, inner):
198 dummyoutfile = StringIO.StringIO()
198 dummyoutfile = StringIO.StringIO()
199 lncount = 0
199 lncount = 0
200 for t, line in inner:
200 for t, line in inner:
201 if t:
201 if t:
202 lncount += 1
202 lncount += 1
203 dummyoutfile.write(line)
203 dummyoutfile.write(line)
204
204
205 fl = self.linenostart
205 fl = self.linenostart
206 mw = len(str(lncount + fl - 1))
206 mw = len(str(lncount + fl - 1))
207 sp = self.linenospecial
207 sp = self.linenospecial
208 st = self.linenostep
208 st = self.linenostep
209 la = self.lineanchors
209 la = self.lineanchors
210 aln = self.anchorlinenos
210 aln = self.anchorlinenos
211 nocls = self.noclasses
211 nocls = self.noclasses
212 if sp:
212 if sp:
213 lines = []
213 lines = []
214
214
215 for i in range(fl, fl + lncount):
215 for i in range(fl, fl + lncount):
216 if i % st == 0:
216 if i % st == 0:
217 if i % sp == 0:
217 if i % sp == 0:
218 if aln:
218 if aln:
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
220 (la, i, mw, i))
220 (la, i, mw, i))
221 else:
221 else:
222 lines.append('<span class="special">%*d</span>' % (mw, i))
222 lines.append('<span class="special">%*d</span>' % (mw, i))
223 else:
223 else:
224 if aln:
224 if aln:
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
226 else:
226 else:
227 lines.append('%*d' % (mw, i))
227 lines.append('%*d' % (mw, i))
228 else:
228 else:
229 lines.append('')
229 lines.append('')
230 ls = '\n'.join(lines)
230 ls = '\n'.join(lines)
231 else:
231 else:
232 lines = []
232 lines = []
233 for i in range(fl, fl + lncount):
233 for i in range(fl, fl + lncount):
234 if i % st == 0:
234 if i % st == 0:
235 if aln:
235 if aln:
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
237 else:
237 else:
238 lines.append('%*d' % (mw, i))
238 lines.append('%*d' % (mw, i))
239 else:
239 else:
240 lines.append('')
240 lines.append('')
241 ls = '\n'.join(lines)
241 ls = '\n'.join(lines)
242
242
243 # in case you wonder about the seemingly redundant <div> here: since the
243 # in case you wonder about the seemingly redundant <div> here: since the
244 # content in the other cell also is wrapped in a div, some browsers in
244 # content in the other cell also is wrapped in a div, some browsers in
245 # some configurations seem to mess up the formatting...
245 # some configurations seem to mess up the formatting...
246 if nocls:
246 if nocls:
247 yield 0, ('<table class="%stable">' % self.cssclass +
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td><div class="linenodiv" '
248 '<tr><td><div class="linenodiv" '
249 'style="background-color: #f0f0f0; padding-right: 10px">'
249 'style="background-color: #f0f0f0; padding-right: 10px">'
250 '<pre style="line-height: 125%">' +
250 '<pre style="line-height: 125%">' +
251 ls + '</pre></div></td><td id="hlcode" class="code">')
251 ls + '</pre></div></td><td id="hlcode" class="code">')
252 else:
252 else:
253 yield 0, ('<table class="%stable">' % self.cssclass +
253 yield 0, ('<table class="%stable">' % self.cssclass +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
255 ls + '</pre></div></td><td id="hlcode" class="code">')
255 ls + '</pre></div></td><td id="hlcode" class="code">')
256 yield 0, dummyoutfile.getvalue()
256 yield 0, dummyoutfile.getvalue()
257 yield 0, '</td></tr></table>'
257 yield 0, '</td></tr></table>'
258
258
259
259
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
261
261
262 def _markup_whitespace(m):
262 def _markup_whitespace(m):
263 groups = m.groups()
263 groups = m.groups()
264 if groups[0]:
264 if groups[0]:
265 return '<u>\t</u>'
265 return '<u>\t</u>'
266 if groups[1]:
266 if groups[1]:
267 return ' <i></i>'
267 return ' <i></i>'
268
268
269 def markup_whitespace(s):
269 def markup_whitespace(s):
270 return _whitespace_re.sub(_markup_whitespace, s)
270 return _whitespace_re.sub(_markup_whitespace, s)
271
271
272 def pygmentize(filenode, **kwargs):
272 def pygmentize(filenode, **kwargs):
273 """
273 """
274 pygmentize function using pygments
274 pygmentize function using pygments
275
275
276 :param filenode:
276 :param filenode:
277 """
277 """
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
279 return literal(markup_whitespace(
279 return literal(markup_whitespace(
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
281
281
282
282
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
284 """
284 """
285 pygmentize function for annotation
285 pygmentize function for annotation
286
286
287 :param filenode:
287 :param filenode:
288 """
288 """
289
289
290 color_dict = {}
290 color_dict = {}
291
291
292 def gen_color(n=10000):
292 def gen_color(n=10000):
293 """generator for getting n of evenly distributed colors using
293 """generator for getting n of evenly distributed colors using
294 hsv color and golden ratio. It always return same order of colors
294 hsv color and golden ratio. It always return same order of colors
295
295
296 :returns: RGB tuple
296 :returns: RGB tuple
297 """
297 """
298
298
299 def hsv_to_rgb(h, s, v):
299 def hsv_to_rgb(h, s, v):
300 if s == 0.0:
300 if s == 0.0:
301 return v, v, v
301 return v, v, v
302 i = int(h * 6.0) # XXX assume int() truncates!
302 i = int(h * 6.0) # XXX assume int() truncates!
303 f = (h * 6.0) - i
303 f = (h * 6.0) - i
304 p = v * (1.0 - s)
304 p = v * (1.0 - s)
305 q = v * (1.0 - s * f)
305 q = v * (1.0 - s * f)
306 t = v * (1.0 - s * (1.0 - f))
306 t = v * (1.0 - s * (1.0 - f))
307 i = i % 6
307 i = i % 6
308 if i == 0:
308 if i == 0:
309 return v, t, p
309 return v, t, p
310 if i == 1:
310 if i == 1:
311 return q, v, p
311 return q, v, p
312 if i == 2:
312 if i == 2:
313 return p, v, t
313 return p, v, t
314 if i == 3:
314 if i == 3:
315 return p, q, v
315 return p, q, v
316 if i == 4:
316 if i == 4:
317 return t, p, v
317 return t, p, v
318 if i == 5:
318 if i == 5:
319 return v, p, q
319 return v, p, q
320
320
321 golden_ratio = 0.618033988749895
321 golden_ratio = 0.618033988749895
322 h = 0.22717784590367374
322 h = 0.22717784590367374
323
323
324 for _unused in xrange(n):
324 for _unused in xrange(n):
325 h += golden_ratio
325 h += golden_ratio
326 h %= 1
326 h %= 1
327 HSV_tuple = [h, 0.95, 0.95]
327 HSV_tuple = [h, 0.95, 0.95]
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
330
330
331 cgenerator = gen_color()
331 cgenerator = gen_color()
332
332
333 def get_color_string(cs):
333 def get_color_string(cs):
334 if cs in color_dict:
334 if cs in color_dict:
335 col = color_dict[cs]
335 col = color_dict[cs]
336 else:
336 else:
337 col = color_dict[cs] = cgenerator.next()
337 col = color_dict[cs] = cgenerator.next()
338 return "color: rgb(%s)! important;" % (', '.join(col))
338 return "color: rgb(%s)! important;" % (', '.join(col))
339
339
340 def url_func(repo_name):
340 def url_func(repo_name):
341
341
342 def _url_func(changeset):
342 def _url_func(changeset):
343 author = escape(changeset.author)
343 author = escape(changeset.author)
344 date = changeset.date
344 date = changeset.date
345 message = escape(changeset.message)
345 message = escape(changeset.message)
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
348 "</b> %s<br/></div>") % (author, date, message)
348 "</b> %s<br/></div>") % (author, date, message)
349
349
350 lnk_format = show_id(changeset)
350 lnk_format = show_id(changeset)
351 uri = link_to(
351 uri = link_to(
352 lnk_format,
352 lnk_format,
353 url('changeset_home', repo_name=repo_name,
353 url('changeset_home', repo_name=repo_name,
354 revision=changeset.raw_id),
354 revision=changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
356 class_='tooltip safe-html-title',
356 class_='tooltip safe-html-title',
357 title=tooltip_html
357 title=tooltip_html
358 )
358 )
359
359
360 uri += '\n'
360 uri += '\n'
361 return uri
361 return uri
362 return _url_func
362 return _url_func
363
363
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
365
365
366
366
367 def is_following_repo(repo_name, user_id):
367 def is_following_repo(repo_name, user_id):
368 from kallithea.model.scm import ScmModel
368 from kallithea.model.scm import ScmModel
369 return ScmModel().is_following_repo(repo_name, user_id)
369 return ScmModel().is_following_repo(repo_name, user_id)
370
370
371 class _Message(object):
371 class _Message(object):
372 """A message returned by ``Flash.pop_messages()``.
372 """A message returned by ``Flash.pop_messages()``.
373
373
374 Converting the message to a string returns the message text. Instances
374 Converting the message to a string returns the message text. Instances
375 also have the following attributes:
375 also have the following attributes:
376
376
377 * ``message``: the message text.
377 * ``message``: the message text.
378 * ``category``: the category specified when the message was created.
378 * ``category``: the category specified when the message was created.
379 """
379 """
380
380
381 def __init__(self, category, message):
381 def __init__(self, category, message):
382 self.category = category
382 self.category = category
383 self.message = message
383 self.message = message
384
384
385 def __str__(self):
385 def __str__(self):
386 return self.message
386 return self.message
387
387
388 __unicode__ = __str__
388 __unicode__ = __str__
389
389
390 def __html__(self):
390 def __html__(self):
391 return escape(safe_unicode(self.message))
391 return escape(safe_unicode(self.message))
392
392
393 class Flash(_Flash):
393 class Flash(_Flash):
394
394
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
396 """
396 """
397 Show a message to the user _and_ log it through the specified function
397 Show a message to the user _and_ log it through the specified function
398
398
399 category: notice (default), warning, error, success
399 category: notice (default), warning, error, success
400 logf: a custom log function - such as log.debug
400 logf: a custom log function - such as log.debug
401
401
402 logf defaults to log.info, unless category equals 'success', in which
402 logf defaults to log.info, unless category equals 'success', in which
403 case logf defaults to log.debug.
403 case logf defaults to log.debug.
404 """
404 """
405 if logf is None:
405 if logf is None:
406 logf = log.info
406 logf = log.info
407 if category == 'success':
407 if category == 'success':
408 logf = log.debug
408 logf = log.debug
409
409
410 logf('Flash %s: %s', category, message)
410 logf('Flash %s: %s', category, message)
411
411
412 super(Flash, self).__call__(message, category, ignore_duplicate)
412 super(Flash, self).__call__(message, category, ignore_duplicate)
413
413
414 def pop_messages(self):
414 def pop_messages(self):
415 """Return all accumulated messages and delete them from the session.
415 """Return all accumulated messages and delete them from the session.
416
416
417 The return value is a list of ``Message`` objects.
417 The return value is a list of ``Message`` objects.
418 """
418 """
419 from pylons import session
419 from pylons import session
420 messages = session.pop(self.session_key, [])
420 messages = session.pop(self.session_key, [])
421 session.save()
421 session.save()
422 return [_Message(*m) for m in messages]
422 return [_Message(*m) for m in messages]
423
423
424 flash = Flash()
424 flash = Flash()
425
425
426 #==============================================================================
426 #==============================================================================
427 # SCM FILTERS available via h.
427 # SCM FILTERS available via h.
428 #==============================================================================
428 #==============================================================================
429 from kallithea.lib.vcs.utils import author_name, author_email
429 from kallithea.lib.vcs.utils import author_name, author_email
430 from kallithea.lib.utils2 import credentials_filter, age as _age
430 from kallithea.lib.utils2 import credentials_filter, age as _age
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
432
432
433 age = lambda x, y=False: _age(x, y)
433 age = lambda x, y=False: _age(x, y)
434 capitalize = lambda x: x.capitalize()
434 capitalize = lambda x: x.capitalize()
435 email = author_email
435 email = author_email
436 short_id = lambda x: x[:12]
436 short_id = lambda x: x[:12]
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
438
438
439
439
440 def show_id(cs):
440 def show_id(cs):
441 """
441 """
442 Configurable function that shows ID
442 Configurable function that shows ID
443 by default it's r123:fffeeefffeee
443 by default it's r123:fffeeefffeee
444
444
445 :param cs: changeset instance
445 :param cs: changeset instance
446 """
446 """
447 from kallithea import CONFIG
447 from kallithea import CONFIG
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
450
450
451 raw_id = cs.raw_id[:def_len]
451 raw_id = cs.raw_id[:def_len]
452 if show_rev:
452 if show_rev:
453 return 'r%s:%s' % (cs.revision, raw_id)
453 return 'r%s:%s' % (cs.revision, raw_id)
454 else:
454 else:
455 return raw_id
455 return raw_id
456
456
457
457
458 def fmt_date(date):
458 def fmt_date(date):
459 if date:
459 if date:
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
461
461
462 return ""
462 return ""
463
463
464
464
465 def is_git(repository):
465 def is_git(repository):
466 if hasattr(repository, 'alias'):
466 if hasattr(repository, 'alias'):
467 _type = repository.alias
467 _type = repository.alias
468 elif hasattr(repository, 'repo_type'):
468 elif hasattr(repository, 'repo_type'):
469 _type = repository.repo_type
469 _type = repository.repo_type
470 else:
470 else:
471 _type = repository
471 _type = repository
472 return _type == 'git'
472 return _type == 'git'
473
473
474
474
475 def is_hg(repository):
475 def is_hg(repository):
476 if hasattr(repository, 'alias'):
476 if hasattr(repository, 'alias'):
477 _type = repository.alias
477 _type = repository.alias
478 elif hasattr(repository, 'repo_type'):
478 elif hasattr(repository, 'repo_type'):
479 _type = repository.repo_type
479 _type = repository.repo_type
480 else:
480 else:
481 _type = repository
481 _type = repository
482 return _type == 'hg'
482 return _type == 'hg'
483
483
484
484
485 @cache_region('long_term', 'user_or_none')
485 @cache_region('long_term', 'user_or_none')
486 def user_or_none(author):
486 def user_or_none(author):
487 """Try to match email part of VCS committer string with a local user - or return None"""
487 """Try to match email part of VCS committer string with a local user - or return None"""
488 email = author_email(author)
488 email = author_email(author)
489 if email:
489 if email:
490 user = User.get_by_email(email, cache=True) # cache will only use sql_cache_short
490 user = User.get_by_email(email, cache=True) # cache will only use sql_cache_short
491 if user is not None:
491 if user is not None:
492 return user
492 return user
493 return None
493 return None
494
494
495 def email_or_none(author):
495 def email_or_none(author):
496 """Try to match email part of VCS committer string with a local user.
496 """Try to match email part of VCS committer string with a local user.
497 Return primary email of user, email part of the specified author name, or None."""
497 Return primary email of user, email part of the specified author name, or None."""
498 if not author:
498 if not author:
499 return None
499 return None
500 user = user_or_none(author)
500 user = user_or_none(author)
501 if user is not None:
501 if user is not None:
502 return user.email # always use main email address - not necessarily the one used to find user
502 return user.email # always use main email address - not necessarily the one used to find user
503
503
504 # extract email from the commit string
504 # extract email from the commit string
505 email = author_email(author)
505 email = author_email(author)
506 if email:
506 if email:
507 return email
507 return email
508
508
509 # No valid email, not a valid user in the system, none!
509 # No valid email, not a valid user in the system, none!
510 return None
510 return None
511
511
512 def person(author, show_attr="username"):
512 def person(author, show_attr="username"):
513 """Find the user identified by 'author', return one of the users attributes,
513 """Find the user identified by 'author', return one of the users attributes,
514 default to the username attribute, None if there is no user"""
514 default to the username attribute, None if there is no user"""
515 # attr to return from fetched user
515 # attr to return from fetched user
516 person_getter = lambda usr: getattr(usr, show_attr)
516 person_getter = lambda usr: getattr(usr, show_attr)
517
517
518 # if author is already an instance use it for extraction
518 # if author is already an instance use it for extraction
519 if isinstance(author, User):
519 if isinstance(author, User):
520 return person_getter(author)
520 return person_getter(author)
521
521
522 user = user_or_none(author)
522 user = user_or_none(author)
523 if user is not None:
523 if user is not None:
524 return person_getter(user)
524 return person_getter(user)
525
525
526 # Still nothing? Just pass back the author name if any, else the email
526 # Still nothing? Just pass back the author name if any, else the email
527 return author_name(author) or email(author)
527 return author_name(author) or email(author)
528
528
529
529
530 def person_by_id(id_, show_attr="username"):
530 def person_by_id(id_, show_attr="username"):
531 # attr to return from fetched user
531 # attr to return from fetched user
532 person_getter = lambda usr: getattr(usr, show_attr)
532 person_getter = lambda usr: getattr(usr, show_attr)
533
533
534 #maybe it's an ID ?
534 #maybe it's an ID ?
535 if str(id_).isdigit() or isinstance(id_, int):
535 if str(id_).isdigit() or isinstance(id_, int):
536 id_ = int(id_)
536 id_ = int(id_)
537 user = User.get(id_)
537 user = User.get(id_)
538 if user is not None:
538 if user is not None:
539 return person_getter(user)
539 return person_getter(user)
540 return id_
540 return id_
541
541
542
542
543 def desc_stylize(value):
543 def desc_stylize(value):
544 """
544 """
545 converts tags from value into html equivalent
545 converts tags from value into html equivalent
546
546
547 :param value:
547 :param value:
548 """
548 """
549 if not value:
549 if not value:
550 return ''
550 return ''
551
551
552 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
552 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
553 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
553 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
554 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
554 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
555 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
555 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
556 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
556 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
557 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
557 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
558 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
558 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
559 '<div class="metatag" tag="lang">\\2</div>', value)
559 '<div class="metatag" tag="lang">\\2</div>', value)
560 value = re.sub(r'\[([a-z]+)\]',
560 value = re.sub(r'\[([a-z]+)\]',
561 '<div class="metatag" tag="\\1">\\1</div>', value)
561 '<div class="metatag" tag="\\1">\\1</div>', value)
562
562
563 return value
563 return value
564
564
565
565
566 def boolicon(value):
566 def boolicon(value):
567 """Returns boolean value of a value, represented as small html image of true/false
567 """Returns boolean value of a value, represented as small html image of true/false
568 icons
568 icons
569
569
570 :param value: value
570 :param value: value
571 """
571 """
572
572
573 if value:
573 if value:
574 return HTML.tag('i', class_="icon-ok")
574 return HTML.tag('i', class_="icon-ok")
575 else:
575 else:
576 return HTML.tag('i', class_="icon-minus-circled")
576 return HTML.tag('i', class_="icon-minus-circled")
577
577
578
578
579 def action_parser(user_log, feed=False, parse_cs=False):
579 def action_parser(user_log, feed=False, parse_cs=False):
580 """
580 """
581 This helper will action_map the specified string action into translated
581 This helper will action_map the specified string action into translated
582 fancy names with icons and links
582 fancy names with icons and links
583
583
584 :param user_log: user log instance
584 :param user_log: user log instance
585 :param feed: use output for feeds (no html and fancy icons)
585 :param feed: use output for feeds (no html and fancy icons)
586 :param parse_cs: parse Changesets into VCS instances
586 :param parse_cs: parse Changesets into VCS instances
587 """
587 """
588
588
589 action = user_log.action
589 action = user_log.action
590 action_params = ' '
590 action_params = ' '
591
591
592 x = action.split(':')
592 x = action.split(':')
593
593
594 if len(x) > 1:
594 if len(x) > 1:
595 action, action_params = x
595 action, action_params = x
596
596
597 def get_cs_links():
597 def get_cs_links():
598 revs_limit = 3 # display this amount always
598 revs_limit = 3 # display this amount always
599 revs_top_limit = 50 # show upto this amount of changesets hidden
599 revs_top_limit = 50 # show upto this amount of changesets hidden
600 revs_ids = action_params.split(',')
600 revs_ids = action_params.split(',')
601 deleted = user_log.repository is None
601 deleted = user_log.repository is None
602 if deleted:
602 if deleted:
603 return ','.join(revs_ids)
603 return ','.join(revs_ids)
604
604
605 repo_name = user_log.repository.repo_name
605 repo_name = user_log.repository.repo_name
606
606
607 def lnk(rev, repo_name):
607 def lnk(rev, repo_name):
608 lazy_cs = False
608 lazy_cs = False
609 title_ = None
609 title_ = None
610 url_ = '#'
610 url_ = '#'
611 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
611 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
612 if rev.op and rev.ref_name:
612 if rev.op and rev.ref_name:
613 if rev.op == 'delete_branch':
613 if rev.op == 'delete_branch':
614 lbl = _('Deleted branch: %s') % rev.ref_name
614 lbl = _('Deleted branch: %s') % rev.ref_name
615 elif rev.op == 'tag':
615 elif rev.op == 'tag':
616 lbl = _('Created tag: %s') % rev.ref_name
616 lbl = _('Created tag: %s') % rev.ref_name
617 else:
617 else:
618 lbl = 'Unknown operation %s' % rev.op
618 lbl = 'Unknown operation %s' % rev.op
619 else:
619 else:
620 lazy_cs = True
620 lazy_cs = True
621 lbl = rev.short_id[:8]
621 lbl = rev.short_id[:8]
622 url_ = url('changeset_home', repo_name=repo_name,
622 url_ = url('changeset_home', repo_name=repo_name,
623 revision=rev.raw_id)
623 revision=rev.raw_id)
624 else:
624 else:
625 # changeset cannot be found - it might have been stripped or removed
625 # changeset cannot be found - it might have been stripped or removed
626 lbl = rev[:12]
626 lbl = rev[:12]
627 title_ = _('Changeset not found')
627 title_ = _('Changeset not found')
628 if parse_cs:
628 if parse_cs:
629 return link_to(lbl, url_, title=title_, class_='tooltip')
629 return link_to(lbl, url_, title=title_, class_='tooltip')
630 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
630 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
631 class_='lazy-cs' if lazy_cs else '')
631 class_='lazy-cs' if lazy_cs else '')
632
632
633 def _get_op(rev_txt):
633 def _get_op(rev_txt):
634 _op = None
634 _op = None
635 _name = rev_txt
635 _name = rev_txt
636 if len(rev_txt.split('=>')) == 2:
636 if len(rev_txt.split('=>')) == 2:
637 _op, _name = rev_txt.split('=>')
637 _op, _name = rev_txt.split('=>')
638 return _op, _name
638 return _op, _name
639
639
640 revs = []
640 revs = []
641 if len(filter(lambda v: v != '', revs_ids)) > 0:
641 if len(filter(lambda v: v != '', revs_ids)) > 0:
642 repo = None
642 repo = None
643 for rev in revs_ids[:revs_top_limit]:
643 for rev in revs_ids[:revs_top_limit]:
644 _op, _name = _get_op(rev)
644 _op, _name = _get_op(rev)
645
645
646 # we want parsed changesets, or new log store format is bad
646 # we want parsed changesets, or new log store format is bad
647 if parse_cs:
647 if parse_cs:
648 try:
648 try:
649 if repo is None:
649 if repo is None:
650 repo = user_log.repository.scm_instance
650 repo = user_log.repository.scm_instance
651 _rev = repo.get_changeset(rev)
651 _rev = repo.get_changeset(rev)
652 revs.append(_rev)
652 revs.append(_rev)
653 except ChangesetDoesNotExistError:
653 except ChangesetDoesNotExistError:
654 log.error('cannot find revision %s in this repo', rev)
654 log.error('cannot find revision %s in this repo', rev)
655 revs.append(rev)
655 revs.append(rev)
656 else:
656 else:
657 _rev = AttributeDict({
657 _rev = AttributeDict({
658 'short_id': rev[:12],
658 'short_id': rev[:12],
659 'raw_id': rev,
659 'raw_id': rev,
660 'message': '',
660 'message': '',
661 'op': _op,
661 'op': _op,
662 'ref_name': _name
662 'ref_name': _name
663 })
663 })
664 revs.append(_rev)
664 revs.append(_rev)
665 cs_links = [" " + ', '.join(
665 cs_links = [" " + ', '.join(
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
667 )]
667 )]
668 _op1, _name1 = _get_op(revs_ids[0])
668 _op1, _name1 = _get_op(revs_ids[0])
669 _op2, _name2 = _get_op(revs_ids[-1])
669 _op2, _name2 = _get_op(revs_ids[-1])
670
670
671 _rev = '%s...%s' % (_name1, _name2)
671 _rev = '%s...%s' % (_name1, _name2)
672
672
673 compare_view = (
673 compare_view = (
674 ' <div class="compare_view tooltip" title="%s">'
674 ' <div class="compare_view tooltip" title="%s">'
675 '<a href="%s">%s</a> </div>' % (
675 '<a href="%s">%s</a> </div>' % (
676 _('Show all combined changesets %s->%s') % (
676 _('Show all combined changesets %s->%s') % (
677 revs_ids[0][:12], revs_ids[-1][:12]
677 revs_ids[0][:12], revs_ids[-1][:12]
678 ),
678 ),
679 url('changeset_home', repo_name=repo_name,
679 url('changeset_home', repo_name=repo_name,
680 revision=_rev
680 revision=_rev
681 ),
681 ),
682 _('Compare view')
682 _('Compare view')
683 )
683 )
684 )
684 )
685
685
686 # if we have exactly one more than normally displayed
686 # if we have exactly one more than normally displayed
687 # just display it, takes less space than displaying
687 # just display it, takes less space than displaying
688 # "and 1 more revisions"
688 # "and 1 more revisions"
689 if len(revs_ids) == revs_limit + 1:
689 if len(revs_ids) == revs_limit + 1:
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
691
691
692 # hidden-by-default ones
692 # hidden-by-default ones
693 if len(revs_ids) > revs_limit + 1:
693 if len(revs_ids) > revs_limit + 1:
694 uniq_id = revs_ids[0]
694 uniq_id = revs_ids[0]
695 html_tmpl = (
695 html_tmpl = (
696 '<span> %s <a class="show_more" id="_%s" '
696 '<span> %s <a class="show_more" id="_%s" '
697 'href="#more">%s</a> %s</span>'
697 'href="#more">%s</a> %s</span>'
698 )
698 )
699 if not feed:
699 if not feed:
700 cs_links.append(html_tmpl % (
700 cs_links.append(html_tmpl % (
701 _('and'),
701 _('and'),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
703 _('revisions')
703 _('revisions')
704 )
704 )
705 )
705 )
706
706
707 if not feed:
707 if not feed:
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
709 else:
709 else:
710 html_tmpl = '<span id="%s"> %s </span>'
710 html_tmpl = '<span id="%s"> %s </span>'
711
711
712 morelinks = ', '.join(
712 morelinks = ', '.join(
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
714 )
714 )
715
715
716 if len(revs_ids) > revs_top_limit:
716 if len(revs_ids) > revs_top_limit:
717 morelinks += ', ...'
717 morelinks += ', ...'
718
718
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
720 if len(revs) > 1:
720 if len(revs) > 1:
721 cs_links.append(compare_view)
721 cs_links.append(compare_view)
722 return ''.join(cs_links)
722 return ''.join(cs_links)
723
723
724 def get_fork_name():
724 def get_fork_name():
725 repo_name = action_params
725 repo_name = action_params
726 url_ = url('summary_home', repo_name=repo_name)
726 url_ = url('summary_home', repo_name=repo_name)
727 return _('Fork name %s') % link_to(action_params, url_)
727 return _('Fork name %s') % link_to(action_params, url_)
728
728
729 def get_user_name():
729 def get_user_name():
730 user_name = action_params
730 user_name = action_params
731 return user_name
731 return user_name
732
732
733 def get_users_group():
733 def get_users_group():
734 group_name = action_params
734 group_name = action_params
735 return group_name
735 return group_name
736
736
737 def get_pull_request():
737 def get_pull_request():
738 pull_request_id = action_params
738 pull_request_id = action_params
739 nice_id = PullRequest.make_nice_id(pull_request_id)
739 nice_id = PullRequest.make_nice_id(pull_request_id)
740
740
741 deleted = user_log.repository is None
741 deleted = user_log.repository is None
742 if deleted:
742 if deleted:
743 repo_name = user_log.repository_name
743 repo_name = user_log.repository_name
744 else:
744 else:
745 repo_name = user_log.repository.repo_name
745 repo_name = user_log.repository.repo_name
746
746
747 return link_to(_('Pull request %s') % nice_id,
747 return link_to(_('Pull request %s') % nice_id,
748 url('pullrequest_show', repo_name=repo_name,
748 url('pullrequest_show', repo_name=repo_name,
749 pull_request_id=pull_request_id))
749 pull_request_id=pull_request_id))
750
750
751 def get_archive_name():
751 def get_archive_name():
752 archive_name = action_params
752 archive_name = action_params
753 return archive_name
753 return archive_name
754
754
755 # action : translated str, callback(extractor), icon
755 # action : translated str, callback(extractor), icon
756 action_map = {
756 action_map = {
757 'user_deleted_repo': (_('[deleted] repository'),
757 'user_deleted_repo': (_('[deleted] repository'),
758 None, 'icon-trashcan'),
758 None, 'icon-trashcan'),
759 'user_created_repo': (_('[created] repository'),
759 'user_created_repo': (_('[created] repository'),
760 None, 'icon-plus'),
760 None, 'icon-plus'),
761 'user_created_fork': (_('[created] repository as fork'),
761 'user_created_fork': (_('[created] repository as fork'),
762 None, 'icon-fork'),
762 None, 'icon-fork'),
763 'user_forked_repo': (_('[forked] repository'),
763 'user_forked_repo': (_('[forked] repository'),
764 get_fork_name, 'icon-fork'),
764 get_fork_name, 'icon-fork'),
765 'user_updated_repo': (_('[updated] repository'),
765 'user_updated_repo': (_('[updated] repository'),
766 None, 'icon-pencil'),
766 None, 'icon-pencil'),
767 'user_downloaded_archive': (_('[downloaded] archive from repository'),
767 'user_downloaded_archive': (_('[downloaded] archive from repository'),
768 get_archive_name, 'icon-download-cloud'),
768 get_archive_name, 'icon-download-cloud'),
769 'admin_deleted_repo': (_('[delete] repository'),
769 'admin_deleted_repo': (_('[delete] repository'),
770 None, 'icon-trashcan'),
770 None, 'icon-trashcan'),
771 'admin_created_repo': (_('[created] repository'),
771 'admin_created_repo': (_('[created] repository'),
772 None, 'icon-plus'),
772 None, 'icon-plus'),
773 'admin_forked_repo': (_('[forked] repository'),
773 'admin_forked_repo': (_('[forked] repository'),
774 None, 'icon-fork'),
774 None, 'icon-fork'),
775 'admin_updated_repo': (_('[updated] repository'),
775 'admin_updated_repo': (_('[updated] repository'),
776 None, 'icon-pencil'),
776 None, 'icon-pencil'),
777 'admin_created_user': (_('[created] user'),
777 'admin_created_user': (_('[created] user'),
778 get_user_name, 'icon-user'),
778 get_user_name, 'icon-user'),
779 'admin_updated_user': (_('[updated] user'),
779 'admin_updated_user': (_('[updated] user'),
780 get_user_name, 'icon-user'),
780 get_user_name, 'icon-user'),
781 'admin_created_users_group': (_('[created] user group'),
781 'admin_created_users_group': (_('[created] user group'),
782 get_users_group, 'icon-pencil'),
782 get_users_group, 'icon-pencil'),
783 'admin_updated_users_group': (_('[updated] user group'),
783 'admin_updated_users_group': (_('[updated] user group'),
784 get_users_group, 'icon-pencil'),
784 get_users_group, 'icon-pencil'),
785 'user_commented_revision': (_('[commented] on revision in repository'),
785 'user_commented_revision': (_('[commented] on revision in repository'),
786 get_cs_links, 'icon-comment'),
786 get_cs_links, 'icon-comment'),
787 'user_commented_pull_request': (_('[commented] on pull request for'),
787 'user_commented_pull_request': (_('[commented] on pull request for'),
788 get_pull_request, 'icon-comment'),
788 get_pull_request, 'icon-comment'),
789 'user_closed_pull_request': (_('[closed] pull request for'),
789 'user_closed_pull_request': (_('[closed] pull request for'),
790 get_pull_request, 'icon-ok'),
790 get_pull_request, 'icon-ok'),
791 'push': (_('[pushed] into'),
791 'push': (_('[pushed] into'),
792 get_cs_links, 'icon-move-up'),
792 get_cs_links, 'icon-move-up'),
793 'push_local': (_('[committed via Kallithea] into repository'),
793 'push_local': (_('[committed via Kallithea] into repository'),
794 get_cs_links, 'icon-pencil'),
794 get_cs_links, 'icon-pencil'),
795 'push_remote': (_('[pulled from remote] into repository'),
795 'push_remote': (_('[pulled from remote] into repository'),
796 get_cs_links, 'icon-move-up'),
796 get_cs_links, 'icon-move-up'),
797 'pull': (_('[pulled] from'),
797 'pull': (_('[pulled] from'),
798 None, 'icon-move-down'),
798 None, 'icon-move-down'),
799 'started_following_repo': (_('[started following] repository'),
799 'started_following_repo': (_('[started following] repository'),
800 None, 'icon-heart'),
800 None, 'icon-heart'),
801 'stopped_following_repo': (_('[stopped following] repository'),
801 'stopped_following_repo': (_('[stopped following] repository'),
802 None, 'icon-heart-empty'),
802 None, 'icon-heart-empty'),
803 }
803 }
804
804
805 action_str = action_map.get(action, action)
805 action_str = action_map.get(action, action)
806 if feed:
806 if feed:
807 action = action_str[0].replace('[', '').replace(']', '')
807 action = action_str[0].replace('[', '').replace(']', '')
808 else:
808 else:
809 action = action_str[0] \
809 action = action_str[0] \
810 .replace('[', '<span class="journal_highlight">') \
810 .replace('[', '<span class="journal_highlight">') \
811 .replace(']', '</span>')
811 .replace(']', '</span>')
812
812
813 action_params_func = lambda: ""
813 action_params_func = lambda: ""
814
814
815 if callable(action_str[1]):
815 if callable(action_str[1]):
816 action_params_func = action_str[1]
816 action_params_func = action_str[1]
817
817
818 def action_parser_icon():
818 def action_parser_icon():
819 action = user_log.action
819 action = user_log.action
820 action_params = None
820 action_params = None
821 x = action.split(':')
821 x = action.split(':')
822
822
823 if len(x) > 1:
823 if len(x) > 1:
824 action, action_params = x
824 action, action_params = x
825
825
826 tmpl = """<i class="%s" alt="%s"></i>"""
826 tmpl = """<i class="%s" alt="%s"></i>"""
827 ico = action_map.get(action, ['', '', ''])[2]
827 ico = action_map.get(action, ['', '', ''])[2]
828 return literal(tmpl % (ico, action))
828 return literal(tmpl % (ico, action))
829
829
830 # returned callbacks we need to call to get
830 # returned callbacks we need to call to get
831 return [lambda: literal(action), action_params_func, action_parser_icon]
831 return [lambda: literal(action), action_params_func, action_parser_icon]
832
832
833
833
834
834
835 #==============================================================================
835 #==============================================================================
836 # PERMS
836 # PERMS
837 #==============================================================================
837 #==============================================================================
838 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
838 from kallithea.lib.auth import HasPermissionAny, \
839 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
839 HasRepoPermissionAny, HasRepoGroupPermissionAny
840 HasRepoGroupPermissionAny
841
840
842
841
843 #==============================================================================
842 #==============================================================================
844 # GRAVATAR URL
843 # GRAVATAR URL
845 #==============================================================================
844 #==============================================================================
846 def gravatar_div(email_address, cls='', size=30, **div_attributes):
845 def gravatar_div(email_address, cls='', size=30, **div_attributes):
847 """Return an html literal with a div around a gravatar if they are enabled.
846 """Return an html literal with a div around a gravatar if they are enabled.
848 Extra keyword parameters starting with 'div_' will get the prefix removed
847 Extra keyword parameters starting with 'div_' will get the prefix removed
849 and be used as attributes on the div. The default class is 'gravatar'.
848 and be used as attributes on the div. The default class is 'gravatar'.
850 """
849 """
851 from pylons import tmpl_context as c
850 from pylons import tmpl_context as c
852 if not c.visual.use_gravatar:
851 if not c.visual.use_gravatar:
853 return ''
852 return ''
854 if 'div_class' not in div_attributes:
853 if 'div_class' not in div_attributes:
855 div_attributes['div_class'] = "gravatar"
854 div_attributes['div_class'] = "gravatar"
856 attributes = []
855 attributes = []
857 for k, v in sorted(div_attributes.items()):
856 for k, v in sorted(div_attributes.items()):
858 assert k.startswith('div_'), k
857 assert k.startswith('div_'), k
859 attributes.append(' %s="%s"' % (k[4:], escape(v)))
858 attributes.append(' %s="%s"' % (k[4:], escape(v)))
860 return literal("""<div%s>%s</div>""" %
859 return literal("""<div%s>%s</div>""" %
861 (''.join(attributes),
860 (''.join(attributes),
862 gravatar(email_address, cls=cls, size=size)))
861 gravatar(email_address, cls=cls, size=size)))
863
862
864 def gravatar(email_address, cls='', size=30):
863 def gravatar(email_address, cls='', size=30):
865 """return html element of the gravatar
864 """return html element of the gravatar
866
865
867 This method will return an <img> with the resolution double the size (for
866 This method will return an <img> with the resolution double the size (for
868 retina screens) of the image. If the url returned from gravatar_url is
867 retina screens) of the image. If the url returned from gravatar_url is
869 empty then we fallback to using an icon.
868 empty then we fallback to using an icon.
870
869
871 """
870 """
872 from pylons import tmpl_context as c
871 from pylons import tmpl_context as c
873 if not c.visual.use_gravatar:
872 if not c.visual.use_gravatar:
874 return ''
873 return ''
875
874
876 src = gravatar_url(email_address, size * 2)
875 src = gravatar_url(email_address, size * 2)
877
876
878 if src:
877 if src:
879 # here it makes sense to use style="width: ..." (instead of, say, a
878 # here it makes sense to use style="width: ..." (instead of, say, a
880 # stylesheet) because we using this to generate a high-res (retina) size
879 # stylesheet) because we using this to generate a high-res (retina) size
881 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
880 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
882 .format(cls=cls, size=size, src=src))
881 .format(cls=cls, size=size, src=src))
883
882
884 else:
883 else:
885 # if src is empty then there was no gravatar, so we use a font icon
884 # if src is empty then there was no gravatar, so we use a font icon
886 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
885 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
887 .format(cls=cls, size=size, src=src))
886 .format(cls=cls, size=size, src=src))
888
887
889 return literal(html)
888 return literal(html)
890
889
891 def gravatar_url(email_address, size=30, default=''):
890 def gravatar_url(email_address, size=30, default=''):
892 # doh, we need to re-import those to mock it later
891 # doh, we need to re-import those to mock it later
893 from pylons import url
892 from pylons import url
894 from pylons import tmpl_context as c
893 from pylons import tmpl_context as c
895 if not c.visual.use_gravatar:
894 if not c.visual.use_gravatar:
896 return ""
895 return ""
897
896
898 _def = 'anonymous@kallithea-scm.org' # default gravatar
897 _def = 'anonymous@kallithea-scm.org' # default gravatar
899 email_address = email_address or _def
898 email_address = email_address or _def
900
899
901 if email_address == _def:
900 if email_address == _def:
902 return default
901 return default
903
902
904 parsed_url = urlparse.urlparse(url.current(qualified=True))
903 parsed_url = urlparse.urlparse(url.current(qualified=True))
905 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
904 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
906 .replace('{email}', email_address) \
905 .replace('{email}', email_address) \
907 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
906 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
908 .replace('{netloc}', parsed_url.netloc) \
907 .replace('{netloc}', parsed_url.netloc) \
909 .replace('{scheme}', parsed_url.scheme) \
908 .replace('{scheme}', parsed_url.scheme) \
910 .replace('{size}', safe_str(size))
909 .replace('{size}', safe_str(size))
911 return url
910 return url
912
911
913 class Page(_Page):
912 class Page(_Page):
914 """
913 """
915 Custom pager to match rendering style with YUI paginator
914 Custom pager to match rendering style with YUI paginator
916 """
915 """
917
916
918 def __init__(self, *args, **kwargs):
917 def __init__(self, *args, **kwargs):
919 kwargs.setdefault('url', url.current)
918 kwargs.setdefault('url', url.current)
920 _Page.__init__(self, *args, **kwargs)
919 _Page.__init__(self, *args, **kwargs)
921
920
922 def _get_pos(self, cur_page, max_page, items):
921 def _get_pos(self, cur_page, max_page, items):
923 edge = (items / 2) + 1
922 edge = (items / 2) + 1
924 if (cur_page <= edge):
923 if (cur_page <= edge):
925 radius = max(items / 2, items - cur_page)
924 radius = max(items / 2, items - cur_page)
926 elif (max_page - cur_page) < edge:
925 elif (max_page - cur_page) < edge:
927 radius = (items - 1) - (max_page - cur_page)
926 radius = (items - 1) - (max_page - cur_page)
928 else:
927 else:
929 radius = items / 2
928 radius = items / 2
930
929
931 left = max(1, (cur_page - (radius)))
930 left = max(1, (cur_page - (radius)))
932 right = min(max_page, cur_page + (radius))
931 right = min(max_page, cur_page + (radius))
933 return left, cur_page, right
932 return left, cur_page, right
934
933
935 def _range(self, regexp_match):
934 def _range(self, regexp_match):
936 """
935 """
937 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
936 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
938
937
939 Arguments:
938 Arguments:
940
939
941 regexp_match
940 regexp_match
942 A "re" (regular expressions) match object containing the
941 A "re" (regular expressions) match object containing the
943 radius of linked pages around the current page in
942 radius of linked pages around the current page in
944 regexp_match.group(1) as a string
943 regexp_match.group(1) as a string
945
944
946 This function is supposed to be called as a callable in
945 This function is supposed to be called as a callable in
947 re.sub.
946 re.sub.
948
947
949 """
948 """
950 radius = int(regexp_match.group(1))
949 radius = int(regexp_match.group(1))
951
950
952 # Compute the first and last page number within the radius
951 # Compute the first and last page number within the radius
953 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
952 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
954 # -> leftmost_page = 5
953 # -> leftmost_page = 5
955 # -> rightmost_page = 9
954 # -> rightmost_page = 9
956 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
955 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
957 self.last_page,
956 self.last_page,
958 (radius * 2) + 1)
957 (radius * 2) + 1)
959 nav_items = []
958 nav_items = []
960
959
961 # Create a link to the first page (unless we are on the first page
960 # Create a link to the first page (unless we are on the first page
962 # or there would be no need to insert '..' spacers)
961 # or there would be no need to insert '..' spacers)
963 if self.page != self.first_page and self.first_page < leftmost_page:
962 if self.page != self.first_page and self.first_page < leftmost_page:
964 nav_items.append(self._pagerlink(self.first_page, self.first_page))
963 nav_items.append(self._pagerlink(self.first_page, self.first_page))
965
964
966 # Insert dots if there are pages between the first page
965 # Insert dots if there are pages between the first page
967 # and the currently displayed page range
966 # and the currently displayed page range
968 if leftmost_page - self.first_page > 1:
967 if leftmost_page - self.first_page > 1:
969 # Wrap in a SPAN tag if nolink_attr is set
968 # Wrap in a SPAN tag if nolink_attr is set
970 text_ = '..'
969 text_ = '..'
971 if self.dotdot_attr:
970 if self.dotdot_attr:
972 text_ = HTML.span(c=text_, **self.dotdot_attr)
971 text_ = HTML.span(c=text_, **self.dotdot_attr)
973 nav_items.append(text_)
972 nav_items.append(text_)
974
973
975 for thispage in xrange(leftmost_page, rightmost_page + 1):
974 for thispage in xrange(leftmost_page, rightmost_page + 1):
976 # Highlight the current page number and do not use a link
975 # Highlight the current page number and do not use a link
977 text_ = str(thispage)
976 text_ = str(thispage)
978 if thispage == self.page:
977 if thispage == self.page:
979 # Wrap in a SPAN tag if nolink_attr is set
978 # Wrap in a SPAN tag if nolink_attr is set
980 if self.curpage_attr:
979 if self.curpage_attr:
981 text_ = HTML.span(c=text_, **self.curpage_attr)
980 text_ = HTML.span(c=text_, **self.curpage_attr)
982 nav_items.append(text_)
981 nav_items.append(text_)
983 # Otherwise create just a link to that page
982 # Otherwise create just a link to that page
984 else:
983 else:
985 nav_items.append(self._pagerlink(thispage, text_))
984 nav_items.append(self._pagerlink(thispage, text_))
986
985
987 # Insert dots if there are pages between the displayed
986 # Insert dots if there are pages between the displayed
988 # page numbers and the end of the page range
987 # page numbers and the end of the page range
989 if self.last_page - rightmost_page > 1:
988 if self.last_page - rightmost_page > 1:
990 text_ = '..'
989 text_ = '..'
991 # Wrap in a SPAN tag if nolink_attr is set
990 # Wrap in a SPAN tag if nolink_attr is set
992 if self.dotdot_attr:
991 if self.dotdot_attr:
993 text_ = HTML.span(c=text_, **self.dotdot_attr)
992 text_ = HTML.span(c=text_, **self.dotdot_attr)
994 nav_items.append(text_)
993 nav_items.append(text_)
995
994
996 # Create a link to the very last page (unless we are on the last
995 # Create a link to the very last page (unless we are on the last
997 # page or there would be no need to insert '..' spacers)
996 # page or there would be no need to insert '..' spacers)
998 if self.page != self.last_page and rightmost_page < self.last_page:
997 if self.page != self.last_page and rightmost_page < self.last_page:
999 nav_items.append(self._pagerlink(self.last_page, self.last_page))
998 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1000
999
1001 #_page_link = url.current()
1000 #_page_link = url.current()
1002 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1001 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1003 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1002 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1004 return self.separator.join(nav_items)
1003 return self.separator.join(nav_items)
1005
1004
1006 def pager(self, format='~2~', page_param='page', partial_param='partial',
1005 def pager(self, format='~2~', page_param='page', partial_param='partial',
1007 show_if_single_page=False, separator=' ', onclick=None,
1006 show_if_single_page=False, separator=' ', onclick=None,
1008 symbol_first='<<', symbol_last='>>',
1007 symbol_first='<<', symbol_last='>>',
1009 symbol_previous='<', symbol_next='>',
1008 symbol_previous='<', symbol_next='>',
1010 link_attr=None,
1009 link_attr=None,
1011 curpage_attr=None,
1010 curpage_attr=None,
1012 dotdot_attr=None, **kwargs):
1011 dotdot_attr=None, **kwargs):
1013 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
1012 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
1014 self.separator = separator
1013 self.separator = separator
1015 self.pager_kwargs = kwargs
1014 self.pager_kwargs = kwargs
1016 self.page_param = page_param
1015 self.page_param = page_param
1017 self.partial_param = partial_param
1016 self.partial_param = partial_param
1018 self.onclick = onclick
1017 self.onclick = onclick
1019 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
1018 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
1020 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
1019 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
1021
1020
1022 # Don't show navigator if there is no more than one page
1021 # Don't show navigator if there is no more than one page
1023 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1022 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1024 return ''
1023 return ''
1025
1024
1026 from string import Template
1025 from string import Template
1027 # Replace ~...~ in token format by range of pages
1026 # Replace ~...~ in token format by range of pages
1028 result = re.sub(r'~(\d+)~', self._range, format)
1027 result = re.sub(r'~(\d+)~', self._range, format)
1029
1028
1030 # Interpolate '%' variables
1029 # Interpolate '%' variables
1031 result = Template(result).safe_substitute({
1030 result = Template(result).safe_substitute({
1032 'first_page': self.first_page,
1031 'first_page': self.first_page,
1033 'last_page': self.last_page,
1032 'last_page': self.last_page,
1034 'page': self.page,
1033 'page': self.page,
1035 'page_count': self.page_count,
1034 'page_count': self.page_count,
1036 'items_per_page': self.items_per_page,
1035 'items_per_page': self.items_per_page,
1037 'first_item': self.first_item,
1036 'first_item': self.first_item,
1038 'last_item': self.last_item,
1037 'last_item': self.last_item,
1039 'item_count': self.item_count,
1038 'item_count': self.item_count,
1040 'link_first': self.page > self.first_page and \
1039 'link_first': self.page > self.first_page and \
1041 self._pagerlink(self.first_page, symbol_first) or '',
1040 self._pagerlink(self.first_page, symbol_first) or '',
1042 'link_last': self.page < self.last_page and \
1041 'link_last': self.page < self.last_page and \
1043 self._pagerlink(self.last_page, symbol_last) or '',
1042 self._pagerlink(self.last_page, symbol_last) or '',
1044 'link_previous': self.previous_page and \
1043 'link_previous': self.previous_page and \
1045 self._pagerlink(self.previous_page, symbol_previous) \
1044 self._pagerlink(self.previous_page, symbol_previous) \
1046 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1045 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1047 'link_next': self.next_page and \
1046 'link_next': self.next_page and \
1048 self._pagerlink(self.next_page, symbol_next) \
1047 self._pagerlink(self.next_page, symbol_next) \
1049 or HTML.span(symbol_next, class_="yui-pg-next")
1048 or HTML.span(symbol_next, class_="yui-pg-next")
1050 })
1049 })
1051
1050
1052 return literal(result)
1051 return literal(result)
1053
1052
1054
1053
1055 #==============================================================================
1054 #==============================================================================
1056 # REPO PAGER, PAGER FOR REPOSITORY
1055 # REPO PAGER, PAGER FOR REPOSITORY
1057 #==============================================================================
1056 #==============================================================================
1058 class RepoPage(Page):
1057 class RepoPage(Page):
1059
1058
1060 def __init__(self, collection, page=1, items_per_page=20,
1059 def __init__(self, collection, page=1, items_per_page=20,
1061 item_count=None, **kwargs):
1060 item_count=None, **kwargs):
1062
1061
1063 """Create a "RepoPage" instance. special pager for paging
1062 """Create a "RepoPage" instance. special pager for paging
1064 repository
1063 repository
1065 """
1064 """
1066 # TODO: call baseclass __init__
1065 # TODO: call baseclass __init__
1067 self._url_generator = kwargs.pop('url', url.current)
1066 self._url_generator = kwargs.pop('url', url.current)
1068
1067
1069 # Safe the kwargs class-wide so they can be used in the pager() method
1068 # Safe the kwargs class-wide so they can be used in the pager() method
1070 self.kwargs = kwargs
1069 self.kwargs = kwargs
1071
1070
1072 # Save a reference to the collection
1071 # Save a reference to the collection
1073 self.original_collection = collection
1072 self.original_collection = collection
1074
1073
1075 self.collection = collection
1074 self.collection = collection
1076
1075
1077 # The self.page is the number of the current page.
1076 # The self.page is the number of the current page.
1078 # The first page has the number 1!
1077 # The first page has the number 1!
1079 try:
1078 try:
1080 self.page = int(page) # make it int() if we get it as a string
1079 self.page = int(page) # make it int() if we get it as a string
1081 except (ValueError, TypeError):
1080 except (ValueError, TypeError):
1082 self.page = 1
1081 self.page = 1
1083
1082
1084 self.items_per_page = items_per_page
1083 self.items_per_page = items_per_page
1085
1084
1086 # Unless the user tells us how many items the collections has
1085 # Unless the user tells us how many items the collections has
1087 # we calculate that ourselves.
1086 # we calculate that ourselves.
1088 if item_count is not None:
1087 if item_count is not None:
1089 self.item_count = item_count
1088 self.item_count = item_count
1090 else:
1089 else:
1091 self.item_count = len(self.collection)
1090 self.item_count = len(self.collection)
1092
1091
1093 # Compute the number of the first and last available page
1092 # Compute the number of the first and last available page
1094 if self.item_count > 0:
1093 if self.item_count > 0:
1095 self.first_page = 1
1094 self.first_page = 1
1096 self.page_count = int(math.ceil(float(self.item_count) /
1095 self.page_count = int(math.ceil(float(self.item_count) /
1097 self.items_per_page))
1096 self.items_per_page))
1098 self.last_page = self.first_page + self.page_count - 1
1097 self.last_page = self.first_page + self.page_count - 1
1099
1098
1100 # Make sure that the requested page number is the range of
1099 # Make sure that the requested page number is the range of
1101 # valid pages
1100 # valid pages
1102 if self.page > self.last_page:
1101 if self.page > self.last_page:
1103 self.page = self.last_page
1102 self.page = self.last_page
1104 elif self.page < self.first_page:
1103 elif self.page < self.first_page:
1105 self.page = self.first_page
1104 self.page = self.first_page
1106
1105
1107 # Note: the number of items on this page can be less than
1106 # Note: the number of items on this page can be less than
1108 # items_per_page if the last page is not full
1107 # items_per_page if the last page is not full
1109 self.first_item = max(0, (self.item_count) - (self.page *
1108 self.first_item = max(0, (self.item_count) - (self.page *
1110 items_per_page))
1109 items_per_page))
1111 self.last_item = ((self.item_count - 1) - items_per_page *
1110 self.last_item = ((self.item_count - 1) - items_per_page *
1112 (self.page - 1))
1111 (self.page - 1))
1113
1112
1114 self.items = list(self.collection[self.first_item:self.last_item + 1])
1113 self.items = list(self.collection[self.first_item:self.last_item + 1])
1115
1114
1116 # Links to previous and next page
1115 # Links to previous and next page
1117 if self.page > self.first_page:
1116 if self.page > self.first_page:
1118 self.previous_page = self.page - 1
1117 self.previous_page = self.page - 1
1119 else:
1118 else:
1120 self.previous_page = None
1119 self.previous_page = None
1121
1120
1122 if self.page < self.last_page:
1121 if self.page < self.last_page:
1123 self.next_page = self.page + 1
1122 self.next_page = self.page + 1
1124 else:
1123 else:
1125 self.next_page = None
1124 self.next_page = None
1126
1125
1127 # No items available
1126 # No items available
1128 else:
1127 else:
1129 self.first_page = None
1128 self.first_page = None
1130 self.page_count = 0
1129 self.page_count = 0
1131 self.last_page = None
1130 self.last_page = None
1132 self.first_item = None
1131 self.first_item = None
1133 self.last_item = None
1132 self.last_item = None
1134 self.previous_page = None
1133 self.previous_page = None
1135 self.next_page = None
1134 self.next_page = None
1136 self.items = []
1135 self.items = []
1137
1136
1138 # This is a subclass of the 'list' type. Initialise the list now.
1137 # This is a subclass of the 'list' type. Initialise the list now.
1139 list.__init__(self, reversed(self.items))
1138 list.__init__(self, reversed(self.items))
1140
1139
1141
1140
1142 def changed_tooltip(nodes):
1141 def changed_tooltip(nodes):
1143 """
1142 """
1144 Generates a html string for changed nodes in changeset page.
1143 Generates a html string for changed nodes in changeset page.
1145 It limits the output to 30 entries
1144 It limits the output to 30 entries
1146
1145
1147 :param nodes: LazyNodesGenerator
1146 :param nodes: LazyNodesGenerator
1148 """
1147 """
1149 if nodes:
1148 if nodes:
1150 pref = ': <br/> '
1149 pref = ': <br/> '
1151 suf = ''
1150 suf = ''
1152 if len(nodes) > 30:
1151 if len(nodes) > 30:
1153 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1152 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1154 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1153 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1155 for x in nodes[:30]]) + suf)
1154 for x in nodes[:30]]) + suf)
1156 else:
1155 else:
1157 return ': ' + _('No files')
1156 return ': ' + _('No files')
1158
1157
1159
1158
1160 def repo_link(groups_and_repos):
1159 def repo_link(groups_and_repos):
1161 """
1160 """
1162 Makes a breadcrumbs link to repo within a group
1161 Makes a breadcrumbs link to repo within a group
1163 joins &raquo; on each group to create a fancy link
1162 joins &raquo; on each group to create a fancy link
1164
1163
1165 ex::
1164 ex::
1166 group >> subgroup >> repo
1165 group >> subgroup >> repo
1167
1166
1168 :param groups_and_repos:
1167 :param groups_and_repos:
1169 :param last_url:
1168 :param last_url:
1170 """
1169 """
1171 groups, just_name, repo_name = groups_and_repos
1170 groups, just_name, repo_name = groups_and_repos
1172 last_url = url('summary_home', repo_name=repo_name)
1171 last_url = url('summary_home', repo_name=repo_name)
1173 last_link = link_to(just_name, last_url)
1172 last_link = link_to(just_name, last_url)
1174
1173
1175 def make_link(group):
1174 def make_link(group):
1176 return link_to(group.name,
1175 return link_to(group.name,
1177 url('repos_group_home', group_name=group.group_name))
1176 url('repos_group_home', group_name=group.group_name))
1178 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1177 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1179
1178
1180
1179
1181 def fancy_file_stats(stats):
1180 def fancy_file_stats(stats):
1182 """
1181 """
1183 Displays a fancy two colored bar for number of added/deleted
1182 Displays a fancy two colored bar for number of added/deleted
1184 lines of code on file
1183 lines of code on file
1185
1184
1186 :param stats: two element list of added/deleted lines of code
1185 :param stats: two element list of added/deleted lines of code
1187 """
1186 """
1188 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1187 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1189 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1188 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1190
1189
1191 def cgen(l_type, a_v, d_v):
1190 def cgen(l_type, a_v, d_v):
1192 mapping = {'tr': 'top-right-rounded-corner-mid',
1191 mapping = {'tr': 'top-right-rounded-corner-mid',
1193 'tl': 'top-left-rounded-corner-mid',
1192 'tl': 'top-left-rounded-corner-mid',
1194 'br': 'bottom-right-rounded-corner-mid',
1193 'br': 'bottom-right-rounded-corner-mid',
1195 'bl': 'bottom-left-rounded-corner-mid'}
1194 'bl': 'bottom-left-rounded-corner-mid'}
1196 map_getter = lambda x: mapping[x]
1195 map_getter = lambda x: mapping[x]
1197
1196
1198 if l_type == 'a' and d_v:
1197 if l_type == 'a' and d_v:
1199 #case when added and deleted are present
1198 #case when added and deleted are present
1200 return ' '.join(map(map_getter, ['tl', 'bl']))
1199 return ' '.join(map(map_getter, ['tl', 'bl']))
1201
1200
1202 if l_type == 'a' and not d_v:
1201 if l_type == 'a' and not d_v:
1203 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1202 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1204
1203
1205 if l_type == 'd' and a_v:
1204 if l_type == 'd' and a_v:
1206 return ' '.join(map(map_getter, ['tr', 'br']))
1205 return ' '.join(map(map_getter, ['tr', 'br']))
1207
1206
1208 if l_type == 'd' and not a_v:
1207 if l_type == 'd' and not a_v:
1209 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1208 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1210
1209
1211 a, d = stats['added'], stats['deleted']
1210 a, d = stats['added'], stats['deleted']
1212 width = 100
1211 width = 100
1213
1212
1214 if stats['binary']:
1213 if stats['binary']:
1215 #binary mode
1214 #binary mode
1216 lbl = ''
1215 lbl = ''
1217 bin_op = 1
1216 bin_op = 1
1218
1217
1219 if BIN_FILENODE in stats['ops']:
1218 if BIN_FILENODE in stats['ops']:
1220 lbl = 'bin+'
1219 lbl = 'bin+'
1221
1220
1222 if NEW_FILENODE in stats['ops']:
1221 if NEW_FILENODE in stats['ops']:
1223 lbl += _('new file')
1222 lbl += _('new file')
1224 bin_op = NEW_FILENODE
1223 bin_op = NEW_FILENODE
1225 elif MOD_FILENODE in stats['ops']:
1224 elif MOD_FILENODE in stats['ops']:
1226 lbl += _('mod')
1225 lbl += _('mod')
1227 bin_op = MOD_FILENODE
1226 bin_op = MOD_FILENODE
1228 elif DEL_FILENODE in stats['ops']:
1227 elif DEL_FILENODE in stats['ops']:
1229 lbl += _('del')
1228 lbl += _('del')
1230 bin_op = DEL_FILENODE
1229 bin_op = DEL_FILENODE
1231 elif RENAMED_FILENODE in stats['ops']:
1230 elif RENAMED_FILENODE in stats['ops']:
1232 lbl += _('rename')
1231 lbl += _('rename')
1233 bin_op = RENAMED_FILENODE
1232 bin_op = RENAMED_FILENODE
1234
1233
1235 #chmod can go with other operations
1234 #chmod can go with other operations
1236 if CHMOD_FILENODE in stats['ops']:
1235 if CHMOD_FILENODE in stats['ops']:
1237 _org_lbl = _('chmod')
1236 _org_lbl = _('chmod')
1238 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1237 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1239
1238
1240 #import ipdb;ipdb.set_trace()
1239 #import ipdb;ipdb.set_trace()
1241 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1240 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1242 b_a = '<div class="bin bin1" style="width:0%"></div>'
1241 b_a = '<div class="bin bin1" style="width:0%"></div>'
1243 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1242 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1244
1243
1245 t = stats['added'] + stats['deleted']
1244 t = stats['added'] + stats['deleted']
1246 unit = float(width) / (t or 1)
1245 unit = float(width) / (t or 1)
1247
1246
1248 # needs > 9% of width to be visible or 0 to be hidden
1247 # needs > 9% of width to be visible or 0 to be hidden
1249 a_p = max(9, unit * a) if a > 0 else 0
1248 a_p = max(9, unit * a) if a > 0 else 0
1250 d_p = max(9, unit * d) if d > 0 else 0
1249 d_p = max(9, unit * d) if d > 0 else 0
1251 p_sum = a_p + d_p
1250 p_sum = a_p + d_p
1252
1251
1253 if p_sum > width:
1252 if p_sum > width:
1254 #adjust the percentage to be == 100% since we adjusted to 9
1253 #adjust the percentage to be == 100% since we adjusted to 9
1255 if a_p > d_p:
1254 if a_p > d_p:
1256 a_p = a_p - (p_sum - width)
1255 a_p = a_p - (p_sum - width)
1257 else:
1256 else:
1258 d_p = d_p - (p_sum - width)
1257 d_p = d_p - (p_sum - width)
1259
1258
1260 a_v = a if a > 0 else ''
1259 a_v = a if a > 0 else ''
1261 d_v = d if d > 0 else ''
1260 d_v = d if d > 0 else ''
1262
1261
1263 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1262 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1264 cgen('a', a_v, d_v), a_p, a_v
1263 cgen('a', a_v, d_v), a_p, a_v
1265 )
1264 )
1266 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1265 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1267 cgen('d', a_v, d_v), d_p, d_v
1266 cgen('d', a_v, d_v), d_p, d_v
1268 )
1267 )
1269 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1268 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1270
1269
1271
1270
1272 def _urlify_text_replace(match_obj):
1271 def _urlify_text_replace(match_obj):
1273 url_full = match_obj.group(1)
1272 url_full = match_obj.group(1)
1274 return '<a href="%(url)s">%(url)s</a>' % {'url': url_full}
1273 return '<a href="%(url)s">%(url)s</a>' % {'url': url_full}
1275
1274
1276
1275
1277 def _urlify_text(s):
1276 def _urlify_text(s):
1278 """
1277 """
1279 Extract urls from text and make html links out of them
1278 Extract urls from text and make html links out of them
1280 """
1279 """
1281 return url_re.sub(_urlify_text_replace, s)
1280 return url_re.sub(_urlify_text_replace, s)
1282
1281
1283 def urlify_text(s, truncate=None, stylize=False, truncatef=truncate):
1282 def urlify_text(s, truncate=None, stylize=False, truncatef=truncate):
1284 """
1283 """
1285 Extract urls from text and make literal html links out of them
1284 Extract urls from text and make literal html links out of them
1286 """
1285 """
1287 if truncate is not None:
1286 if truncate is not None:
1288 s = truncatef(s, truncate, whole_word=True)
1287 s = truncatef(s, truncate, whole_word=True)
1289 s = html_escape(s)
1288 s = html_escape(s)
1290 if stylize:
1289 if stylize:
1291 s = desc_stylize(s)
1290 s = desc_stylize(s)
1292 s = _urlify_text(s)
1291 s = _urlify_text(s)
1293 return literal(s)
1292 return literal(s)
1294
1293
1295
1294
1296 def _urlify_changeset_replace_f(repository):
1295 def _urlify_changeset_replace_f(repository):
1297 from pylons import url # doh, we need to re-import url to mock it later
1296 from pylons import url # doh, we need to re-import url to mock it later
1298 def urlify_changeset_replace(match_obj):
1297 def urlify_changeset_replace(match_obj):
1299 rev = match_obj.group(0)
1298 rev = match_obj.group(0)
1300 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1299 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1301 'url': url('changeset_home', repo_name=repository, revision=rev),
1300 'url': url('changeset_home', repo_name=repository, revision=rev),
1302 'rev': rev,
1301 'rev': rev,
1303 }
1302 }
1304 return urlify_changeset_replace
1303 return urlify_changeset_replace
1305
1304
1306
1305
1307 urilify_changeset_re = r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])'
1306 urilify_changeset_re = r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])'
1308
1307
1309 def urlify_changesets(text_, repository):
1308 def urlify_changesets(text_, repository):
1310 """
1309 """
1311 Extract revision ids from changeset and make link from them
1310 Extract revision ids from changeset and make link from them
1312
1311
1313 :param text_:
1312 :param text_:
1314 :param repository: repo name to build the URL with
1313 :param repository: repo name to build the URL with
1315 """
1314 """
1316 urlify_changeset_replace = _urlify_changeset_replace_f(repository)
1315 urlify_changeset_replace = _urlify_changeset_replace_f(repository)
1317 return re.sub(urilify_changeset_re, urlify_changeset_replace, text_)
1316 return re.sub(urilify_changeset_re, urlify_changeset_replace, text_)
1318
1317
1319
1318
1320 def linkify_others(t, l):
1319 def linkify_others(t, l):
1321 # attempt at fixing double quoting?
1320 # attempt at fixing double quoting?
1322 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1321 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1323 links = []
1322 links = []
1324 for e in urls.split(t):
1323 for e in urls.split(t):
1325 if not urls.match(e):
1324 if not urls.match(e):
1326 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1325 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1327 else:
1326 else:
1328 links.append(e)
1327 links.append(e)
1329
1328
1330 return ''.join(links)
1329 return ''.join(links)
1331
1330
1332 def urlify_commit(text_, repository, link_=None):
1331 def urlify_commit(text_, repository, link_=None):
1333 """
1332 """
1334 Parses given text message and makes proper links.
1333 Parses given text message and makes proper links.
1335 issues are linked to given issue-server, and rest is a changeset link
1334 issues are linked to given issue-server, and rest is a changeset link
1336 if link_ is given, in other case it's a plain text
1335 if link_ is given, in other case it's a plain text
1337
1336
1338 :param text_:
1337 :param text_:
1339 :param repository:
1338 :param repository:
1340 :param link_: changeset link
1339 :param link_: changeset link
1341 """
1340 """
1342 newtext = html_escape(text_)
1341 newtext = html_escape(text_)
1343
1342
1344 # urlify changesets - extract revisions and make link out of them
1343 # urlify changesets - extract revisions and make link out of them
1345 newtext = urlify_changesets(newtext, repository)
1344 newtext = urlify_changesets(newtext, repository)
1346
1345
1347 # extract http/https links and make them real urls
1346 # extract http/https links and make them real urls
1348 newtext = _urlify_text(newtext)
1347 newtext = _urlify_text(newtext)
1349
1348
1350 newtext = urlify_issues(newtext, repository, link_)
1349 newtext = urlify_issues(newtext, repository, link_)
1351
1350
1352 return literal(newtext)
1351 return literal(newtext)
1353
1352
1354
1353
1355 def _urlify_issues_replace_f(repository, ISSUE_SERVER_LNK, ISSUE_PREFIX):
1354 def _urlify_issues_replace_f(repository, ISSUE_SERVER_LNK, ISSUE_PREFIX):
1356 def urlify_issues_replace(match_obj):
1355 def urlify_issues_replace(match_obj):
1357 pref = ''
1356 pref = ''
1358 if match_obj.group().startswith(' '):
1357 if match_obj.group().startswith(' '):
1359 pref = ' '
1358 pref = ' '
1360
1359
1361 issue_id = ''.join(match_obj.groups())
1360 issue_id = ''.join(match_obj.groups())
1362 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1361 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1363 if repository:
1362 if repository:
1364 issue_url = issue_url.replace('{repo}', repository)
1363 issue_url = issue_url.replace('{repo}', repository)
1365 repo_name = repository.split(URL_SEP)[-1]
1364 repo_name = repository.split(URL_SEP)[-1]
1366 issue_url = issue_url.replace('{repo_name}', repo_name)
1365 issue_url = issue_url.replace('{repo_name}', repo_name)
1367
1366
1368 return (
1367 return (
1369 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1368 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1370 '%(issue-prefix)s%(id-repr)s'
1369 '%(issue-prefix)s%(id-repr)s'
1371 '</a>'
1370 '</a>'
1372 ) % {
1371 ) % {
1373 'pref': pref,
1372 'pref': pref,
1374 'cls': 'issue-tracker-link',
1373 'cls': 'issue-tracker-link',
1375 'url': issue_url,
1374 'url': issue_url,
1376 'id-repr': issue_id,
1375 'id-repr': issue_id,
1377 'issue-prefix': ISSUE_PREFIX,
1376 'issue-prefix': ISSUE_PREFIX,
1378 'serv': ISSUE_SERVER_LNK,
1377 'serv': ISSUE_SERVER_LNK,
1379 }
1378 }
1380 return urlify_issues_replace
1379 return urlify_issues_replace
1381
1380
1382
1381
1383 def urlify_issues(newtext, repository, link_=None):
1382 def urlify_issues(newtext, repository, link_=None):
1384 from kallithea import CONFIG as conf
1383 from kallithea import CONFIG as conf
1385
1384
1386 # allow multiple issue servers to be used
1385 # allow multiple issue servers to be used
1387 valid_indices = [
1386 valid_indices = [
1388 x.group(1)
1387 x.group(1)
1389 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1388 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1390 if x and 'issue_server_link%s' % x.group(1) in conf
1389 if x and 'issue_server_link%s' % x.group(1) in conf
1391 and 'issue_prefix%s' % x.group(1) in conf
1390 and 'issue_prefix%s' % x.group(1) in conf
1392 ]
1391 ]
1393
1392
1394 if valid_indices:
1393 if valid_indices:
1395 log.debug('found issue server suffixes `%s` during valuation of: %s',
1394 log.debug('found issue server suffixes `%s` during valuation of: %s',
1396 ','.join(valid_indices), newtext)
1395 ','.join(valid_indices), newtext)
1397
1396
1398 for pattern_index in valid_indices:
1397 for pattern_index in valid_indices:
1399 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1398 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1400 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1399 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1401 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1400 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1402
1401
1403 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1402 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1404 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1403 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1405 ISSUE_PREFIX)
1404 ISSUE_PREFIX)
1406
1405
1407 URL_PAT = re.compile(ISSUE_PATTERN)
1406 URL_PAT = re.compile(ISSUE_PATTERN)
1408
1407
1409 urlify_issues_replace = _urlify_issues_replace_f(repository, ISSUE_SERVER_LNK, ISSUE_PREFIX)
1408 urlify_issues_replace = _urlify_issues_replace_f(repository, ISSUE_SERVER_LNK, ISSUE_PREFIX)
1410 newtext = URL_PAT.sub(urlify_issues_replace, newtext)
1409 newtext = URL_PAT.sub(urlify_issues_replace, newtext)
1411 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1410 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1412
1411
1413 # if we actually did something above
1412 # if we actually did something above
1414 if link_:
1413 if link_:
1415 # wrap not links into final link => link_
1414 # wrap not links into final link => link_
1416 newtext = linkify_others(newtext, link_)
1415 newtext = linkify_others(newtext, link_)
1417 return newtext
1416 return newtext
1418
1417
1419
1418
1420 def _mentions_replace(match_obj):
1419 def _mentions_replace(match_obj):
1421 return '<b>@%s</b>' % match_obj.group(1)
1420 return '<b>@%s</b>' % match_obj.group(1)
1422
1421
1423
1422
1424 def render_w_mentions(source, repo_name=None):
1423 def render_w_mentions(source, repo_name=None):
1425 """
1424 """
1426 Render plain text with revision hashes and issue references urlified
1425 Render plain text with revision hashes and issue references urlified
1427 and with @mention highlighting.
1426 and with @mention highlighting.
1428 """
1427 """
1429 s = source.rstrip()
1428 s = source.rstrip()
1430 s = safe_unicode(s)
1429 s = safe_unicode(s)
1431 s = '\n'.join(s.splitlines())
1430 s = '\n'.join(s.splitlines())
1432 s = html_escape(s)
1431 s = html_escape(s)
1433 # this sequence of html-ifications seems to be safe and non-conflicting
1432 # this sequence of html-ifications seems to be safe and non-conflicting
1434 # if the issues regexp is sane
1433 # if the issues regexp is sane
1435 s = _urlify_text(s)
1434 s = _urlify_text(s)
1436 if repo_name is not None:
1435 if repo_name is not None:
1437 s = urlify_changesets(s, repo_name)
1436 s = urlify_changesets(s, repo_name)
1438 s = urlify_issues(s, repo_name)
1437 s = urlify_issues(s, repo_name)
1439 s = MENTIONS_REGEX.sub(_mentions_replace, s)
1438 s = MENTIONS_REGEX.sub(_mentions_replace, s)
1440 return literal('<div class="formatted-fixed">%s</div>' % s)
1439 return literal('<div class="formatted-fixed">%s</div>' % s)
1441
1440
1442
1441
1443 def short_ref(ref_type, ref_name):
1442 def short_ref(ref_type, ref_name):
1444 if ref_type == 'rev':
1443 if ref_type == 'rev':
1445 return short_id(ref_name)
1444 return short_id(ref_name)
1446 return ref_name
1445 return ref_name
1447
1446
1448 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1447 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1449 """
1448 """
1450 Return full markup for a href to changeset_home for a changeset.
1449 Return full markup for a href to changeset_home for a changeset.
1451 If ref_type is branch it will link to changelog.
1450 If ref_type is branch it will link to changelog.
1452 ref_name is shortened if ref_type is 'rev'.
1451 ref_name is shortened if ref_type is 'rev'.
1453 if rev is specified show it too, explicitly linking to that revision.
1452 if rev is specified show it too, explicitly linking to that revision.
1454 """
1453 """
1455 txt = short_ref(ref_type, ref_name)
1454 txt = short_ref(ref_type, ref_name)
1456 if ref_type == 'branch':
1455 if ref_type == 'branch':
1457 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1456 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1458 else:
1457 else:
1459 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1458 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1460 l = link_to(repo_name + '#' + txt, u)
1459 l = link_to(repo_name + '#' + txt, u)
1461 if rev and ref_type != 'rev':
1460 if rev and ref_type != 'rev':
1462 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1461 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1463 return l
1462 return l
1464
1463
1465 def changeset_status(repo, revision):
1464 def changeset_status(repo, revision):
1466 return ChangesetStatusModel().get_status(repo, revision)
1465 return ChangesetStatusModel().get_status(repo, revision)
1467
1466
1468
1467
1469 def changeset_status_lbl(changeset_status):
1468 def changeset_status_lbl(changeset_status):
1470 return ChangesetStatus.get_status_lbl(changeset_status)
1469 return ChangesetStatus.get_status_lbl(changeset_status)
1471
1470
1472
1471
1473 def get_permission_name(key):
1472 def get_permission_name(key):
1474 return dict(Permission.PERMS).get(key)
1473 return dict(Permission.PERMS).get(key)
1475
1474
1476
1475
1477 def journal_filter_help():
1476 def journal_filter_help():
1478 return _(textwrap.dedent('''
1477 return _(textwrap.dedent('''
1479 Example filter terms:
1478 Example filter terms:
1480 repository:vcs
1479 repository:vcs
1481 username:developer
1480 username:developer
1482 action:*push*
1481 action:*push*
1483 ip:127.0.0.1
1482 ip:127.0.0.1
1484 date:20120101
1483 date:20120101
1485 date:[20120101100000 TO 20120102]
1484 date:[20120101100000 TO 20120102]
1486
1485
1487 Generate wildcards using '*' character:
1486 Generate wildcards using '*' character:
1488 "repository:vcs*" - search everything starting with 'vcs'
1487 "repository:vcs*" - search everything starting with 'vcs'
1489 "repository:*vcs*" - search for repository containing 'vcs'
1488 "repository:*vcs*" - search for repository containing 'vcs'
1490
1489
1491 Optional AND / OR operators in queries
1490 Optional AND / OR operators in queries
1492 "repository:vcs OR repository:test"
1491 "repository:vcs OR repository:test"
1493 "username:test AND repository:test*"
1492 "username:test AND repository:test*"
1494 '''))
1493 '''))
1495
1494
1496
1495
1497 def not_mapped_error(repo_name):
1496 def not_mapped_error(repo_name):
1498 flash(_('%s repository is not mapped to db perhaps'
1497 flash(_('%s repository is not mapped to db perhaps'
1499 ' it was created or renamed from the filesystem'
1498 ' it was created or renamed from the filesystem'
1500 ' please run the application again'
1499 ' please run the application again'
1501 ' in order to rescan repositories') % repo_name, category='error')
1500 ' in order to rescan repositories') % repo_name, category='error')
1502
1501
1503
1502
1504 def ip_range(ip_addr):
1503 def ip_range(ip_addr):
1505 from kallithea.model.db import UserIpMap
1504 from kallithea.model.db import UserIpMap
1506 s, e = UserIpMap._get_ip_range(ip_addr)
1505 s, e = UserIpMap._get_ip_range(ip_addr)
1507 return '%s - %s' % (s, e)
1506 return '%s - %s' % (s, e)
1508
1507
1509
1508
1510 def form(url, method="post", **attrs):
1509 def form(url, method="post", **attrs):
1511 """Like webhelpers.html.tags.form but automatically using secure_form with
1510 """Like webhelpers.html.tags.form but automatically using secure_form with
1512 authentication_token for POST. authentication_token is thus never leaked
1511 authentication_token for POST. authentication_token is thus never leaked
1513 in the URL."""
1512 in the URL."""
1514 if method.lower() == 'get':
1513 if method.lower() == 'get':
1515 return insecure_form(url, method=method, **attrs)
1514 return insecure_form(url, method=method, **attrs)
1516 # webhelpers will turn everything but GET into POST
1515 # webhelpers will turn everything but GET into POST
1517 return secure_form(url, method=method, **attrs)
1516 return secure_form(url, method=method, **attrs)
@@ -1,806 +1,806 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.model.scm
15 kallithea.model.scm
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Scm model for Kallithea
18 Scm model 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 9, 2010
22 :created_on: Apr 9, 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 sys
29 import sys
30 import posixpath
30 import posixpath
31 import re
31 import re
32 import time
32 import time
33 import traceback
33 import traceback
34 import logging
34 import logging
35 import cStringIO
35 import cStringIO
36 import pkg_resources
36 import pkg_resources
37
37
38 from sqlalchemy import func
38 from sqlalchemy import func
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 import kallithea
41 import kallithea
42 from kallithea.lib.vcs import get_backend
42 from kallithea.lib.vcs import get_backend
43 from kallithea.lib.vcs.exceptions import RepositoryError
43 from kallithea.lib.vcs.exceptions import RepositoryError
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.nodes import FileNode
45 from kallithea.lib.vcs.nodes import FileNode
46 from kallithea.lib.vcs.backends.base import EmptyChangeset
46 from kallithea.lib.vcs.backends.base import EmptyChangeset
47
47
48 from kallithea import BACKENDS
48 from kallithea import BACKENDS
49 from kallithea.lib import helpers as h
49 from kallithea.lib import helpers as h
50 from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
50 from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
51 _set_extras
51 _set_extras
52 from kallithea.lib.auth import HasRepoPermissionAny, HasRepoGroupPermissionAny, \
52 from kallithea.lib.auth import HasRepoPermissionAny, HasRepoGroupPermissionAny, \
53 HasUserGroupPermissionAny, HasPermissionAny, HasPermissionAll
53 HasUserGroupPermissionAny, HasPermissionAny, HasPermissionAny
54 from kallithea.lib.utils import get_filesystem_repos, make_ui, \
54 from kallithea.lib.utils import get_filesystem_repos, make_ui, \
55 action_logger
55 action_logger
56 from kallithea.model import BaseModel
56 from kallithea.model import BaseModel
57 from kallithea.model.db import Repository, Ui, CacheInvalidation, \
57 from kallithea.model.db import Repository, Ui, CacheInvalidation, \
58 UserFollowing, UserLog, User, RepoGroup, PullRequest
58 UserFollowing, UserLog, User, RepoGroup, PullRequest
59 from kallithea.lib.hooks import log_push_action
59 from kallithea.lib.hooks import log_push_action
60 from kallithea.lib.exceptions import NonRelativePathError, IMCCommitError
60 from kallithea.lib.exceptions import NonRelativePathError, IMCCommitError
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class UserTemp(object):
65 class UserTemp(object):
66 def __init__(self, user_id):
66 def __init__(self, user_id):
67 self.user_id = user_id
67 self.user_id = user_id
68
68
69 def __repr__(self):
69 def __repr__(self):
70 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
70 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
71
71
72
72
73 class RepoTemp(object):
73 class RepoTemp(object):
74 def __init__(self, repo_id):
74 def __init__(self, repo_id):
75 self.repo_id = repo_id
75 self.repo_id = repo_id
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
78 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
79
79
80
80
81 class _PermCheckIterator(object):
81 class _PermCheckIterator(object):
82 def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None):
82 def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None):
83 """
83 """
84 Creates iterator from given list of objects, additionally
84 Creates iterator from given list of objects, additionally
85 checking permission for them from perm_set var
85 checking permission for them from perm_set var
86
86
87 :param obj_list: list of db objects
87 :param obj_list: list of db objects
88 :param obj_attr: attribute of object to pass into perm_checker
88 :param obj_attr: attribute of object to pass into perm_checker
89 :param perm_set: list of permissions to check
89 :param perm_set: list of permissions to check
90 :param perm_checker: callable to check permissions against
90 :param perm_checker: callable to check permissions against
91 """
91 """
92 self.obj_list = obj_list
92 self.obj_list = obj_list
93 self.obj_attr = obj_attr
93 self.obj_attr = obj_attr
94 self.perm_set = perm_set
94 self.perm_set = perm_set
95 self.perm_checker = perm_checker
95 self.perm_checker = perm_checker
96 self.extra_kwargs = extra_kwargs or {}
96 self.extra_kwargs = extra_kwargs or {}
97
97
98 def __len__(self):
98 def __len__(self):
99 return len(self.obj_list)
99 return len(self.obj_list)
100
100
101 def __repr__(self):
101 def __repr__(self):
102 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
102 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
103
103
104 def __iter__(self):
104 def __iter__(self):
105 for db_obj in self.obj_list:
105 for db_obj in self.obj_list:
106 # check permission at this level
106 # check permission at this level
107 name = getattr(db_obj, self.obj_attr, None)
107 name = getattr(db_obj, self.obj_attr, None)
108 if not self.perm_checker(*self.perm_set)(
108 if not self.perm_checker(*self.perm_set)(
109 name, self.__class__.__name__, **self.extra_kwargs):
109 name, self.__class__.__name__, **self.extra_kwargs):
110 continue
110 continue
111
111
112 yield db_obj
112 yield db_obj
113
113
114
114
115 class RepoList(_PermCheckIterator):
115 class RepoList(_PermCheckIterator):
116
116
117 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
117 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
118 if not perm_set:
118 if not perm_set:
119 perm_set = ['repository.read', 'repository.write', 'repository.admin']
119 perm_set = ['repository.read', 'repository.write', 'repository.admin']
120
120
121 super(RepoList, self).__init__(obj_list=db_repo_list,
121 super(RepoList, self).__init__(obj_list=db_repo_list,
122 obj_attr='repo_name', perm_set=perm_set,
122 obj_attr='repo_name', perm_set=perm_set,
123 perm_checker=HasRepoPermissionAny,
123 perm_checker=HasRepoPermissionAny,
124 extra_kwargs=extra_kwargs)
124 extra_kwargs=extra_kwargs)
125
125
126
126
127 class RepoGroupList(_PermCheckIterator):
127 class RepoGroupList(_PermCheckIterator):
128
128
129 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
129 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
130 if not perm_set:
130 if not perm_set:
131 perm_set = ['group.read', 'group.write', 'group.admin']
131 perm_set = ['group.read', 'group.write', 'group.admin']
132
132
133 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
133 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
134 obj_attr='group_name', perm_set=perm_set,
134 obj_attr='group_name', perm_set=perm_set,
135 perm_checker=HasRepoGroupPermissionAny,
135 perm_checker=HasRepoGroupPermissionAny,
136 extra_kwargs=extra_kwargs)
136 extra_kwargs=extra_kwargs)
137
137
138
138
139 class UserGroupList(_PermCheckIterator):
139 class UserGroupList(_PermCheckIterator):
140
140
141 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
141 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
142 if not perm_set:
142 if not perm_set:
143 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
143 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
144
144
145 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
145 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
146 obj_attr='users_group_name', perm_set=perm_set,
146 obj_attr='users_group_name', perm_set=perm_set,
147 perm_checker=HasUserGroupPermissionAny,
147 perm_checker=HasUserGroupPermissionAny,
148 extra_kwargs=extra_kwargs)
148 extra_kwargs=extra_kwargs)
149
149
150
150
151 class ScmModel(BaseModel):
151 class ScmModel(BaseModel):
152 """
152 """
153 Generic Scm Model
153 Generic Scm Model
154 """
154 """
155
155
156 def __get_repo(self, instance):
156 def __get_repo(self, instance):
157 cls = Repository
157 cls = Repository
158 if isinstance(instance, cls):
158 if isinstance(instance, cls):
159 return instance
159 return instance
160 elif isinstance(instance, int) or safe_str(instance).isdigit():
160 elif isinstance(instance, int) or safe_str(instance).isdigit():
161 return cls.get(instance)
161 return cls.get(instance)
162 elif isinstance(instance, basestring):
162 elif isinstance(instance, basestring):
163 return cls.get_by_repo_name(instance)
163 return cls.get_by_repo_name(instance)
164 elif instance is not None:
164 elif instance is not None:
165 raise Exception('given object must be int, basestr or Instance'
165 raise Exception('given object must be int, basestr or Instance'
166 ' of %s got %s' % (type(cls), type(instance)))
166 ' of %s got %s' % (type(cls), type(instance)))
167
167
168 @LazyProperty
168 @LazyProperty
169 def repos_path(self):
169 def repos_path(self):
170 """
170 """
171 Gets the repositories root path from database
171 Gets the repositories root path from database
172 """
172 """
173
173
174 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
174 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
175
175
176 return q.ui_value
176 return q.ui_value
177
177
178 def repo_scan(self, repos_path=None):
178 def repo_scan(self, repos_path=None):
179 """
179 """
180 Listing of repositories in given path. This path should not be a
180 Listing of repositories in given path. This path should not be a
181 repository itself. Return a dictionary of repository objects
181 repository itself. Return a dictionary of repository objects
182
182
183 :param repos_path: path to directory containing repositories
183 :param repos_path: path to directory containing repositories
184 """
184 """
185
185
186 if repos_path is None:
186 if repos_path is None:
187 repos_path = self.repos_path
187 repos_path = self.repos_path
188
188
189 log.info('scanning for repositories in %s', repos_path)
189 log.info('scanning for repositories in %s', repos_path)
190
190
191 baseui = make_ui('db')
191 baseui = make_ui('db')
192 repos = {}
192 repos = {}
193
193
194 for name, path in get_filesystem_repos(repos_path):
194 for name, path in get_filesystem_repos(repos_path):
195 # name need to be decomposed and put back together using the /
195 # name need to be decomposed and put back together using the /
196 # since this is internal storage separator for kallithea
196 # since this is internal storage separator for kallithea
197 name = Repository.normalize_repo_name(name)
197 name = Repository.normalize_repo_name(name)
198
198
199 try:
199 try:
200 if name in repos:
200 if name in repos:
201 raise RepositoryError('Duplicate repository name %s '
201 raise RepositoryError('Duplicate repository name %s '
202 'found in %s' % (name, path))
202 'found in %s' % (name, path))
203 else:
203 else:
204
204
205 klass = get_backend(path[0])
205 klass = get_backend(path[0])
206
206
207 if path[0] == 'hg' and path[0] in BACKENDS.keys():
207 if path[0] == 'hg' and path[0] in BACKENDS.keys():
208 repos[name] = klass(safe_str(path[1]), baseui=baseui)
208 repos[name] = klass(safe_str(path[1]), baseui=baseui)
209
209
210 if path[0] == 'git' and path[0] in BACKENDS.keys():
210 if path[0] == 'git' and path[0] in BACKENDS.keys():
211 repos[name] = klass(path[1])
211 repos[name] = klass(path[1])
212 except OSError:
212 except OSError:
213 continue
213 continue
214 log.debug('found %s paths with repositories', len(repos))
214 log.debug('found %s paths with repositories', len(repos))
215 return repos
215 return repos
216
216
217 def get_repos(self, repos):
217 def get_repos(self, repos):
218 """Return the repos the user has access to"""
218 """Return the repos the user has access to"""
219 return RepoList(repos)
219 return RepoList(repos)
220
220
221 def get_repo_groups(self, groups=None):
221 def get_repo_groups(self, groups=None):
222 """Return the repo groups the user has access to
222 """Return the repo groups the user has access to
223 If no groups are specified, use top level groups.
223 If no groups are specified, use top level groups.
224 """
224 """
225 if groups is None:
225 if groups is None:
226 groups = RepoGroup.query() \
226 groups = RepoGroup.query() \
227 .filter(RepoGroup.group_parent_id == None).all()
227 .filter(RepoGroup.group_parent_id == None).all()
228 return RepoGroupList(groups)
228 return RepoGroupList(groups)
229
229
230 def mark_for_invalidation(self, repo_name):
230 def mark_for_invalidation(self, repo_name):
231 """
231 """
232 Mark caches of this repo invalid in the database.
232 Mark caches of this repo invalid in the database.
233
233
234 :param repo_name: the repo for which caches should be marked invalid
234 :param repo_name: the repo for which caches should be marked invalid
235 """
235 """
236 CacheInvalidation.set_invalidate(repo_name)
236 CacheInvalidation.set_invalidate(repo_name)
237 repo = Repository.get_by_repo_name(repo_name)
237 repo = Repository.get_by_repo_name(repo_name)
238 if repo is not None:
238 if repo is not None:
239 repo.update_changeset_cache()
239 repo.update_changeset_cache()
240
240
241 def toggle_following_repo(self, follow_repo_id, user_id):
241 def toggle_following_repo(self, follow_repo_id, user_id):
242
242
243 f = self.sa.query(UserFollowing) \
243 f = self.sa.query(UserFollowing) \
244 .filter(UserFollowing.follows_repo_id == follow_repo_id) \
244 .filter(UserFollowing.follows_repo_id == follow_repo_id) \
245 .filter(UserFollowing.user_id == user_id).scalar()
245 .filter(UserFollowing.user_id == user_id).scalar()
246
246
247 if f is not None:
247 if f is not None:
248 try:
248 try:
249 self.sa.delete(f)
249 self.sa.delete(f)
250 action_logger(UserTemp(user_id),
250 action_logger(UserTemp(user_id),
251 'stopped_following_repo',
251 'stopped_following_repo',
252 RepoTemp(follow_repo_id))
252 RepoTemp(follow_repo_id))
253 return
253 return
254 except Exception:
254 except Exception:
255 log.error(traceback.format_exc())
255 log.error(traceback.format_exc())
256 raise
256 raise
257
257
258 try:
258 try:
259 f = UserFollowing()
259 f = UserFollowing()
260 f.user_id = user_id
260 f.user_id = user_id
261 f.follows_repo_id = follow_repo_id
261 f.follows_repo_id = follow_repo_id
262 self.sa.add(f)
262 self.sa.add(f)
263
263
264 action_logger(UserTemp(user_id),
264 action_logger(UserTemp(user_id),
265 'started_following_repo',
265 'started_following_repo',
266 RepoTemp(follow_repo_id))
266 RepoTemp(follow_repo_id))
267 except Exception:
267 except Exception:
268 log.error(traceback.format_exc())
268 log.error(traceback.format_exc())
269 raise
269 raise
270
270
271 def toggle_following_user(self, follow_user_id, user_id):
271 def toggle_following_user(self, follow_user_id, user_id):
272 f = self.sa.query(UserFollowing) \
272 f = self.sa.query(UserFollowing) \
273 .filter(UserFollowing.follows_user_id == follow_user_id) \
273 .filter(UserFollowing.follows_user_id == follow_user_id) \
274 .filter(UserFollowing.user_id == user_id).scalar()
274 .filter(UserFollowing.user_id == user_id).scalar()
275
275
276 if f is not None:
276 if f is not None:
277 try:
277 try:
278 self.sa.delete(f)
278 self.sa.delete(f)
279 return
279 return
280 except Exception:
280 except Exception:
281 log.error(traceback.format_exc())
281 log.error(traceback.format_exc())
282 raise
282 raise
283
283
284 try:
284 try:
285 f = UserFollowing()
285 f = UserFollowing()
286 f.user_id = user_id
286 f.user_id = user_id
287 f.follows_user_id = follow_user_id
287 f.follows_user_id = follow_user_id
288 self.sa.add(f)
288 self.sa.add(f)
289 except Exception:
289 except Exception:
290 log.error(traceback.format_exc())
290 log.error(traceback.format_exc())
291 raise
291 raise
292
292
293 def is_following_repo(self, repo_name, user_id, cache=False):
293 def is_following_repo(self, repo_name, user_id, cache=False):
294 r = self.sa.query(Repository) \
294 r = self.sa.query(Repository) \
295 .filter(Repository.repo_name == repo_name).scalar()
295 .filter(Repository.repo_name == repo_name).scalar()
296
296
297 f = self.sa.query(UserFollowing) \
297 f = self.sa.query(UserFollowing) \
298 .filter(UserFollowing.follows_repository == r) \
298 .filter(UserFollowing.follows_repository == r) \
299 .filter(UserFollowing.user_id == user_id).scalar()
299 .filter(UserFollowing.user_id == user_id).scalar()
300
300
301 return f is not None
301 return f is not None
302
302
303 def is_following_user(self, username, user_id, cache=False):
303 def is_following_user(self, username, user_id, cache=False):
304 u = User.get_by_username(username)
304 u = User.get_by_username(username)
305
305
306 f = self.sa.query(UserFollowing) \
306 f = self.sa.query(UserFollowing) \
307 .filter(UserFollowing.follows_user == u) \
307 .filter(UserFollowing.follows_user == u) \
308 .filter(UserFollowing.user_id == user_id).scalar()
308 .filter(UserFollowing.user_id == user_id).scalar()
309
309
310 return f is not None
310 return f is not None
311
311
312 def get_followers(self, repo):
312 def get_followers(self, repo):
313 repo = self._get_repo(repo)
313 repo = self._get_repo(repo)
314
314
315 return self.sa.query(UserFollowing) \
315 return self.sa.query(UserFollowing) \
316 .filter(UserFollowing.follows_repository == repo).count()
316 .filter(UserFollowing.follows_repository == repo).count()
317
317
318 def get_forks(self, repo):
318 def get_forks(self, repo):
319 repo = self._get_repo(repo)
319 repo = self._get_repo(repo)
320 return self.sa.query(Repository) \
320 return self.sa.query(Repository) \
321 .filter(Repository.fork == repo).count()
321 .filter(Repository.fork == repo).count()
322
322
323 def get_pull_requests(self, repo):
323 def get_pull_requests(self, repo):
324 repo = self._get_repo(repo)
324 repo = self._get_repo(repo)
325 return self.sa.query(PullRequest) \
325 return self.sa.query(PullRequest) \
326 .filter(PullRequest.other_repo == repo) \
326 .filter(PullRequest.other_repo == repo) \
327 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
327 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
328
328
329 def mark_as_fork(self, repo, fork, user):
329 def mark_as_fork(self, repo, fork, user):
330 repo = self.__get_repo(repo)
330 repo = self.__get_repo(repo)
331 fork = self.__get_repo(fork)
331 fork = self.__get_repo(fork)
332 if fork and repo.repo_id == fork.repo_id:
332 if fork and repo.repo_id == fork.repo_id:
333 raise Exception("Cannot set repository as fork of itself")
333 raise Exception("Cannot set repository as fork of itself")
334
334
335 if fork and repo.repo_type != fork.repo_type:
335 if fork and repo.repo_type != fork.repo_type:
336 raise RepositoryError("Cannot set repository as fork of repository with other type")
336 raise RepositoryError("Cannot set repository as fork of repository with other type")
337
337
338 repo.fork = fork
338 repo.fork = fork
339 self.sa.add(repo)
339 self.sa.add(repo)
340 return repo
340 return repo
341
341
342 def _handle_rc_scm_extras(self, username, repo_name, repo_alias,
342 def _handle_rc_scm_extras(self, username, repo_name, repo_alias,
343 action=None):
343 action=None):
344 from kallithea import CONFIG
344 from kallithea import CONFIG
345 from kallithea.lib.base import _get_ip_addr
345 from kallithea.lib.base import _get_ip_addr
346 try:
346 try:
347 from pylons import request
347 from pylons import request
348 environ = request.environ
348 environ = request.environ
349 except TypeError:
349 except TypeError:
350 # we might use this outside of request context, let's fake the
350 # we might use this outside of request context, let's fake the
351 # environ data
351 # environ data
352 from webob import Request
352 from webob import Request
353 environ = Request.blank('').environ
353 environ = Request.blank('').environ
354 extras = {
354 extras = {
355 'ip': _get_ip_addr(environ),
355 'ip': _get_ip_addr(environ),
356 'username': username,
356 'username': username,
357 'action': action or 'push_local',
357 'action': action or 'push_local',
358 'repository': repo_name,
358 'repository': repo_name,
359 'scm': repo_alias,
359 'scm': repo_alias,
360 'config': CONFIG['__file__'],
360 'config': CONFIG['__file__'],
361 'server_url': get_server_url(environ),
361 'server_url': get_server_url(environ),
362 'make_lock': None,
362 'make_lock': None,
363 'locked_by': [None, None]
363 'locked_by': [None, None]
364 }
364 }
365 _set_extras(extras)
365 _set_extras(extras)
366
366
367 def _handle_push(self, repo, username, action, repo_name, revisions):
367 def _handle_push(self, repo, username, action, repo_name, revisions):
368 """
368 """
369 Triggers push action hooks
369 Triggers push action hooks
370
370
371 :param repo: SCM repo
371 :param repo: SCM repo
372 :param username: username who pushes
372 :param username: username who pushes
373 :param action: push/push_local/push_remote
373 :param action: push/push_local/push_remote
374 :param repo_name: name of repo
374 :param repo_name: name of repo
375 :param revisions: list of revisions that we pushed
375 :param revisions: list of revisions that we pushed
376 """
376 """
377 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
377 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
378 _scm_repo = repo._repo
378 _scm_repo = repo._repo
379 # trigger push hook
379 # trigger push hook
380 if repo.alias == 'hg':
380 if repo.alias == 'hg':
381 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
381 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
382 elif repo.alias == 'git':
382 elif repo.alias == 'git':
383 log_push_action(None, _scm_repo, _git_revs=revisions)
383 log_push_action(None, _scm_repo, _git_revs=revisions)
384
384
385 def _get_IMC_module(self, scm_type):
385 def _get_IMC_module(self, scm_type):
386 """
386 """
387 Returns InMemoryCommit class based on scm_type
387 Returns InMemoryCommit class based on scm_type
388
388
389 :param scm_type:
389 :param scm_type:
390 """
390 """
391 if scm_type == 'hg':
391 if scm_type == 'hg':
392 from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
392 from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
393 return MercurialInMemoryChangeset
393 return MercurialInMemoryChangeset
394
394
395 if scm_type == 'git':
395 if scm_type == 'git':
396 from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
396 from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
397 return GitInMemoryChangeset
397 return GitInMemoryChangeset
398
398
399 raise Exception('Invalid scm_type, must be one of hg,git got %s'
399 raise Exception('Invalid scm_type, must be one of hg,git got %s'
400 % (scm_type,))
400 % (scm_type,))
401
401
402 def pull_changes(self, repo, username):
402 def pull_changes(self, repo, username):
403 """
403 """
404 Pull from "clone URL".
404 Pull from "clone URL".
405 """
405 """
406 dbrepo = self.__get_repo(repo)
406 dbrepo = self.__get_repo(repo)
407 clone_uri = dbrepo.clone_uri
407 clone_uri = dbrepo.clone_uri
408 if not clone_uri:
408 if not clone_uri:
409 raise Exception("This repository doesn't have a clone uri")
409 raise Exception("This repository doesn't have a clone uri")
410
410
411 repo = dbrepo.scm_instance
411 repo = dbrepo.scm_instance
412 repo_name = dbrepo.repo_name
412 repo_name = dbrepo.repo_name
413 try:
413 try:
414 if repo.alias == 'git':
414 if repo.alias == 'git':
415 repo.fetch(clone_uri)
415 repo.fetch(clone_uri)
416 # git doesn't really have something like post-fetch action
416 # git doesn't really have something like post-fetch action
417 # we fake that now. #TODO: extract fetched revisions somehow
417 # we fake that now. #TODO: extract fetched revisions somehow
418 # here
418 # here
419 self._handle_push(repo,
419 self._handle_push(repo,
420 username=username,
420 username=username,
421 action='push_remote',
421 action='push_remote',
422 repo_name=repo_name,
422 repo_name=repo_name,
423 revisions=[])
423 revisions=[])
424 else:
424 else:
425 self._handle_rc_scm_extras(username, dbrepo.repo_name,
425 self._handle_rc_scm_extras(username, dbrepo.repo_name,
426 repo.alias, action='push_remote')
426 repo.alias, action='push_remote')
427 repo.pull(clone_uri)
427 repo.pull(clone_uri)
428
428
429 self.mark_for_invalidation(repo_name)
429 self.mark_for_invalidation(repo_name)
430 except Exception:
430 except Exception:
431 log.error(traceback.format_exc())
431 log.error(traceback.format_exc())
432 raise
432 raise
433
433
434 def commit_change(self, repo, repo_name, cs, user, author, message,
434 def commit_change(self, repo, repo_name, cs, user, author, message,
435 content, f_path):
435 content, f_path):
436 """
436 """
437 Commit a change to a single file
437 Commit a change to a single file
438
438
439 :param repo: a db_repo.scm_instance
439 :param repo: a db_repo.scm_instance
440 """
440 """
441 user = self._get_user(user)
441 user = self._get_user(user)
442 IMC = self._get_IMC_module(repo.alias)
442 IMC = self._get_IMC_module(repo.alias)
443
443
444 # decoding here will force that we have proper encoded values
444 # decoding here will force that we have proper encoded values
445 # in any other case this will throw exceptions and deny commit
445 # in any other case this will throw exceptions and deny commit
446 content = safe_str(content)
446 content = safe_str(content)
447 path = safe_str(f_path)
447 path = safe_str(f_path)
448 # message and author needs to be unicode
448 # message and author needs to be unicode
449 # proper backend should then translate that into required type
449 # proper backend should then translate that into required type
450 message = safe_unicode(message)
450 message = safe_unicode(message)
451 author = safe_unicode(author)
451 author = safe_unicode(author)
452 imc = IMC(repo)
452 imc = IMC(repo)
453 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
453 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
454 try:
454 try:
455 tip = imc.commit(message=message, author=author,
455 tip = imc.commit(message=message, author=author,
456 parents=[cs], branch=cs.branch)
456 parents=[cs], branch=cs.branch)
457 except Exception as e:
457 except Exception as e:
458 log.error(traceback.format_exc())
458 log.error(traceback.format_exc())
459 raise IMCCommitError(str(e))
459 raise IMCCommitError(str(e))
460 finally:
460 finally:
461 # always clear caches, if commit fails we want fresh object also
461 # always clear caches, if commit fails we want fresh object also
462 self.mark_for_invalidation(repo_name)
462 self.mark_for_invalidation(repo_name)
463 self._handle_push(repo,
463 self._handle_push(repo,
464 username=user.username,
464 username=user.username,
465 action='push_local',
465 action='push_local',
466 repo_name=repo_name,
466 repo_name=repo_name,
467 revisions=[tip.raw_id])
467 revisions=[tip.raw_id])
468 return tip
468 return tip
469
469
470 def _sanitize_path(self, f_path):
470 def _sanitize_path(self, f_path):
471 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
471 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
472 raise NonRelativePathError('%s is not an relative path' % f_path)
472 raise NonRelativePathError('%s is not an relative path' % f_path)
473 if f_path:
473 if f_path:
474 f_path = posixpath.normpath(f_path)
474 f_path = posixpath.normpath(f_path)
475 return f_path
475 return f_path
476
476
477 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
477 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
478 """
478 """
479 Recursively walk root dir and return a set of all paths found.
479 Recursively walk root dir and return a set of all paths found.
480
480
481 :param repo_name: name of repository
481 :param repo_name: name of repository
482 :param revision: revision for which to list nodes
482 :param revision: revision for which to list nodes
483 :param root_path: root path to list
483 :param root_path: root path to list
484 :param flat: return as a list, if False returns a dict with description
484 :param flat: return as a list, if False returns a dict with description
485
485
486 """
486 """
487 _files = list()
487 _files = list()
488 _dirs = list()
488 _dirs = list()
489 try:
489 try:
490 _repo = self.__get_repo(repo_name)
490 _repo = self.__get_repo(repo_name)
491 changeset = _repo.scm_instance.get_changeset(revision)
491 changeset = _repo.scm_instance.get_changeset(revision)
492 root_path = root_path.lstrip('/')
492 root_path = root_path.lstrip('/')
493 for topnode, dirs, files in changeset.walk(root_path):
493 for topnode, dirs, files in changeset.walk(root_path):
494 for f in files:
494 for f in files:
495 _files.append(f.path if flat else {"name": f.path,
495 _files.append(f.path if flat else {"name": f.path,
496 "type": "file"})
496 "type": "file"})
497 for d in dirs:
497 for d in dirs:
498 _dirs.append(d.path if flat else {"name": d.path,
498 _dirs.append(d.path if flat else {"name": d.path,
499 "type": "dir"})
499 "type": "dir"})
500 except RepositoryError:
500 except RepositoryError:
501 log.debug(traceback.format_exc())
501 log.debug(traceback.format_exc())
502 raise
502 raise
503
503
504 return _dirs, _files
504 return _dirs, _files
505
505
506 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
506 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
507 author=None, trigger_push_hook=True):
507 author=None, trigger_push_hook=True):
508 """
508 """
509 Commits specified nodes to repo.
509 Commits specified nodes to repo.
510
510
511 :param user: Kallithea User object or user_id, the committer
511 :param user: Kallithea User object or user_id, the committer
512 :param repo: Kallithea Repository object
512 :param repo: Kallithea Repository object
513 :param message: commit message
513 :param message: commit message
514 :param nodes: mapping {filename:{'content':content},...}
514 :param nodes: mapping {filename:{'content':content},...}
515 :param parent_cs: parent changeset, can be empty than it's initial commit
515 :param parent_cs: parent changeset, can be empty than it's initial commit
516 :param author: author of commit, cna be different that committer only for git
516 :param author: author of commit, cna be different that committer only for git
517 :param trigger_push_hook: trigger push hooks
517 :param trigger_push_hook: trigger push hooks
518
518
519 :returns: new committed changeset
519 :returns: new committed changeset
520 """
520 """
521
521
522 user = self._get_user(user)
522 user = self._get_user(user)
523 scm_instance = repo.scm_instance_no_cache()
523 scm_instance = repo.scm_instance_no_cache()
524
524
525 processed_nodes = []
525 processed_nodes = []
526 for f_path in nodes:
526 for f_path in nodes:
527 content = nodes[f_path]['content']
527 content = nodes[f_path]['content']
528 f_path = self._sanitize_path(f_path)
528 f_path = self._sanitize_path(f_path)
529 f_path = safe_str(f_path)
529 f_path = safe_str(f_path)
530 # decoding here will force that we have proper encoded values
530 # decoding here will force that we have proper encoded values
531 # in any other case this will throw exceptions and deny commit
531 # in any other case this will throw exceptions and deny commit
532 if isinstance(content, (basestring,)):
532 if isinstance(content, (basestring,)):
533 content = safe_str(content)
533 content = safe_str(content)
534 elif isinstance(content, (file, cStringIO.OutputType,)):
534 elif isinstance(content, (file, cStringIO.OutputType,)):
535 content = content.read()
535 content = content.read()
536 else:
536 else:
537 raise Exception('Content is of unrecognized type %s' % (
537 raise Exception('Content is of unrecognized type %s' % (
538 type(content)
538 type(content)
539 ))
539 ))
540 processed_nodes.append((f_path, content))
540 processed_nodes.append((f_path, content))
541
541
542 message = safe_unicode(message)
542 message = safe_unicode(message)
543 committer = user.full_contact
543 committer = user.full_contact
544 author = safe_unicode(author) if author else committer
544 author = safe_unicode(author) if author else committer
545
545
546 IMC = self._get_IMC_module(scm_instance.alias)
546 IMC = self._get_IMC_module(scm_instance.alias)
547 imc = IMC(scm_instance)
547 imc = IMC(scm_instance)
548
548
549 if not parent_cs:
549 if not parent_cs:
550 parent_cs = EmptyChangeset(alias=scm_instance.alias)
550 parent_cs = EmptyChangeset(alias=scm_instance.alias)
551
551
552 if isinstance(parent_cs, EmptyChangeset):
552 if isinstance(parent_cs, EmptyChangeset):
553 # EmptyChangeset means we we're editing empty repository
553 # EmptyChangeset means we we're editing empty repository
554 parents = None
554 parents = None
555 else:
555 else:
556 parents = [parent_cs]
556 parents = [parent_cs]
557 # add multiple nodes
557 # add multiple nodes
558 for path, content in processed_nodes:
558 for path, content in processed_nodes:
559 imc.add(FileNode(path, content=content))
559 imc.add(FileNode(path, content=content))
560
560
561 tip = imc.commit(message=message,
561 tip = imc.commit(message=message,
562 author=author,
562 author=author,
563 parents=parents,
563 parents=parents,
564 branch=parent_cs.branch)
564 branch=parent_cs.branch)
565
565
566 self.mark_for_invalidation(repo.repo_name)
566 self.mark_for_invalidation(repo.repo_name)
567 if trigger_push_hook:
567 if trigger_push_hook:
568 self._handle_push(scm_instance,
568 self._handle_push(scm_instance,
569 username=user.username,
569 username=user.username,
570 action='push_local',
570 action='push_local',
571 repo_name=repo.repo_name,
571 repo_name=repo.repo_name,
572 revisions=[tip.raw_id])
572 revisions=[tip.raw_id])
573 return tip
573 return tip
574
574
575 def update_nodes(self, user, repo, message, nodes, parent_cs=None,
575 def update_nodes(self, user, repo, message, nodes, parent_cs=None,
576 author=None, trigger_push_hook=True):
576 author=None, trigger_push_hook=True):
577 """
577 """
578 Commits specified nodes to repo. Again.
578 Commits specified nodes to repo. Again.
579 """
579 """
580 user = self._get_user(user)
580 user = self._get_user(user)
581 scm_instance = repo.scm_instance_no_cache()
581 scm_instance = repo.scm_instance_no_cache()
582
582
583 message = safe_unicode(message)
583 message = safe_unicode(message)
584 committer = user.full_contact
584 committer = user.full_contact
585 author = safe_unicode(author) if author else committer
585 author = safe_unicode(author) if author else committer
586
586
587 imc_class = self._get_IMC_module(scm_instance.alias)
587 imc_class = self._get_IMC_module(scm_instance.alias)
588 imc = imc_class(scm_instance)
588 imc = imc_class(scm_instance)
589
589
590 if not parent_cs:
590 if not parent_cs:
591 parent_cs = EmptyChangeset(alias=scm_instance.alias)
591 parent_cs = EmptyChangeset(alias=scm_instance.alias)
592
592
593 if isinstance(parent_cs, EmptyChangeset):
593 if isinstance(parent_cs, EmptyChangeset):
594 # EmptyChangeset means we we're editing empty repository
594 # EmptyChangeset means we we're editing empty repository
595 parents = None
595 parents = None
596 else:
596 else:
597 parents = [parent_cs]
597 parents = [parent_cs]
598
598
599 # add multiple nodes
599 # add multiple nodes
600 for _filename, data in nodes.items():
600 for _filename, data in nodes.items():
601 # new filename, can be renamed from the old one
601 # new filename, can be renamed from the old one
602 filename = self._sanitize_path(data['filename'])
602 filename = self._sanitize_path(data['filename'])
603 old_filename = self._sanitize_path(_filename)
603 old_filename = self._sanitize_path(_filename)
604 content = data['content']
604 content = data['content']
605
605
606 filenode = FileNode(old_filename, content=content)
606 filenode = FileNode(old_filename, content=content)
607 op = data['op']
607 op = data['op']
608 if op == 'add':
608 if op == 'add':
609 imc.add(filenode)
609 imc.add(filenode)
610 elif op == 'del':
610 elif op == 'del':
611 imc.remove(filenode)
611 imc.remove(filenode)
612 elif op == 'mod':
612 elif op == 'mod':
613 if filename != old_filename:
613 if filename != old_filename:
614 #TODO: handle renames, needs vcs lib changes
614 #TODO: handle renames, needs vcs lib changes
615 imc.remove(filenode)
615 imc.remove(filenode)
616 imc.add(FileNode(filename, content=content))
616 imc.add(FileNode(filename, content=content))
617 else:
617 else:
618 imc.change(filenode)
618 imc.change(filenode)
619
619
620 # commit changes
620 # commit changes
621 tip = imc.commit(message=message,
621 tip = imc.commit(message=message,
622 author=author,
622 author=author,
623 parents=parents,
623 parents=parents,
624 branch=parent_cs.branch)
624 branch=parent_cs.branch)
625
625
626 self.mark_for_invalidation(repo.repo_name)
626 self.mark_for_invalidation(repo.repo_name)
627 if trigger_push_hook:
627 if trigger_push_hook:
628 self._handle_push(scm_instance,
628 self._handle_push(scm_instance,
629 username=user.username,
629 username=user.username,
630 action='push_local',
630 action='push_local',
631 repo_name=repo.repo_name,
631 repo_name=repo.repo_name,
632 revisions=[tip.raw_id])
632 revisions=[tip.raw_id])
633
633
634 def delete_nodes(self, user, repo, message, nodes, parent_cs=None,
634 def delete_nodes(self, user, repo, message, nodes, parent_cs=None,
635 author=None, trigger_push_hook=True):
635 author=None, trigger_push_hook=True):
636 """
636 """
637 Deletes specified nodes from repo.
637 Deletes specified nodes from repo.
638
638
639 :param user: Kallithea User object or user_id, the committer
639 :param user: Kallithea User object or user_id, the committer
640 :param repo: Kallithea Repository object
640 :param repo: Kallithea Repository object
641 :param message: commit message
641 :param message: commit message
642 :param nodes: mapping {filename:{'content':content},...}
642 :param nodes: mapping {filename:{'content':content},...}
643 :param parent_cs: parent changeset, can be empty than it's initial commit
643 :param parent_cs: parent changeset, can be empty than it's initial commit
644 :param author: author of commit, cna be different that committer only for git
644 :param author: author of commit, cna be different that committer only for git
645 :param trigger_push_hook: trigger push hooks
645 :param trigger_push_hook: trigger push hooks
646
646
647 :returns: new committed changeset after deletion
647 :returns: new committed changeset after deletion
648 """
648 """
649
649
650 user = self._get_user(user)
650 user = self._get_user(user)
651 scm_instance = repo.scm_instance_no_cache()
651 scm_instance = repo.scm_instance_no_cache()
652
652
653 processed_nodes = []
653 processed_nodes = []
654 for f_path in nodes:
654 for f_path in nodes:
655 f_path = self._sanitize_path(f_path)
655 f_path = self._sanitize_path(f_path)
656 # content can be empty but for compatibility it allows same dicts
656 # content can be empty but for compatibility it allows same dicts
657 # structure as add_nodes
657 # structure as add_nodes
658 content = nodes[f_path].get('content')
658 content = nodes[f_path].get('content')
659 processed_nodes.append((f_path, content))
659 processed_nodes.append((f_path, content))
660
660
661 message = safe_unicode(message)
661 message = safe_unicode(message)
662 committer = user.full_contact
662 committer = user.full_contact
663 author = safe_unicode(author) if author else committer
663 author = safe_unicode(author) if author else committer
664
664
665 IMC = self._get_IMC_module(scm_instance.alias)
665 IMC = self._get_IMC_module(scm_instance.alias)
666 imc = IMC(scm_instance)
666 imc = IMC(scm_instance)
667
667
668 if not parent_cs:
668 if not parent_cs:
669 parent_cs = EmptyChangeset(alias=scm_instance.alias)
669 parent_cs = EmptyChangeset(alias=scm_instance.alias)
670
670
671 if isinstance(parent_cs, EmptyChangeset):
671 if isinstance(parent_cs, EmptyChangeset):
672 # EmptyChangeset means we we're editing empty repository
672 # EmptyChangeset means we we're editing empty repository
673 parents = None
673 parents = None
674 else:
674 else:
675 parents = [parent_cs]
675 parents = [parent_cs]
676 # add multiple nodes
676 # add multiple nodes
677 for path, content in processed_nodes:
677 for path, content in processed_nodes:
678 imc.remove(FileNode(path, content=content))
678 imc.remove(FileNode(path, content=content))
679
679
680 tip = imc.commit(message=message,
680 tip = imc.commit(message=message,
681 author=author,
681 author=author,
682 parents=parents,
682 parents=parents,
683 branch=parent_cs.branch)
683 branch=parent_cs.branch)
684
684
685 self.mark_for_invalidation(repo.repo_name)
685 self.mark_for_invalidation(repo.repo_name)
686 if trigger_push_hook:
686 if trigger_push_hook:
687 self._handle_push(scm_instance,
687 self._handle_push(scm_instance,
688 username=user.username,
688 username=user.username,
689 action='push_local',
689 action='push_local',
690 repo_name=repo.repo_name,
690 repo_name=repo.repo_name,
691 revisions=[tip.raw_id])
691 revisions=[tip.raw_id])
692 return tip
692 return tip
693
693
694 def get_unread_journal(self):
694 def get_unread_journal(self):
695 return self.sa.query(UserLog).count()
695 return self.sa.query(UserLog).count()
696
696
697 def get_repo_landing_revs(self, repo=None):
697 def get_repo_landing_revs(self, repo=None):
698 """
698 """
699 Generates select option with tags branches and bookmarks (for hg only)
699 Generates select option with tags branches and bookmarks (for hg only)
700 grouped by type
700 grouped by type
701
701
702 :param repo:
702 :param repo:
703 """
703 """
704
704
705 hist_l = []
705 hist_l = []
706 choices = []
706 choices = []
707 repo = self.__get_repo(repo)
707 repo = self.__get_repo(repo)
708 hist_l.append(['rev:tip', _('latest tip')])
708 hist_l.append(['rev:tip', _('latest tip')])
709 choices.append('rev:tip')
709 choices.append('rev:tip')
710 if repo is None:
710 if repo is None:
711 return choices, hist_l
711 return choices, hist_l
712
712
713 repo = repo.scm_instance
713 repo = repo.scm_instance
714
714
715 branches_group = ([(u'branch:%s' % k, k) for k, v in
715 branches_group = ([(u'branch:%s' % k, k) for k, v in
716 repo.branches.iteritems()], _("Branches"))
716 repo.branches.iteritems()], _("Branches"))
717 hist_l.append(branches_group)
717 hist_l.append(branches_group)
718 choices.extend([x[0] for x in branches_group[0]])
718 choices.extend([x[0] for x in branches_group[0]])
719
719
720 if repo.alias == 'hg':
720 if repo.alias == 'hg':
721 bookmarks_group = ([(u'book:%s' % k, k) for k, v in
721 bookmarks_group = ([(u'book:%s' % k, k) for k, v in
722 repo.bookmarks.iteritems()], _("Bookmarks"))
722 repo.bookmarks.iteritems()], _("Bookmarks"))
723 hist_l.append(bookmarks_group)
723 hist_l.append(bookmarks_group)
724 choices.extend([x[0] for x in bookmarks_group[0]])
724 choices.extend([x[0] for x in bookmarks_group[0]])
725
725
726 tags_group = ([(u'tag:%s' % k, k) for k, v in
726 tags_group = ([(u'tag:%s' % k, k) for k, v in
727 repo.tags.iteritems()], _("Tags"))
727 repo.tags.iteritems()], _("Tags"))
728 hist_l.append(tags_group)
728 hist_l.append(tags_group)
729 choices.extend([x[0] for x in tags_group[0]])
729 choices.extend([x[0] for x in tags_group[0]])
730
730
731 return choices, hist_l
731 return choices, hist_l
732
732
733 def install_git_hooks(self, repo, force_create=False):
733 def install_git_hooks(self, repo, force_create=False):
734 """
734 """
735 Creates a kallithea hook inside a git repository
735 Creates a kallithea hook inside a git repository
736
736
737 :param repo: Instance of VCS repo
737 :param repo: Instance of VCS repo
738 :param force_create: Create even if same name hook exists
738 :param force_create: Create even if same name hook exists
739 """
739 """
740
740
741 loc = os.path.join(repo.path, 'hooks')
741 loc = os.path.join(repo.path, 'hooks')
742 if not repo.bare:
742 if not repo.bare:
743 loc = os.path.join(repo.path, '.git', 'hooks')
743 loc = os.path.join(repo.path, '.git', 'hooks')
744 if not os.path.isdir(loc):
744 if not os.path.isdir(loc):
745 os.makedirs(loc)
745 os.makedirs(loc)
746
746
747 tmpl_post = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
747 tmpl_post = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
748 tmpl_post += pkg_resources.resource_string(
748 tmpl_post += pkg_resources.resource_string(
749 'kallithea', os.path.join('config', 'post_receive_tmpl.py')
749 'kallithea', os.path.join('config', 'post_receive_tmpl.py')
750 )
750 )
751 tmpl_pre = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
751 tmpl_pre = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
752 tmpl_pre += pkg_resources.resource_string(
752 tmpl_pre += pkg_resources.resource_string(
753 'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
753 'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
754 )
754 )
755
755
756 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
756 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
757 _hook_file = os.path.join(loc, '%s-receive' % h_type)
757 _hook_file = os.path.join(loc, '%s-receive' % h_type)
758 has_hook = False
758 has_hook = False
759 log.debug('Installing git hook in repo %s', repo)
759 log.debug('Installing git hook in repo %s', repo)
760 if os.path.exists(_hook_file):
760 if os.path.exists(_hook_file):
761 # let's take a look at this hook, maybe it's kallithea ?
761 # let's take a look at this hook, maybe it's kallithea ?
762 log.debug('hook exists, checking if it is from kallithea')
762 log.debug('hook exists, checking if it is from kallithea')
763 with open(_hook_file, 'rb') as f:
763 with open(_hook_file, 'rb') as f:
764 data = f.read()
764 data = f.read()
765 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
765 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
766 % 'KALLITHEA_HOOK_VER').search(data)
766 % 'KALLITHEA_HOOK_VER').search(data)
767 if matches:
767 if matches:
768 try:
768 try:
769 ver = matches.groups()[0]
769 ver = matches.groups()[0]
770 log.debug('got %s it is kallithea', ver)
770 log.debug('got %s it is kallithea', ver)
771 has_hook = True
771 has_hook = True
772 except Exception:
772 except Exception:
773 log.error(traceback.format_exc())
773 log.error(traceback.format_exc())
774 else:
774 else:
775 # there is no hook in this dir, so we want to create one
775 # there is no hook in this dir, so we want to create one
776 has_hook = True
776 has_hook = True
777
777
778 if has_hook or force_create:
778 if has_hook or force_create:
779 log.debug('writing %s hook file !', h_type)
779 log.debug('writing %s hook file !', h_type)
780 try:
780 try:
781 with open(_hook_file, 'wb') as f:
781 with open(_hook_file, 'wb') as f:
782 tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
782 tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
783 f.write(tmpl)
783 f.write(tmpl)
784 os.chmod(_hook_file, 0755)
784 os.chmod(_hook_file, 0755)
785 except IOError as e:
785 except IOError as e:
786 log.error('error writing %s: %s', _hook_file, e)
786 log.error('error writing %s: %s', _hook_file, e)
787 else:
787 else:
788 log.debug('skipping writing hook file')
788 log.debug('skipping writing hook file')
789
789
790 def AvailableRepoGroupChoices(top_perms, repo_group_perms, extras=()):
790 def AvailableRepoGroupChoices(top_perms, repo_group_perms, extras=()):
791 """Return group_id,string tuples with choices for all the repo groups where
791 """Return group_id,string tuples with choices for all the repo groups where
792 the user has the necessary permissions.
792 the user has the necessary permissions.
793
793
794 Top level is -1.
794 Top level is -1.
795 """
795 """
796 groups = RepoGroup.query().all()
796 groups = RepoGroup.query().all()
797 if HasPermissionAll('hg.admin')('available repo groups'):
797 if HasPermissionAny('hg.admin')('available repo groups'):
798 groups.append(None)
798 groups.append(None)
799 else:
799 else:
800 groups = list(RepoGroupList(groups, perm_set=repo_group_perms))
800 groups = list(RepoGroupList(groups, perm_set=repo_group_perms))
801 if top_perms and HasPermissionAny(*top_perms)('available repo groups'):
801 if top_perms and HasPermissionAny(*top_perms)('available repo groups'):
802 groups.append(None)
802 groups.append(None)
803 for extra in extras:
803 for extra in extras:
804 if not any(rg == extra for rg in groups):
804 if not any(rg == extra for rg in groups):
805 groups.append(extra)
805 groups.append(extra)
806 return RepoGroup.groups_choices(groups=groups)
806 return RepoGroup.groups_choices(groups=groups)
@@ -1,679 +1,679 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="root.html"/>
2 <%inherit file="root.html"/>
3
3
4 <!-- CONTENT -->
4 <!-- CONTENT -->
5 <div id="content">
5 <div id="content">
6 ${self.flash_msg()}
6 ${self.flash_msg()}
7 <div id="main">
7 <div id="main">
8 ${next.main()}
8 ${next.main()}
9 </div>
9 </div>
10 </div>
10 </div>
11 <!-- END CONTENT -->
11 <!-- END CONTENT -->
12
12
13 <!-- FOOTER -->
13 <!-- FOOTER -->
14 <div id="footer">
14 <div id="footer">
15 <div id="footer-inner" class="title">
15 <div id="footer-inner" class="title">
16 <div>
16 <div>
17 <p class="footer-link">
17 <p class="footer-link">
18 ${_('Server instance: %s') % c.instance_id if c.instance_id else ''}
18 ${_('Server instance: %s') % c.instance_id if c.instance_id else ''}
19 </p>
19 </p>
20 <p class="footer-link-right">
20 <p class="footer-link-right">
21 This site is powered by
21 This site is powered by
22 %if c.visual.show_version:
22 %if c.visual.show_version:
23 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a> ${c.kallithea_version},
23 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a> ${c.kallithea_version},
24 %else:
24 %else:
25 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
25 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
26 %endif
26 %endif
27 which is
27 which is
28 <a href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2016 by various authors &amp; licensed under GPLv3</a>.
28 <a href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2016 by various authors &amp; licensed under GPLv3</a>.
29 %if c.issues_url:
29 %if c.issues_url:
30 &ndash; <a href="${c.issues_url}" target="_blank">${_('Support')}</a>
30 &ndash; <a href="${c.issues_url}" target="_blank">${_('Support')}</a>
31 %endif
31 %endif
32 </p>
32 </p>
33 </div>
33 </div>
34 </div>
34 </div>
35 </div>
35 </div>
36
36
37 <!-- END FOOTER -->
37 <!-- END FOOTER -->
38
38
39 ### MAKO DEFS ###
39 ### MAKO DEFS ###
40
40
41 <%block name="branding_title">
41 <%block name="branding_title">
42 %if c.site_name:
42 %if c.site_name:
43 &middot; ${c.site_name}
43 &middot; ${c.site_name}
44 %endif
44 %endif
45 </%block>
45 </%block>
46
46
47 <%def name="flash_msg()">
47 <%def name="flash_msg()">
48 <%include file="/base/flash_msg.html"/>
48 <%include file="/base/flash_msg.html"/>
49 </%def>
49 </%def>
50
50
51 <%def name="breadcrumbs()">
51 <%def name="breadcrumbs()">
52 <div class="breadcrumbs">
52 <div class="breadcrumbs">
53 ${self.breadcrumbs_links()}
53 ${self.breadcrumbs_links()}
54 </div>
54 </div>
55 </%def>
55 </%def>
56
56
57 <%def name="admin_menu()">
57 <%def name="admin_menu()">
58 <ul class="dropdown-menu" role="menu">
58 <ul class="dropdown-menu" role="menu">
59 <li><a href="${h.url('admin_home')}"><i class="icon-book"></i> ${_('Admin Journal')}</a></li>
59 <li><a href="${h.url('admin_home')}"><i class="icon-book"></i> ${_('Admin Journal')}</a></li>
60 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
60 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
61 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
61 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
62 <li><a href="${h.url('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
62 <li><a href="${h.url('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
63 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
63 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
64 <li><a href="${h.url('admin_permissions')}"><i class="icon-block"></i> ${_('Default Permissions')}</a></li>
64 <li><a href="${h.url('admin_permissions')}"><i class="icon-block"></i> ${_('Default Permissions')}</a></li>
65 <li><a href="${h.url('auth_home')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
65 <li><a href="${h.url('auth_home')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
66 <li><a href="${h.url('defaults')}"><i class="icon-wrench"></i> ${_('Repository Defaults')}</a></li>
66 <li><a href="${h.url('defaults')}"><i class="icon-wrench"></i> ${_('Repository Defaults')}</a></li>
67 <li class="last"><a href="${h.url('admin_settings')}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
67 <li class="last"><a href="${h.url('admin_settings')}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
68 </ul>
68 </ul>
69
69
70 </%def>
70 </%def>
71
71
72
72
73 ## admin menu used for people that have some admin resources
73 ## admin menu used for people that have some admin resources
74 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
74 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
75 <ul class="dropdown-menu" role="menu">
75 <ul class="dropdown-menu" role="menu">
76 %if repositories:
76 %if repositories:
77 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
77 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
78 %endif
78 %endif
79 %if repository_groups:
79 %if repository_groups:
80 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
80 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
81 %endif
81 %endif
82 %if user_groups:
82 %if user_groups:
83 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
83 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
84 %endif
84 %endif
85 </ul>
85 </ul>
86 </%def>
86 </%def>
87
87
88 <%def name="repotag(repo)">
88 <%def name="repotag(repo)">
89 %if h.is_hg(repo):
89 %if h.is_hg(repo):
90 <span class="repotag" title="${_('Mercurial repository')}">hg</span>
90 <span class="repotag" title="${_('Mercurial repository')}">hg</span>
91 %endif
91 %endif
92 %if h.is_git(repo):
92 %if h.is_git(repo):
93 <span class="repotag" title="${_('Git repository')}">git</span>
93 <span class="repotag" title="${_('Git repository')}">git</span>
94 %endif
94 %endif
95 </%def>
95 </%def>
96
96
97 <%def name="repo_context_bar(current=None, rev=None)">
97 <%def name="repo_context_bar(current=None, rev=None)">
98 <% rev = None if rev == 'tip' else rev %>
98 <% rev = None if rev == 'tip' else rev %>
99 <%
99 <%
100 def is_current(selected):
100 def is_current(selected):
101 if selected == current:
101 if selected == current:
102 return h.literal('class="current"')
102 return h.literal('class="current"')
103 %>
103 %>
104
104
105 <!--- CONTEXT BAR -->
105 <!--- CONTEXT BAR -->
106 <div id="context-bar" class="box">
106 <div id="context-bar" class="box">
107 <h2>
107 <h2>
108 ${repotag(c.db_repo)}
108 ${repotag(c.db_repo)}
109
109
110 ## public/private
110 ## public/private
111 %if c.db_repo.private:
111 %if c.db_repo.private:
112 <i class="icon-keyhole-circled"></i>
112 <i class="icon-keyhole-circled"></i>
113 %else:
113 %else:
114 <i class="icon-globe"></i>
114 <i class="icon-globe"></i>
115 %endif
115 %endif
116 ${h.repo_link(c.db_repo.groups_and_repo)}
116 ${h.repo_link(c.db_repo.groups_and_repo)}
117
117
118 %if current == 'createfork':
118 %if current == 'createfork':
119 - ${_('Create Fork')}
119 - ${_('Create Fork')}
120 %endif
120 %endif
121 </h2>
121 </h2>
122 <!--
122 <!--
123 <div id="breadcrumbs">
123 <div id="breadcrumbs">
124 ${h.link_to(_('Repositories'),h.url('home'))}
124 ${h.link_to(_('Repositories'),h.url('home'))}
125 &raquo;
125 &raquo;
126 ${h.repo_link(c.db_repo.groups_and_repo)}
126 ${h.repo_link(c.db_repo.groups_and_repo)}
127 </div>
127 </div>
128 -->
128 -->
129 <ul id="context-pages" class="horizontal-list">
129 <ul id="context-pages" class="horizontal-list">
130 <li ${is_current('summary')} data-context="summary"><a href="${h.url('summary_home', repo_name=c.repo_name)}"><i class="icon-doc-text"></i> ${_('Summary')}</a></li>
130 <li ${is_current('summary')} data-context="summary"><a href="${h.url('summary_home', repo_name=c.repo_name)}"><i class="icon-doc-text"></i> ${_('Summary')}</a></li>
131 %if rev:
131 %if rev:
132 <li ${is_current('changelog')} data-context="changelog"><a href="${h.url('changelog_file_home', repo_name=c.repo_name, revision=rev, f_path='')}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
132 <li ${is_current('changelog')} data-context="changelog"><a href="${h.url('changelog_file_home', repo_name=c.repo_name, revision=rev, f_path='')}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
133 %else:
133 %else:
134 <li ${is_current('changelog')} data-context="changelog"><a href="${h.url('changelog_home', repo_name=c.repo_name)}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
134 <li ${is_current('changelog')} data-context="changelog"><a href="${h.url('changelog_home', repo_name=c.repo_name)}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
135 %endif
135 %endif
136 <li ${is_current('files')} data-context="files"><a href="${h.url('files_home', repo_name=c.repo_name, revision=rev or 'tip')}"><i class="icon-doc-inv"></i> ${_('Files')}</a></li>
136 <li ${is_current('files')} data-context="files"><a href="${h.url('files_home', repo_name=c.repo_name, revision=rev or 'tip')}"><i class="icon-doc-inv"></i> ${_('Files')}</a></li>
137 <li ${is_current('switch-to')} data-context="switch-to">
137 <li ${is_current('switch-to')} data-context="switch-to">
138 <input id="branch_switcher" name="branch_switcher" type="hidden">
138 <input id="branch_switcher" name="branch_switcher" type="hidden">
139 </li>
139 </li>
140 <li ${is_current('options')} data-context="options">
140 <li ${is_current('options')} data-context="options">
141 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
141 %if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
142 <a href="${h.url('edit_repo',repo_name=c.repo_name)}" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
142 <a href="${h.url('edit_repo',repo_name=c.repo_name)}" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
143 %else:
143 %else:
144 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
144 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
145 %endif
145 %endif
146 <ul class="dropdown-menu" role="menu">
146 <ul class="dropdown-menu" role="menu">
147 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
147 %if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
148 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
148 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
149 %endif
149 %endif
150 %if c.db_repo.fork:
150 %if c.db_repo.fork:
151 <li><a href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1], other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}">
151 <li><a href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1], other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}">
152 <i class="icon-git-compare"></i> ${_('Compare Fork')}</a></li>
152 <i class="icon-git-compare"></i> ${_('Compare Fork')}</a></li>
153 %endif
153 %endif
154 <li><a href="${h.url('compare_home',repo_name=c.repo_name)}"><i class="icon-git-compare"></i> ${_('Compare')}</a></li>
154 <li><a href="${h.url('compare_home',repo_name=c.repo_name)}"><i class="icon-git-compare"></i> ${_('Compare')}</a></li>
155
155
156 <li><a href="${h.url('search_repo',repo_name=c.repo_name)}"><i class="icon-search"></i> ${_('Search')}</a></li>
156 <li><a href="${h.url('search_repo',repo_name=c.repo_name)}"><i class="icon-search"></i> ${_('Search')}</a></li>
157
157
158 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.db_repo.enable_locking:
158 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.db_repo.enable_locking:
159 %if c.db_repo.locked[0]:
159 %if c.db_repo.locked[0]:
160 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock"></i> ${_('Unlock')}</a></li>
160 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock"></i> ${_('Unlock')}</a></li>
161 %else:
161 %else:
162 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock-open-alt"></i> ${_('Lock')}</li>
162 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock-open-alt"></i> ${_('Lock')}</li>
163 %endif
163 %endif
164 %endif
164 %endif
165 ## TODO: this check feels wrong, it would be better to have a check for permissions
165 ## TODO: this check feels wrong, it would be better to have a check for permissions
166 ## also it feels like a job for the controller
166 ## also it feels like a job for the controller
167 %if c.authuser.username != 'default':
167 %if c.authuser.username != 'default':
168 <li>
168 <li>
169 <a class="${'following' if c.repository_following else 'follow'}" onclick="toggleFollowingRepo(this, ${c.db_repo.repo_id});">
169 <a class="${'following' if c.repository_following else 'follow'}" onclick="toggleFollowingRepo(this, ${c.db_repo.repo_id});">
170 <span class="show-follow"><i class="icon-heart-empty"></i> ${_('Follow')}</span>
170 <span class="show-follow"><i class="icon-heart-empty"></i> ${_('Follow')}</span>
171 <span class="show-following"><i class="icon-heart"></i> ${_('Unfollow')}</span>
171 <span class="show-following"><i class="icon-heart"></i> ${_('Unfollow')}</span>
172 </a>
172 </a>
173 </li>
173 </li>
174 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Fork')}</a></li>
174 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Fork')}</a></li>
175 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Create Pull Request')}</a></li>
175 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Create Pull Request')}</a></li>
176 %endif
176 %endif
177 </ul>
177 </ul>
178 </li>
178 </li>
179 <li ${is_current('showpullrequest')} data-context="showpullrequest">
179 <li ${is_current('showpullrequest')} data-context="showpullrequest">
180 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}"> <i class="icon-git-pull-request"></i> ${_('Pull Requests')}
180 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}"> <i class="icon-git-pull-request"></i> ${_('Pull Requests')}
181 %if c.repository_pull_requests:
181 %if c.repository_pull_requests:
182 <span class="badge">${c.repository_pull_requests}</span>
182 <span class="badge">${c.repository_pull_requests}</span>
183 %endif
183 %endif
184 </a>
184 </a>
185 </li>
185 </li>
186 </ul>
186 </ul>
187 </div>
187 </div>
188 <script type="text/javascript">
188 <script type="text/javascript">
189 $(document).ready(function() {
189 $(document).ready(function() {
190 var bcache = {};
190 var bcache = {};
191
191
192 $("#branch_switcher").select2({
192 $("#branch_switcher").select2({
193 placeholder: '<i class="icon-exchange"></i> ${_('Switch To')} <span class="caret"></span>',
193 placeholder: '<i class="icon-exchange"></i> ${_('Switch To')} <span class="caret"></span>',
194 dropdownAutoWidth: true,
194 dropdownAutoWidth: true,
195 sortResults: prefixFirstSort,
195 sortResults: prefixFirstSort,
196 formatResult: function(obj) {
196 formatResult: function(obj) {
197 return obj.text;
197 return obj.text;
198 },
198 },
199 formatSelection: function(obj) {
199 formatSelection: function(obj) {
200 return obj.text;
200 return obj.text;
201 },
201 },
202 formatNoMatches: function(term) {
202 formatNoMatches: function(term) {
203 return "${_('No matches found')}";
203 return "${_('No matches found')}";
204 },
204 },
205 escapeMarkup: function(m) {
205 escapeMarkup: function(m) {
206 // don't escape our custom placeholder
206 // don't escape our custom placeholder
207 if (m.substr(0, 29) == '<i class="icon-exchange"></i>') {
207 if (m.substr(0, 29) == '<i class="icon-exchange"></i>') {
208 return m;
208 return m;
209 }
209 }
210
210
211 return Select2.util.escapeMarkup(m);
211 return Select2.util.escapeMarkup(m);
212 },
212 },
213 containerCssClass: "repo-switcher",
213 containerCssClass: "repo-switcher",
214 dropdownCssClass: "repo-switcher-dropdown",
214 dropdownCssClass: "repo-switcher-dropdown",
215 query: function(query) {
215 query: function(query) {
216 var key = 'cache';
216 var key = 'cache';
217 var cached = bcache[key];
217 var cached = bcache[key];
218 if (cached) {
218 if (cached) {
219 var data = {
219 var data = {
220 results: []
220 results: []
221 };
221 };
222 // filter results
222 // filter results
223 $.each(cached.results, function() {
223 $.each(cached.results, function() {
224 var section = this.text;
224 var section = this.text;
225 var children = [];
225 var children = [];
226 $.each(this.children, function() {
226 $.each(this.children, function() {
227 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
227 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
228 children.push({
228 children.push({
229 'id': this.id,
229 'id': this.id,
230 'text': this.text,
230 'text': this.text,
231 'type': this.type,
231 'type': this.type,
232 'obj': this.obj
232 'obj': this.obj
233 });
233 });
234 }
234 }
235 });
235 });
236 if (children.length !== 0) {
236 if (children.length !== 0) {
237 data.results.push({
237 data.results.push({
238 'text': section,
238 'text': section,
239 'children': children
239 'children': children
240 });
240 });
241 }
241 }
242
242
243 });
243 });
244 query.callback(data);
244 query.callback(data);
245 } else {
245 } else {
246 $.ajax({
246 $.ajax({
247 url: pyroutes.url('repo_refs_data', {
247 url: pyroutes.url('repo_refs_data', {
248 'repo_name': '${c.repo_name}'
248 'repo_name': '${c.repo_name}'
249 }),
249 }),
250 data: {},
250 data: {},
251 dataType: 'json',
251 dataType: 'json',
252 type: 'GET',
252 type: 'GET',
253 success: function(data) {
253 success: function(data) {
254 bcache[key] = data;
254 bcache[key] = data;
255 query.callback(data);
255 query.callback(data);
256 }
256 }
257 });
257 });
258 }
258 }
259 }
259 }
260 });
260 });
261
261
262 $("#branch_switcher").on('select2-selecting', function(e) {
262 $("#branch_switcher").on('select2-selecting', function(e) {
263 e.preventDefault();
263 e.preventDefault();
264 var context = $('#context-bar .current').data('context');
264 var context = $('#context-bar .current').data('context');
265 if (context == 'files') {
265 if (context == 'files') {
266 window.location = pyroutes.url('files_home', {
266 window.location = pyroutes.url('files_home', {
267 'repo_name': REPO_NAME,
267 'repo_name': REPO_NAME,
268 'revision': e.choice.id,
268 'revision': e.choice.id,
269 'f_path': '',
269 'f_path': '',
270 'at': e.choice.text
270 'at': e.choice.text
271 });
271 });
272 } else if (context == 'changelog') {
272 } else if (context == 'changelog') {
273 if (e.choice.type == 'tag' || e.choice.type == 'book') {
273 if (e.choice.type == 'tag' || e.choice.type == 'book') {
274 $("#branch_filter").append($('<'+'option/>').val(e.choice.text));
274 $("#branch_filter").append($('<'+'option/>').val(e.choice.text));
275 }
275 }
276 $("#branch_filter").val(e.choice.text).change();
276 $("#branch_filter").val(e.choice.text).change();
277 } else {
277 } else {
278 window.location = pyroutes.url('changelog_home', {
278 window.location = pyroutes.url('changelog_home', {
279 'repo_name': '${c.repo_name}',
279 'repo_name': '${c.repo_name}',
280 'branch': e.choice.text
280 'branch': e.choice.text
281 });
281 });
282 }
282 }
283 });
283 });
284 });
284 });
285 </script>
285 </script>
286 <!--- END CONTEXT BAR -->
286 <!--- END CONTEXT BAR -->
287 </%def>
287 </%def>
288
288
289 <%def name="menu(current=None)">
289 <%def name="menu(current=None)">
290 <%
290 <%
291 def is_current(selected):
291 def is_current(selected):
292 if selected == current:
292 if selected == current:
293 return h.literal('class="current"')
293 return h.literal('class="current"')
294 %>
294 %>
295
295
296 <ul id="quick" class="horizontal-list">
296 <ul id="quick" class="horizontal-list">
297 <!-- repo switcher -->
297 <!-- repo switcher -->
298 <li ${is_current('repositories')}>
298 <li ${is_current('repositories')}>
299 <input id="repo_switcher" name="repo_switcher" type="hidden">
299 <input id="repo_switcher" name="repo_switcher" type="hidden">
300 </li>
300 </li>
301
301
302 ##ROOT MENU
302 ##ROOT MENU
303 %if c.authuser.username != 'default':
303 %if c.authuser.username != 'default':
304 <li ${is_current('journal')}>
304 <li ${is_current('journal')}>
305 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
305 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
306 <i class="icon-book"></i> ${_('Journal')}
306 <i class="icon-book"></i> ${_('Journal')}
307 </a>
307 </a>
308 </li>
308 </li>
309 %else:
309 %else:
310 <li ${is_current('journal')}>
310 <li ${is_current('journal')}>
311 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
311 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
312 <i class="icon-book"></i> ${_('Public journal')}
312 <i class="icon-book"></i> ${_('Public journal')}
313 </a>
313 </a>
314 </li>
314 </li>
315 %endif
315 %endif
316 <li ${is_current('gists')} class="dropdown">
316 <li ${is_current('gists')} class="dropdown">
317 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Show public gists')}" href="${h.url('gists')}">
317 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Show public gists')}" href="${h.url('gists')}">
318 <i class="icon-clippy"></i> ${_('Gists')} <span class="caret"></span>
318 <i class="icon-clippy"></i> ${_('Gists')} <span class="caret"></span>
319 </a>
319 </a>
320 <ul class="dropdown-menu" role="menu">
320 <ul class="dropdown-menu" role="menu">
321 <li><a href="${h.url('new_gist', public=1)}"><i class="icon-paste"></i> ${_('Create New Gist')}</a></li>
321 <li><a href="${h.url('new_gist', public=1)}"><i class="icon-paste"></i> ${_('Create New Gist')}</a></li>
322 <li><a href="${h.url('gists')}"><i class="icon-globe"></i> ${_('All Public Gists')}</a></li>
322 <li><a href="${h.url('gists')}"><i class="icon-globe"></i> ${_('All Public Gists')}</a></li>
323 %if c.authuser.username != 'default':
323 %if c.authuser.username != 'default':
324 <li><a href="${h.url('gists', public=1)}"><i class="icon-user"></i> ${_('My Public Gists')}</a></li>
324 <li><a href="${h.url('gists', public=1)}"><i class="icon-user"></i> ${_('My Public Gists')}</a></li>
325 <li><a href="${h.url('gists', private=1)}"><i class="icon-keyhole-circled"></i> ${_('My Private Gists')}</a></li>
325 <li><a href="${h.url('gists', private=1)}"><i class="icon-keyhole-circled"></i> ${_('My Private Gists')}</a></li>
326 %endif
326 %endif
327 </ul>
327 </ul>
328 </li>
328 </li>
329 <li ${is_current('search')}>
329 <li ${is_current('search')}>
330 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
330 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
331 <i class="icon-search"></i> ${_('Search')}
331 <i class="icon-search"></i> ${_('Search')}
332 </a>
332 </a>
333 </li>
333 </li>
334 % if h.HasPermissionAll('hg.admin')('access admin main page'):
334 % if h.HasPermissionAny('hg.admin')('access admin main page'):
335 <li ${is_current('admin')} class="dropdown">
335 <li ${is_current('admin')} class="dropdown">
336 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}" href="${h.url('admin_home')}">
336 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}" href="${h.url('admin_home')}">
337 <i class="icon-gear"></i> ${_('Admin')} <span class="caret"></span>
337 <i class="icon-gear"></i> ${_('Admin')} <span class="caret"></span>
338 </a>
338 </a>
339 ${admin_menu()}
339 ${admin_menu()}
340 </li>
340 </li>
341 % elif c.authuser.repositories_admin or c.authuser.repository_groups_admin or c.authuser.user_groups_admin:
341 % elif c.authuser.repositories_admin or c.authuser.repository_groups_admin or c.authuser.user_groups_admin:
342 <li ${is_current('admin')} class="dropdown">
342 <li ${is_current('admin')} class="dropdown">
343 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}">
343 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}">
344 <i class="icon-gear"></i> ${_('Admin')}
344 <i class="icon-gear"></i> ${_('Admin')}
345 </a>
345 </a>
346 ${admin_menu_simple(c.authuser.repositories_admin,
346 ${admin_menu_simple(c.authuser.repositories_admin,
347 c.authuser.repository_groups_admin,
347 c.authuser.repository_groups_admin,
348 c.authuser.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
348 c.authuser.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
349 </li>
349 </li>
350 % endif
350 % endif
351
351
352 <li ${is_current('my_pullrequests')}>
352 <li ${is_current('my_pullrequests')}>
353 <a class="menu_link" title="${_('My Pull Requests')}" href="${h.url('my_pullrequests')}">
353 <a class="menu_link" title="${_('My Pull Requests')}" href="${h.url('my_pullrequests')}">
354 <i class="icon-git-pull-request"></i> ${_('My Pull Requests')}
354 <i class="icon-git-pull-request"></i> ${_('My Pull Requests')}
355 %if c.my_pr_count != 0:
355 %if c.my_pr_count != 0:
356 <span class="badge">${c.my_pr_count}</span>
356 <span class="badge">${c.my_pr_count}</span>
357 %endif
357 %endif
358 </a>
358 </a>
359 </li>
359 </li>
360
360
361 ## USER MENU
361 ## USER MENU
362 <li class="dropdown">
362 <li class="dropdown">
363 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" id="quick_login_link"
363 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" id="quick_login_link"
364 %if c.authuser.username != 'default':
364 %if c.authuser.username != 'default':
365 href="${h.url('notifications')}"
365 href="${h.url('notifications')}"
366 %endif
366 %endif
367 >
367 >
368 ${h.gravatar_div(c.authuser.email, size=20, div_class="icon", div_style="display:inline")}
368 ${h.gravatar_div(c.authuser.email, size=20, div_class="icon", div_style="display:inline")}
369 %if c.authuser.username != 'default':
369 %if c.authuser.username != 'default':
370 <span class="menu_link_user">${c.authuser.username}</span>
370 <span class="menu_link_user">${c.authuser.username}</span>
371 %if c.unread_notifications != 0:
371 %if c.unread_notifications != 0:
372 <span class="badge">${c.unread_notifications}</span>
372 <span class="badge">${c.unread_notifications}</span>
373 %endif
373 %endif
374 %else:
374 %else:
375 <span>${_('Not Logged In')}</span>
375 <span>${_('Not Logged In')}</span>
376 %endif
376 %endif
377 </a>
377 </a>
378
378
379 <div class="user-menu">
379 <div class="user-menu">
380 <div id="quick_login">
380 <div id="quick_login">
381 %if c.authuser.username == 'default' or c.authuser.user_id is None:
381 %if c.authuser.username == 'default' or c.authuser.user_id is None:
382 <h4>${_('Login to Your Account')}</h4>
382 <h4>${_('Login to Your Account')}</h4>
383 ${h.form(h.url('login_home', came_from=request.path_qs))}
383 ${h.form(h.url('login_home', came_from=request.path_qs))}
384 <div class="form">
384 <div class="form">
385 <div class="fields">
385 <div class="fields">
386 <div class="field">
386 <div class="field">
387 <div class="label">
387 <div class="label">
388 <label for="username">${_('Username')}:</label>
388 <label for="username">${_('Username')}:</label>
389 </div>
389 </div>
390 <div class="input">
390 <div class="input">
391 ${h.text('username',class_='focus')}
391 ${h.text('username',class_='focus')}
392 </div>
392 </div>
393
393
394 </div>
394 </div>
395 <div class="field">
395 <div class="field">
396 <div class="label">
396 <div class="label">
397 <label for="password">${_('Password')}:</label>
397 <label for="password">${_('Password')}:</label>
398 </div>
398 </div>
399 <div class="input">
399 <div class="input">
400 ${h.password('password',class_='focus')}
400 ${h.password('password',class_='focus')}
401 </div>
401 </div>
402
402
403 </div>
403 </div>
404 <div class="buttons">
404 <div class="buttons">
405 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
405 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
406 <div class="register">
406 <div class="register">
407 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
407 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
408 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
408 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
409 %endif
409 %endif
410 </div>
410 </div>
411 <div class="submit">
411 <div class="submit">
412 ${h.submit('sign_in',_('Log In'),class_="btn btn-mini")}
412 ${h.submit('sign_in',_('Log In'),class_="btn btn-mini")}
413 </div>
413 </div>
414 </div>
414 </div>
415 </div>
415 </div>
416 </div>
416 </div>
417 ${h.end_form()}
417 ${h.end_form()}
418 %else:
418 %else:
419 <div class="links_left">
419 <div class="links_left">
420 ${h.gravatar_div(c.authuser.email, size=48, div_class="big_gravatar")}
420 ${h.gravatar_div(c.authuser.email, size=48, div_class="big_gravatar")}
421 <div class="full_name">${c.authuser.full_name_or_username}</div>
421 <div class="full_name">${c.authuser.full_name_or_username}</div>
422 <div class="email">${c.authuser.email}</div>
422 <div class="email">${c.authuser.email}</div>
423 </div>
423 </div>
424 <div class="links_right">
424 <div class="links_right">
425 <ol class="links">
425 <ol class="links">
426 <li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
426 <li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
427 <li>${h.link_to(_('My Account'),h.url('my_account'))}</li>
427 <li>${h.link_to(_('My Account'),h.url('my_account'))}</li>
428 %if not c.authuser.is_external_auth:
428 %if not c.authuser.is_external_auth:
429 ## Cannot log out if using external (container) authentication.
429 ## Cannot log out if using external (container) authentication.
430 <li class="logout">${h.link_to(_('Log Out'), h.url('logout_home'))}</li>
430 <li class="logout">${h.link_to(_('Log Out'), h.url('logout_home'))}</li>
431 %endif
431 %endif
432 </ol>
432 </ol>
433 </div>
433 </div>
434 %endif
434 %endif
435 </div>
435 </div>
436 </div>
436 </div>
437 </li>
437 </li>
438
438
439 <script type="text/javascript">
439 <script type="text/javascript">
440 $(document).ready(function(){
440 $(document).ready(function(){
441 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
441 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
442 var cache = {}
442 var cache = {}
443 /*format the look of items in the list*/
443 /*format the look of items in the list*/
444 var format = function(state){
444 var format = function(state){
445 if (!state.id){
445 if (!state.id){
446 return state.text; // optgroup
446 return state.text; // optgroup
447 }
447 }
448 var obj_dict = state.obj;
448 var obj_dict = state.obj;
449 var tmpl = '';
449 var tmpl = '';
450
450
451 if(obj_dict && state.type == 'repo'){
451 if(obj_dict && state.type == 'repo'){
452 tmpl += '<span class="repo-icons">';
452 tmpl += '<span class="repo-icons">';
453 if(obj_dict['repo_type'] === 'hg'){
453 if(obj_dict['repo_type'] === 'hg'){
454 tmpl += '<span class="repotag">hg</span> ';
454 tmpl += '<span class="repotag">hg</span> ';
455 }
455 }
456 else if(obj_dict['repo_type'] === 'git'){
456 else if(obj_dict['repo_type'] === 'git'){
457 tmpl += '<span class="repotag">git</span> ';
457 tmpl += '<span class="repotag">git</span> ';
458 }
458 }
459 if(obj_dict['private']){
459 if(obj_dict['private']){
460 tmpl += '<i class="icon-keyhole-circled"></i> ';
460 tmpl += '<i class="icon-keyhole-circled"></i> ';
461 }
461 }
462 else if(visual_show_public_icon){
462 else if(visual_show_public_icon){
463 tmpl += '<i class="icon-globe"></i> ';
463 tmpl += '<i class="icon-globe"></i> ';
464 }
464 }
465 tmpl += '</span>';
465 tmpl += '</span>';
466 }
466 }
467 if(obj_dict && state.type == 'group'){
467 if(obj_dict && state.type == 'group'){
468 tmpl += '<i class="icon-folder"></i> ';
468 tmpl += '<i class="icon-folder"></i> ';
469 }
469 }
470 tmpl += state.text;
470 tmpl += state.text;
471 return tmpl;
471 return tmpl;
472 }
472 }
473
473
474 $("#repo_switcher").select2({
474 $("#repo_switcher").select2({
475 placeholder: '<i class="icon-database"></i> ${_('Repositories')} <span class="caret"></span>',
475 placeholder: '<i class="icon-database"></i> ${_('Repositories')} <span class="caret"></span>',
476 dropdownAutoWidth: true,
476 dropdownAutoWidth: true,
477 sortResults: prefixFirstSort,
477 sortResults: prefixFirstSort,
478 formatResult: format,
478 formatResult: format,
479 formatSelection: format,
479 formatSelection: format,
480 formatNoMatches: function(term){
480 formatNoMatches: function(term){
481 return "${_('No matches found')}";
481 return "${_('No matches found')}";
482 },
482 },
483 containerCssClass: "repo-switcher",
483 containerCssClass: "repo-switcher",
484 dropdownCssClass: "repo-switcher-dropdown",
484 dropdownCssClass: "repo-switcher-dropdown",
485 escapeMarkup: function(m){
485 escapeMarkup: function(m){
486 // don't escape our custom placeholder
486 // don't escape our custom placeholder
487 if(m.substr(0,29) == '<i class="icon-database"></i>'){
487 if(m.substr(0,29) == '<i class="icon-database"></i>'){
488 return m;
488 return m;
489 }
489 }
490
490
491 return Select2.util.escapeMarkup(m);
491 return Select2.util.escapeMarkup(m);
492 },
492 },
493 query: function(query){
493 query: function(query){
494 var key = 'cache';
494 var key = 'cache';
495 var cached = cache[key] ;
495 var cached = cache[key] ;
496 if(cached) {
496 if(cached) {
497 var data = {results: []};
497 var data = {results: []};
498 //filter results
498 //filter results
499 $.each(cached.results, function(){
499 $.each(cached.results, function(){
500 var section = this.text;
500 var section = this.text;
501 var children = [];
501 var children = [];
502 $.each(this.children, function(){
502 $.each(this.children, function(){
503 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
503 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
504 children.push({'id': this.id, 'text': this.text, 'type': this.type, 'obj': this.obj});
504 children.push({'id': this.id, 'text': this.text, 'type': this.type, 'obj': this.obj});
505 }
505 }
506 });
506 });
507 if(children.length !== 0){
507 if(children.length !== 0){
508 data.results.push({'text': section, 'children': children});
508 data.results.push({'text': section, 'children': children});
509 }
509 }
510
510
511 });
511 });
512 query.callback(data);
512 query.callback(data);
513 }else{
513 }else{
514 $.ajax({
514 $.ajax({
515 url: "${h.url('repo_switcher_data')}",
515 url: "${h.url('repo_switcher_data')}",
516 data: {},
516 data: {},
517 dataType: 'json',
517 dataType: 'json',
518 type: 'GET',
518 type: 'GET',
519 success: function(data) {
519 success: function(data) {
520 cache[key] = data;
520 cache[key] = data;
521 query.callback({results: data.results});
521 query.callback({results: data.results});
522 }
522 }
523 });
523 });
524 }
524 }
525 }
525 }
526 });
526 });
527
527
528 $("#repo_switcher").on('select2-selecting', function(e){
528 $("#repo_switcher").on('select2-selecting', function(e){
529 e.preventDefault();
529 e.preventDefault();
530 window.location = pyroutes.url('summary_home', {'repo_name': e.val});
530 window.location = pyroutes.url('summary_home', {'repo_name': e.val});
531 });
531 });
532 });
532 });
533
533
534 ## Global mouse bindings ##
534 ## Global mouse bindings ##
535
535
536 // general help "?"
536 // general help "?"
537 Mousetrap.bind(['?'], function(e) {
537 Mousetrap.bind(['?'], function(e) {
538 $('#help_kb').modal({});
538 $('#help_kb').modal({});
539 });
539 });
540
540
541 // / open the quick filter
541 // / open the quick filter
542 Mousetrap.bind(['/'], function(e) {
542 Mousetrap.bind(['/'], function(e) {
543 $("#repo_switcher").select2("open");
543 $("#repo_switcher").select2("open");
544
544
545 // return false to prevent default browser behavior
545 // return false to prevent default browser behavior
546 // and stop event from bubbling
546 // and stop event from bubbling
547 return false;
547 return false;
548 });
548 });
549
549
550 // ctrl/command+b, show the the main bar
550 // ctrl/command+b, show the the main bar
551 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
551 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
552 if($('#header-inner').hasClass('hover') && $('#content').hasClass('hover')){
552 if($('#header-inner').hasClass('hover') && $('#content').hasClass('hover')){
553 $('#header-inner').removeClass('hover');
553 $('#header-inner').removeClass('hover');
554 $('#content').removeClass('hover');
554 $('#content').removeClass('hover');
555 }
555 }
556 else{
556 else{
557 $('#header-inner').addClass('hover');
557 $('#header-inner').addClass('hover');
558 $('#content').addClass('hover');
558 $('#content').addClass('hover');
559 }
559 }
560 return false;
560 return false;
561 });
561 });
562
562
563 // general nav g + action
563 // general nav g + action
564 Mousetrap.bind(['g h'], function(e) {
564 Mousetrap.bind(['g h'], function(e) {
565 window.location = pyroutes.url('home');
565 window.location = pyroutes.url('home');
566 });
566 });
567 Mousetrap.bind(['g g'], function(e) {
567 Mousetrap.bind(['g g'], function(e) {
568 window.location = pyroutes.url('gists', {'private':1});
568 window.location = pyroutes.url('gists', {'private':1});
569 });
569 });
570 Mousetrap.bind(['g G'], function(e) {
570 Mousetrap.bind(['g G'], function(e) {
571 window.location = pyroutes.url('gists', {'public':1});
571 window.location = pyroutes.url('gists', {'public':1});
572 });
572 });
573 Mousetrap.bind(['n g'], function(e) {
573 Mousetrap.bind(['n g'], function(e) {
574 window.location = pyroutes.url('new_gist');
574 window.location = pyroutes.url('new_gist');
575 });
575 });
576 Mousetrap.bind(['n r'], function(e) {
576 Mousetrap.bind(['n r'], function(e) {
577 window.location = pyroutes.url('new_repo');
577 window.location = pyroutes.url('new_repo');
578 });
578 });
579
579
580 % if hasattr(c, 'repo_name') and hasattr(c, 'db_repo'):
580 % if hasattr(c, 'repo_name') and hasattr(c, 'db_repo'):
581 // nav in repo context
581 // nav in repo context
582 Mousetrap.bind(['g s'], function(e) {
582 Mousetrap.bind(['g s'], function(e) {
583 window.location = pyroutes.url('summary_home', {'repo_name': REPO_NAME});
583 window.location = pyroutes.url('summary_home', {'repo_name': REPO_NAME});
584 });
584 });
585 Mousetrap.bind(['g c'], function(e) {
585 Mousetrap.bind(['g c'], function(e) {
586 window.location = pyroutes.url('changelog_home', {'repo_name': REPO_NAME});
586 window.location = pyroutes.url('changelog_home', {'repo_name': REPO_NAME});
587 });
587 });
588 Mousetrap.bind(['g F'], function(e) {
588 Mousetrap.bind(['g F'], function(e) {
589 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': '', 'search': '1'});
589 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': '', 'search': '1'});
590 });
590 });
591 Mousetrap.bind(['g f'], function(e) {
591 Mousetrap.bind(['g f'], function(e) {
592 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': ''});
592 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': ''});
593 });
593 });
594 Mousetrap.bind(['g o'], function(e) {
594 Mousetrap.bind(['g o'], function(e) {
595 window.location = pyroutes.url('edit_repo', {'repo_name': REPO_NAME});
595 window.location = pyroutes.url('edit_repo', {'repo_name': REPO_NAME});
596 });
596 });
597 Mousetrap.bind(['g O'], function(e) {
597 Mousetrap.bind(['g O'], function(e) {
598 window.location = pyroutes.url('edit_repo_perms', {'repo_name': REPO_NAME});
598 window.location = pyroutes.url('edit_repo_perms', {'repo_name': REPO_NAME});
599 });
599 });
600 % endif
600 % endif
601
601
602 </script>
602 </script>
603 </%def>
603 </%def>
604
604
605 %if 0:
605 %if 0:
606 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
606 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
607 <div class="modal-dialog">
607 <div class="modal-dialog">
608 <div class="modal-content">
608 <div class="modal-content">
609 <div class="modal-header">
609 <div class="modal-header">
610 <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="icon-cancel-circled"></i></button>
610 <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="icon-cancel-circled"></i></button>
611 <h4 class="modal-title">${_('Keyboard shortcuts')}</h4>
611 <h4 class="modal-title">${_('Keyboard shortcuts')}</h4>
612 </div>
612 </div>
613 <div class="modal-body">
613 <div class="modal-body">
614 <div class="row">
614 <div class="row">
615 <div class="col-md-5">
615 <div class="col-md-5">
616 <table class="keyboard-mappings">
616 <table class="keyboard-mappings">
617 <tbody>
617 <tbody>
618 <tr>
618 <tr>
619 <th></th>
619 <th></th>
620 <th>${_('Site-wide shortcuts')}</th>
620 <th>${_('Site-wide shortcuts')}</th>
621 </tr>
621 </tr>
622 <%
622 <%
623 elems = [
623 elems = [
624 ('/', 'Open quick search box'),
624 ('/', 'Open quick search box'),
625 ('ctrl/cmd+b', 'Show main settings bar'),
625 ('ctrl/cmd+b', 'Show main settings bar'),
626 ('g h', 'Goto home page'),
626 ('g h', 'Goto home page'),
627 ('g g', 'Goto my private gists page'),
627 ('g g', 'Goto my private gists page'),
628 ('g G', 'Goto my public gists page'),
628 ('g G', 'Goto my public gists page'),
629 ('n r', 'New repository page'),
629 ('n r', 'New repository page'),
630 ('n g', 'New gist page'),
630 ('n g', 'New gist page'),
631 ]
631 ]
632 %>
632 %>
633 %for key, desc in elems:
633 %for key, desc in elems:
634 <tr>
634 <tr>
635 <td class="keys">
635 <td class="keys">
636 <span class="key">${key}</span>
636 <span class="key">${key}</span>
637 </td>
637 </td>
638 <td>${desc}</td>
638 <td>${desc}</td>
639 </tr>
639 </tr>
640 %endfor
640 %endfor
641 </tbody>
641 </tbody>
642 </table>
642 </table>
643 </div>
643 </div>
644 <div class="col-md-offset-5">
644 <div class="col-md-offset-5">
645 <table class="keyboard-mappings">
645 <table class="keyboard-mappings">
646 <tbody>
646 <tbody>
647 <tr>
647 <tr>
648 <th></th>
648 <th></th>
649 <th>${_('Repositories')}</th>
649 <th>${_('Repositories')}</th>
650 </tr>
650 </tr>
651 <%
651 <%
652 elems = [
652 elems = [
653 ('g s', 'Goto summary page'),
653 ('g s', 'Goto summary page'),
654 ('g c', 'Goto changelog page'),
654 ('g c', 'Goto changelog page'),
655 ('g f', 'Goto files page'),
655 ('g f', 'Goto files page'),
656 ('g F', 'Goto files page with file search activated'),
656 ('g F', 'Goto files page with file search activated'),
657 ('g o', 'Goto repository settings'),
657 ('g o', 'Goto repository settings'),
658 ('g O', 'Goto repository permissions settings'),
658 ('g O', 'Goto repository permissions settings'),
659 ]
659 ]
660 %>
660 %>
661 %for key, desc in elems:
661 %for key, desc in elems:
662 <tr>
662 <tr>
663 <td class="keys">
663 <td class="keys">
664 <span class="key">${key}</span>
664 <span class="key">${key}</span>
665 </td>
665 </td>
666 <td>${desc}</td>
666 <td>${desc}</td>
667 </tr>
667 </tr>
668 %endfor
668 %endfor
669 </tbody>
669 </tbody>
670 </table>
670 </table>
671 </div>
671 </div>
672 </div>
672 </div>
673 </div>
673 </div>
674 <div class="modal-footer">
674 <div class="modal-footer">
675 </div>
675 </div>
676 </div><!-- /.modal-content -->
676 </div><!-- /.modal-content -->
677 </div><!-- /.modal-dialog -->
677 </div><!-- /.modal-dialog -->
678 </div><!-- /.modal -->
678 </div><!-- /.modal -->
679 %endif
679 %endif
@@ -1,451 +1,451 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%block name="title">
3 <%block name="title">
4 ${_('%s Statistics') % c.repo_name}
4 ${_('%s Statistics') % c.repo_name}
5 </%block>
5 </%block>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('Statistics')}
8 ${_('Statistics')}
9 </%def>
9 </%def>
10
10
11 <%block name="header_menu">
11 <%block name="header_menu">
12 ${self.menu('repositories')}
12 ${self.menu('repositories')}
13 </%block>
13 </%block>
14
14
15 <%block name="head_extra">
15 <%block name="head_extra">
16 <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
16 <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
17 <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
17 <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
18 </%block>
18 </%block>
19
19
20 <%def name="main()">
20 <%def name="main()">
21 ${self.repo_context_bar('summary')}
21 ${self.repo_context_bar('summary')}
22 <%
22 <%
23 summary = lambda n:{False:'summary-short'}.get(n)
23 summary = lambda n:{False:'summary-short'}.get(n)
24 %>
24 %>
25 <div class="box">
25 <div class="box">
26 <!-- box / title -->
26 <!-- box / title -->
27 <div class="title">
27 <div class="title">
28 ${self.breadcrumbs()}
28 ${self.breadcrumbs()}
29 </div>
29 </div>
30
30
31 <div class="graph">
31 <div class="graph">
32 <div style="padding:0 10px 10px 17px;">
32 <div style="padding:0 10px 10px 17px;">
33 %if c.no_data:
33 %if c.no_data:
34 ${c.no_data_msg}
34 ${c.no_data_msg}
35 %if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
35 %if h.HasPermissionAny('hg.admin')('enable stats on from summary'):
36 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name),class_="btn btn-mini")}
36 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name),class_="btn btn-mini")}
37 %endif
37 %endif
38 %else:
38 %else:
39 ${_('Stats gathered: ')} ${c.stats_percentage}%
39 ${_('Stats gathered: ')} ${c.stats_percentage}%
40 %endif
40 %endif
41 </div>
41 </div>
42 <div id="commit_history" style="width:450px;height:300px;float:left"></div>
42 <div id="commit_history" style="width:450px;height:300px;float:left"></div>
43
43
44 <div id="legend_data" style="float: left;">
44 <div id="legend_data" style="float: left;">
45 <div id="legend_container"></div>
45 <div id="legend_container"></div>
46 <div id="legend_choices">
46 <div id="legend_choices">
47 <table id="legend_choices_tables" class="noborder" style="font-size:smaller;color:#545454"></table>
47 <table id="legend_choices_tables" class="noborder" style="font-size:smaller;color:#545454"></table>
48 </div>
48 </div>
49 </div>
49 </div>
50
50
51 <div style="clear: both; height: 10px;"></div>
51 <div style="clear: both; height: 10px;"></div>
52 <div id="overview" style="width: 450px; height: 100px; float: left;"></div>
52 <div id="overview" style="width: 450px; height: 100px; float: left;"></div>
53 </div>
53 </div>
54 </div>
54 </div>
55
55
56 <script type="text/javascript">
56 <script type="text/javascript">
57 var data = ${c.trending_languages|n};
57 var data = ${c.trending_languages|n};
58 var total = 0;
58 var total = 0;
59 var no_data = true;
59 var no_data = true;
60 var tbl = document.createElement('table');
60 var tbl = document.createElement('table');
61 tbl.setAttribute('class','trending_language_tbl');
61 tbl.setAttribute('class','trending_language_tbl');
62 var cnt = 0;
62 var cnt = 0;
63 for (var i=0;i<data.length;i++){
63 for (var i=0;i<data.length;i++){
64 total+= data[i][1].count;
64 total+= data[i][1].count;
65 }
65 }
66 for (var i=0;i<data.length;i++){
66 for (var i=0;i<data.length;i++){
67 cnt += 1;
67 cnt += 1;
68 no_data = false;
68 no_data = false;
69
69
70 var hide = cnt>2;
70 var hide = cnt>2;
71 var tr = document.createElement('tr');
71 var tr = document.createElement('tr');
72 if (hide){
72 if (hide){
73 tr.setAttribute('style','display:none');
73 tr.setAttribute('style','display:none');
74 tr.setAttribute('class','stats_hidden');
74 tr.setAttribute('class','stats_hidden');
75 }
75 }
76 var k = data[i][0];
76 var k = data[i][0];
77 var obj = data[i][1];
77 var obj = data[i][1];
78 var percentage = Math.round((obj.count/total*100),2);
78 var percentage = Math.round((obj.count/total*100),2);
79
79
80 var td1 = document.createElement('td');
80 var td1 = document.createElement('td');
81 td1.width = 150;
81 td1.width = 150;
82 var trending_language_label = document.createElement('div');
82 var trending_language_label = document.createElement('div');
83 trending_language_label.innerHTML = obj.desc+" ("+k+")";
83 trending_language_label.innerHTML = obj.desc+" ("+k+")";
84 td1.appendChild(trending_language_label);
84 td1.appendChild(trending_language_label);
85
85
86 var td2 = document.createElement('td');
86 var td2 = document.createElement('td');
87 td2.setAttribute('style','padding-right:14px !important');
87 td2.setAttribute('style','padding-right:14px !important');
88 var trending_language = document.createElement('div');
88 var trending_language = document.createElement('div');
89 var nr_files = obj.count+" ${_('files')}";
89 var nr_files = obj.count+" ${_('files')}";
90
90
91 trending_language.title = k+" "+nr_files;
91 trending_language.title = k+" "+nr_files;
92
92
93 if (percentage>22){
93 if (percentage>22){
94 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
94 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
95 }
95 }
96 else{
96 else{
97 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
97 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
98 }
98 }
99
99
100 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
100 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
101 trending_language.style.width=percentage+"%";
101 trending_language.style.width=percentage+"%";
102 td2.appendChild(trending_language);
102 td2.appendChild(trending_language);
103
103
104 tr.appendChild(td1);
104 tr.appendChild(td1);
105 tr.appendChild(td2);
105 tr.appendChild(td2);
106 tbl.appendChild(tr);
106 tbl.appendChild(tr);
107 if(cnt == 3){
107 if(cnt == 3){
108 var show_more = document.createElement('tr');
108 var show_more = document.createElement('tr');
109 var td = document.createElement('td');
109 var td = document.createElement('td');
110 lnk = document.createElement('a');
110 lnk = document.createElement('a');
111
111
112 lnk.href='#';
112 lnk.href='#';
113 lnk.innerHTML = "${_('Show more')}";
113 lnk.innerHTML = "${_('Show more')}";
114 lnk.id='code_stats_show_more';
114 lnk.id='code_stats_show_more';
115 td.appendChild(lnk);
115 td.appendChild(lnk);
116
116
117 show_more.appendChild(td);
117 show_more.appendChild(td);
118 show_more.appendChild(document.createElement('td'));
118 show_more.appendChild(document.createElement('td'));
119 tbl.appendChild(show_more);
119 tbl.appendChild(show_more);
120 }
120 }
121
121
122 }
122 }
123
123
124 </script>
124 </script>
125 <script type="text/javascript">
125 <script type="text/javascript">
126 var YUD = YAHOO.util.Dom;
126 var YUD = YAHOO.util.Dom;
127 var YUE = YAHOO.util.Event;
127 var YUE = YAHOO.util.Event;
128
128
129 /**
129 /**
130 * Plots summary graph
130 * Plots summary graph
131 *
131 *
132 * @class SummaryPlot
132 * @class SummaryPlot
133 * @param {from} initial from for detailed graph
133 * @param {from} initial from for detailed graph
134 * @param {to} initial to for detailed graph
134 * @param {to} initial to for detailed graph
135 * @param {dataset}
135 * @param {dataset}
136 * @param {overview_dataset}
136 * @param {overview_dataset}
137 */
137 */
138 function SummaryPlot(from,to,dataset,overview_dataset) {
138 function SummaryPlot(from,to,dataset,overview_dataset) {
139 var initial_ranges = {
139 var initial_ranges = {
140 "xaxis":{
140 "xaxis":{
141 "from":from,
141 "from":from,
142 "to":to
142 "to":to
143 }
143 }
144 };
144 };
145 var dataset = dataset;
145 var dataset = dataset;
146 var overview_dataset = [overview_dataset];
146 var overview_dataset = [overview_dataset];
147 var choiceContainer = YUD.get("legend_choices");
147 var choiceContainer = YUD.get("legend_choices");
148 var choiceContainerTable = YUD.get("legend_choices_tables");
148 var choiceContainerTable = YUD.get("legend_choices_tables");
149 var plotContainer = YUD.get('commit_history');
149 var plotContainer = YUD.get('commit_history');
150 var overviewContainer = YUD.get('overview');
150 var overviewContainer = YUD.get('overview');
151
151
152 var plot_options = {
152 var plot_options = {
153 bars: {show:true, align: 'center', lineWidth: 4},
153 bars: {show:true, align: 'center', lineWidth: 4},
154 legend: {show:true, container: "legend_container"},
154 legend: {show:true, container: "legend_container"},
155 points: {show:true, radius: 0, fill: false},
155 points: {show:true, radius: 0, fill: false},
156 yaxis: {tickDecimals: 0},
156 yaxis: {tickDecimals: 0},
157 xaxis: {
157 xaxis: {
158 mode: "time",
158 mode: "time",
159 timeformat: "%d/%m",
159 timeformat: "%d/%m",
160 min: from,
160 min: from,
161 max: to
161 max: to
162 },
162 },
163 grid: {
163 grid: {
164 hoverable: true,
164 hoverable: true,
165 clickable: true,
165 clickable: true,
166 autoHighlight: true,
166 autoHighlight: true,
167 color: "#999"
167 color: "#999"
168 },
168 },
169 //selection: {mode: "x"}
169 //selection: {mode: "x"}
170 };
170 };
171 var overview_options = {
171 var overview_options = {
172 legend:{show:false},
172 legend:{show:false},
173 bars: {show:true, barWidth: 2},
173 bars: {show:true, barWidth: 2},
174 shadowSize: 0,
174 shadowSize: 0,
175 xaxis: {mode: "time", timeformat: "%d/%m/%y"},
175 xaxis: {mode: "time", timeformat: "%d/%m/%y"},
176 yaxis: {ticks: 3, min: 0, tickDecimals:0},
176 yaxis: {ticks: 3, min: 0, tickDecimals:0},
177 grid: {color: "#999"},
177 grid: {color: "#999"},
178 selection: {mode: "x"}
178 selection: {mode: "x"}
179 };
179 };
180
180
181 /**
181 /**
182 *get dummy data needed in few places
182 *get dummy data needed in few places
183 */
183 */
184 function getDummyData(label){
184 function getDummyData(label){
185 return {"label":label,
185 return {"label":label,
186 "data":[{"time":0,
186 "data":[{"time":0,
187 "commits":0,
187 "commits":0,
188 "added":0,
188 "added":0,
189 "changed":0,
189 "changed":0,
190 "removed":0
190 "removed":0
191 }],
191 }],
192 "schema":["commits"],
192 "schema":["commits"],
193 "color":'#ffffff'
193 "color":'#ffffff'
194 }
194 }
195 }
195 }
196
196
197 /**
197 /**
198 * generate checkboxes accordingly to data
198 * generate checkboxes accordingly to data
199 * @param keys
199 * @param keys
200 * @returns
200 * @returns
201 */
201 */
202 function generateCheckboxes(data) {
202 function generateCheckboxes(data) {
203 //append checkboxes
203 //append checkboxes
204 var i = 0;
204 var i = 0;
205 choiceContainerTable.innerHTML = '';
205 choiceContainerTable.innerHTML = '';
206 for(var pos in data) {
206 for(var pos in data) {
207
207
208 data[pos].color = i;
208 data[pos].color = i;
209 i++;
209 i++;
210 if(data[pos].label != ''){
210 if(data[pos].label != ''){
211 choiceContainerTable.innerHTML +=
211 choiceContainerTable.innerHTML +=
212 '<tr><td><input type="checkbox" id="id_user_{0}" name="{0}" checked="checked" /> \
212 '<tr><td><input type="checkbox" id="id_user_{0}" name="{0}" checked="checked" /> \
213 <label for="id_user_{0}">{0}</label></td></tr>'.format(data[pos].label);
213 <label for="id_user_{0}">{0}</label></td></tr>'.format(data[pos].label);
214 }
214 }
215 }
215 }
216 }
216 }
217
217
218 /**
218 /**
219 * ToolTip show
219 * ToolTip show
220 */
220 */
221 function showTooltip(x, y, contents) {
221 function showTooltip(x, y, contents) {
222 var div=document.getElementById('tooltip');
222 var div=document.getElementById('tooltip');
223 if(!div) {
223 if(!div) {
224 div = document.createElement('div');
224 div = document.createElement('div');
225 div.id="tooltip";
225 div.id="tooltip";
226 div.style.position="absolute";
226 div.style.position="absolute";
227 div.style.border='1px solid #fdd';
227 div.style.border='1px solid #fdd';
228 div.style.padding='2px';
228 div.style.padding='2px';
229 div.style.backgroundColor='#fee';
229 div.style.backgroundColor='#fee';
230 document.body.appendChild(div);
230 document.body.appendChild(div);
231 }
231 }
232 YUD.setStyle(div, 'opacity', 0);
232 YUD.setStyle(div, 'opacity', 0);
233 div.innerHTML = contents;
233 div.innerHTML = contents;
234 div.style.top=(y + 5) + "px";
234 div.style.top=(y + 5) + "px";
235 div.style.left=(x + 5) + "px";
235 div.style.left=(x + 5) + "px";
236
236
237 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
237 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
238 anim.animate();
238 anim.animate();
239 }
239 }
240
240
241 /**
241 /**
242 * This function will detect if selected period has some changesets
242 * This function will detect if selected period has some changesets
243 for this user if it does this data is then pushed for displaying
243 for this user if it does this data is then pushed for displaying
244 Additionally it will only display users that are selected by the checkbox
244 Additionally it will only display users that are selected by the checkbox
245 */
245 */
246 function getDataAccordingToRanges(ranges) {
246 function getDataAccordingToRanges(ranges) {
247
247
248 var data = [];
248 var data = [];
249 var new_dataset = {};
249 var new_dataset = {};
250 var keys = [];
250 var keys = [];
251 var max_commits = 0;
251 var max_commits = 0;
252 for(var key in dataset){
252 for(var key in dataset){
253
253
254 for(var ds in dataset[key].data){
254 for(var ds in dataset[key].data){
255 commit_data = dataset[key].data[ds];
255 commit_data = dataset[key].data[ds];
256 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
256 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
257
257
258 if(new_dataset[key] === undefined){
258 if(new_dataset[key] === undefined){
259 new_dataset[key] = {data:[],schema:["commits"],label:key};
259 new_dataset[key] = {data:[],schema:["commits"],label:key};
260 }
260 }
261 new_dataset[key].data.push(commit_data);
261 new_dataset[key].data.push(commit_data);
262 }
262 }
263 }
263 }
264 if (new_dataset[key] !== undefined){
264 if (new_dataset[key] !== undefined){
265 data.push(new_dataset[key]);
265 data.push(new_dataset[key]);
266 }
266 }
267 }
267 }
268
268
269 if (data.length > 0){
269 if (data.length > 0){
270 return data;
270 return data;
271 }
271 }
272 else{
272 else{
273 //just return dummy data for graph to plot itself
273 //just return dummy data for graph to plot itself
274 return [getDummyData('')];
274 return [getDummyData('')];
275 }
275 }
276 }
276 }
277
277
278 /**
278 /**
279 * redraw using new checkbox data
279 * redraw using new checkbox data
280 */
280 */
281 function plotchoiced(e,args){
281 function plotchoiced(e,args){
282 var cur_data = args[0];
282 var cur_data = args[0];
283 var cur_ranges = args[1];
283 var cur_ranges = args[1];
284
284
285 var new_data = [];
285 var new_data = [];
286 var inputs = choiceContainer.getElementsByTagName("input");
286 var inputs = choiceContainer.getElementsByTagName("input");
287
287
288 //show only checked labels
288 //show only checked labels
289 for(var i=0; i<inputs.length; i++) {
289 for(var i=0; i<inputs.length; i++) {
290 var checkbox_key = inputs[i].name;
290 var checkbox_key = inputs[i].name;
291
291
292 if(inputs[i].checked){
292 if(inputs[i].checked){
293 for(var d in cur_data){
293 for(var d in cur_data){
294 if(cur_data[d].label == checkbox_key){
294 if(cur_data[d].label == checkbox_key){
295 new_data.push(cur_data[d]);
295 new_data.push(cur_data[d]);
296 }
296 }
297 }
297 }
298 }
298 }
299 else{
299 else{
300 //push dummy data to not hide the label
300 //push dummy data to not hide the label
301 new_data.push(getDummyData(checkbox_key));
301 new_data.push(getDummyData(checkbox_key));
302 }
302 }
303 }
303 }
304
304
305 var new_options = YAHOO.lang.merge(plot_options, {
305 var new_options = YAHOO.lang.merge(plot_options, {
306 xaxis: {
306 xaxis: {
307 min: cur_ranges.xaxis.from,
307 min: cur_ranges.xaxis.from,
308 max: cur_ranges.xaxis.to,
308 max: cur_ranges.xaxis.to,
309 mode: "time",
309 mode: "time",
310 timeformat: "%d/%m"
310 timeformat: "%d/%m"
311 }
311 }
312 });
312 });
313 if (!new_data){
313 if (!new_data){
314 new_data = [[0,1]];
314 new_data = [[0,1]];
315 }
315 }
316 // do the zooming
316 // do the zooming
317 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
317 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
318
318
319 plot.subscribe("plotselected", plotselected);
319 plot.subscribe("plotselected", plotselected);
320
320
321 //resubscribe plothover
321 //resubscribe plothover
322 plot.subscribe("plothover", plothover);
322 plot.subscribe("plothover", plothover);
323
323
324 // don't fire event on the overview to prevent eternal loop
324 // don't fire event on the overview to prevent eternal loop
325 overview.setSelection(cur_ranges, true);
325 overview.setSelection(cur_ranges, true);
326
326
327 }
327 }
328
328
329 /**
329 /**
330 * plot only selected items from overview
330 * plot only selected items from overview
331 * @param ranges
331 * @param ranges
332 * @returns
332 * @returns
333 */
333 */
334 function plotselected(ranges,cur_data) {
334 function plotselected(ranges,cur_data) {
335 //updates the data for new plot
335 //updates the data for new plot
336 var data = getDataAccordingToRanges(ranges);
336 var data = getDataAccordingToRanges(ranges);
337 generateCheckboxes(data);
337 generateCheckboxes(data);
338
338
339 var new_options = YAHOO.lang.merge(plot_options, {
339 var new_options = YAHOO.lang.merge(plot_options, {
340 xaxis: {
340 xaxis: {
341 min: ranges.xaxis.from,
341 min: ranges.xaxis.from,
342 max: ranges.xaxis.to,
342 max: ranges.xaxis.to,
343 mode:"time",
343 mode:"time",
344 timeformat: "%d/%m"
344 timeformat: "%d/%m"
345 }
345 }
346 });
346 });
347 // do the zooming
347 // do the zooming
348 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
348 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
349
349
350 plot.subscribe("plotselected", plotselected);
350 plot.subscribe("plotselected", plotselected);
351
351
352 //resubscribe plothover
352 //resubscribe plothover
353 plot.subscribe("plothover", plothover);
353 plot.subscribe("plothover", plothover);
354
354
355 // don't fire event on the overview to prevent eternal loop
355 // don't fire event on the overview to prevent eternal loop
356 overview.setSelection(ranges, true);
356 overview.setSelection(ranges, true);
357
357
358 //resubscribe choiced
358 //resubscribe choiced
359 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
359 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
360 }
360 }
361
361
362 var previousPoint = null;
362 var previousPoint = null;
363
363
364 function plothover(o) {
364 function plothover(o) {
365 var pos = o.pos;
365 var pos = o.pos;
366 var item = o.item;
366 var item = o.item;
367
367
368 //YUD.get("x").innerHTML = pos.x.toFixed(2);
368 //YUD.get("x").innerHTML = pos.x.toFixed(2);
369 //YUD.get("y").innerHTML = pos.y.toFixed(2);
369 //YUD.get("y").innerHTML = pos.y.toFixed(2);
370 if (item) {
370 if (item) {
371 if (previousPoint != item.datapoint) {
371 if (previousPoint != item.datapoint) {
372 previousPoint = item.datapoint;
372 previousPoint = item.datapoint;
373
373
374 var tooltip = YUD.get("tooltip");
374 var tooltip = YUD.get("tooltip");
375 if(tooltip) {
375 if(tooltip) {
376 tooltip.parentNode.removeChild(tooltip);
376 tooltip.parentNode.removeChild(tooltip);
377 }
377 }
378 var x = item.datapoint.x.toFixed(2);
378 var x = item.datapoint.x.toFixed(2);
379 var y = item.datapoint.y.toFixed(2);
379 var y = item.datapoint.y.toFixed(2);
380
380
381 if (!item.series.label){
381 if (!item.series.label){
382 item.series.label = 'commits';
382 item.series.label = 'commits';
383 }
383 }
384 var d = new Date(x*1000);
384 var d = new Date(x*1000);
385 var fd = d.toDateString();
385 var fd = d.toDateString();
386 var nr_commits = parseInt(y);
386 var nr_commits = parseInt(y);
387
387
388 var cur_data = dataset[item.series.label].data[item.dataIndex];
388 var cur_data = dataset[item.series.label].data[item.dataIndex];
389 var added = cur_data.added;
389 var added = cur_data.added;
390 var changed = cur_data.changed;
390 var changed = cur_data.changed;
391 var removed = cur_data.removed;
391 var removed = cur_data.removed;
392
392
393 var nr_commits_suffix = " ${_('commits')} ";
393 var nr_commits_suffix = " ${_('commits')} ";
394 var added_suffix = " ${_('files added')} ";
394 var added_suffix = " ${_('files added')} ";
395 var changed_suffix = " ${_('files changed')} ";
395 var changed_suffix = " ${_('files changed')} ";
396 var removed_suffix = " ${_('files removed')} ";
396 var removed_suffix = " ${_('files removed')} ";
397
397
398 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
398 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
399 if(added==1){added_suffix=" ${_('file added')} ";}
399 if(added==1){added_suffix=" ${_('file added')} ";}
400 if(changed==1){changed_suffix=" ${_('file changed')} ";}
400 if(changed==1){changed_suffix=" ${_('file changed')} ";}
401 if(removed==1){removed_suffix=" ${_('file removed')} ";}
401 if(removed==1){removed_suffix=" ${_('file removed')} ";}
402
402
403 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
403 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
404 +'<br/>'+
404 +'<br/>'+
405 nr_commits + nr_commits_suffix+'<br/>'+
405 nr_commits + nr_commits_suffix+'<br/>'+
406 added + added_suffix +'<br/>'+
406 added + added_suffix +'<br/>'+
407 changed + changed_suffix + '<br/>'+
407 changed + changed_suffix + '<br/>'+
408 removed + removed_suffix + '<br/>');
408 removed + removed_suffix + '<br/>');
409 }
409 }
410 }
410 }
411 else {
411 else {
412 var tooltip = YUD.get("tooltip");
412 var tooltip = YUD.get("tooltip");
413
413
414 if(tooltip) {
414 if(tooltip) {
415 tooltip.parentNode.removeChild(tooltip);
415 tooltip.parentNode.removeChild(tooltip);
416 }
416 }
417 previousPoint = null;
417 previousPoint = null;
418 }
418 }
419 }
419 }
420
420
421 /**
421 /**
422 * MAIN EXECUTION
422 * MAIN EXECUTION
423 */
423 */
424
424
425 var data = getDataAccordingToRanges(initial_ranges);
425 var data = getDataAccordingToRanges(initial_ranges);
426 generateCheckboxes(data);
426 generateCheckboxes(data);
427
427
428 //main plot
428 //main plot
429 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
429 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
430
430
431 //overview
431 //overview
432 var overview = YAHOO.widget.Flot(overviewContainer,
432 var overview = YAHOO.widget.Flot(overviewContainer,
433 overview_dataset, overview_options);
433 overview_dataset, overview_options);
434
434
435 //show initial selection on overview
435 //show initial selection on overview
436 overview.setSelection(initial_ranges);
436 overview.setSelection(initial_ranges);
437
437
438 plot.subscribe("plotselected", plotselected);
438 plot.subscribe("plotselected", plotselected);
439 plot.subscribe("plothover", plothover);
439 plot.subscribe("plothover", plothover);
440
440
441 overview.subscribe("plotselected", function (ranges) {
441 overview.subscribe("plotselected", function (ranges) {
442 plot.setSelection(ranges);
442 plot.setSelection(ranges);
443 });
443 });
444
444
445 // user choices on overview
445 // user choices on overview
446 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
446 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
447 }
447 }
448 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
448 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
449 </script>
449 </script>
450
450
451 </%def>
451 </%def>
@@ -1,405 +1,405 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%block name="title">
3 <%block name="title">
4 ${_('%s Summary') % c.repo_name}
4 ${_('%s Summary') % c.repo_name}
5 </%block>
5 </%block>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('Summary')}
8 ${_('Summary')}
9
9
10 ## locking icon
10 ## locking icon
11 %if c.db_repo.enable_locking:
11 %if c.db_repo.enable_locking:
12 %if c.db_repo.locked[0]:
12 %if c.db_repo.locked[0]:
13 <span class="locking_locked tooltip icon-block" title="${_('Repository locked by %s') % h.person_by_id(c.db_repo.locked[0])}"></span>
13 <span class="locking_locked tooltip icon-block" title="${_('Repository locked by %s') % h.person_by_id(c.db_repo.locked[0])}"></span>
14 %else:
14 %else:
15 <span class="locking_unlocked tooltip icon-ok" title="${_('Repository unlocked')}"></span>
15 <span class="locking_unlocked tooltip icon-ok" title="${_('Repository unlocked')}"></span>
16 %endif
16 %endif
17 %endif
17 %endif
18
18
19 ##FORK
19 ##FORK
20 %if c.db_repo.fork:
20 %if c.db_repo.fork:
21 <span>
21 <span>
22 - <i class="icon-fork"></i> ${_('Fork of')} "<a href="${h.url('summary_home',repo_name=c.db_repo.fork.repo_name)}">${c.db_repo.fork.repo_name}</a>"
22 - <i class="icon-fork"></i> ${_('Fork of')} "<a href="${h.url('summary_home',repo_name=c.db_repo.fork.repo_name)}">${c.db_repo.fork.repo_name}</a>"
23 </span>
23 </span>
24 %endif
24 %endif
25
25
26 ##REMOTE
26 ##REMOTE
27 %if c.db_repo.clone_uri:
27 %if c.db_repo.clone_uri:
28 <span>
28 <span>
29 - <i class="icon-fork"></i> ${_('Clone from')} "<a href="${h.url(str(h.hide_credentials(c.db_repo.clone_uri)))}">${h.hide_credentials(c.db_repo.clone_uri)}</a>"
29 - <i class="icon-fork"></i> ${_('Clone from')} "<a href="${h.url(str(h.hide_credentials(c.db_repo.clone_uri)))}">${h.hide_credentials(c.db_repo.clone_uri)}</a>"
30 <span>
30 <span>
31 %endif
31 %endif
32 </%def>
32 </%def>
33
33
34 <%block name="header_menu">
34 <%block name="header_menu">
35 ${self.menu('repositories')}
35 ${self.menu('repositories')}
36 </%block>
36 </%block>
37
37
38 <%block name="head_extra">
38 <%block name="head_extra">
39 <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
39 <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
40 <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
40 <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
41
41
42 <script>
42 <script>
43 redirect_hash_branch = function(){
43 redirect_hash_branch = function(){
44 var branch = window.location.hash.replace(/^#(.*)/, '$1');
44 var branch = window.location.hash.replace(/^#(.*)/, '$1');
45 if (branch){
45 if (branch){
46 window.location = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}"
46 window.location = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}"
47 .replace('__BRANCH__',branch);
47 .replace('__BRANCH__',branch);
48 }
48 }
49 }
49 }
50 redirect_hash_branch();
50 redirect_hash_branch();
51 window.onhashchange = function() {
51 window.onhashchange = function() {
52 redirect_hash_branch();
52 redirect_hash_branch();
53 };
53 };
54 </script>
54 </script>
55 </%block>
55 </%block>
56
56
57 <%def name="main()">
57 <%def name="main()">
58 ${self.repo_context_bar('summary')}
58 ${self.repo_context_bar('summary')}
59 <%
59 <%
60 summary = lambda n:{False:'summary-short'}.get(n)
60 summary = lambda n:{False:'summary-short'}.get(n)
61 %>
61 %>
62 <div class="box">
62 <div class="box">
63 <!-- box / title -->
63 <!-- box / title -->
64 <div class="title">
64 <div class="title">
65 ${self.breadcrumbs()}
65 ${self.breadcrumbs()}
66 </div>
66 </div>
67 <!-- end box / title -->
67 <!-- end box / title -->
68 <div class="form">
68 <div class="form">
69 <div id="summary" class="fields">
69 <div id="summary" class="fields">
70 <div class="field">
70 <div class="field">
71 <div class="label-summary">
71 <div class="label-summary">
72 <label>${_('Clone URL')}:</label>
72 <label>${_('Clone URL')}:</label>
73 </div>
73 </div>
74 <div class="input ${summary(c.show_stats)}">
74 <div class="input ${summary(c.show_stats)}">
75 ${self.repotag(c.db_repo)}
75 ${self.repotag(c.db_repo)}
76 <input style="width:80%" type="text" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
76 <input style="width:80%" type="text" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
77 <input style="display:none;width:80%" type="text" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}"/>
77 <input style="display:none;width:80%" type="text" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}"/>
78 <div style="display:none" id="clone_by_name" class="btn btn-small">${_('Show by Name')}</div>
78 <div style="display:none" id="clone_by_name" class="btn btn-small">${_('Show by Name')}</div>
79 <div id="clone_by_id" class="btn btn-small">${_('Show by ID')}</div>
79 <div id="clone_by_id" class="btn btn-small">${_('Show by ID')}</div>
80 </div>
80 </div>
81 </div>
81 </div>
82
82
83 <div class="field">
83 <div class="field">
84 <div class="label-summary">
84 <div class="label-summary">
85 <label>${_('Description')}:</label>
85 <label>${_('Description')}:</label>
86 </div>
86 </div>
87 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(c.db_repo.description, stylize=c.visual.stylify_metatags)}</div>
87 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(c.db_repo.description, stylize=c.visual.stylify_metatags)}</div>
88 </div>
88 </div>
89
89
90 <div class="field">
90 <div class="field">
91 <div class="label-summary">
91 <div class="label-summary">
92 <label>${_('Trending files')}:</label>
92 <label>${_('Trending files')}:</label>
93 </div>
93 </div>
94 <div class="input ${summary(c.show_stats)}">
94 <div class="input ${summary(c.show_stats)}">
95 %if c.show_stats:
95 %if c.show_stats:
96 <div id="lang_stats"></div>
96 <div id="lang_stats"></div>
97 %else:
97 %else:
98 ${_('Statistics are disabled for this repository')}
98 ${_('Statistics are disabled for this repository')}
99 %if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
99 %if h.HasPermissionAny('hg.admin')('enable stats on from summary'):
100 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'),class_="btn btn-mini")}
100 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'),class_="btn btn-mini")}
101 %endif
101 %endif
102 %endif
102 %endif
103 </div>
103 </div>
104 </div>
104 </div>
105
105
106 <div class="field">
106 <div class="field">
107 <div class="label-summary">
107 <div class="label-summary">
108 <label>${_('Download')}:</label>
108 <label>${_('Download')}:</label>
109 </div>
109 </div>
110 <div class="input ${summary(c.show_stats)}">
110 <div class="input ${summary(c.show_stats)}">
111 %if len(c.db_repo_scm_instance.revisions) == 0:
111 %if len(c.db_repo_scm_instance.revisions) == 0:
112 ${_('There are no downloads yet')}
112 ${_('There are no downloads yet')}
113 %elif not c.enable_downloads:
113 %elif not c.enable_downloads:
114 ${_('Downloads are disabled for this repository')}
114 ${_('Downloads are disabled for this repository')}
115 %if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
115 %if h.HasPermissionAny('hg.admin')('enable downloads on from summary'):
116 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'),class_="btn btn-mini")}
116 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'),class_="btn btn-mini")}
117 %endif
117 %endif
118 %else:
118 %else:
119 <span id="${'zip_link'}">
119 <span id="${'zip_link'}">
120 <a class="btn btn-small" href="${h.url('files_archive_home',repo_name=c.db_repo.repo_name,fname='tip.zip')}"><i class="icon-file-zip"></i> ${_('Download as zip')}</a>
120 <a class="btn btn-small" href="${h.url('files_archive_home',repo_name=c.db_repo.repo_name,fname='tip.zip')}"><i class="icon-file-zip"></i> ${_('Download as zip')}</a>
121 </span>
121 </span>
122 ${h.hidden('download_options')}
122 ${h.hidden('download_options')}
123 <span style="vertical-align: bottom">
123 <span style="vertical-align: bottom">
124 <input id="archive_subrepos" type="checkbox" name="subrepos" />
124 <input id="archive_subrepos" type="checkbox" name="subrepos" />
125 <label for="archive_subrepos" class="tooltip" title="${_('Check this to download archive with subrepos')}" >${_('With subrepos')}</label>
125 <label for="archive_subrepos" class="tooltip" title="${_('Check this to download archive with subrepos')}" >${_('With subrepos')}</label>
126 </span>
126 </span>
127 %endif
127 %endif
128 </div>
128 </div>
129 </div>
129 </div>
130 </div>
130 </div>
131 <div id="summary-menu-stats">
131 <div id="summary-menu-stats">
132 <ul>
132 <ul>
133 <li>
133 <li>
134 <a title="${_('Owner')} ${c.db_repo.user.email}">
134 <a title="${_('Owner')} ${c.db_repo.user.email}">
135 <i class="icon-user"></i> ${c.db_repo.user.username}
135 <i class="icon-user"></i> ${c.db_repo.user.username}
136 ${h.gravatar_div(c.db_repo.user.email, size=18, div_style="float: right; margin: 0px 0px 0px 0px", div_title=c.db_repo.user.full_name)}
136 ${h.gravatar_div(c.db_repo.user.email, size=18, div_style="float: right; margin: 0px 0px 0px 0px", div_title=c.db_repo.user.full_name)}
137 </a>
137 </a>
138 </li>
138 </li>
139 <li>
139 <li>
140 <a title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
140 <a title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
141 <i class="icon-heart"></i> ${_('Followers')}
141 <i class="icon-heart"></i> ${_('Followers')}
142 <span class="badge" id="current_followers_count">${c.repository_followers}</span>
142 <span class="badge" id="current_followers_count">${c.repository_followers}</span>
143 </a>
143 </a>
144 </li>
144 </li>
145 <li>
145 <li>
146 <a title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
146 <a title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
147 <i class="icon-fork"></i> ${_('Forks')}
147 <i class="icon-fork"></i> ${_('Forks')}
148 <span class="badge">${c.repository_forks}</span>
148 <span class="badge">${c.repository_forks}</span>
149 </a>
149 </a>
150 </li>
150 </li>
151
151
152 %if c.authuser.username != 'default':
152 %if c.authuser.username != 'default':
153 <li class="repo_size">
153 <li class="repo_size">
154 <a href="#" onclick="javascript:showRepoSize('repo_size_2','${c.db_repo.repo_name}')"><i class="icon-ruler"></i> ${_('Repository Size')}</a>
154 <a href="#" onclick="javascript:showRepoSize('repo_size_2','${c.db_repo.repo_name}')"><i class="icon-ruler"></i> ${_('Repository Size')}</a>
155 <span class="stats-bullet" id="repo_size_2"></span>
155 <span class="stats-bullet" id="repo_size_2"></span>
156 </li>
156 </li>
157 %endif
157 %endif
158
158
159 <li>
159 <li>
160 %if c.authuser.username != 'default':
160 %if c.authuser.username != 'default':
161 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
161 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
162 %else:
162 %else:
163 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
163 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
164 %endif
164 %endif
165 </li>
165 </li>
166
166
167 %if c.show_stats:
167 %if c.show_stats:
168 <li>
168 <li>
169 <a title="${_('Statistics')}" href="${h.url('repo_stats_home',repo_name=c.repo_name)}">
169 <a title="${_('Statistics')}" href="${h.url('repo_stats_home',repo_name=c.repo_name)}">
170 <i class="icon-graph"></i> ${_('Statistics')}
170 <i class="icon-graph"></i> ${_('Statistics')}
171 </a>
171 </a>
172 </li>
172 </li>
173 %endif
173 %endif
174 </ul>
174 </ul>
175 </div>
175 </div>
176 </div>
176 </div>
177 </div>
177 </div>
178
178
179
179
180 <div class="box">
180 <div class="box">
181 <div class="title">
181 <div class="title">
182 <div class="breadcrumbs">
182 <div class="breadcrumbs">
183 %if c.repo_changesets:
183 %if c.repo_changesets:
184 ${h.link_to(_('Latest Changes'),h.url('changelog_home',repo_name=c.repo_name))}
184 ${h.link_to(_('Latest Changes'),h.url('changelog_home',repo_name=c.repo_name))}
185 %else:
185 %else:
186 ${_('Quick Start')}
186 ${_('Quick Start')}
187 %endif
187 %endif
188 </div>
188 </div>
189 </div>
189 </div>
190 <div class="table">
190 <div class="table">
191 <div id="shortlog_data">
191 <div id="shortlog_data">
192 <%include file='../changelog/changelog_summary_data.html'/>
192 <%include file='../changelog/changelog_summary_data.html'/>
193 </div>
193 </div>
194 </div>
194 </div>
195 </div>
195 </div>
196
196
197 %if c.readme_data:
197 %if c.readme_data:
198 <div id="readme" class="anchor">
198 <div id="readme" class="anchor">
199 <div class="box" style="background-color: #FAFAFA">
199 <div class="box" style="background-color: #FAFAFA">
200 <div class="title" title="${_('Readme file from revision %s:%s') % (c.db_repo.landing_rev[0], c.db_repo.landing_rev[1])}">
200 <div class="title" title="${_('Readme file from revision %s:%s') % (c.db_repo.landing_rev[0], c.db_repo.landing_rev[1])}">
201 <div class="breadcrumbs">
201 <div class="breadcrumbs">
202 <a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a>
202 <a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a>
203 </div>
203 </div>
204 </div>
204 </div>
205 <div class="readme">
205 <div class="readme">
206 <div class="readme_box">
206 <div class="readme_box">
207 ${c.readme_data|n}
207 ${c.readme_data|n}
208 </div>
208 </div>
209 </div>
209 </div>
210 </div>
210 </div>
211 </div>
211 </div>
212 %endif
212 %endif
213
213
214 <script type="text/javascript">
214 <script type="text/javascript">
215 $(document).ready(function(){
215 $(document).ready(function(){
216 var $clone_url = $('#clone_url');
216 var $clone_url = $('#clone_url');
217 var $clone_url_id = $('#clone_url_id');
217 var $clone_url_id = $('#clone_url_id');
218 var $clone_by_name = $('#clone_by_name');
218 var $clone_by_name = $('#clone_by_name');
219 var $clone_by_id = $('#clone_by_id');
219 var $clone_by_id = $('#clone_by_id');
220 $clone_url.click(function(e){
220 $clone_url.click(function(e){
221 if($clone_url.hasClass('selected')){
221 if($clone_url.hasClass('selected')){
222 return ;
222 return ;
223 }else{
223 }else{
224 $clone_url.addClass('selected');
224 $clone_url.addClass('selected');
225 $clone_url.select();
225 $clone_url.select();
226 }
226 }
227 });
227 });
228
228
229 $clone_by_name.click(function(e){
229 $clone_by_name.click(function(e){
230 // show url by name and hide name button
230 // show url by name and hide name button
231 $clone_url.show();
231 $clone_url.show();
232 $clone_by_name.hide();
232 $clone_by_name.hide();
233
233
234 // hide url by id and show name button
234 // hide url by id and show name button
235 $clone_by_id.show();
235 $clone_by_id.show();
236 $clone_url_id.hide();
236 $clone_url_id.hide();
237 });
237 });
238
238
239 $clone_by_id.click(function(e){
239 $clone_by_id.click(function(e){
240 // show url by id and hide id button
240 // show url by id and hide id button
241 $clone_by_id.hide();
241 $clone_by_id.hide();
242 $clone_url_id.show();
242 $clone_url_id.show();
243
243
244 // hide url by name and show id button
244 // hide url by name and show id button
245 $clone_by_name.show();
245 $clone_by_name.show();
246 $clone_url.hide();
246 $clone_url.hide();
247 });
247 });
248
248
249 var cache = {}
249 var cache = {}
250 $("#download_options").select2({
250 $("#download_options").select2({
251 placeholder: _TM['Select changeset'],
251 placeholder: _TM['Select changeset'],
252 dropdownAutoWidth: true,
252 dropdownAutoWidth: true,
253 query: function(query){
253 query: function(query){
254 var key = 'cache';
254 var key = 'cache';
255 var cached = cache[key] ;
255 var cached = cache[key] ;
256 if(cached) {
256 if(cached) {
257 var data = {results: []};
257 var data = {results: []};
258 //filter results
258 //filter results
259 $.each(cached.results, function(){
259 $.each(cached.results, function(){
260 var section = this.text;
260 var section = this.text;
261 var children = [];
261 var children = [];
262 $.each(this.children, function(){
262 $.each(this.children, function(){
263 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
263 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
264 children.push({'id': this.id, 'text': this.text});
264 children.push({'id': this.id, 'text': this.text});
265 }
265 }
266 });
266 });
267 data.results.push({'text': section, 'children': children});
267 data.results.push({'text': section, 'children': children});
268 });
268 });
269 query.callback(data);
269 query.callback(data);
270 }else{
270 }else{
271 $.ajax({
271 $.ajax({
272 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.repo_name}'}),
272 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.repo_name}'}),
273 data: {},
273 data: {},
274 dataType: 'json',
274 dataType: 'json',
275 type: 'GET',
275 type: 'GET',
276 success: function(data) {
276 success: function(data) {
277 cache[key] = data;
277 cache[key] = data;
278 query.callback({results: data.results});
278 query.callback({results: data.results});
279 }
279 }
280 });
280 });
281 }
281 }
282 }
282 }
283 });
283 });
284 // on change of download options
284 // on change of download options
285 $('#download_options').change(function(e){
285 $('#download_options').change(function(e){
286 var new_cs = e.added
286 var new_cs = e.added
287
287
288 for(k in tmpl_links){
288 for(k in tmpl_links){
289 var s = $('#'+k+'_link');
289 var s = $('#'+k+'_link');
290 if(s){
290 if(s){
291 var title_tmpl = "${_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__')}";
291 var title_tmpl = "${_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__')}";
292 title_tmpl= title_tmpl.replace('__CS_NAME__',new_cs.text);
292 title_tmpl= title_tmpl.replace('__CS_NAME__',new_cs.text);
293 title_tmpl = title_tmpl.replace('__CS_EXT__',k);
293 title_tmpl = title_tmpl.replace('__CS_EXT__',k);
294 title_tmpl = '<i class="icon-file-zip"></i> '+ title_tmpl;
294 title_tmpl = '<i class="icon-file-zip"></i> '+ title_tmpl;
295 var url = tmpl_links[k].replace('__CS__',new_cs.id);
295 var url = tmpl_links[k].replace('__CS__',new_cs.id);
296 var subrepos = $('#archive_subrepos').is(':checked');
296 var subrepos = $('#archive_subrepos').is(':checked');
297 url = url.replace('__SUB__',subrepos);
297 url = url.replace('__SUB__',subrepos);
298 url = url.replace('__NAME__',title_tmpl);
298 url = url.replace('__NAME__',title_tmpl);
299
299
300 s.html(url);
300 s.html(url);
301 }
301 }
302 }
302 }
303 });
303 });
304
304
305 var tmpl_links = {};
305 var tmpl_links = {};
306 %for cnt,archive in enumerate(c.db_repo_scm_instance._get_archives()):
306 %for cnt,archive in enumerate(c.db_repo_scm_instance._get_archives()):
307 tmpl_links["${archive['type']}"] = '${h.link_to('__NAME__', h.url('files_archive_home',repo_name=c.db_repo.repo_name, fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_='btn btn-small')}';
307 tmpl_links["${archive['type']}"] = '${h.link_to('__NAME__', h.url('files_archive_home',repo_name=c.db_repo.repo_name, fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_='btn btn-small')}';
308 %endfor
308 %endfor
309 });
309 });
310 </script>
310 </script>
311
311
312 %if c.show_stats:
312 %if c.show_stats:
313 <script type="text/javascript">
313 <script type="text/javascript">
314 $(document).ready(function(){
314 $(document).ready(function(){
315 var data = ${c.trending_languages|n};
315 var data = ${c.trending_languages|n};
316 var total = 0;
316 var total = 0;
317 var no_data = true;
317 var no_data = true;
318 var tbl = document.createElement('table');
318 var tbl = document.createElement('table');
319 tbl.setAttribute('class','trending_language_tbl');
319 tbl.setAttribute('class','trending_language_tbl');
320 var cnt = 0;
320 var cnt = 0;
321 for (var i=0;i<data.length;i++){
321 for (var i=0;i<data.length;i++){
322 total+= data[i][1].count;
322 total+= data[i][1].count;
323 }
323 }
324 for (var i=0;i<data.length;i++){
324 for (var i=0;i<data.length;i++){
325 cnt += 1;
325 cnt += 1;
326 no_data = false;
326 no_data = false;
327
327
328 var hide = cnt>2;
328 var hide = cnt>2;
329 var tr = document.createElement('tr');
329 var tr = document.createElement('tr');
330 if (hide){
330 if (hide){
331 tr.setAttribute('style','display:none');
331 tr.setAttribute('style','display:none');
332 tr.setAttribute('class','stats_hidden');
332 tr.setAttribute('class','stats_hidden');
333 }
333 }
334 var k = data[i][0];
334 var k = data[i][0];
335 var obj = data[i][1];
335 var obj = data[i][1];
336 var percentage = Math.round((obj.count/total*100),2);
336 var percentage = Math.round((obj.count/total*100),2);
337
337
338 var td1 = document.createElement('td');
338 var td1 = document.createElement('td');
339 td1.width = 150;
339 td1.width = 150;
340 var trending_language_label = document.createElement('div');
340 var trending_language_label = document.createElement('div');
341 trending_language_label.innerHTML = obj.desc+" ("+k+")";
341 trending_language_label.innerHTML = obj.desc+" ("+k+")";
342 td1.appendChild(trending_language_label);
342 td1.appendChild(trending_language_label);
343
343
344 var td2 = document.createElement('td');
344 var td2 = document.createElement('td');
345 td2.setAttribute('style','padding-right:14px !important');
345 td2.setAttribute('style','padding-right:14px !important');
346 var trending_language = document.createElement('div');
346 var trending_language = document.createElement('div');
347 var nr_files = obj.count+" ${_('files')}";
347 var nr_files = obj.count+" ${_('files')}";
348
348
349 trending_language.title = k+" "+nr_files;
349 trending_language.title = k+" "+nr_files;
350
350
351 if (percentage>22){
351 if (percentage>22){
352 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
352 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
353 }
353 }
354 else{
354 else{
355 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
355 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
356 }
356 }
357
357
358 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
358 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
359 trending_language.style.width=percentage+"%";
359 trending_language.style.width=percentage+"%";
360 td2.appendChild(trending_language);
360 td2.appendChild(trending_language);
361
361
362 tr.appendChild(td1);
362 tr.appendChild(td1);
363 tr.appendChild(td2);
363 tr.appendChild(td2);
364 tbl.appendChild(tr);
364 tbl.appendChild(tr);
365 if(cnt == 3){
365 if(cnt == 3){
366 var show_more = document.createElement('tr');
366 var show_more = document.createElement('tr');
367 var td = document.createElement('td');
367 var td = document.createElement('td');
368 lnk = document.createElement('a');
368 lnk = document.createElement('a');
369
369
370 lnk.href='#';
370 lnk.href='#';
371 lnk.innerHTML = "${_('Show more')}";
371 lnk.innerHTML = "${_('Show more')}";
372 lnk.id='code_stats_show_more';
372 lnk.id='code_stats_show_more';
373 td.appendChild(lnk);
373 td.appendChild(lnk);
374
374
375 show_more.appendChild(td);
375 show_more.appendChild(td);
376 show_more.appendChild(document.createElement('td'));
376 show_more.appendChild(document.createElement('td'));
377 tbl.appendChild(show_more);
377 tbl.appendChild(show_more);
378 }
378 }
379
379
380 }
380 }
381 if (data.length == 0) {
381 if (data.length == 0) {
382 tbl.innerHTML = "<tr><td>${_('No data ready yet')}</td></tr>";
382 tbl.innerHTML = "<tr><td>${_('No data ready yet')}</td></tr>";
383 }
383 }
384
384
385 $('#lang_stats').append(tbl);
385 $('#lang_stats').append(tbl);
386 $('#code_stats_show_more').click(function(){
386 $('#code_stats_show_more').click(function(){
387 $('.stats_hidden').show();
387 $('.stats_hidden').show();
388 $('#code_stats_show_more').hide();
388 $('#code_stats_show_more').hide();
389 });
389 });
390 });
390 });
391 </script>
391 </script>
392 %endif
392 %endif
393
393
394 ## Shortlog paging
394 ## Shortlog paging
395 <script type="text/javascript">
395 <script type="text/javascript">
396 $(document).ready(function(){
396 $(document).ready(function(){
397 var $shortlog_data = $('#shortlog_data');
397 var $shortlog_data = $('#shortlog_data');
398 $shortlog_data.on('click','.pager_link',function(e){
398 $shortlog_data.on('click','.pager_link',function(e){
399 asynchtml(e.target.href, $shortlog_data, function(){tooltip_activate();});
399 asynchtml(e.target.href, $shortlog_data, function(){tooltip_activate();});
400 e.preventDefault();
400 e.preventDefault();
401 });
401 });
402 });
402 });
403 </script>
403 </script>
404
404
405 </%def>
405 </%def>
General Comments 0
You need to be logged in to leave comments. Login now