##// END OF EJS Templates
cleanup: drop some dead code spotted by "vulture"
Mads Kiilerich -
r8098:b6b69559 default
parent child Browse files
Show More
@@ -1,32 +1,29 b''
1 .. _models:
1 .. _models:
2
2
3 ========================
3 ========================
4 The :mod:`models` module
4 The :mod:`models` module
5 ========================
5 ========================
6
6
7 .. automodule:: kallithea.model
7 .. automodule:: kallithea.model
8 :members:
8 :members:
9
9
10 .. automodule:: kallithea.model.comment
10 .. automodule:: kallithea.model.comment
11 :members:
11 :members:
12
12
13 .. automodule:: kallithea.model.permission
13 .. automodule:: kallithea.model.permission
14 :members:
14 :members:
15
15
16 .. automodule:: kallithea.model.repo_permission
17 :members:
18
19 .. automodule:: kallithea.model.repo
16 .. automodule:: kallithea.model.repo
20 :members:
17 :members:
21
18
22 .. automodule:: kallithea.model.repo_group
19 .. automodule:: kallithea.model.repo_group
23 :members:
20 :members:
24
21
25 .. automodule:: kallithea.model.scm
22 .. automodule:: kallithea.model.scm
26 :members:
23 :members:
27
24
28 .. automodule:: kallithea.model.user
25 .. automodule:: kallithea.model.user
29 :members:
26 :members:
30
27
31 .. automodule:: kallithea.model.user_group
28 .. automodule:: kallithea.model.user_group
32 :members:
29 :members:
@@ -1,398 +1,397 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
30
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from tg import app_globals, request
33 from tg import app_globals, request
34 from tg import tmpl_context as c
34 from tg import tmpl_context as c
35 from tg.i18n import ugettext as _
35 from tg.i18n import ugettext as _
36 from tg.i18n import ungettext
36 from tg.i18n import ungettext
37 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
37 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
38
38
39 from kallithea.config.routing import url
39 from kallithea.config.routing import url
40 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
41 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired
41 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired
42 from kallithea.lib.base import BaseController, render
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.utils2 import safe_int
43 from kallithea.lib.utils2 import safe_int
44 from kallithea.model.db import RepoGroup, Repository
44 from kallithea.model.db import RepoGroup, Repository
45 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
45 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
46 from kallithea.model.meta import Session
46 from kallithea.model.meta import Session
47 from kallithea.model.repo import RepoModel
47 from kallithea.model.repo import RepoModel
48 from kallithea.model.repo_group import RepoGroupModel
48 from kallithea.model.repo_group import RepoGroupModel
49 from kallithea.model.scm import AvailableRepoGroupChoices, RepoGroupList
49 from kallithea.model.scm import AvailableRepoGroupChoices, RepoGroupList
50
50
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class RepoGroupsController(BaseController):
55 class RepoGroupsController(BaseController):
56
56
57 @LoginRequired(allow_default_user=True)
57 @LoginRequired(allow_default_user=True)
58 def _before(self, *args, **kwargs):
58 def _before(self, *args, **kwargs):
59 super(RepoGroupsController, self)._before(*args, **kwargs)
59 super(RepoGroupsController, self)._before(*args, **kwargs)
60
60
61 def __load_defaults(self, extras=(), exclude=()):
61 def __load_defaults(self, extras=(), exclude=()):
62 """extras is used for keeping current parent ignoring permissions
62 """extras is used for keeping current parent ignoring permissions
63 exclude is used for not moving group to itself TODO: also exclude descendants
63 exclude is used for not moving group to itself TODO: also exclude descendants
64 Note: only admin can create top level groups
64 Note: only admin can create top level groups
65 """
65 """
66 repo_groups = AvailableRepoGroupChoices([], 'admin', extras)
66 repo_groups = AvailableRepoGroupChoices([], 'admin', extras)
67 exclude_group_ids = set(rg.group_id for rg in exclude)
67 exclude_group_ids = set(rg.group_id for rg in exclude)
68 c.repo_groups = [rg for rg in repo_groups
68 c.repo_groups = [rg for rg in repo_groups
69 if rg[0] not in exclude_group_ids]
69 if rg[0] not in exclude_group_ids]
70
70
71 def __load_data(self, group_id):
71 def __load_data(self, group_id):
72 """
72 """
73 Load defaults settings for edit, and update
73 Load defaults settings for edit, and update
74
74
75 :param group_id:
75 :param group_id:
76 """
76 """
77 repo_group = RepoGroup.get_or_404(group_id)
77 repo_group = RepoGroup.get_or_404(group_id)
78 data = repo_group.get_dict()
78 data = repo_group.get_dict()
79 data['group_name'] = repo_group.name
79 data['group_name'] = repo_group.name
80
80
81 # fill repository group users
81 # fill repository group users
82 for p in repo_group.repo_group_to_perm:
82 for p in repo_group.repo_group_to_perm:
83 data.update({'u_perm_%s' % p.user.username:
83 data.update({'u_perm_%s' % p.user.username:
84 p.permission.permission_name})
84 p.permission.permission_name})
85
85
86 # fill repository group groups
86 # fill repository group groups
87 for p in repo_group.users_group_to_perm:
87 for p in repo_group.users_group_to_perm:
88 data.update({'g_perm_%s' % p.users_group.users_group_name:
88 data.update({'g_perm_%s' % p.users_group.users_group_name:
89 p.permission.permission_name})
89 p.permission.permission_name})
90
90
91 return data
91 return data
92
92
93 def _revoke_perms_on_yourself(self, form_result):
93 def _revoke_perms_on_yourself(self, form_result):
94 _up = [u for u in form_result['perms_updates'] if request.authuser.username == u[0]]
94 _up = [u for u in form_result['perms_updates'] if request.authuser.username == u[0]]
95 _new = [u for u in form_result['perms_new'] if request.authuser.username == u[0]]
95 _new = [u for u in form_result['perms_new'] if request.authuser.username == u[0]]
96 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
96 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
97 return True
97 return True
98 return False
98 return False
99
99
100 def index(self, format='html'):
100 def index(self, format='html'):
101 _list = RepoGroup.query(sorted=True).all()
101 _list = RepoGroup.query(sorted=True).all()
102 group_iter = RepoGroupList(_list, perm_level='admin')
102 group_iter = RepoGroupList(_list, perm_level='admin')
103 repo_groups_data = []
103 repo_groups_data = []
104 total_records = len(group_iter)
105 _tmpl_lookup = app_globals.mako_lookup
104 _tmpl_lookup = app_globals.mako_lookup
106 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
105 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
107
106
108 repo_group_name = lambda repo_group_name, children_groups: (
107 repo_group_name = lambda repo_group_name, children_groups: (
109 template.get_def("repo_group_name")
108 template.get_def("repo_group_name")
110 .render_unicode(repo_group_name, children_groups, _=_, h=h, c=c)
109 .render_unicode(repo_group_name, children_groups, _=_, h=h, c=c)
111 )
110 )
112 repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
111 repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
113 template.get_def("repo_group_actions")
112 template.get_def("repo_group_actions")
114 .render_unicode(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
113 .render_unicode(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
115 ungettext=ungettext)
114 ungettext=ungettext)
116 )
115 )
117
116
118 for repo_gr in group_iter:
117 for repo_gr in group_iter:
119 children_groups = [g.name for g in repo_gr.parents] + [repo_gr.name]
118 children_groups = [g.name for g in repo_gr.parents] + [repo_gr.name]
120 repo_count = repo_gr.repositories.count()
119 repo_count = repo_gr.repositories.count()
121 repo_groups_data.append({
120 repo_groups_data.append({
122 "raw_name": repo_gr.group_name,
121 "raw_name": repo_gr.group_name,
123 "group_name": repo_group_name(repo_gr.group_name, children_groups),
122 "group_name": repo_group_name(repo_gr.group_name, children_groups),
124 "desc": h.escape(repo_gr.group_description),
123 "desc": h.escape(repo_gr.group_description),
125 "repos": repo_count,
124 "repos": repo_count,
126 "owner": h.person(repo_gr.owner),
125 "owner": h.person(repo_gr.owner),
127 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
126 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
128 repo_count)
127 repo_count)
129 })
128 })
130
129
131 c.data = {
130 c.data = {
132 "sort": None,
131 "sort": None,
133 "dir": "asc",
132 "dir": "asc",
134 "records": repo_groups_data
133 "records": repo_groups_data
135 }
134 }
136
135
137 return render('admin/repo_groups/repo_groups.html')
136 return render('admin/repo_groups/repo_groups.html')
138
137
139 def create(self):
138 def create(self):
140 self.__load_defaults()
139 self.__load_defaults()
141
140
142 # permissions for can create group based on parent_id are checked
141 # permissions for can create group based on parent_id are checked
143 # here in the Form
142 # here in the Form
144 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
143 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
145 form_result = None
144 form_result = None
146 try:
145 try:
147 form_result = repo_group_form.to_python(dict(request.POST))
146 form_result = repo_group_form.to_python(dict(request.POST))
148 gr = RepoGroupModel().create(
147 gr = RepoGroupModel().create(
149 group_name=form_result['group_name'],
148 group_name=form_result['group_name'],
150 group_description=form_result['group_description'],
149 group_description=form_result['group_description'],
151 parent=form_result['parent_group_id'],
150 parent=form_result['parent_group_id'],
152 owner=request.authuser.user_id, # TODO: make editable
151 owner=request.authuser.user_id, # TODO: make editable
153 copy_permissions=form_result['group_copy_permissions']
152 copy_permissions=form_result['group_copy_permissions']
154 )
153 )
155 Session().commit()
154 Session().commit()
156 # TODO: in future action_logger(, '', '', '')
155 # TODO: in future action_logger(, '', '', '')
157 except formencode.Invalid as errors:
156 except formencode.Invalid as errors:
158 return htmlfill.render(
157 return htmlfill.render(
159 render('admin/repo_groups/repo_group_add.html'),
158 render('admin/repo_groups/repo_group_add.html'),
160 defaults=errors.value,
159 defaults=errors.value,
161 errors=errors.error_dict or {},
160 errors=errors.error_dict or {},
162 prefix_error=False,
161 prefix_error=False,
163 encoding="UTF-8",
162 encoding="UTF-8",
164 force_defaults=False)
163 force_defaults=False)
165 except Exception:
164 except Exception:
166 log.error(traceback.format_exc())
165 log.error(traceback.format_exc())
167 h.flash(_('Error occurred during creation of repository group %s')
166 h.flash(_('Error occurred during creation of repository group %s')
168 % request.POST.get('group_name'), category='error')
167 % request.POST.get('group_name'), category='error')
169 if form_result is None:
168 if form_result is None:
170 raise
169 raise
171 parent_group_id = form_result['parent_group_id']
170 parent_group_id = form_result['parent_group_id']
172 # TODO: maybe we should get back to the main view, not the admin one
171 # TODO: maybe we should get back to the main view, not the admin one
173 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
172 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
174 h.flash(_('Created repository group %s') % gr.group_name,
173 h.flash(_('Created repository group %s') % gr.group_name,
175 category='success')
174 category='success')
176 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
175 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
177
176
178 def new(self):
177 def new(self):
179 if HasPermissionAny('hg.admin')('group create'):
178 if HasPermissionAny('hg.admin')('group create'):
180 # we're global admin, we're ok and we can create TOP level groups
179 # we're global admin, we're ok and we can create TOP level groups
181 pass
180 pass
182 else:
181 else:
183 # we pass in parent group into creation form, thus we know
182 # we pass in parent group into creation form, thus we know
184 # what would be the group, we can check perms here !
183 # what would be the group, we can check perms here !
185 group_id = safe_int(request.GET.get('parent_group'))
184 group_id = safe_int(request.GET.get('parent_group'))
186 group = RepoGroup.get(group_id) if group_id else None
185 group = RepoGroup.get(group_id) if group_id else None
187 group_name = group.group_name if group else None
186 group_name = group.group_name if group else None
188 if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'):
187 if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'):
189 pass
188 pass
190 else:
189 else:
191 raise HTTPForbidden()
190 raise HTTPForbidden()
192
191
193 self.__load_defaults()
192 self.__load_defaults()
194 return render('admin/repo_groups/repo_group_add.html')
193 return render('admin/repo_groups/repo_group_add.html')
195
194
196 @HasRepoGroupPermissionLevelDecorator('admin')
195 @HasRepoGroupPermissionLevelDecorator('admin')
197 def update(self, group_name):
196 def update(self, group_name):
198 c.repo_group = RepoGroup.guess_instance(group_name)
197 c.repo_group = RepoGroup.guess_instance(group_name)
199 self.__load_defaults(extras=[c.repo_group.parent_group],
198 self.__load_defaults(extras=[c.repo_group.parent_group],
200 exclude=[c.repo_group])
199 exclude=[c.repo_group])
201
200
202 # TODO: kill allow_empty_group - it is only used for redundant form validation!
201 # TODO: kill allow_empty_group - it is only used for redundant form validation!
203 if HasPermissionAny('hg.admin')('group edit'):
202 if HasPermissionAny('hg.admin')('group edit'):
204 # we're global admin, we're ok and we can create TOP level groups
203 # we're global admin, we're ok and we can create TOP level groups
205 allow_empty_group = True
204 allow_empty_group = True
206 elif not c.repo_group.parent_group:
205 elif not c.repo_group.parent_group:
207 allow_empty_group = True
206 allow_empty_group = True
208 else:
207 else:
209 allow_empty_group = False
208 allow_empty_group = False
210 repo_group_form = RepoGroupForm(
209 repo_group_form = RepoGroupForm(
211 edit=True,
210 edit=True,
212 old_data=c.repo_group.get_dict(),
211 old_data=c.repo_group.get_dict(),
213 repo_groups=c.repo_groups,
212 repo_groups=c.repo_groups,
214 can_create_in_root=allow_empty_group,
213 can_create_in_root=allow_empty_group,
215 )()
214 )()
216 try:
215 try:
217 form_result = repo_group_form.to_python(dict(request.POST))
216 form_result = repo_group_form.to_python(dict(request.POST))
218
217
219 new_gr = RepoGroupModel().update(group_name, form_result)
218 new_gr = RepoGroupModel().update(group_name, form_result)
220 Session().commit()
219 Session().commit()
221 h.flash(_('Updated repository group %s')
220 h.flash(_('Updated repository group %s')
222 % form_result['group_name'], category='success')
221 % form_result['group_name'], category='success')
223 # we now have new name !
222 # we now have new name !
224 group_name = new_gr.group_name
223 group_name = new_gr.group_name
225 # TODO: in future action_logger(, '', '', '')
224 # TODO: in future action_logger(, '', '', '')
226 except formencode.Invalid as errors:
225 except formencode.Invalid as errors:
227 c.active = 'settings'
226 c.active = 'settings'
228 return htmlfill.render(
227 return htmlfill.render(
229 render('admin/repo_groups/repo_group_edit.html'),
228 render('admin/repo_groups/repo_group_edit.html'),
230 defaults=errors.value,
229 defaults=errors.value,
231 errors=errors.error_dict or {},
230 errors=errors.error_dict or {},
232 prefix_error=False,
231 prefix_error=False,
233 encoding="UTF-8",
232 encoding="UTF-8",
234 force_defaults=False)
233 force_defaults=False)
235 except Exception:
234 except Exception:
236 log.error(traceback.format_exc())
235 log.error(traceback.format_exc())
237 h.flash(_('Error occurred during update of repository group %s')
236 h.flash(_('Error occurred during update of repository group %s')
238 % request.POST.get('group_name'), category='error')
237 % request.POST.get('group_name'), category='error')
239
238
240 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
239 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
241
240
242 @HasRepoGroupPermissionLevelDecorator('admin')
241 @HasRepoGroupPermissionLevelDecorator('admin')
243 def delete(self, group_name):
242 def delete(self, group_name):
244 gr = c.repo_group = RepoGroup.guess_instance(group_name)
243 gr = c.repo_group = RepoGroup.guess_instance(group_name)
245 repos = gr.repositories.all()
244 repos = gr.repositories.all()
246 if repos:
245 if repos:
247 h.flash(_('This group contains %s repositories and cannot be '
246 h.flash(_('This group contains %s repositories and cannot be '
248 'deleted') % len(repos), category='warning')
247 'deleted') % len(repos), category='warning')
249 raise HTTPFound(location=url('repos_groups'))
248 raise HTTPFound(location=url('repos_groups'))
250
249
251 children = gr.children.all()
250 children = gr.children.all()
252 if children:
251 if children:
253 h.flash(_('This group contains %s subgroups and cannot be deleted'
252 h.flash(_('This group contains %s subgroups and cannot be deleted'
254 % (len(children))), category='warning')
253 % (len(children))), category='warning')
255 raise HTTPFound(location=url('repos_groups'))
254 raise HTTPFound(location=url('repos_groups'))
256
255
257 try:
256 try:
258 RepoGroupModel().delete(group_name)
257 RepoGroupModel().delete(group_name)
259 Session().commit()
258 Session().commit()
260 h.flash(_('Removed repository group %s') % group_name,
259 h.flash(_('Removed repository group %s') % group_name,
261 category='success')
260 category='success')
262 # TODO: in future action_logger(, '', '', '')
261 # TODO: in future action_logger(, '', '', '')
263 except Exception:
262 except Exception:
264 log.error(traceback.format_exc())
263 log.error(traceback.format_exc())
265 h.flash(_('Error occurred during deletion of repository group %s')
264 h.flash(_('Error occurred during deletion of repository group %s')
266 % group_name, category='error')
265 % group_name, category='error')
267
266
268 if gr.parent_group:
267 if gr.parent_group:
269 raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name))
268 raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name))
270 raise HTTPFound(location=url('repos_groups'))
269 raise HTTPFound(location=url('repos_groups'))
271
270
272 def show_by_name(self, group_name):
271 def show_by_name(self, group_name):
273 """
272 """
274 This is a proxy that does a lookup group_name -> id, and shows
273 This is a proxy that does a lookup group_name -> id, and shows
275 the group by id view instead
274 the group by id view instead
276 """
275 """
277 group_name = group_name.rstrip('/')
276 group_name = group_name.rstrip('/')
278 id_ = RepoGroup.get_by_group_name(group_name)
277 id_ = RepoGroup.get_by_group_name(group_name)
279 if id_:
278 if id_:
280 return self.show(group_name)
279 return self.show(group_name)
281 raise HTTPNotFound
280 raise HTTPNotFound
282
281
283 @HasRepoGroupPermissionLevelDecorator('read')
282 @HasRepoGroupPermissionLevelDecorator('read')
284 def show(self, group_name):
283 def show(self, group_name):
285 c.active = 'settings'
284 c.active = 'settings'
286
285
287 c.group = c.repo_group = RepoGroup.guess_instance(group_name)
286 c.group = c.repo_group = RepoGroup.guess_instance(group_name)
288
287
289 groups = RepoGroup.query(sorted=True).filter_by(parent_group=c.group).all()
288 groups = RepoGroup.query(sorted=True).filter_by(parent_group=c.group).all()
290 repo_groups_list = self.scm_model.get_repo_groups(groups)
289 repo_groups_list = self.scm_model.get_repo_groups(groups)
291
290
292 repos_list = Repository.query(sorted=True).filter_by(group=c.group).all()
291 repos_list = Repository.query(sorted=True).filter_by(group=c.group).all()
293 c.data = RepoModel().get_repos_as_dict(repos_list,
292 c.data = RepoModel().get_repos_as_dict(repos_list,
294 repo_groups_list=repo_groups_list,
293 repo_groups_list=repo_groups_list,
295 short_name=True)
294 short_name=True)
296
295
297 return render('admin/repo_groups/repo_group_show.html')
296 return render('admin/repo_groups/repo_group_show.html')
298
297
299 @HasRepoGroupPermissionLevelDecorator('admin')
298 @HasRepoGroupPermissionLevelDecorator('admin')
300 def edit(self, group_name):
299 def edit(self, group_name):
301 c.active = 'settings'
300 c.active = 'settings'
302
301
303 c.repo_group = RepoGroup.guess_instance(group_name)
302 c.repo_group = RepoGroup.guess_instance(group_name)
304 self.__load_defaults(extras=[c.repo_group.parent_group],
303 self.__load_defaults(extras=[c.repo_group.parent_group],
305 exclude=[c.repo_group])
304 exclude=[c.repo_group])
306 defaults = self.__load_data(c.repo_group.group_id)
305 defaults = self.__load_data(c.repo_group.group_id)
307
306
308 return htmlfill.render(
307 return htmlfill.render(
309 render('admin/repo_groups/repo_group_edit.html'),
308 render('admin/repo_groups/repo_group_edit.html'),
310 defaults=defaults,
309 defaults=defaults,
311 encoding="UTF-8",
310 encoding="UTF-8",
312 force_defaults=False
311 force_defaults=False
313 )
312 )
314
313
315 @HasRepoGroupPermissionLevelDecorator('admin')
314 @HasRepoGroupPermissionLevelDecorator('admin')
316 def edit_repo_group_advanced(self, group_name):
315 def edit_repo_group_advanced(self, group_name):
317 c.active = 'advanced'
316 c.active = 'advanced'
318 c.repo_group = RepoGroup.guess_instance(group_name)
317 c.repo_group = RepoGroup.guess_instance(group_name)
319
318
320 return render('admin/repo_groups/repo_group_edit.html')
319 return render('admin/repo_groups/repo_group_edit.html')
321
320
322 @HasRepoGroupPermissionLevelDecorator('admin')
321 @HasRepoGroupPermissionLevelDecorator('admin')
323 def edit_repo_group_perms(self, group_name):
322 def edit_repo_group_perms(self, group_name):
324 c.active = 'perms'
323 c.active = 'perms'
325 c.repo_group = RepoGroup.guess_instance(group_name)
324 c.repo_group = RepoGroup.guess_instance(group_name)
326 self.__load_defaults()
325 self.__load_defaults()
327 defaults = self.__load_data(c.repo_group.group_id)
326 defaults = self.__load_data(c.repo_group.group_id)
328
327
329 return htmlfill.render(
328 return htmlfill.render(
330 render('admin/repo_groups/repo_group_edit.html'),
329 render('admin/repo_groups/repo_group_edit.html'),
331 defaults=defaults,
330 defaults=defaults,
332 encoding="UTF-8",
331 encoding="UTF-8",
333 force_defaults=False
332 force_defaults=False
334 )
333 )
335
334
336 @HasRepoGroupPermissionLevelDecorator('admin')
335 @HasRepoGroupPermissionLevelDecorator('admin')
337 def update_perms(self, group_name):
336 def update_perms(self, group_name):
338 """
337 """
339 Update permissions for given repository group
338 Update permissions for given repository group
340
339
341 :param group_name:
340 :param group_name:
342 """
341 """
343
342
344 c.repo_group = RepoGroup.guess_instance(group_name)
343 c.repo_group = RepoGroup.guess_instance(group_name)
345 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
344 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
346 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
345 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
347 if not request.authuser.is_admin:
346 if not request.authuser.is_admin:
348 if self._revoke_perms_on_yourself(form_result):
347 if self._revoke_perms_on_yourself(form_result):
349 msg = _('Cannot revoke permission for yourself as admin')
348 msg = _('Cannot revoke permission for yourself as admin')
350 h.flash(msg, category='warning')
349 h.flash(msg, category='warning')
351 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
350 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
352 recursive = form_result['recursive']
351 recursive = form_result['recursive']
353 # iterate over all members(if in recursive mode) of this groups and
352 # iterate over all members(if in recursive mode) of this groups and
354 # set the permissions !
353 # set the permissions !
355 # this can be potentially heavy operation
354 # this can be potentially heavy operation
356 RepoGroupModel()._update_permissions(c.repo_group,
355 RepoGroupModel()._update_permissions(c.repo_group,
357 form_result['perms_new'],
356 form_result['perms_new'],
358 form_result['perms_updates'],
357 form_result['perms_updates'],
359 recursive)
358 recursive)
360 # TODO: implement this
359 # TODO: implement this
361 #action_logger(request.authuser, 'admin_changed_repo_permissions',
360 #action_logger(request.authuser, 'admin_changed_repo_permissions',
362 # repo_name, request.ip_addr)
361 # repo_name, request.ip_addr)
363 Session().commit()
362 Session().commit()
364 h.flash(_('Repository group permissions updated'), category='success')
363 h.flash(_('Repository group permissions updated'), category='success')
365 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
364 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
366
365
367 @HasRepoGroupPermissionLevelDecorator('admin')
366 @HasRepoGroupPermissionLevelDecorator('admin')
368 def delete_perms(self, group_name):
367 def delete_perms(self, group_name):
369 try:
368 try:
370 obj_type = request.POST.get('obj_type')
369 obj_type = request.POST.get('obj_type')
371 obj_id = None
370 obj_id = None
372 if obj_type == 'user':
371 if obj_type == 'user':
373 obj_id = safe_int(request.POST.get('user_id'))
372 obj_id = safe_int(request.POST.get('user_id'))
374 elif obj_type == 'user_group':
373 elif obj_type == 'user_group':
375 obj_id = safe_int(request.POST.get('user_group_id'))
374 obj_id = safe_int(request.POST.get('user_group_id'))
376
375
377 if not request.authuser.is_admin:
376 if not request.authuser.is_admin:
378 if obj_type == 'user' and request.authuser.user_id == obj_id:
377 if obj_type == 'user' and request.authuser.user_id == obj_id:
379 msg = _('Cannot revoke permission for yourself as admin')
378 msg = _('Cannot revoke permission for yourself as admin')
380 h.flash(msg, category='warning')
379 h.flash(msg, category='warning')
381 raise Exception('revoke admin permission on self')
380 raise Exception('revoke admin permission on self')
382 recursive = request.POST.get('recursive', 'none')
381 recursive = request.POST.get('recursive', 'none')
383 if obj_type == 'user':
382 if obj_type == 'user':
384 RepoGroupModel().delete_permission(repo_group=group_name,
383 RepoGroupModel().delete_permission(repo_group=group_name,
385 obj=obj_id, obj_type='user',
384 obj=obj_id, obj_type='user',
386 recursive=recursive)
385 recursive=recursive)
387 elif obj_type == 'user_group':
386 elif obj_type == 'user_group':
388 RepoGroupModel().delete_permission(repo_group=group_name,
387 RepoGroupModel().delete_permission(repo_group=group_name,
389 obj=obj_id,
388 obj=obj_id,
390 obj_type='user_group',
389 obj_type='user_group',
391 recursive=recursive)
390 recursive=recursive)
392
391
393 Session().commit()
392 Session().commit()
394 except Exception:
393 except Exception:
395 log.error(traceback.format_exc())
394 log.error(traceback.format_exc())
396 h.flash(_('An error occurred during revoking of permission'),
395 h.flash(_('An error occurred during revoking of permission'),
397 category='error')
396 category='error')
398 raise HTTPInternalServerError()
397 raise HTTPInternalServerError()
@@ -1,411 +1,410 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.user_groups
15 kallithea.controllers.admin.user_groups
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 User Groups crud controller
18 User Groups crud controller
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jan 25, 2011
22 :created_on: Jan 25, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from sqlalchemy.orm import joinedload
33 from sqlalchemy.orm import joinedload
34 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35 from tg import app_globals, request
35 from tg import app_globals, request
36 from tg import tmpl_context as c
36 from tg import tmpl_context as c
37 from tg.i18n import ugettext as _
37 from tg.i18n import ugettext as _
38 from webob.exc import HTTPFound, HTTPInternalServerError
38 from webob.exc import HTTPFound, HTTPInternalServerError
39
39
40 from kallithea.config.routing import url
40 from kallithea.config.routing import url
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired
42 from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired
43 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.base import BaseController, render
44 from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException
44 from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException
45 from kallithea.lib.utils import action_logger
45 from kallithea.lib.utils import action_logger
46 from kallithea.lib.utils2 import safe_int, safe_str
46 from kallithea.lib.utils2 import safe_int, safe_str
47 from kallithea.model.db import User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm
47 from kallithea.model.db import User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm
48 from kallithea.model.forms import CustomDefaultPermissionsForm, UserGroupForm, UserGroupPermsForm
48 from kallithea.model.forms import CustomDefaultPermissionsForm, UserGroupForm, UserGroupPermsForm
49 from kallithea.model.meta import Session
49 from kallithea.model.meta import Session
50 from kallithea.model.scm import UserGroupList
50 from kallithea.model.scm import UserGroupList
51 from kallithea.model.user_group import UserGroupModel
51 from kallithea.model.user_group import UserGroupModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class UserGroupsController(BaseController):
57 class UserGroupsController(BaseController):
58 """REST Controller styled on the Atom Publishing Protocol"""
58 """REST Controller styled on the Atom Publishing Protocol"""
59
59
60 @LoginRequired(allow_default_user=True)
60 @LoginRequired(allow_default_user=True)
61 def _before(self, *args, **kwargs):
61 def _before(self, *args, **kwargs):
62 super(UserGroupsController, self)._before(*args, **kwargs)
62 super(UserGroupsController, self)._before(*args, **kwargs)
63
63
64 def __load_data(self, user_group_id):
64 def __load_data(self, user_group_id):
65 c.group_members_obj = sorted((x.user for x in c.user_group.members),
65 c.group_members_obj = sorted((x.user for x in c.user_group.members),
66 key=lambda u: u.username.lower())
66 key=lambda u: u.username.lower())
67
67
68 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
68 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
69 c.available_members = sorted(((x.user_id, x.username) for x in
69 c.available_members = sorted(((x.user_id, x.username) for x in
70 User.query().all()),
70 User.query().all()),
71 key=lambda u: u[1].lower())
71 key=lambda u: u[1].lower())
72
72
73 def __load_defaults(self, user_group_id):
73 def __load_defaults(self, user_group_id):
74 """
74 """
75 Load defaults settings for edit, and update
75 Load defaults settings for edit, and update
76
76
77 :param user_group_id:
77 :param user_group_id:
78 """
78 """
79 user_group = UserGroup.get_or_404(user_group_id)
79 user_group = UserGroup.get_or_404(user_group_id)
80 data = user_group.get_dict()
80 data = user_group.get_dict()
81 return data
81 return data
82
82
83 def index(self, format='html'):
83 def index(self, format='html'):
84 _list = UserGroup.query() \
84 _list = UserGroup.query() \
85 .order_by(func.lower(UserGroup.users_group_name)) \
85 .order_by(func.lower(UserGroup.users_group_name)) \
86 .all()
86 .all()
87 group_iter = UserGroupList(_list, perm_level='admin')
87 group_iter = UserGroupList(_list, perm_level='admin')
88 user_groups_data = []
88 user_groups_data = []
89 total_records = len(group_iter)
90 _tmpl_lookup = app_globals.mako_lookup
89 _tmpl_lookup = app_globals.mako_lookup
91 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
90 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
92
91
93 user_group_name = lambda user_group_id, user_group_name: (
92 user_group_name = lambda user_group_id, user_group_name: (
94 template.get_def("user_group_name")
93 template.get_def("user_group_name")
95 .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
94 .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
96 )
95 )
97 user_group_actions = lambda user_group_id, user_group_name: (
96 user_group_actions = lambda user_group_id, user_group_name: (
98 template.get_def("user_group_actions")
97 template.get_def("user_group_actions")
99 .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
98 .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
100 )
99 )
101 for user_gr in group_iter:
100 for user_gr in group_iter:
102
101
103 user_groups_data.append({
102 user_groups_data.append({
104 "raw_name": user_gr.users_group_name,
103 "raw_name": user_gr.users_group_name,
105 "group_name": user_group_name(user_gr.users_group_id,
104 "group_name": user_group_name(user_gr.users_group_id,
106 user_gr.users_group_name),
105 user_gr.users_group_name),
107 "desc": h.escape(user_gr.user_group_description),
106 "desc": h.escape(user_gr.user_group_description),
108 "members": len(user_gr.members),
107 "members": len(user_gr.members),
109 "active": h.boolicon(user_gr.users_group_active),
108 "active": h.boolicon(user_gr.users_group_active),
110 "owner": h.person(user_gr.owner.username),
109 "owner": h.person(user_gr.owner.username),
111 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
110 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
112 })
111 })
113
112
114 c.data = {
113 c.data = {
115 "sort": None,
114 "sort": None,
116 "dir": "asc",
115 "dir": "asc",
117 "records": user_groups_data
116 "records": user_groups_data
118 }
117 }
119
118
120 return render('admin/user_groups/user_groups.html')
119 return render('admin/user_groups/user_groups.html')
121
120
122 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
121 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
123 def create(self):
122 def create(self):
124 users_group_form = UserGroupForm()()
123 users_group_form = UserGroupForm()()
125 try:
124 try:
126 form_result = users_group_form.to_python(dict(request.POST))
125 form_result = users_group_form.to_python(dict(request.POST))
127 ug = UserGroupModel().create(name=form_result['users_group_name'],
126 ug = UserGroupModel().create(name=form_result['users_group_name'],
128 description=form_result['user_group_description'],
127 description=form_result['user_group_description'],
129 owner=request.authuser.user_id,
128 owner=request.authuser.user_id,
130 active=form_result['users_group_active'])
129 active=form_result['users_group_active'])
131
130
132 gr = form_result['users_group_name']
131 gr = form_result['users_group_name']
133 action_logger(request.authuser,
132 action_logger(request.authuser,
134 'admin_created_users_group:%s' % gr,
133 'admin_created_users_group:%s' % gr,
135 None, request.ip_addr)
134 None, request.ip_addr)
136 h.flash(h.HTML(_('Created user group %s')) % h.link_to(gr, url('edit_users_group', id=ug.users_group_id)),
135 h.flash(h.HTML(_('Created user group %s')) % h.link_to(gr, url('edit_users_group', id=ug.users_group_id)),
137 category='success')
136 category='success')
138 Session().commit()
137 Session().commit()
139 except formencode.Invalid as errors:
138 except formencode.Invalid as errors:
140 return htmlfill.render(
139 return htmlfill.render(
141 render('admin/user_groups/user_group_add.html'),
140 render('admin/user_groups/user_group_add.html'),
142 defaults=errors.value,
141 defaults=errors.value,
143 errors=errors.error_dict or {},
142 errors=errors.error_dict or {},
144 prefix_error=False,
143 prefix_error=False,
145 encoding="UTF-8",
144 encoding="UTF-8",
146 force_defaults=False)
145 force_defaults=False)
147 except Exception:
146 except Exception:
148 log.error(traceback.format_exc())
147 log.error(traceback.format_exc())
149 h.flash(_('Error occurred during creation of user group %s')
148 h.flash(_('Error occurred during creation of user group %s')
150 % request.POST.get('users_group_name'), category='error')
149 % request.POST.get('users_group_name'), category='error')
151
150
152 raise HTTPFound(location=url('users_groups'))
151 raise HTTPFound(location=url('users_groups'))
153
152
154 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
153 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
155 def new(self, format='html'):
154 def new(self, format='html'):
156 return render('admin/user_groups/user_group_add.html')
155 return render('admin/user_groups/user_group_add.html')
157
156
158 @HasUserGroupPermissionLevelDecorator('admin')
157 @HasUserGroupPermissionLevelDecorator('admin')
159 def update(self, id):
158 def update(self, id):
160 c.user_group = UserGroup.get_or_404(id)
159 c.user_group = UserGroup.get_or_404(id)
161 c.active = 'settings'
160 c.active = 'settings'
162 self.__load_data(id)
161 self.__load_data(id)
163
162
164 available_members = [safe_str(x[0]) for x in c.available_members]
163 available_members = [safe_str(x[0]) for x in c.available_members]
165
164
166 users_group_form = UserGroupForm(edit=True,
165 users_group_form = UserGroupForm(edit=True,
167 old_data=c.user_group.get_dict(),
166 old_data=c.user_group.get_dict(),
168 available_members=available_members)()
167 available_members=available_members)()
169
168
170 try:
169 try:
171 form_result = users_group_form.to_python(request.POST)
170 form_result = users_group_form.to_python(request.POST)
172 UserGroupModel().update(c.user_group, form_result)
171 UserGroupModel().update(c.user_group, form_result)
173 gr = form_result['users_group_name']
172 gr = form_result['users_group_name']
174 action_logger(request.authuser,
173 action_logger(request.authuser,
175 'admin_updated_users_group:%s' % gr,
174 'admin_updated_users_group:%s' % gr,
176 None, request.ip_addr)
175 None, request.ip_addr)
177 h.flash(_('Updated user group %s') % gr, category='success')
176 h.flash(_('Updated user group %s') % gr, category='success')
178 Session().commit()
177 Session().commit()
179 except formencode.Invalid as errors:
178 except formencode.Invalid as errors:
180 ug_model = UserGroupModel()
179 ug_model = UserGroupModel()
181 defaults = errors.value
180 defaults = errors.value
182 e = errors.error_dict or {}
181 e = errors.error_dict or {}
183 defaults.update({
182 defaults.update({
184 'create_repo_perm': ug_model.has_perm(id,
183 'create_repo_perm': ug_model.has_perm(id,
185 'hg.create.repository'),
184 'hg.create.repository'),
186 'fork_repo_perm': ug_model.has_perm(id,
185 'fork_repo_perm': ug_model.has_perm(id,
187 'hg.fork.repository'),
186 'hg.fork.repository'),
188 })
187 })
189
188
190 return htmlfill.render(
189 return htmlfill.render(
191 render('admin/user_groups/user_group_edit.html'),
190 render('admin/user_groups/user_group_edit.html'),
192 defaults=defaults,
191 defaults=defaults,
193 errors=e,
192 errors=e,
194 prefix_error=False,
193 prefix_error=False,
195 encoding="UTF-8",
194 encoding="UTF-8",
196 force_defaults=False)
195 force_defaults=False)
197 except Exception:
196 except Exception:
198 log.error(traceback.format_exc())
197 log.error(traceback.format_exc())
199 h.flash(_('Error occurred during update of user group %s')
198 h.flash(_('Error occurred during update of user group %s')
200 % request.POST.get('users_group_name'), category='error')
199 % request.POST.get('users_group_name'), category='error')
201
200
202 raise HTTPFound(location=url('edit_users_group', id=id))
201 raise HTTPFound(location=url('edit_users_group', id=id))
203
202
204 @HasUserGroupPermissionLevelDecorator('admin')
203 @HasUserGroupPermissionLevelDecorator('admin')
205 def delete(self, id):
204 def delete(self, id):
206 usr_gr = UserGroup.get_or_404(id)
205 usr_gr = UserGroup.get_or_404(id)
207 try:
206 try:
208 UserGroupModel().delete(usr_gr)
207 UserGroupModel().delete(usr_gr)
209 Session().commit()
208 Session().commit()
210 h.flash(_('Successfully deleted user group'), category='success')
209 h.flash(_('Successfully deleted user group'), category='success')
211 except UserGroupsAssignedException as e:
210 except UserGroupsAssignedException as e:
212 h.flash(e, category='error')
211 h.flash(e, category='error')
213 except Exception:
212 except Exception:
214 log.error(traceback.format_exc())
213 log.error(traceback.format_exc())
215 h.flash(_('An error occurred during deletion of user group'),
214 h.flash(_('An error occurred during deletion of user group'),
216 category='error')
215 category='error')
217 raise HTTPFound(location=url('users_groups'))
216 raise HTTPFound(location=url('users_groups'))
218
217
219 @HasUserGroupPermissionLevelDecorator('admin')
218 @HasUserGroupPermissionLevelDecorator('admin')
220 def edit(self, id, format='html'):
219 def edit(self, id, format='html'):
221 c.user_group = UserGroup.get_or_404(id)
220 c.user_group = UserGroup.get_or_404(id)
222 c.active = 'settings'
221 c.active = 'settings'
223 self.__load_data(id)
222 self.__load_data(id)
224
223
225 defaults = self.__load_defaults(id)
224 defaults = self.__load_defaults(id)
226
225
227 return htmlfill.render(
226 return htmlfill.render(
228 render('admin/user_groups/user_group_edit.html'),
227 render('admin/user_groups/user_group_edit.html'),
229 defaults=defaults,
228 defaults=defaults,
230 encoding="UTF-8",
229 encoding="UTF-8",
231 force_defaults=False
230 force_defaults=False
232 )
231 )
233
232
234 @HasUserGroupPermissionLevelDecorator('admin')
233 @HasUserGroupPermissionLevelDecorator('admin')
235 def edit_perms(self, id):
234 def edit_perms(self, id):
236 c.user_group = UserGroup.get_or_404(id)
235 c.user_group = UserGroup.get_or_404(id)
237 c.active = 'perms'
236 c.active = 'perms'
238
237
239 defaults = {}
238 defaults = {}
240 # fill user group users
239 # fill user group users
241 for p in c.user_group.user_user_group_to_perm:
240 for p in c.user_group.user_user_group_to_perm:
242 defaults.update({'u_perm_%s' % p.user.username:
241 defaults.update({'u_perm_%s' % p.user.username:
243 p.permission.permission_name})
242 p.permission.permission_name})
244
243
245 for p in c.user_group.user_group_user_group_to_perm:
244 for p in c.user_group.user_group_user_group_to_perm:
246 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
245 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
247 p.permission.permission_name})
246 p.permission.permission_name})
248
247
249 return htmlfill.render(
248 return htmlfill.render(
250 render('admin/user_groups/user_group_edit.html'),
249 render('admin/user_groups/user_group_edit.html'),
251 defaults=defaults,
250 defaults=defaults,
252 encoding="UTF-8",
251 encoding="UTF-8",
253 force_defaults=False
252 force_defaults=False
254 )
253 )
255
254
256 @HasUserGroupPermissionLevelDecorator('admin')
255 @HasUserGroupPermissionLevelDecorator('admin')
257 def update_perms(self, id):
256 def update_perms(self, id):
258 """
257 """
259 grant permission for given usergroup
258 grant permission for given usergroup
260
259
261 :param id:
260 :param id:
262 """
261 """
263 user_group = UserGroup.get_or_404(id)
262 user_group = UserGroup.get_or_404(id)
264 form = UserGroupPermsForm()().to_python(request.POST)
263 form = UserGroupPermsForm()().to_python(request.POST)
265
264
266 # set the permissions !
265 # set the permissions !
267 try:
266 try:
268 UserGroupModel()._update_permissions(user_group, form['perms_new'],
267 UserGroupModel()._update_permissions(user_group, form['perms_new'],
269 form['perms_updates'])
268 form['perms_updates'])
270 except RepoGroupAssignmentError:
269 except RepoGroupAssignmentError:
271 h.flash(_('Target group cannot be the same'), category='error')
270 h.flash(_('Target group cannot be the same'), category='error')
272 raise HTTPFound(location=url('edit_user_group_perms', id=id))
271 raise HTTPFound(location=url('edit_user_group_perms', id=id))
273 # TODO: implement this
272 # TODO: implement this
274 #action_logger(request.authuser, 'admin_changed_repo_permissions',
273 #action_logger(request.authuser, 'admin_changed_repo_permissions',
275 # repo_name, request.ip_addr)
274 # repo_name, request.ip_addr)
276 Session().commit()
275 Session().commit()
277 h.flash(_('User group permissions updated'), category='success')
276 h.flash(_('User group permissions updated'), category='success')
278 raise HTTPFound(location=url('edit_user_group_perms', id=id))
277 raise HTTPFound(location=url('edit_user_group_perms', id=id))
279
278
280 @HasUserGroupPermissionLevelDecorator('admin')
279 @HasUserGroupPermissionLevelDecorator('admin')
281 def delete_perms(self, id):
280 def delete_perms(self, id):
282 try:
281 try:
283 obj_type = request.POST.get('obj_type')
282 obj_type = request.POST.get('obj_type')
284 obj_id = None
283 obj_id = None
285 if obj_type == 'user':
284 if obj_type == 'user':
286 obj_id = safe_int(request.POST.get('user_id'))
285 obj_id = safe_int(request.POST.get('user_id'))
287 elif obj_type == 'user_group':
286 elif obj_type == 'user_group':
288 obj_id = safe_int(request.POST.get('user_group_id'))
287 obj_id = safe_int(request.POST.get('user_group_id'))
289
288
290 if not request.authuser.is_admin:
289 if not request.authuser.is_admin:
291 if obj_type == 'user' and request.authuser.user_id == obj_id:
290 if obj_type == 'user' and request.authuser.user_id == obj_id:
292 msg = _('Cannot revoke permission for yourself as admin')
291 msg = _('Cannot revoke permission for yourself as admin')
293 h.flash(msg, category='warning')
292 h.flash(msg, category='warning')
294 raise Exception('revoke admin permission on self')
293 raise Exception('revoke admin permission on self')
295 if obj_type == 'user':
294 if obj_type == 'user':
296 UserGroupModel().revoke_user_permission(user_group=id,
295 UserGroupModel().revoke_user_permission(user_group=id,
297 user=obj_id)
296 user=obj_id)
298 elif obj_type == 'user_group':
297 elif obj_type == 'user_group':
299 UserGroupModel().revoke_user_group_permission(target_user_group=id,
298 UserGroupModel().revoke_user_group_permission(target_user_group=id,
300 user_group=obj_id)
299 user_group=obj_id)
301 Session().commit()
300 Session().commit()
302 except Exception:
301 except Exception:
303 log.error(traceback.format_exc())
302 log.error(traceback.format_exc())
304 h.flash(_('An error occurred during revoking of permission'),
303 h.flash(_('An error occurred during revoking of permission'),
305 category='error')
304 category='error')
306 raise HTTPInternalServerError()
305 raise HTTPInternalServerError()
307
306
308 @HasUserGroupPermissionLevelDecorator('admin')
307 @HasUserGroupPermissionLevelDecorator('admin')
309 def edit_default_perms(self, id):
308 def edit_default_perms(self, id):
310 c.user_group = UserGroup.get_or_404(id)
309 c.user_group = UserGroup.get_or_404(id)
311 c.active = 'default_perms'
310 c.active = 'default_perms'
312
311
313 permissions = {
312 permissions = {
314 'repositories': {},
313 'repositories': {},
315 'repositories_groups': {}
314 'repositories_groups': {}
316 }
315 }
317 ugroup_repo_perms = UserGroupRepoToPerm.query() \
316 ugroup_repo_perms = UserGroupRepoToPerm.query() \
318 .options(joinedload(UserGroupRepoToPerm.permission)) \
317 .options(joinedload(UserGroupRepoToPerm.permission)) \
319 .options(joinedload(UserGroupRepoToPerm.repository)) \
318 .options(joinedload(UserGroupRepoToPerm.repository)) \
320 .filter(UserGroupRepoToPerm.users_group_id == id) \
319 .filter(UserGroupRepoToPerm.users_group_id == id) \
321 .all()
320 .all()
322
321
323 for gr in ugroup_repo_perms:
322 for gr in ugroup_repo_perms:
324 permissions['repositories'][gr.repository.repo_name] \
323 permissions['repositories'][gr.repository.repo_name] \
325 = gr.permission.permission_name
324 = gr.permission.permission_name
326
325
327 ugroup_group_perms = UserGroupRepoGroupToPerm.query() \
326 ugroup_group_perms = UserGroupRepoGroupToPerm.query() \
328 .options(joinedload(UserGroupRepoGroupToPerm.permission)) \
327 .options(joinedload(UserGroupRepoGroupToPerm.permission)) \
329 .options(joinedload(UserGroupRepoGroupToPerm.group)) \
328 .options(joinedload(UserGroupRepoGroupToPerm.group)) \
330 .filter(UserGroupRepoGroupToPerm.users_group_id == id) \
329 .filter(UserGroupRepoGroupToPerm.users_group_id == id) \
331 .all()
330 .all()
332
331
333 for gr in ugroup_group_perms:
332 for gr in ugroup_group_perms:
334 permissions['repositories_groups'][gr.group.group_name] \
333 permissions['repositories_groups'][gr.group.group_name] \
335 = gr.permission.permission_name
334 = gr.permission.permission_name
336 c.permissions = permissions
335 c.permissions = permissions
337
336
338 ug_model = UserGroupModel()
337 ug_model = UserGroupModel()
339
338
340 defaults = c.user_group.get_dict()
339 defaults = c.user_group.get_dict()
341 defaults.update({
340 defaults.update({
342 'create_repo_perm': ug_model.has_perm(c.user_group,
341 'create_repo_perm': ug_model.has_perm(c.user_group,
343 'hg.create.repository'),
342 'hg.create.repository'),
344 'create_user_group_perm': ug_model.has_perm(c.user_group,
343 'create_user_group_perm': ug_model.has_perm(c.user_group,
345 'hg.usergroup.create.true'),
344 'hg.usergroup.create.true'),
346 'fork_repo_perm': ug_model.has_perm(c.user_group,
345 'fork_repo_perm': ug_model.has_perm(c.user_group,
347 'hg.fork.repository'),
346 'hg.fork.repository'),
348 })
347 })
349
348
350 return htmlfill.render(
349 return htmlfill.render(
351 render('admin/user_groups/user_group_edit.html'),
350 render('admin/user_groups/user_group_edit.html'),
352 defaults=defaults,
351 defaults=defaults,
353 encoding="UTF-8",
352 encoding="UTF-8",
354 force_defaults=False
353 force_defaults=False
355 )
354 )
356
355
357 @HasUserGroupPermissionLevelDecorator('admin')
356 @HasUserGroupPermissionLevelDecorator('admin')
358 def update_default_perms(self, id):
357 def update_default_perms(self, id):
359 user_group = UserGroup.get_or_404(id)
358 user_group = UserGroup.get_or_404(id)
360
359
361 try:
360 try:
362 form = CustomDefaultPermissionsForm()()
361 form = CustomDefaultPermissionsForm()()
363 form_result = form.to_python(request.POST)
362 form_result = form.to_python(request.POST)
364
363
365 usergroup_model = UserGroupModel()
364 usergroup_model = UserGroupModel()
366
365
367 defs = UserGroupToPerm.query() \
366 defs = UserGroupToPerm.query() \
368 .filter(UserGroupToPerm.users_group == user_group) \
367 .filter(UserGroupToPerm.users_group == user_group) \
369 .all()
368 .all()
370 for ug in defs:
369 for ug in defs:
371 Session().delete(ug)
370 Session().delete(ug)
372
371
373 if form_result['create_repo_perm']:
372 if form_result['create_repo_perm']:
374 usergroup_model.grant_perm(id, 'hg.create.repository')
373 usergroup_model.grant_perm(id, 'hg.create.repository')
375 else:
374 else:
376 usergroup_model.grant_perm(id, 'hg.create.none')
375 usergroup_model.grant_perm(id, 'hg.create.none')
377 if form_result['create_user_group_perm']:
376 if form_result['create_user_group_perm']:
378 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
377 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
379 else:
378 else:
380 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
379 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
381 if form_result['fork_repo_perm']:
380 if form_result['fork_repo_perm']:
382 usergroup_model.grant_perm(id, 'hg.fork.repository')
381 usergroup_model.grant_perm(id, 'hg.fork.repository')
383 else:
382 else:
384 usergroup_model.grant_perm(id, 'hg.fork.none')
383 usergroup_model.grant_perm(id, 'hg.fork.none')
385
384
386 h.flash(_("Updated permissions"), category='success')
385 h.flash(_("Updated permissions"), category='success')
387 Session().commit()
386 Session().commit()
388 except Exception:
387 except Exception:
389 log.error(traceback.format_exc())
388 log.error(traceback.format_exc())
390 h.flash(_('An error occurred during permissions saving'),
389 h.flash(_('An error occurred during permissions saving'),
391 category='error')
390 category='error')
392
391
393 raise HTTPFound(location=url('edit_user_group_default_perms', id=id))
392 raise HTTPFound(location=url('edit_user_group_default_perms', id=id))
394
393
395 @HasUserGroupPermissionLevelDecorator('admin')
394 @HasUserGroupPermissionLevelDecorator('admin')
396 def edit_advanced(self, id):
395 def edit_advanced(self, id):
397 c.user_group = UserGroup.get_or_404(id)
396 c.user_group = UserGroup.get_or_404(id)
398 c.active = 'advanced'
397 c.active = 'advanced'
399 c.group_members_obj = sorted((x.user for x in c.user_group.members),
398 c.group_members_obj = sorted((x.user for x in c.user_group.members),
400 key=lambda u: u.username.lower())
399 key=lambda u: u.username.lower())
401 return render('admin/user_groups/user_group_edit.html')
400 return render('admin/user_groups/user_group_edit.html')
402
401
403 @HasUserGroupPermissionLevelDecorator('admin')
402 @HasUserGroupPermissionLevelDecorator('admin')
404 def edit_members(self, id):
403 def edit_members(self, id):
405 c.user_group = UserGroup.get_or_404(id)
404 c.user_group = UserGroup.get_or_404(id)
406 c.active = 'members'
405 c.active = 'members'
407 c.group_members_obj = sorted((x.user for x in c.user_group.members),
406 c.group_members_obj = sorted((x.user for x in c.user_group.members),
408 key=lambda u: u.username.lower())
407 key=lambda u: u.username.lower())
409
408
410 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
409 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
411 return render('admin/user_groups/user_group_edit.html')
410 return render('admin/user_groups/user_group_edit.html')
@@ -1,471 +1,470 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
18 Users crud controller
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
30
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from sqlalchemy.sql.expression import func
33 from sqlalchemy.sql.expression import func
34 from tg import app_globals, request
34 from tg import app_globals, request
35 from tg import tmpl_context as c
35 from tg import tmpl_context as c
36 from tg.i18n import ugettext as _
36 from tg.i18n import ugettext as _
37 from webob.exc import HTTPFound, HTTPNotFound
37 from webob.exc import HTTPFound, HTTPNotFound
38
38
39 from kallithea.config.routing import url
39 from kallithea.config.routing import url
40 from kallithea.lib import auth_modules
40 from kallithea.lib import auth_modules
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired
42 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired
43 from kallithea.lib.base import BaseController, IfSshEnabled, render
43 from kallithea.lib.base import BaseController, IfSshEnabled, render
44 from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException
44 from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException
45 from kallithea.lib.utils import action_logger
45 from kallithea.lib.utils import action_logger
46 from kallithea.lib.utils2 import datetime_to_time, generate_api_key, safe_int
46 from kallithea.lib.utils2 import datetime_to_time, generate_api_key, safe_int
47 from kallithea.model.api_key import ApiKeyModel
47 from kallithea.model.api_key import ApiKeyModel
48 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
48 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
49 from kallithea.model.forms import CustomDefaultPermissionsForm, UserForm
49 from kallithea.model.forms import CustomDefaultPermissionsForm, UserForm
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51 from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException
51 from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException
52 from kallithea.model.user import UserModel
52 from kallithea.model.user import UserModel
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class UsersController(BaseController):
58 class UsersController(BaseController):
59 """REST Controller styled on the Atom Publishing Protocol"""
59 """REST Controller styled on the Atom Publishing Protocol"""
60
60
61 @LoginRequired()
61 @LoginRequired()
62 @HasPermissionAnyDecorator('hg.admin')
62 @HasPermissionAnyDecorator('hg.admin')
63 def _before(self, *args, **kwargs):
63 def _before(self, *args, **kwargs):
64 super(UsersController, self)._before(*args, **kwargs)
64 super(UsersController, self)._before(*args, **kwargs)
65
65
66 def index(self, format='html'):
66 def index(self, format='html'):
67 c.users_list = User.query().order_by(User.username) \
67 c.users_list = User.query().order_by(User.username) \
68 .filter_by(is_default_user=False) \
68 .filter_by(is_default_user=False) \
69 .order_by(func.lower(User.username)) \
69 .order_by(func.lower(User.username)) \
70 .all()
70 .all()
71
71
72 users_data = []
72 users_data = []
73 total_records = len(c.users_list)
74 _tmpl_lookup = app_globals.mako_lookup
73 _tmpl_lookup = app_globals.mako_lookup
75 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
74 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
76
75
77 grav_tmpl = '<div class="gravatar">%s</div>'
76 grav_tmpl = '<div class="gravatar">%s</div>'
78
77
79 username = lambda user_id, username: (
78 username = lambda user_id, username: (
80 template.get_def("user_name")
79 template.get_def("user_name")
81 .render_unicode(user_id, username, _=_, h=h, c=c))
80 .render_unicode(user_id, username, _=_, h=h, c=c))
82
81
83 user_actions = lambda user_id, username: (
82 user_actions = lambda user_id, username: (
84 template.get_def("user_actions")
83 template.get_def("user_actions")
85 .render_unicode(user_id, username, _=_, h=h, c=c))
84 .render_unicode(user_id, username, _=_, h=h, c=c))
86
85
87 for user in c.users_list:
86 for user in c.users_list:
88 users_data.append({
87 users_data.append({
89 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
88 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
90 "raw_name": user.username,
89 "raw_name": user.username,
91 "username": username(user.user_id, user.username),
90 "username": username(user.user_id, user.username),
92 "firstname": h.escape(user.name),
91 "firstname": h.escape(user.name),
93 "lastname": h.escape(user.lastname),
92 "lastname": h.escape(user.lastname),
94 "last_login": h.fmt_date(user.last_login),
93 "last_login": h.fmt_date(user.last_login),
95 "last_login_raw": datetime_to_time(user.last_login),
94 "last_login_raw": datetime_to_time(user.last_login),
96 "active": h.boolicon(user.active),
95 "active": h.boolicon(user.active),
97 "admin": h.boolicon(user.admin),
96 "admin": h.boolicon(user.admin),
98 "extern_type": user.extern_type,
97 "extern_type": user.extern_type,
99 "extern_name": user.extern_name,
98 "extern_name": user.extern_name,
100 "action": user_actions(user.user_id, user.username),
99 "action": user_actions(user.user_id, user.username),
101 })
100 })
102
101
103 c.data = {
102 c.data = {
104 "sort": None,
103 "sort": None,
105 "dir": "asc",
104 "dir": "asc",
106 "records": users_data
105 "records": users_data
107 }
106 }
108
107
109 return render('admin/users/users.html')
108 return render('admin/users/users.html')
110
109
111 def create(self):
110 def create(self):
112 c.default_extern_type = User.DEFAULT_AUTH_TYPE
111 c.default_extern_type = User.DEFAULT_AUTH_TYPE
113 c.default_extern_name = ''
112 c.default_extern_name = ''
114 user_model = UserModel()
113 user_model = UserModel()
115 user_form = UserForm()()
114 user_form = UserForm()()
116 try:
115 try:
117 form_result = user_form.to_python(dict(request.POST))
116 form_result = user_form.to_python(dict(request.POST))
118 user = user_model.create(form_result)
117 user = user_model.create(form_result)
119 action_logger(request.authuser, 'admin_created_user:%s' % user.username,
118 action_logger(request.authuser, 'admin_created_user:%s' % user.username,
120 None, request.ip_addr)
119 None, request.ip_addr)
121 h.flash(_('Created user %s') % user.username,
120 h.flash(_('Created user %s') % user.username,
122 category='success')
121 category='success')
123 Session().commit()
122 Session().commit()
124 except formencode.Invalid as errors:
123 except formencode.Invalid as errors:
125 return htmlfill.render(
124 return htmlfill.render(
126 render('admin/users/user_add.html'),
125 render('admin/users/user_add.html'),
127 defaults=errors.value,
126 defaults=errors.value,
128 errors=errors.error_dict or {},
127 errors=errors.error_dict or {},
129 prefix_error=False,
128 prefix_error=False,
130 encoding="UTF-8",
129 encoding="UTF-8",
131 force_defaults=False)
130 force_defaults=False)
132 except UserCreationError as e:
131 except UserCreationError as e:
133 h.flash(e, 'error')
132 h.flash(e, 'error')
134 except Exception:
133 except Exception:
135 log.error(traceback.format_exc())
134 log.error(traceback.format_exc())
136 h.flash(_('Error occurred during creation of user %s')
135 h.flash(_('Error occurred during creation of user %s')
137 % request.POST.get('username'), category='error')
136 % request.POST.get('username'), category='error')
138 raise HTTPFound(location=url('edit_user', id=user.user_id))
137 raise HTTPFound(location=url('edit_user', id=user.user_id))
139
138
140 def new(self, format='html'):
139 def new(self, format='html'):
141 c.default_extern_type = User.DEFAULT_AUTH_TYPE
140 c.default_extern_type = User.DEFAULT_AUTH_TYPE
142 c.default_extern_name = ''
141 c.default_extern_name = ''
143 return render('admin/users/user_add.html')
142 return render('admin/users/user_add.html')
144
143
145 def update(self, id):
144 def update(self, id):
146 user_model = UserModel()
145 user_model = UserModel()
147 user = user_model.get(id)
146 user = user_model.get(id)
148 _form = UserForm(edit=True, old_data={'user_id': id,
147 _form = UserForm(edit=True, old_data={'user_id': id,
149 'email': user.email})()
148 'email': user.email})()
150 form_result = {}
149 form_result = {}
151 try:
150 try:
152 form_result = _form.to_python(dict(request.POST))
151 form_result = _form.to_python(dict(request.POST))
153 skip_attrs = ['extern_type', 'extern_name',
152 skip_attrs = ['extern_type', 'extern_name',
154 ] + auth_modules.get_managed_fields(user)
153 ] + auth_modules.get_managed_fields(user)
155
154
156 user_model.update(id, form_result, skip_attrs=skip_attrs)
155 user_model.update(id, form_result, skip_attrs=skip_attrs)
157 usr = form_result['username']
156 usr = form_result['username']
158 action_logger(request.authuser, 'admin_updated_user:%s' % usr,
157 action_logger(request.authuser, 'admin_updated_user:%s' % usr,
159 None, request.ip_addr)
158 None, request.ip_addr)
160 h.flash(_('User updated successfully'), category='success')
159 h.flash(_('User updated successfully'), category='success')
161 Session().commit()
160 Session().commit()
162 except formencode.Invalid as errors:
161 except formencode.Invalid as errors:
163 defaults = errors.value
162 defaults = errors.value
164 e = errors.error_dict or {}
163 e = errors.error_dict or {}
165 defaults.update({
164 defaults.update({
166 'create_repo_perm': user_model.has_perm(id,
165 'create_repo_perm': user_model.has_perm(id,
167 'hg.create.repository'),
166 'hg.create.repository'),
168 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
167 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
169 })
168 })
170 return htmlfill.render(
169 return htmlfill.render(
171 self._render_edit_profile(user),
170 self._render_edit_profile(user),
172 defaults=defaults,
171 defaults=defaults,
173 errors=e,
172 errors=e,
174 prefix_error=False,
173 prefix_error=False,
175 encoding="UTF-8",
174 encoding="UTF-8",
176 force_defaults=False)
175 force_defaults=False)
177 except Exception:
176 except Exception:
178 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
179 h.flash(_('Error occurred during update of user %s')
178 h.flash(_('Error occurred during update of user %s')
180 % form_result.get('username'), category='error')
179 % form_result.get('username'), category='error')
181 raise HTTPFound(location=url('edit_user', id=id))
180 raise HTTPFound(location=url('edit_user', id=id))
182
181
183 def delete(self, id):
182 def delete(self, id):
184 usr = User.get_or_404(id)
183 usr = User.get_or_404(id)
185 try:
184 try:
186 UserModel().delete(usr)
185 UserModel().delete(usr)
187 Session().commit()
186 Session().commit()
188 h.flash(_('Successfully deleted user'), category='success')
187 h.flash(_('Successfully deleted user'), category='success')
189 except (UserOwnsReposException, DefaultUserException) as e:
188 except (UserOwnsReposException, DefaultUserException) as e:
190 h.flash(e, category='warning')
189 h.flash(e, category='warning')
191 except Exception:
190 except Exception:
192 log.error(traceback.format_exc())
191 log.error(traceback.format_exc())
193 h.flash(_('An error occurred during deletion of user'),
192 h.flash(_('An error occurred during deletion of user'),
194 category='error')
193 category='error')
195 raise HTTPFound(location=url('users'))
194 raise HTTPFound(location=url('users'))
196
195
197 def _get_user_or_raise_if_default(self, id):
196 def _get_user_or_raise_if_default(self, id):
198 try:
197 try:
199 return User.get_or_404(id, allow_default=False)
198 return User.get_or_404(id, allow_default=False)
200 except DefaultUserException:
199 except DefaultUserException:
201 h.flash(_("The default user cannot be edited"), category='warning')
200 h.flash(_("The default user cannot be edited"), category='warning')
202 raise HTTPNotFound
201 raise HTTPNotFound
203
202
204 def _render_edit_profile(self, user):
203 def _render_edit_profile(self, user):
205 c.user = user
204 c.user = user
206 c.active = 'profile'
205 c.active = 'profile'
207 c.perm_user = AuthUser(dbuser=user)
206 c.perm_user = AuthUser(dbuser=user)
208 managed_fields = auth_modules.get_managed_fields(user)
207 managed_fields = auth_modules.get_managed_fields(user)
209 c.readonly = lambda n: 'readonly' if n in managed_fields else None
208 c.readonly = lambda n: 'readonly' if n in managed_fields else None
210 return render('admin/users/user_edit.html')
209 return render('admin/users/user_edit.html')
211
210
212 def edit(self, id, format='html'):
211 def edit(self, id, format='html'):
213 user = self._get_user_or_raise_if_default(id)
212 user = self._get_user_or_raise_if_default(id)
214 defaults = user.get_dict()
213 defaults = user.get_dict()
215
214
216 return htmlfill.render(
215 return htmlfill.render(
217 self._render_edit_profile(user),
216 self._render_edit_profile(user),
218 defaults=defaults,
217 defaults=defaults,
219 encoding="UTF-8",
218 encoding="UTF-8",
220 force_defaults=False)
219 force_defaults=False)
221
220
222 def edit_advanced(self, id):
221 def edit_advanced(self, id):
223 c.user = self._get_user_or_raise_if_default(id)
222 c.user = self._get_user_or_raise_if_default(id)
224 c.active = 'advanced'
223 c.active = 'advanced'
225 c.perm_user = AuthUser(dbuser=c.user)
224 c.perm_user = AuthUser(dbuser=c.user)
226
225
227 umodel = UserModel()
226 umodel = UserModel()
228 defaults = c.user.get_dict()
227 defaults = c.user.get_dict()
229 defaults.update({
228 defaults.update({
230 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
229 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
231 'create_user_group_perm': umodel.has_perm(c.user,
230 'create_user_group_perm': umodel.has_perm(c.user,
232 'hg.usergroup.create.true'),
231 'hg.usergroup.create.true'),
233 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
232 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
234 })
233 })
235 return htmlfill.render(
234 return htmlfill.render(
236 render('admin/users/user_edit.html'),
235 render('admin/users/user_edit.html'),
237 defaults=defaults,
236 defaults=defaults,
238 encoding="UTF-8",
237 encoding="UTF-8",
239 force_defaults=False)
238 force_defaults=False)
240
239
241 def edit_api_keys(self, id):
240 def edit_api_keys(self, id):
242 c.user = self._get_user_or_raise_if_default(id)
241 c.user = self._get_user_or_raise_if_default(id)
243 c.active = 'api_keys'
242 c.active = 'api_keys'
244 show_expired = True
243 show_expired = True
245 c.lifetime_values = [
244 c.lifetime_values = [
246 (str(-1), _('Forever')),
245 (str(-1), _('Forever')),
247 (str(5), _('5 minutes')),
246 (str(5), _('5 minutes')),
248 (str(60), _('1 hour')),
247 (str(60), _('1 hour')),
249 (str(60 * 24), _('1 day')),
248 (str(60 * 24), _('1 day')),
250 (str(60 * 24 * 30), _('1 month')),
249 (str(60 * 24 * 30), _('1 month')),
251 ]
250 ]
252 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
251 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
253 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
252 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
254 show_expired=show_expired)
253 show_expired=show_expired)
255 defaults = c.user.get_dict()
254 defaults = c.user.get_dict()
256 return htmlfill.render(
255 return htmlfill.render(
257 render('admin/users/user_edit.html'),
256 render('admin/users/user_edit.html'),
258 defaults=defaults,
257 defaults=defaults,
259 encoding="UTF-8",
258 encoding="UTF-8",
260 force_defaults=False)
259 force_defaults=False)
261
260
262 def add_api_key(self, id):
261 def add_api_key(self, id):
263 c.user = self._get_user_or_raise_if_default(id)
262 c.user = self._get_user_or_raise_if_default(id)
264
263
265 lifetime = safe_int(request.POST.get('lifetime'), -1)
264 lifetime = safe_int(request.POST.get('lifetime'), -1)
266 description = request.POST.get('description')
265 description = request.POST.get('description')
267 ApiKeyModel().create(c.user.user_id, description, lifetime)
266 ApiKeyModel().create(c.user.user_id, description, lifetime)
268 Session().commit()
267 Session().commit()
269 h.flash(_("API key successfully created"), category='success')
268 h.flash(_("API key successfully created"), category='success')
270 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
269 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
271
270
272 def delete_api_key(self, id):
271 def delete_api_key(self, id):
273 c.user = self._get_user_or_raise_if_default(id)
272 c.user = self._get_user_or_raise_if_default(id)
274
273
275 api_key = request.POST.get('del_api_key')
274 api_key = request.POST.get('del_api_key')
276 if request.POST.get('del_api_key_builtin'):
275 if request.POST.get('del_api_key_builtin'):
277 c.user.api_key = generate_api_key()
276 c.user.api_key = generate_api_key()
278 Session().commit()
277 Session().commit()
279 h.flash(_("API key successfully reset"), category='success')
278 h.flash(_("API key successfully reset"), category='success')
280 elif api_key:
279 elif api_key:
281 ApiKeyModel().delete(api_key, c.user.user_id)
280 ApiKeyModel().delete(api_key, c.user.user_id)
282 Session().commit()
281 Session().commit()
283 h.flash(_("API key successfully deleted"), category='success')
282 h.flash(_("API key successfully deleted"), category='success')
284
283
285 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
284 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
286
285
287 def update_account(self, id):
286 def update_account(self, id):
288 pass
287 pass
289
288
290 def edit_perms(self, id):
289 def edit_perms(self, id):
291 c.user = self._get_user_or_raise_if_default(id)
290 c.user = self._get_user_or_raise_if_default(id)
292 c.active = 'perms'
291 c.active = 'perms'
293 c.perm_user = AuthUser(dbuser=c.user)
292 c.perm_user = AuthUser(dbuser=c.user)
294
293
295 umodel = UserModel()
294 umodel = UserModel()
296 defaults = c.user.get_dict()
295 defaults = c.user.get_dict()
297 defaults.update({
296 defaults.update({
298 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
297 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
299 'create_user_group_perm': umodel.has_perm(c.user,
298 'create_user_group_perm': umodel.has_perm(c.user,
300 'hg.usergroup.create.true'),
299 'hg.usergroup.create.true'),
301 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
300 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
302 })
301 })
303 return htmlfill.render(
302 return htmlfill.render(
304 render('admin/users/user_edit.html'),
303 render('admin/users/user_edit.html'),
305 defaults=defaults,
304 defaults=defaults,
306 encoding="UTF-8",
305 encoding="UTF-8",
307 force_defaults=False)
306 force_defaults=False)
308
307
309 def update_perms(self, id):
308 def update_perms(self, id):
310 user = self._get_user_or_raise_if_default(id)
309 user = self._get_user_or_raise_if_default(id)
311
310
312 try:
311 try:
313 form = CustomDefaultPermissionsForm()()
312 form = CustomDefaultPermissionsForm()()
314 form_result = form.to_python(request.POST)
313 form_result = form.to_python(request.POST)
315
314
316 user_model = UserModel()
315 user_model = UserModel()
317
316
318 defs = UserToPerm.query() \
317 defs = UserToPerm.query() \
319 .filter(UserToPerm.user == user) \
318 .filter(UserToPerm.user == user) \
320 .all()
319 .all()
321 for ug in defs:
320 for ug in defs:
322 Session().delete(ug)
321 Session().delete(ug)
323
322
324 if form_result['create_repo_perm']:
323 if form_result['create_repo_perm']:
325 user_model.grant_perm(id, 'hg.create.repository')
324 user_model.grant_perm(id, 'hg.create.repository')
326 else:
325 else:
327 user_model.grant_perm(id, 'hg.create.none')
326 user_model.grant_perm(id, 'hg.create.none')
328 if form_result['create_user_group_perm']:
327 if form_result['create_user_group_perm']:
329 user_model.grant_perm(id, 'hg.usergroup.create.true')
328 user_model.grant_perm(id, 'hg.usergroup.create.true')
330 else:
329 else:
331 user_model.grant_perm(id, 'hg.usergroup.create.false')
330 user_model.grant_perm(id, 'hg.usergroup.create.false')
332 if form_result['fork_repo_perm']:
331 if form_result['fork_repo_perm']:
333 user_model.grant_perm(id, 'hg.fork.repository')
332 user_model.grant_perm(id, 'hg.fork.repository')
334 else:
333 else:
335 user_model.grant_perm(id, 'hg.fork.none')
334 user_model.grant_perm(id, 'hg.fork.none')
336 h.flash(_("Updated permissions"), category='success')
335 h.flash(_("Updated permissions"), category='success')
337 Session().commit()
336 Session().commit()
338 except Exception:
337 except Exception:
339 log.error(traceback.format_exc())
338 log.error(traceback.format_exc())
340 h.flash(_('An error occurred during permissions saving'),
339 h.flash(_('An error occurred during permissions saving'),
341 category='error')
340 category='error')
342 raise HTTPFound(location=url('edit_user_perms', id=id))
341 raise HTTPFound(location=url('edit_user_perms', id=id))
343
342
344 def edit_emails(self, id):
343 def edit_emails(self, id):
345 c.user = self._get_user_or_raise_if_default(id)
344 c.user = self._get_user_or_raise_if_default(id)
346 c.active = 'emails'
345 c.active = 'emails'
347 c.user_email_map = UserEmailMap.query() \
346 c.user_email_map = UserEmailMap.query() \
348 .filter(UserEmailMap.user == c.user).all()
347 .filter(UserEmailMap.user == c.user).all()
349
348
350 defaults = c.user.get_dict()
349 defaults = c.user.get_dict()
351 return htmlfill.render(
350 return htmlfill.render(
352 render('admin/users/user_edit.html'),
351 render('admin/users/user_edit.html'),
353 defaults=defaults,
352 defaults=defaults,
354 encoding="UTF-8",
353 encoding="UTF-8",
355 force_defaults=False)
354 force_defaults=False)
356
355
357 def add_email(self, id):
356 def add_email(self, id):
358 user = self._get_user_or_raise_if_default(id)
357 user = self._get_user_or_raise_if_default(id)
359 email = request.POST.get('new_email')
358 email = request.POST.get('new_email')
360 user_model = UserModel()
359 user_model = UserModel()
361
360
362 try:
361 try:
363 user_model.add_extra_email(id, email)
362 user_model.add_extra_email(id, email)
364 Session().commit()
363 Session().commit()
365 h.flash(_("Added email %s to user") % email, category='success')
364 h.flash(_("Added email %s to user") % email, category='success')
366 except formencode.Invalid as error:
365 except formencode.Invalid as error:
367 msg = error.error_dict['email']
366 msg = error.error_dict['email']
368 h.flash(msg, category='error')
367 h.flash(msg, category='error')
369 except Exception:
368 except Exception:
370 log.error(traceback.format_exc())
369 log.error(traceback.format_exc())
371 h.flash(_('An error occurred during email saving'),
370 h.flash(_('An error occurred during email saving'),
372 category='error')
371 category='error')
373 raise HTTPFound(location=url('edit_user_emails', id=id))
372 raise HTTPFound(location=url('edit_user_emails', id=id))
374
373
375 def delete_email(self, id):
374 def delete_email(self, id):
376 user = self._get_user_or_raise_if_default(id)
375 user = self._get_user_or_raise_if_default(id)
377 email_id = request.POST.get('del_email_id')
376 email_id = request.POST.get('del_email_id')
378 user_model = UserModel()
377 user_model = UserModel()
379 user_model.delete_extra_email(id, email_id)
378 user_model.delete_extra_email(id, email_id)
380 Session().commit()
379 Session().commit()
381 h.flash(_("Removed email from user"), category='success')
380 h.flash(_("Removed email from user"), category='success')
382 raise HTTPFound(location=url('edit_user_emails', id=id))
381 raise HTTPFound(location=url('edit_user_emails', id=id))
383
382
384 def edit_ips(self, id):
383 def edit_ips(self, id):
385 c.user = self._get_user_or_raise_if_default(id)
384 c.user = self._get_user_or_raise_if_default(id)
386 c.active = 'ips'
385 c.active = 'ips'
387 c.user_ip_map = UserIpMap.query() \
386 c.user_ip_map = UserIpMap.query() \
388 .filter(UserIpMap.user == c.user).all()
387 .filter(UserIpMap.user == c.user).all()
389
388
390 c.default_user_ip_map = UserIpMap.query() \
389 c.default_user_ip_map = UserIpMap.query() \
391 .filter(UserIpMap.user == User.get_default_user()).all()
390 .filter(UserIpMap.user == User.get_default_user()).all()
392
391
393 defaults = c.user.get_dict()
392 defaults = c.user.get_dict()
394 return htmlfill.render(
393 return htmlfill.render(
395 render('admin/users/user_edit.html'),
394 render('admin/users/user_edit.html'),
396 defaults=defaults,
395 defaults=defaults,
397 encoding="UTF-8",
396 encoding="UTF-8",
398 force_defaults=False)
397 force_defaults=False)
399
398
400 def add_ip(self, id):
399 def add_ip(self, id):
401 ip = request.POST.get('new_ip')
400 ip = request.POST.get('new_ip')
402 user_model = UserModel()
401 user_model = UserModel()
403
402
404 try:
403 try:
405 user_model.add_extra_ip(id, ip)
404 user_model.add_extra_ip(id, ip)
406 Session().commit()
405 Session().commit()
407 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
406 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
408 except formencode.Invalid as error:
407 except formencode.Invalid as error:
409 msg = error.error_dict['ip']
408 msg = error.error_dict['ip']
410 h.flash(msg, category='error')
409 h.flash(msg, category='error')
411 except Exception:
410 except Exception:
412 log.error(traceback.format_exc())
411 log.error(traceback.format_exc())
413 h.flash(_('An error occurred while adding IP address'),
412 h.flash(_('An error occurred while adding IP address'),
414 category='error')
413 category='error')
415
414
416 if 'default_user' in request.POST:
415 if 'default_user' in request.POST:
417 raise HTTPFound(location=url('admin_permissions_ips'))
416 raise HTTPFound(location=url('admin_permissions_ips'))
418 raise HTTPFound(location=url('edit_user_ips', id=id))
417 raise HTTPFound(location=url('edit_user_ips', id=id))
419
418
420 def delete_ip(self, id):
419 def delete_ip(self, id):
421 ip_id = request.POST.get('del_ip_id')
420 ip_id = request.POST.get('del_ip_id')
422 user_model = UserModel()
421 user_model = UserModel()
423 user_model.delete_extra_ip(id, ip_id)
422 user_model.delete_extra_ip(id, ip_id)
424 Session().commit()
423 Session().commit()
425 h.flash(_("Removed IP address from user whitelist"), category='success')
424 h.flash(_("Removed IP address from user whitelist"), category='success')
426
425
427 if 'default_user' in request.POST:
426 if 'default_user' in request.POST:
428 raise HTTPFound(location=url('admin_permissions_ips'))
427 raise HTTPFound(location=url('admin_permissions_ips'))
429 raise HTTPFound(location=url('edit_user_ips', id=id))
428 raise HTTPFound(location=url('edit_user_ips', id=id))
430
429
431 @IfSshEnabled
430 @IfSshEnabled
432 def edit_ssh_keys(self, id):
431 def edit_ssh_keys(self, id):
433 c.user = self._get_user_or_raise_if_default(id)
432 c.user = self._get_user_or_raise_if_default(id)
434 c.active = 'ssh_keys'
433 c.active = 'ssh_keys'
435 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
434 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
436 defaults = c.user.get_dict()
435 defaults = c.user.get_dict()
437 return htmlfill.render(
436 return htmlfill.render(
438 render('admin/users/user_edit.html'),
437 render('admin/users/user_edit.html'),
439 defaults=defaults,
438 defaults=defaults,
440 encoding="UTF-8",
439 encoding="UTF-8",
441 force_defaults=False)
440 force_defaults=False)
442
441
443 @IfSshEnabled
442 @IfSshEnabled
444 def ssh_keys_add(self, id):
443 def ssh_keys_add(self, id):
445 c.user = self._get_user_or_raise_if_default(id)
444 c.user = self._get_user_or_raise_if_default(id)
446
445
447 description = request.POST.get('description')
446 description = request.POST.get('description')
448 public_key = request.POST.get('public_key')
447 public_key = request.POST.get('public_key')
449 try:
448 try:
450 new_ssh_key = SshKeyModel().create(c.user.user_id,
449 new_ssh_key = SshKeyModel().create(c.user.user_id,
451 description, public_key)
450 description, public_key)
452 Session().commit()
451 Session().commit()
453 SshKeyModel().write_authorized_keys()
452 SshKeyModel().write_authorized_keys()
454 h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
453 h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
455 except SshKeyModelException as e:
454 except SshKeyModelException as e:
456 h.flash(e.args[0], category='error')
455 h.flash(e.args[0], category='error')
457 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
456 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
458
457
459 @IfSshEnabled
458 @IfSshEnabled
460 def ssh_keys_delete(self, id):
459 def ssh_keys_delete(self, id):
461 c.user = self._get_user_or_raise_if_default(id)
460 c.user = self._get_user_or_raise_if_default(id)
462
461
463 fingerprint = request.POST.get('del_public_key_fingerprint')
462 fingerprint = request.POST.get('del_public_key_fingerprint')
464 try:
463 try:
465 SshKeyModel().delete(fingerprint, c.user.user_id)
464 SshKeyModel().delete(fingerprint, c.user.user_id)
466 Session().commit()
465 Session().commit()
467 SshKeyModel().write_authorized_keys()
466 SshKeyModel().write_authorized_keys()
468 h.flash(_("SSH key successfully deleted"), category='success')
467 h.flash(_("SSH key successfully deleted"), category='success')
469 except SshKeyModelException as e:
468 except SshKeyModelException as e:
470 h.flash(e.args[0], category='error')
469 h.flash(e.args[0], category='error')
471 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
470 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
@@ -1,491 +1,490 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changeset
15 kallithea.controllers.changeset
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changeset controller showing changes between revisions
18 changeset controller showing changes between revisions
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 25, 2010
22 :created_on: Apr 25, 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 binascii
28 import binascii
29 import logging
29 import logging
30 import traceback
30 import traceback
31 from collections import OrderedDict, defaultdict
31 from collections import OrderedDict, defaultdict
32
32
33 from tg import request, response
33 from tg import request, response
34 from tg import tmpl_context as c
34 from tg import tmpl_context as c
35 from tg.i18n import ugettext as _
35 from tg.i18n import ugettext as _
36 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
36 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
37
37
38 import kallithea.lib.helpers as h
38 import kallithea.lib.helpers as h
39 from kallithea.lib import diffs
39 from kallithea.lib import diffs
40 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
40 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
41 from kallithea.lib.base import BaseRepoController, jsonify, render
41 from kallithea.lib.base import BaseRepoController, jsonify, render
42 from kallithea.lib.graphmod import graph_data
42 from kallithea.lib.graphmod import graph_data
43 from kallithea.lib.utils import action_logger
43 from kallithea.lib.utils import action_logger
44 from kallithea.lib.utils2 import ascii_str, safe_str
44 from kallithea.lib.utils2 import ascii_str, safe_str
45 from kallithea.lib.vcs.backends.base import EmptyChangeset
45 from kallithea.lib.vcs.backends.base import EmptyChangeset
46 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
46 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
47 from kallithea.model.changeset_status import ChangesetStatusModel
47 from kallithea.model.changeset_status import ChangesetStatusModel
48 from kallithea.model.comment import ChangesetCommentsModel
48 from kallithea.model.comment import ChangesetCommentsModel
49 from kallithea.model.db import ChangesetComment, ChangesetStatus
49 from kallithea.model.db import ChangesetComment, ChangesetStatus
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51 from kallithea.model.pull_request import PullRequestModel
51 from kallithea.model.pull_request import PullRequestModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def anchor_url(revision, path, GET):
62 def anchor_url(revision, path, GET):
63 fid = h.FID(revision, path)
63 fid = h.FID(revision, path)
64 return h.url.current(anchor=fid, **dict(GET))
64 return h.url.current(anchor=fid, **dict(GET))
65
65
66
66
67 def get_ignore_ws(fid, GET):
67 def get_ignore_ws(fid, GET):
68 ig_ws_global = GET.get('ignorews')
68 ig_ws_global = GET.get('ignorews')
69 ig_ws = [k for k in GET.getall(fid) if k.startswith('WS')]
69 ig_ws = [k for k in GET.getall(fid) if k.startswith('WS')]
70 if ig_ws:
70 if ig_ws:
71 try:
71 try:
72 return int(ig_ws[0].split(':')[-1])
72 return int(ig_ws[0].split(':')[-1])
73 except ValueError:
73 except ValueError:
74 raise HTTPBadRequest()
74 raise HTTPBadRequest()
75 return ig_ws_global
75 return ig_ws_global
76
76
77
77
78 def _ignorews_url(GET, fileid=None):
78 def _ignorews_url(GET, fileid=None):
79 fileid = str(fileid) if fileid else None
79 fileid = str(fileid) if fileid else None
80 params = defaultdict(list)
80 params = defaultdict(list)
81 _update_with_GET(params, GET)
81 _update_with_GET(params, GET)
82 lbl = _('Show whitespace')
82 lbl = _('Show whitespace')
83 ig_ws = get_ignore_ws(fileid, GET)
83 ig_ws = get_ignore_ws(fileid, GET)
84 ln_ctx = get_line_ctx(fileid, GET)
84 ln_ctx = get_line_ctx(fileid, GET)
85 # global option
85 # global option
86 if fileid is None:
86 if fileid is None:
87 if ig_ws is None:
87 if ig_ws is None:
88 params['ignorews'] += [1]
88 params['ignorews'] += [1]
89 lbl = _('Ignore whitespace')
89 lbl = _('Ignore whitespace')
90 ctx_key = 'context'
90 ctx_key = 'context'
91 ctx_val = ln_ctx
91 ctx_val = ln_ctx
92 # per file options
92 # per file options
93 else:
93 else:
94 if ig_ws is None:
94 if ig_ws is None:
95 params[fileid] += ['WS:1']
95 params[fileid] += ['WS:1']
96 lbl = _('Ignore whitespace')
96 lbl = _('Ignore whitespace')
97
97
98 ctx_key = fileid
98 ctx_key = fileid
99 ctx_val = 'C:%s' % ln_ctx
99 ctx_val = 'C:%s' % ln_ctx
100 # if we have passed in ln_ctx pass it along to our params
100 # if we have passed in ln_ctx pass it along to our params
101 if ln_ctx:
101 if ln_ctx:
102 params[ctx_key] += [ctx_val]
102 params[ctx_key] += [ctx_val]
103
103
104 params['anchor'] = fileid
104 params['anchor'] = fileid
105 icon = h.literal('<i class="icon-strike"></i>')
105 icon = h.literal('<i class="icon-strike"></i>')
106 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
106 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
107
107
108
108
109 def get_line_ctx(fid, GET):
109 def get_line_ctx(fid, GET):
110 ln_ctx_global = GET.get('context')
110 ln_ctx_global = GET.get('context')
111 if fid:
111 if fid:
112 ln_ctx = [k for k in GET.getall(fid) if k.startswith('C')]
112 ln_ctx = [k for k in GET.getall(fid) if k.startswith('C')]
113 else:
113 else:
114 _ln_ctx = [k for k in GET if k.startswith('C')]
114 _ln_ctx = [k for k in GET if k.startswith('C')]
115 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
115 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
116 if ln_ctx:
116 if ln_ctx:
117 ln_ctx = [ln_ctx]
117 ln_ctx = [ln_ctx]
118
118
119 if ln_ctx:
119 if ln_ctx:
120 retval = ln_ctx[0].split(':')[-1]
120 retval = ln_ctx[0].split(':')[-1]
121 else:
121 else:
122 retval = ln_ctx_global
122 retval = ln_ctx_global
123
123
124 try:
124 try:
125 return int(retval)
125 return int(retval)
126 except Exception:
126 except Exception:
127 return 3
127 return 3
128
128
129
129
130 def _context_url(GET, fileid=None):
130 def _context_url(GET, fileid=None):
131 """
131 """
132 Generates url for context lines
132 Generates url for context lines
133
133
134 :param fileid:
134 :param fileid:
135 """
135 """
136
136
137 fileid = str(fileid) if fileid else None
137 fileid = str(fileid) if fileid else None
138 ig_ws = get_ignore_ws(fileid, GET)
138 ig_ws = get_ignore_ws(fileid, GET)
139 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
139 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
140
140
141 params = defaultdict(list)
141 params = defaultdict(list)
142 _update_with_GET(params, GET)
142 _update_with_GET(params, GET)
143
143
144 # global option
144 # global option
145 if fileid is None:
145 if fileid is None:
146 if ln_ctx > 0:
146 if ln_ctx > 0:
147 params['context'] += [ln_ctx]
147 params['context'] += [ln_ctx]
148
148
149 if ig_ws:
149 if ig_ws:
150 ig_ws_key = 'ignorews'
150 ig_ws_key = 'ignorews'
151 ig_ws_val = 1
151 ig_ws_val = 1
152
152
153 # per file option
153 # per file option
154 else:
154 else:
155 params[fileid] += ['C:%s' % ln_ctx]
155 params[fileid] += ['C:%s' % ln_ctx]
156 ig_ws_key = fileid
156 ig_ws_key = fileid
157 ig_ws_val = 'WS:%s' % 1
157 ig_ws_val = 'WS:%s' % 1
158
158
159 if ig_ws:
159 if ig_ws:
160 params[ig_ws_key] += [ig_ws_val]
160 params[ig_ws_key] += [ig_ws_val]
161
161
162 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
162 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
163
163
164 params['anchor'] = fileid
164 params['anchor'] = fileid
165 icon = h.literal('<i class="icon-sort"></i>')
165 icon = h.literal('<i class="icon-sort"></i>')
166 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
166 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
167
167
168
168
169 def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True):
169 def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True):
170 """
170 """
171 Add a comment to the specified changeset or pull request, using POST values
171 Add a comment to the specified changeset or pull request, using POST values
172 from the request.
172 from the request.
173
173
174 Comments can be inline (when a file path and line number is specified in
174 Comments can be inline (when a file path and line number is specified in
175 POST) or general comments.
175 POST) or general comments.
176 A comment can be accompanied by a review status change (accepted, rejected,
176 A comment can be accompanied by a review status change (accepted, rejected,
177 etc.). Pull requests can be closed or deleted.
177 etc.). Pull requests can be closed or deleted.
178
178
179 Parameter 'allowed_to_change_status' is used for both status changes and
179 Parameter 'allowed_to_change_status' is used for both status changes and
180 closing of pull requests. For deleting of pull requests, more specific
180 closing of pull requests. For deleting of pull requests, more specific
181 checks are done.
181 checks are done.
182 """
182 """
183
183
184 assert request.environ.get('HTTP_X_PARTIAL_XHR')
184 assert request.environ.get('HTTP_X_PARTIAL_XHR')
185 if pull_request:
185 if pull_request:
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187 else:
187 else:
188 pull_request_id = None
188 pull_request_id = None
189
189
190 status = request.POST.get('changeset_status')
190 status = request.POST.get('changeset_status')
191 close_pr = request.POST.get('save_close')
191 close_pr = request.POST.get('save_close')
192 delete = request.POST.get('save_delete')
192 delete = request.POST.get('save_delete')
193 f_path = request.POST.get('f_path')
193 f_path = request.POST.get('f_path')
194 line_no = request.POST.get('line')
194 line_no = request.POST.get('line')
195
195
196 if (status or close_pr or delete) and (f_path or line_no):
196 if (status or close_pr or delete) and (f_path or line_no):
197 # status votes and closing is only possible in general comments
197 # status votes and closing is only possible in general comments
198 raise HTTPBadRequest()
198 raise HTTPBadRequest()
199
199
200 if not allowed_to_change_status:
200 if not allowed_to_change_status:
201 if status or close_pr:
201 if status or close_pr:
202 h.flash(_('No permission to change status'), 'error')
202 h.flash(_('No permission to change status'), 'error')
203 raise HTTPForbidden()
203 raise HTTPForbidden()
204
204
205 if pull_request and delete == "delete":
205 if pull_request and delete == "delete":
206 if (pull_request.owner_id == request.authuser.user_id or
206 if (pull_request.owner_id == request.authuser.user_id or
207 h.HasPermissionAny('hg.admin')() or
207 h.HasPermissionAny('hg.admin')() or
208 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
208 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
209 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
209 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
210 ) and not pull_request.is_closed():
210 ) and not pull_request.is_closed():
211 PullRequestModel().delete(pull_request)
211 PullRequestModel().delete(pull_request)
212 Session().commit()
212 Session().commit()
213 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
213 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
214 category='success')
214 category='success')
215 return {
215 return {
216 'location': h.url('my_pullrequests'), # or repo pr list?
216 'location': h.url('my_pullrequests'), # or repo pr list?
217 }
217 }
218 raise HTTPFound(location=h.url('my_pullrequests')) # or repo pr list?
219 raise HTTPForbidden()
218 raise HTTPForbidden()
220
219
221 text = request.POST.get('text', '').strip()
220 text = request.POST.get('text', '').strip()
222
221
223 comment = ChangesetCommentsModel().create(
222 comment = ChangesetCommentsModel().create(
224 text=text,
223 text=text,
225 repo=c.db_repo.repo_id,
224 repo=c.db_repo.repo_id,
226 author=request.authuser.user_id,
225 author=request.authuser.user_id,
227 revision=revision,
226 revision=revision,
228 pull_request=pull_request_id,
227 pull_request=pull_request_id,
229 f_path=f_path or None,
228 f_path=f_path or None,
230 line_no=line_no or None,
229 line_no=line_no or None,
231 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
230 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
232 closing_pr=close_pr,
231 closing_pr=close_pr,
233 )
232 )
234
233
235 if status:
234 if status:
236 ChangesetStatusModel().set_status(
235 ChangesetStatusModel().set_status(
237 c.db_repo.repo_id,
236 c.db_repo.repo_id,
238 status,
237 status,
239 request.authuser.user_id,
238 request.authuser.user_id,
240 comment,
239 comment,
241 revision=revision,
240 revision=revision,
242 pull_request=pull_request_id,
241 pull_request=pull_request_id,
243 )
242 )
244
243
245 if pull_request:
244 if pull_request:
246 action = 'user_commented_pull_request:%s' % pull_request_id
245 action = 'user_commented_pull_request:%s' % pull_request_id
247 else:
246 else:
248 action = 'user_commented_revision:%s' % revision
247 action = 'user_commented_revision:%s' % revision
249 action_logger(request.authuser, action, c.db_repo, request.ip_addr)
248 action_logger(request.authuser, action, c.db_repo, request.ip_addr)
250
249
251 if pull_request and close_pr:
250 if pull_request and close_pr:
252 PullRequestModel().close_pull_request(pull_request_id)
251 PullRequestModel().close_pull_request(pull_request_id)
253 action_logger(request.authuser,
252 action_logger(request.authuser,
254 'user_closed_pull_request:%s' % pull_request_id,
253 'user_closed_pull_request:%s' % pull_request_id,
255 c.db_repo, request.ip_addr)
254 c.db_repo, request.ip_addr)
256
255
257 Session().commit()
256 Session().commit()
258
257
259 data = {
258 data = {
260 'target_id': h.safeid(request.POST.get('f_path')),
259 'target_id': h.safeid(request.POST.get('f_path')),
261 }
260 }
262 if comment is not None:
261 if comment is not None:
263 c.comment = comment
262 c.comment = comment
264 data.update(comment.get_dict())
263 data.update(comment.get_dict())
265 data.update({'rendered_text':
264 data.update({'rendered_text':
266 render('changeset/changeset_comment_block.html')})
265 render('changeset/changeset_comment_block.html')})
267
266
268 return data
267 return data
269
268
270 def delete_cs_pr_comment(repo_name, comment_id):
269 def delete_cs_pr_comment(repo_name, comment_id):
271 """Delete a comment from a changeset or pull request"""
270 """Delete a comment from a changeset or pull request"""
272 co = ChangesetComment.get_or_404(comment_id)
271 co = ChangesetComment.get_or_404(comment_id)
273 if co.repo.repo_name != repo_name:
272 if co.repo.repo_name != repo_name:
274 raise HTTPNotFound()
273 raise HTTPNotFound()
275 if co.pull_request and co.pull_request.is_closed():
274 if co.pull_request and co.pull_request.is_closed():
276 # don't allow deleting comments on closed pull request
275 # don't allow deleting comments on closed pull request
277 raise HTTPForbidden()
276 raise HTTPForbidden()
278
277
279 owner = co.author_id == request.authuser.user_id
278 owner = co.author_id == request.authuser.user_id
280 repo_admin = h.HasRepoPermissionLevel('admin')(repo_name)
279 repo_admin = h.HasRepoPermissionLevel('admin')(repo_name)
281 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
280 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
282 ChangesetCommentsModel().delete(comment=co)
281 ChangesetCommentsModel().delete(comment=co)
283 Session().commit()
282 Session().commit()
284 return True
283 return True
285 else:
284 else:
286 raise HTTPForbidden()
285 raise HTTPForbidden()
287
286
288 class ChangesetController(BaseRepoController):
287 class ChangesetController(BaseRepoController):
289
288
290 def _before(self, *args, **kwargs):
289 def _before(self, *args, **kwargs):
291 super(ChangesetController, self)._before(*args, **kwargs)
290 super(ChangesetController, self)._before(*args, **kwargs)
292 c.affected_files_cut_off = 60
291 c.affected_files_cut_off = 60
293
292
294 def _index(self, revision, method):
293 def _index(self, revision, method):
295 c.pull_request = None
294 c.pull_request = None
296 c.anchor_url = anchor_url
295 c.anchor_url = anchor_url
297 c.ignorews_url = _ignorews_url
296 c.ignorews_url = _ignorews_url
298 c.context_url = _context_url
297 c.context_url = _context_url
299 c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files
298 c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files
300 # get ranges of revisions if preset
299 # get ranges of revisions if preset
301 rev_range = revision.split('...')[:2]
300 rev_range = revision.split('...')[:2]
302 enable_comments = True
301 enable_comments = True
303 c.cs_repo = c.db_repo
302 c.cs_repo = c.db_repo
304 try:
303 try:
305 if len(rev_range) == 2:
304 if len(rev_range) == 2:
306 enable_comments = False
305 enable_comments = False
307 rev_start = rev_range[0]
306 rev_start = rev_range[0]
308 rev_end = rev_range[1]
307 rev_end = rev_range[1]
309 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
308 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
310 end=rev_end)
309 end=rev_end)
311 else:
310 else:
312 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
311 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
313
312
314 c.cs_ranges = list(rev_ranges)
313 c.cs_ranges = list(rev_ranges)
315 if not c.cs_ranges:
314 if not c.cs_ranges:
316 raise RepositoryError('Changeset range returned empty result')
315 raise RepositoryError('Changeset range returned empty result')
317
316
318 except (ChangesetDoesNotExistError, EmptyRepositoryError):
317 except (ChangesetDoesNotExistError, EmptyRepositoryError):
319 log.debug(traceback.format_exc())
318 log.debug(traceback.format_exc())
320 msg = _('Such revision does not exist for this repository')
319 msg = _('Such revision does not exist for this repository')
321 h.flash(msg, category='error')
320 h.flash(msg, category='error')
322 raise HTTPNotFound()
321 raise HTTPNotFound()
323
322
324 c.changes = OrderedDict()
323 c.changes = OrderedDict()
325
324
326 c.lines_added = 0 # count of lines added
325 c.lines_added = 0 # count of lines added
327 c.lines_deleted = 0 # count of lines removes
326 c.lines_deleted = 0 # count of lines removes
328
327
329 c.changeset_statuses = ChangesetStatus.STATUSES
328 c.changeset_statuses = ChangesetStatus.STATUSES
330 comments = dict()
329 comments = dict()
331 c.statuses = []
330 c.statuses = []
332 c.inline_comments = []
331 c.inline_comments = []
333 c.inline_cnt = 0
332 c.inline_cnt = 0
334
333
335 # Iterate over ranges (default changeset view is always one changeset)
334 # Iterate over ranges (default changeset view is always one changeset)
336 for changeset in c.cs_ranges:
335 for changeset in c.cs_ranges:
337 if method == 'show':
336 if method == 'show':
338 c.statuses.extend([ChangesetStatusModel().get_status(
337 c.statuses.extend([ChangesetStatusModel().get_status(
339 c.db_repo.repo_id, changeset.raw_id)])
338 c.db_repo.repo_id, changeset.raw_id)])
340
339
341 # Changeset comments
340 # Changeset comments
342 comments.update((com.comment_id, com)
341 comments.update((com.comment_id, com)
343 for com in ChangesetCommentsModel()
342 for com in ChangesetCommentsModel()
344 .get_comments(c.db_repo.repo_id,
343 .get_comments(c.db_repo.repo_id,
345 revision=changeset.raw_id))
344 revision=changeset.raw_id))
346
345
347 # Status change comments - mostly from pull requests
346 # Status change comments - mostly from pull requests
348 comments.update((st.comment_id, st.comment)
347 comments.update((st.comment_id, st.comment)
349 for st in ChangesetStatusModel()
348 for st in ChangesetStatusModel()
350 .get_statuses(c.db_repo.repo_id,
349 .get_statuses(c.db_repo.repo_id,
351 changeset.raw_id, with_revisions=True)
350 changeset.raw_id, with_revisions=True)
352 if st.comment_id is not None)
351 if st.comment_id is not None)
353
352
354 inlines = ChangesetCommentsModel() \
353 inlines = ChangesetCommentsModel() \
355 .get_inline_comments(c.db_repo.repo_id,
354 .get_inline_comments(c.db_repo.repo_id,
356 revision=changeset.raw_id)
355 revision=changeset.raw_id)
357 c.inline_comments.extend(inlines)
356 c.inline_comments.extend(inlines)
358
357
359 cs2 = changeset.raw_id
358 cs2 = changeset.raw_id
360 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
359 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
361 context_lcl = get_line_ctx('', request.GET)
360 context_lcl = get_line_ctx('', request.GET)
362 ign_whitespace_lcl = get_ignore_ws('', request.GET)
361 ign_whitespace_lcl = get_ignore_ws('', request.GET)
363
362
364 raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2,
363 raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2,
365 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
364 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
366 diff_limit = None if c.fulldiff else self.cut_off_limit
365 diff_limit = None if c.fulldiff else self.cut_off_limit
367 file_diff_data = []
366 file_diff_data = []
368 if method == 'show':
367 if method == 'show':
369 diff_processor = diffs.DiffProcessor(raw_diff,
368 diff_processor = diffs.DiffProcessor(raw_diff,
370 vcs=c.db_repo_scm_instance.alias,
369 vcs=c.db_repo_scm_instance.alias,
371 diff_limit=diff_limit)
370 diff_limit=diff_limit)
372 c.limited_diff = diff_processor.limited_diff
371 c.limited_diff = diff_processor.limited_diff
373 for f in diff_processor.parsed:
372 for f in diff_processor.parsed:
374 st = f['stats']
373 st = f['stats']
375 c.lines_added += st['added']
374 c.lines_added += st['added']
376 c.lines_deleted += st['deleted']
375 c.lines_deleted += st['deleted']
377 filename = f['filename']
376 filename = f['filename']
378 fid = h.FID(changeset.raw_id, filename)
377 fid = h.FID(changeset.raw_id, filename)
379 url_fid = h.FID('', filename)
378 url_fid = h.FID('', filename)
380 html_diff = diffs.as_html(enable_comments=enable_comments, parsed_lines=[f])
379 html_diff = diffs.as_html(enable_comments=enable_comments, parsed_lines=[f])
381 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st))
380 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st))
382 else:
381 else:
383 # downloads/raw we only need RAW diff nothing else
382 # downloads/raw we only need RAW diff nothing else
384 file_diff_data.append(('', None, None, None, raw_diff, None))
383 file_diff_data.append(('', None, None, None, raw_diff, None))
385 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
384 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
386
385
387 # sort comments in creation order
386 # sort comments in creation order
388 c.comments = [com for com_id, com in sorted(comments.items())]
387 c.comments = [com for com_id, com in sorted(comments.items())]
389
388
390 # count inline comments
389 # count inline comments
391 for __, lines in c.inline_comments:
390 for __, lines in c.inline_comments:
392 for comments in lines.values():
391 for comments in lines.values():
393 c.inline_cnt += len(comments)
392 c.inline_cnt += len(comments)
394
393
395 if len(c.cs_ranges) == 1:
394 if len(c.cs_ranges) == 1:
396 c.changeset = c.cs_ranges[0]
395 c.changeset = c.cs_ranges[0]
397 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
396 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
398 for x in c.changeset.parents])
397 for x in c.changeset.parents])
399 c.changeset_graft_source_hash = ascii_str(c.changeset.extra.get(b'source', b''))
398 c.changeset_graft_source_hash = ascii_str(c.changeset.extra.get(b'source', b''))
400 c.changeset_transplant_source_hash = ascii_str(binascii.hexlify(c.changeset.extra.get(b'transplant_source', b'')))
399 c.changeset_transplant_source_hash = ascii_str(binascii.hexlify(c.changeset.extra.get(b'transplant_source', b'')))
401 if method == 'download':
400 if method == 'download':
402 response.content_type = 'text/plain'
401 response.content_type = 'text/plain'
403 response.content_disposition = 'attachment; filename=%s.diff' \
402 response.content_disposition = 'attachment; filename=%s.diff' \
404 % revision[:12]
403 % revision[:12]
405 return raw_diff
404 return raw_diff
406 elif method == 'patch':
405 elif method == 'patch':
407 response.content_type = 'text/plain'
406 response.content_type = 'text/plain'
408 c.diff = safe_str(raw_diff)
407 c.diff = safe_str(raw_diff)
409 return render('changeset/patch_changeset.html')
408 return render('changeset/patch_changeset.html')
410 elif method == 'raw':
409 elif method == 'raw':
411 response.content_type = 'text/plain'
410 response.content_type = 'text/plain'
412 return raw_diff
411 return raw_diff
413 elif method == 'show':
412 elif method == 'show':
414 if len(c.cs_ranges) == 1:
413 if len(c.cs_ranges) == 1:
415 return render('changeset/changeset.html')
414 return render('changeset/changeset.html')
416 else:
415 else:
417 c.cs_ranges_org = None
416 c.cs_ranges_org = None
418 c.cs_comments = {}
417 c.cs_comments = {}
419 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
418 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
420 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
419 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
421 return render('changeset/changeset_range.html')
420 return render('changeset/changeset_range.html')
422
421
423 @LoginRequired(allow_default_user=True)
422 @LoginRequired(allow_default_user=True)
424 @HasRepoPermissionLevelDecorator('read')
423 @HasRepoPermissionLevelDecorator('read')
425 def index(self, revision, method='show'):
424 def index(self, revision, method='show'):
426 return self._index(revision, method=method)
425 return self._index(revision, method=method)
427
426
428 @LoginRequired(allow_default_user=True)
427 @LoginRequired(allow_default_user=True)
429 @HasRepoPermissionLevelDecorator('read')
428 @HasRepoPermissionLevelDecorator('read')
430 def changeset_raw(self, revision):
429 def changeset_raw(self, revision):
431 return self._index(revision, method='raw')
430 return self._index(revision, method='raw')
432
431
433 @LoginRequired(allow_default_user=True)
432 @LoginRequired(allow_default_user=True)
434 @HasRepoPermissionLevelDecorator('read')
433 @HasRepoPermissionLevelDecorator('read')
435 def changeset_patch(self, revision):
434 def changeset_patch(self, revision):
436 return self._index(revision, method='patch')
435 return self._index(revision, method='patch')
437
436
438 @LoginRequired(allow_default_user=True)
437 @LoginRequired(allow_default_user=True)
439 @HasRepoPermissionLevelDecorator('read')
438 @HasRepoPermissionLevelDecorator('read')
440 def changeset_download(self, revision):
439 def changeset_download(self, revision):
441 return self._index(revision, method='download')
440 return self._index(revision, method='download')
442
441
443 @LoginRequired()
442 @LoginRequired()
444 @HasRepoPermissionLevelDecorator('read')
443 @HasRepoPermissionLevelDecorator('read')
445 @jsonify
444 @jsonify
446 def comment(self, repo_name, revision):
445 def comment(self, repo_name, revision):
447 return create_cs_pr_comment(repo_name, revision=revision)
446 return create_cs_pr_comment(repo_name, revision=revision)
448
447
449 @LoginRequired()
448 @LoginRequired()
450 @HasRepoPermissionLevelDecorator('read')
449 @HasRepoPermissionLevelDecorator('read')
451 @jsonify
450 @jsonify
452 def delete_comment(self, repo_name, comment_id):
451 def delete_comment(self, repo_name, comment_id):
453 return delete_cs_pr_comment(repo_name, comment_id)
452 return delete_cs_pr_comment(repo_name, comment_id)
454
453
455 @LoginRequired(allow_default_user=True)
454 @LoginRequired(allow_default_user=True)
456 @HasRepoPermissionLevelDecorator('read')
455 @HasRepoPermissionLevelDecorator('read')
457 @jsonify
456 @jsonify
458 def changeset_info(self, repo_name, revision):
457 def changeset_info(self, repo_name, revision):
459 if request.is_xhr:
458 if request.is_xhr:
460 try:
459 try:
461 return c.db_repo_scm_instance.get_changeset(revision)
460 return c.db_repo_scm_instance.get_changeset(revision)
462 except ChangesetDoesNotExistError as e:
461 except ChangesetDoesNotExistError as e:
463 return EmptyChangeset(message=str(e))
462 return EmptyChangeset(message=str(e))
464 else:
463 else:
465 raise HTTPBadRequest()
464 raise HTTPBadRequest()
466
465
467 @LoginRequired(allow_default_user=True)
466 @LoginRequired(allow_default_user=True)
468 @HasRepoPermissionLevelDecorator('read')
467 @HasRepoPermissionLevelDecorator('read')
469 @jsonify
468 @jsonify
470 def changeset_children(self, repo_name, revision):
469 def changeset_children(self, repo_name, revision):
471 if request.is_xhr:
470 if request.is_xhr:
472 changeset = c.db_repo_scm_instance.get_changeset(revision)
471 changeset = c.db_repo_scm_instance.get_changeset(revision)
473 result = {"results": []}
472 result = {"results": []}
474 if changeset.children:
473 if changeset.children:
475 result = {"results": changeset.children}
474 result = {"results": changeset.children}
476 return result
475 return result
477 else:
476 else:
478 raise HTTPBadRequest()
477 raise HTTPBadRequest()
479
478
480 @LoginRequired(allow_default_user=True)
479 @LoginRequired(allow_default_user=True)
481 @HasRepoPermissionLevelDecorator('read')
480 @HasRepoPermissionLevelDecorator('read')
482 @jsonify
481 @jsonify
483 def changeset_parents(self, repo_name, revision):
482 def changeset_parents(self, repo_name, revision):
484 if request.is_xhr:
483 if request.is_xhr:
485 changeset = c.db_repo_scm_instance.get_changeset(revision)
484 changeset = c.db_repo_scm_instance.get_changeset(revision)
486 result = {"results": []}
485 result = {"results": []}
487 if changeset.parents:
486 if changeset.parents:
488 result = {"results": changeset.parents}
487 result = {"results": changeset.parents}
489 return result
488 return result
490 else:
489 else:
491 raise HTTPBadRequest()
490 raise HTTPBadRequest()
@@ -1,420 +1,419 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.db_manage
15 kallithea.lib.db_manage
16 ~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Database creation, and setup module for Kallithea. Used for creation
18 Database creation, and setup module for Kallithea. Used for creation
19 of database as well as for migration operations
19 of database as well as for migration operations
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Apr 10, 2010
23 :created_on: Apr 10, 2010
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 import logging
29 import logging
30 import os
30 import os
31 import sys
31 import sys
32 import uuid
32 import uuid
33
33
34 import alembic.command
34 import alembic.command
35 import alembic.config
35 import alembic.config
36 import sqlalchemy
36 import sqlalchemy
37 from sqlalchemy.engine import create_engine
37 from sqlalchemy.engine import create_engine
38
38
39 from kallithea.model.base import init_model
39 from kallithea.model.base import init_model
40 from kallithea.model.db import Permission, RepoGroup, Repository, Setting, Ui, User, UserRepoGroupToPerm, UserToPerm
40 from kallithea.model.db import Permission, RepoGroup, Repository, Setting, Ui, User, UserRepoGroupToPerm, UserToPerm
41 #from kallithea.model import meta
41 #from kallithea.model import meta
42 from kallithea.model.meta import Base, Session
42 from kallithea.model.meta import Base, Session
43 from kallithea.model.permission import PermissionModel
43 from kallithea.model.permission import PermissionModel
44 from kallithea.model.repo_group import RepoGroupModel
44 from kallithea.model.repo_group import RepoGroupModel
45 from kallithea.model.user import UserModel
45 from kallithea.model.user import UserModel
46
46
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class DbManage(object):
51 class DbManage(object):
52 def __init__(self, dbconf, root, tests=False, SESSION=None, cli_args=None):
52 def __init__(self, dbconf, root, tests=False, SESSION=None, cli_args=None):
53 self.dbname = dbconf.split('/')[-1]
53 self.dbname = dbconf.split('/')[-1]
54 self.tests = tests
54 self.tests = tests
55 self.root = root
55 self.root = root
56 self.dburi = dbconf
56 self.dburi = dbconf
57 self.db_exists = False
58 self.cli_args = cli_args or {}
57 self.cli_args = cli_args or {}
59 self.init_db(SESSION=SESSION)
58 self.init_db(SESSION=SESSION)
60
59
61 def _ask_ok(self, msg):
60 def _ask_ok(self, msg):
62 """Invoke ask_ok unless the force_ask option provides the answer"""
61 """Invoke ask_ok unless the force_ask option provides the answer"""
63 force_ask = self.cli_args.get('force_ask')
62 force_ask = self.cli_args.get('force_ask')
64 if force_ask is not None:
63 if force_ask is not None:
65 return force_ask
64 return force_ask
66 from kallithea.lib.utils2 import ask_ok
65 from kallithea.lib.utils2 import ask_ok
67 return ask_ok(msg)
66 return ask_ok(msg)
68
67
69 def init_db(self, SESSION=None):
68 def init_db(self, SESSION=None):
70 if SESSION:
69 if SESSION:
71 self.sa = SESSION
70 self.sa = SESSION
72 else:
71 else:
73 # init new sessions
72 # init new sessions
74 engine = create_engine(self.dburi)
73 engine = create_engine(self.dburi)
75 init_model(engine)
74 init_model(engine)
76 self.sa = Session()
75 self.sa = Session()
77
76
78 def create_tables(self, override=False):
77 def create_tables(self, override=False):
79 """
78 """
80 Create a auth database
79 Create a auth database
81 """
80 """
82
81
83 log.info("Any existing database is going to be destroyed")
82 log.info("Any existing database is going to be destroyed")
84 if self.tests:
83 if self.tests:
85 destroy = True
84 destroy = True
86 else:
85 else:
87 destroy = self._ask_ok('Are you sure to destroy old database ? [y/n]')
86 destroy = self._ask_ok('Are you sure to destroy old database ? [y/n]')
88 if not destroy:
87 if not destroy:
89 print('Nothing done.')
88 print('Nothing done.')
90 sys.exit(0)
89 sys.exit(0)
91 if destroy:
90 if destroy:
92 # drop and re-create old schemas
91 # drop and re-create old schemas
93
92
94 url = sqlalchemy.engine.url.make_url(self.dburi)
93 url = sqlalchemy.engine.url.make_url(self.dburi)
95 database = url.database
94 database = url.database
96
95
97 # Some databases enforce foreign key constraints and Base.metadata.drop_all() doesn't work
96 # Some databases enforce foreign key constraints and Base.metadata.drop_all() doesn't work
98 if url.drivername == 'mysql':
97 if url.drivername == 'mysql':
99 url.database = None # don't connect to the database (it might not exist)
98 url.database = None # don't connect to the database (it might not exist)
100 engine = sqlalchemy.create_engine(url)
99 engine = sqlalchemy.create_engine(url)
101 with engine.connect() as conn:
100 with engine.connect() as conn:
102 conn.execute('DROP DATABASE IF EXISTS ' + database)
101 conn.execute('DROP DATABASE IF EXISTS ' + database)
103 conn.execute('CREATE DATABASE ' + database)
102 conn.execute('CREATE DATABASE ' + database)
104 elif url.drivername == 'postgresql':
103 elif url.drivername == 'postgresql':
105 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
104 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
106 url.database = 'postgres' # connect to the system database (as the real one might not exist)
105 url.database = 'postgres' # connect to the system database (as the real one might not exist)
107 engine = sqlalchemy.create_engine(url)
106 engine = sqlalchemy.create_engine(url)
108 with engine.connect() as conn:
107 with engine.connect() as conn:
109 conn.connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
108 conn.connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
110 conn.execute('DROP DATABASE IF EXISTS ' + database)
109 conn.execute('DROP DATABASE IF EXISTS ' + database)
111 conn.execute('CREATE DATABASE ' + database)
110 conn.execute('CREATE DATABASE ' + database)
112 else:
111 else:
113 # known to work on SQLite - possibly not on other databases with strong referential integrity
112 # known to work on SQLite - possibly not on other databases with strong referential integrity
114 Base.metadata.drop_all()
113 Base.metadata.drop_all()
115
114
116 checkfirst = not override
115 checkfirst = not override
117 Base.metadata.create_all(checkfirst=checkfirst)
116 Base.metadata.create_all(checkfirst=checkfirst)
118
117
119 # Create an Alembic configuration and generate the version table,
118 # Create an Alembic configuration and generate the version table,
120 # "stamping" it with the most recent Alembic migration revision, to
119 # "stamping" it with the most recent Alembic migration revision, to
121 # tell Alembic that all the schema upgrades are already in effect.
120 # tell Alembic that all the schema upgrades are already in effect.
122 alembic_cfg = alembic.config.Config()
121 alembic_cfg = alembic.config.Config()
123 alembic_cfg.set_main_option('script_location', 'kallithea:alembic')
122 alembic_cfg.set_main_option('script_location', 'kallithea:alembic')
124 alembic_cfg.set_main_option('sqlalchemy.url', self.dburi)
123 alembic_cfg.set_main_option('sqlalchemy.url', self.dburi)
125 # This command will give an error in an Alembic multi-head scenario,
124 # This command will give an error in an Alembic multi-head scenario,
126 # but in practice, such a scenario should not come up during database
125 # but in practice, such a scenario should not come up during database
127 # creation, even during development.
126 # creation, even during development.
128 alembic.command.stamp(alembic_cfg, 'head')
127 alembic.command.stamp(alembic_cfg, 'head')
129
128
130 log.info('Created tables for %s', self.dbname)
129 log.info('Created tables for %s', self.dbname)
131
130
132 def fix_repo_paths(self):
131 def fix_repo_paths(self):
133 """
132 """
134 Fixes a old kallithea version path into new one without a '*'
133 Fixes a old kallithea version path into new one without a '*'
135 """
134 """
136
135
137 paths = Ui.query() \
136 paths = Ui.query() \
138 .filter(Ui.ui_key == '/') \
137 .filter(Ui.ui_key == '/') \
139 .scalar()
138 .scalar()
140
139
141 paths.ui_value = paths.ui_value.replace('*', '')
140 paths.ui_value = paths.ui_value.replace('*', '')
142
141
143 self.sa.commit()
142 self.sa.commit()
144
143
145 def fix_default_user(self):
144 def fix_default_user(self):
146 """
145 """
147 Fixes a old default user with some 'nicer' default values,
146 Fixes a old default user with some 'nicer' default values,
148 used mostly for anonymous access
147 used mostly for anonymous access
149 """
148 """
150 def_user = User.query().filter_by(is_default_user=True).one()
149 def_user = User.query().filter_by(is_default_user=True).one()
151
150
152 def_user.name = 'Anonymous'
151 def_user.name = 'Anonymous'
153 def_user.lastname = 'User'
152 def_user.lastname = 'User'
154 def_user.email = 'anonymous@kallithea-scm.org'
153 def_user.email = 'anonymous@kallithea-scm.org'
155
154
156 self.sa.commit()
155 self.sa.commit()
157
156
158 def fix_settings(self):
157 def fix_settings(self):
159 """
158 """
160 Fixes kallithea settings adds ga_code key for google analytics
159 Fixes kallithea settings adds ga_code key for google analytics
161 """
160 """
162
161
163 hgsettings3 = Setting('ga_code', '')
162 hgsettings3 = Setting('ga_code', '')
164
163
165 self.sa.add(hgsettings3)
164 self.sa.add(hgsettings3)
166 self.sa.commit()
165 self.sa.commit()
167
166
168 def admin_prompt(self, second=False):
167 def admin_prompt(self, second=False):
169 if not self.tests:
168 if not self.tests:
170 import getpass
169 import getpass
171
170
172 username = self.cli_args.get('username')
171 username = self.cli_args.get('username')
173 password = self.cli_args.get('password')
172 password = self.cli_args.get('password')
174 email = self.cli_args.get('email')
173 email = self.cli_args.get('email')
175
174
176 def get_password():
175 def get_password():
177 password = getpass.getpass('Specify admin password '
176 password = getpass.getpass('Specify admin password '
178 '(min 6 chars):')
177 '(min 6 chars):')
179 confirm = getpass.getpass('Confirm password:')
178 confirm = getpass.getpass('Confirm password:')
180
179
181 if password != confirm:
180 if password != confirm:
182 log.error('passwords mismatch')
181 log.error('passwords mismatch')
183 return False
182 return False
184 if len(password) < 6:
183 if len(password) < 6:
185 log.error('password is to short use at least 6 characters')
184 log.error('password is to short use at least 6 characters')
186 return False
185 return False
187
186
188 return password
187 return password
189 if username is None:
188 if username is None:
190 username = input('Specify admin username:')
189 username = input('Specify admin username:')
191 if password is None:
190 if password is None:
192 password = get_password()
191 password = get_password()
193 if not password:
192 if not password:
194 # second try
193 # second try
195 password = get_password()
194 password = get_password()
196 if not password:
195 if not password:
197 sys.exit()
196 sys.exit()
198 if email is None:
197 if email is None:
199 email = input('Specify admin email:')
198 email = input('Specify admin email:')
200 self.create_user(username, password, email, True)
199 self.create_user(username, password, email, True)
201 else:
200 else:
202 log.info('creating admin and regular test users')
201 log.info('creating admin and regular test users')
203 from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, \
202 from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, \
204 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
203 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
205 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
204 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
206 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
205 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
207 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
206 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
208
207
209 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
208 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
210 TEST_USER_ADMIN_EMAIL, True)
209 TEST_USER_ADMIN_EMAIL, True)
211
210
212 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
211 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
213 TEST_USER_REGULAR_EMAIL, False)
212 TEST_USER_REGULAR_EMAIL, False)
214
213
215 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
214 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
216 TEST_USER_REGULAR2_EMAIL, False)
215 TEST_USER_REGULAR2_EMAIL, False)
217
216
218 def create_auth_plugin_options(self, skip_existing=False):
217 def create_auth_plugin_options(self, skip_existing=False):
219 """
218 """
220 Create default auth plugin settings, and make it active
219 Create default auth plugin settings, and make it active
221
220
222 :param skip_existing:
221 :param skip_existing:
223 """
222 """
224
223
225 for k, v, t in [('auth_plugins', 'kallithea.lib.auth_modules.auth_internal', 'list'),
224 for k, v, t in [('auth_plugins', 'kallithea.lib.auth_modules.auth_internal', 'list'),
226 ('auth_internal_enabled', 'True', 'bool')]:
225 ('auth_internal_enabled', 'True', 'bool')]:
227 if skip_existing and Setting.get_by_name(k) is not None:
226 if skip_existing and Setting.get_by_name(k) is not None:
228 log.debug('Skipping option %s', k)
227 log.debug('Skipping option %s', k)
229 continue
228 continue
230 setting = Setting(k, v, t)
229 setting = Setting(k, v, t)
231 self.sa.add(setting)
230 self.sa.add(setting)
232
231
233 def create_default_options(self, skip_existing=False):
232 def create_default_options(self, skip_existing=False):
234 """Creates default settings"""
233 """Creates default settings"""
235
234
236 for k, v, t in [
235 for k, v, t in [
237 ('default_repo_enable_downloads', False, 'bool'),
236 ('default_repo_enable_downloads', False, 'bool'),
238 ('default_repo_enable_statistics', False, 'bool'),
237 ('default_repo_enable_statistics', False, 'bool'),
239 ('default_repo_private', False, 'bool'),
238 ('default_repo_private', False, 'bool'),
240 ('default_repo_type', 'hg', 'unicode')
239 ('default_repo_type', 'hg', 'unicode')
241 ]:
240 ]:
242 if skip_existing and Setting.get_by_name(k) is not None:
241 if skip_existing and Setting.get_by_name(k) is not None:
243 log.debug('Skipping option %s', k)
242 log.debug('Skipping option %s', k)
244 continue
243 continue
245 setting = Setting(k, v, t)
244 setting = Setting(k, v, t)
246 self.sa.add(setting)
245 self.sa.add(setting)
247
246
248 def fixup_groups(self):
247 def fixup_groups(self):
249 def_usr = User.get_default_user()
248 def_usr = User.get_default_user()
250 for g in RepoGroup.query().all():
249 for g in RepoGroup.query().all():
251 g.group_name = g.get_new_name(g.name)
250 g.group_name = g.get_new_name(g.name)
252 # get default perm
251 # get default perm
253 default = UserRepoGroupToPerm.query() \
252 default = UserRepoGroupToPerm.query() \
254 .filter(UserRepoGroupToPerm.group == g) \
253 .filter(UserRepoGroupToPerm.group == g) \
255 .filter(UserRepoGroupToPerm.user == def_usr) \
254 .filter(UserRepoGroupToPerm.user == def_usr) \
256 .scalar()
255 .scalar()
257
256
258 if default is None:
257 if default is None:
259 log.debug('missing default permission for group %s adding', g)
258 log.debug('missing default permission for group %s adding', g)
260 RepoGroupModel()._create_default_perms(g)
259 RepoGroupModel()._create_default_perms(g)
261
260
262 def reset_permissions(self, username):
261 def reset_permissions(self, username):
263 """
262 """
264 Resets permissions to default state, useful when old systems had
263 Resets permissions to default state, useful when old systems had
265 bad permissions, we must clean them up
264 bad permissions, we must clean them up
266
265
267 :param username:
266 :param username:
268 """
267 """
269 default_user = User.get_by_username(username)
268 default_user = User.get_by_username(username)
270 if not default_user:
269 if not default_user:
271 return
270 return
272
271
273 u2p = UserToPerm.query() \
272 u2p = UserToPerm.query() \
274 .filter(UserToPerm.user == default_user).all()
273 .filter(UserToPerm.user == default_user).all()
275 fixed = False
274 fixed = False
276 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
275 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
277 for p in u2p:
276 for p in u2p:
278 Session().delete(p)
277 Session().delete(p)
279 fixed = True
278 fixed = True
280 self.populate_default_permissions()
279 self.populate_default_permissions()
281 return fixed
280 return fixed
282
281
283 def update_repo_info(self):
282 def update_repo_info(self):
284 for repo in Repository.query():
283 for repo in Repository.query():
285 repo.update_changeset_cache()
284 repo.update_changeset_cache()
286
285
287 def prompt_repo_root_path(self, test_repo_path='', retries=3):
286 def prompt_repo_root_path(self, test_repo_path='', retries=3):
288 _path = self.cli_args.get('repos_location')
287 _path = self.cli_args.get('repos_location')
289 if retries == 3:
288 if retries == 3:
290 log.info('Setting up repositories config')
289 log.info('Setting up repositories config')
291
290
292 if _path is not None:
291 if _path is not None:
293 path = _path
292 path = _path
294 elif not self.tests and not test_repo_path:
293 elif not self.tests and not test_repo_path:
295 path = input(
294 path = input(
296 'Enter a valid absolute path to store repositories. '
295 'Enter a valid absolute path to store repositories. '
297 'All repositories in that path will be added automatically:'
296 'All repositories in that path will be added automatically:'
298 )
297 )
299 else:
298 else:
300 path = test_repo_path
299 path = test_repo_path
301 path_ok = True
300 path_ok = True
302
301
303 # check proper dir
302 # check proper dir
304 if not os.path.isdir(path):
303 if not os.path.isdir(path):
305 path_ok = False
304 path_ok = False
306 log.error('Given path %s is not a valid directory', path)
305 log.error('Given path %s is not a valid directory', path)
307
306
308 elif not os.path.isabs(path):
307 elif not os.path.isabs(path):
309 path_ok = False
308 path_ok = False
310 log.error('Given path %s is not an absolute path', path)
309 log.error('Given path %s is not an absolute path', path)
311
310
312 # check if path is at least readable.
311 # check if path is at least readable.
313 if not os.access(path, os.R_OK):
312 if not os.access(path, os.R_OK):
314 path_ok = False
313 path_ok = False
315 log.error('Given path %s is not readable', path)
314 log.error('Given path %s is not readable', path)
316
315
317 # check write access, warn user about non writeable paths
316 # check write access, warn user about non writeable paths
318 elif not os.access(path, os.W_OK) and path_ok:
317 elif not os.access(path, os.W_OK) and path_ok:
319 log.warning('No write permission to given path %s', path)
318 log.warning('No write permission to given path %s', path)
320 if not self._ask_ok('Given path %s is not writeable, do you want to '
319 if not self._ask_ok('Given path %s is not writeable, do you want to '
321 'continue with read only mode ? [y/n]' % (path,)):
320 'continue with read only mode ? [y/n]' % (path,)):
322 log.error('Canceled by user')
321 log.error('Canceled by user')
323 sys.exit(-1)
322 sys.exit(-1)
324
323
325 if retries == 0:
324 if retries == 0:
326 sys.exit('max retries reached')
325 sys.exit('max retries reached')
327 if not path_ok:
326 if not path_ok:
328 if _path is not None:
327 if _path is not None:
329 sys.exit('Invalid repo path: %s' % _path)
328 sys.exit('Invalid repo path: %s' % _path)
330 retries -= 1
329 retries -= 1
331 return self.prompt_repo_root_path(test_repo_path, retries) # recursing!!!
330 return self.prompt_repo_root_path(test_repo_path, retries) # recursing!!!
332
331
333 real_path = os.path.normpath(os.path.realpath(path))
332 real_path = os.path.normpath(os.path.realpath(path))
334
333
335 if real_path != os.path.normpath(path):
334 if real_path != os.path.normpath(path):
336 log.warning('Using normalized path %s instead of %s', real_path, path)
335 log.warning('Using normalized path %s instead of %s', real_path, path)
337
336
338 return real_path
337 return real_path
339
338
340 def create_settings(self, repo_root_path):
339 def create_settings(self, repo_root_path):
341 ui_config = [
340 ui_config = [
342 ('paths', '/', repo_root_path, True),
341 ('paths', '/', repo_root_path, True),
343 #('phases', 'publish', 'false', False)
342 #('phases', 'publish', 'false', False)
344 ('hooks', Ui.HOOK_UPDATE, 'hg update >&2', False),
343 ('hooks', Ui.HOOK_UPDATE, 'hg update >&2', False),
345 ('hooks', Ui.HOOK_REPO_SIZE, 'python:kallithea.lib.hooks.repo_size', True),
344 ('hooks', Ui.HOOK_REPO_SIZE, 'python:kallithea.lib.hooks.repo_size', True),
346 ('extensions', 'largefiles', '', True),
345 ('extensions', 'largefiles', '', True),
347 ('largefiles', 'usercache', os.path.join(repo_root_path, '.cache', 'largefiles'), True),
346 ('largefiles', 'usercache', os.path.join(repo_root_path, '.cache', 'largefiles'), True),
348 ('extensions', 'hgsubversion', '', False),
347 ('extensions', 'hgsubversion', '', False),
349 ('extensions', 'hggit', '', False),
348 ('extensions', 'hggit', '', False),
350 ]
349 ]
351 for ui_section, ui_key, ui_value, ui_active in ui_config:
350 for ui_section, ui_key, ui_value, ui_active in ui_config:
352 ui_conf = Ui(
351 ui_conf = Ui(
353 ui_section=ui_section,
352 ui_section=ui_section,
354 ui_key=ui_key,
353 ui_key=ui_key,
355 ui_value=ui_value,
354 ui_value=ui_value,
356 ui_active=ui_active)
355 ui_active=ui_active)
357 self.sa.add(ui_conf)
356 self.sa.add(ui_conf)
358
357
359 settings = [
358 settings = [
360 ('realm', 'Kallithea', 'unicode'),
359 ('realm', 'Kallithea', 'unicode'),
361 ('title', '', 'unicode'),
360 ('title', '', 'unicode'),
362 ('ga_code', '', 'unicode'),
361 ('ga_code', '', 'unicode'),
363 ('show_public_icon', True, 'bool'),
362 ('show_public_icon', True, 'bool'),
364 ('show_private_icon', True, 'bool'),
363 ('show_private_icon', True, 'bool'),
365 ('stylify_metalabels', False, 'bool'),
364 ('stylify_metalabels', False, 'bool'),
366 ('dashboard_items', 100, 'int'), # TODO: call it page_size
365 ('dashboard_items', 100, 'int'), # TODO: call it page_size
367 ('admin_grid_items', 25, 'int'),
366 ('admin_grid_items', 25, 'int'),
368 ('show_version', True, 'bool'),
367 ('show_version', True, 'bool'),
369 ('use_gravatar', True, 'bool'),
368 ('use_gravatar', True, 'bool'),
370 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
369 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
371 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
370 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
372 ('clone_ssh_tmpl', Repository.DEFAULT_CLONE_SSH, 'unicode'),
371 ('clone_ssh_tmpl', Repository.DEFAULT_CLONE_SSH, 'unicode'),
373 ]
372 ]
374 for key, val, type_ in settings:
373 for key, val, type_ in settings:
375 sett = Setting(key, val, type_)
374 sett = Setting(key, val, type_)
376 self.sa.add(sett)
375 self.sa.add(sett)
377
376
378 self.create_auth_plugin_options()
377 self.create_auth_plugin_options()
379 self.create_default_options()
378 self.create_default_options()
380
379
381 log.info('Populated Ui and Settings defaults')
380 log.info('Populated Ui and Settings defaults')
382
381
383 def create_user(self, username, password, email='', admin=False):
382 def create_user(self, username, password, email='', admin=False):
384 log.info('creating user %s', username)
383 log.info('creating user %s', username)
385 UserModel().create_or_update(username, password, email,
384 UserModel().create_or_update(username, password, email,
386 firstname='Kallithea', lastname='Admin',
385 firstname='Kallithea', lastname='Admin',
387 active=True, admin=admin,
386 active=True, admin=admin,
388 extern_type=User.DEFAULT_AUTH_TYPE)
387 extern_type=User.DEFAULT_AUTH_TYPE)
389
388
390 def create_default_user(self):
389 def create_default_user(self):
391 log.info('creating default user')
390 log.info('creating default user')
392 # create default user for handling default permissions.
391 # create default user for handling default permissions.
393 user = UserModel().create_or_update(username=User.DEFAULT_USER,
392 user = UserModel().create_or_update(username=User.DEFAULT_USER,
394 password=str(uuid.uuid1())[:20],
393 password=str(uuid.uuid1())[:20],
395 email='anonymous@kallithea-scm.org',
394 email='anonymous@kallithea-scm.org',
396 firstname='Anonymous',
395 firstname='Anonymous',
397 lastname='User')
396 lastname='User')
398 # based on configuration options activate/deactivate this user which
397 # based on configuration options activate/deactivate this user which
399 # controls anonymous access
398 # controls anonymous access
400 if self.cli_args.get('public_access') is False:
399 if self.cli_args.get('public_access') is False:
401 log.info('Public access disabled')
400 log.info('Public access disabled')
402 user.active = False
401 user.active = False
403 Session().commit()
402 Session().commit()
404
403
405 def create_permissions(self):
404 def create_permissions(self):
406 """
405 """
407 Creates all permissions defined in the system
406 Creates all permissions defined in the system
408 """
407 """
409 # module.(access|create|change|delete)_[name]
408 # module.(access|create|change|delete)_[name]
410 # module.(none|read|write|admin)
409 # module.(none|read|write|admin)
411 log.info('creating permissions')
410 log.info('creating permissions')
412 PermissionModel().create_permissions()
411 PermissionModel().create_permissions()
413
412
414 def populate_default_permissions(self):
413 def populate_default_permissions(self):
415 """
414 """
416 Populate default permissions. It will create only the default
415 Populate default permissions. It will create only the default
417 permissions that are missing, and not alter already defined ones
416 permissions that are missing, and not alter already defined ones
418 """
417 """
419 log.info('creating default user permissions')
418 log.info('creating default user permissions')
420 PermissionModel().create_default_permissions(user=User.DEFAULT_USER)
419 PermissionModel().create_default_permissions(user=User.DEFAULT_USER)
@@ -1,82 +1,78 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.exceptions
15 kallithea.lib.exceptions
16 ~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Set of custom exceptions used in Kallithea
18 Set of custom exceptions used in 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 17, 2010
22 :created_on: Nov 17, 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 class LdapUsernameError(Exception):
28 class LdapUsernameError(Exception):
29 pass
29 pass
30
30
31
31
32 class LdapPasswordError(Exception):
32 class LdapPasswordError(Exception):
33 pass
33 pass
34
34
35
35
36 class LdapConnectionError(Exception):
36 class LdapConnectionError(Exception):
37 pass
37 pass
38
38
39
39
40 class LdapImportError(Exception):
40 class LdapImportError(Exception):
41 pass
41 pass
42
42
43
43
44 class DefaultUserException(Exception):
44 class DefaultUserException(Exception):
45 """An invalid action was attempted on the default user"""
45 """An invalid action was attempted on the default user"""
46 pass
46 pass
47
47
48
48
49 class UserOwnsReposException(Exception):
49 class UserOwnsReposException(Exception):
50 pass
50 pass
51
51
52
52
53 class UserGroupsAssignedException(Exception):
53 class UserGroupsAssignedException(Exception):
54 pass
54 pass
55
55
56
56
57 class AttachedForksError(Exception):
57 class AttachedForksError(Exception):
58 pass
58 pass
59
59
60
60
61 class RepoGroupAssignmentError(Exception):
61 class RepoGroupAssignmentError(Exception):
62 pass
62 pass
63
63
64
64
65 class NonRelativePathError(Exception):
65 class NonRelativePathError(Exception):
66 pass
66 pass
67
67
68
68
69 class IMCCommitError(Exception):
69 class IMCCommitError(Exception):
70 pass
70 pass
71
71
72
72
73 class UserCreationError(Exception):
73 class UserCreationError(Exception):
74 pass
74 pass
75
75
76
76
77 class RepositoryCreationError(Exception):
78 pass
79
80
81 class HgsubversionImportError(Exception):
77 class HgsubversionImportError(Exception):
82 pass
78 pass
@@ -1,181 +1,152 b''
1 from kallithea.lib.rcmail.exceptions import BadHeaders, InvalidMessage
1 from kallithea.lib.rcmail.exceptions import BadHeaders, InvalidMessage
2 from kallithea.lib.rcmail.response import MailResponse
2 from kallithea.lib.rcmail.response import MailResponse
3
3
4
4
5 class Attachment(object):
6 """
7 Encapsulates file attachment information.
8
9 :param filename: filename of attachment
10 :param content_type: file mimetype
11 :param data: the raw file data, either as string or file obj
12 :param disposition: content-disposition (if any)
13 """
14
15 def __init__(self,
16 filename=None,
17 content_type=None,
18 data=None,
19 disposition=None):
20
21 self.filename = filename
22 self.content_type = content_type
23 self.disposition = disposition or 'attachment'
24 self._data = data
25
26 @property
27 def data(self):
28 if isinstance(self._data, str):
29 return self._data
30 self._data = self._data.read()
31 return self._data
32
33
34 class Message(object):
5 class Message(object):
35 """
6 """
36 Encapsulates an email message.
7 Encapsulates an email message.
37
8
38 :param subject: email subject header
9 :param subject: email subject header
39 :param recipients: list of email addresses
10 :param recipients: list of email addresses
40 :param body: plain text message
11 :param body: plain text message
41 :param html: HTML message
12 :param html: HTML message
42 :param sender: email sender address
13 :param sender: email sender address
43 :param cc: CC list
14 :param cc: CC list
44 :param bcc: BCC list
15 :param bcc: BCC list
45 :param extra_headers: dict of extra email headers
16 :param extra_headers: dict of extra email headers
46 :param attachments: list of Attachment instances
17 :param attachments: list of Attachment instances
47 :param recipients_separator: alternative separator for any of
18 :param recipients_separator: alternative separator for any of
48 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields
19 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields
49 """
20 """
50
21
51 def __init__(self,
22 def __init__(self,
52 subject=None,
23 subject=None,
53 recipients=None,
24 recipients=None,
54 body=None,
25 body=None,
55 html=None,
26 html=None,
56 sender=None,
27 sender=None,
57 cc=None,
28 cc=None,
58 bcc=None,
29 bcc=None,
59 extra_headers=None,
30 extra_headers=None,
60 attachments=None,
31 attachments=None,
61 recipients_separator="; "):
32 recipients_separator="; "):
62
33
63 self.subject = subject or ''
34 self.subject = subject or ''
64 self.sender = sender
35 self.sender = sender
65 self.body = body
36 self.body = body
66 self.html = html
37 self.html = html
67
38
68 self.recipients = recipients or []
39 self.recipients = recipients or []
69 self.attachments = attachments or []
40 self.attachments = attachments or []
70 self.cc = cc or []
41 self.cc = cc or []
71 self.bcc = bcc or []
42 self.bcc = bcc or []
72 self.extra_headers = extra_headers or {}
43 self.extra_headers = extra_headers or {}
73
44
74 self.recipients_separator = recipients_separator
45 self.recipients_separator = recipients_separator
75
46
76 @property
47 @property
77 def send_to(self):
48 def send_to(self):
78 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
49 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
79
50
80 def to_message(self):
51 def to_message(self):
81 """
52 """
82 Returns raw email.Message instance.Validates message first.
53 Returns raw email.Message instance.Validates message first.
83 """
54 """
84
55
85 self.validate()
56 self.validate()
86
57
87 return self.get_response().to_message()
58 return self.get_response().to_message()
88
59
89 def get_response(self):
60 def get_response(self):
90 """
61 """
91 Creates a Lamson MailResponse instance
62 Creates a Lamson MailResponse instance
92 """
63 """
93
64
94 response = MailResponse(Subject=self.subject,
65 response = MailResponse(Subject=self.subject,
95 To=self.recipients,
66 To=self.recipients,
96 From=self.sender,
67 From=self.sender,
97 Body=self.body,
68 Body=self.body,
98 Html=self.html,
69 Html=self.html,
99 separator=self.recipients_separator)
70 separator=self.recipients_separator)
100
71
101 if self.cc:
72 if self.cc:
102 response.base['Cc'] = self.cc
73 response.base['Cc'] = self.cc
103
74
104 for attachment in self.attachments:
75 for attachment in self.attachments:
105
76
106 response.attach(attachment.filename,
77 response.attach(attachment.filename,
107 attachment.content_type,
78 attachment.content_type,
108 attachment.data,
79 attachment.data,
109 attachment.disposition)
80 attachment.disposition)
110
81
111 response.update(self.extra_headers)
82 response.update(self.extra_headers)
112
83
113 return response
84 return response
114
85
115 def is_bad_headers(self):
86 def is_bad_headers(self):
116 """
87 """
117 Checks for bad headers i.e. newlines in subject, sender or recipients.
88 Checks for bad headers i.e. newlines in subject, sender or recipients.
118 """
89 """
119
90
120 headers = [self.subject, self.sender]
91 headers = [self.subject, self.sender]
121 headers += list(self.send_to)
92 headers += list(self.send_to)
122 headers += self.extra_headers.values()
93 headers += self.extra_headers.values()
123
94
124 for val in headers:
95 for val in headers:
125 for c in '\r\n':
96 for c in '\r\n':
126 if c in val:
97 if c in val:
127 return True
98 return True
128 return False
99 return False
129
100
130 def validate(self):
101 def validate(self):
131 """
102 """
132 Checks if message is valid and raises appropriate exception.
103 Checks if message is valid and raises appropriate exception.
133 """
104 """
134
105
135 if not self.recipients:
106 if not self.recipients:
136 raise InvalidMessage("No recipients have been added")
107 raise InvalidMessage("No recipients have been added")
137
108
138 if not self.body and not self.html:
109 if not self.body and not self.html:
139 raise InvalidMessage("No body has been set")
110 raise InvalidMessage("No body has been set")
140
111
141 if not self.sender:
112 if not self.sender:
142 raise InvalidMessage("No sender address has been set")
113 raise InvalidMessage("No sender address has been set")
143
114
144 if self.is_bad_headers():
115 if self.is_bad_headers():
145 raise BadHeaders
116 raise BadHeaders
146
117
147 def add_recipient(self, recipient):
118 def add_recipient(self, recipient):
148 """
119 """
149 Adds another recipient to the message.
120 Adds another recipient to the message.
150
121
151 :param recipient: email address of recipient.
122 :param recipient: email address of recipient.
152 """
123 """
153
124
154 self.recipients.append(recipient)
125 self.recipients.append(recipient)
155
126
156 def add_cc(self, recipient):
127 def add_cc(self, recipient):
157 """
128 """
158 Adds an email address to the CC list.
129 Adds an email address to the CC list.
159
130
160 :param recipient: email address of recipient.
131 :param recipient: email address of recipient.
161 """
132 """
162
133
163 self.cc.append(recipient)
134 self.cc.append(recipient)
164
135
165 def add_bcc(self, recipient):
136 def add_bcc(self, recipient):
166 """
137 """
167 Adds an email address to the BCC list.
138 Adds an email address to the BCC list.
168
139
169 :param recipient: email address of recipient.
140 :param recipient: email address of recipient.
170 """
141 """
171
142
172 self.bcc.append(recipient)
143 self.bcc.append(recipient)
173
144
174 def attach(self, attachment):
145 def attach(self, attachment):
175 """
146 """
176 Adds an attachment to the message.
147 Adds an attachment to the message.
177
148
178 :param attachment: an **Attachment** instance.
149 :param attachment: an **Attachment** instance.
179 """
150 """
180
151
181 self.attachments.append(attachment)
152 self.attachments.append(attachment)
@@ -1,454 +1,454 b''
1 # The code in this module is entirely lifted from the Lamson project
1 # The code in this module is entirely lifted from the Lamson project
2 # (http://lamsonproject.org/). Its copyright is:
2 # (http://lamsonproject.org/). Its copyright is:
3
3
4 # Copyright (c) 2008, Zed A. Shaw
4 # Copyright (c) 2008, Zed A. Shaw
5 # All rights reserved.
5 # All rights reserved.
6
6
7 # It is provided under this license:
7 # It is provided under this license:
8
8
9 # Redistribution and use in source and binary forms, with or without
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions are met:
10 # modification, are permitted provided that the following conditions are met:
11
11
12 # * Redistributions of source code must retain the above copyright notice, this
12 # * Redistributions of source code must retain the above copyright notice, this
13 # list of conditions and the following disclaimer.
13 # list of conditions and the following disclaimer.
14
14
15 # * Redistributions in binary form must reproduce the above copyright notice,
15 # * Redistributions in binary form must reproduce the above copyright notice,
16 # this list of conditions and the following disclaimer in the documentation
16 # this list of conditions and the following disclaimer in the documentation
17 # and/or other materials provided with the distribution.
17 # and/or other materials provided with the distribution.
18
18
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 # be used to endorse or promote products derived from this software without
20 # be used to endorse or promote products derived from this software without
21 # specific prior written permission.
21 # specific prior written permission.
22
22
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 # POSSIBILITY OF SUCH DAMAGE.
34 # POSSIBILITY OF SUCH DAMAGE.
35
35
36 import mimetypes
36 import mimetypes
37 import os
37 import os
38 import string
38 import string
39 from email import encoders
39 from email import encoders
40 from email.charset import Charset
40 from email.charset import Charset
41 from email.mime.base import MIMEBase
41 from email.mime.base import MIMEBase
42 from email.utils import parseaddr
42 from email.utils import parseaddr
43
43
44
44
45 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
45 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
46 DEFAULT_ENCODING = "utf-8"
46 DEFAULT_ENCODING = "utf-8"
47 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
47 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
48
48
49
49
50 def normalize_header(header):
50 def normalize_header(header):
51 return string.capwords(header.lower(), '-')
51 return string.capwords(header.lower(), '-')
52
52
53
53
54 class EncodingError(Exception):
54 class EncodingError(Exception):
55 """Thrown when there is an encoding error."""
55 """Thrown when there is an encoding error."""
56 pass
56 pass
57
57
58
58
59 class MailBase(object):
59 class MailBase(object):
60 """MailBase is used as the basis of lamson.mail and contains the basics of
60 """MailBase is used as the basis of lamson.mail and contains the basics of
61 encoding an email. You actually can do all your email processing with this
61 encoding an email. You actually can do all your email processing with this
62 class, but it's more raw.
62 class, but it's more raw.
63 """
63 """
64 def __init__(self, items=()):
64 def __init__(self, items=()):
65 self.headers = dict(items)
65 self.headers = dict(items)
66 self.parts = []
66 self.parts = []
67 self.body = None
67 self.body = None
68 self.content_encoding = {'Content-Type': (None, {}),
68 self.content_encoding = {'Content-Type': (None, {}),
69 'Content-Disposition': (None, {}),
69 'Content-Disposition': (None, {}),
70 'Content-Transfer-Encoding': (None, {})}
70 'Content-Transfer-Encoding': (None, {})}
71
71
72 def __getitem__(self, key):
72 def __getitem__(self, key):
73 return self.headers.get(normalize_header(key), None)
73 return self.headers.get(normalize_header(key), None)
74
74
75 def __len__(self):
75 def __len__(self):
76 return len(self.headers)
76 return len(self.headers)
77
77
78 def __iter__(self):
78 def __iter__(self):
79 return iter(self.headers)
79 return iter(self.headers)
80
80
81 def __contains__(self, key):
81 def __contains__(self, key):
82 return normalize_header(key) in self.headers
82 return normalize_header(key) in self.headers
83
83
84 def __setitem__(self, key, value):
84 def __setitem__(self, key, value):
85 self.headers[normalize_header(key)] = value
85 self.headers[normalize_header(key)] = value
86
86
87 def __delitem__(self, key):
87 def __delitem__(self, key):
88 del self.headers[normalize_header(key)]
88 del self.headers[normalize_header(key)]
89
89
90 def __bool__(self):
90 def __bool__(self):
91 return self.body is not None or len(self.headers) > 0 or len(self.parts) > 0
91 return self.body is not None or len(self.headers) > 0 or len(self.parts) > 0
92
92
93 def keys(self):
93 def keys(self):
94 """Returns the sorted keys."""
94 """Returns the sorted keys."""
95 return sorted(self.headers.keys())
95 return sorted(self.headers.keys())
96
96
97 def attach_file(self, filename, data, ctype, disposition):
97 def attach_file(self, filename, data, ctype, disposition):
98 """
98 """
99 A file attachment is a raw attachment with a disposition that
99 A file attachment is a raw attachment with a disposition that
100 indicates the file name.
100 indicates the file name.
101 """
101 """
102 assert filename, "You can't attach a file without a filename."
102 assert filename, "You can't attach a file without a filename."
103 ctype = ctype.lower()
103 ctype = ctype.lower()
104
104
105 part = MailBase()
105 part = MailBase()
106 part.body = data
106 part.body = data
107 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
107 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
108 part.content_encoding['Content-Disposition'] = (disposition,
108 part.content_encoding['Content-Disposition'] = (disposition,
109 {'filename': filename})
109 {'filename': filename})
110 self.parts.append(part)
110 self.parts.append(part)
111
111
112 def attach_text(self, data, ctype):
112 def attach_text(self, data, ctype):
113 """
113 """
114 This attaches a simpler text encoded part, which doesn't have a
114 This attaches a simpler text encoded part, which doesn't have a
115 filename.
115 filename.
116 """
116 """
117 ctype = ctype.lower()
117 ctype = ctype.lower()
118
118
119 part = MailBase()
119 part = MailBase()
120 part.body = data
120 part.body = data
121 part.content_encoding['Content-Type'] = (ctype, {})
121 part.content_encoding['Content-Type'] = (ctype, {})
122 self.parts.append(part)
122 self.parts.append(part)
123
123
124 def walk(self):
124 def walk(self):
125 for p in self.parts:
125 for p in self.parts:
126 yield p
126 yield p
127 for x in p.walk():
127 for x in p.walk():
128 yield x
128 yield x
129
129
130
130
131 class MailResponse(object):
131 class MailResponse(object):
132 """
132 """
133 You are given MailResponse objects from the lamson.view methods, and
133 You are given MailResponse objects from the lamson.view methods, and
134 whenever you want to generate an email to send to someone. It has the
134 whenever you want to generate an email to send to someone. It has the
135 same basic functionality as MailRequest, but it is designed to be written
135 same basic functionality as MailRequest, but it is designed to be written
136 to, rather than read from (although you can do both).
136 to, rather than read from (although you can do both).
137
137
138 You can easily set a Body or Html during creation or after by passing it
138 You can easily set a Body or Html during creation or after by passing it
139 as __init__ parameters, or by setting those attributes.
139 as __init__ parameters, or by setting those attributes.
140
140
141 You can initially set the From, To, and Subject, but they are headers so
141 You can initially set the From, To, and Subject, but they are headers so
142 use the dict notation to change them: msg['From'] = 'joe@example.com'.
142 use the dict notation to change them: msg['From'] = 'joe@example.com'.
143
143
144 The message is not fully crafted until right when you convert it with
144 The message is not fully crafted until right when you convert it with
145 MailResponse.to_message. This lets you change it and work with it, then
145 MailResponse.to_message. This lets you change it and work with it, then
146 send it out when it's ready.
146 send it out when it's ready.
147 """
147 """
148 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
148 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
149 separator="; "):
149 separator="; "):
150 self.Body = Body
150 self.Body = Body
151 self.Html = Html
151 self.Html = Html
152 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
152 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
153 self.multipart = self.Body and self.Html
153 self.multipart = self.Body and self.Html
154 self.attachments = []
154 self.attachments = []
155 self.separator = separator
155 self.separator = separator
156
156
157 def __contains__(self, key):
157 def __contains__(self, key):
158 return self.base.__contains__(key)
158 return self.base.__contains__(key)
159
159
160 def __getitem__(self, key):
160 def __getitem__(self, key):
161 return self.base.__getitem__(key)
161 return self.base.__getitem__(key)
162
162
163 def __setitem__(self, key, val):
163 def __setitem__(self, key, val):
164 return self.base.__setitem__(key, val)
164 return self.base.__setitem__(key, val)
165
165
166 def __delitem__(self, name):
166 def __delitem__(self, name):
167 del self.base[name]
167 del self.base[name]
168
168
169 def attach(self, filename=None, content_type=None, data=None,
169 def attach(self, filename=None, content_type=None, data=None,
170 disposition=None):
170 disposition=None):
171 """
171 """
172
172
173 Simplifies attaching files from disk or data as files. To attach
173 Simplifies attaching files from disk or data as files. To attach
174 simple text simple give data and a content_type. To attach a file,
174 simple text simple give data and a content_type. To attach a file,
175 give the data/content_type/filename/disposition combination.
175 give the data/content_type/filename/disposition combination.
176
176
177 For convenience, if you don't give data and only a filename, then it
177 For convenience, if you don't give data and only a filename, then it
178 will read that file's contents when you call to_message() later. If
178 will read that file's contents when you call to_message() later. If
179 you give data and filename then it will assume you've filled data
179 you give data and filename then it will assume you've filled data
180 with what the file's contents are and filename is just the name to
180 with what the file's contents are and filename is just the name to
181 use.
181 use.
182 """
182 """
183
183
184 assert filename or data, ("You must give a filename or some data to "
184 assert filename or data, ("You must give a filename or some data to "
185 "attach.")
185 "attach.")
186 assert data or os.path.exists(filename), ("File doesn't exist, and no "
186 assert data or os.path.exists(filename), ("File doesn't exist, and no "
187 "data given.")
187 "data given.")
188
188
189 self.multipart = True
189 self.multipart = True
190
190
191 if filename and not content_type:
191 if filename and not content_type:
192 content_type, encoding = mimetypes.guess_type(filename)
192 content_type, encoding = mimetypes.guess_type(filename)
193
193
194 assert content_type, ("No content type given, and couldn't guess "
194 assert content_type, ("No content type given, and couldn't guess "
195 "from the filename: %r" % filename)
195 "from the filename: %r" % filename)
196
196
197 self.attachments.append({'filename': filename,
197 self.attachments.append({'filename': filename,
198 'content_type': content_type,
198 'content_type': content_type,
199 'data': data,
199 'data': data,
200 'disposition': disposition})
200 'disposition': disposition})
201
201
202 def attach_part(self, part):
202 def attach_part(self, part):
203 """
203 """
204 Attaches a raw MailBase part from a MailRequest (or anywhere)
204 Attaches a raw MailBase part from a MailRequest (or anywhere)
205 so that you can copy it over.
205 so that you can copy it over.
206 """
206 """
207 self.multipart = True
207 self.multipart = True
208
208
209 self.attachments.append({'filename': None,
209 self.attachments.append({'filename': None,
210 'content_type': None,
210 'content_type': None,
211 'data': None,
211 'data': None,
212 'disposition': None,
212 'disposition': None,
213 'part': part,
213 'part': part,
214 })
214 })
215
215
216 def attach_all_parts(self, mail_request):
216 def attach_all_parts(self, mail_request):
217 """
217 """
218 Used for copying the attachment parts of a mail.MailRequest
218 Used for copying the attachment parts of a mail.MailRequest
219 object for mailing lists that need to maintain attachments.
219 object for mailing lists that need to maintain attachments.
220 """
220 """
221 for part in mail_request.all_parts():
221 for part in mail_request.all_parts():
222 self.attach_part(part)
222 self.attach_part(part)
223
223
224 self.base.content_encoding = mail_request.base.content_encoding.copy()
224 self.base.content_encoding = mail_request.base.content_encoding.copy()
225
225
226 def clear(self):
226 def clear(self):
227 """
227 """
228 Clears out the attachments so you can redo them. Use this to keep the
228 Clears out the attachments so you can redo them. Use this to keep the
229 headers for a series of different messages with different attachments.
229 headers for a series of different messages with different attachments.
230 """
230 """
231 del self.attachments[:]
231 del self.attachments[:]
232 del self.base.parts[:]
232 del self.base.parts[:]
233 self.multipart = False
233 self.multipart = False
234
234
235 def update(self, message):
235 def update(self, message):
236 """
236 """
237 Used to easily set a bunch of heading from another dict
237 Used to easily set a bunch of heading from another dict
238 like object.
238 like object.
239 """
239 """
240 for k in message.keys():
240 for k in message.keys():
241 self.base[k] = message[k]
241 self.base[k] = message[k]
242
242
243 def __str__(self):
243 def __str__(self):
244 """
244 """
245 Converts to a string.
245 Converts to a string.
246 """
246 """
247 return self.to_message().as_string()
247 return self.to_message().as_string()
248
248
249 def _encode_attachment(self, filename=None, content_type=None, data=None,
249 def _encode_attachment(self, filename=None, content_type=None, data=None,
250 disposition=None, part=None):
250 disposition=None, part=None):
251 """
251 """
252 Used internally to take the attachments mentioned in self.attachments
252 Used internally to take the attachments mentioned in self.attachments
253 and do the actual encoding in a lazy way when you call to_message.
253 and do the actual encoding in a lazy way when you call to_message.
254 """
254 """
255 if part:
255 if part:
256 self.base.parts.append(part)
256 self.base.parts.append(part)
257 elif filename:
257 elif filename:
258 if not data:
258 if not data:
259 data = open(filename).read()
259 data = open(filename).read()
260
260
261 self.base.attach_file(filename, data, content_type,
261 self.base.attach_file(filename, data, content_type,
262 disposition or 'attachment')
262 disposition or 'attachment')
263 else:
263 else:
264 self.base.attach_text(data, content_type)
264 self.base.attach_text(data, content_type)
265
265
266 ctype = self.base.content_encoding['Content-Type'][0]
266 ctype = self.base.content_encoding['Content-Type'][0]
267
267
268 if ctype and not ctype.startswith('multipart'):
268 if ctype and not ctype.startswith('multipart'):
269 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
269 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
270
270
271 def to_message(self):
271 def to_message(self):
272 """
272 """
273 Figures out all the required steps to finally craft the
273 Figures out all the required steps to finally craft the
274 message you need and return it. The resulting message
274 message you need and return it. The resulting message
275 is also available as a self.base attribute.
275 is also available as a self.base attribute.
276
276
277 What is returned is a Python email API message you can
277 What is returned is a Python email API message you can
278 use with those APIs. The self.base attribute is the raw
278 use with those APIs. The self.base attribute is the raw
279 lamson.encoding.MailBase.
279 lamson.encoding.MailBase.
280 """
280 """
281 del self.base.parts[:]
281 del self.base.parts[:]
282
282
283 if self.Body and self.Html:
283 if self.Body and self.Html:
284 self.multipart = True
284 self.multipart = True
285 self.base.content_encoding['Content-Type'] = (
285 self.base.content_encoding['Content-Type'] = (
286 'multipart/alternative', {})
286 'multipart/alternative', {})
287
287
288 if self.multipart:
288 if self.multipart:
289 self.base.body = None
289 self.base.body = None
290 if self.Body:
290 if self.Body:
291 self.base.attach_text(self.Body, 'text/plain')
291 self.base.attach_text(self.Body, 'text/plain')
292
292
293 if self.Html:
293 if self.Html:
294 self.base.attach_text(self.Html, 'text/html')
294 self.base.attach_text(self.Html, 'text/html')
295
295
296 for args in self.attachments:
296 for args in self.attachments:
297 self._encode_attachment(**args)
297 self._encode_attachment(**args)
298
298
299 elif self.Body:
299 elif self.Body:
300 self.base.body = self.Body
300 self.base.body = self.Body
301 self.base.content_encoding['Content-Type'] = ('text/plain', {})
301 self.base.content_encoding['Content-Type'] = ('text/plain', {})
302
302
303 elif self.Html:
303 elif self.Html:
304 self.base.body = self.Html
304 self.base.body = self.Html
305 self.base.content_encoding['Content-Type'] = ('text/html', {})
305 self.base.content_encoding['Content-Type'] = ('text/html', {})
306
306
307 return to_message(self.base, separator=self.separator)
307 return to_message(self.base, separator=self.separator)
308
308
309 def all_parts(self):
309 def all_parts(self):
310 """
310 """
311 Returns all the encoded parts. Only useful for debugging
311 Returns all the encoded parts. Only useful for debugging
312 or inspecting after calling to_message().
312 or inspecting after calling to_message().
313 """
313 """
314 return self.base.parts
314 return self.base.parts
315
315
316 def keys(self):
316 def keys(self):
317 return self.base.keys()
317 return self.base.keys()
318
318
319
319
320 def to_message(mail, separator="; "):
320 def to_message(mail, separator="; "):
321 """
321 """
322 Given a MailBase message, this will construct a MIMEPart
322 Given a MailBase message, this will construct a MIMEPart
323 that is canonicalized for use with the Python email API.
323 that is canonicalized for use with the Python email API.
324 """
324 """
325 ctype, params = mail.content_encoding['Content-Type']
325 ctype, params = mail.content_encoding['Content-Type']
326
326
327 if not ctype:
327 if not ctype:
328 if mail.parts:
328 if mail.parts:
329 ctype = 'multipart/mixed'
329 ctype = 'multipart/mixed'
330 else:
330 else:
331 ctype = 'text/plain'
331 ctype = 'text/plain'
332 else:
332 else:
333 if mail.parts:
333 if mail.parts:
334 assert ctype.startswith(("multipart", "message")), \
334 assert ctype.startswith(("multipart", "message")), \
335 "Content type should be multipart or message, not %r" % ctype
335 "Content type should be multipart or message, not %r" % ctype
336
336
337 # adjust the content type according to what it should be now
337 # adjust the content type according to what it should be now
338 mail.content_encoding['Content-Type'] = (ctype, params)
338 mail.content_encoding['Content-Type'] = (ctype, params)
339
339
340 try:
340 try:
341 out = MIMEPart(ctype, **params)
341 out = MIMEPart(ctype, **params)
342 except TypeError as e: # pragma: no cover
342 except TypeError as e: # pragma: no cover
343 raise EncodingError("Content-Type malformed, not allowed: %r; "
343 raise EncodingError("Content-Type malformed, not allowed: %r; "
344 "%r (Python ERROR: %s)" %
344 "%r (Python ERROR: %s)" %
345 (ctype, params, e.args[0]))
345 (ctype, params, e.args[0]))
346
346
347 for k in mail.keys():
347 for k in mail.keys():
348 if k in ADDRESS_HEADERS_WHITELIST:
348 if k in ADDRESS_HEADERS_WHITELIST:
349 out[k] = header_to_mime_encoding(
349 out[k] = header_to_mime_encoding(
350 mail[k],
350 mail[k],
351 not_email=False,
351 not_email=False,
352 separator=separator
352 separator=separator
353 )
353 )
354 else:
354 else:
355 out[k] = header_to_mime_encoding(
355 out[k] = header_to_mime_encoding(
356 mail[k],
356 mail[k],
357 not_email=True
357 not_email=True
358 )
358 )
359
359
360 out.extract_payload(mail)
360 out.extract_payload(mail)
361
361
362 # go through the children
362 # go through the children
363 for part in mail.parts:
363 for part in mail.parts:
364 out.attach(to_message(part))
364 out.attach(to_message(part))
365
365
366 return out
366 return out
367
367
368
368
369 class MIMEPart(MIMEBase):
369 class MIMEPart(MIMEBase):
370 """
370 """
371 A reimplementation of nearly everything in email.mime to be more useful
371 A reimplementation of nearly everything in email.mime to be more useful
372 for actually attaching things. Rather than one class for every type of
372 for actually attaching things. Rather than one class for every type of
373 thing you'd encode, there's just this one, and it figures out how to
373 thing you'd encode, there's just this one, and it figures out how to
374 encode what you ask it.
374 encode what you ask it.
375 """
375 """
376 def __init__(self, type, **params):
376 def __init__(self, type, **params):
377 self.maintype, self.subtype = type.split('/')
377 self.maintype, self.subtype = type.split('/')
378 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
378 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
379
379
380 def add_text(self, content):
380 def add_text(self, content):
381 # this is text, so encode it in canonical form
381 # this is text, so encode it in canonical form
382 try:
382 try:
383 encoded = content.encode('ascii')
383 encoded = content.encode('ascii')
384 charset = 'ascii'
384 charset = 'ascii'
385 except UnicodeError:
385 except UnicodeError:
386 encoded = content.encode('utf-8')
386 encoded = content.encode('utf-8')
387 charset = 'utf-8'
387 charset = 'utf-8'
388
388
389 self.set_payload(encoded, charset=charset)
389 self.set_payload(encoded, charset=charset)
390
390
391 def extract_payload(self, mail):
391 def extract_payload(self, mail):
392 if mail.body is None:
392 if mail.body is None:
393 return # only None, '' is still ok
393 return # only None, '' is still ok
394
394
395 ctype, ctype_params = mail.content_encoding['Content-Type']
395 ctype, _ctype_params = mail.content_encoding['Content-Type']
396 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
396 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
397
397
398 assert ctype, ("Extract payload requires that mail.content_encoding "
398 assert ctype, ("Extract payload requires that mail.content_encoding "
399 "have a valid Content-Type.")
399 "have a valid Content-Type.")
400
400
401 if ctype.startswith("text/"):
401 if ctype.startswith("text/"):
402 self.add_text(mail.body)
402 self.add_text(mail.body)
403 else:
403 else:
404 if cdisp:
404 if cdisp:
405 # replicate the content-disposition settings
405 # replicate the content-disposition settings
406 self.add_header('Content-Disposition', cdisp, **cdisp_params)
406 self.add_header('Content-Disposition', cdisp, **cdisp_params)
407
407
408 self.set_payload(mail.body)
408 self.set_payload(mail.body)
409 encoders.encode_base64(self)
409 encoders.encode_base64(self)
410
410
411 def __repr__(self):
411 def __repr__(self):
412 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
412 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
413 self.subtype,
413 self.subtype,
414 self.maintype,
414 self.maintype,
415 self['Content-Type'],
415 self['Content-Type'],
416 self['Content-Disposition'],
416 self['Content-Disposition'],
417 self.is_multipart())
417 self.is_multipart())
418
418
419
419
420 def header_to_mime_encoding(value, not_email=False, separator=", "):
420 def header_to_mime_encoding(value, not_email=False, separator=", "):
421 if not value:
421 if not value:
422 return ""
422 return ""
423
423
424 encoder = Charset(DEFAULT_ENCODING)
424 encoder = Charset(DEFAULT_ENCODING)
425 if isinstance(value, list):
425 if isinstance(value, list):
426 return separator.join(properly_encode_header(
426 return separator.join(properly_encode_header(
427 v, encoder, not_email) for v in value)
427 v, encoder, not_email) for v in value)
428 else:
428 else:
429 return properly_encode_header(value, encoder, not_email)
429 return properly_encode_header(value, encoder, not_email)
430
430
431
431
432 def properly_encode_header(value, encoder, not_email):
432 def properly_encode_header(value, encoder, not_email):
433 """
433 """
434 The only thing special (weird) about this function is that it tries
434 The only thing special (weird) about this function is that it tries
435 to do a fast check to see if the header value has an email address in
435 to do a fast check to see if the header value has an email address in
436 it. Since random headers could have an email address, and email addresses
436 it. Since random headers could have an email address, and email addresses
437 have weird special formatting rules, we have to check for it.
437 have weird special formatting rules, we have to check for it.
438
438
439 Normally this works fine, but in Librelist, we need to "obfuscate" email
439 Normally this works fine, but in Librelist, we need to "obfuscate" email
440 addresses by changing the '@' to '-AT-'. This is where
440 addresses by changing the '@' to '-AT-'. This is where
441 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
441 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
442 to check if a header value has an email address. If you need to make this
442 to check if a header value has an email address. If you need to make this
443 check different, then change this.
443 check different, then change this.
444 """
444 """
445 try:
445 try:
446 value.encode("ascii")
446 value.encode("ascii")
447 return value
447 return value
448 except UnicodeError:
448 except UnicodeError:
449 if not not_email and VALUE_IS_EMAIL_ADDRESS(value):
449 if not not_email and VALUE_IS_EMAIL_ADDRESS(value):
450 # this could have an email address, make sure we don't screw it up
450 # this could have an email address, make sure we don't screw it up
451 name, address = parseaddr(value)
451 name, address = parseaddr(value)
452 return '"%s" <%s>' % (encoder.header_encode(name), address)
452 return '"%s" <%s>' % (encoder.header_encode(name), address)
453
453
454 return encoder.header_encode(value)
454 return encoder.header_encode(value)
@@ -1,1057 +1,1055 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import itertools
13 import itertools
14
14
15 from kallithea.lib.vcs.conf import settings
15 from kallithea.lib.vcs.conf import settings
16 from kallithea.lib.vcs.exceptions import (
16 from kallithea.lib.vcs.exceptions import (
17 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
17 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
18 from kallithea.lib.vcs.utils import author_email, author_name
18 from kallithea.lib.vcs.utils import author_email, author_name
19 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
19 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
20 from kallithea.lib.vcs.utils.lazy import LazyProperty
20 from kallithea.lib.vcs.utils.lazy import LazyProperty
21
21
22
22
23 class BaseRepository(object):
23 class BaseRepository(object):
24 """
24 """
25 Base Repository for final backends
25 Base Repository for final backends
26
26
27 **Attributes**
27 **Attributes**
28
28
29 ``DEFAULT_BRANCH_NAME``
29 ``DEFAULT_BRANCH_NAME``
30 name of default branch (i.e. "trunk" for svn, "master" for git etc.
30 name of default branch (i.e. "trunk" for svn, "master" for git etc.
31
31
32 ``scm``
32 ``scm``
33 alias of scm, i.e. *git* or *hg*
33 alias of scm, i.e. *git* or *hg*
34
34
35 ``repo``
35 ``repo``
36 object from external api
36 object from external api
37
37
38 ``revisions``
38 ``revisions``
39 list of all available revisions' ids, in ascending order
39 list of all available revisions' ids, in ascending order
40
40
41 ``changesets``
41 ``changesets``
42 storage dict caching returned changesets
42 storage dict caching returned changesets
43
43
44 ``path``
44 ``path``
45 absolute path to the repository
45 absolute path to the repository
46
46
47 ``branches``
47 ``branches``
48 branches as list of changesets
48 branches as list of changesets
49
49
50 ``tags``
50 ``tags``
51 tags as list of changesets
51 tags as list of changesets
52 """
52 """
53 scm = None
53 scm = None
54 DEFAULT_BRANCH_NAME = None
54 DEFAULT_BRANCH_NAME = None
55 EMPTY_CHANGESET = '0' * 40
55 EMPTY_CHANGESET = '0' * 40
56
56
57 def __init__(self, repo_path, create=False, **kwargs):
57 def __init__(self, repo_path, create=False, **kwargs):
58 """
58 """
59 Initializes repository. Raises RepositoryError if repository could
59 Initializes repository. Raises RepositoryError if repository could
60 not be find at the given ``repo_path`` or directory at ``repo_path``
60 not be find at the given ``repo_path`` or directory at ``repo_path``
61 exists and ``create`` is set to True.
61 exists and ``create`` is set to True.
62
62
63 :param repo_path: local path of the repository
63 :param repo_path: local path of the repository
64 :param create=False: if set to True, would try to create repository.
64 :param create=False: if set to True, would try to create repository.
65 :param src_url=None: if set, should be proper url from which repository
65 :param src_url=None: if set, should be proper url from which repository
66 would be cloned; requires ``create`` parameter to be set to True -
66 would be cloned; requires ``create`` parameter to be set to True -
67 raises RepositoryError if src_url is set and create evaluates to
67 raises RepositoryError if src_url is set and create evaluates to
68 False
68 False
69 """
69 """
70 raise NotImplementedError
70 raise NotImplementedError
71
71
72 def __str__(self):
72 def __str__(self):
73 return '<%s at %s>' % (self.__class__.__name__, self.path)
73 return '<%s at %s>' % (self.__class__.__name__, self.path)
74
74
75 def __repr__(self):
75 def __repr__(self):
76 return self.__str__()
76 return self.__str__()
77
77
78 def __len__(self):
78 def __len__(self):
79 return self.count()
79 return self.count()
80
80
81 def __eq__(self, other):
81 def __eq__(self, other):
82 same_instance = isinstance(other, self.__class__)
82 same_instance = isinstance(other, self.__class__)
83 return same_instance and getattr(other, 'path', None) == self.path
83 return same_instance and getattr(other, 'path', None) == self.path
84
84
85 def __ne__(self, other):
85 def __ne__(self, other):
86 return not self.__eq__(other)
86 return not self.__eq__(other)
87
87
88 @LazyProperty
88 @LazyProperty
89 def alias(self):
89 def alias(self):
90 for k, v in settings.BACKENDS.items():
90 for k, v in settings.BACKENDS.items():
91 if v.split('.')[-1] == str(self.__class__.__name__):
91 if v.split('.')[-1] == str(self.__class__.__name__):
92 return k
92 return k
93
93
94 @LazyProperty
94 @LazyProperty
95 def name(self):
95 def name(self):
96 """
96 """
97 Return repository name (without group name)
97 Return repository name (without group name)
98 """
98 """
99 raise NotImplementedError
99 raise NotImplementedError
100
100
101 @LazyProperty
101 @LazyProperty
102 def owner(self):
102 def owner(self):
103 raise NotImplementedError
103 raise NotImplementedError
104
104
105 @LazyProperty
105 @LazyProperty
106 def description(self):
106 def description(self):
107 raise NotImplementedError
107 raise NotImplementedError
108
108
109 @LazyProperty
109 @LazyProperty
110 def size(self):
110 def size(self):
111 """
111 """
112 Returns combined size in bytes for all repository files
112 Returns combined size in bytes for all repository files
113 """
113 """
114
114
115 size = 0
115 size = 0
116 try:
116 try:
117 tip = self.get_changeset()
117 tip = self.get_changeset()
118 for topnode, dirs, files in tip.walk('/'):
118 for topnode, dirs, files in tip.walk('/'):
119 for f in files:
119 for f in files:
120 size += tip.get_file_size(f.path)
120 size += tip.get_file_size(f.path)
121
121
122 except RepositoryError as e:
122 except RepositoryError as e:
123 pass
123 pass
124 return size
124 return size
125
125
126 def is_valid(self):
126 def is_valid(self):
127 """
127 """
128 Validates repository.
128 Validates repository.
129 """
129 """
130 raise NotImplementedError
130 raise NotImplementedError
131
131
132 def is_empty(self):
132 def is_empty(self):
133 return self._empty
133 return self._empty
134
134
135 #==========================================================================
135 #==========================================================================
136 # CHANGESETS
136 # CHANGESETS
137 #==========================================================================
137 #==========================================================================
138
138
139 def get_changeset(self, revision=None):
139 def get_changeset(self, revision=None):
140 """
140 """
141 Returns instance of ``Changeset`` class. If ``revision`` is None, most
141 Returns instance of ``Changeset`` class. If ``revision`` is None, most
142 recent changeset is returned.
142 recent changeset is returned.
143
143
144 :raises ``EmptyRepositoryError``: if there are no revisions
144 :raises ``EmptyRepositoryError``: if there are no revisions
145 """
145 """
146 raise NotImplementedError
146 raise NotImplementedError
147
147
148 def __iter__(self):
148 def __iter__(self):
149 """
149 """
150 Allows Repository objects to be iterated.
150 Allows Repository objects to be iterated.
151
151
152 *Requires* implementation of ``__getitem__`` method.
152 *Requires* implementation of ``__getitem__`` method.
153 """
153 """
154 for revision in self.revisions:
154 for revision in self.revisions:
155 yield self.get_changeset(revision)
155 yield self.get_changeset(revision)
156
156
157 def get_changesets(self, start=None, end=None, start_date=None,
157 def get_changesets(self, start=None, end=None, start_date=None,
158 end_date=None, branch_name=None, reverse=False, max_revisions=None):
158 end_date=None, branch_name=None, reverse=False, max_revisions=None):
159 """
159 """
160 Returns iterator of ``BaseChangeset`` objects from start to end,
160 Returns iterator of ``BaseChangeset`` objects from start to end,
161 both inclusive.
161 both inclusive.
162
162
163 :param start: None or str
163 :param start: None or str
164 :param end: None or str
164 :param end: None or str
165 :param start_date:
165 :param start_date:
166 :param end_date:
166 :param end_date:
167 :param branch_name:
167 :param branch_name:
168 :param reversed:
168 :param reversed:
169 """
169 """
170 raise NotImplementedError
170 raise NotImplementedError
171
171
172 def __getitem__(self, key):
172 def __getitem__(self, key):
173 if isinstance(key, slice):
173 if isinstance(key, slice):
174 return (self.get_changeset(rev) for rev in self.revisions[key])
174 return (self.get_changeset(rev) for rev in self.revisions[key])
175 return self.get_changeset(key)
175 return self.get_changeset(key)
176
176
177 def count(self):
177 def count(self):
178 return len(self.revisions)
178 return len(self.revisions)
179
179
180 def tag(self, name, user, revision=None, message=None, date=None, **opts):
180 def tag(self, name, user, revision=None, message=None, date=None, **opts):
181 """
181 """
182 Creates and returns a tag for the given ``revision``.
182 Creates and returns a tag for the given ``revision``.
183
183
184 :param name: name for new tag
184 :param name: name for new tag
185 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
185 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
186 :param revision: changeset id for which new tag would be created
186 :param revision: changeset id for which new tag would be created
187 :param message: message of the tag's commit
187 :param message: message of the tag's commit
188 :param date: date of tag's commit
188 :param date: date of tag's commit
189
189
190 :raises TagAlreadyExistError: if tag with same name already exists
190 :raises TagAlreadyExistError: if tag with same name already exists
191 """
191 """
192 raise NotImplementedError
192 raise NotImplementedError
193
193
194 def remove_tag(self, name, user, message=None, date=None):
194 def remove_tag(self, name, user, message=None, date=None):
195 """
195 """
196 Removes tag with the given ``name``.
196 Removes tag with the given ``name``.
197
197
198 :param name: name of the tag to be removed
198 :param name: name of the tag to be removed
199 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
199 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param message: message of the tag's removal commit
200 :param message: message of the tag's removal commit
201 :param date: date of tag's removal commit
201 :param date: date of tag's removal commit
202
202
203 :raises TagDoesNotExistError: if tag with given name does not exists
203 :raises TagDoesNotExistError: if tag with given name does not exists
204 """
204 """
205 raise NotImplementedError
205 raise NotImplementedError
206
206
207 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
207 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
208 context=3):
208 context=3):
209 """
209 """
210 Returns (git like) *diff*, as plain text. Shows changes introduced by
210 Returns (git like) *diff*, as plain text. Shows changes introduced by
211 ``rev2`` since ``rev1``.
211 ``rev2`` since ``rev1``.
212
212
213 :param rev1: Entry point from which diff is shown. Can be
213 :param rev1: Entry point from which diff is shown. Can be
214 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
214 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
215 the changes since empty state of the repository until ``rev2``
215 the changes since empty state of the repository until ``rev2``
216 :param rev2: Until which revision changes should be shown.
216 :param rev2: Until which revision changes should be shown.
217 :param ignore_whitespace: If set to ``True``, would not show whitespace
217 :param ignore_whitespace: If set to ``True``, would not show whitespace
218 changes. Defaults to ``False``.
218 changes. Defaults to ``False``.
219 :param context: How many lines before/after changed lines should be
219 :param context: How many lines before/after changed lines should be
220 shown. Defaults to ``3``.
220 shown. Defaults to ``3``.
221 """
221 """
222 raise NotImplementedError
222 raise NotImplementedError
223
223
224 # ========== #
224 # ========== #
225 # COMMIT API #
225 # COMMIT API #
226 # ========== #
226 # ========== #
227
227
228 @LazyProperty
228 @LazyProperty
229 def in_memory_changeset(self):
229 def in_memory_changeset(self):
230 """
230 """
231 Returns ``InMemoryChangeset`` object for this repository.
231 Returns ``InMemoryChangeset`` object for this repository.
232 """
232 """
233 raise NotImplementedError
233 raise NotImplementedError
234
234
235 def add(self, filenode, **kwargs):
235 def add(self, filenode, **kwargs):
236 """
236 """
237 Commit api function that will add given ``FileNode`` into this
237 Commit api function that will add given ``FileNode`` into this
238 repository.
238 repository.
239
239
240 :raises ``NodeAlreadyExistsError``: if there is a file with same path
240 :raises ``NodeAlreadyExistsError``: if there is a file with same path
241 already in repository
241 already in repository
242 :raises ``NodeAlreadyAddedError``: if given node is already marked as
242 :raises ``NodeAlreadyAddedError``: if given node is already marked as
243 *added*
243 *added*
244 """
244 """
245 raise NotImplementedError
245 raise NotImplementedError
246
246
247 def remove(self, filenode, **kwargs):
247 def remove(self, filenode, **kwargs):
248 """
248 """
249 Commit api function that will remove given ``FileNode`` into this
249 Commit api function that will remove given ``FileNode`` into this
250 repository.
250 repository.
251
251
252 :raises ``EmptyRepositoryError``: if there are no changesets yet
252 :raises ``EmptyRepositoryError``: if there are no changesets yet
253 :raises ``NodeDoesNotExistError``: if there is no file with given path
253 :raises ``NodeDoesNotExistError``: if there is no file with given path
254 """
254 """
255 raise NotImplementedError
255 raise NotImplementedError
256
256
257 def commit(self, message, **kwargs):
257 def commit(self, message, **kwargs):
258 """
258 """
259 Persists current changes made on this repository and returns newly
259 Persists current changes made on this repository and returns newly
260 created changeset.
260 created changeset.
261
262 :raises ``NothingChangedError``: if no changes has been made
263 """
261 """
264 raise NotImplementedError
262 raise NotImplementedError
265
263
266 def get_state(self):
264 def get_state(self):
267 """
265 """
268 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
266 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
269 containing ``FileNode`` objects.
267 containing ``FileNode`` objects.
270 """
268 """
271 raise NotImplementedError
269 raise NotImplementedError
272
270
273 def get_config_value(self, section, name, config_file=None):
271 def get_config_value(self, section, name, config_file=None):
274 """
272 """
275 Returns configuration value for a given [``section``] and ``name``.
273 Returns configuration value for a given [``section``] and ``name``.
276
274
277 :param section: Section we want to retrieve value from
275 :param section: Section we want to retrieve value from
278 :param name: Name of configuration we want to retrieve
276 :param name: Name of configuration we want to retrieve
279 :param config_file: A path to file which should be used to retrieve
277 :param config_file: A path to file which should be used to retrieve
280 configuration from (might also be a list of file paths)
278 configuration from (might also be a list of file paths)
281 """
279 """
282 raise NotImplementedError
280 raise NotImplementedError
283
281
284 def get_user_name(self, config_file=None):
282 def get_user_name(self, config_file=None):
285 """
283 """
286 Returns user's name from global configuration file.
284 Returns user's name from global configuration file.
287
285
288 :param config_file: A path to file which should be used to retrieve
286 :param config_file: A path to file which should be used to retrieve
289 configuration from (might also be a list of file paths)
287 configuration from (might also be a list of file paths)
290 """
288 """
291 raise NotImplementedError
289 raise NotImplementedError
292
290
293 def get_user_email(self, config_file=None):
291 def get_user_email(self, config_file=None):
294 """
292 """
295 Returns user's email from global configuration file.
293 Returns user's email from global configuration file.
296
294
297 :param config_file: A path to file which should be used to retrieve
295 :param config_file: A path to file which should be used to retrieve
298 configuration from (might also be a list of file paths)
296 configuration from (might also be a list of file paths)
299 """
297 """
300 raise NotImplementedError
298 raise NotImplementedError
301
299
302 # =========== #
300 # =========== #
303 # WORKDIR API #
301 # WORKDIR API #
304 # =========== #
302 # =========== #
305
303
306 @LazyProperty
304 @LazyProperty
307 def workdir(self):
305 def workdir(self):
308 """
306 """
309 Returns ``Workdir`` instance for this repository.
307 Returns ``Workdir`` instance for this repository.
310 """
308 """
311 raise NotImplementedError
309 raise NotImplementedError
312
310
313
311
314 class BaseChangeset(object):
312 class BaseChangeset(object):
315 """
313 """
316 Each backend should implement it's changeset representation.
314 Each backend should implement it's changeset representation.
317
315
318 **Attributes**
316 **Attributes**
319
317
320 ``repository``
318 ``repository``
321 repository object within which changeset exists
319 repository object within which changeset exists
322
320
323 ``raw_id``
321 ``raw_id``
324 raw changeset representation (i.e. full 40 length sha for git
322 raw changeset representation (i.e. full 40 length sha for git
325 backend)
323 backend)
326
324
327 ``short_id``
325 ``short_id``
328 shortened (if apply) version of ``raw_id``; it would be simple
326 shortened (if apply) version of ``raw_id``; it would be simple
329 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
327 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
330 as ``raw_id`` for subversion
328 as ``raw_id`` for subversion
331
329
332 ``revision``
330 ``revision``
333 revision number as integer
331 revision number as integer
334
332
335 ``files``
333 ``files``
336 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
334 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
337
335
338 ``dirs``
336 ``dirs``
339 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
337 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
340
338
341 ``nodes``
339 ``nodes``
342 combined list of ``Node`` objects
340 combined list of ``Node`` objects
343
341
344 ``author``
342 ``author``
345 author of the changeset, as str
343 author of the changeset, as str
346
344
347 ``message``
345 ``message``
348 message of the changeset, as str
346 message of the changeset, as str
349
347
350 ``parents``
348 ``parents``
351 list of parent changesets
349 list of parent changesets
352
350
353 ``last``
351 ``last``
354 ``True`` if this is last changeset in repository, ``False``
352 ``True`` if this is last changeset in repository, ``False``
355 otherwise; trying to access this attribute while there is no
353 otherwise; trying to access this attribute while there is no
356 changesets would raise ``EmptyRepositoryError``
354 changesets would raise ``EmptyRepositoryError``
357 """
355 """
358 def __str__(self):
356 def __str__(self):
359 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
357 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
360 self.short_id)
358 self.short_id)
361
359
362 def __repr__(self):
360 def __repr__(self):
363 return self.__str__()
361 return self.__str__()
364
362
365 def __eq__(self, other):
363 def __eq__(self, other):
366 if type(self) is not type(other):
364 if type(self) is not type(other):
367 return False
365 return False
368 return self.raw_id == other.raw_id
366 return self.raw_id == other.raw_id
369
367
370 def __json__(self, with_file_list=False):
368 def __json__(self, with_file_list=False):
371 if with_file_list:
369 if with_file_list:
372 return dict(
370 return dict(
373 short_id=self.short_id,
371 short_id=self.short_id,
374 raw_id=self.raw_id,
372 raw_id=self.raw_id,
375 revision=self.revision,
373 revision=self.revision,
376 message=self.message,
374 message=self.message,
377 date=self.date,
375 date=self.date,
378 author=self.author,
376 author=self.author,
379 added=[el.path for el in self.added],
377 added=[el.path for el in self.added],
380 changed=[el.path for el in self.changed],
378 changed=[el.path for el in self.changed],
381 removed=[el.path for el in self.removed],
379 removed=[el.path for el in self.removed],
382 )
380 )
383 else:
381 else:
384 return dict(
382 return dict(
385 short_id=self.short_id,
383 short_id=self.short_id,
386 raw_id=self.raw_id,
384 raw_id=self.raw_id,
387 revision=self.revision,
385 revision=self.revision,
388 message=self.message,
386 message=self.message,
389 date=self.date,
387 date=self.date,
390 author=self.author,
388 author=self.author,
391 )
389 )
392
390
393 @LazyProperty
391 @LazyProperty
394 def last(self):
392 def last(self):
395 if self.repository is None:
393 if self.repository is None:
396 raise ChangesetError("Cannot check if it's most recent revision")
394 raise ChangesetError("Cannot check if it's most recent revision")
397 return self.raw_id == self.repository.revisions[-1]
395 return self.raw_id == self.repository.revisions[-1]
398
396
399 @LazyProperty
397 @LazyProperty
400 def parents(self):
398 def parents(self):
401 """
399 """
402 Returns list of parents changesets.
400 Returns list of parents changesets.
403 """
401 """
404 raise NotImplementedError
402 raise NotImplementedError
405
403
406 @LazyProperty
404 @LazyProperty
407 def children(self):
405 def children(self):
408 """
406 """
409 Returns list of children changesets.
407 Returns list of children changesets.
410 """
408 """
411 raise NotImplementedError
409 raise NotImplementedError
412
410
413 @LazyProperty
411 @LazyProperty
414 def raw_id(self):
412 def raw_id(self):
415 """
413 """
416 Returns raw string identifying this changeset.
414 Returns raw string identifying this changeset.
417 """
415 """
418 raise NotImplementedError
416 raise NotImplementedError
419
417
420 @LazyProperty
418 @LazyProperty
421 def short_id(self):
419 def short_id(self):
422 """
420 """
423 Returns shortened version of ``raw_id`` attribute, as string,
421 Returns shortened version of ``raw_id`` attribute, as string,
424 identifying this changeset, useful for web representation.
422 identifying this changeset, useful for web representation.
425 """
423 """
426 raise NotImplementedError
424 raise NotImplementedError
427
425
428 @LazyProperty
426 @LazyProperty
429 def revision(self):
427 def revision(self):
430 """
428 """
431 Returns integer identifying this changeset.
429 Returns integer identifying this changeset.
432
430
433 """
431 """
434 raise NotImplementedError
432 raise NotImplementedError
435
433
436 @LazyProperty
434 @LazyProperty
437 def committer(self):
435 def committer(self):
438 """
436 """
439 Returns Committer for given commit
437 Returns Committer for given commit
440 """
438 """
441
439
442 raise NotImplementedError
440 raise NotImplementedError
443
441
444 @LazyProperty
442 @LazyProperty
445 def committer_name(self):
443 def committer_name(self):
446 """
444 """
447 Returns Author name for given commit
445 Returns Author name for given commit
448 """
446 """
449
447
450 return author_name(self.committer)
448 return author_name(self.committer)
451
449
452 @LazyProperty
450 @LazyProperty
453 def committer_email(self):
451 def committer_email(self):
454 """
452 """
455 Returns Author email address for given commit
453 Returns Author email address for given commit
456 """
454 """
457
455
458 return author_email(self.committer)
456 return author_email(self.committer)
459
457
460 @LazyProperty
458 @LazyProperty
461 def author(self):
459 def author(self):
462 """
460 """
463 Returns Author for given commit
461 Returns Author for given commit
464 """
462 """
465
463
466 raise NotImplementedError
464 raise NotImplementedError
467
465
468 @LazyProperty
466 @LazyProperty
469 def author_name(self):
467 def author_name(self):
470 """
468 """
471 Returns Author name for given commit
469 Returns Author name for given commit
472 """
470 """
473
471
474 return author_name(self.author)
472 return author_name(self.author)
475
473
476 @LazyProperty
474 @LazyProperty
477 def author_email(self):
475 def author_email(self):
478 """
476 """
479 Returns Author email address for given commit
477 Returns Author email address for given commit
480 """
478 """
481
479
482 return author_email(self.author)
480 return author_email(self.author)
483
481
484 def get_file_mode(self, path):
482 def get_file_mode(self, path):
485 """
483 """
486 Returns stat mode of the file at the given ``path``.
484 Returns stat mode of the file at the given ``path``.
487 """
485 """
488 raise NotImplementedError
486 raise NotImplementedError
489
487
490 def get_file_content(self, path):
488 def get_file_content(self, path):
491 """
489 """
492 Returns content of the file at the given ``path``.
490 Returns content of the file at the given ``path``.
493 """
491 """
494 raise NotImplementedError
492 raise NotImplementedError
495
493
496 def get_file_size(self, path):
494 def get_file_size(self, path):
497 """
495 """
498 Returns size of the file at the given ``path``.
496 Returns size of the file at the given ``path``.
499 """
497 """
500 raise NotImplementedError
498 raise NotImplementedError
501
499
502 def get_file_changeset(self, path):
500 def get_file_changeset(self, path):
503 """
501 """
504 Returns last commit of the file at the given ``path``.
502 Returns last commit of the file at the given ``path``.
505 """
503 """
506 raise NotImplementedError
504 raise NotImplementedError
507
505
508 def get_file_history(self, path):
506 def get_file_history(self, path):
509 """
507 """
510 Returns history of file as reversed list of ``Changeset`` objects for
508 Returns history of file as reversed list of ``Changeset`` objects for
511 which file at given ``path`` has been modified.
509 which file at given ``path`` has been modified.
512 """
510 """
513 raise NotImplementedError
511 raise NotImplementedError
514
512
515 def get_nodes(self, path):
513 def get_nodes(self, path):
516 """
514 """
517 Returns combined ``DirNode`` and ``FileNode`` objects list representing
515 Returns combined ``DirNode`` and ``FileNode`` objects list representing
518 state of changeset at the given ``path``.
516 state of changeset at the given ``path``.
519
517
520 :raises ``ChangesetError``: if node at the given ``path`` is not
518 :raises ``ChangesetError``: if node at the given ``path`` is not
521 instance of ``DirNode``
519 instance of ``DirNode``
522 """
520 """
523 raise NotImplementedError
521 raise NotImplementedError
524
522
525 def get_node(self, path):
523 def get_node(self, path):
526 """
524 """
527 Returns ``Node`` object from the given ``path``.
525 Returns ``Node`` object from the given ``path``.
528
526
529 :raises ``NodeDoesNotExistError``: if there is no node at the given
527 :raises ``NodeDoesNotExistError``: if there is no node at the given
530 ``path``
528 ``path``
531 """
529 """
532 raise NotImplementedError
530 raise NotImplementedError
533
531
534 def fill_archive(self, stream=None, kind='tgz', prefix=None):
532 def fill_archive(self, stream=None, kind='tgz', prefix=None):
535 """
533 """
536 Fills up given stream.
534 Fills up given stream.
537
535
538 :param stream: file like object.
536 :param stream: file like object.
539 :param kind: one of following: ``zip``, ``tar``, ``tgz``
537 :param kind: one of following: ``zip``, ``tar``, ``tgz``
540 or ``tbz2``. Default: ``tgz``.
538 or ``tbz2``. Default: ``tgz``.
541 :param prefix: name of root directory in archive.
539 :param prefix: name of root directory in archive.
542 Default is repository name and changeset's raw_id joined with dash.
540 Default is repository name and changeset's raw_id joined with dash.
543
541
544 repo-tip.<kind>
542 repo-tip.<kind>
545 """
543 """
546
544
547 raise NotImplementedError
545 raise NotImplementedError
548
546
549 def get_chunked_archive(self, **kwargs):
547 def get_chunked_archive(self, **kwargs):
550 """
548 """
551 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
549 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
552
550
553 :param chunk_size: extra parameter which controls size of returned
551 :param chunk_size: extra parameter which controls size of returned
554 chunks. Default:8k.
552 chunks. Default:8k.
555 """
553 """
556
554
557 chunk_size = kwargs.pop('chunk_size', 8192)
555 chunk_size = kwargs.pop('chunk_size', 8192)
558 stream = kwargs.get('stream')
556 stream = kwargs.get('stream')
559 self.fill_archive(**kwargs)
557 self.fill_archive(**kwargs)
560 while True:
558 while True:
561 data = stream.read(chunk_size)
559 data = stream.read(chunk_size)
562 if not data:
560 if not data:
563 break
561 break
564 yield data
562 yield data
565
563
566 @LazyProperty
564 @LazyProperty
567 def root(self):
565 def root(self):
568 """
566 """
569 Returns ``RootNode`` object for this changeset.
567 Returns ``RootNode`` object for this changeset.
570 """
568 """
571 return self.get_node('')
569 return self.get_node('')
572
570
573 def next(self, branch=None):
571 def next(self, branch=None):
574 """
572 """
575 Returns next changeset from current, if branch is gives it will return
573 Returns next changeset from current, if branch is gives it will return
576 next changeset belonging to this branch
574 next changeset belonging to this branch
577
575
578 :param branch: show changesets within the given named branch
576 :param branch: show changesets within the given named branch
579 """
577 """
580 raise NotImplementedError
578 raise NotImplementedError
581
579
582 def prev(self, branch=None):
580 def prev(self, branch=None):
583 """
581 """
584 Returns previous changeset from current, if branch is gives it will
582 Returns previous changeset from current, if branch is gives it will
585 return previous changeset belonging to this branch
583 return previous changeset belonging to this branch
586
584
587 :param branch: show changesets within the given named branch
585 :param branch: show changesets within the given named branch
588 """
586 """
589 raise NotImplementedError
587 raise NotImplementedError
590
588
591 @LazyProperty
589 @LazyProperty
592 def added(self):
590 def added(self):
593 """
591 """
594 Returns list of added ``FileNode`` objects.
592 Returns list of added ``FileNode`` objects.
595 """
593 """
596 raise NotImplementedError
594 raise NotImplementedError
597
595
598 @LazyProperty
596 @LazyProperty
599 def changed(self):
597 def changed(self):
600 """
598 """
601 Returns list of modified ``FileNode`` objects.
599 Returns list of modified ``FileNode`` objects.
602 """
600 """
603 raise NotImplementedError
601 raise NotImplementedError
604
602
605 @LazyProperty
603 @LazyProperty
606 def removed(self):
604 def removed(self):
607 """
605 """
608 Returns list of removed ``FileNode`` objects.
606 Returns list of removed ``FileNode`` objects.
609 """
607 """
610 raise NotImplementedError
608 raise NotImplementedError
611
609
612 @LazyProperty
610 @LazyProperty
613 def size(self):
611 def size(self):
614 """
612 """
615 Returns total number of bytes from contents of all filenodes.
613 Returns total number of bytes from contents of all filenodes.
616 """
614 """
617 return sum((node.size for node in self.get_filenodes_generator()))
615 return sum((node.size for node in self.get_filenodes_generator()))
618
616
619 def walk(self, topurl=''):
617 def walk(self, topurl=''):
620 """
618 """
621 Similar to os.walk method. Instead of filesystem it walks through
619 Similar to os.walk method. Instead of filesystem it walks through
622 changeset starting at given ``topurl``. Returns generator of tuples
620 changeset starting at given ``topurl``. Returns generator of tuples
623 (topnode, dirnodes, filenodes).
621 (topnode, dirnodes, filenodes).
624 """
622 """
625 topnode = self.get_node(topurl)
623 topnode = self.get_node(topurl)
626 yield (topnode, topnode.dirs, topnode.files)
624 yield (topnode, topnode.dirs, topnode.files)
627 for dirnode in topnode.dirs:
625 for dirnode in topnode.dirs:
628 for tup in self.walk(dirnode.path):
626 for tup in self.walk(dirnode.path):
629 yield tup
627 yield tup
630
628
631 def get_filenodes_generator(self):
629 def get_filenodes_generator(self):
632 """
630 """
633 Returns generator that yields *all* file nodes.
631 Returns generator that yields *all* file nodes.
634 """
632 """
635 for topnode, dirs, files in self.walk():
633 for topnode, dirs, files in self.walk():
636 for node in files:
634 for node in files:
637 yield node
635 yield node
638
636
639 def as_dict(self):
637 def as_dict(self):
640 """
638 """
641 Returns dictionary with changeset's attributes and their values.
639 Returns dictionary with changeset's attributes and their values.
642 """
640 """
643 data = get_dict_for_attrs(self, ['raw_id', 'short_id',
641 data = get_dict_for_attrs(self, ['raw_id', 'short_id',
644 'revision', 'date', 'message'])
642 'revision', 'date', 'message'])
645 data['author'] = {'name': self.author_name, 'email': self.author_email}
643 data['author'] = {'name': self.author_name, 'email': self.author_email}
646 data['added'] = [node.path for node in self.added]
644 data['added'] = [node.path for node in self.added]
647 data['changed'] = [node.path for node in self.changed]
645 data['changed'] = [node.path for node in self.changed]
648 data['removed'] = [node.path for node in self.removed]
646 data['removed'] = [node.path for node in self.removed]
649 return data
647 return data
650
648
651 @LazyProperty
649 @LazyProperty
652 def closesbranch(self):
650 def closesbranch(self):
653 return False
651 return False
654
652
655 @LazyProperty
653 @LazyProperty
656 def obsolete(self):
654 def obsolete(self):
657 return False
655 return False
658
656
659 @LazyProperty
657 @LazyProperty
660 def bumped(self):
658 def bumped(self):
661 return False
659 return False
662
660
663 @LazyProperty
661 @LazyProperty
664 def divergent(self):
662 def divergent(self):
665 return False
663 return False
666
664
667 @LazyProperty
665 @LazyProperty
668 def extinct(self):
666 def extinct(self):
669 return False
667 return False
670
668
671 @LazyProperty
669 @LazyProperty
672 def unstable(self):
670 def unstable(self):
673 return False
671 return False
674
672
675 @LazyProperty
673 @LazyProperty
676 def phase(self):
674 def phase(self):
677 return ''
675 return ''
678
676
679
677
680 class BaseWorkdir(object):
678 class BaseWorkdir(object):
681 """
679 """
682 Working directory representation of single repository.
680 Working directory representation of single repository.
683
681
684 :attribute: repository: repository object of working directory
682 :attribute: repository: repository object of working directory
685 """
683 """
686
684
687 def __init__(self, repository):
685 def __init__(self, repository):
688 self.repository = repository
686 self.repository = repository
689
687
690 def get_branch(self):
688 def get_branch(self):
691 """
689 """
692 Returns name of current branch.
690 Returns name of current branch.
693 """
691 """
694 raise NotImplementedError
692 raise NotImplementedError
695
693
696 def get_changeset(self):
694 def get_changeset(self):
697 """
695 """
698 Returns current changeset.
696 Returns current changeset.
699 """
697 """
700 raise NotImplementedError
698 raise NotImplementedError
701
699
702 def get_added(self):
700 def get_added(self):
703 """
701 """
704 Returns list of ``FileNode`` objects marked as *new* in working
702 Returns list of ``FileNode`` objects marked as *new* in working
705 directory.
703 directory.
706 """
704 """
707 raise NotImplementedError
705 raise NotImplementedError
708
706
709 def get_changed(self):
707 def get_changed(self):
710 """
708 """
711 Returns list of ``FileNode`` objects *changed* in working directory.
709 Returns list of ``FileNode`` objects *changed* in working directory.
712 """
710 """
713 raise NotImplementedError
711 raise NotImplementedError
714
712
715 def get_removed(self):
713 def get_removed(self):
716 """
714 """
717 Returns list of ``RemovedFileNode`` objects marked as *removed* in
715 Returns list of ``RemovedFileNode`` objects marked as *removed* in
718 working directory.
716 working directory.
719 """
717 """
720 raise NotImplementedError
718 raise NotImplementedError
721
719
722 def get_untracked(self):
720 def get_untracked(self):
723 """
721 """
724 Returns list of ``FileNode`` objects which are present within working
722 Returns list of ``FileNode`` objects which are present within working
725 directory however are not tracked by repository.
723 directory however are not tracked by repository.
726 """
724 """
727 raise NotImplementedError
725 raise NotImplementedError
728
726
729 def get_status(self):
727 def get_status(self):
730 """
728 """
731 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
729 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
732 lists.
730 lists.
733 """
731 """
734 raise NotImplementedError
732 raise NotImplementedError
735
733
736 def commit(self, message, **kwargs):
734 def commit(self, message, **kwargs):
737 """
735 """
738 Commits local (from working directory) changes and returns newly
736 Commits local (from working directory) changes and returns newly
739 created
737 created
740 ``Changeset``. Updates repository's ``revisions`` list.
738 ``Changeset``. Updates repository's ``revisions`` list.
741
739
742 :raises ``CommitError``: if any error occurs while committing
740 :raises ``CommitError``: if any error occurs while committing
743 """
741 """
744 raise NotImplementedError
742 raise NotImplementedError
745
743
746 def update(self, revision=None):
744 def update(self, revision=None):
747 """
745 """
748 Fetches content of the given revision and populates it within working
746 Fetches content of the given revision and populates it within working
749 directory.
747 directory.
750 """
748 """
751 raise NotImplementedError
749 raise NotImplementedError
752
750
753 def checkout_branch(self, branch=None):
751 def checkout_branch(self, branch=None):
754 """
752 """
755 Checks out ``branch`` or the backend's default branch.
753 Checks out ``branch`` or the backend's default branch.
756
754
757 Raises ``BranchDoesNotExistError`` if the branch does not exist.
755 Raises ``BranchDoesNotExistError`` if the branch does not exist.
758 """
756 """
759 raise NotImplementedError
757 raise NotImplementedError
760
758
761
759
762 class BaseInMemoryChangeset(object):
760 class BaseInMemoryChangeset(object):
763 """
761 """
764 Represents differences between repository's state (most recent head) and
762 Represents differences between repository's state (most recent head) and
765 changes made *in place*.
763 changes made *in place*.
766
764
767 **Attributes**
765 **Attributes**
768
766
769 ``repository``
767 ``repository``
770 repository object for this in-memory-changeset
768 repository object for this in-memory-changeset
771
769
772 ``added``
770 ``added``
773 list of ``FileNode`` objects marked as *added*
771 list of ``FileNode`` objects marked as *added*
774
772
775 ``changed``
773 ``changed``
776 list of ``FileNode`` objects marked as *changed*
774 list of ``FileNode`` objects marked as *changed*
777
775
778 ``removed``
776 ``removed``
779 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
777 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
780 *removed*
778 *removed*
781
779
782 ``parents``
780 ``parents``
783 list of ``Changeset`` representing parents of in-memory changeset.
781 list of ``Changeset`` representing parents of in-memory changeset.
784 Should always be 2-element sequence.
782 Should always be 2-element sequence.
785
783
786 """
784 """
787
785
788 def __init__(self, repository):
786 def __init__(self, repository):
789 self.repository = repository
787 self.repository = repository
790 self.added = []
788 self.added = []
791 self.changed = []
789 self.changed = []
792 self.removed = []
790 self.removed = []
793 self.parents = []
791 self.parents = []
794
792
795 def add(self, *filenodes):
793 def add(self, *filenodes):
796 """
794 """
797 Marks given ``FileNode`` objects as *to be committed*.
795 Marks given ``FileNode`` objects as *to be committed*.
798
796
799 :raises ``NodeAlreadyExistsError``: if node with same path exists at
797 :raises ``NodeAlreadyExistsError``: if node with same path exists at
800 latest changeset
798 latest changeset
801 :raises ``NodeAlreadyAddedError``: if node with same path is already
799 :raises ``NodeAlreadyAddedError``: if node with same path is already
802 marked as *added*
800 marked as *added*
803 """
801 """
804 # Check if not already marked as *added* first
802 # Check if not already marked as *added* first
805 for node in filenodes:
803 for node in filenodes:
806 if node.path in (n.path for n in self.added):
804 if node.path in (n.path for n in self.added):
807 raise NodeAlreadyAddedError("Such FileNode %s is already "
805 raise NodeAlreadyAddedError("Such FileNode %s is already "
808 "marked for addition" % node.path)
806 "marked for addition" % node.path)
809 for node in filenodes:
807 for node in filenodes:
810 self.added.append(node)
808 self.added.append(node)
811
809
812 def change(self, *filenodes):
810 def change(self, *filenodes):
813 """
811 """
814 Marks given ``FileNode`` objects to be *changed* in next commit.
812 Marks given ``FileNode`` objects to be *changed* in next commit.
815
813
816 :raises ``EmptyRepositoryError``: if there are no changesets yet
814 :raises ``EmptyRepositoryError``: if there are no changesets yet
817 :raises ``NodeAlreadyExistsError``: if node with same path is already
815 :raises ``NodeAlreadyExistsError``: if node with same path is already
818 marked to be *changed*
816 marked to be *changed*
819 :raises ``NodeAlreadyRemovedError``: if node with same path is already
817 :raises ``NodeAlreadyRemovedError``: if node with same path is already
820 marked to be *removed*
818 marked to be *removed*
821 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
819 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
822 changeset
820 changeset
823 :raises ``NodeNotChangedError``: if node hasn't really be changed
821 :raises ``NodeNotChangedError``: if node hasn't really be changed
824 """
822 """
825 for node in filenodes:
823 for node in filenodes:
826 if node.path in (n.path for n in self.removed):
824 if node.path in (n.path for n in self.removed):
827 raise NodeAlreadyRemovedError("Node at %s is already marked "
825 raise NodeAlreadyRemovedError("Node at %s is already marked "
828 "as removed" % node.path)
826 "as removed" % node.path)
829 try:
827 try:
830 self.repository.get_changeset()
828 self.repository.get_changeset()
831 except EmptyRepositoryError:
829 except EmptyRepositoryError:
832 raise EmptyRepositoryError("Nothing to change - try to *add* new "
830 raise EmptyRepositoryError("Nothing to change - try to *add* new "
833 "nodes rather than changing them")
831 "nodes rather than changing them")
834 for node in filenodes:
832 for node in filenodes:
835 if node.path in (n.path for n in self.changed):
833 if node.path in (n.path for n in self.changed):
836 raise NodeAlreadyChangedError("Node at '%s' is already "
834 raise NodeAlreadyChangedError("Node at '%s' is already "
837 "marked as changed" % node.path)
835 "marked as changed" % node.path)
838 self.changed.append(node)
836 self.changed.append(node)
839
837
840 def remove(self, *filenodes):
838 def remove(self, *filenodes):
841 """
839 """
842 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
840 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
843 *removed* in next commit.
841 *removed* in next commit.
844
842
845 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
843 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
846 be *removed*
844 be *removed*
847 :raises ``NodeAlreadyChangedError``: if node has been already marked to
845 :raises ``NodeAlreadyChangedError``: if node has been already marked to
848 be *changed*
846 be *changed*
849 """
847 """
850 for node in filenodes:
848 for node in filenodes:
851 if node.path in (n.path for n in self.removed):
849 if node.path in (n.path for n in self.removed):
852 raise NodeAlreadyRemovedError("Node is already marked to "
850 raise NodeAlreadyRemovedError("Node is already marked to "
853 "for removal at %s" % node.path)
851 "for removal at %s" % node.path)
854 if node.path in (n.path for n in self.changed):
852 if node.path in (n.path for n in self.changed):
855 raise NodeAlreadyChangedError("Node is already marked to "
853 raise NodeAlreadyChangedError("Node is already marked to "
856 "be changed at %s" % node.path)
854 "be changed at %s" % node.path)
857 # We only mark node as *removed* - real removal is done by
855 # We only mark node as *removed* - real removal is done by
858 # commit method
856 # commit method
859 self.removed.append(node)
857 self.removed.append(node)
860
858
861 def reset(self):
859 def reset(self):
862 """
860 """
863 Resets this instance to initial state (cleans ``added``, ``changed``
861 Resets this instance to initial state (cleans ``added``, ``changed``
864 and ``removed`` lists).
862 and ``removed`` lists).
865 """
863 """
866 self.added = []
864 self.added = []
867 self.changed = []
865 self.changed = []
868 self.removed = []
866 self.removed = []
869 self.parents = []
867 self.parents = []
870
868
871 def get_ipaths(self):
869 def get_ipaths(self):
872 """
870 """
873 Returns generator of paths from nodes marked as added, changed or
871 Returns generator of paths from nodes marked as added, changed or
874 removed.
872 removed.
875 """
873 """
876 for node in itertools.chain(self.added, self.changed, self.removed):
874 for node in itertools.chain(self.added, self.changed, self.removed):
877 yield node.path
875 yield node.path
878
876
879 def get_paths(self):
877 def get_paths(self):
880 """
878 """
881 Returns list of paths from nodes marked as added, changed or removed.
879 Returns list of paths from nodes marked as added, changed or removed.
882 """
880 """
883 return list(self.get_ipaths())
881 return list(self.get_ipaths())
884
882
885 def check_integrity(self, parents=None):
883 def check_integrity(self, parents=None):
886 """
884 """
887 Checks in-memory changeset's integrity. Also, sets parents if not
885 Checks in-memory changeset's integrity. Also, sets parents if not
888 already set.
886 already set.
889
887
890 :raises CommitError: if any error occurs (i.e.
888 :raises CommitError: if any error occurs (i.e.
891 ``NodeDoesNotExistError``).
889 ``NodeDoesNotExistError``).
892 """
890 """
893 if not self.parents:
891 if not self.parents:
894 parents = parents or []
892 parents = parents or []
895 if len(parents) == 0:
893 if len(parents) == 0:
896 try:
894 try:
897 parents = [self.repository.get_changeset(), None]
895 parents = [self.repository.get_changeset(), None]
898 except EmptyRepositoryError:
896 except EmptyRepositoryError:
899 parents = [None, None]
897 parents = [None, None]
900 elif len(parents) == 1:
898 elif len(parents) == 1:
901 parents += [None]
899 parents += [None]
902 self.parents = parents
900 self.parents = parents
903
901
904 # Local parents, only if not None
902 # Local parents, only if not None
905 parents = [p for p in self.parents if p]
903 parents = [p for p in self.parents if p]
906
904
907 # Check nodes marked as added
905 # Check nodes marked as added
908 for p in parents:
906 for p in parents:
909 for node in self.added:
907 for node in self.added:
910 try:
908 try:
911 p.get_node(node.path)
909 p.get_node(node.path)
912 except NodeDoesNotExistError:
910 except NodeDoesNotExistError:
913 pass
911 pass
914 else:
912 else:
915 raise NodeAlreadyExistsError("Node at %s already exists "
913 raise NodeAlreadyExistsError("Node at %s already exists "
916 "at %s" % (node.path, p))
914 "at %s" % (node.path, p))
917
915
918 # Check nodes marked as changed
916 # Check nodes marked as changed
919 missing = set(node.path for node in self.changed)
917 missing = set(node.path for node in self.changed)
920 not_changed = set(node.path for node in self.changed)
918 not_changed = set(node.path for node in self.changed)
921 if self.changed and not parents:
919 if self.changed and not parents:
922 raise NodeDoesNotExistError(self.changed[0].path)
920 raise NodeDoesNotExistError(self.changed[0].path)
923 for p in parents:
921 for p in parents:
924 for node in self.changed:
922 for node in self.changed:
925 try:
923 try:
926 old = p.get_node(node.path)
924 old = p.get_node(node.path)
927 missing.remove(node.path)
925 missing.remove(node.path)
928 # if content actually changed, remove node from unchanged
926 # if content actually changed, remove node from unchanged
929 if old.content != node.content:
927 if old.content != node.content:
930 not_changed.remove(node.path)
928 not_changed.remove(node.path)
931 except NodeDoesNotExistError:
929 except NodeDoesNotExistError:
932 pass
930 pass
933 if self.changed and missing:
931 if self.changed and missing:
934 raise NodeDoesNotExistError("Node at %s is missing "
932 raise NodeDoesNotExistError("Node at %s is missing "
935 "(parents: %s)" % (node.path, parents))
933 "(parents: %s)" % (node.path, parents))
936
934
937 if self.changed and not_changed:
935 if self.changed and not_changed:
938 raise NodeNotChangedError("Node at %s wasn't actually changed "
936 raise NodeNotChangedError("Node at %s wasn't actually changed "
939 "since parents' changesets: %s" % (not_changed.pop(),
937 "since parents' changesets: %s" % (not_changed.pop(),
940 parents)
938 parents)
941 )
939 )
942
940
943 # Check nodes marked as removed
941 # Check nodes marked as removed
944 if self.removed and not parents:
942 if self.removed and not parents:
945 raise NodeDoesNotExistError("Cannot remove node at %s as there "
943 raise NodeDoesNotExistError("Cannot remove node at %s as there "
946 "were no parents specified" % self.removed[0].path)
944 "were no parents specified" % self.removed[0].path)
947 really_removed = set()
945 really_removed = set()
948 for p in parents:
946 for p in parents:
949 for node in self.removed:
947 for node in self.removed:
950 try:
948 try:
951 p.get_node(node.path)
949 p.get_node(node.path)
952 really_removed.add(node.path)
950 really_removed.add(node.path)
953 except ChangesetError:
951 except ChangesetError:
954 pass
952 pass
955 not_removed = list(set(node.path for node in self.removed) - really_removed)
953 not_removed = list(set(node.path for node in self.removed) - really_removed)
956 if not_removed:
954 if not_removed:
957 raise NodeDoesNotExistError("Cannot remove node at %s from "
955 raise NodeDoesNotExistError("Cannot remove node at %s from "
958 "following parents: %s" % (not_removed[0], parents))
956 "following parents: %s" % (not_removed[0], parents))
959
957
960 def commit(self, message, author, parents=None, branch=None, date=None,
958 def commit(self, message, author, parents=None, branch=None, date=None,
961 **kwargs):
959 **kwargs):
962 """
960 """
963 Performs in-memory commit (doesn't check workdir in any way) and
961 Performs in-memory commit (doesn't check workdir in any way) and
964 returns newly created ``Changeset``. Updates repository's
962 returns newly created ``Changeset``. Updates repository's
965 ``revisions``.
963 ``revisions``.
966
964
967 .. note::
965 .. note::
968 While overriding this method each backend's should call
966 While overriding this method each backend's should call
969 ``self.check_integrity(parents)`` in the first place.
967 ``self.check_integrity(parents)`` in the first place.
970
968
971 :param message: message of the commit
969 :param message: message of the commit
972 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
970 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
973 :param parents: single parent or sequence of parents from which commit
971 :param parents: single parent or sequence of parents from which commit
974 would be derived
972 would be derived
975 :param date: ``datetime.datetime`` instance. Defaults to
973 :param date: ``datetime.datetime`` instance. Defaults to
976 ``datetime.datetime.now()``.
974 ``datetime.datetime.now()``.
977 :param branch: branch name, as string. If none given, default backend's
975 :param branch: branch name, as string. If none given, default backend's
978 branch would be used.
976 branch would be used.
979
977
980 :raises ``CommitError``: if any error occurs while committing
978 :raises ``CommitError``: if any error occurs while committing
981 """
979 """
982 raise NotImplementedError
980 raise NotImplementedError
983
981
984
982
985 class EmptyChangeset(BaseChangeset):
983 class EmptyChangeset(BaseChangeset):
986 """
984 """
987 An dummy empty changeset. It's possible to pass hash when creating
985 An dummy empty changeset. It's possible to pass hash when creating
988 an EmptyChangeset
986 an EmptyChangeset
989 """
987 """
990
988
991 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
989 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
992 alias=None, revision=-1, message='', author='', date=None):
990 alias=None, revision=-1, message='', author='', date=None):
993 self._empty_cs = cs
991 self._empty_cs = cs
994 self.revision = revision
992 self.revision = revision
995 self.message = message
993 self.message = message
996 self.author = author
994 self.author = author
997 self.date = date or datetime.datetime.fromtimestamp(0)
995 self.date = date or datetime.datetime.fromtimestamp(0)
998 self.repository = repo
996 self.repository = repo
999 self.requested_revision = requested_revision
997 self.requested_revision = requested_revision
1000 self.alias = alias
998 self.alias = alias
1001
999
1002 @LazyProperty
1000 @LazyProperty
1003 def raw_id(self):
1001 def raw_id(self):
1004 """
1002 """
1005 Returns raw string identifying this changeset, useful for web
1003 Returns raw string identifying this changeset, useful for web
1006 representation.
1004 representation.
1007 """
1005 """
1008
1006
1009 return self._empty_cs
1007 return self._empty_cs
1010
1008
1011 @LazyProperty
1009 @LazyProperty
1012 def branch(self):
1010 def branch(self):
1013 from kallithea.lib.vcs.backends import get_backend
1011 from kallithea.lib.vcs.backends import get_backend
1014 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1012 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1015
1013
1016 @LazyProperty
1014 @LazyProperty
1017 def branches(self):
1015 def branches(self):
1018 from kallithea.lib.vcs.backends import get_backend
1016 from kallithea.lib.vcs.backends import get_backend
1019 return [get_backend(self.alias).DEFAULT_BRANCH_NAME]
1017 return [get_backend(self.alias).DEFAULT_BRANCH_NAME]
1020
1018
1021 @LazyProperty
1019 @LazyProperty
1022 def short_id(self):
1020 def short_id(self):
1023 return self.raw_id[:12]
1021 return self.raw_id[:12]
1024
1022
1025 def get_file_changeset(self, path):
1023 def get_file_changeset(self, path):
1026 return self
1024 return self
1027
1025
1028 def get_file_content(self, path):
1026 def get_file_content(self, path):
1029 return ''
1027 return ''
1030
1028
1031 def get_file_size(self, path):
1029 def get_file_size(self, path):
1032 return 0
1030 return 0
1033
1031
1034
1032
1035 class CollectionGenerator(object):
1033 class CollectionGenerator(object):
1036
1034
1037 def __init__(self, repo, revs):
1035 def __init__(self, repo, revs):
1038 self.repo = repo
1036 self.repo = repo
1039 self.revs = revs
1037 self.revs = revs
1040
1038
1041 def __len__(self):
1039 def __len__(self):
1042 return len(self.revs)
1040 return len(self.revs)
1043
1041
1044 def __iter__(self):
1042 def __iter__(self):
1045 for rev in self.revs:
1043 for rev in self.revs:
1046 yield self.repo.get_changeset(rev)
1044 yield self.repo.get_changeset(rev)
1047
1045
1048 def __getitem__(self, what):
1046 def __getitem__(self, what):
1049 """Return either a single element by index, or a sliced collection."""
1047 """Return either a single element by index, or a sliced collection."""
1050 if isinstance(what, slice):
1048 if isinstance(what, slice):
1051 return CollectionGenerator(self.repo, self.revs[what])
1049 return CollectionGenerator(self.repo, self.revs[what])
1052 else:
1050 else:
1053 # single item
1051 # single item
1054 return self.repo.get_changeset(self.revs[what])
1052 return self.repo.get_changeset(self.revs[what])
1055
1053
1056 def __repr__(self):
1054 def __repr__(self):
1057 return '<CollectionGenerator[len:%s]>' % (len(self))
1055 return '<CollectionGenerator[len:%s]>' % (len(self))
@@ -1,94 +1,82 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.exceptions
3 vcs.exceptions
4 ~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~
5
5
6 Custom exceptions module
6 Custom exceptions module
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12
12
13 class VCSError(Exception):
13 class VCSError(Exception):
14 pass
14 pass
15
15
16
16
17 class RepositoryError(VCSError):
17 class RepositoryError(VCSError):
18 pass
18 pass
19
19
20
20
21 class EmptyRepositoryError(RepositoryError):
21 class EmptyRepositoryError(RepositoryError):
22 pass
22 pass
23
23
24
24
25 class TagAlreadyExistError(RepositoryError):
25 class TagAlreadyExistError(RepositoryError):
26 pass
26 pass
27
27
28
28
29 class TagDoesNotExistError(RepositoryError):
29 class TagDoesNotExistError(RepositoryError):
30 pass
30 pass
31
31
32
32
33 class BranchAlreadyExistError(RepositoryError):
34 pass
35
36
37 class BranchDoesNotExistError(RepositoryError):
33 class BranchDoesNotExistError(RepositoryError):
38 pass
34 pass
39
35
40
36
41 class ChangesetError(RepositoryError):
37 class ChangesetError(RepositoryError):
42 pass
38 pass
43
39
44
40
45 class ChangesetDoesNotExistError(ChangesetError):
41 class ChangesetDoesNotExistError(ChangesetError):
46 pass
42 pass
47
43
48
44
49 class CommitError(RepositoryError):
45 class CommitError(RepositoryError):
50 pass
46 pass
51
47
52
48
53 class NothingChangedError(CommitError):
54 pass
55
56
57 class NodeError(VCSError):
49 class NodeError(VCSError):
58 pass
50 pass
59
51
60
52
61 class RemovedFileNodeError(NodeError):
53 class RemovedFileNodeError(NodeError):
62 pass
54 pass
63
55
64
56
65 class NodeAlreadyExistsError(CommitError):
57 class NodeAlreadyExistsError(CommitError):
66 pass
58 pass
67
59
68
60
69 class NodeAlreadyChangedError(CommitError):
61 class NodeAlreadyChangedError(CommitError):
70 pass
62 pass
71
63
72
64
73 class NodeDoesNotExistError(CommitError):
65 class NodeDoesNotExistError(CommitError):
74 pass
66 pass
75
67
76
68
77 class NodeNotChangedError(CommitError):
69 class NodeNotChangedError(CommitError):
78 pass
70 pass
79
71
80
72
81 class NodeAlreadyAddedError(CommitError):
73 class NodeAlreadyAddedError(CommitError):
82 pass
74 pass
83
75
84
76
85 class NodeAlreadyRemovedError(CommitError):
77 class NodeAlreadyRemovedError(CommitError):
86 pass
78 pass
87
79
88
80
89 class ImproperArchiveTypeError(VCSError):
81 class ImproperArchiveTypeError(VCSError):
90 pass
82 pass
91
92
93 class CommandError(VCSError):
94 pass
@@ -1,420 +1,409 b''
1 """
1 """
2 Module provides a class allowing to wrap communication over subprocess.Popen
2 Module provides a class allowing to wrap communication over subprocess.Popen
3 input, output, error streams into a meaningful, non-blocking, concurrent
3 input, output, error streams into a meaningful, non-blocking, concurrent
4 stream processor exposing the output data as an iterator fitting to be a
4 stream processor exposing the output data as an iterator fitting to be a
5 return value passed by a WSGI application to a WSGI server per PEP 3333.
5 return value passed by a WSGI application to a WSGI server per PEP 3333.
6
6
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
8
8
9 This file is part of git_http_backend.py Project.
9 This file is part of git_http_backend.py Project.
10
10
11 git_http_backend.py Project is free software: you can redistribute it and/or
11 git_http_backend.py Project is free software: you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public License as
12 modify it under the terms of the GNU Lesser General Public License as
13 published by the Free Software Foundation, either version 2.1 of the License,
13 published by the Free Software Foundation, either version 2.1 of the License,
14 or (at your option) any later version.
14 or (at your option) any later version.
15
15
16 git_http_backend.py Project is distributed in the hope that it will be useful,
16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU Lesser General Public License for more details.
19 GNU Lesser General Public License for more details.
20
20
21 You should have received a copy of the GNU Lesser General Public License
21 You should have received a copy of the GNU Lesser General Public License
22 along with git_http_backend.py Project.
22 along with git_http_backend.py Project.
23 If not, see <http://www.gnu.org/licenses/>.
23 If not, see <http://www.gnu.org/licenses/>.
24 """
24 """
25 import collections
25 import collections
26 import os
26 import os
27 import subprocess
27 import subprocess
28 import threading
28 import threading
29
29
30
30
31 class StreamFeeder(threading.Thread):
31 class StreamFeeder(threading.Thread):
32 """
32 """
33 Normal writing into pipe-like is blocking once the buffer is filled.
33 Normal writing into pipe-like is blocking once the buffer is filled.
34 This thread allows a thread to seep data from a file-like into a pipe
34 This thread allows a thread to seep data from a file-like into a pipe
35 without blocking the main thread.
35 without blocking the main thread.
36 We close inpipe once the end of the source stream is reached.
36 We close inpipe once the end of the source stream is reached.
37 """
37 """
38
38
39 def __init__(self, source):
39 def __init__(self, source):
40 super(StreamFeeder, self).__init__()
40 super(StreamFeeder, self).__init__()
41 self.daemon = True
41 self.daemon = True
42 filelike = False
42 filelike = False
43 self.bytes = bytes()
43 self.bytes = bytes()
44 if type(source) in (type(''), bytes, bytearray): # string-like
44 if type(source) in (type(''), bytes, bytearray): # string-like
45 self.bytes = bytes(source)
45 self.bytes = bytes(source)
46 else: # can be either file pointer or file-like
46 else: # can be either file pointer or file-like
47 if isinstance(source, int): # file pointer it is
47 if isinstance(source, int): # file pointer it is
48 # converting file descriptor (int) stdin into file-like
48 # converting file descriptor (int) stdin into file-like
49 source = os.fdopen(source, 'rb', 16384)
49 source = os.fdopen(source, 'rb', 16384)
50 # let's see if source is file-like by now
50 # let's see if source is file-like by now
51 filelike = hasattr(source, 'read')
51 filelike = hasattr(source, 'read')
52 if not filelike and not self.bytes:
52 if not filelike and not self.bytes:
53 raise TypeError("StreamFeeder's source object must be a readable "
53 raise TypeError("StreamFeeder's source object must be a readable "
54 "file-like, a file descriptor, or a string-like.")
54 "file-like, a file descriptor, or a string-like.")
55 self.source = source
55 self.source = source
56 self.readiface, self.writeiface = os.pipe()
56 self.readiface, self.writeiface = os.pipe()
57
57
58 def run(self):
58 def run(self):
59 t = self.writeiface
59 t = self.writeiface
60 if self.bytes:
60 if self.bytes:
61 os.write(t, self.bytes)
61 os.write(t, self.bytes)
62 else:
62 else:
63 s = self.source
63 s = self.source
64 b = s.read(4096)
64 b = s.read(4096)
65 while b:
65 while b:
66 os.write(t, b)
66 os.write(t, b)
67 b = s.read(4096)
67 b = s.read(4096)
68 os.close(t)
68 os.close(t)
69
69
70 @property
70 @property
71 def output(self):
71 def output(self):
72 return self.readiface
72 return self.readiface
73
73
74
74
75 class InputStreamChunker(threading.Thread):
75 class InputStreamChunker(threading.Thread):
76 def __init__(self, source, target, buffer_size, chunk_size):
76 def __init__(self, source, target, buffer_size, chunk_size):
77
77
78 super(InputStreamChunker, self).__init__()
78 super(InputStreamChunker, self).__init__()
79
79
80 self.daemon = True # die die die.
80 self.daemon = True # die die die.
81
81
82 self.source = source
82 self.source = source
83 self.target = target
83 self.target = target
84 self.chunk_count_max = int(buffer_size / chunk_size) + 1
84 self.chunk_count_max = int(buffer_size / chunk_size) + 1
85 self.chunk_size = chunk_size
85 self.chunk_size = chunk_size
86
86
87 self.data_added = threading.Event()
87 self.data_added = threading.Event()
88 self.data_added.clear()
88 self.data_added.clear()
89
89
90 self.keep_reading = threading.Event()
90 self.keep_reading = threading.Event()
91 self.keep_reading.set()
91 self.keep_reading.set()
92
92
93 self.EOF = threading.Event()
93 self.EOF = threading.Event()
94 self.EOF.clear()
94 self.EOF.clear()
95
95
96 self.go = threading.Event()
96 self.go = threading.Event()
97 self.go.set()
97 self.go.set()
98
98
99 def stop(self):
99 def stop(self):
100 self.go.clear()
100 self.go.clear()
101 self.EOF.set()
101 self.EOF.set()
102 try:
102 try:
103 # this is not proper, but is done to force the reader thread let
103 # this is not proper, but is done to force the reader thread let
104 # go of the input because, if successful, .close() will send EOF
104 # go of the input because, if successful, .close() will send EOF
105 # down the pipe.
105 # down the pipe.
106 self.source.close()
106 self.source.close()
107 except:
107 except:
108 pass
108 pass
109
109
110 def run(self):
110 def run(self):
111 s = self.source
111 s = self.source
112 t = self.target
112 t = self.target
113 cs = self.chunk_size
113 cs = self.chunk_size
114 ccm = self.chunk_count_max
114 ccm = self.chunk_count_max
115 kr = self.keep_reading
115 kr = self.keep_reading
116 da = self.data_added
116 da = self.data_added
117 go = self.go
117 go = self.go
118
118
119 try:
119 try:
120 b = s.read(cs)
120 b = s.read(cs)
121 except ValueError:
121 except ValueError:
122 b = ''
122 b = ''
123
123
124 while b and go.is_set():
124 while b and go.is_set():
125 if len(t) > ccm:
125 if len(t) > ccm:
126 kr.clear()
126 kr.clear()
127 kr.wait(2)
127 kr.wait(2)
128 if not kr.wait(10):
128 if not kr.wait(10):
129 raise IOError(
129 raise IOError(
130 "Timed out while waiting for input from subprocess.")
130 "Timed out while waiting for input from subprocess.")
131 t.append(b)
131 t.append(b)
132 da.set()
132 da.set()
133 try:
133 try:
134 b = s.read(cs)
134 b = s.read(cs)
135 except ValueError: # probably "I/O operation on closed file"
135 except ValueError: # probably "I/O operation on closed file"
136 b = ''
136 b = ''
137
137
138 self.EOF.set()
138 self.EOF.set()
139 da.set() # for cases when done but there was no input.
139 da.set() # for cases when done but there was no input.
140
140
141
141
142 class BufferedGenerator(object):
142 class BufferedGenerator(object):
143 """
143 """
144 Class behaves as a non-blocking, buffered pipe reader.
144 Class behaves as a non-blocking, buffered pipe reader.
145 Reads chunks of data (through a thread)
145 Reads chunks of data (through a thread)
146 from a blocking pipe, and attaches these to an array (Deque) of chunks.
146 from a blocking pipe, and attaches these to an array (Deque) of chunks.
147 Reading is halted in the thread when max chunks is internally buffered.
147 Reading is halted in the thread when max chunks is internally buffered.
148 The .next() may operate in blocking or non-blocking fashion by yielding
148 The .next() may operate in blocking or non-blocking fashion by yielding
149 '' if no data is ready
149 '' if no data is ready
150 to be sent or by not returning until there is some data to send
150 to be sent or by not returning until there is some data to send
151 When we get EOF from underlying source pipe we raise the marker to raise
151 When we get EOF from underlying source pipe we raise the marker to raise
152 StopIteration after the last chunk of data is yielded.
152 StopIteration after the last chunk of data is yielded.
153 """
153 """
154
154
155 def __init__(self, source, buffer_size=65536, chunk_size=4096,
155 def __init__(self, source, buffer_size=65536, chunk_size=4096,
156 starting_values=None, bottomless=False):
156 starting_values=None, bottomless=False):
157 starting_values = starting_values or []
157 starting_values = starting_values or []
158 if bottomless:
158 if bottomless:
159 maxlen = int(buffer_size / chunk_size)
159 maxlen = int(buffer_size / chunk_size)
160 else:
160 else:
161 maxlen = None
161 maxlen = None
162
162
163 self.data = collections.deque(starting_values, maxlen)
163 self.data = collections.deque(starting_values, maxlen)
164 self.worker = InputStreamChunker(source, self.data, buffer_size,
164 self.worker = InputStreamChunker(source, self.data, buffer_size,
165 chunk_size)
165 chunk_size)
166 if starting_values:
166 if starting_values:
167 self.worker.data_added.set()
167 self.worker.data_added.set()
168 self.worker.start()
168 self.worker.start()
169
169
170 ####################
170 ####################
171 # Generator's methods
171 # Generator's methods
172 ####################
172 ####################
173
173
174 def __iter__(self):
174 def __iter__(self):
175 return self
175 return self
176
176
177 def __next__(self):
177 def __next__(self):
178 while not len(self.data) and not self.worker.EOF.is_set():
178 while not len(self.data) and not self.worker.EOF.is_set():
179 self.worker.data_added.clear()
179 self.worker.data_added.clear()
180 self.worker.data_added.wait(0.2)
180 self.worker.data_added.wait(0.2)
181 if len(self.data):
181 if len(self.data):
182 self.worker.keep_reading.set()
182 self.worker.keep_reading.set()
183 return bytes(self.data.popleft())
183 return bytes(self.data.popleft())
184 elif self.worker.EOF.is_set():
184 elif self.worker.EOF.is_set():
185 raise StopIteration
185 raise StopIteration
186
186
187 def throw(self, type, value=None, traceback=None):
187 def throw(self, type, value=None, traceback=None):
188 if not self.worker.EOF.is_set():
188 if not self.worker.EOF.is_set():
189 raise type(value)
189 raise type(value)
190
190
191 def start(self):
191 def start(self):
192 self.worker.start()
192 self.worker.start()
193
193
194 def stop(self):
194 def stop(self):
195 self.worker.stop()
195 self.worker.stop()
196
196
197 def close(self):
197 def close(self):
198 try:
198 try:
199 self.worker.stop()
199 self.worker.stop()
200 self.throw(GeneratorExit)
200 self.throw(GeneratorExit)
201 except (GeneratorExit, StopIteration):
201 except (GeneratorExit, StopIteration):
202 pass
202 pass
203
203
204 ####################
204 ####################
205 # Threaded reader's infrastructure.
205 # Threaded reader's infrastructure.
206 ####################
206 ####################
207 @property
207 @property
208 def input(self):
208 def input(self):
209 return self.worker.w
209 return self.worker.w
210
210
211 @property
211 @property
212 def data_added_event(self):
212 def data_added_event(self):
213 return self.worker.data_added
213 return self.worker.data_added
214
214
215 @property
215 @property
216 def data_added(self):
216 def data_added(self):
217 return self.worker.data_added.is_set()
217 return self.worker.data_added.is_set()
218
218
219 @property
219 @property
220 def reading_paused(self):
220 def reading_paused(self):
221 return not self.worker.keep_reading.is_set()
221 return not self.worker.keep_reading.is_set()
222
222
223 @property
223 @property
224 def done_reading_event(self):
225 """
226 Done_reading does not mean that the iterator's buffer is empty.
227 Iterator might have done reading from underlying source, but the read
228 chunks might still be available for serving through .next() method.
229
230 :returns: An threading.Event class instance.
231 """
232 return self.worker.EOF
233
234 @property
235 def done_reading(self):
224 def done_reading(self):
236 """
225 """
237 Done_reading does not mean that the iterator's buffer is empty.
226 Done_reading does not mean that the iterator's buffer is empty.
238 Iterator might have done reading from underlying source, but the read
227 Iterator might have done reading from underlying source, but the read
239 chunks might still be available for serving through .next() method.
228 chunks might still be available for serving through .next() method.
240
229
241 :returns: An Bool value.
230 :returns: An Bool value.
242 """
231 """
243 return self.worker.EOF.is_set()
232 return self.worker.EOF.is_set()
244
233
245 @property
234 @property
246 def length(self):
235 def length(self):
247 """
236 """
248 returns int.
237 returns int.
249
238
250 This is the length of the queue of chunks, not the length of
239 This is the length of the queue of chunks, not the length of
251 the combined contents in those chunks.
240 the combined contents in those chunks.
252
241
253 __len__() cannot be meaningfully implemented because this
242 __len__() cannot be meaningfully implemented because this
254 reader is just flying through a bottomless pit content and
243 reader is just flying through a bottomless pit content and
255 can only know the length of what it already saw.
244 can only know the length of what it already saw.
256
245
257 If __len__() on WSGI server per PEP 3333 returns a value,
246 If __len__() on WSGI server per PEP 3333 returns a value,
258 the response's length will be set to that. In order not to
247 the response's length will be set to that. In order not to
259 confuse WSGI PEP3333 servers, we will not implement __len__
248 confuse WSGI PEP3333 servers, we will not implement __len__
260 at all.
249 at all.
261 """
250 """
262 return len(self.data)
251 return len(self.data)
263
252
264 def prepend(self, x):
253 def prepend(self, x):
265 self.data.appendleft(x)
254 self.data.appendleft(x)
266
255
267 def append(self, x):
256 def append(self, x):
268 self.data.append(x)
257 self.data.append(x)
269
258
270 def extend(self, o):
259 def extend(self, o):
271 self.data.extend(o)
260 self.data.extend(o)
272
261
273 def __getitem__(self, i):
262 def __getitem__(self, i):
274 return self.data[i]
263 return self.data[i]
275
264
276
265
277 class SubprocessIOChunker(object):
266 class SubprocessIOChunker(object):
278 """
267 """
279 Processor class wrapping handling of subprocess IO.
268 Processor class wrapping handling of subprocess IO.
280
269
281 In a way, this is a "communicate()" replacement with a twist.
270 In a way, this is a "communicate()" replacement with a twist.
282
271
283 - We are multithreaded. Writing in and reading out, err are all sep threads.
272 - We are multithreaded. Writing in and reading out, err are all sep threads.
284 - We support concurrent (in and out) stream processing.
273 - We support concurrent (in and out) stream processing.
285 - The output is not a stream. It's a queue of read string (bytes, not str)
274 - The output is not a stream. It's a queue of read string (bytes, not str)
286 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
275 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
287 - We are non-blocking in more respects than communicate()
276 - We are non-blocking in more respects than communicate()
288 (reading from subprocess out pauses when internal buffer is full, but
277 (reading from subprocess out pauses when internal buffer is full, but
289 does not block the parent calling code. On the flip side, reading from
278 does not block the parent calling code. On the flip side, reading from
290 slow-yielding subprocess may block the iteration until data shows up. This
279 slow-yielding subprocess may block the iteration until data shows up. This
291 does not block the parallel inpipe reading occurring parallel thread.)
280 does not block the parallel inpipe reading occurring parallel thread.)
292
281
293 The purpose of the object is to allow us to wrap subprocess interactions into
282 The purpose of the object is to allow us to wrap subprocess interactions into
294 an iterable that can be passed to a WSGI server as the application's return
283 an iterable that can be passed to a WSGI server as the application's return
295 value. Because of stream-processing-ability, WSGI does not have to read ALL
284 value. Because of stream-processing-ability, WSGI does not have to read ALL
296 of the subprocess's output and buffer it, before handing it to WSGI server for
285 of the subprocess's output and buffer it, before handing it to WSGI server for
297 HTTP response. Instead, the class initializer reads just a bit of the stream
286 HTTP response. Instead, the class initializer reads just a bit of the stream
298 to figure out if error occurred or likely to occur and if not, just hands the
287 to figure out if error occurred or likely to occur and if not, just hands the
299 further iteration over subprocess output to the server for completion of HTTP
288 further iteration over subprocess output to the server for completion of HTTP
300 response.
289 response.
301
290
302 The real or perceived subprocess error is trapped and raised as one of
291 The real or perceived subprocess error is trapped and raised as one of
303 EnvironmentError family of exceptions
292 EnvironmentError family of exceptions
304
293
305 Example usage:
294 Example usage:
306 # try:
295 # try:
307 # answer = SubprocessIOChunker(
296 # answer = SubprocessIOChunker(
308 # cmd,
297 # cmd,
309 # input,
298 # input,
310 # buffer_size = 65536,
299 # buffer_size = 65536,
311 # chunk_size = 4096
300 # chunk_size = 4096
312 # )
301 # )
313 # except (EnvironmentError) as e:
302 # except (EnvironmentError) as e:
314 # print str(e)
303 # print str(e)
315 # raise e
304 # raise e
316 #
305 #
317 # return answer
306 # return answer
318
307
319
308
320 """
309 """
321
310
322 def __init__(self, cmd, inputstream=None, buffer_size=65536,
311 def __init__(self, cmd, inputstream=None, buffer_size=65536,
323 chunk_size=4096, starting_values=None, **kwargs):
312 chunk_size=4096, starting_values=None, **kwargs):
324 """
313 """
325 Initializes SubprocessIOChunker
314 Initializes SubprocessIOChunker
326
315
327 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
316 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
328 :param inputstream: (Default: None) A file-like, string, or file pointer.
317 :param inputstream: (Default: None) A file-like, string, or file pointer.
329 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
318 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
330 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
319 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
331 :param starting_values: (Default: []) An array of strings to put in front of output que.
320 :param starting_values: (Default: []) An array of strings to put in front of output que.
332 """
321 """
333 starting_values = starting_values or []
322 starting_values = starting_values or []
334 if inputstream:
323 if inputstream:
335 input_streamer = StreamFeeder(inputstream)
324 input_streamer = StreamFeeder(inputstream)
336 input_streamer.start()
325 input_streamer.start()
337 inputstream = input_streamer.output
326 inputstream = input_streamer.output
338
327
339 # Note: fragile cmd mangling has been removed for use in Kallithea
328 # Note: fragile cmd mangling has been removed for use in Kallithea
340 assert isinstance(cmd, list), cmd
329 assert isinstance(cmd, list), cmd
341
330
342 _p = subprocess.Popen(cmd, bufsize=-1,
331 _p = subprocess.Popen(cmd, bufsize=-1,
343 stdin=inputstream,
332 stdin=inputstream,
344 stdout=subprocess.PIPE,
333 stdout=subprocess.PIPE,
345 stderr=subprocess.PIPE,
334 stderr=subprocess.PIPE,
346 **kwargs)
335 **kwargs)
347
336
348 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
337 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
349 starting_values)
338 starting_values)
350 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
339 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
351
340
352 while not bg_out.done_reading and not bg_out.reading_paused:
341 while not bg_out.done_reading and not bg_out.reading_paused:
353 # doing this until we reach either end of file, or end of buffer.
342 # doing this until we reach either end of file, or end of buffer.
354 bg_out.data_added_event.wait(1)
343 bg_out.data_added_event.wait(1)
355 bg_out.data_added_event.clear()
344 bg_out.data_added_event.clear()
356
345
357 # at this point it's still ambiguous if we are done reading or just full buffer.
346 # at this point it's still ambiguous if we are done reading or just full buffer.
358 # Either way, if error (returned by ended process, or implied based on
347 # Either way, if error (returned by ended process, or implied based on
359 # presence of stuff in stderr output) we error out.
348 # presence of stuff in stderr output) we error out.
360 # Else, we are happy.
349 # Else, we are happy.
361 returncode = _p.poll()
350 returncode = _p.poll()
362 if (returncode is not None # process has terminated
351 if (returncode is not None # process has terminated
363 and returncode != 0
352 and returncode != 0
364 ): # and it failed
353 ): # and it failed
365 bg_out.stop()
354 bg_out.stop()
366 out = b''.join(bg_out)
355 out = b''.join(bg_out)
367 bg_err.stop()
356 bg_err.stop()
368 err = b''.join(bg_err)
357 err = b''.join(bg_err)
369 if (err.strip() == b'fatal: The remote end hung up unexpectedly' and
358 if (err.strip() == b'fatal: The remote end hung up unexpectedly' and
370 out.startswith(b'0034shallow ')
359 out.startswith(b'0034shallow ')
371 ):
360 ):
372 # hack inspired by https://github.com/schacon/grack/pull/7
361 # hack inspired by https://github.com/schacon/grack/pull/7
373 bg_out = iter([out])
362 bg_out = iter([out])
374 _p = None
363 _p = None
375 elif err:
364 elif err:
376 raise EnvironmentError("Subprocess exited due to an error: %s" % err)
365 raise EnvironmentError("Subprocess exited due to an error: %s" % err)
377 else:
366 else:
378 raise EnvironmentError(
367 raise EnvironmentError(
379 "Subprocess exited with non 0 ret code: %s" % returncode)
368 "Subprocess exited with non 0 ret code: %s" % returncode)
380 self.process = _p
369 self.process = _p
381 self.output = bg_out
370 self.output = bg_out
382 self.error = bg_err
371 self.error = bg_err
383 self.inputstream = inputstream
372 self.inputstream = inputstream
384
373
385 def __iter__(self):
374 def __iter__(self):
386 return self
375 return self
387
376
388 def __next__(self):
377 def __next__(self):
389 if self.process:
378 if self.process:
390 returncode = self.process.poll()
379 returncode = self.process.poll()
391 if (returncode is not None # process has terminated
380 if (returncode is not None # process has terminated
392 and returncode != 0
381 and returncode != 0
393 ): # and it failed
382 ): # and it failed
394 self.output.stop()
383 self.output.stop()
395 self.error.stop()
384 self.error.stop()
396 err = ''.join(self.error)
385 err = ''.join(self.error)
397 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
386 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
398 return next(self.output)
387 return next(self.output)
399
388
400 def throw(self, type, value=None, traceback=None):
389 def throw(self, type, value=None, traceback=None):
401 if self.output.length or not self.output.done_reading:
390 if self.output.length or not self.output.done_reading:
402 raise type(value)
391 raise type(value)
403
392
404 def close(self):
393 def close(self):
405 try:
394 try:
406 self.process.terminate()
395 self.process.terminate()
407 except:
396 except:
408 pass
397 pass
409 try:
398 try:
410 self.output.close()
399 self.output.close()
411 except:
400 except:
412 pass
401 pass
413 try:
402 try:
414 self.error.close()
403 self.error.close()
415 except:
404 except:
416 pass
405 pass
417 try:
406 try:
418 os.close(self.inputstream)
407 os.close(self.inputstream)
419 except:
408 except:
420 pass
409 pass
@@ -1,64 +1,43 b''
1 import threading
2
3
4 class _Missing(object):
1 class _Missing(object):
5
2
6 def __repr__(self):
3 def __repr__(self):
7 return 'no value'
4 return 'no value'
8
5
9 def __reduce__(self):
6 def __reduce__(self):
10 return '_missing'
7 return '_missing'
11
8
12
9
13 _missing = _Missing()
10 _missing = _Missing()
14
11
15
12
16 class LazyProperty(object):
13 class LazyProperty(object):
17 """
14 """
18 Decorator for easier creation of ``property`` from potentially expensive to
15 Decorator for easier creation of ``property`` from potentially expensive to
19 calculate attribute of the class.
16 calculate attribute of the class.
20
17
21 Usage::
18 Usage::
22
19
23 class Foo(object):
20 class Foo(object):
24 @LazyProperty
21 @LazyProperty
25 def bar(self):
22 def bar(self):
26 print 'Calculating self._bar'
23 print 'Calculating self._bar'
27 return 42
24 return 42
28
25
29 Taken from http://blog.pythonisito.com/2008/08/lazy-descriptors.html and
26 Taken from http://blog.pythonisito.com/2008/08/lazy-descriptors.html and
30 used widely.
27 used widely.
31 """
28 """
32
29
33 def __init__(self, func):
30 def __init__(self, func):
34 self._func = func
31 self._func = func
35 self.__module__ = func.__module__
32 self.__module__ = func.__module__
36 self.__name__ = func.__name__
33 self.__name__ = func.__name__
37 self.__doc__ = func.__doc__
34 self.__doc__ = func.__doc__
38
35
39 def __get__(self, obj, klass=None):
36 def __get__(self, obj, klass=None):
40 if obj is None:
37 if obj is None:
41 return self
38 return self
42 value = obj.__dict__.get(self.__name__, _missing)
39 value = obj.__dict__.get(self.__name__, _missing)
43 if value is _missing:
40 if value is _missing:
44 value = self._func(obj)
41 value = self._func(obj)
45 obj.__dict__[self.__name__] = value
42 obj.__dict__[self.__name__] = value
46 return value
43 return value
47
48
49 class ThreadLocalLazyProperty(LazyProperty):
50 """
51 Same as above but uses thread local dict for cache storage.
52 """
53
54 def __get__(self, obj, klass=None):
55 if obj is None:
56 return self
57 if not hasattr(obj, '__tl_dict__'):
58 obj.__tl_dict__ = threading.local().__dict__
59
60 value = obj.__tl_dict__.get(self.__name__, _missing)
61 if value is _missing:
62 value = self._func(obj)
63 obj.__tl_dict__[self.__name__] = value
64 return value
@@ -1,38 +1,38 b''
1 import os
1 import os
2
2
3
3
4 abspath = lambda * p: os.path.abspath(os.path.join(*p))
4 abspath = lambda * p: os.path.abspath(os.path.join(*p))
5
5
6
6
7 def get_dirs_for_path(*paths):
7 def get_dirs_for_path(*paths):
8 """
8 """
9 Returns list of directories, including intermediate.
9 Returns list of directories, including intermediate.
10 """
10 """
11 for path in paths:
11 for path in paths:
12 head = path
12 head = path
13 while head:
13 while head:
14 head, tail = os.path.split(head)
14 head, _tail = os.path.split(head)
15 if head:
15 if head:
16 yield head
16 yield head
17 else:
17 else:
18 # We don't need to yield empty path
18 # We don't need to yield empty path
19 break
19 break
20
20
21
21
22 def get_dir_size(path):
22 def get_dir_size(path):
23 root_path = path
23 root_path = path
24 size = 0
24 size = 0
25 for path, dirs, files in os.walk(root_path):
25 for path, dirs, files in os.walk(root_path):
26 for f in files:
26 for f in files:
27 try:
27 try:
28 size += os.path.getsize(os.path.join(path, f))
28 size += os.path.getsize(os.path.join(path, f))
29 except OSError:
29 except OSError:
30 pass
30 pass
31 return size
31 return size
32
32
33
33
34 def get_user_home():
34 def get_user_home():
35 """
35 """
36 Returns home path of the user.
36 Returns home path of the user.
37 """
37 """
38 return os.getenv('HOME', os.getenv('USERPROFILE')) or ''
38 return os.getenv('HOME', os.getenv('USERPROFILE')) or ''
@@ -1,230 +1,228 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.notification
15 kallithea.model.notification
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Model for notifications
18 Model for notifications
19
19
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Nov 20, 2011
23 :created_on: Nov 20, 2011
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 import datetime
29 import datetime
30 import logging
30 import logging
31
31
32 from tg import app_globals
32 from tg import app_globals
33 from tg import tmpl_context as c
33 from tg import tmpl_context as c
34 from tg.i18n import ugettext as _
34 from tg.i18n import ugettext as _
35
35
36 import kallithea
37 from kallithea.lib import helpers as h
36 from kallithea.lib import helpers as h
38 from kallithea.model.db import User
37 from kallithea.model.db import User
39
38
40
39
41 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
42
41
43
42
44 class NotificationModel(object):
43 class NotificationModel(object):
45
44
46 TYPE_CHANGESET_COMMENT = 'cs_comment'
45 TYPE_CHANGESET_COMMENT = 'cs_comment'
47 TYPE_MESSAGE = 'message'
46 TYPE_MESSAGE = 'message'
48 TYPE_MENTION = 'mention' # not used
47 TYPE_MENTION = 'mention' # not used
49 TYPE_REGISTRATION = 'registration'
48 TYPE_REGISTRATION = 'registration'
50 TYPE_PULL_REQUEST = 'pull_request'
49 TYPE_PULL_REQUEST = 'pull_request'
51 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
50 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
52
51
53 def create(self, created_by, subject, body, recipients=None,
52 def create(self, created_by, subject, body, recipients=None,
54 type_=TYPE_MESSAGE, with_email=True,
53 type_=TYPE_MESSAGE, with_email=True,
55 email_kwargs=None, repo_name=None):
54 email_kwargs=None, repo_name=None):
56 """
55 """
57
56
58 Creates notification of given type
57 Creates notification of given type
59
58
60 :param created_by: int, str or User instance. User who created this
59 :param created_by: int, str or User instance. User who created this
61 notification
60 notification
62 :param subject:
61 :param subject:
63 :param body:
62 :param body:
64 :param recipients: list of int, str or User objects, when None
63 :param recipients: list of int, str or User objects, when None
65 is given send to all admins
64 is given send to all admins
66 :param type_: type of notification
65 :param type_: type of notification
67 :param with_email: send email with this notification
66 :param with_email: send email with this notification
68 :param email_kwargs: additional dict to pass as args to email template
67 :param email_kwargs: additional dict to pass as args to email template
69 """
68 """
70 from kallithea.lib.celerylib import tasks
69 from kallithea.lib.celerylib import tasks
71 email_kwargs = email_kwargs or {}
70 email_kwargs = email_kwargs or {}
72 if recipients and not getattr(recipients, '__iter__', False):
71 if recipients and not getattr(recipients, '__iter__', False):
73 raise Exception('recipients must be a list or iterable')
72 raise Exception('recipients must be a list or iterable')
74
73
75 created_by_obj = User.guess_instance(created_by)
74 created_by_obj = User.guess_instance(created_by)
76
75
77 recipients_objs = set()
76 recipients_objs = set()
78 if recipients:
77 if recipients:
79 for u in recipients:
78 for u in recipients:
80 obj = User.guess_instance(u)
79 obj = User.guess_instance(u)
81 if obj is not None:
80 if obj is not None:
82 recipients_objs.add(obj)
81 recipients_objs.add(obj)
83 else:
82 else:
84 # TODO: inform user that requested operation couldn't be completed
83 # TODO: inform user that requested operation couldn't be completed
85 log.error('cannot email unknown user %r', u)
84 log.error('cannot email unknown user %r', u)
86 log.debug('sending notifications %s to %s',
85 log.debug('sending notifications %s to %s',
87 type_, recipients_objs
86 type_, recipients_objs
88 )
87 )
89 elif recipients is None:
88 elif recipients is None:
90 # empty recipients means to all admins
89 # empty recipients means to all admins
91 recipients_objs = User.query().filter(User.admin == True).all()
90 recipients_objs = User.query().filter(User.admin == True).all()
92 log.debug('sending notifications %s to admins: %s',
91 log.debug('sending notifications %s to admins: %s',
93 type_, recipients_objs
92 type_, recipients_objs
94 )
93 )
95 #else: silently skip notification mails?
94 #else: silently skip notification mails?
96
95
97 if not with_email:
96 if not with_email:
98 return
97 return
99
98
100 headers = {}
99 headers = {}
101 headers['X-Kallithea-Notification-Type'] = type_
100 headers['X-Kallithea-Notification-Type'] = type_
102 if 'threading' in email_kwargs:
101 if 'threading' in email_kwargs:
103 headers['References'] = ' '.join('<%s>' % x for x in email_kwargs['threading'])
102 headers['References'] = ' '.join('<%s>' % x for x in email_kwargs['threading'])
104
103
105 # this is passed into template
104 # this is passed into template
106 created_on = h.fmt_date(datetime.datetime.now())
105 created_on = h.fmt_date(datetime.datetime.now())
107 html_kwargs = {
106 html_kwargs = {
108 'subject': subject,
107 'subject': subject,
109 'body': h.render_w_mentions(body, repo_name),
108 'body': h.render_w_mentions(body, repo_name),
110 'when': created_on,
109 'when': created_on,
111 'user': created_by_obj.username,
110 'user': created_by_obj.username,
112 }
111 }
113
112
114 txt_kwargs = {
113 txt_kwargs = {
115 'subject': subject,
114 'subject': subject,
116 'body': body,
115 'body': body,
117 'when': created_on,
116 'when': created_on,
118 'user': created_by_obj.username,
117 'user': created_by_obj.username,
119 }
118 }
120
119
121 html_kwargs.update(email_kwargs)
120 html_kwargs.update(email_kwargs)
122 txt_kwargs.update(email_kwargs)
121 txt_kwargs.update(email_kwargs)
123 email_subject = EmailNotificationModel() \
122 email_subject = EmailNotificationModel() \
124 .get_email_description(type_, **txt_kwargs)
123 .get_email_description(type_, **txt_kwargs)
125 email_txt_body = EmailNotificationModel() \
124 email_txt_body = EmailNotificationModel() \
126 .get_email_tmpl(type_, 'txt', **txt_kwargs)
125 .get_email_tmpl(type_, 'txt', **txt_kwargs)
127 email_html_body = EmailNotificationModel() \
126 email_html_body = EmailNotificationModel() \
128 .get_email_tmpl(type_, 'html', **html_kwargs)
127 .get_email_tmpl(type_, 'html', **html_kwargs)
129
128
130 # don't send email to person who created this comment
129 # don't send email to person who created this comment
131 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
130 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
132
131
133 # send email with notification to all other participants
132 # send email with notification to all other participants
134 for rec in rec_objs:
133 for rec in rec_objs:
135 tasks.send_email([rec.email], email_subject, email_txt_body,
134 tasks.send_email([rec.email], email_subject, email_txt_body,
136 email_html_body, headers, author=created_by_obj)
135 email_html_body, headers, author=created_by_obj)
137
136
138
137
139 class EmailNotificationModel(object):
138 class EmailNotificationModel(object):
140
139
141 TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
140 TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
142 TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
141 TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
143 # NotificationModel.TYPE_MENTION is not used
142 # NotificationModel.TYPE_MENTION is not used
144 TYPE_PASSWORD_RESET = 'password_link'
143 TYPE_PASSWORD_RESET = 'password_link'
145 TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
144 TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
146 TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
145 TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
147 TYPE_PULL_REQUEST_COMMENT = NotificationModel.TYPE_PULL_REQUEST_COMMENT
146 TYPE_PULL_REQUEST_COMMENT = NotificationModel.TYPE_PULL_REQUEST_COMMENT
148 TYPE_DEFAULT = 'default'
147 TYPE_DEFAULT = 'default'
149
148
150 def __init__(self):
149 def __init__(self):
151 super(EmailNotificationModel, self).__init__()
150 super(EmailNotificationModel, self).__init__()
152 self._template_root = kallithea.CONFIG['paths']['templates'][0]
153 self._tmpl_lookup = app_globals.mako_lookup
151 self._tmpl_lookup = app_globals.mako_lookup
154 self.email_types = {
152 self.email_types = {
155 self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
153 self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
156 self.TYPE_PASSWORD_RESET: 'password_reset',
154 self.TYPE_PASSWORD_RESET: 'password_reset',
157 self.TYPE_REGISTRATION: 'registration',
155 self.TYPE_REGISTRATION: 'registration',
158 self.TYPE_DEFAULT: 'default',
156 self.TYPE_DEFAULT: 'default',
159 self.TYPE_PULL_REQUEST: 'pull_request',
157 self.TYPE_PULL_REQUEST: 'pull_request',
160 self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
158 self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
161 }
159 }
162 self._subj_map = {
160 self._subj_map = {
163 self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
161 self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
164 self.TYPE_MESSAGE: 'Test Message',
162 self.TYPE_MESSAGE: 'Test Message',
165 # self.TYPE_PASSWORD_RESET
163 # self.TYPE_PASSWORD_RESET
166 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
164 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
167 # self.TYPE_DEFAULT
165 # self.TYPE_DEFAULT
168 self.TYPE_PULL_REQUEST: _('[Review] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
166 self.TYPE_PULL_REQUEST: _('[Review] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
169 self.TYPE_PULL_REQUEST_COMMENT: _('[Comment] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
167 self.TYPE_PULL_REQUEST_COMMENT: _('[Comment] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
170 }
168 }
171
169
172 def get_email_description(self, type_, **kwargs):
170 def get_email_description(self, type_, **kwargs):
173 """
171 """
174 return subject for email based on given type
172 return subject for email based on given type
175 """
173 """
176 tmpl = self._subj_map[type_]
174 tmpl = self._subj_map[type_]
177 try:
175 try:
178 subj = tmpl % kwargs
176 subj = tmpl % kwargs
179 except KeyError as e:
177 except KeyError as e:
180 log.error('error generating email subject for %r from %s: %s', type_, ', '.join(self._subj_map), e)
178 log.error('error generating email subject for %r from %s: %s', type_, ', '.join(self._subj_map), e)
181 raise
179 raise
182 # gmail doesn't do proper threading but will ignore leading square
180 # gmail doesn't do proper threading but will ignore leading square
183 # bracket content ... so that is where we put status info
181 # bracket content ... so that is where we put status info
184 bracket_tags = []
182 bracket_tags = []
185 status_change = kwargs.get('status_change')
183 status_change = kwargs.get('status_change')
186 if status_change:
184 if status_change:
187 bracket_tags.append(str(status_change)) # apply str to evaluate LazyString before .join
185 bracket_tags.append(str(status_change)) # apply str to evaluate LazyString before .join
188 if kwargs.get('closing_pr'):
186 if kwargs.get('closing_pr'):
189 bracket_tags.append(_('Closing'))
187 bracket_tags.append(_('Closing'))
190 if bracket_tags:
188 if bracket_tags:
191 if subj.startswith('['):
189 if subj.startswith('['):
192 subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
190 subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
193 else:
191 else:
194 subj = '[' + ', '.join(bracket_tags) + '] ' + subj
192 subj = '[' + ', '.join(bracket_tags) + '] ' + subj
195 return subj
193 return subj
196
194
197 def get_email_tmpl(self, type_, content_type, **kwargs):
195 def get_email_tmpl(self, type_, content_type, **kwargs):
198 """
196 """
199 return generated template for email based on given type
197 return generated template for email based on given type
200 """
198 """
201
199
202 base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
200 base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
203 email_template = self._tmpl_lookup.get_template(base)
201 email_template = self._tmpl_lookup.get_template(base)
204 # translator and helpers inject
202 # translator and helpers inject
205 _kwargs = {'_': _,
203 _kwargs = {'_': _,
206 'h': h,
204 'h': h,
207 'c': c}
205 'c': c}
208 _kwargs.update(kwargs)
206 _kwargs.update(kwargs)
209 if content_type == 'html':
207 if content_type == 'html':
210 _kwargs.update({
208 _kwargs.update({
211 "color_text": "#202020",
209 "color_text": "#202020",
212 "color_emph": "#395fa0",
210 "color_emph": "#395fa0",
213 "color_link": "#395fa0",
211 "color_link": "#395fa0",
214 "color_border": "#ddd",
212 "color_border": "#ddd",
215 "color_background_grey": "#f9f9f9",
213 "color_background_grey": "#f9f9f9",
216 "color_button": "#395fa0",
214 "color_button": "#395fa0",
217 "monospace_style": "font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace",
215 "monospace_style": "font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace",
218 "sans_style": "font-family:Helvetica,Arial,sans-serif",
216 "sans_style": "font-family:Helvetica,Arial,sans-serif",
219 })
217 })
220 _kwargs.update({
218 _kwargs.update({
221 "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
219 "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
222 "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
220 "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
223 "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
221 "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
224 "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
222 "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
225 "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
223 "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
226 "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
224 "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
227 })
225 })
228
226
229 log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
227 log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
230 return email_template.render_unicode(**_kwargs)
228 return email_template.render_unicode(**_kwargs)
@@ -1,138 +1,138 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.ssh_key
15 kallithea.model.ssh_key
16 ~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 SSH key model for Kallithea
18 SSH key model for Kallithea
19
19
20 """
20 """
21
21
22 import errno
22 import errno
23 import logging
23 import logging
24 import os
24 import os
25 import stat
25 import stat
26 import tempfile
26 import tempfile
27
27
28 from tg import config
28 from tg import config
29 from tg.i18n import ugettext as _
29 from tg.i18n import ugettext as _
30
30
31 from kallithea.lib import ssh
31 from kallithea.lib import ssh
32 from kallithea.lib.utils2 import str2bool
32 from kallithea.lib.utils2 import str2bool
33 from kallithea.lib.vcs.exceptions import RepositoryError
33 from kallithea.lib.vcs.exceptions import RepositoryError
34 from kallithea.model.db import User, UserSshKeys
34 from kallithea.model.db import User, UserSshKeys
35 from kallithea.model.meta import Session
35 from kallithea.model.meta import Session
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class SshKeyModelException(RepositoryError):
41 class SshKeyModelException(RepositoryError):
42 """Exception raised by SshKeyModel methods to report errors"""
42 """Exception raised by SshKeyModel methods to report errors"""
43
43
44
44
45 class SshKeyModel(object):
45 class SshKeyModel(object):
46
46
47 def create(self, user, description, public_key):
47 def create(self, user, description, public_key):
48 """
48 """
49 :param user: user or user_id
49 :param user: user or user_id
50 :param description: description of SshKey
50 :param description: description of SshKey
51 :param publickey: public key text
51 :param publickey: public key text
52 Will raise SshKeyModelException on errors
52 Will raise SshKeyModelException on errors
53 """
53 """
54 try:
54 try:
55 keytype, pub, comment = ssh.parse_pub_key(public_key)
55 keytype, _pub, comment = ssh.parse_pub_key(public_key)
56 except ssh.SshKeyParseError as e:
56 except ssh.SshKeyParseError as e:
57 raise SshKeyModelException(_('SSH key %r is invalid: %s') % (public_key, e.args[0]))
57 raise SshKeyModelException(_('SSH key %r is invalid: %s') % (public_key, e.args[0]))
58 if not description.strip():
58 if not description.strip():
59 description = comment.strip()
59 description = comment.strip()
60
60
61 user = User.guess_instance(user)
61 user = User.guess_instance(user)
62
62
63 new_ssh_key = UserSshKeys()
63 new_ssh_key = UserSshKeys()
64 new_ssh_key.user_id = user.user_id
64 new_ssh_key.user_id = user.user_id
65 new_ssh_key.description = description
65 new_ssh_key.description = description
66 new_ssh_key.public_key = public_key
66 new_ssh_key.public_key = public_key
67
67
68 for ssh_key in UserSshKeys.query().filter(UserSshKeys.fingerprint == new_ssh_key.fingerprint).all():
68 for ssh_key in UserSshKeys.query().filter(UserSshKeys.fingerprint == new_ssh_key.fingerprint).all():
69 raise SshKeyModelException(_('SSH key %s is already used by %s') %
69 raise SshKeyModelException(_('SSH key %s is already used by %s') %
70 (new_ssh_key.fingerprint, ssh_key.user.username))
70 (new_ssh_key.fingerprint, ssh_key.user.username))
71
71
72 Session().add(new_ssh_key)
72 Session().add(new_ssh_key)
73
73
74 return new_ssh_key
74 return new_ssh_key
75
75
76 def delete(self, fingerprint, user):
76 def delete(self, fingerprint, user):
77 """
77 """
78 Deletes ssh key with given fingerprint for the given user.
78 Deletes ssh key with given fingerprint for the given user.
79 Will raise SshKeyModelException on errors
79 Will raise SshKeyModelException on errors
80 """
80 """
81 ssh_key = UserSshKeys.query().filter(UserSshKeys.fingerprint == fingerprint)
81 ssh_key = UserSshKeys.query().filter(UserSshKeys.fingerprint == fingerprint)
82
82
83 user = User.guess_instance(user)
83 user = User.guess_instance(user)
84 ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
84 ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
85
85
86 ssh_key = ssh_key.scalar()
86 ssh_key = ssh_key.scalar()
87 if ssh_key is None:
87 if ssh_key is None:
88 raise SshKeyModelException(_('SSH key with fingerprint %r found') % fingerprint)
88 raise SshKeyModelException(_('SSH key with fingerprint %r found') % fingerprint)
89 Session().delete(ssh_key)
89 Session().delete(ssh_key)
90
90
91 def get_ssh_keys(self, user):
91 def get_ssh_keys(self, user):
92 user = User.guess_instance(user)
92 user = User.guess_instance(user)
93 user_ssh_keys = UserSshKeys.query() \
93 user_ssh_keys = UserSshKeys.query() \
94 .filter(UserSshKeys.user_id == user.user_id).all()
94 .filter(UserSshKeys.user_id == user.user_id).all()
95 return user_ssh_keys
95 return user_ssh_keys
96
96
97 def write_authorized_keys(self):
97 def write_authorized_keys(self):
98 if not str2bool(config.get('ssh_enabled', False)):
98 if not str2bool(config.get('ssh_enabled', False)):
99 log.error("Will not write SSH authorized_keys file - ssh_enabled is not configured")
99 log.error("Will not write SSH authorized_keys file - ssh_enabled is not configured")
100 return
100 return
101 authorized_keys = config.get('ssh_authorized_keys')
101 authorized_keys = config.get('ssh_authorized_keys')
102 kallithea_cli_path = config.get('kallithea_cli_path', 'kallithea-cli')
102 kallithea_cli_path = config.get('kallithea_cli_path', 'kallithea-cli')
103 if not authorized_keys:
103 if not authorized_keys:
104 log.error('Cannot write SSH authorized_keys file - ssh_authorized_keys is not configured')
104 log.error('Cannot write SSH authorized_keys file - ssh_authorized_keys is not configured')
105 return
105 return
106 log.info('Writing %s', authorized_keys)
106 log.info('Writing %s', authorized_keys)
107
107
108 authorized_keys_dir = os.path.dirname(authorized_keys)
108 authorized_keys_dir = os.path.dirname(authorized_keys)
109 try:
109 try:
110 os.makedirs(authorized_keys_dir)
110 os.makedirs(authorized_keys_dir)
111 os.chmod(authorized_keys_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # ~/.ssh/ must be 0700
111 os.chmod(authorized_keys_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # ~/.ssh/ must be 0700
112 except OSError as exception:
112 except OSError as exception:
113 if exception.errno != errno.EEXIST:
113 if exception.errno != errno.EEXIST:
114 raise
114 raise
115 # Now, test that the directory is or was created in a readable way by previous.
115 # Now, test that the directory is or was created in a readable way by previous.
116 if not (os.path.isdir(authorized_keys_dir) and
116 if not (os.path.isdir(authorized_keys_dir) and
117 os.access(authorized_keys_dir, os.W_OK)):
117 os.access(authorized_keys_dir, os.W_OK)):
118 raise SshKeyModelException("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys))
118 raise SshKeyModelException("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys))
119
119
120 # Make sure we don't overwrite a key file with important content
120 # Make sure we don't overwrite a key file with important content
121 if os.path.exists(authorized_keys):
121 if os.path.exists(authorized_keys):
122 with open(authorized_keys) as f:
122 with open(authorized_keys) as f:
123 for l in f:
123 for l in f:
124 if not l.strip() or l.startswith('#'):
124 if not l.strip() or l.startswith('#'):
125 pass # accept empty lines and comments
125 pass # accept empty lines and comments
126 elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l:
126 elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l:
127 pass # Kallithea entries are ok to overwrite
127 pass # Kallithea entries are ok to overwrite
128 else:
128 else:
129 raise SshKeyModelException("Safety check failed, found %r line in %s - please remove it if Kallithea should manage the file" % (l.strip(), authorized_keys))
129 raise SshKeyModelException("Safety check failed, found %r line in %s - please remove it if Kallithea should manage the file" % (l.strip(), authorized_keys))
130
130
131 fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys))
131 fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys))
132 with os.fdopen(fh, 'w') as f:
132 with os.fdopen(fh, 'w') as f:
133 f.write("# WARNING: This .ssh/authorized_keys file is managed by Kallithea. Manual editing or adding new entries will make Kallithea back off.\n")
133 f.write("# WARNING: This .ssh/authorized_keys file is managed by Kallithea. Manual editing or adding new entries will make Kallithea back off.\n")
134 for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True):
134 for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True):
135 f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key))
135 f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key))
136 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
136 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
137 # Note: simple overwrite / rename isn't enough to replace the file on Windows
137 # Note: simple overwrite / rename isn't enough to replace the file on Windows
138 os.replace(tmp_authorized_keys, authorized_keys)
138 os.replace(tmp_authorized_keys, authorized_keys)
@@ -1,804 +1,804 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 Set of generic validators
15 Set of generic validators
16 """
16 """
17
17
18 import logging
18 import logging
19 import os
19 import os
20 import re
20 import re
21 from collections import defaultdict
21 from collections import defaultdict
22
22
23 import formencode
23 import formencode
24 import ipaddr
24 import ipaddr
25 import sqlalchemy
25 import sqlalchemy
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
27 from sqlalchemy import func
27 from sqlalchemy import func
28 from tg.i18n import ugettext as _
28 from tg.i18n import ugettext as _
29
29
30 from kallithea.config.routing import ADMIN_PREFIX
30 from kallithea.config.routing import ADMIN_PREFIX
31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
32 from kallithea.lib.compat import OrderedSet
32 from kallithea.lib.compat import OrderedSet
33 from kallithea.lib.exceptions import LdapImportError
33 from kallithea.lib.exceptions import LdapImportError
34 from kallithea.lib.utils import is_valid_repo_uri
34 from kallithea.lib.utils import is_valid_repo_uri
35 from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
35 from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
36 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
36 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
37
37
38
38
39 # silence warnings and pylint
39 # silence warnings and pylint
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def UniqueListFromString():
46 def UniqueListFromString():
47 class _UniqueListFromString(formencode.FancyValidator):
47 class _UniqueListFromString(formencode.FancyValidator):
48 """
48 """
49 Split value on ',' and make unique while preserving order
49 Split value on ',' and make unique while preserving order
50 """
50 """
51 messages = dict(
51 messages = dict(
52 empty=_('Value cannot be an empty list'),
52 empty=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
54 )
54 )
55
55
56 def _convert_to_python(self, value, state):
56 def _convert_to_python(self, value, state):
57 value = aslist(value, ',')
57 value = aslist(value, ',')
58 seen = set()
58 seen = set()
59 return [c for c in value if not (c in seen or seen.add(c))]
59 return [c for c in value if not (c in seen or seen.add(c))]
60
60
61 def empty_value(self, value):
61 def empty_value(self, value):
62 return []
62 return []
63
63
64 return _UniqueListFromString
64 return _UniqueListFromString
65
65
66
66
67 def ValidUsername(edit=False, old_data=None):
67 def ValidUsername(edit=False, old_data=None):
68 old_data = old_data or {}
68 old_data = old_data or {}
69
69
70 class _validator(formencode.validators.FancyValidator):
70 class _validator(formencode.validators.FancyValidator):
71 messages = {
71 messages = {
72 'username_exists': _('Username "%(username)s" already exists'),
72 'username_exists': _('Username "%(username)s" already exists'),
73 'system_invalid_username':
73 'system_invalid_username':
74 _('Username "%(username)s" cannot be used'),
74 _('Username "%(username)s" cannot be used'),
75 'invalid_username':
75 'invalid_username':
76 _('Username may only contain alphanumeric characters '
76 _('Username may only contain alphanumeric characters '
77 'underscores, periods or dashes and must begin with an '
77 'underscores, periods or dashes and must begin with an '
78 'alphanumeric character or underscore')
78 'alphanumeric character or underscore')
79 }
79 }
80
80
81 def _validate_python(self, value, state):
81 def _validate_python(self, value, state):
82 if value in ['default', 'new_user']:
82 if value in ['default', 'new_user']:
83 msg = self.message('system_invalid_username', state, username=value)
83 msg = self.message('system_invalid_username', state, username=value)
84 raise formencode.Invalid(msg, value, state)
84 raise formencode.Invalid(msg, value, state)
85 # check if user is unique
85 # check if user is unique
86 old_un = None
86 old_un = None
87 if edit:
87 if edit:
88 old_un = User.get(old_data.get('user_id')).username
88 old_un = User.get(old_data.get('user_id')).username
89
89
90 if old_un != value or not edit:
90 if old_un != value or not edit:
91 if User.get_by_username(value, case_insensitive=True):
91 if User.get_by_username(value, case_insensitive=True):
92 msg = self.message('username_exists', state, username=value)
92 msg = self.message('username_exists', state, username=value)
93 raise formencode.Invalid(msg, value, state)
93 raise formencode.Invalid(msg, value, state)
94
94
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
96 msg = self.message('invalid_username', state)
96 msg = self.message('invalid_username', state)
97 raise formencode.Invalid(msg, value, state)
97 raise formencode.Invalid(msg, value, state)
98 return _validator
98 return _validator
99
99
100
100
101 def ValidRegex(msg=None):
101 def ValidRegex(msg=None):
102 class _validator(formencode.validators.Regex):
102 class _validator(formencode.validators.Regex):
103 messages = dict(invalid=msg or _('The input is not valid'))
103 messages = dict(invalid=msg or _('The input is not valid'))
104 return _validator
104 return _validator
105
105
106
106
107 def ValidRepoUser():
107 def ValidRepoUser():
108 class _validator(formencode.validators.FancyValidator):
108 class _validator(formencode.validators.FancyValidator):
109 messages = {
109 messages = {
110 'invalid_username': _('Username %(username)s is not valid')
110 'invalid_username': _('Username %(username)s is not valid')
111 }
111 }
112
112
113 def _validate_python(self, value, state):
113 def _validate_python(self, value, state):
114 try:
114 try:
115 User.query().filter(User.active == True) \
115 User.query().filter(User.active == True) \
116 .filter(User.username == value).one()
116 .filter(User.username == value).one()
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
118 msg = self.message('invalid_username', state, username=value)
118 msg = self.message('invalid_username', state, username=value)
119 raise formencode.Invalid(msg, value, state,
119 raise formencode.Invalid(msg, value, state,
120 error_dict=dict(username=msg)
120 error_dict=dict(username=msg)
121 )
121 )
122
122
123 return _validator
123 return _validator
124
124
125
125
126 def ValidUserGroup(edit=False, old_data=None):
126 def ValidUserGroup(edit=False, old_data=None):
127 old_data = old_data or {}
127 old_data = old_data or {}
128
128
129 class _validator(formencode.validators.FancyValidator):
129 class _validator(formencode.validators.FancyValidator):
130 messages = {
130 messages = {
131 'invalid_group': _('Invalid user group name'),
131 'invalid_group': _('Invalid user group name'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
133 'invalid_usergroup_name':
133 'invalid_usergroup_name':
134 _('user group name may only contain alphanumeric '
134 _('user group name may only contain alphanumeric '
135 'characters underscores, periods or dashes and must begin '
135 'characters underscores, periods or dashes and must begin '
136 'with alphanumeric character')
136 'with alphanumeric character')
137 }
137 }
138
138
139 def _validate_python(self, value, state):
139 def _validate_python(self, value, state):
140 if value in ['default']:
140 if value in ['default']:
141 msg = self.message('invalid_group', state)
141 msg = self.message('invalid_group', state)
142 raise formencode.Invalid(msg, value, state,
142 raise formencode.Invalid(msg, value, state,
143 error_dict=dict(users_group_name=msg)
143 error_dict=dict(users_group_name=msg)
144 )
144 )
145 # check if group is unique
145 # check if group is unique
146 old_ugname = None
146 old_ugname = None
147 if edit:
147 if edit:
148 old_id = old_data.get('users_group_id')
148 old_id = old_data.get('users_group_id')
149 old_ugname = UserGroup.get(old_id).users_group_name
149 old_ugname = UserGroup.get(old_id).users_group_name
150
150
151 if old_ugname != value or not edit:
151 if old_ugname != value or not edit:
152 is_existing_group = UserGroup.get_by_group_name(value,
152 is_existing_group = UserGroup.get_by_group_name(value,
153 case_insensitive=True)
153 case_insensitive=True)
154 if is_existing_group:
154 if is_existing_group:
155 msg = self.message('group_exist', state, usergroup=value)
155 msg = self.message('group_exist', state, usergroup=value)
156 raise formencode.Invalid(msg, value, state,
156 raise formencode.Invalid(msg, value, state,
157 error_dict=dict(users_group_name=msg)
157 error_dict=dict(users_group_name=msg)
158 )
158 )
159
159
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
161 msg = self.message('invalid_usergroup_name', state)
161 msg = self.message('invalid_usergroup_name', state)
162 raise formencode.Invalid(msg, value, state,
162 raise formencode.Invalid(msg, value, state,
163 error_dict=dict(users_group_name=msg)
163 error_dict=dict(users_group_name=msg)
164 )
164 )
165
165
166 return _validator
166 return _validator
167
167
168
168
169 def ValidRepoGroup(edit=False, old_data=None):
169 def ValidRepoGroup(edit=False, old_data=None):
170 old_data = old_data or {}
170 old_data = old_data or {}
171
171
172 class _validator(formencode.validators.FancyValidator):
172 class _validator(formencode.validators.FancyValidator):
173 messages = {
173 messages = {
174 'parent_group_id': _('Cannot assign this group as parent'),
174 'parent_group_id': _('Cannot assign this group as parent'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
176 'repo_exists':
176 'repo_exists':
177 _('Repository with name "%(group_name)s" already exists')
177 _('Repository with name "%(group_name)s" already exists')
178 }
178 }
179
179
180 def _validate_python(self, value, state):
180 def _validate_python(self, value, state):
181 # TODO WRITE VALIDATIONS
181 # TODO WRITE VALIDATIONS
182 group_name = value.get('group_name')
182 group_name = value.get('group_name')
183 parent_group_id = value.get('parent_group_id')
183 parent_group_id = value.get('parent_group_id')
184
184
185 # slugify repo group just in case :)
185 # slugify repo group just in case :)
186 slug = repo_name_slug(group_name)
186 slug = repo_name_slug(group_name)
187
187
188 # check for parent of self
188 # check for parent of self
189 parent_of_self = lambda: (
189 parent_of_self = lambda: (
190 old_data['group_id'] == parent_group_id
190 old_data['group_id'] == parent_group_id
191 if parent_group_id else False
191 if parent_group_id else False
192 )
192 )
193 if edit and parent_of_self():
193 if edit and parent_of_self():
194 msg = self.message('parent_group_id', state)
194 msg = self.message('parent_group_id', state)
195 raise formencode.Invalid(msg, value, state,
195 raise formencode.Invalid(msg, value, state,
196 error_dict=dict(parent_group_id=msg)
196 error_dict=dict(parent_group_id=msg)
197 )
197 )
198
198
199 old_gname = None
199 old_gname = None
200 if edit:
200 if edit:
201 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
201 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
202
202
203 if old_gname != group_name or not edit:
203 if old_gname != group_name or not edit:
204
204
205 # check group
205 # check group
206 gr = RepoGroup.query() \
206 gr = RepoGroup.query() \
207 .filter(func.lower(RepoGroup.group_name) == func.lower(slug)) \
207 .filter(func.lower(RepoGroup.group_name) == func.lower(slug)) \
208 .filter(RepoGroup.parent_group_id == parent_group_id) \
208 .filter(RepoGroup.parent_group_id == parent_group_id) \
209 .scalar()
209 .scalar()
210 if gr is not None:
210 if gr is not None:
211 msg = self.message('group_exists', state, group_name=slug)
211 msg = self.message('group_exists', state, group_name=slug)
212 raise formencode.Invalid(msg, value, state,
212 raise formencode.Invalid(msg, value, state,
213 error_dict=dict(group_name=msg)
213 error_dict=dict(group_name=msg)
214 )
214 )
215
215
216 # check for same repo
216 # check for same repo
217 repo = Repository.query() \
217 repo = Repository.query() \
218 .filter(func.lower(Repository.repo_name) == func.lower(slug)) \
218 .filter(func.lower(Repository.repo_name) == func.lower(slug)) \
219 .scalar()
219 .scalar()
220 if repo is not None:
220 if repo is not None:
221 msg = self.message('repo_exists', state, group_name=slug)
221 msg = self.message('repo_exists', state, group_name=slug)
222 raise formencode.Invalid(msg, value, state,
222 raise formencode.Invalid(msg, value, state,
223 error_dict=dict(group_name=msg)
223 error_dict=dict(group_name=msg)
224 )
224 )
225
225
226 return _validator
226 return _validator
227
227
228
228
229 def ValidPassword():
229 def ValidPassword():
230 class _validator(formencode.validators.FancyValidator):
230 class _validator(formencode.validators.FancyValidator):
231 messages = {
231 messages = {
232 'invalid_password':
232 'invalid_password':
233 _('Invalid characters (non-ascii) in password')
233 _('Invalid characters (non-ascii) in password')
234 }
234 }
235
235
236 def _validate_python(self, value, state):
236 def _validate_python(self, value, state):
237 try:
237 try:
238 (value or '').encode('ascii')
238 (value or '').encode('ascii')
239 except UnicodeError:
239 except UnicodeError:
240 msg = self.message('invalid_password', state)
240 msg = self.message('invalid_password', state)
241 raise formencode.Invalid(msg, value, state,)
241 raise formencode.Invalid(msg, value, state,)
242 return _validator
242 return _validator
243
243
244
244
245 def ValidOldPassword(username):
245 def ValidOldPassword(username):
246 class _validator(formencode.validators.FancyValidator):
246 class _validator(formencode.validators.FancyValidator):
247 messages = {
247 messages = {
248 'invalid_password': _('Invalid old password')
248 'invalid_password': _('Invalid old password')
249 }
249 }
250
250
251 def _validate_python(self, value, state):
251 def _validate_python(self, value, state):
252 from kallithea.lib import auth_modules
252 from kallithea.lib import auth_modules
253 if auth_modules.authenticate(username, value, '') is None:
253 if auth_modules.authenticate(username, value, '') is None:
254 msg = self.message('invalid_password', state)
254 msg = self.message('invalid_password', state)
255 raise formencode.Invalid(msg, value, state,
255 raise formencode.Invalid(msg, value, state,
256 error_dict=dict(current_password=msg)
256 error_dict=dict(current_password=msg)
257 )
257 )
258 return _validator
258 return _validator
259
259
260
260
261 def ValidPasswordsMatch(password_field, password_confirmation_field):
261 def ValidPasswordsMatch(password_field, password_confirmation_field):
262 class _validator(formencode.validators.FancyValidator):
262 class _validator(formencode.validators.FancyValidator):
263 messages = {
263 messages = {
264 'password_mismatch': _('Passwords do not match'),
264 'password_mismatch': _('Passwords do not match'),
265 }
265 }
266
266
267 def _validate_python(self, value, state):
267 def _validate_python(self, value, state):
268 if value.get(password_field) != value[password_confirmation_field]:
268 if value.get(password_field) != value[password_confirmation_field]:
269 msg = self.message('password_mismatch', state)
269 msg = self.message('password_mismatch', state)
270 raise formencode.Invalid(msg, value, state,
270 raise formencode.Invalid(msg, value, state,
271 error_dict={password_field: msg, password_confirmation_field: msg}
271 error_dict={password_field: msg, password_confirmation_field: msg}
272 )
272 )
273 return _validator
273 return _validator
274
274
275
275
276 def ValidAuth():
276 def ValidAuth():
277 class _validator(formencode.validators.FancyValidator):
277 class _validator(formencode.validators.FancyValidator):
278 messages = {
278 messages = {
279 'invalid_auth': _('Invalid username or password'),
279 'invalid_auth': _('Invalid username or password'),
280 }
280 }
281
281
282 def _validate_python(self, value, state):
282 def _validate_python(self, value, state):
283 from kallithea.lib import auth_modules
283 from kallithea.lib import auth_modules
284
284
285 password = value['password']
285 password = value['password']
286 username = value['username']
286 username = value['username']
287
287
288 # authenticate returns unused dict but has called
288 # authenticate returns unused dict but has called
289 # plugin._authenticate which has create_or_update'ed the username user in db
289 # plugin._authenticate which has create_or_update'ed the username user in db
290 if auth_modules.authenticate(username, password) is None:
290 if auth_modules.authenticate(username, password) is None:
291 user = User.get_by_username_or_email(username)
291 user = User.get_by_username_or_email(username)
292 if user and not user.active:
292 if user and not user.active:
293 log.warning('user %s is disabled', username)
293 log.warning('user %s is disabled', username)
294 msg = self.message('invalid_auth', state)
294 msg = self.message('invalid_auth', state)
295 raise formencode.Invalid(msg, value, state,
295 raise formencode.Invalid(msg, value, state,
296 error_dict=dict(username=' ', password=msg)
296 error_dict=dict(username=' ', password=msg)
297 )
297 )
298 else:
298 else:
299 log.warning('user %s failed to authenticate', username)
299 log.warning('user %s failed to authenticate', username)
300 msg = self.message('invalid_auth', state)
300 msg = self.message('invalid_auth', state)
301 raise formencode.Invalid(msg, value, state,
301 raise formencode.Invalid(msg, value, state,
302 error_dict=dict(username=' ', password=msg)
302 error_dict=dict(username=' ', password=msg)
303 )
303 )
304 return _validator
304 return _validator
305
305
306
306
307 def ValidRepoName(edit=False, old_data=None):
307 def ValidRepoName(edit=False, old_data=None):
308 old_data = old_data or {}
308 old_data = old_data or {}
309
309
310 class _validator(formencode.validators.FancyValidator):
310 class _validator(formencode.validators.FancyValidator):
311 messages = {
311 messages = {
312 'invalid_repo_name':
312 'invalid_repo_name':
313 _('Repository name %(repo)s is not allowed'),
313 _('Repository name %(repo)s is not allowed'),
314 'repository_exists':
314 'repository_exists':
315 _('Repository named %(repo)s already exists'),
315 _('Repository named %(repo)s already exists'),
316 'repository_in_group_exists': _('Repository "%(repo)s" already '
316 'repository_in_group_exists': _('Repository "%(repo)s" already '
317 'exists in group "%(group)s"'),
317 'exists in group "%(group)s"'),
318 'same_group_exists': _('Repository group with name "%(repo)s" '
318 'same_group_exists': _('Repository group with name "%(repo)s" '
319 'already exists')
319 'already exists')
320 }
320 }
321
321
322 def _convert_to_python(self, value, state):
322 def _convert_to_python(self, value, state):
323 repo_name = repo_name_slug(value.get('repo_name', ''))
323 repo_name = repo_name_slug(value.get('repo_name', ''))
324 repo_group = value.get('repo_group')
324 repo_group = value.get('repo_group')
325 if repo_group:
325 if repo_group:
326 gr = RepoGroup.get(repo_group)
326 gr = RepoGroup.get(repo_group)
327 group_path = gr.full_path
327 group_path = gr.full_path
328 group_name = gr.group_name
328 group_name = gr.group_name
329 # value needs to be aware of group name in order to check
329 # value needs to be aware of group name in order to check
330 # db key This is an actual just the name to store in the
330 # db key This is an actual just the name to store in the
331 # database
331 # database
332 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
332 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
333 else:
333 else:
334 group_name = group_path = ''
334 group_name = group_path = ''
335 repo_name_full = repo_name
335 repo_name_full = repo_name
336
336
337 value['repo_name'] = repo_name
337 value['repo_name'] = repo_name
338 value['repo_name_full'] = repo_name_full
338 value['repo_name_full'] = repo_name_full
339 value['group_path'] = group_path
339 value['group_path'] = group_path
340 value['group_name'] = group_name
340 value['group_name'] = group_name
341 return value
341 return value
342
342
343 def _validate_python(self, value, state):
343 def _validate_python(self, value, state):
344 repo_name = value.get('repo_name')
344 repo_name = value.get('repo_name')
345 repo_name_full = value.get('repo_name_full')
345 repo_name_full = value.get('repo_name_full')
346 group_path = value.get('group_path')
346 group_path = value.get('group_path')
347 group_name = value.get('group_name')
347 group_name = value.get('group_name')
348
348
349 if repo_name in [ADMIN_PREFIX, '']:
349 if repo_name in [ADMIN_PREFIX, '']:
350 msg = self.message('invalid_repo_name', state, repo=repo_name)
350 msg = self.message('invalid_repo_name', state, repo=repo_name)
351 raise formencode.Invalid(msg, value, state,
351 raise formencode.Invalid(msg, value, state,
352 error_dict=dict(repo_name=msg)
352 error_dict=dict(repo_name=msg)
353 )
353 )
354
354
355 rename = old_data.get('repo_name') != repo_name_full
355 rename = old_data.get('repo_name') != repo_name_full
356 create = not edit
356 create = not edit
357 if rename or create:
357 if rename or create:
358 repo = Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
358 repo = Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
359 repo_group = RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
359 repo_group = RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
360 if group_path != '':
360 if group_path != '':
361 if repo is not None:
361 if repo is not None:
362 msg = self.message('repository_in_group_exists', state,
362 msg = self.message('repository_in_group_exists', state,
363 repo=repo.repo_name, group=group_name)
363 repo=repo.repo_name, group=group_name)
364 raise formencode.Invalid(msg, value, state,
364 raise formencode.Invalid(msg, value, state,
365 error_dict=dict(repo_name=msg)
365 error_dict=dict(repo_name=msg)
366 )
366 )
367 elif repo_group is not None:
367 elif repo_group is not None:
368 msg = self.message('same_group_exists', state,
368 msg = self.message('same_group_exists', state,
369 repo=repo_name)
369 repo=repo_name)
370 raise formencode.Invalid(msg, value, state,
370 raise formencode.Invalid(msg, value, state,
371 error_dict=dict(repo_name=msg)
371 error_dict=dict(repo_name=msg)
372 )
372 )
373 elif repo is not None:
373 elif repo is not None:
374 msg = self.message('repository_exists', state,
374 msg = self.message('repository_exists', state,
375 repo=repo.repo_name)
375 repo=repo.repo_name)
376 raise formencode.Invalid(msg, value, state,
376 raise formencode.Invalid(msg, value, state,
377 error_dict=dict(repo_name=msg)
377 error_dict=dict(repo_name=msg)
378 )
378 )
379 return value
379 return value
380 return _validator
380 return _validator
381
381
382
382
383 def ValidForkName(*args, **kwargs):
383 def ValidForkName(*args, **kwargs):
384 return ValidRepoName(*args, **kwargs)
384 return ValidRepoName(*args, **kwargs)
385
385
386
386
387 def SlugifyName():
387 def SlugifyName():
388 class _validator(formencode.validators.FancyValidator):
388 class _validator(formencode.validators.FancyValidator):
389
389
390 def _convert_to_python(self, value, state):
390 def _convert_to_python(self, value, state):
391 return repo_name_slug(value)
391 return repo_name_slug(value)
392
392
393 def _validate_python(self, value, state):
393 def _validate_python(self, value, state):
394 pass
394 pass
395
395
396 return _validator
396 return _validator
397
397
398
398
399 def ValidCloneUri():
399 def ValidCloneUri():
400 from kallithea.lib.utils import make_ui
400 from kallithea.lib.utils import make_ui
401
401
402 class _validator(formencode.validators.FancyValidator):
402 class _validator(formencode.validators.FancyValidator):
403 messages = {
403 messages = {
404 'clone_uri': _('Invalid repository URL'),
404 'clone_uri': _('Invalid repository URL'),
405 'invalid_clone_uri': _('Invalid repository URL. It must be a '
405 'invalid_clone_uri': _('Invalid repository URL. It must be a '
406 'valid http, https, ssh, svn+http or svn+https URL'),
406 'valid http, https, ssh, svn+http or svn+https URL'),
407 }
407 }
408
408
409 def _validate_python(self, value, state):
409 def _validate_python(self, value, state):
410 repo_type = value.get('repo_type')
410 repo_type = value.get('repo_type')
411 url = value.get('clone_uri')
411 url = value.get('clone_uri')
412
412
413 if url and url != value.get('clone_uri_hidden'):
413 if url and url != value.get('clone_uri_hidden'):
414 try:
414 try:
415 is_valid_repo_uri(repo_type, url, make_ui())
415 is_valid_repo_uri(repo_type, url, make_ui())
416 except Exception:
416 except Exception:
417 log.exception('URL validation failed')
417 log.exception('URL validation failed')
418 msg = self.message('clone_uri', state)
418 msg = self.message('clone_uri', state)
419 raise formencode.Invalid(msg, value, state,
419 raise formencode.Invalid(msg, value, state,
420 error_dict=dict(clone_uri=msg)
420 error_dict=dict(clone_uri=msg)
421 )
421 )
422 return _validator
422 return _validator
423
423
424
424
425 def ValidForkType(old_data=None):
425 def ValidForkType(old_data=None):
426 old_data = old_data or {}
426 old_data = old_data or {}
427
427
428 class _validator(formencode.validators.FancyValidator):
428 class _validator(formencode.validators.FancyValidator):
429 messages = {
429 messages = {
430 'invalid_fork_type': _('Fork has to be the same type as parent')
430 'invalid_fork_type': _('Fork has to be the same type as parent')
431 }
431 }
432
432
433 def _validate_python(self, value, state):
433 def _validate_python(self, value, state):
434 if old_data['repo_type'] != value:
434 if old_data['repo_type'] != value:
435 msg = self.message('invalid_fork_type', state)
435 msg = self.message('invalid_fork_type', state)
436 raise formencode.Invalid(msg, value, state,
436 raise formencode.Invalid(msg, value, state,
437 error_dict=dict(repo_type=msg)
437 error_dict=dict(repo_type=msg)
438 )
438 )
439 return _validator
439 return _validator
440
440
441
441
442 def CanWriteGroup(old_data=None):
442 def CanWriteGroup(old_data=None):
443 class _validator(formencode.validators.FancyValidator):
443 class _validator(formencode.validators.FancyValidator):
444 messages = {
444 messages = {
445 'permission_denied': _("You don't have permissions "
445 'permission_denied': _("You don't have permissions "
446 "to create repository in this group"),
446 "to create repository in this group"),
447 'permission_denied_root': _("no permission to create repository "
447 'permission_denied_root': _("no permission to create repository "
448 "in root location")
448 "in root location")
449 }
449 }
450
450
451 def _convert_to_python(self, value, state):
451 def _convert_to_python(self, value, state):
452 # root location
452 # root location
453 if value == -1:
453 if value == -1:
454 return None
454 return None
455 return value
455 return value
456
456
457 def _validate_python(self, value, state):
457 def _validate_python(self, value, state):
458 gr = RepoGroup.get(value)
458 gr = RepoGroup.get(value)
459 gr_name = gr.group_name if gr is not None else None # None means ROOT location
459 gr_name = gr.group_name if gr is not None else None # None means ROOT location
460
460
461 # create repositories with write permission on group is set to true
461 # create repositories with write permission on group is set to true
462 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
462 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
463 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
463 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
464 'can write into group validator')
464 'can write into group validator')
465 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
465 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
466 'can write into group validator')
466 'can write into group validator')
467 forbidden = not (group_admin or (group_write and create_on_write))
467 forbidden = not (group_admin or (group_write and create_on_write))
468 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
468 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
469 gid = (old_data['repo_group'].get('group_id')
469 gid = (old_data['repo_group'].get('group_id')
470 if (old_data and 'repo_group' in old_data) else None)
470 if (old_data and 'repo_group' in old_data) else None)
471 value_changed = gid != value
471 value_changed = gid != value
472 new = not old_data
472 new = not old_data
473 # do check if we changed the value, there's a case that someone got
473 # do check if we changed the value, there's a case that someone got
474 # revoked write permissions to a repository, he still created, we
474 # revoked write permissions to a repository, he still created, we
475 # don't need to check permission if he didn't change the value of
475 # don't need to check permission if he didn't change the value of
476 # groups in form box
476 # groups in form box
477 if value_changed or new:
477 if value_changed or new:
478 # parent group need to be existing
478 # parent group need to be existing
479 if gr and forbidden:
479 if gr and forbidden:
480 msg = self.message('permission_denied', state)
480 msg = self.message('permission_denied', state)
481 raise formencode.Invalid(msg, value, state,
481 raise formencode.Invalid(msg, value, state,
482 error_dict=dict(repo_type=msg)
482 error_dict=dict(repo_type=msg)
483 )
483 )
484 ## check if we can write to root location !
484 ## check if we can write to root location !
485 elif gr is None and not can_create_repos():
485 elif gr is None and not can_create_repos():
486 msg = self.message('permission_denied_root', state)
486 msg = self.message('permission_denied_root', state)
487 raise formencode.Invalid(msg, value, state,
487 raise formencode.Invalid(msg, value, state,
488 error_dict=dict(repo_type=msg)
488 error_dict=dict(repo_type=msg)
489 )
489 )
490
490
491 return _validator
491 return _validator
492
492
493
493
494 def CanCreateGroup(can_create_in_root=False):
494 def CanCreateGroup(can_create_in_root=False):
495 class _validator(formencode.validators.FancyValidator):
495 class _validator(formencode.validators.FancyValidator):
496 messages = {
496 messages = {
497 'permission_denied': _("You don't have permissions "
497 'permission_denied': _("You don't have permissions "
498 "to create a group in this location")
498 "to create a group in this location")
499 }
499 }
500
500
501 def to_python(self, value, state):
501 def to_python(self, value, state):
502 # root location
502 # root location
503 if value == -1:
503 if value == -1:
504 return None
504 return None
505 return value
505 return value
506
506
507 def _validate_python(self, value, state):
507 def _validate_python(self, value, state):
508 gr = RepoGroup.get(value)
508 gr = RepoGroup.get(value)
509 gr_name = gr.group_name if gr is not None else None # None means ROOT location
509 gr_name = gr.group_name if gr is not None else None # None means ROOT location
510
510
511 if can_create_in_root and gr is None:
511 if can_create_in_root and gr is None:
512 # we can create in root, we're fine no validations required
512 # we can create in root, we're fine no validations required
513 return
513 return
514
514
515 forbidden_in_root = gr is None and not can_create_in_root
515 forbidden_in_root = gr is None and not can_create_in_root
516 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
516 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
517 if forbidden_in_root or forbidden:
517 if forbidden_in_root or forbidden:
518 msg = self.message('permission_denied', state)
518 msg = self.message('permission_denied', state)
519 raise formencode.Invalid(msg, value, state,
519 raise formencode.Invalid(msg, value, state,
520 error_dict=dict(parent_group_id=msg)
520 error_dict=dict(parent_group_id=msg)
521 )
521 )
522
522
523 return _validator
523 return _validator
524
524
525
525
526 def ValidPerms(type_='repo'):
526 def ValidPerms(type_='repo'):
527 if type_ == 'repo_group':
527 if type_ == 'repo_group':
528 EMPTY_PERM = 'group.none'
528 EMPTY_PERM = 'group.none'
529 elif type_ == 'repo':
529 elif type_ == 'repo':
530 EMPTY_PERM = 'repository.none'
530 EMPTY_PERM = 'repository.none'
531 elif type_ == 'user_group':
531 elif type_ == 'user_group':
532 EMPTY_PERM = 'usergroup.none'
532 EMPTY_PERM = 'usergroup.none'
533
533
534 class _validator(formencode.validators.FancyValidator):
534 class _validator(formencode.validators.FancyValidator):
535 messages = {
535 messages = {
536 'perm_new_member_name':
536 'perm_new_member_name':
537 _('This username or user group name is not valid')
537 _('This username or user group name is not valid')
538 }
538 }
539
539
540 def to_python(self, value, state):
540 def to_python(self, value, state):
541 perms_update = OrderedSet()
541 perms_update = OrderedSet()
542 perms_new = OrderedSet()
542 perms_new = OrderedSet()
543 # build a list of permission to update and new permission to create
543 # build a list of permission to update and new permission to create
544
544
545 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
545 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
546 new_perms_group = defaultdict(dict)
546 new_perms_group = defaultdict(dict)
547 for k, v in value.copy().items():
547 for k, v in value.copy().items():
548 if k.startswith('perm_new_member'):
548 if k.startswith('perm_new_member'):
549 del value[k]
549 del value[k]
550 _type, part = k.split('perm_new_member_')
550 _type, part = k.split('perm_new_member_')
551 args = part.split('_')
551 args = part.split('_')
552 if len(args) == 1:
552 if len(args) == 1:
553 new_perms_group[args[0]]['perm'] = v
553 new_perms_group[args[0]]['perm'] = v
554 elif len(args) == 2:
554 elif len(args) == 2:
555 _key, pos = args
555 _key, pos = args
556 new_perms_group[pos][_key] = v
556 new_perms_group[pos][_key] = v
557
557
558 # fill new permissions in order of how they were added
558 # fill new permissions in order of how they were added
559 for k in sorted(new_perms_group, key=lambda k: int(k)):
559 for k in sorted(new_perms_group, key=lambda k: int(k)):
560 perm_dict = new_perms_group[k]
560 perm_dict = new_perms_group[k]
561 new_member = perm_dict.get('name')
561 new_member = perm_dict.get('name')
562 new_perm = perm_dict.get('perm')
562 new_perm = perm_dict.get('perm')
563 new_type = perm_dict.get('type')
563 new_type = perm_dict.get('type')
564 if new_member and new_perm and new_type:
564 if new_member and new_perm and new_type:
565 perms_new.add((new_member, new_perm, new_type))
565 perms_new.add((new_member, new_perm, new_type))
566
566
567 for k, v in value.items():
567 for k, v in value.items():
568 if k.startswith('u_perm_') or k.startswith('g_perm_'):
568 if k.startswith('u_perm_') or k.startswith('g_perm_'):
569 member = k[7:]
569 member = k[7:]
570 t = {'u': 'user',
570 t = {'u': 'user',
571 'g': 'users_group'
571 'g': 'users_group'
572 }[k[0]]
572 }[k[0]]
573 if member == User.DEFAULT_USER:
573 if member == User.DEFAULT_USER:
574 if str2bool(value.get('repo_private')):
574 if str2bool(value.get('repo_private')):
575 # set none for default when updating to
575 # set none for default when updating to
576 # private repo protects against form manipulation
576 # private repo protects against form manipulation
577 v = EMPTY_PERM
577 v = EMPTY_PERM
578 perms_update.add((member, v, t))
578 perms_update.add((member, v, t))
579
579
580 value['perms_updates'] = list(perms_update)
580 value['perms_updates'] = list(perms_update)
581 value['perms_new'] = list(perms_new)
581 value['perms_new'] = list(perms_new)
582
582
583 # update permissions
583 # update permissions
584 for k, v, t in perms_new:
584 for k, v, t in perms_new:
585 try:
585 try:
586 if t == 'user':
586 if t == 'user':
587 self.user_db = User.query() \
587 _user_db = User.query() \
588 .filter(User.active == True) \
588 .filter(User.active == True) \
589 .filter(User.username == k).one()
589 .filter(User.username == k).one()
590 if t == 'users_group':
590 if t == 'users_group':
591 self.user_db = UserGroup.query() \
591 _user_db = UserGroup.query() \
592 .filter(UserGroup.users_group_active == True) \
592 .filter(UserGroup.users_group_active == True) \
593 .filter(UserGroup.users_group_name == k).one()
593 .filter(UserGroup.users_group_name == k).one()
594
594
595 except Exception:
595 except Exception:
596 log.exception('Updated permission failed')
596 log.exception('Updated permission failed')
597 msg = self.message('perm_new_member_type', state)
597 msg = self.message('perm_new_member_type', state)
598 raise formencode.Invalid(msg, value, state,
598 raise formencode.Invalid(msg, value, state,
599 error_dict=dict(perm_new_member_name=msg)
599 error_dict=dict(perm_new_member_name=msg)
600 )
600 )
601 return value
601 return value
602 return _validator
602 return _validator
603
603
604
604
605 def ValidSettings():
605 def ValidSettings():
606 class _validator(formencode.validators.FancyValidator):
606 class _validator(formencode.validators.FancyValidator):
607 def _convert_to_python(self, value, state):
607 def _convert_to_python(self, value, state):
608 # settings form for users that are not admin
608 # settings form for users that are not admin
609 # can't edit certain parameters, it's extra backup if they mangle
609 # can't edit certain parameters, it's extra backup if they mangle
610 # with forms
610 # with forms
611
611
612 forbidden_params = [
612 forbidden_params = [
613 'user', 'repo_type',
613 'user', 'repo_type',
614 'repo_enable_downloads', 'repo_enable_statistics'
614 'repo_enable_downloads', 'repo_enable_statistics'
615 ]
615 ]
616
616
617 for param in forbidden_params:
617 for param in forbidden_params:
618 if param in value:
618 if param in value:
619 del value[param]
619 del value[param]
620 return value
620 return value
621
621
622 def _validate_python(self, value, state):
622 def _validate_python(self, value, state):
623 pass
623 pass
624 return _validator
624 return _validator
625
625
626
626
627 def ValidPath():
627 def ValidPath():
628 class _validator(formencode.validators.FancyValidator):
628 class _validator(formencode.validators.FancyValidator):
629 messages = {
629 messages = {
630 'invalid_path': _('This is not a valid path')
630 'invalid_path': _('This is not a valid path')
631 }
631 }
632
632
633 def _validate_python(self, value, state):
633 def _validate_python(self, value, state):
634 if not os.path.isdir(value):
634 if not os.path.isdir(value):
635 msg = self.message('invalid_path', state)
635 msg = self.message('invalid_path', state)
636 raise formencode.Invalid(msg, value, state,
636 raise formencode.Invalid(msg, value, state,
637 error_dict=dict(paths_root_path=msg)
637 error_dict=dict(paths_root_path=msg)
638 )
638 )
639 return _validator
639 return _validator
640
640
641
641
642 def UniqSystemEmail(old_data=None):
642 def UniqSystemEmail(old_data=None):
643 old_data = old_data or {}
643 old_data = old_data or {}
644
644
645 class _validator(formencode.validators.FancyValidator):
645 class _validator(formencode.validators.FancyValidator):
646 messages = {
646 messages = {
647 'email_taken': _('This email address is already in use')
647 'email_taken': _('This email address is already in use')
648 }
648 }
649
649
650 def _convert_to_python(self, value, state):
650 def _convert_to_python(self, value, state):
651 return value.lower()
651 return value.lower()
652
652
653 def _validate_python(self, value, state):
653 def _validate_python(self, value, state):
654 if (old_data.get('email') or '').lower() != value:
654 if (old_data.get('email') or '').lower() != value:
655 user = User.get_by_email(value)
655 user = User.get_by_email(value)
656 if user is not None:
656 if user is not None:
657 msg = self.message('email_taken', state)
657 msg = self.message('email_taken', state)
658 raise formencode.Invalid(msg, value, state,
658 raise formencode.Invalid(msg, value, state,
659 error_dict=dict(email=msg)
659 error_dict=dict(email=msg)
660 )
660 )
661 return _validator
661 return _validator
662
662
663
663
664 def ValidSystemEmail():
664 def ValidSystemEmail():
665 class _validator(formencode.validators.FancyValidator):
665 class _validator(formencode.validators.FancyValidator):
666 messages = {
666 messages = {
667 'non_existing_email': _('Email address "%(email)s" not found')
667 'non_existing_email': _('Email address "%(email)s" not found')
668 }
668 }
669
669
670 def _convert_to_python(self, value, state):
670 def _convert_to_python(self, value, state):
671 return value.lower()
671 return value.lower()
672
672
673 def _validate_python(self, value, state):
673 def _validate_python(self, value, state):
674 user = User.get_by_email(value)
674 user = User.get_by_email(value)
675 if user is None:
675 if user is None:
676 msg = self.message('non_existing_email', state, email=value)
676 msg = self.message('non_existing_email', state, email=value)
677 raise formencode.Invalid(msg, value, state,
677 raise formencode.Invalid(msg, value, state,
678 error_dict=dict(email=msg)
678 error_dict=dict(email=msg)
679 )
679 )
680
680
681 return _validator
681 return _validator
682
682
683
683
684 def LdapLibValidator():
684 def LdapLibValidator():
685 class _validator(formencode.validators.FancyValidator):
685 class _validator(formencode.validators.FancyValidator):
686 messages = {
686 messages = {
687
687
688 }
688 }
689
689
690 def _validate_python(self, value, state):
690 def _validate_python(self, value, state):
691 try:
691 try:
692 import ldap
692 import ldap
693 ldap # pyflakes silence !
693 ldap # pyflakes silence !
694 except ImportError:
694 except ImportError:
695 raise LdapImportError()
695 raise LdapImportError()
696
696
697 return _validator
697 return _validator
698
698
699
699
700 def AttrLoginValidator():
700 def AttrLoginValidator():
701 class _validator(formencode.validators.UnicodeString):
701 class _validator(formencode.validators.UnicodeString):
702 messages = {
702 messages = {
703 'invalid_cn':
703 'invalid_cn':
704 _('The LDAP Login attribute of the CN must be specified - '
704 _('The LDAP Login attribute of the CN must be specified - '
705 'this is the name of the attribute that is equivalent '
705 'this is the name of the attribute that is equivalent '
706 'to "username"')
706 'to "username"')
707 }
707 }
708 messages['empty'] = messages['invalid_cn']
708 messages['empty'] = messages['invalid_cn']
709
709
710 return _validator
710 return _validator
711
711
712
712
713 def ValidIp():
713 def ValidIp():
714 class _validator(CIDR):
714 class _validator(CIDR):
715 messages = dict(
715 messages = dict(
716 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
716 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
717 illegalBits=_('The network size (bits) must be within the range'
717 illegalBits=_('The network size (bits) must be within the range'
718 ' of 0-32 (not %(bits)r)')
718 ' of 0-32 (not %(bits)r)')
719 )
719 )
720
720
721 def to_python(self, value, state):
721 def to_python(self, value, state):
722 v = super(_validator, self).to_python(value, state)
722 v = super(_validator, self).to_python(value, state)
723 v = v.strip()
723 v = v.strip()
724 net = ipaddr.IPNetwork(address=v)
724 net = ipaddr.IPNetwork(address=v)
725 if isinstance(net, ipaddr.IPv4Network):
725 if isinstance(net, ipaddr.IPv4Network):
726 # if IPv4 doesn't end with a mask, add /32
726 # if IPv4 doesn't end with a mask, add /32
727 if '/' not in value:
727 if '/' not in value:
728 v += '/32'
728 v += '/32'
729 if isinstance(net, ipaddr.IPv6Network):
729 if isinstance(net, ipaddr.IPv6Network):
730 # if IPv6 doesn't end with a mask, add /128
730 # if IPv6 doesn't end with a mask, add /128
731 if '/' not in value:
731 if '/' not in value:
732 v += '/128'
732 v += '/128'
733 return v
733 return v
734
734
735 def _validate_python(self, value, state):
735 def _validate_python(self, value, state):
736 try:
736 try:
737 addr = value.strip()
737 addr = value.strip()
738 # this raises an ValueError if address is not IPv4 or IPv6
738 # this raises an ValueError if address is not IPv4 or IPv6
739 ipaddr.IPNetwork(address=addr)
739 ipaddr.IPNetwork(address=addr)
740 except ValueError:
740 except ValueError:
741 raise formencode.Invalid(self.message('badFormat', state),
741 raise formencode.Invalid(self.message('badFormat', state),
742 value, state)
742 value, state)
743
743
744 return _validator
744 return _validator
745
745
746
746
747 def FieldKey():
747 def FieldKey():
748 class _validator(formencode.validators.FancyValidator):
748 class _validator(formencode.validators.FancyValidator):
749 messages = dict(
749 messages = dict(
750 badFormat=_('Key name can only consist of letters, '
750 badFormat=_('Key name can only consist of letters, '
751 'underscore, dash or numbers')
751 'underscore, dash or numbers')
752 )
752 )
753
753
754 def _validate_python(self, value, state):
754 def _validate_python(self, value, state):
755 if not re.match('[a-zA-Z0-9_-]+$', value):
755 if not re.match('[a-zA-Z0-9_-]+$', value):
756 raise formencode.Invalid(self.message('badFormat', state),
756 raise formencode.Invalid(self.message('badFormat', state),
757 value, state)
757 value, state)
758 return _validator
758 return _validator
759
759
760
760
761 def BasePath():
761 def BasePath():
762 class _validator(formencode.validators.FancyValidator):
762 class _validator(formencode.validators.FancyValidator):
763 messages = dict(
763 messages = dict(
764 badPath=_('Filename cannot be inside a directory')
764 badPath=_('Filename cannot be inside a directory')
765 )
765 )
766
766
767 def _convert_to_python(self, value, state):
767 def _convert_to_python(self, value, state):
768 return value
768 return value
769
769
770 def _validate_python(self, value, state):
770 def _validate_python(self, value, state):
771 if value != os.path.basename(value):
771 if value != os.path.basename(value):
772 raise formencode.Invalid(self.message('badPath', state),
772 raise formencode.Invalid(self.message('badPath', state),
773 value, state)
773 value, state)
774 return _validator
774 return _validator
775
775
776
776
777 def ValidAuthPlugins():
777 def ValidAuthPlugins():
778 class _validator(formencode.validators.FancyValidator):
778 class _validator(formencode.validators.FancyValidator):
779 messages = dict(
779 messages = dict(
780 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
780 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
781 )
781 )
782
782
783 def _convert_to_python(self, value, state):
783 def _convert_to_python(self, value, state):
784 # filter empty values
784 # filter empty values
785 return [s for s in value if s not in [None, '']]
785 return [s for s in value if s not in [None, '']]
786
786
787 def _validate_python(self, value, state):
787 def _validate_python(self, value, state):
788 from kallithea.lib import auth_modules
788 from kallithea.lib import auth_modules
789 module_list = value
789 module_list = value
790 unique_names = {}
790 unique_names = {}
791 try:
791 try:
792 for module in module_list:
792 for module in module_list:
793 plugin = auth_modules.loadplugin(module)
793 plugin = auth_modules.loadplugin(module)
794 plugin_name = plugin.name
794 plugin_name = plugin.name
795 if plugin_name in unique_names:
795 if plugin_name in unique_names:
796 msg = self.message('import_duplicate', state,
796 msg = self.message('import_duplicate', state,
797 loaded=unique_names[plugin_name],
797 loaded=unique_names[plugin_name],
798 next_to_load=plugin_name)
798 next_to_load=plugin_name)
799 raise formencode.Invalid(msg, value, state)
799 raise formencode.Invalid(msg, value, state)
800 unique_names[plugin_name] = plugin
800 unique_names[plugin_name] = plugin
801 except (ImportError, AttributeError, TypeError) as e:
801 except (ImportError, AttributeError, TypeError) as e:
802 raise formencode.Invalid(str(e), value, state)
802 raise formencode.Invalid(str(e), value, state)
803
803
804 return _validator
804 return _validator
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now