##// END OF EJS Templates
follow Python conventions for boolean values...
Mads Kiilerich -
r3625:260a7a01 beta
parent child Browse files
Show More
@@ -1,173 +1,173
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.notifications
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 notifications controller for RhodeCode
7 7
8 8 :created_on: Nov 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import request
30 30 from pylons import tmpl_context as c, url
31 31 from pylons.controllers.util import redirect, abort
32 32
33 33 from webhelpers.paginate import Page
34 34
35 35 from rhodecode.lib.base import BaseController, render
36 36 from rhodecode.model.db import Notification
37 37
38 38 from rhodecode.model.notification import NotificationModel
39 39 from rhodecode.lib.auth import LoginRequired, NotAnonymous
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.model.meta import Session
42 42 from rhodecode.lib.utils2 import safe_int
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class NotificationsController(BaseController):
49 49 """REST Controller styled on the Atom Publishing Protocol"""
50 50 # To properly map this controller, ensure your config/routing.py
51 51 # file has a resource setup:
52 52 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 53 # path_prefix='/_admin', name_prefix='_admin_')
54 54
55 55 @LoginRequired()
56 56 @NotAnonymous()
57 57 def __before__(self):
58 58 super(NotificationsController, self).__before__()
59 59
60 60 def index(self, format='html'):
61 61 """GET /_admin/notifications: All items in the collection"""
62 62 # url('notifications')
63 63 c.user = self.rhodecode_user
64 64 notif = NotificationModel().get_for_user(self.rhodecode_user.user_id,
65 65 filter_=request.GET.getall('type'))
66 66
67 67 p = safe_int(request.params.get('page', 1), 1)
68 68 c.notifications = Page(notif, page=p, items_per_page=10)
69 69 c.pull_request_type = Notification.TYPE_PULL_REQUEST
70 70 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
71 71 Notification.TYPE_PULL_REQUEST_COMMENT]
72 72
73 73 _current_filter = request.GET.getall('type')
74 74 c.current_filter = 'all'
75 75 if _current_filter == [c.pull_request_type]:
76 76 c.current_filter = 'pull_request'
77 77 elif _current_filter == c.comment_type:
78 78 c.current_filter = 'comment'
79 79
80 80 return render('admin/notifications/notifications.html')
81 81
82 82 def mark_all_read(self):
83 83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
84 84 nm = NotificationModel()
85 85 # mark all read
86 86 nm.mark_all_read_for_user(self.rhodecode_user.user_id,
87 87 filter_=request.GET.getall('type'))
88 88 Session().commit()
89 89 c.user = self.rhodecode_user
90 90 notif = nm.get_for_user(self.rhodecode_user.user_id,
91 91 filter_=request.GET.getall('type'))
92 92 c.notifications = Page(notif, page=1, items_per_page=10)
93 93 return render('admin/notifications/notifications_data.html')
94 94
95 95 def create(self):
96 96 """POST /_admin/notifications: Create a new item"""
97 97 # url('notifications')
98 98
99 99 def new(self, format='html'):
100 100 """GET /_admin/notifications/new: Form to create a new item"""
101 101 # url('new_notification')
102 102
103 103 def update(self, notification_id):
104 104 """PUT /_admin/notifications/id: Update an existing item"""
105 105 # Forms posted to this method should contain a hidden field:
106 106 # <input type="hidden" name="_method" value="PUT" />
107 107 # Or using helpers:
108 108 # h.form(url('notification', notification_id=ID),
109 109 # method='put')
110 110 # url('notification', notification_id=ID)
111 111 try:
112 112 no = Notification.get(notification_id)
113 113 owner = all(un.user.user_id == c.rhodecode_user.user_id
114 114 for un in no.notifications_to_users)
115 115 if h.HasPermissionAny('hg.admin')() or owner:
116 116 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
117 117 Session().commit()
118 118 return 'ok'
119 119 except Exception:
120 120 Session().rollback()
121 121 log.error(traceback.format_exc())
122 122 return 'fail'
123 123
124 124 def delete(self, notification_id):
125 125 """DELETE /_admin/notifications/id: Delete an existing item"""
126 126 # Forms posted to this method should contain a hidden field:
127 127 # <input type="hidden" name="_method" value="DELETE" />
128 128 # Or using helpers:
129 129 # h.form(url('notification', notification_id=ID),
130 130 # method='delete')
131 131 # url('notification', notification_id=ID)
132 132
133 133 try:
134 134 no = Notification.get(notification_id)
135 135 owner = all(un.user.user_id == c.rhodecode_user.user_id
136 136 for un in no.notifications_to_users)
137 137 if h.HasPermissionAny('hg.admin')() or owner:
138 138 NotificationModel().delete(c.rhodecode_user.user_id, no)
139 139 Session().commit()
140 140 return 'ok'
141 141 except Exception:
142 142 Session().rollback()
143 143 log.error(traceback.format_exc())
144 144 return 'fail'
145 145
146 146 def show(self, notification_id, format='html'):
147 147 """GET /_admin/notifications/id: Show a specific item"""
148 148 # url('notification', notification_id=ID)
149 149 c.user = self.rhodecode_user
150 150 no = Notification.get(notification_id)
151 151
152 152 owner = any(un.user.user_id == c.rhodecode_user.user_id
153 153 for un in no.notifications_to_users)
154 154
155 155 if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner):
156 156 unotification = NotificationModel()\
157 157 .get_user_notification(c.user.user_id, no)
158 158
159 159 # if this association to user is not valid, we don't want to show
160 160 # this message
161 161 if unotification:
162 if unotification.read is False:
162 if not unotification.read:
163 163 unotification.mark_as_read()
164 164 Session().commit()
165 165 c.notification = no
166 166
167 167 return render('admin/notifications/show_notification.html')
168 168
169 169 return abort(403)
170 170
171 171 def edit(self, notification_id, format='html'):
172 172 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
173 173 # url('edit_notification', notification_id=ID)
@@ -1,392 +1,392
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repository groups controller for RhodeCode
7 7
8 8 :created_on: Mar 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import abort, redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 from sqlalchemy.exc import IntegrityError
37 37
38 38 import rhodecode
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
42 42 HasReposGroupPermissionAnyDecorator, HasReposGroupPermissionAll,\
43 43 HasPermissionAll
44 44 from rhodecode.lib.base import BaseController, render
45 45 from rhodecode.model.db import RepoGroup, Repository
46 46 from rhodecode.model.repos_group import ReposGroupModel
47 47 from rhodecode.model.forms import ReposGroupForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.repo import RepoModel
50 50 from webob.exc import HTTPInternalServerError, HTTPNotFound
51 51 from rhodecode.lib.utils2 import str2bool, safe_int
52 52 from sqlalchemy.sql.expression import func
53 53 from rhodecode.model.scm import GroupList
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class ReposGroupsController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60 # To properly map this controller, ensure your config/routing.py
61 61 # file has a resource setup:
62 62 # map.resource('repos_group', 'repos_groups')
63 63
64 64 @LoginRequired()
65 65 def __before__(self):
66 66 super(ReposGroupsController, self).__before__()
67 67
68 68 def __load_defaults(self, allow_empty_group=False, exclude_group_ids=[]):
69 69 if HasPermissionAll('hg.admin')('group edit'):
70 70 #we're global admin, we're ok and we can create TOP level groups
71 71 allow_empty_group = True
72 72
73 73 #override the choices for this form, we need to filter choices
74 74 #and display only those we have ADMIN right
75 75 groups_with_admin_rights = GroupList(RepoGroup.query().all(),
76 76 perm_set=['group.admin'])
77 77 c.repo_groups = RepoGroup.groups_choices(groups=groups_with_admin_rights,
78 78 show_empty_group=allow_empty_group)
79 79 # exclude filtered ids
80 80 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
81 81 c.repo_groups)
82 82 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
83 83 repo_model = RepoModel()
84 84 c.users_array = repo_model.get_users_js()
85 85 c.users_groups_array = repo_model.get_users_groups_js()
86 86
87 87 def __load_data(self, group_id):
88 88 """
89 89 Load defaults settings for edit, and update
90 90
91 91 :param group_id:
92 92 """
93 93 repo_group = RepoGroup.get_or_404(group_id)
94 94 data = repo_group.get_dict()
95 95 data['group_name'] = repo_group.name
96 96
97 97 # fill repository users
98 98 for p in repo_group.repo_group_to_perm:
99 99 data.update({'u_perm_%s' % p.user.username:
100 100 p.permission.permission_name})
101 101
102 102 # fill repository groups
103 103 for p in repo_group.users_group_to_perm:
104 104 data.update({'g_perm_%s' % p.users_group.users_group_name:
105 105 p.permission.permission_name})
106 106
107 107 return data
108 108
109 109 def _revoke_perms_on_yourself(self, form_result):
110 110 _up = filter(lambda u: c.rhodecode_user.username == u[0],
111 111 form_result['perms_updates'])
112 112 _new = filter(lambda u: c.rhodecode_user.username == u[0],
113 113 form_result['perms_new'])
114 114 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
115 115 return True
116 116 return False
117 117
118 118 def index(self, format='html'):
119 119 """GET /repos_groups: All items in the collection"""
120 120 # url('repos_groups')
121 121 group_iter = GroupList(RepoGroup.query().all(), perm_set=['group.admin'])
122 122 sk = lambda g: g.parents[0].group_name if g.parents else g.group_name
123 123 c.groups = sorted(group_iter, key=sk)
124 124 return render('admin/repos_groups/repos_groups_show.html')
125 125
126 126 def create(self):
127 127 """POST /repos_groups: Create a new item"""
128 128 # url('repos_groups')
129 129
130 130 self.__load_defaults()
131 131
132 132 # permissions for can create group based on parent_id are checked
133 133 # here in the Form
134 134 repos_group_form = ReposGroupForm(available_groups=
135 135 map(lambda k: unicode(k[0]), c.repo_groups))()
136 136 try:
137 137 form_result = repos_group_form.to_python(dict(request.POST))
138 138 ReposGroupModel().create(
139 139 group_name=form_result['group_name'],
140 140 group_description=form_result['group_description'],
141 141 parent=form_result['group_parent_id'],
142 142 owner=self.rhodecode_user.user_id
143 143 )
144 144 Session().commit()
145 145 h.flash(_('Created repos group %s') \
146 146 % form_result['group_name'], category='success')
147 147 #TODO: in futureaction_logger(, '', '', '', self.sa)
148 148 except formencode.Invalid, errors:
149 149 return htmlfill.render(
150 150 render('admin/repos_groups/repos_groups_add.html'),
151 151 defaults=errors.value,
152 152 errors=errors.error_dict or {},
153 153 prefix_error=False,
154 154 encoding="UTF-8")
155 155 except Exception:
156 156 log.error(traceback.format_exc())
157 157 h.flash(_('Error occurred during creation of repos group %s') \
158 158 % request.POST.get('group_name'), category='error')
159 159 parent_group_id = form_result['group_parent_id']
160 160 #TODO: maybe we should get back to the main view, not the admin one
161 161 return redirect(url('repos_groups', parent_group=parent_group_id))
162 162
163 163 def new(self, format='html'):
164 164 """GET /repos_groups/new: Form to create a new item"""
165 165 # url('new_repos_group')
166 166 if HasPermissionAll('hg.admin')('group create'):
167 167 #we're global admin, we're ok and we can create TOP level groups
168 168 pass
169 169 else:
170 170 # we pass in parent group into creation form, thus we know
171 171 # what would be the group, we can check perms here !
172 172 group_id = safe_int(request.GET.get('parent_group'))
173 173 group = RepoGroup.get(group_id) if group_id else None
174 174 group_name = group.group_name if group else None
175 175 if HasReposGroupPermissionAll('group.admin')(group_name, 'group create'):
176 176 pass
177 177 else:
178 178 return abort(403)
179 179
180 180 self.__load_defaults()
181 181 return render('admin/repos_groups/repos_groups_add.html')
182 182
183 183 @HasReposGroupPermissionAnyDecorator('group.admin')
184 184 def update(self, group_name):
185 185 """PUT /repos_groups/group_name: Update an existing item"""
186 186 # Forms posted to this method should contain a hidden field:
187 187 # <input type="hidden" name="_method" value="PUT" />
188 188 # Or using helpers:
189 189 # h.form(url('repos_group', group_name=GROUP_NAME),
190 190 # method='put')
191 191 # url('repos_group', group_name=GROUP_NAME)
192 192
193 193 c.repos_group = ReposGroupModel()._get_repos_group(group_name)
194 194 if HasPermissionAll('hg.admin')('group edit'):
195 195 #we're global admin, we're ok and we can create TOP level groups
196 196 allow_empty_group = True
197 197 elif not c.repos_group.parent_group:
198 198 allow_empty_group = True
199 199 else:
200 200 allow_empty_group = False
201 201 self.__load_defaults(allow_empty_group=allow_empty_group,
202 202 exclude_group_ids=[c.repos_group.group_id])
203 203
204 204 repos_group_form = ReposGroupForm(
205 205 edit=True,
206 206 old_data=c.repos_group.get_dict(),
207 207 available_groups=c.repo_groups_choices,
208 208 can_create_in_root=allow_empty_group,
209 209 )()
210 210 try:
211 211 form_result = repos_group_form.to_python(dict(request.POST))
212 212 if not c.rhodecode_user.is_admin:
213 213 if self._revoke_perms_on_yourself(form_result):
214 214 msg = _('Cannot revoke permission for yourself as admin')
215 215 h.flash(msg, category='warning')
216 216 raise Exception('revoke admin permission on self')
217 217
218 218 new_gr = ReposGroupModel().update(group_name, form_result)
219 219 Session().commit()
220 220 h.flash(_('Updated repos group %s') \
221 221 % form_result['group_name'], category='success')
222 222 # we now have new name !
223 223 group_name = new_gr.group_name
224 224 #TODO: in future action_logger(, '', '', '', self.sa)
225 225 except formencode.Invalid, errors:
226 226
227 227 return htmlfill.render(
228 228 render('admin/repos_groups/repos_groups_edit.html'),
229 229 defaults=errors.value,
230 230 errors=errors.error_dict or {},
231 231 prefix_error=False,
232 232 encoding="UTF-8")
233 233 except Exception:
234 234 log.error(traceback.format_exc())
235 235 h.flash(_('Error occurred during update of repos group %s') \
236 236 % request.POST.get('group_name'), category='error')
237 237
238 238 return redirect(url('edit_repos_group', group_name=group_name))
239 239
240 240 @HasReposGroupPermissionAnyDecorator('group.admin')
241 241 def delete(self, group_name):
242 242 """DELETE /repos_groups/group_name: Delete an existing item"""
243 243 # Forms posted to this method should contain a hidden field:
244 244 # <input type="hidden" name="_method" value="DELETE" />
245 245 # Or using helpers:
246 246 # h.form(url('repos_group', group_name=GROUP_NAME),
247 247 # method='delete')
248 248 # url('repos_group', group_name=GROUP_NAME)
249 249
250 250 gr = c.repos_group = ReposGroupModel()._get_repos_group(group_name)
251 251 repos = gr.repositories.all()
252 252 if repos:
253 253 h.flash(_('This group contains %s repositores and cannot be '
254 254 'deleted') % len(repos), category='warning')
255 255 return redirect(url('repos_groups'))
256 256
257 257 children = gr.children.all()
258 258 if children:
259 259 h.flash(_('This group contains %s subgroups and cannot be deleted'
260 260 % (len(children))), category='warning')
261 261 return redirect(url('repos_groups'))
262 262
263 263 try:
264 264 ReposGroupModel().delete(group_name)
265 265 Session().commit()
266 266 h.flash(_('Removed repos group %s') % group_name,
267 267 category='success')
268 268 #TODO: in future action_logger(, '', '', '', self.sa)
269 269 except Exception:
270 270 log.error(traceback.format_exc())
271 271 h.flash(_('Error occurred during deletion of repos '
272 272 'group %s') % group_name, category='error')
273 273
274 274 return redirect(url('repos_groups'))
275 275
276 276 @HasReposGroupPermissionAnyDecorator('group.admin')
277 277 def delete_repos_group_user_perm(self, group_name):
278 278 """
279 279 DELETE an existing repository group permission user
280 280
281 281 :param group_name:
282 282 """
283 283 try:
284 284 if not c.rhodecode_user.is_admin:
285 285 if c.rhodecode_user.user_id == safe_int(request.POST['user_id']):
286 286 msg = _('Cannot revoke permission for yourself as admin')
287 287 h.flash(msg, category='warning')
288 288 raise Exception('revoke admin permission on self')
289 289 recursive = str2bool(request.POST.get('recursive', False))
290 290 ReposGroupModel().delete_permission(
291 291 repos_group=group_name, obj=request.POST['user_id'],
292 292 obj_type='user', recursive=recursive
293 293 )
294 294 Session().commit()
295 295 except Exception:
296 296 log.error(traceback.format_exc())
297 297 h.flash(_('An error occurred during deletion of group user'),
298 298 category='error')
299 299 raise HTTPInternalServerError()
300 300
301 301 @HasReposGroupPermissionAnyDecorator('group.admin')
302 302 def delete_repos_group_users_group_perm(self, group_name):
303 303 """
304 304 DELETE an existing repository group permission user group
305 305
306 306 :param group_name:
307 307 """
308 308
309 309 try:
310 310 recursive = str2bool(request.POST.get('recursive', False))
311 311 ReposGroupModel().delete_permission(
312 312 repos_group=group_name, obj=request.POST['users_group_id'],
313 313 obj_type='users_group', recursive=recursive
314 314 )
315 315 Session().commit()
316 316 except Exception:
317 317 log.error(traceback.format_exc())
318 318 h.flash(_('An error occurred during deletion of group'
319 319 ' user groups'),
320 320 category='error')
321 321 raise HTTPInternalServerError()
322 322
323 323 def show_by_name(self, group_name):
324 324 """
325 325 This is a proxy that does a lookup group_name -> id, and shows
326 326 the group by id view instead
327 327 """
328 328 group_name = group_name.rstrip('/')
329 329 id_ = RepoGroup.get_by_group_name(group_name)
330 330 if id_:
331 331 return self.show(id_.group_id)
332 332 raise HTTPNotFound
333 333
334 334 @HasReposGroupPermissionAnyDecorator('group.read', 'group.write',
335 335 'group.admin')
336 336 def show(self, group_name, format='html'):
337 337 """GET /repos_groups/group_name: Show a specific item"""
338 338 # url('repos_group', group_name=GROUP_NAME)
339 339
340 340 c.group = c.repos_group = ReposGroupModel()._get_repos_group(group_name)
341 341 c.group_repos = c.group.repositories.all()
342 342
343 343 #overwrite our cached list with current filter
344 344 gr_filter = c.group_repos
345 345 c.repo_cnt = 0
346 346
347 347 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
348 348 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
349 349 c.groups = self.scm_model.get_repos_groups(groups)
350 350
351 if c.visual.lightweight_dashboard is False:
351 if not c.visual.lightweight_dashboard:
352 352 c.repos_list = self.scm_model.get_repos(all_repos=gr_filter)
353 353 ## lightweight version of dashboard
354 354 else:
355 355 c.repos_list = Repository.query()\
356 356 .filter(Repository.group_id == c.group.group_id)\
357 357 .order_by(func.lower(Repository.repo_name))\
358 358 .all()
359 359
360 360 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
361 361 admin=False)
362 362 #json used to render the grid
363 363 c.data = json.dumps(repos_data)
364 364
365 365 return render('admin/repos_groups/repos_groups.html')
366 366
367 367 @HasReposGroupPermissionAnyDecorator('group.admin')
368 368 def edit(self, group_name, format='html'):
369 369 """GET /repos_groups/group_name/edit: Form to edit an existing item"""
370 370 # url('edit_repos_group', group_name=GROUP_NAME)
371 371
372 372 c.repos_group = ReposGroupModel()._get_repos_group(group_name)
373 373 #we can only allow moving empty group if it's already a top-level
374 374 #group, ie has no parents, or we're admin
375 375 if HasPermissionAll('hg.admin')('group edit'):
376 376 #we're global admin, we're ok and we can create TOP level groups
377 377 allow_empty_group = True
378 378 elif not c.repos_group.parent_group:
379 379 allow_empty_group = True
380 380 else:
381 381 allow_empty_group = False
382 382
383 383 self.__load_defaults(allow_empty_group=allow_empty_group,
384 384 exclude_group_ids=[c.repos_group.group_id])
385 385 defaults = self.__load_data(c.repos_group.group_id)
386 386
387 387 return htmlfill.render(
388 388 render('admin/repos_groups/repos_groups_edit.html'),
389 389 defaults=defaults,
390 390 encoding="UTF-8",
391 391 force_defaults=False
392 392 )
@@ -1,647 +1,647
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30 import shutil
31 31
32 32 from pylons import request, response, tmpl_context as c, url
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import redirect
35 35 from rhodecode.lib.utils import jsonify
36 36
37 37 from rhodecode.lib import diffs
38 38 from rhodecode.lib import helpers as h
39 39
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
42 42 str2bool
43 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 44 from rhodecode.lib.base import BaseRepoController, render
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.vcs.conf import settings
47 47 from rhodecode.lib.vcs.exceptions import RepositoryError, \
48 48 ChangesetDoesNotExistError, EmptyRepositoryError, \
49 49 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
50 50 NodeDoesNotExistError, ChangesetError, NodeError
51 51 from rhodecode.lib.vcs.nodes import FileNode
52 52
53 53 from rhodecode.model.repo import RepoModel
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
58 58 _context_url, get_line_ctx, get_ignore_ws
59 59 from webob.exc import HTTPNotFound
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 class FilesController(BaseRepoController):
66 66
67 67 def __before__(self):
68 68 super(FilesController, self).__before__()
69 69 c.cut_off_limit = self.cut_off_limit
70 70
71 71 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
72 72 """
73 73 Safe way to get changeset if error occur it redirects to tip with
74 74 proper message
75 75
76 76 :param rev: revision to fetch
77 77 :param repo_name: repo name to redirect after
78 78 """
79 79
80 80 try:
81 81 return c.rhodecode_repo.get_changeset(rev)
82 82 except EmptyRepositoryError, e:
83 83 if not redirect_after:
84 84 return None
85 85 url_ = url('files_add_home',
86 86 repo_name=c.repo_name,
87 87 revision=0, f_path='')
88 88 add_new = h.link_to(_('click here to add new file'), url_)
89 89 h.flash(h.literal(_('There are no files yet %s') % add_new),
90 90 category='warning')
91 91 redirect(h.url('summary_home', repo_name=repo_name))
92 92
93 93 except RepositoryError, e: # including ChangesetDoesNotExistError
94 94 h.flash(str(e), category='error')
95 95 raise HTTPNotFound()
96 96
97 97 def __get_filenode_or_redirect(self, repo_name, cs, path):
98 98 """
99 99 Returns file_node, if error occurs or given path is directory,
100 100 it'll redirect to top level path
101 101
102 102 :param repo_name: repo_name
103 103 :param cs: given changeset
104 104 :param path: path to lookup
105 105 """
106 106
107 107 try:
108 108 file_node = cs.get_node(path)
109 109 if file_node.is_dir():
110 110 raise RepositoryError('given path is a directory')
111 111 except RepositoryError, e:
112 112 h.flash(str(e), category='error')
113 113 raise HTTPNotFound()
114 114
115 115 return file_node
116 116
117 117 @LoginRequired()
118 118 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
119 119 'repository.admin')
120 120 def index(self, repo_name, revision, f_path, annotate=False):
121 121 # redirect to given revision from form if given
122 122 post_revision = request.POST.get('at_rev', None)
123 123 if post_revision:
124 124 cs = self.__get_cs_or_redirect(post_revision, repo_name)
125 125
126 126 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
127 127 c.branch = request.GET.get('branch', None)
128 128 c.f_path = f_path
129 129 c.annotate = annotate
130 130 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
131 131 cur_rev = c.changeset.revision
132 132
133 133 # prev link
134 134 try:
135 135 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 136 c.url_prev = url('files_home', repo_name=c.repo_name,
137 137 revision=prev_rev.raw_id, f_path=f_path)
138 138 if c.branch:
139 139 c.url_prev += '?branch=%s' % c.branch
140 140 except (ChangesetDoesNotExistError, VCSError):
141 141 c.url_prev = '#'
142 142
143 143 # next link
144 144 try:
145 145 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 146 c.url_next = url('files_home', repo_name=c.repo_name,
147 147 revision=next_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_next += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_next = '#'
152 152
153 153 # files or dirs
154 154 try:
155 155 c.file = c.changeset.get_node(f_path)
156 156
157 157 if c.file.is_file():
158 158 c.load_full_history = False
159 159 file_last_cs = c.file.last_changeset
160 160 c.file_changeset = (c.changeset
161 161 if c.changeset.revision < file_last_cs.revision
162 162 else file_last_cs)
163 163 #determine if we're on branch head
164 164 _branches = c.rhodecode_repo.branches
165 165 c.on_branch_head = revision in _branches.keys() + _branches.values()
166 166 _hist = []
167 167 c.file_history = []
168 168 if c.load_full_history:
169 169 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
170 170
171 171 c.authors = []
172 172 for a in set([x.author for x in _hist]):
173 173 c.authors.append((h.email(a), h.person(a)))
174 174 else:
175 175 c.authors = c.file_history = []
176 176 except RepositoryError, e:
177 177 h.flash(str(e), category='error')
178 178 raise HTTPNotFound()
179 179
180 180 if request.environ.get('HTTP_X_PARTIAL_XHR'):
181 181 return render('files/files_ypjax.html')
182 182
183 183 return render('files/files.html')
184 184
185 185 def history(self, repo_name, revision, f_path, annotate=False):
186 186 if request.environ.get('HTTP_X_PARTIAL_XHR'):
187 187 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
188 188 c.f_path = f_path
189 189 c.annotate = annotate
190 190 c.file = c.changeset.get_node(f_path)
191 191 if c.file.is_file():
192 192 file_last_cs = c.file.last_changeset
193 193 c.file_changeset = (c.changeset
194 194 if c.changeset.revision < file_last_cs.revision
195 195 else file_last_cs)
196 196 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
197 197 c.authors = []
198 198 for a in set([x.author for x in _hist]):
199 199 c.authors.append((h.email(a), h.person(a)))
200 200 return render('files/files_history_box.html')
201 201
202 202 @LoginRequired()
203 203 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
204 204 'repository.admin')
205 205 def rawfile(self, repo_name, revision, f_path):
206 206 cs = self.__get_cs_or_redirect(revision, repo_name)
207 207 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
208 208
209 209 response.content_disposition = 'attachment; filename=%s' % \
210 210 safe_str(f_path.split(Repository.url_sep())[-1])
211 211
212 212 response.content_type = file_node.mimetype
213 213 return file_node.content
214 214
215 215 @LoginRequired()
216 216 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
217 217 'repository.admin')
218 218 def raw(self, repo_name, revision, f_path):
219 219 cs = self.__get_cs_or_redirect(revision, repo_name)
220 220 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
221 221
222 222 raw_mimetype_mapping = {
223 223 # map original mimetype to a mimetype used for "show as raw"
224 224 # you can also provide a content-disposition to override the
225 225 # default "attachment" disposition.
226 226 # orig_type: (new_type, new_dispo)
227 227
228 228 # show images inline:
229 229 'image/x-icon': ('image/x-icon', 'inline'),
230 230 'image/png': ('image/png', 'inline'),
231 231 'image/gif': ('image/gif', 'inline'),
232 232 'image/jpeg': ('image/jpeg', 'inline'),
233 233 'image/svg+xml': ('image/svg+xml', 'inline'),
234 234 }
235 235
236 236 mimetype = file_node.mimetype
237 237 try:
238 238 mimetype, dispo = raw_mimetype_mapping[mimetype]
239 239 except KeyError:
240 240 # we don't know anything special about this, handle it safely
241 241 if file_node.is_binary:
242 242 # do same as download raw for binary files
243 243 mimetype, dispo = 'application/octet-stream', 'attachment'
244 244 else:
245 245 # do not just use the original mimetype, but force text/plain,
246 246 # otherwise it would serve text/html and that might be unsafe.
247 247 # Note: underlying vcs library fakes text/plain mimetype if the
248 248 # mimetype can not be determined and it thinks it is not
249 249 # binary.This might lead to erroneous text display in some
250 250 # cases, but helps in other cases, like with text files
251 251 # without extension.
252 252 mimetype, dispo = 'text/plain', 'inline'
253 253
254 254 if dispo == 'attachment':
255 255 dispo = 'attachment; filename=%s' % \
256 256 safe_str(f_path.split(os.sep)[-1])
257 257
258 258 response.content_disposition = dispo
259 259 response.content_type = mimetype
260 260 return file_node.content
261 261
262 262 @LoginRequired()
263 263 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
264 264 def edit(self, repo_name, revision, f_path):
265 265 repo = c.rhodecode_db_repo
266 266 if repo.enable_locking and repo.locked[0]:
267 267 h.flash(_('This repository is has been locked by %s on %s')
268 268 % (h.person_by_id(repo.locked[0]),
269 269 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
270 270 'warning')
271 271 return redirect(h.url('files_home',
272 272 repo_name=repo_name, revision='tip'))
273 273
274 274 # check if revision is a branch identifier- basically we cannot
275 275 # create multiple heads via file editing
276 276 _branches = repo.scm_instance.branches
277 277 # check if revision is a branch name or branch hash
278 278 if revision not in _branches.keys() + _branches.values():
279 279 h.flash(_('You can only edit files with revision '
280 280 'being a valid branch '), category='warning')
281 281 return redirect(h.url('files_home',
282 282 repo_name=repo_name, revision='tip',
283 283 f_path=f_path))
284 284
285 285 r_post = request.POST
286 286
287 287 c.cs = self.__get_cs_or_redirect(revision, repo_name)
288 288 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
289 289
290 290 if c.file.is_binary:
291 291 return redirect(url('files_home', repo_name=c.repo_name,
292 292 revision=c.cs.raw_id, f_path=f_path))
293 293 c.default_message = _('Edited file %s via RhodeCode') % (f_path)
294 294 c.f_path = f_path
295 295
296 296 if r_post:
297 297
298 298 old_content = c.file.content
299 299 sl = old_content.splitlines(1)
300 300 first_line = sl[0] if sl else ''
301 301 # modes: 0 - Unix, 1 - Mac, 2 - DOS
302 302 mode = detect_mode(first_line, 0)
303 303 content = convert_line_endings(r_post.get('content'), mode)
304 304
305 305 message = r_post.get('message') or c.default_message
306 306 author = self.rhodecode_user.full_contact
307 307
308 308 if content == old_content:
309 309 h.flash(_('No changes'), category='warning')
310 310 return redirect(url('changeset_home', repo_name=c.repo_name,
311 311 revision='tip'))
312 312 try:
313 313 self.scm_model.commit_change(repo=c.rhodecode_repo,
314 314 repo_name=repo_name, cs=c.cs,
315 315 user=self.rhodecode_user.user_id,
316 316 author=author, message=message,
317 317 content=content, f_path=f_path)
318 318 h.flash(_('Successfully committed to %s') % f_path,
319 319 category='success')
320 320
321 321 except Exception:
322 322 log.error(traceback.format_exc())
323 323 h.flash(_('Error occurred during commit'), category='error')
324 324 return redirect(url('changeset_home',
325 325 repo_name=c.repo_name, revision='tip'))
326 326
327 327 return render('files/files_edit.html')
328 328
329 329 @LoginRequired()
330 330 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
331 331 def add(self, repo_name, revision, f_path):
332 332
333 333 repo = Repository.get_by_repo_name(repo_name)
334 334 if repo.enable_locking and repo.locked[0]:
335 335 h.flash(_('This repository is has been locked by %s on %s')
336 336 % (h.person_by_id(repo.locked[0]),
337 337 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
338 338 'warning')
339 339 return redirect(h.url('files_home',
340 340 repo_name=repo_name, revision='tip'))
341 341
342 342 r_post = request.POST
343 343 c.cs = self.__get_cs_or_redirect(revision, repo_name,
344 344 redirect_after=False)
345 345 if c.cs is None:
346 346 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
347 347 c.default_message = (_('Added file via RhodeCode'))
348 348 c.f_path = f_path
349 349
350 350 if r_post:
351 351 unix_mode = 0
352 352 content = convert_line_endings(r_post.get('content'), unix_mode)
353 353
354 354 message = r_post.get('message') or c.default_message
355 355 filename = r_post.get('filename')
356 356 location = r_post.get('location')
357 357 file_obj = r_post.get('upload_file', None)
358 358
359 359 if file_obj is not None and hasattr(file_obj, 'filename'):
360 360 filename = file_obj.filename
361 361 content = file_obj.file
362 362
363 363 if not content:
364 364 h.flash(_('No content'), category='warning')
365 365 return redirect(url('changeset_home', repo_name=c.repo_name,
366 366 revision='tip'))
367 367 if not filename:
368 368 h.flash(_('No filename'), category='warning')
369 369 return redirect(url('changeset_home', repo_name=c.repo_name,
370 370 revision='tip'))
371 371 if location.startswith('/') or location.startswith('.') or '../' in location:
372 372 h.flash(_('location must be relative path and must not '
373 373 'contain .. in path'), category='warning')
374 374 return redirect(url('changeset_home', repo_name=c.repo_name,
375 375 revision='tip'))
376 376 if location:
377 377 location = os.path.normpath(location)
378 378 filename = os.path.basename(filename)
379 379 node_path = os.path.join(location, filename)
380 380 author = self.rhodecode_user.full_contact
381 381
382 382 try:
383 383 self.scm_model.create_node(repo=c.rhodecode_repo,
384 384 repo_name=repo_name, cs=c.cs,
385 385 user=self.rhodecode_user.user_id,
386 386 author=author, message=message,
387 387 content=content, f_path=node_path)
388 388 h.flash(_('Successfully committed to %s') % node_path,
389 389 category='success')
390 390 except (NodeError, NodeAlreadyExistsError), e:
391 391 h.flash(_(e), category='error')
392 392 except Exception:
393 393 log.error(traceback.format_exc())
394 394 h.flash(_('Error occurred during commit'), category='error')
395 395 return redirect(url('changeset_home',
396 396 repo_name=c.repo_name, revision='tip'))
397 397
398 398 return render('files/files_add.html')
399 399
400 400 @LoginRequired()
401 401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 402 'repository.admin')
403 403 def archivefile(self, repo_name, fname):
404 404
405 405 fileformat = None
406 406 revision = None
407 407 ext = None
408 408 subrepos = request.GET.get('subrepos') == 'true'
409 409
410 410 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
411 411 archive_spec = fname.split(ext_data[1])
412 412 if len(archive_spec) == 2 and archive_spec[1] == '':
413 413 fileformat = a_type or ext_data[1]
414 414 revision = archive_spec[0]
415 415 ext = ext_data[1]
416 416
417 417 try:
418 418 dbrepo = RepoModel().get_by_repo_name(repo_name)
419 if dbrepo.enable_downloads is False:
419 if not dbrepo.enable_downloads:
420 420 return _('downloads disabled')
421 421
422 422 if c.rhodecode_repo.alias == 'hg':
423 423 # patch and reset hooks section of UI config to not run any
424 424 # hooks on fetching archives with subrepos
425 425 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
426 426 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
427 427
428 428 cs = c.rhodecode_repo.get_changeset(revision)
429 429 content_type = settings.ARCHIVE_SPECS[fileformat][0]
430 430 except ChangesetDoesNotExistError:
431 431 return _('Unknown revision %s') % revision
432 432 except EmptyRepositoryError:
433 433 return _('Empty repository')
434 434 except (ImproperArchiveTypeError, KeyError):
435 435 return _('Unknown archive type')
436 436 # archive cache
437 437 from rhodecode import CONFIG
438 438 rev_name = cs.raw_id[:12]
439 439 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
440 440 safe_str(rev_name), ext)
441 441
442 442 use_cached_archive = False # defines if we use cached version of archive
443 443 archive_cache_enabled = CONFIG.get('archive_cache_dir')
444 444 if not subrepos and archive_cache_enabled:
445 445 #check if we it's ok to write
446 446 if not os.path.isdir(CONFIG['archive_cache_dir']):
447 447 os.makedirs(CONFIG['archive_cache_dir'])
448 448 cached_archive_path = os.path.join(CONFIG['archive_cache_dir'], archive_name)
449 449 if os.path.isfile(cached_archive_path):
450 450 log.debug('Found cached archive in %s' % cached_archive_path)
451 451 fd, archive = None, cached_archive_path
452 452 use_cached_archive = True
453 453 else:
454 454 log.debug('Archive %s is not yet cached' % (archive_name))
455 455
456 456 if not use_cached_archive:
457 457 #generate new archive
458 458 try:
459 459 fd, archive = tempfile.mkstemp()
460 460 t = open(archive, 'wb')
461 461 log.debug('Creating new temp archive in %s' % archive)
462 462 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
463 463 if archive_cache_enabled:
464 464 #if we generated the archive and use cache rename that
465 465 log.debug('Storing new archive in %s' % cached_archive_path)
466 466 shutil.move(archive, cached_archive_path)
467 467 archive = cached_archive_path
468 468 finally:
469 469 t.close()
470 470
471 471 def get_chunked_archive(archive):
472 472 stream = open(archive, 'rb')
473 473 while True:
474 474 data = stream.read(16 * 1024)
475 475 if not data:
476 476 stream.close()
477 477 if fd: # fd means we used temporary file
478 478 os.close(fd)
479 479 if not archive_cache_enabled:
480 480 log.debug('Destroing temp archive %s' % archive)
481 481 os.remove(archive)
482 482 break
483 483 yield data
484 484
485 485 response.content_disposition = str('attachment; filename=%s' % (archive_name))
486 486 response.content_type = str(content_type)
487 487 return get_chunked_archive(archive)
488 488
489 489 @LoginRequired()
490 490 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
491 491 'repository.admin')
492 492 def diff(self, repo_name, f_path):
493 493 ignore_whitespace = request.GET.get('ignorews') == '1'
494 494 line_context = request.GET.get('context', 3)
495 495 diff1 = request.GET.get('diff1', '')
496 496 diff2 = request.GET.get('diff2', '')
497 497 c.action = request.GET.get('diff')
498 498 c.no_changes = diff1 == diff2
499 499 c.f_path = f_path
500 500 c.big_diff = False
501 501 c.anchor_url = anchor_url
502 502 c.ignorews_url = _ignorews_url
503 503 c.context_url = _context_url
504 504 c.changes = OrderedDict()
505 505 c.changes[diff2] = []
506 506
507 507 #special case if we want a show rev only, it's impl here
508 508 #to reduce JS and callbacks
509 509
510 510 if request.GET.get('show_rev'):
511 511 if str2bool(request.GET.get('annotate', 'False')):
512 512 _url = url('files_annotate_home', repo_name=c.repo_name,
513 513 revision=diff1, f_path=c.f_path)
514 514 else:
515 515 _url = url('files_home', repo_name=c.repo_name,
516 516 revision=diff1, f_path=c.f_path)
517 517
518 518 return redirect(_url)
519 519 try:
520 520 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
521 521 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
522 522 try:
523 523 node1 = c.changeset_1.get_node(f_path)
524 524 if node1.is_dir():
525 525 raise NodeError('%s path is a %s not a file'
526 526 % (node1, type(node1)))
527 527 except NodeDoesNotExistError:
528 528 c.changeset_1 = EmptyChangeset(cs=diff1,
529 529 revision=c.changeset_1.revision,
530 530 repo=c.rhodecode_repo)
531 531 node1 = FileNode(f_path, '', changeset=c.changeset_1)
532 532 else:
533 533 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
534 534 node1 = FileNode(f_path, '', changeset=c.changeset_1)
535 535
536 536 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
537 537 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
538 538 try:
539 539 node2 = c.changeset_2.get_node(f_path)
540 540 if node2.is_dir():
541 541 raise NodeError('%s path is a %s not a file'
542 542 % (node2, type(node2)))
543 543 except NodeDoesNotExistError:
544 544 c.changeset_2 = EmptyChangeset(cs=diff2,
545 545 revision=c.changeset_2.revision,
546 546 repo=c.rhodecode_repo)
547 547 node2 = FileNode(f_path, '', changeset=c.changeset_2)
548 548 else:
549 549 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
550 550 node2 = FileNode(f_path, '', changeset=c.changeset_2)
551 551 except (RepositoryError, NodeError):
552 552 log.error(traceback.format_exc())
553 553 return redirect(url('files_home', repo_name=c.repo_name,
554 554 f_path=f_path))
555 555
556 556 if c.action == 'download':
557 557 _diff = diffs.get_gitdiff(node1, node2,
558 558 ignore_whitespace=ignore_whitespace,
559 559 context=line_context)
560 560 diff = diffs.DiffProcessor(_diff, format='gitdiff')
561 561
562 562 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
563 563 response.content_type = 'text/plain'
564 564 response.content_disposition = (
565 565 'attachment; filename=%s' % diff_name
566 566 )
567 567 return diff.as_raw()
568 568
569 569 elif c.action == 'raw':
570 570 _diff = diffs.get_gitdiff(node1, node2,
571 571 ignore_whitespace=ignore_whitespace,
572 572 context=line_context)
573 573 diff = diffs.DiffProcessor(_diff, format='gitdiff')
574 574 response.content_type = 'text/plain'
575 575 return diff.as_raw()
576 576
577 577 else:
578 578 fid = h.FID(diff2, node2.path)
579 579 line_context_lcl = get_line_ctx(fid, request.GET)
580 580 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
581 581
582 582 lim = request.GET.get('fulldiff') or self.cut_off_limit
583 583 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
584 584 filenode_new=node2,
585 585 cut_off_limit=lim,
586 586 ignore_whitespace=ign_whitespace_lcl,
587 587 line_context=line_context_lcl,
588 588 enable_comments=False)
589 589 op = ''
590 590 filename = node1.path
591 591 cs_changes = {
592 592 'fid': [cs1, cs2, op, filename, diff, st]
593 593 }
594 594 c.changes = cs_changes
595 595
596 596 return render('files/file_diff.html')
597 597
598 598 def _get_node_history(self, cs, f_path, changesets=None):
599 599 """
600 600 get changesets history for given node
601 601
602 602 :param cs: changeset to calculate history
603 603 :param f_path: path for node to calculate history for
604 604 :param changesets: if passed don't calculate history and take
605 605 changesets defined in this list
606 606 """
607 607 # calculate history based on tip
608 608 tip_cs = c.rhodecode_repo.get_changeset()
609 609 if changesets is None:
610 610 try:
611 611 changesets = tip_cs.get_file_history(f_path)
612 612 except (NodeDoesNotExistError, ChangesetError):
613 613 #this node is not present at tip !
614 614 changesets = cs.get_file_history(f_path)
615 615 hist_l = []
616 616
617 617 changesets_group = ([], _("Changesets"))
618 618 branches_group = ([], _("Branches"))
619 619 tags_group = ([], _("Tags"))
620 620 _hg = cs.repository.alias == 'hg'
621 621 for chs in changesets:
622 622 #_branch = '(%s)' % chs.branch if _hg else ''
623 623 _branch = chs.branch
624 624 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
625 625 changesets_group[0].append((chs.raw_id, n_desc,))
626 626 hist_l.append(changesets_group)
627 627
628 628 for name, chs in c.rhodecode_repo.branches.items():
629 629 branches_group[0].append((chs, name),)
630 630 hist_l.append(branches_group)
631 631
632 632 for name, chs in c.rhodecode_repo.tags.items():
633 633 tags_group[0].append((chs, name),)
634 634 hist_l.append(tags_group)
635 635
636 636 return hist_l, changesets
637 637
638 638 @LoginRequired()
639 639 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
640 640 'repository.admin')
641 641 @jsonify
642 642 def nodelist(self, repo_name, revision, f_path):
643 643 if request.environ.get('HTTP_X_PARTIAL_XHR'):
644 644 cs = self.__get_cs_or_redirect(revision, repo_name)
645 645 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
646 646 flat=False)
647 647 return {'nodes': _d + _f}
@@ -1,87 +1,87
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.home
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Home controller for Rhodecode
7 7
8 8 :created_on: Feb 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c, request
29 29 from pylons.i18n.translation import _
30 30 from webob.exc import HTTPBadRequest
31 31 from sqlalchemy.sql.expression import func
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.db import Repository
39 39 from rhodecode.model.repo import RepoModel
40 40
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class HomeController(BaseController):
46 46
47 47 @LoginRequired()
48 48 def __before__(self):
49 49 super(HomeController, self).__before__()
50 50
51 51 def index(self):
52 52 c.groups = self.scm_model.get_repos_groups()
53 53 c.group = None
54 54
55 if c.visual.lightweight_dashboard is False:
55 if not c.visual.lightweight_dashboard:
56 56 c.repos_list = self.scm_model.get_repos()
57 57 ## lightweight version of dashboard
58 58 else:
59 59 c.repos_list = Repository.query()\
60 60 .filter(Repository.group_id == None)\
61 61 .order_by(func.lower(Repository.repo_name))\
62 62 .all()
63 63
64 64 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
65 65 admin=False)
66 66 #json used to render the grid
67 67 c.data = json.dumps(repos_data)
68 68
69 69 return render('/index.html')
70 70
71 71 def repo_switcher(self):
72 72 if request.is_xhr:
73 73 all_repos = Repository.query().order_by(Repository.repo_name).all()
74 74 c.repos_list = self.scm_model.get_repos(all_repos,
75 75 sort_key='name_sort',
76 76 simple=True)
77 77 return render('/repo_switcher_list.html')
78 78 else:
79 79 raise HTTPBadRequest()
80 80
81 81 def branch_tag_switcher(self, repo_name):
82 82 if request.is_xhr:
83 83 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
84 84 if c.rhodecode_db_repo:
85 85 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
86 86 return render('/switch_to_list.html')
87 87 raise HTTPBadRequest()
@@ -1,191 +1,191
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.login
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Login controller for rhodeocode
7 7
8 8 :created_on: Apr 22, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import formencode
28 28 import datetime
29 29 import urlparse
30 30
31 31 from formencode import htmlfill
32 32 from webob.exc import HTTPFound
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons import request, response, session, tmpl_context as c, url
36 36
37 37 import rhodecode.lib.helpers as h
38 38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyDecorator
39 39 from rhodecode.lib.base import BaseController, render
40 40 from rhodecode.model.db import User
41 41 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
42 42 from rhodecode.model.user import UserModel
43 43 from rhodecode.model.meta import Session
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class LoginController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(LoginController, self).__before__()
53 53
54 54 def index(self):
55 55 # redirect if already logged in
56 56 c.came_from = request.GET.get('came_from')
57 57 not_default = self.rhodecode_user.username != 'default'
58 58 ip_allowed = self.rhodecode_user.ip_allowed
59 59 if self.rhodecode_user.is_authenticated and not_default and ip_allowed:
60 60 return redirect(url('home'))
61 61
62 62 if request.POST:
63 63 # import Login Form validator class
64 64 login_form = LoginForm()
65 65 try:
66 66 session.invalidate()
67 67 c.form_result = login_form.to_python(dict(request.POST))
68 68 # form checks for username/password, now we're authenticated
69 69 username = c.form_result['username']
70 70 user = User.get_by_username(username, case_insensitive=True)
71 71 auth_user = AuthUser(user.user_id)
72 72 auth_user.set_authenticated()
73 73 cs = auth_user.get_cookie_store()
74 74 session['rhodecode_user'] = cs
75 75 user.update_lastlogin()
76 76 Session().commit()
77 77
78 78 # If they want to be remembered, update the cookie
79 if c.form_result['remember'] is not False:
79 if c.form_result['remember']:
80 80 _year = (datetime.datetime.now() +
81 81 datetime.timedelta(seconds=60 * 60 * 24 * 365))
82 82 session._set_cookie_expires(_year)
83 83
84 84 session.save()
85 85
86 86 log.info('user %s is now authenticated and stored in '
87 87 'session, session attrs %s' % (username, cs))
88 88
89 89 # dumps session attrs back to cookie
90 90 session._update_cookie_out()
91 91
92 92 # we set new cookie
93 93 headers = None
94 94 if session.request['set_cookie']:
95 95 # send set-cookie headers back to response to update cookie
96 96 headers = [('Set-Cookie', session.request['cookie_out'])]
97 97
98 98 allowed_schemes = ['http', 'https']
99 99 if c.came_from:
100 100 parsed = urlparse.urlparse(c.came_from)
101 101 server_parsed = urlparse.urlparse(url.current())
102 102 if parsed.scheme and parsed.scheme not in allowed_schemes:
103 103 log.error(
104 104 'Suspicious URL scheme detected %s for url %s' %
105 105 (parsed.scheme, parsed))
106 106 c.came_from = url('home')
107 107 elif server_parsed.netloc != parsed.netloc:
108 108 log.error('Suspicious NETLOC detected %s for url %s'
109 109 'server url is: %s' %
110 110 (parsed.netloc, parsed, server_parsed))
111 111 c.came_from = url('home')
112 112 raise HTTPFound(location=c.came_from, headers=headers)
113 113 else:
114 114 raise HTTPFound(location=url('home'), headers=headers)
115 115
116 116 except formencode.Invalid, errors:
117 117 return htmlfill.render(
118 118 render('/login.html'),
119 119 defaults=errors.value,
120 120 errors=errors.error_dict or {},
121 121 prefix_error=False,
122 122 encoding="UTF-8")
123 123
124 124 return render('/login.html')
125 125
126 126 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
127 127 'hg.register.manual_activate')
128 128 def register(self):
129 129 c.auto_active = 'hg.register.auto_activate' in User.get_by_username('default')\
130 130 .AuthUser.permissions['global']
131 131
132 132 if request.POST:
133 133 register_form = RegisterForm()()
134 134 try:
135 135 form_result = register_form.to_python(dict(request.POST))
136 136 form_result['active'] = c.auto_active
137 137 UserModel().create_registration(form_result)
138 138 h.flash(_('You have successfully registered into RhodeCode'),
139 139 category='success')
140 140 Session().commit()
141 141 return redirect(url('login_home'))
142 142
143 143 except formencode.Invalid, errors:
144 144 return htmlfill.render(
145 145 render('/register.html'),
146 146 defaults=errors.value,
147 147 errors=errors.error_dict or {},
148 148 prefix_error=False,
149 149 encoding="UTF-8")
150 150
151 151 return render('/register.html')
152 152
153 153 def password_reset(self):
154 154 if request.POST:
155 155 password_reset_form = PasswordResetForm()()
156 156 try:
157 157 form_result = password_reset_form.to_python(dict(request.POST))
158 158 UserModel().reset_password_link(form_result)
159 159 h.flash(_('Your password reset link was sent'),
160 160 category='success')
161 161 return redirect(url('login_home'))
162 162
163 163 except formencode.Invalid, errors:
164 164 return htmlfill.render(
165 165 render('/password_reset.html'),
166 166 defaults=errors.value,
167 167 errors=errors.error_dict or {},
168 168 prefix_error=False,
169 169 encoding="UTF-8")
170 170
171 171 return render('/password_reset.html')
172 172
173 173 def password_reset_confirmation(self):
174 174 if request.GET and request.GET.get('key'):
175 175 try:
176 176 user = User.get_by_api_key(request.GET.get('key'))
177 177 data = dict(email=user.email)
178 178 UserModel().reset_password(data)
179 179 h.flash(_('Your password reset was successful, '
180 180 'new password has been sent to your email'),
181 181 category='success')
182 182 except Exception, e:
183 183 log.error(e)
184 184 return redirect(url('reset_password'))
185 185
186 186 return redirect(url('login_home'))
187 187
188 188 def logout(self):
189 189 session.delete()
190 190 log.info('Logging out and deleting session for user')
191 191 redirect(url('home'))
@@ -1,1029 +1,1029
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37 from sqlalchemy.orm.exc import ObjectDeletedError
38 38
39 39 from rhodecode import __platform__, is_windows, is_unix
40 40 from rhodecode.model.meta import Session
41 41
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode
43 43 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
44 44 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
45 45 from rhodecode.lib.auth_ldap import AuthLdap
46 46
47 47 from rhodecode.model import meta
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import Permission, RhodeCodeSetting, User, UserIpMap
50 50 from rhodecode.lib.caching_query import FromCache
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class PasswordGenerator(object):
56 56 """
57 57 This is a simple class for generating password from different sets of
58 58 characters
59 59 usage::
60 60
61 61 passwd_gen = PasswordGenerator()
62 62 #print 8-letter password containing only big and small letters
63 63 of alphabet
64 64 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
65 65 """
66 66 ALPHABETS_NUM = r'''1234567890'''
67 67 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
68 68 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
69 69 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
70 70 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
71 71 + ALPHABETS_NUM + ALPHABETS_SPECIAL
72 72 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
73 73 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
74 74 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
75 75 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
76 76
77 77 def __init__(self, passwd=''):
78 78 self.passwd = passwd
79 79
80 80 def gen_password(self, length, type_=None):
81 81 if type_ is None:
82 82 type_ = self.ALPHABETS_FULL
83 83 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
84 84 return self.passwd
85 85
86 86
87 87 class RhodeCodeCrypto(object):
88 88
89 89 @classmethod
90 90 def hash_string(cls, str_):
91 91 """
92 92 Cryptographic function used for password hashing based on pybcrypt
93 93 or pycrypto in windows
94 94
95 95 :param password: password to hash
96 96 """
97 97 if is_windows:
98 98 from hashlib import sha256
99 99 return sha256(str_).hexdigest()
100 100 elif is_unix:
101 101 import bcrypt
102 102 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
103 103 else:
104 104 raise Exception('Unknown or unsupported platform %s' \
105 105 % __platform__)
106 106
107 107 @classmethod
108 108 def hash_check(cls, password, hashed):
109 109 """
110 110 Checks matching password with it's hashed value, runs different
111 111 implementation based on platform it runs on
112 112
113 113 :param password: password
114 114 :param hashed: password in hashed form
115 115 """
116 116
117 117 if is_windows:
118 118 from hashlib import sha256
119 119 return sha256(password).hexdigest() == hashed
120 120 elif is_unix:
121 121 import bcrypt
122 122 return bcrypt.hashpw(password, hashed) == hashed
123 123 else:
124 124 raise Exception('Unknown or unsupported platform %s' \
125 125 % __platform__)
126 126
127 127
128 128 def get_crypt_password(password):
129 129 return RhodeCodeCrypto.hash_string(password)
130 130
131 131
132 132 def check_password(password, hashed):
133 133 return RhodeCodeCrypto.hash_check(password, hashed)
134 134
135 135
136 136 def generate_api_key(str_, salt=None):
137 137 """
138 138 Generates API KEY from given string
139 139
140 140 :param str_:
141 141 :param salt:
142 142 """
143 143
144 144 if salt is None:
145 145 salt = _RandomNameSequence().next()
146 146
147 147 return hashlib.sha1(str_ + salt).hexdigest()
148 148
149 149
150 150 def authfunc(environ, username, password):
151 151 """
152 152 Dummy authentication wrapper function used in Mercurial and Git for
153 153 access control.
154 154
155 155 :param environ: needed only for using in Basic auth
156 156 """
157 157 return authenticate(username, password)
158 158
159 159
160 160 def authenticate(username, password):
161 161 """
162 162 Authentication function used for access control,
163 163 firstly checks for db authentication then if ldap is enabled for ldap
164 164 authentication, also creates ldap user if not in database
165 165
166 166 :param username: username
167 167 :param password: password
168 168 """
169 169
170 170 user_model = UserModel()
171 171 user = User.get_by_username(username)
172 172
173 173 log.debug('Authenticating user using RhodeCode account')
174 174 if user is not None and not user.ldap_dn:
175 175 if user.active:
176 176 if user.username == 'default' and user.active:
177 177 log.info('user %s authenticated correctly as anonymous user' %
178 178 username)
179 179 return True
180 180
181 181 elif user.username == username and check_password(password,
182 182 user.password):
183 183 log.info('user %s authenticated correctly' % username)
184 184 return True
185 185 else:
186 186 log.warning('user %s tried auth but is disabled' % username)
187 187
188 188 else:
189 189 log.debug('Regular authentication failed')
190 190 user_obj = User.get_by_username(username, case_insensitive=True)
191 191
192 192 if user_obj is not None and not user_obj.ldap_dn:
193 193 log.debug('this user already exists as non ldap')
194 194 return False
195 195
196 196 ldap_settings = RhodeCodeSetting.get_ldap_settings()
197 197 #======================================================================
198 198 # FALLBACK TO LDAP AUTH IF ENABLE
199 199 #======================================================================
200 200 if str2bool(ldap_settings.get('ldap_active')):
201 201 log.debug("Authenticating user using ldap")
202 202 kwargs = {
203 203 'server': ldap_settings.get('ldap_host', ''),
204 204 'base_dn': ldap_settings.get('ldap_base_dn', ''),
205 205 'port': ldap_settings.get('ldap_port'),
206 206 'bind_dn': ldap_settings.get('ldap_dn_user'),
207 207 'bind_pass': ldap_settings.get('ldap_dn_pass'),
208 208 'tls_kind': ldap_settings.get('ldap_tls_kind'),
209 209 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
210 210 'ldap_filter': ldap_settings.get('ldap_filter'),
211 211 'search_scope': ldap_settings.get('ldap_search_scope'),
212 212 'attr_login': ldap_settings.get('ldap_attr_login'),
213 213 'ldap_version': 3,
214 214 }
215 215 log.debug('Checking for ldap authentication')
216 216 try:
217 217 aldap = AuthLdap(**kwargs)
218 218 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
219 219 password)
220 220 log.debug('Got ldap DN response %s' % user_dn)
221 221
222 222 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
223 223 .get(k), [''])[0]
224 224
225 225 user_attrs = {
226 226 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
227 227 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
228 228 'email': get_ldap_attr('ldap_attr_email'),
229 229 'active': 'hg.register.auto_activate' in User\
230 230 .get_by_username('default').AuthUser.permissions['global']
231 231 }
232 232
233 233 # don't store LDAP password since we don't need it. Override
234 234 # with some random generated password
235 235 _password = PasswordGenerator().gen_password(length=8)
236 236 # create this user on the fly if it doesn't exist in rhodecode
237 237 # database
238 238 if user_model.create_ldap(username, _password, user_dn,
239 239 user_attrs):
240 240 log.info('created new ldap user %s' % username)
241 241
242 242 Session().commit()
243 243 return True
244 244 except (LdapUsernameError, LdapPasswordError,):
245 245 pass
246 246 except (Exception,):
247 247 log.error(traceback.format_exc())
248 248 pass
249 249 return False
250 250
251 251
252 252 def login_container_auth(username):
253 253 user = User.get_by_username(username)
254 254 if user is None:
255 255 user_attrs = {
256 256 'name': username,
257 257 'lastname': None,
258 258 'email': None,
259 259 'active': 'hg.register.auto_activate' in User\
260 260 .get_by_username('default').AuthUser.permissions['global']
261 261 }
262 262 user = UserModel().create_for_container_auth(username, user_attrs)
263 263 if not user:
264 264 return None
265 265 log.info('User %s was created by container authentication' % username)
266 266
267 267 if not user.active:
268 268 return None
269 269
270 270 user.update_lastlogin()
271 271 Session().commit()
272 272
273 273 log.debug('User %s is now logged in by container authentication',
274 274 user.username)
275 275 return user
276 276
277 277
278 278 def get_container_username(environ, config, clean_username=False):
279 279 """
280 280 Get's the container_auth username (or email). It tries to get username
281 281 from REMOTE_USER if container_auth_enabled is enabled, if that fails
282 282 it tries to get username from HTTP_X_FORWARDED_USER if proxypass_auth_enabled
283 283 is enabled. clean_username extracts the username from this data if it's
284 284 having @ in it.
285 285
286 286 :param environ:
287 287 :param config:
288 288 :param clean_username:
289 289 """
290 290 username = None
291 291
292 292 if str2bool(config.get('container_auth_enabled', False)):
293 293 from paste.httpheaders import REMOTE_USER
294 294 username = REMOTE_USER(environ)
295 295 log.debug('extracted REMOTE_USER:%s' % (username))
296 296
297 297 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
298 298 username = environ.get('HTTP_X_FORWARDED_USER')
299 299 log.debug('extracted HTTP_X_FORWARDED_USER:%s' % (username))
300 300
301 301 if username and clean_username:
302 302 # Removing realm and domain from username
303 303 username = username.partition('@')[0]
304 304 username = username.rpartition('\\')[2]
305 305 log.debug('Received username %s from container' % username)
306 306
307 307 return username
308 308
309 309
310 310 class CookieStoreWrapper(object):
311 311
312 312 def __init__(self, cookie_store):
313 313 self.cookie_store = cookie_store
314 314
315 315 def __repr__(self):
316 316 return 'CookieStore<%s>' % (self.cookie_store)
317 317
318 318 def get(self, key, other=None):
319 319 if isinstance(self.cookie_store, dict):
320 320 return self.cookie_store.get(key, other)
321 321 elif isinstance(self.cookie_store, AuthUser):
322 322 return self.cookie_store.__dict__.get(key, other)
323 323
324 324
325 325 class AuthUser(object):
326 326 """
327 327 A simple object that handles all attributes of user in RhodeCode
328 328
329 329 It does lookup based on API key,given user, or user present in session
330 330 Then it fills all required information for such user. It also checks if
331 331 anonymous access is enabled and if so, it returns default user as logged
332 332 in
333 333 """
334 334
335 335 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
336 336
337 337 self.user_id = user_id
338 338 self.api_key = None
339 339 self.username = username
340 340 self.ip_addr = ip_addr
341 341
342 342 self.name = ''
343 343 self.lastname = ''
344 344 self.email = ''
345 345 self.is_authenticated = False
346 346 self.admin = False
347 347 self.inherit_default_permissions = False
348 348 self.permissions = {}
349 349 self._api_key = api_key
350 350 self.propagate_data()
351 351 self._instance = None
352 352
353 353 def propagate_data(self):
354 354 user_model = UserModel()
355 355 self.anonymous_user = User.get_by_username('default', cache=True)
356 356 is_user_loaded = False
357 357
358 358 # try go get user by api key
359 359 if self._api_key and self._api_key != self.anonymous_user.api_key:
360 360 log.debug('Auth User lookup by API KEY %s' % self._api_key)
361 361 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
362 362 # lookup by userid
363 363 elif (self.user_id is not None and
364 364 self.user_id != self.anonymous_user.user_id):
365 365 log.debug('Auth User lookup by USER ID %s' % self.user_id)
366 366 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
367 367 # lookup by username
368 368 elif self.username and \
369 369 str2bool(config.get('container_auth_enabled', False)):
370 370
371 371 log.debug('Auth User lookup by USER NAME %s' % self.username)
372 372 dbuser = login_container_auth(self.username)
373 373 if dbuser is not None:
374 374 log.debug('filling all attributes to object')
375 375 for k, v in dbuser.get_dict().items():
376 376 setattr(self, k, v)
377 377 self.set_authenticated()
378 378 is_user_loaded = True
379 379 else:
380 380 log.debug('No data in %s that could been used to log in' % self)
381 381
382 382 if not is_user_loaded:
383 383 # if we cannot authenticate user try anonymous
384 if self.anonymous_user.active is True:
384 if self.anonymous_user.active:
385 385 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
386 386 # then we set this user is logged in
387 387 self.is_authenticated = True
388 388 else:
389 389 self.user_id = None
390 390 self.username = None
391 391 self.is_authenticated = False
392 392
393 393 if not self.username:
394 394 self.username = 'None'
395 395
396 396 log.debug('Auth User is now %s' % self)
397 397 user_model.fill_perms(self)
398 398
399 399 @property
400 400 def is_admin(self):
401 401 return self.admin
402 402
403 403 @property
404 404 def repos_admin(self):
405 405 """
406 406 Returns list of repositories you're an admin of
407 407 """
408 408 return [x[0] for x in self.permissions['repositories'].iteritems()
409 409 if x[1] == 'repository.admin']
410 410
411 411 @property
412 412 def groups_admin(self):
413 413 """
414 414 Returns list of repository groups you're an admin of
415 415 """
416 416 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
417 417 if x[1] == 'group.admin']
418 418
419 419 @property
420 420 def ip_allowed(self):
421 421 """
422 422 Checks if ip_addr used in constructor is allowed from defined list of
423 423 allowed ip_addresses for user
424 424
425 425 :returns: boolean, True if ip is in allowed ip range
426 426 """
427 427 #check IP
428 428 allowed_ips = AuthUser.get_allowed_ips(self.user_id, cache=True)
429 429 if check_ip_access(source_ip=self.ip_addr, allowed_ips=allowed_ips):
430 430 log.debug('IP:%s is in range of %s' % (self.ip_addr, allowed_ips))
431 431 return True
432 432 else:
433 433 log.info('Access for IP:%s forbidden, '
434 434 'not in %s' % (self.ip_addr, allowed_ips))
435 435 return False
436 436
437 437 def __repr__(self):
438 438 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
439 439 self.is_authenticated)
440 440
441 441 def set_authenticated(self, authenticated=True):
442 442 if self.user_id != self.anonymous_user.user_id:
443 443 self.is_authenticated = authenticated
444 444
445 445 def get_cookie_store(self):
446 446 return {'username': self.username,
447 447 'user_id': self.user_id,
448 448 'is_authenticated': self.is_authenticated}
449 449
450 450 @classmethod
451 451 def from_cookie_store(cls, cookie_store):
452 452 """
453 453 Creates AuthUser from a cookie store
454 454
455 455 :param cls:
456 456 :param cookie_store:
457 457 """
458 458 user_id = cookie_store.get('user_id')
459 459 username = cookie_store.get('username')
460 460 api_key = cookie_store.get('api_key')
461 461 return AuthUser(user_id, api_key, username)
462 462
463 463 @classmethod
464 464 def get_allowed_ips(cls, user_id, cache=False):
465 465 _set = set()
466 466 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
467 467 if cache:
468 468 user_ips = user_ips.options(FromCache("sql_cache_short",
469 469 "get_user_ips_%s" % user_id))
470 470 for ip in user_ips:
471 471 try:
472 472 _set.add(ip.ip_addr)
473 473 except ObjectDeletedError:
474 474 # since we use heavy caching sometimes it happens that we get
475 475 # deleted objects here, we just skip them
476 476 pass
477 477 return _set or set(['0.0.0.0/0', '::/0'])
478 478
479 479
480 480 def set_available_permissions(config):
481 481 """
482 482 This function will propagate pylons globals with all available defined
483 483 permission given in db. We don't want to check each time from db for new
484 484 permissions since adding a new permission also requires application restart
485 485 ie. to decorate new views with the newly created permission
486 486
487 487 :param config: current pylons config instance
488 488
489 489 """
490 490 log.info('getting information about all available permissions')
491 491 try:
492 492 sa = meta.Session
493 493 all_perms = sa.query(Permission).all()
494 494 except Exception:
495 495 pass
496 496 finally:
497 497 meta.Session.remove()
498 498
499 499 config['available_permissions'] = [x.permission_name for x in all_perms]
500 500
501 501
502 502 #==============================================================================
503 503 # CHECK DECORATORS
504 504 #==============================================================================
505 505 class LoginRequired(object):
506 506 """
507 507 Must be logged in to execute this function else
508 508 redirect to login page
509 509
510 510 :param api_access: if enabled this checks only for valid auth token
511 511 and grants access based on valid token
512 512 """
513 513
514 514 def __init__(self, api_access=False):
515 515 self.api_access = api_access
516 516
517 517 def __call__(self, func):
518 518 return decorator(self.__wrapper, func)
519 519
520 520 def __wrapper(self, func, *fargs, **fkwargs):
521 521 cls = fargs[0]
522 522 user = cls.rhodecode_user
523 523 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
524 524
525 525 #check IP
526 526 ip_access_ok = True
527 527 if not user.ip_allowed:
528 528 from rhodecode.lib import helpers as h
529 529 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr))),
530 530 category='warning')
531 531 ip_access_ok = False
532 532
533 533 api_access_ok = False
534 534 if self.api_access:
535 535 log.debug('Checking API KEY access for %s' % cls)
536 536 if user.api_key == request.GET.get('api_key'):
537 537 api_access_ok = True
538 538 else:
539 539 log.debug("API KEY token not valid")
540 540
541 541 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
542 542 if (user.is_authenticated or api_access_ok) and ip_access_ok:
543 543 reason = 'RegularAuth' if user.is_authenticated else 'APIAuth'
544 544 log.info('user %s is authenticated and granted access to %s '
545 545 'using %s' % (user.username, loc, reason)
546 546 )
547 547 return func(*fargs, **fkwargs)
548 548 else:
549 549 log.warn('user %s NOT authenticated on func: %s' % (
550 550 user, loc)
551 551 )
552 552 p = url.current()
553 553
554 554 log.debug('redirecting to login page with %s' % p)
555 555 return redirect(url('login_home', came_from=p))
556 556
557 557
558 558 class NotAnonymous(object):
559 559 """
560 560 Must be logged in to execute this function else
561 561 redirect to login page"""
562 562
563 563 def __call__(self, func):
564 564 return decorator(self.__wrapper, func)
565 565
566 566 def __wrapper(self, func, *fargs, **fkwargs):
567 567 cls = fargs[0]
568 568 self.user = cls.rhodecode_user
569 569
570 570 log.debug('Checking if user is not anonymous @%s' % cls)
571 571
572 572 anonymous = self.user.username == 'default'
573 573
574 574 if anonymous:
575 575 p = url.current()
576 576
577 577 import rhodecode.lib.helpers as h
578 578 h.flash(_('You need to be a registered user to '
579 579 'perform this action'),
580 580 category='warning')
581 581 return redirect(url('login_home', came_from=p))
582 582 else:
583 583 return func(*fargs, **fkwargs)
584 584
585 585
586 586 class PermsDecorator(object):
587 587 """Base class for controller decorators"""
588 588
589 589 def __init__(self, *required_perms):
590 590 available_perms = config['available_permissions']
591 591 for perm in required_perms:
592 592 if perm not in available_perms:
593 593 raise Exception("'%s' permission is not defined" % perm)
594 594 self.required_perms = set(required_perms)
595 595 self.user_perms = None
596 596
597 597 def __call__(self, func):
598 598 return decorator(self.__wrapper, func)
599 599
600 600 def __wrapper(self, func, *fargs, **fkwargs):
601 601 cls = fargs[0]
602 602 self.user = cls.rhodecode_user
603 603 self.user_perms = self.user.permissions
604 604 log.debug('checking %s permissions %s for %s %s',
605 605 self.__class__.__name__, self.required_perms, cls, self.user)
606 606
607 607 if self.check_permissions():
608 608 log.debug('Permission granted for %s %s' % (cls, self.user))
609 609 return func(*fargs, **fkwargs)
610 610
611 611 else:
612 612 log.debug('Permission denied for %s %s' % (cls, self.user))
613 613 anonymous = self.user.username == 'default'
614 614
615 615 if anonymous:
616 616 p = url.current()
617 617
618 618 import rhodecode.lib.helpers as h
619 619 h.flash(_('You need to be a signed in to '
620 620 'view this page'),
621 621 category='warning')
622 622 return redirect(url('login_home', came_from=p))
623 623
624 624 else:
625 625 # redirect with forbidden ret code
626 626 return abort(403)
627 627
628 628 def check_permissions(self):
629 629 """Dummy function for overriding"""
630 630 raise Exception('You have to write this function in child class')
631 631
632 632
633 633 class HasPermissionAllDecorator(PermsDecorator):
634 634 """
635 635 Checks for access permission for all given predicates. All of them
636 636 have to be meet in order to fulfill the request
637 637 """
638 638
639 639 def check_permissions(self):
640 640 if self.required_perms.issubset(self.user_perms.get('global')):
641 641 return True
642 642 return False
643 643
644 644
645 645 class HasPermissionAnyDecorator(PermsDecorator):
646 646 """
647 647 Checks for access permission for any of given predicates. In order to
648 648 fulfill the request any of predicates must be meet
649 649 """
650 650
651 651 def check_permissions(self):
652 652 if self.required_perms.intersection(self.user_perms.get('global')):
653 653 return True
654 654 return False
655 655
656 656
657 657 class HasRepoPermissionAllDecorator(PermsDecorator):
658 658 """
659 659 Checks for access permission for all given predicates for specific
660 660 repository. All of them have to be meet in order to fulfill the request
661 661 """
662 662
663 663 def check_permissions(self):
664 664 repo_name = get_repo_slug(request)
665 665 try:
666 666 user_perms = set([self.user_perms['repositories'][repo_name]])
667 667 except KeyError:
668 668 return False
669 669 if self.required_perms.issubset(user_perms):
670 670 return True
671 671 return False
672 672
673 673
674 674 class HasRepoPermissionAnyDecorator(PermsDecorator):
675 675 """
676 676 Checks for access permission for any of given predicates for specific
677 677 repository. In order to fulfill the request any of predicates must be meet
678 678 """
679 679
680 680 def check_permissions(self):
681 681 repo_name = get_repo_slug(request)
682 682 try:
683 683 user_perms = set([self.user_perms['repositories'][repo_name]])
684 684 except KeyError:
685 685 return False
686 686
687 687 if self.required_perms.intersection(user_perms):
688 688 return True
689 689 return False
690 690
691 691
692 692 class HasReposGroupPermissionAllDecorator(PermsDecorator):
693 693 """
694 694 Checks for access permission for all given predicates for specific
695 695 repository. All of them have to be meet in order to fulfill the request
696 696 """
697 697
698 698 def check_permissions(self):
699 699 group_name = get_repos_group_slug(request)
700 700 try:
701 701 user_perms = set([self.user_perms['repositories_groups'][group_name]])
702 702 except KeyError:
703 703 return False
704 704
705 705 if self.required_perms.issubset(user_perms):
706 706 return True
707 707 return False
708 708
709 709
710 710 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
711 711 """
712 712 Checks for access permission for any of given predicates for specific
713 713 repository. In order to fulfill the request any of predicates must be meet
714 714 """
715 715
716 716 def check_permissions(self):
717 717 group_name = get_repos_group_slug(request)
718 718 try:
719 719 user_perms = set([self.user_perms['repositories_groups'][group_name]])
720 720 except KeyError:
721 721 return False
722 722
723 723 if self.required_perms.intersection(user_perms):
724 724 return True
725 725 return False
726 726
727 727
728 728 #==============================================================================
729 729 # CHECK FUNCTIONS
730 730 #==============================================================================
731 731 class PermsFunction(object):
732 732 """Base function for other check functions"""
733 733
734 734 def __init__(self, *perms):
735 735 available_perms = config['available_permissions']
736 736
737 737 for perm in perms:
738 738 if perm not in available_perms:
739 739 raise Exception("'%s' permission is not defined" % perm)
740 740 self.required_perms = set(perms)
741 741 self.user_perms = None
742 742 self.repo_name = None
743 743 self.group_name = None
744 744
745 745 def __call__(self, check_location=''):
746 746 #TODO: put user as attribute here
747 747 user = request.user
748 748 cls_name = self.__class__.__name__
749 749 check_scope = {
750 750 'HasPermissionAll': '',
751 751 'HasPermissionAny': '',
752 752 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
753 753 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
754 754 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
755 755 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
756 756 }.get(cls_name, '?')
757 757 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
758 758 self.required_perms, user, check_scope,
759 759 check_location or 'unspecified location')
760 760 if not user:
761 761 log.debug('Empty request user')
762 762 return False
763 763 self.user_perms = user.permissions
764 764 if self.check_permissions():
765 765 log.debug('Permission to %s granted for user: %s @ %s', self.repo_name, user,
766 766 check_location or 'unspecified location')
767 767 return True
768 768
769 769 else:
770 770 log.debug('Permission to %s denied for user: %s @ %s', self.repo_name, user,
771 771 check_location or 'unspecified location')
772 772 return False
773 773
774 774 def check_permissions(self):
775 775 """Dummy function for overriding"""
776 776 raise Exception('You have to write this function in child class')
777 777
778 778
779 779 class HasPermissionAll(PermsFunction):
780 780 def check_permissions(self):
781 781 if self.required_perms.issubset(self.user_perms.get('global')):
782 782 return True
783 783 return False
784 784
785 785
786 786 class HasPermissionAny(PermsFunction):
787 787 def check_permissions(self):
788 788 if self.required_perms.intersection(self.user_perms.get('global')):
789 789 return True
790 790 return False
791 791
792 792
793 793 class HasRepoPermissionAll(PermsFunction):
794 794 def __call__(self, repo_name=None, check_location=''):
795 795 self.repo_name = repo_name
796 796 return super(HasRepoPermissionAll, self).__call__(check_location)
797 797
798 798 def check_permissions(self):
799 799 if not self.repo_name:
800 800 self.repo_name = get_repo_slug(request)
801 801
802 802 try:
803 803 self._user_perms = set(
804 804 [self.user_perms['repositories'][self.repo_name]]
805 805 )
806 806 except KeyError:
807 807 return False
808 808 if self.required_perms.issubset(self._user_perms):
809 809 return True
810 810 return False
811 811
812 812
813 813 class HasRepoPermissionAny(PermsFunction):
814 814 def __call__(self, repo_name=None, check_location=''):
815 815 self.repo_name = repo_name
816 816 return super(HasRepoPermissionAny, self).__call__(check_location)
817 817
818 818 def check_permissions(self):
819 819 if not self.repo_name:
820 820 self.repo_name = get_repo_slug(request)
821 821
822 822 try:
823 823 self._user_perms = set(
824 824 [self.user_perms['repositories'][self.repo_name]]
825 825 )
826 826 except KeyError:
827 827 return False
828 828 if self.required_perms.intersection(self._user_perms):
829 829 return True
830 830 return False
831 831
832 832
833 833 class HasReposGroupPermissionAny(PermsFunction):
834 834 def __call__(self, group_name=None, check_location=''):
835 835 self.group_name = group_name
836 836 return super(HasReposGroupPermissionAny, self).__call__(check_location)
837 837
838 838 def check_permissions(self):
839 839 try:
840 840 self._user_perms = set(
841 841 [self.user_perms['repositories_groups'][self.group_name]]
842 842 )
843 843 except KeyError:
844 844 return False
845 845 if self.required_perms.intersection(self._user_perms):
846 846 return True
847 847 return False
848 848
849 849
850 850 class HasReposGroupPermissionAll(PermsFunction):
851 851 def __call__(self, group_name=None, check_location=''):
852 852 self.group_name = group_name
853 853 return super(HasReposGroupPermissionAll, self).__call__(check_location)
854 854
855 855 def check_permissions(self):
856 856 try:
857 857 self._user_perms = set(
858 858 [self.user_perms['repositories_groups'][self.group_name]]
859 859 )
860 860 except KeyError:
861 861 return False
862 862 if self.required_perms.issubset(self._user_perms):
863 863 return True
864 864 return False
865 865
866 866
867 867 #==============================================================================
868 868 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
869 869 #==============================================================================
870 870 class HasPermissionAnyMiddleware(object):
871 871 def __init__(self, *perms):
872 872 self.required_perms = set(perms)
873 873
874 874 def __call__(self, user, repo_name):
875 875 # repo_name MUST be unicode, since we handle keys in permission
876 876 # dict by unicode
877 877 repo_name = safe_unicode(repo_name)
878 878 usr = AuthUser(user.user_id)
879 879 try:
880 880 self.user_perms = set([usr.permissions['repositories'][repo_name]])
881 881 except Exception:
882 882 log.error('Exception while accessing permissions %s' %
883 883 traceback.format_exc())
884 884 self.user_perms = set()
885 885 self.username = user.username
886 886 self.repo_name = repo_name
887 887 return self.check_permissions()
888 888
889 889 def check_permissions(self):
890 890 log.debug('checking VCS protocol '
891 891 'permissions %s for user:%s repository:%s', self.user_perms,
892 892 self.username, self.repo_name)
893 893 if self.required_perms.intersection(self.user_perms):
894 894 log.debug('permission granted for user:%s on repo:%s' % (
895 895 self.username, self.repo_name
896 896 )
897 897 )
898 898 return True
899 899 log.debug('permission denied for user:%s on repo:%s' % (
900 900 self.username, self.repo_name
901 901 )
902 902 )
903 903 return False
904 904
905 905
906 906 #==============================================================================
907 907 # SPECIAL VERSION TO HANDLE API AUTH
908 908 #==============================================================================
909 909 class _BaseApiPerm(object):
910 910 def __init__(self, *perms):
911 911 self.required_perms = set(perms)
912 912
913 913 def __call__(self, check_location='unspecified', user=None, repo_name=None):
914 914 cls_name = self.__class__.__name__
915 915 check_scope = 'user:%s, repo:%s' % (user, repo_name)
916 916 log.debug('checking cls:%s %s %s @ %s', cls_name,
917 917 self.required_perms, check_scope, check_location)
918 918 if not user:
919 919 log.debug('Empty User passed into arguments')
920 920 return False
921 921
922 922 ## process user
923 923 if not isinstance(user, AuthUser):
924 924 user = AuthUser(user.user_id)
925 925
926 926 if self.check_permissions(user.permissions, repo_name):
927 927 log.debug('Permission to %s granted for user: %s @ %s', repo_name,
928 928 user, check_location)
929 929 return True
930 930
931 931 else:
932 932 log.debug('Permission to %s denied for user: %s @ %s', repo_name,
933 933 user, check_location)
934 934 return False
935 935
936 936 def check_permissions(self, perm_defs, repo_name):
937 937 """
938 938 implement in child class should return True if permissions are ok,
939 939 False otherwise
940 940
941 941 :param perm_defs: dict with permission definitions
942 942 :param repo_name: repo name
943 943 """
944 944 raise NotImplementedError()
945 945
946 946
947 947 class HasPermissionAllApi(_BaseApiPerm):
948 948 def __call__(self, user, check_location=''):
949 949 return super(HasPermissionAllApi, self)\
950 950 .__call__(check_location=check_location, user=user)
951 951
952 952 def check_permissions(self, perm_defs, repo):
953 953 if self.required_perms.issubset(perm_defs.get('global')):
954 954 return True
955 955 return False
956 956
957 957
958 958 class HasPermissionAnyApi(_BaseApiPerm):
959 959 def __call__(self, user, check_location=''):
960 960 return super(HasPermissionAnyApi, self)\
961 961 .__call__(check_location=check_location, user=user)
962 962
963 963 def check_permissions(self, perm_defs, repo):
964 964 if self.required_perms.intersection(perm_defs.get('global')):
965 965 return True
966 966 return False
967 967
968 968
969 969 class HasRepoPermissionAllApi(_BaseApiPerm):
970 970 def __call__(self, user, repo_name, check_location=''):
971 971 return super(HasRepoPermissionAllApi, self)\
972 972 .__call__(check_location=check_location, user=user,
973 973 repo_name=repo_name)
974 974
975 975 def check_permissions(self, perm_defs, repo_name):
976 976
977 977 try:
978 978 self._user_perms = set(
979 979 [perm_defs['repositories'][repo_name]]
980 980 )
981 981 except KeyError:
982 982 log.warning(traceback.format_exc())
983 983 return False
984 984 if self.required_perms.issubset(self._user_perms):
985 985 return True
986 986 return False
987 987
988 988
989 989 class HasRepoPermissionAnyApi(_BaseApiPerm):
990 990 def __call__(self, user, repo_name, check_location=''):
991 991 return super(HasRepoPermissionAnyApi, self)\
992 992 .__call__(check_location=check_location, user=user,
993 993 repo_name=repo_name)
994 994
995 995 def check_permissions(self, perm_defs, repo_name):
996 996
997 997 try:
998 998 _user_perms = set(
999 999 [perm_defs['repositories'][repo_name]]
1000 1000 )
1001 1001 except KeyError:
1002 1002 log.warning(traceback.format_exc())
1003 1003 return False
1004 1004 if self.required_perms.intersection(_user_perms):
1005 1005 return True
1006 1006 return False
1007 1007
1008 1008
1009 1009 def check_ip_access(source_ip, allowed_ips=None):
1010 1010 """
1011 1011 Checks if source_ip is a subnet of any of allowed_ips.
1012 1012
1013 1013 :param source_ip:
1014 1014 :param allowed_ips: list of allowed ips together with mask
1015 1015 """
1016 1016 from rhodecode.lib import ipaddr
1017 1017 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1018 1018 if isinstance(allowed_ips, (tuple, list, set)):
1019 1019 for ip in allowed_ips:
1020 1020 try:
1021 1021 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1022 1022 return True
1023 1023 # for any case we cannot determine the IP, don't crash just
1024 1024 # skip it and log as error, we want to say forbidden still when
1025 1025 # sending bad IP
1026 1026 except Exception:
1027 1027 log.error(traceback.format_exc())
1028 1028 continue
1029 1029 return False
@@ -1,130 +1,130
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 celery libs for RhodeCode
7 7
8 8 :created_on: Nov 27, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import socket
29 29 import traceback
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32 from pylons import config
33 33
34 34 from hashlib import md5
35 35 from decorator import decorator
36 36
37 37 from rhodecode.lib.vcs.utils.lazy import LazyProperty
38 38 from rhodecode import CELERY_ON, CELERY_EAGER
39 39 from rhodecode.lib.utils2 import str2bool, safe_str
40 40 from rhodecode.lib.pidlock import DaemonLock, LockHeld
41 41 from rhodecode.model import init_model
42 42 from rhodecode.model import meta
43 43 from rhodecode.model.db import Statistics, Repository, User
44 44
45 45 from sqlalchemy import engine_from_config
46 46
47 47 from celery.messaging import establish_connection
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ResultWrapper(object):
53 53 def __init__(self, task):
54 54 self.task = task
55 55
56 56 @LazyProperty
57 57 def result(self):
58 58 return self.task
59 59
60 60
61 61 def run_task(task, *args, **kwargs):
62 62 global CELERY_ON
63 63 if CELERY_ON:
64 64 try:
65 65 t = task.apply_async(args=args, kwargs=kwargs)
66 66 log.info('running task %s:%s' % (t.task_id, task))
67 67 return t
68 68
69 69 except socket.error, e:
70 70 if isinstance(e, IOError) and e.errno == 111:
71 71 log.debug('Unable to connect to celeryd. Sync execution')
72 72 CELERY_ON = False
73 73 else:
74 74 log.error(traceback.format_exc())
75 75 except KeyError, e:
76 76 log.debug('Unable to connect to celeryd. Sync execution')
77 77 except Exception, e:
78 78 log.error(traceback.format_exc())
79 79
80 80 log.debug('executing task %s in sync mode' % task)
81 81 return ResultWrapper(task(*args, **kwargs))
82 82
83 83
84 84 def __get_lockkey(func, *fargs, **fkwargs):
85 85 params = list(fargs)
86 86 params.extend(['%s-%s' % ar for ar in fkwargs.items()])
87 87
88 88 func_name = str(func.__name__) if hasattr(func, '__name__') else str(func)
89 89
90 90 lockkey = 'task_%s.lock' % \
91 91 md5(func_name + '-' + '-'.join(map(safe_str, params))).hexdigest()
92 92 return lockkey
93 93
94 94
95 95 def locked_task(func):
96 96 def __wrapper(func, *fargs, **fkwargs):
97 97 lockkey = __get_lockkey(func, *fargs, **fkwargs)
98 98 lockkey_path = config['app_conf']['cache_dir']
99 99
100 100 log.info('running task with lockkey %s' % lockkey)
101 101 try:
102 102 l = DaemonLock(file_=jn(lockkey_path, lockkey))
103 103 ret = func(*fargs, **fkwargs)
104 104 l.release()
105 105 return ret
106 106 except LockHeld:
107 107 log.info('LockHeld')
108 108 return 'Task with key %s already running' % lockkey
109 109
110 110 return decorator(__wrapper, func)
111 111
112 112
113 113 def get_session():
114 114 if CELERY_ON:
115 115 engine = engine_from_config(config, 'sqlalchemy.db1.')
116 116 init_model(engine)
117 117 sa = meta.Session()
118 118 return sa
119 119
120 120
121 121 def dbsession(func):
122 122 def __wrapper(func, *fargs, **fkwargs):
123 123 try:
124 124 ret = func(*fargs, **fkwargs)
125 125 return ret
126 126 finally:
127 if CELERY_ON and CELERY_EAGER is False:
127 if CELERY_ON and not CELERY_EAGER:
128 128 meta.Session.remove()
129 129
130 130 return decorator(__wrapper, func)
@@ -1,722 +1,722
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.db_manage
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Database creation, and setup module for RhodeCode. Used for creation
7 7 of database as well as for migration operations
8 8
9 9 :created_on: Apr 10, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import sys
29 29 import uuid
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32
33 33 from rhodecode import __dbversion__, __py_version__
34 34
35 35 from rhodecode.model.user import UserModel
36 36 from rhodecode.lib.utils import ask_ok
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
39 39 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup, \
40 40 UserRepoGroupToPerm
41 41
42 42 from sqlalchemy.engine import create_engine
43 43 from rhodecode.model.repos_group import ReposGroupModel
44 44 #from rhodecode.model import meta
45 45 from rhodecode.model.meta import Session, Base
46 46 from rhodecode.model.repo import RepoModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def notify(msg):
53 53 """
54 54 Notification for migrations messages
55 55 """
56 56 ml = len(msg) + (4 * 2)
57 57 print >> sys.stdout, ('*** %s ***\n%s' % (msg, '*' * ml)).upper()
58 58
59 59
60 60 class DbManage(object):
61 61 def __init__(self, log_sql, dbconf, root, tests=False, cli_args={}):
62 62 self.dbname = dbconf.split('/')[-1]
63 63 self.tests = tests
64 64 self.root = root
65 65 self.dburi = dbconf
66 66 self.log_sql = log_sql
67 67 self.db_exists = False
68 68 self.cli_args = cli_args
69 69 self.init_db()
70 70 global ask_ok
71 71
72 if self.cli_args.get('force_ask') is True:
72 if self.cli_args.get('force_ask'):
73 73 ask_ok = lambda *args, **kwargs: True
74 elif self.cli_args.get('force_ask') is False:
74 elif not self.cli_args.get('force_ask'):
75 75 ask_ok = lambda *args, **kwargs: False
76 76
77 77 def init_db(self):
78 78 engine = create_engine(self.dburi, echo=self.log_sql)
79 79 init_model(engine)
80 80 self.sa = Session()
81 81
82 82 def create_tables(self, override=False):
83 83 """
84 84 Create a auth database
85 85 """
86 86
87 87 log.info("Any existing database is going to be destroyed")
88 88 if self.tests:
89 89 destroy = True
90 90 else:
91 91 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
92 92 if not destroy:
93 93 sys.exit('Nothing done')
94 94 if destroy:
95 95 Base.metadata.drop_all()
96 96
97 97 checkfirst = not override
98 98 Base.metadata.create_all(checkfirst=checkfirst)
99 99 log.info('Created tables for %s' % self.dbname)
100 100
101 101 def set_db_version(self):
102 102 ver = DbMigrateVersion()
103 103 ver.version = __dbversion__
104 104 ver.repository_id = 'rhodecode_db_migrations'
105 105 ver.repository_path = 'versions'
106 106 self.sa.add(ver)
107 107 log.info('db version set to: %s' % __dbversion__)
108 108
109 109 def upgrade(self):
110 110 """
111 111 Upgrades given database schema to given revision following
112 112 all needed steps, to perform the upgrade
113 113
114 114 """
115 115
116 116 from rhodecode.lib.dbmigrate.migrate.versioning import api
117 117 from rhodecode.lib.dbmigrate.migrate.exceptions import \
118 118 DatabaseNotControlledError
119 119
120 120 if 'sqlite' in self.dburi:
121 121 print (
122 122 '********************** WARNING **********************\n'
123 123 'Make sure your version of sqlite is at least 3.7.X. \n'
124 124 'Earlier versions are known to fail on some migrations\n'
125 125 '*****************************************************\n'
126 126 )
127 127 upgrade = ask_ok('You are about to perform database upgrade, make '
128 128 'sure You backed up your database before. '
129 129 'Continue ? [y/n]')
130 130 if not upgrade:
131 131 sys.exit('Nothing done')
132 132
133 133 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
134 134 'rhodecode/lib/dbmigrate')
135 135 db_uri = self.dburi
136 136
137 137 try:
138 138 curr_version = api.db_version(db_uri, repository_path)
139 139 msg = ('Found current database under version'
140 140 ' control with version %s' % curr_version)
141 141
142 142 except (RuntimeError, DatabaseNotControlledError):
143 143 curr_version = 1
144 144 msg = ('Current database is not under version control. Setting'
145 145 ' as version %s' % curr_version)
146 146 api.version_control(db_uri, repository_path, curr_version)
147 147
148 148 notify(msg)
149 149
150 150 if curr_version == __dbversion__:
151 151 sys.exit('This database is already at the newest version')
152 152
153 153 #======================================================================
154 154 # UPGRADE STEPS
155 155 #======================================================================
156 156
157 157 class UpgradeSteps(object):
158 158 """
159 159 Those steps follow schema versions so for example schema
160 160 for example schema with seq 002 == step_2 and so on.
161 161 """
162 162
163 163 def __init__(self, klass):
164 164 self.klass = klass
165 165
166 166 def step_0(self):
167 167 # step 0 is the schema upgrade, and than follow proper upgrades
168 168 notify('attempting to do database upgrade from '
169 169 'version %s to version %s' %(curr_version, __dbversion__))
170 170 api.upgrade(db_uri, repository_path, __dbversion__)
171 171 notify('Schema upgrade completed')
172 172
173 173 def step_1(self):
174 174 pass
175 175
176 176 def step_2(self):
177 177 notify('Patching repo paths for newer version of RhodeCode')
178 178 self.klass.fix_repo_paths()
179 179
180 180 notify('Patching default user of RhodeCode')
181 181 self.klass.fix_default_user()
182 182
183 183 log.info('Changing ui settings')
184 184 self.klass.create_ui_settings()
185 185
186 186 def step_3(self):
187 187 notify('Adding additional settings into RhodeCode db')
188 188 self.klass.fix_settings()
189 189 notify('Adding ldap defaults')
190 190 self.klass.create_ldap_options(skip_existing=True)
191 191
192 192 def step_4(self):
193 193 notify('create permissions and fix groups')
194 194 self.klass.create_permissions()
195 195 self.klass.fixup_groups()
196 196
197 197 def step_5(self):
198 198 pass
199 199
200 200 def step_6(self):
201 201
202 202 notify('re-checking permissions')
203 203 self.klass.create_permissions()
204 204
205 205 notify('installing new UI options')
206 206 sett4 = RhodeCodeSetting('show_public_icon', True)
207 207 Session().add(sett4)
208 208 sett5 = RhodeCodeSetting('show_private_icon', True)
209 209 Session().add(sett5)
210 210 sett6 = RhodeCodeSetting('stylify_metatags', False)
211 211 Session().add(sett6)
212 212
213 213 notify('fixing old PULL hook')
214 214 _pull = RhodeCodeUi.get_by_key('preoutgoing.pull_logger')
215 215 if _pull:
216 216 _pull.ui_key = RhodeCodeUi.HOOK_PULL
217 217 Session().add(_pull)
218 218
219 219 notify('fixing old PUSH hook')
220 220 _push = RhodeCodeUi.get_by_key('pretxnchangegroup.push_logger')
221 221 if _push:
222 222 _push.ui_key = RhodeCodeUi.HOOK_PUSH
223 223 Session().add(_push)
224 224
225 225 notify('installing new pre-push hook')
226 226 hooks4 = RhodeCodeUi()
227 227 hooks4.ui_section = 'hooks'
228 228 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
229 229 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
230 230 Session().add(hooks4)
231 231
232 232 notify('installing new pre-pull hook')
233 233 hooks6 = RhodeCodeUi()
234 234 hooks6.ui_section = 'hooks'
235 235 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
236 236 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
237 237 Session().add(hooks6)
238 238
239 239 notify('installing hgsubversion option')
240 240 # enable hgsubversion disabled by default
241 241 hgsubversion = RhodeCodeUi()
242 242 hgsubversion.ui_section = 'extensions'
243 243 hgsubversion.ui_key = 'hgsubversion'
244 244 hgsubversion.ui_value = ''
245 245 hgsubversion.ui_active = False
246 246 Session().add(hgsubversion)
247 247
248 248 notify('installing hg git option')
249 249 # enable hggit disabled by default
250 250 hggit = RhodeCodeUi()
251 251 hggit.ui_section = 'extensions'
252 252 hggit.ui_key = 'hggit'
253 253 hggit.ui_value = ''
254 254 hggit.ui_active = False
255 255 Session().add(hggit)
256 256
257 257 notify('re-check default permissions')
258 258 default_user = User.get_by_username(User.DEFAULT_USER)
259 259 perm = Permission.get_by_key('hg.fork.repository')
260 260 reg_perm = UserToPerm()
261 261 reg_perm.user = default_user
262 262 reg_perm.permission = perm
263 263 Session().add(reg_perm)
264 264
265 265 def step_7(self):
266 266 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
267 267 Session().commit()
268 268 if perm_fixes:
269 269 notify('There was an inconsistent state of permissions '
270 270 'detected for default user. Permissions are now '
271 271 'reset to the default value for default user. '
272 272 'Please validate and check default permissions '
273 273 'in admin panel')
274 274
275 275 def step_8(self):
276 276 self.klass.populate_default_permissions()
277 277 self.klass.create_default_options(skip_existing=True)
278 278 Session().commit()
279 279
280 280 def step_9(self):
281 281 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
282 282 Session().commit()
283 283 if perm_fixes:
284 284 notify('There was an inconsistent state of permissions '
285 285 'detected for default user. Permissions are now '
286 286 'reset to the default value for default user. '
287 287 'Please validate and check default permissions '
288 288 'in admin panel')
289 289
290 290 def step_10(self):
291 291 pass
292 292
293 293 def step_11(self):
294 294 self.klass.update_repo_info()
295 295
296 296 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
297 297
298 298 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
299 299 _step = None
300 300 for step in upgrade_steps:
301 301 notify('performing upgrade step %s' % step)
302 302 getattr(UpgradeSteps(self), 'step_%s' % step)()
303 303 self.sa.commit()
304 304 _step = step
305 305
306 306 notify('upgrade to version %s successful' % _step)
307 307
308 308 def fix_repo_paths(self):
309 309 """
310 310 Fixes a old rhodecode version path into new one without a '*'
311 311 """
312 312
313 313 paths = self.sa.query(RhodeCodeUi)\
314 314 .filter(RhodeCodeUi.ui_key == '/')\
315 315 .scalar()
316 316
317 317 paths.ui_value = paths.ui_value.replace('*', '')
318 318
319 319 try:
320 320 self.sa.add(paths)
321 321 self.sa.commit()
322 322 except:
323 323 self.sa.rollback()
324 324 raise
325 325
326 326 def fix_default_user(self):
327 327 """
328 328 Fixes a old default user with some 'nicer' default values,
329 329 used mostly for anonymous access
330 330 """
331 331 def_user = self.sa.query(User)\
332 332 .filter(User.username == 'default')\
333 333 .one()
334 334
335 335 def_user.name = 'Anonymous'
336 336 def_user.lastname = 'User'
337 337 def_user.email = 'anonymous@rhodecode.org'
338 338
339 339 try:
340 340 self.sa.add(def_user)
341 341 self.sa.commit()
342 342 except:
343 343 self.sa.rollback()
344 344 raise
345 345
346 346 def fix_settings(self):
347 347 """
348 348 Fixes rhodecode settings adds ga_code key for google analytics
349 349 """
350 350
351 351 hgsettings3 = RhodeCodeSetting('ga_code', '')
352 352
353 353 try:
354 354 self.sa.add(hgsettings3)
355 355 self.sa.commit()
356 356 except:
357 357 self.sa.rollback()
358 358 raise
359 359
360 360 def admin_prompt(self, second=False):
361 361 if not self.tests:
362 362 import getpass
363 363
364 364 # defaults
365 365 defaults = self.cli_args
366 366 username = defaults.get('username')
367 367 password = defaults.get('password')
368 368 email = defaults.get('email')
369 369
370 370 def get_password():
371 371 password = getpass.getpass('Specify admin password '
372 372 '(min 6 chars):')
373 373 confirm = getpass.getpass('Confirm password:')
374 374
375 375 if password != confirm:
376 376 log.error('passwords mismatch')
377 377 return False
378 378 if len(password) < 6:
379 379 log.error('password is to short use at least 6 characters')
380 380 return False
381 381
382 382 return password
383 383 if username is None:
384 384 username = raw_input('Specify admin username:')
385 385 if password is None:
386 386 password = get_password()
387 387 if not password:
388 388 #second try
389 389 password = get_password()
390 390 if not password:
391 391 sys.exit()
392 392 if email is None:
393 393 email = raw_input('Specify admin email:')
394 394 self.create_user(username, password, email, True)
395 395 else:
396 396 log.info('creating admin and regular test users')
397 397 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
398 398 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
399 399 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
400 400 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
401 401 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
402 402
403 403 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
404 404 TEST_USER_ADMIN_EMAIL, True)
405 405
406 406 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
407 407 TEST_USER_REGULAR_EMAIL, False)
408 408
409 409 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
410 410 TEST_USER_REGULAR2_EMAIL, False)
411 411
412 412 def create_ui_settings(self):
413 413 """
414 414 Creates ui settings, fills out hooks
415 415 and disables dotencode
416 416 """
417 417
418 418 #HOOKS
419 419 hooks1_key = RhodeCodeUi.HOOK_UPDATE
420 420 hooks1_ = self.sa.query(RhodeCodeUi)\
421 421 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
422 422
423 423 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
424 424 hooks1.ui_section = 'hooks'
425 425 hooks1.ui_key = hooks1_key
426 426 hooks1.ui_value = 'hg update >&2'
427 427 hooks1.ui_active = False
428 428 self.sa.add(hooks1)
429 429
430 430 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
431 431 hooks2_ = self.sa.query(RhodeCodeUi)\
432 432 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
433 433 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
434 434 hooks2.ui_section = 'hooks'
435 435 hooks2.ui_key = hooks2_key
436 436 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
437 437 self.sa.add(hooks2)
438 438
439 439 hooks3 = RhodeCodeUi()
440 440 hooks3.ui_section = 'hooks'
441 441 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
442 442 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
443 443 self.sa.add(hooks3)
444 444
445 445 hooks4 = RhodeCodeUi()
446 446 hooks4.ui_section = 'hooks'
447 447 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
448 448 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
449 449 self.sa.add(hooks4)
450 450
451 451 hooks5 = RhodeCodeUi()
452 452 hooks5.ui_section = 'hooks'
453 453 hooks5.ui_key = RhodeCodeUi.HOOK_PULL
454 454 hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
455 455 self.sa.add(hooks5)
456 456
457 457 hooks6 = RhodeCodeUi()
458 458 hooks6.ui_section = 'hooks'
459 459 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
460 460 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
461 461 self.sa.add(hooks6)
462 462
463 463 # enable largefiles
464 464 largefiles = RhodeCodeUi()
465 465 largefiles.ui_section = 'extensions'
466 466 largefiles.ui_key = 'largefiles'
467 467 largefiles.ui_value = ''
468 468 self.sa.add(largefiles)
469 469
470 470 # enable hgsubversion disabled by default
471 471 hgsubversion = RhodeCodeUi()
472 472 hgsubversion.ui_section = 'extensions'
473 473 hgsubversion.ui_key = 'hgsubversion'
474 474 hgsubversion.ui_value = ''
475 475 hgsubversion.ui_active = False
476 476 self.sa.add(hgsubversion)
477 477
478 478 # enable hggit disabled by default
479 479 hggit = RhodeCodeUi()
480 480 hggit.ui_section = 'extensions'
481 481 hggit.ui_key = 'hggit'
482 482 hggit.ui_value = ''
483 483 hggit.ui_active = False
484 484 self.sa.add(hggit)
485 485
486 486 def create_ldap_options(self, skip_existing=False):
487 487 """Creates ldap settings"""
488 488
489 489 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
490 490 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
491 491 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
492 492 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
493 493 ('ldap_filter', ''), ('ldap_search_scope', ''),
494 494 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
495 495 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
496 496
497 497 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
498 498 log.debug('Skipping option %s' % k)
499 499 continue
500 500 setting = RhodeCodeSetting(k, v)
501 501 self.sa.add(setting)
502 502
503 503 def create_default_options(self, skip_existing=False):
504 504 """Creates default settings"""
505 505
506 506 for k, v in [
507 507 ('default_repo_enable_locking', False),
508 508 ('default_repo_enable_downloads', False),
509 509 ('default_repo_enable_statistics', False),
510 510 ('default_repo_private', False),
511 511 ('default_repo_type', 'hg')]:
512 512
513 513 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
514 514 log.debug('Skipping option %s' % k)
515 515 continue
516 516 setting = RhodeCodeSetting(k, v)
517 517 self.sa.add(setting)
518 518
519 519 def fixup_groups(self):
520 520 def_usr = User.get_by_username('default')
521 521 for g in RepoGroup.query().all():
522 522 g.group_name = g.get_new_name(g.name)
523 523 self.sa.add(g)
524 524 # get default perm
525 525 default = UserRepoGroupToPerm.query()\
526 526 .filter(UserRepoGroupToPerm.group == g)\
527 527 .filter(UserRepoGroupToPerm.user == def_usr)\
528 528 .scalar()
529 529
530 530 if default is None:
531 531 log.debug('missing default permission for group %s adding' % g)
532 532 ReposGroupModel()._create_default_perms(g)
533 533
534 534 def reset_permissions(self, username):
535 535 """
536 536 Resets permissions to default state, usefull when old systems had
537 537 bad permissions, we must clean them up
538 538
539 539 :param username:
540 540 :type username:
541 541 """
542 542 default_user = User.get_by_username(username)
543 543 if not default_user:
544 544 return
545 545
546 546 u2p = UserToPerm.query()\
547 547 .filter(UserToPerm.user == default_user).all()
548 548 fixed = False
549 549 if len(u2p) != len(User.DEFAULT_PERMISSIONS):
550 550 for p in u2p:
551 551 Session().delete(p)
552 552 fixed = True
553 553 self.populate_default_permissions()
554 554 return fixed
555 555
556 556 def update_repo_info(self):
557 557 RepoModel.update_repoinfo()
558 558
559 559 def config_prompt(self, test_repo_path='', retries=3):
560 560 defaults = self.cli_args
561 561 _path = defaults.get('repos_location')
562 562 if retries == 3:
563 563 log.info('Setting up repositories config')
564 564
565 565 if _path is not None:
566 566 path = _path
567 567 elif not self.tests and not test_repo_path:
568 568 path = raw_input(
569 569 'Enter a valid absolute path to store repositories. '
570 570 'All repositories in that path will be added automatically:'
571 571 )
572 572 else:
573 573 path = test_repo_path
574 574 path_ok = True
575 575
576 576 # check proper dir
577 577 if not os.path.isdir(path):
578 578 path_ok = False
579 579 log.error('Given path %s is not a valid directory' % path)
580 580
581 581 elif not os.path.isabs(path):
582 582 path_ok = False
583 583 log.error('Given path %s is not an absolute path' % path)
584 584
585 585 # check write access
586 586 elif not os.access(path, os.W_OK) and path_ok:
587 587 path_ok = False
588 588 log.error('No write permission to given path %s' % path)
589 589
590 590 if retries == 0:
591 591 sys.exit('max retries reached')
592 if path_ok is False:
592 if not path_ok:
593 593 retries -= 1
594 594 return self.config_prompt(test_repo_path, retries)
595 595
596 596 real_path = os.path.normpath(os.path.realpath(path))
597 597
598 598 if real_path != os.path.normpath(path):
599 599 if not ask_ok(('Path looks like a symlink, Rhodecode will store '
600 600 'given path as %s ? [y/n]') % (real_path)):
601 601 log.error('Canceled by user')
602 602 sys.exit(-1)
603 603
604 604 return real_path
605 605
606 606 def create_settings(self, path):
607 607
608 608 self.create_ui_settings()
609 609
610 610 #HG UI OPTIONS
611 611 web1 = RhodeCodeUi()
612 612 web1.ui_section = 'web'
613 613 web1.ui_key = 'push_ssl'
614 614 web1.ui_value = 'false'
615 615
616 616 web2 = RhodeCodeUi()
617 617 web2.ui_section = 'web'
618 618 web2.ui_key = 'allow_archive'
619 619 web2.ui_value = 'gz zip bz2'
620 620
621 621 web3 = RhodeCodeUi()
622 622 web3.ui_section = 'web'
623 623 web3.ui_key = 'allow_push'
624 624 web3.ui_value = '*'
625 625
626 626 web4 = RhodeCodeUi()
627 627 web4.ui_section = 'web'
628 628 web4.ui_key = 'baseurl'
629 629 web4.ui_value = '/'
630 630
631 631 paths = RhodeCodeUi()
632 632 paths.ui_section = 'paths'
633 633 paths.ui_key = '/'
634 634 paths.ui_value = path
635 635
636 636 phases = RhodeCodeUi()
637 637 phases.ui_section = 'phases'
638 638 phases.ui_key = 'publish'
639 639 phases.ui_value = False
640 640
641 641 sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
642 642 sett2 = RhodeCodeSetting('title', 'RhodeCode')
643 643 sett3 = RhodeCodeSetting('ga_code', '')
644 644
645 645 sett4 = RhodeCodeSetting('show_public_icon', True)
646 646 sett5 = RhodeCodeSetting('show_private_icon', True)
647 647 sett6 = RhodeCodeSetting('stylify_metatags', False)
648 648
649 649 self.sa.add(web1)
650 650 self.sa.add(web2)
651 651 self.sa.add(web3)
652 652 self.sa.add(web4)
653 653 self.sa.add(paths)
654 654 self.sa.add(sett1)
655 655 self.sa.add(sett2)
656 656 self.sa.add(sett3)
657 657 self.sa.add(sett4)
658 658 self.sa.add(sett5)
659 659 self.sa.add(sett6)
660 660
661 661 self.create_ldap_options()
662 662 self.create_default_options()
663 663
664 664 log.info('created ui config')
665 665
666 666 def create_user(self, username, password, email='', admin=False):
667 667 log.info('creating user %s' % username)
668 668 UserModel().create_or_update(username, password, email,
669 669 firstname='RhodeCode', lastname='Admin',
670 670 active=True, admin=admin)
671 671
672 672 def create_default_user(self):
673 673 log.info('creating default user')
674 674 # create default user for handling default permissions.
675 675 UserModel().create_or_update(username='default',
676 676 password=str(uuid.uuid1())[:8],
677 677 email='anonymous@rhodecode.org',
678 678 firstname='Anonymous', lastname='User')
679 679
680 680 def create_permissions(self):
681 681 # module.(access|create|change|delete)_[name]
682 682 # module.(none|read|write|admin)
683 683
684 684 for p in Permission.PERMS:
685 685 if not Permission.get_by_key(p[0]):
686 686 new_perm = Permission()
687 687 new_perm.permission_name = p[0]
688 688 new_perm.permission_longname = p[0]
689 689 self.sa.add(new_perm)
690 690
691 691 def populate_default_permissions(self):
692 692 log.info('creating default user permissions')
693 693
694 694 default_user = User.get_by_username('default')
695 695
696 696 for def_perm in User.DEFAULT_PERMISSIONS:
697 697
698 698 perm = self.sa.query(Permission)\
699 699 .filter(Permission.permission_name == def_perm)\
700 700 .scalar()
701 701 if not perm:
702 702 raise Exception(
703 703 'CRITICAL: permission %s not found inside database !!'
704 704 % def_perm
705 705 )
706 706 if not UserToPerm.query()\
707 707 .filter(UserToPerm.permission == perm)\
708 708 .filter(UserToPerm.user == default_user).scalar():
709 709 reg_perm = UserToPerm()
710 710 reg_perm.user = default_user
711 711 reg_perm.permission = perm
712 712 self.sa.add(reg_perm)
713 713
714 714 @staticmethod
715 715 def check_waitress():
716 716 """
717 717 Function executed at the end of setup
718 718 """
719 719 if not __py_version__ >= (2, 6):
720 720 notify('Python2.5 detected, please switch '
721 721 'egg:waitress#main -> egg:Paste#http '
722 722 'in your .ini file')
@@ -1,717 +1,717
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31
32 32 from itertools import tee, imap
33 33
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.lib.vcs.exceptions import VCSError
37 37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 39 from rhodecode.lib.helpers import escape
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def wrap_to_table(str_):
46 46 return '''<table class="code-difftable">
47 47 <tr class="line no-comment">
48 48 <td class="lineno new"></td>
49 49 <td class="code no-comment"><pre>%s</pre></td>
50 50 </tr>
51 51 </table>''' % str_
52 52
53 53
54 54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 55 ignore_whitespace=True, line_context=3,
56 56 enable_comments=False):
57 57 """
58 58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 59 proper message
60 60 """
61 61
62 62 if filenode_old is None:
63 63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64 64
65 65 if filenode_old.is_binary or filenode_new.is_binary:
66 66 diff = wrap_to_table(_('binary file'))
67 67 stats = (0, 0)
68 68 size = 0
69 69
70 70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72 72
73 73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 74 ignore_whitespace=ignore_whitespace,
75 75 context=line_context)
76 76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77 77
78 78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 79 stats = diff_processor.stat()
80 80 size = len(diff or '')
81 81 else:
82 82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 83 'diff menu to display this diff'))
84 84 stats = (0, 0)
85 85 size = 0
86 86 if not diff:
87 87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 88 [filenode_new, filenode_old])
89 89 if submodules:
90 90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 91 else:
92 92 diff = wrap_to_table(_('No changes detected'))
93 93
94 94 cs1 = filenode_old.changeset.raw_id
95 95 cs2 = filenode_new.changeset.raw_id
96 96
97 97 return size, cs1, cs2, diff, stats
98 98
99 99
100 100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 101 """
102 102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103 103
104 104 :param ignore_whitespace: ignore whitespaces in diff
105 105 """
106 106 # make sure we pass in default context
107 107 context = context or 3
108 108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 109 [filenode_new, filenode_old])
110 110 if submodules:
111 111 return ''
112 112
113 113 for filenode in (filenode_old, filenode_new):
114 114 if not isinstance(filenode, FileNode):
115 115 raise VCSError("Given object should be FileNode object, not %s"
116 116 % filenode.__class__)
117 117
118 118 repo = filenode_new.changeset.repository
119 119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121 121
122 122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 123 ignore_whitespace, context)
124 124 return vcs_gitdiff
125 125
126 126 NEW_FILENODE = 1
127 127 DEL_FILENODE = 2
128 128 MOD_FILENODE = 3
129 129 RENAMED_FILENODE = 4
130 130 CHMOD_FILENODE = 5
131 131
132 132
133 133 class DiffLimitExceeded(Exception):
134 134 pass
135 135
136 136
137 137 class LimitedDiffContainer(object):
138 138
139 139 def __init__(self, diff_limit, cur_diff_size, diff):
140 140 self.diff = diff
141 141 self.diff_limit = diff_limit
142 142 self.cur_diff_size = cur_diff_size
143 143
144 144 def __iter__(self):
145 145 for l in self.diff:
146 146 yield l
147 147
148 148
149 149 class DiffProcessor(object):
150 150 """
151 151 Give it a unified or git diff and it returns a list of the files that were
152 152 mentioned in the diff together with a dict of meta information that
153 153 can be used to render it in a HTML template.
154 154 """
155 155 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
156 156 _newline_marker = re.compile(r'^\\ No newline at end of file')
157 157 _git_header_re = re.compile(r"""
158 158 #^diff[ ]--git
159 159 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
160 160 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
161 161 ^rename[ ]from[ ](?P<rename_from>\S+)\n
162 162 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
163 163 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
164 164 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
165 165 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
166 166 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
167 167 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
168 168 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
169 169 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
170 170 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
171 171 """, re.VERBOSE | re.MULTILINE)
172 172 _hg_header_re = re.compile(r"""
173 173 #^diff[ ]--git
174 174 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
175 175 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
176 176 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
177 177 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
178 178 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
179 179 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
180 180 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
181 181 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
182 182 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
183 183 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
184 184 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
185 185 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
186 186 """, re.VERBOSE | re.MULTILINE)
187 187
188 188 #used for inline highlighter word split
189 189 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
190 190
191 191 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
192 192 """
193 193 :param diff: a text in diff format
194 194 :param vcs: type of version controll hg or git
195 195 :param format: format of diff passed, `udiff` or `gitdiff`
196 196 :param diff_limit: define the size of diff that is considered "big"
197 197 based on that parameter cut off will be triggered, set to None
198 198 to show full diff
199 199 """
200 200 if not isinstance(diff, basestring):
201 201 raise Exception('Diff must be a basestring got %s instead' % type(diff))
202 202
203 203 self._diff = diff
204 204 self._format = format
205 205 self.adds = 0
206 206 self.removes = 0
207 207 # calculate diff size
208 208 self.diff_size = len(diff)
209 209 self.diff_limit = diff_limit
210 210 self.cur_diff_size = 0
211 211 self.parsed = False
212 212 self.parsed_diff = []
213 213 self.vcs = vcs
214 214
215 215 if format == 'gitdiff':
216 216 self.differ = self._highlight_line_difflib
217 217 self._parser = self._parse_gitdiff
218 218 else:
219 219 self.differ = self._highlight_line_udiff
220 220 self._parser = self._parse_udiff
221 221
222 222 def _copy_iterator(self):
223 223 """
224 224 make a fresh copy of generator, we should not iterate thru
225 225 an original as it's needed for repeating operations on
226 226 this instance of DiffProcessor
227 227 """
228 228 self.__udiff, iterator_copy = tee(self.__udiff)
229 229 return iterator_copy
230 230
231 231 def _escaper(self, string):
232 232 """
233 233 Escaper for diff escapes special chars and checks the diff limit
234 234
235 235 :param string:
236 236 :type string:
237 237 """
238 238
239 239 self.cur_diff_size += len(string)
240 240
241 241 # escaper get's iterated on each .next() call and it checks if each
242 242 # parsed line doesn't exceed the diff limit
243 243 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
244 244 raise DiffLimitExceeded('Diff Limit Exceeded')
245 245
246 246 return safe_unicode(string).replace('&', '&amp;')\
247 247 .replace('<', '&lt;')\
248 248 .replace('>', '&gt;')
249 249
250 250 def _line_counter(self, l):
251 251 """
252 252 Checks each line and bumps total adds/removes for this diff
253 253
254 254 :param l:
255 255 """
256 256 if l.startswith('+') and not l.startswith('+++'):
257 257 self.adds += 1
258 258 elif l.startswith('-') and not l.startswith('---'):
259 259 self.removes += 1
260 260 return safe_unicode(l)
261 261
262 262 def _highlight_line_difflib(self, line, next_):
263 263 """
264 264 Highlight inline changes in both lines.
265 265 """
266 266
267 267 if line['action'] == 'del':
268 268 old, new = line, next_
269 269 else:
270 270 old, new = next_, line
271 271
272 272 oldwords = self._token_re.split(old['line'])
273 273 newwords = self._token_re.split(new['line'])
274 274 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
275 275
276 276 oldfragments, newfragments = [], []
277 277 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
278 278 oldfrag = ''.join(oldwords[i1:i2])
279 279 newfrag = ''.join(newwords[j1:j2])
280 280 if tag != 'equal':
281 281 if oldfrag:
282 282 oldfrag = '<del>%s</del>' % oldfrag
283 283 if newfrag:
284 284 newfrag = '<ins>%s</ins>' % newfrag
285 285 oldfragments.append(oldfrag)
286 286 newfragments.append(newfrag)
287 287
288 288 old['line'] = "".join(oldfragments)
289 289 new['line'] = "".join(newfragments)
290 290
291 291 def _highlight_line_udiff(self, line, next_):
292 292 """
293 293 Highlight inline changes in both lines.
294 294 """
295 295 start = 0
296 296 limit = min(len(line['line']), len(next_['line']))
297 297 while start < limit and line['line'][start] == next_['line'][start]:
298 298 start += 1
299 299 end = -1
300 300 limit -= start
301 301 while -end <= limit and line['line'][end] == next_['line'][end]:
302 302 end -= 1
303 303 end += 1
304 304 if start or end:
305 305 def do(l):
306 306 last = end + len(l['line'])
307 307 if l['action'] == 'add':
308 308 tag = 'ins'
309 309 else:
310 310 tag = 'del'
311 311 l['line'] = '%s<%s>%s</%s>%s' % (
312 312 l['line'][:start],
313 313 tag,
314 314 l['line'][start:last],
315 315 tag,
316 316 l['line'][last:]
317 317 )
318 318 do(line)
319 319 do(next_)
320 320
321 321 def _get_header(self, diff_chunk):
322 322 """
323 323 parses the diff header, and returns parts, and leftover diff
324 324 parts consists of 14 elements::
325 325
326 326 a_path, b_path, similarity_index, rename_from, rename_to,
327 327 old_mode, new_mode, new_file_mode, deleted_file_mode,
328 328 a_blob_id, b_blob_id, b_mode, a_file, b_file
329 329
330 330 :param diff_chunk:
331 331 :type diff_chunk:
332 332 """
333 333
334 334 if self.vcs == 'git':
335 335 match = self._git_header_re.match(diff_chunk)
336 336 diff = diff_chunk[match.end():]
337 337 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
338 338 elif self.vcs == 'hg':
339 339 match = self._hg_header_re.match(diff_chunk)
340 340 diff = diff_chunk[match.end():]
341 341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
342 342 else:
343 343 raise Exception('VCS type %s is not supported' % self.vcs)
344 344
345 345 def _clean_line(self, line, command):
346 346 if command in ['+', '-', ' ']:
347 347 #only modify the line if it's actually a diff thing
348 348 line = line[1:]
349 349 return line
350 350
351 351 def _parse_gitdiff(self, inline_diff=True):
352 352 _files = []
353 353 diff_container = lambda arg: arg
354 354
355 355 ##split the diff in chunks of separate --git a/file b/file chunks
356 356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 357 binary = False
358 358 binary_msg = 'unknown binary'
359 359 head, diff = self._get_header(raw_diff)
360 360
361 361 if not head['a_file'] and head['b_file']:
362 362 op = 'A'
363 363 elif head['a_file'] and head['b_file']:
364 364 op = 'M'
365 365 elif head['a_file'] and not head['b_file']:
366 366 op = 'D'
367 367 else:
368 368 #probably we're dealing with a binary file 1
369 369 binary = True
370 370 if head['deleted_file_mode']:
371 371 op = 'D'
372 372 stats = ['b', DEL_FILENODE]
373 373 binary_msg = 'deleted binary file'
374 374 elif head['new_file_mode']:
375 375 op = 'A'
376 376 stats = ['b', NEW_FILENODE]
377 377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 378 else:
379 379 if head['new_mode'] and head['old_mode']:
380 380 stats = ['b', CHMOD_FILENODE]
381 381 op = 'M'
382 382 binary_msg = ('modified binary file chmod %s => %s'
383 383 % (head['old_mode'], head['new_mode']))
384 384 elif (head['rename_from'] and head['rename_to']
385 385 and head['rename_from'] != head['rename_to']):
386 386 stats = ['b', RENAMED_FILENODE]
387 387 op = 'M'
388 388 binary_msg = ('file renamed from %s to %s'
389 389 % (head['rename_from'], head['rename_to']))
390 390 else:
391 391 stats = ['b', MOD_FILENODE]
392 392 op = 'M'
393 393 binary_msg = 'modified binary file'
394 394
395 395 if not binary:
396 396 try:
397 397 chunks, stats = self._parse_lines(diff)
398 398 except DiffLimitExceeded:
399 399 diff_container = lambda _diff: LimitedDiffContainer(
400 400 self.diff_limit,
401 401 self.cur_diff_size,
402 402 _diff)
403 403 break
404 404 else:
405 405 chunks = []
406 406 chunks.append([{
407 407 'old_lineno': '',
408 408 'new_lineno': '',
409 409 'action': 'binary',
410 410 'line': binary_msg,
411 411 }])
412 412
413 413 _files.append({
414 414 'filename': head['b_path'],
415 415 'old_revision': head['a_blob_id'],
416 416 'new_revision': head['b_blob_id'],
417 417 'chunks': chunks,
418 418 'operation': op,
419 419 'stats': stats,
420 420 })
421 421
422 422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423 423
424 if inline_diff is False:
424 if not inline_diff:
425 425 return diff_container(sorted(_files, key=sorter))
426 426
427 427 # highlight inline changes
428 428 for diff_data in _files:
429 429 for chunk in diff_data['chunks']:
430 430 lineiter = iter(chunk)
431 431 try:
432 432 while 1:
433 433 line = lineiter.next()
434 434 if line['action'] not in ['unmod', 'context']:
435 435 nextline = lineiter.next()
436 436 if nextline['action'] in ['unmod', 'context'] or \
437 437 nextline['action'] == line['action']:
438 438 continue
439 439 self.differ(line, nextline)
440 440 except StopIteration:
441 441 pass
442 442
443 443 return diff_container(sorted(_files, key=sorter))
444 444
445 445 def _parse_udiff(self, inline_diff=True):
446 446 raise NotImplementedError()
447 447
448 448 def _parse_lines(self, diff):
449 449 """
450 450 Parse the diff an return data for the template.
451 451 """
452 452
453 453 lineiter = iter(diff)
454 454 stats = [0, 0]
455 455
456 456 try:
457 457 chunks = []
458 458 line = lineiter.next()
459 459
460 460 while line:
461 461 lines = []
462 462 chunks.append(lines)
463 463
464 464 match = self._chunk_re.match(line)
465 465
466 466 if not match:
467 467 break
468 468
469 469 gr = match.groups()
470 470 (old_line, old_end,
471 471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 472 old_line -= 1
473 473 new_line -= 1
474 474
475 475 context = len(gr) == 5
476 476 old_end += old_line
477 477 new_end += new_line
478 478
479 479 if context:
480 480 # skip context only if it's first line
481 481 if int(gr[0]) > 1:
482 482 lines.append({
483 483 'old_lineno': '...',
484 484 'new_lineno': '...',
485 485 'action': 'context',
486 486 'line': line,
487 487 })
488 488
489 489 line = lineiter.next()
490 490
491 491 while old_line < old_end or new_line < new_end:
492 492 command = ' '
493 493 if line:
494 494 command = line[0]
495 495
496 496 affects_old = affects_new = False
497 497
498 498 # ignore those if we don't expect them
499 499 if command in '#@':
500 500 continue
501 501 elif command == '+':
502 502 affects_new = True
503 503 action = 'add'
504 504 stats[0] += 1
505 505 elif command == '-':
506 506 affects_old = True
507 507 action = 'del'
508 508 stats[1] += 1
509 509 else:
510 510 affects_old = affects_new = True
511 511 action = 'unmod'
512 512
513 513 if not self._newline_marker.match(line):
514 514 old_line += affects_old
515 515 new_line += affects_new
516 516 lines.append({
517 517 'old_lineno': affects_old and old_line or '',
518 518 'new_lineno': affects_new and new_line or '',
519 519 'action': action,
520 520 'line': self._clean_line(line, command)
521 521 })
522 522
523 523 line = lineiter.next()
524 524
525 525 if self._newline_marker.match(line):
526 526 # we need to append to lines, since this is not
527 527 # counted in the line specs of diff
528 528 lines.append({
529 529 'old_lineno': '...',
530 530 'new_lineno': '...',
531 531 'action': 'context',
532 532 'line': self._clean_line(line, command)
533 533 })
534 534
535 535 except StopIteration:
536 536 pass
537 537 return chunks, stats
538 538
539 539 def _safe_id(self, idstring):
540 540 """Make a string safe for including in an id attribute.
541 541
542 542 The HTML spec says that id attributes 'must begin with
543 543 a letter ([A-Za-z]) and may be followed by any number
544 544 of letters, digits ([0-9]), hyphens ("-"), underscores
545 545 ("_"), colons (":"), and periods (".")'. These regexps
546 546 are slightly over-zealous, in that they remove colons
547 547 and periods unnecessarily.
548 548
549 549 Whitespace is transformed into underscores, and then
550 550 anything which is not a hyphen or a character that
551 551 matches \w (alphanumerics and underscore) is removed.
552 552
553 553 """
554 554 # Transform all whitespace to underscore
555 555 idstring = re.sub(r'\s', "_", '%s' % idstring)
556 556 # Remove everything that is not a hyphen or a member of \w
557 557 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
558 558 return idstring
559 559
560 560 def prepare(self, inline_diff=True):
561 561 """
562 562 Prepare the passed udiff for HTML rendering. It'l return a list
563 563 of dicts with diff information
564 564 """
565 565 parsed = self._parser(inline_diff=inline_diff)
566 566 self.parsed = True
567 567 self.parsed_diff = parsed
568 568 return parsed
569 569
570 570 def as_raw(self, diff_lines=None):
571 571 """
572 572 Returns raw string diff
573 573 """
574 574 return self._diff
575 575 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
576 576
577 577 def as_html(self, table_class='code-difftable', line_class='line',
578 578 old_lineno_class='lineno old', new_lineno_class='lineno new',
579 579 code_class='code', enable_comments=False, parsed_lines=None):
580 580 """
581 581 Return given diff as html table with customized css classes
582 582 """
583 583 def _link_to_if(condition, label, url):
584 584 """
585 585 Generates a link if condition is meet or just the label if not.
586 586 """
587 587
588 588 if condition:
589 589 return '''<a href="%(url)s">%(label)s</a>''' % {
590 590 'url': url,
591 591 'label': label
592 592 }
593 593 else:
594 594 return label
595 595 if not self.parsed:
596 596 self.prepare()
597 597
598 598 diff_lines = self.parsed_diff
599 599 if parsed_lines:
600 600 diff_lines = parsed_lines
601 601
602 602 _html_empty = True
603 603 _html = []
604 604 _html.append('''<table class="%(table_class)s">\n''' % {
605 605 'table_class': table_class
606 606 })
607 607
608 608 for diff in diff_lines:
609 609 for line in diff['chunks']:
610 610 _html_empty = False
611 611 for change in line:
612 612 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
613 613 'lc': line_class,
614 614 'action': change['action']
615 615 })
616 616 anchor_old_id = ''
617 617 anchor_new_id = ''
618 618 anchor_old = "%(filename)s_o%(oldline_no)s" % {
619 619 'filename': self._safe_id(diff['filename']),
620 620 'oldline_no': change['old_lineno']
621 621 }
622 622 anchor_new = "%(filename)s_n%(oldline_no)s" % {
623 623 'filename': self._safe_id(diff['filename']),
624 624 'oldline_no': change['new_lineno']
625 625 }
626 626 cond_old = (change['old_lineno'] != '...' and
627 627 change['old_lineno'])
628 628 cond_new = (change['new_lineno'] != '...' and
629 629 change['new_lineno'])
630 630 if cond_old:
631 631 anchor_old_id = 'id="%s"' % anchor_old
632 632 if cond_new:
633 633 anchor_new_id = 'id="%s"' % anchor_new
634 634 ###########################################################
635 635 # OLD LINE NUMBER
636 636 ###########################################################
637 637 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
638 638 'a_id': anchor_old_id,
639 639 'olc': old_lineno_class
640 640 })
641 641
642 642 _html.append('''%(link)s''' % {
643 643 'link': _link_to_if(True, change['old_lineno'],
644 644 '#%s' % anchor_old)
645 645 })
646 646 _html.append('''</td>\n''')
647 647 ###########################################################
648 648 # NEW LINE NUMBER
649 649 ###########################################################
650 650
651 651 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
652 652 'a_id': anchor_new_id,
653 653 'nlc': new_lineno_class
654 654 })
655 655
656 656 _html.append('''%(link)s''' % {
657 657 'link': _link_to_if(True, change['new_lineno'],
658 658 '#%s' % anchor_new)
659 659 })
660 660 _html.append('''</td>\n''')
661 661 ###########################################################
662 662 # CODE
663 663 ###########################################################
664 664 comments = '' if enable_comments else 'no-comment'
665 665 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
666 666 'cc': code_class,
667 667 'inc': comments
668 668 })
669 669 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
670 670 'code': change['line']
671 671 })
672 672
673 673 _html.append('''\t</td>''')
674 674 _html.append('''\n</tr>\n''')
675 675 _html.append('''</table>''')
676 676 if _html_empty:
677 677 return None
678 678 return ''.join(_html)
679 679
680 680 def stat(self):
681 681 """
682 682 Returns tuple of added, and removed lines for this instance
683 683 """
684 684 return self.adds, self.removes
685 685
686 686
687 687 def differ(org_repo, org_ref, other_repo, other_ref,
688 688 context=3, ignore_whitespace=False):
689 689 """
690 690 General differ between branches, bookmarks, revisions of two remote or
691 691 local but related repositories
692 692
693 693 :param org_repo:
694 694 :param org_ref:
695 695 :param other_repo:
696 696 :type other_repo:
697 697 :type other_ref:
698 698 """
699 699
700 700 org_repo_scm = org_repo.scm_instance
701 701 other_repo_scm = other_repo.scm_instance
702 702
703 703 org_repo = org_repo_scm._repo
704 704 other_repo = other_repo_scm._repo
705 705
706 706 org_ref = safe_str(org_ref[1])
707 707 other_ref = safe_str(other_ref[1])
708 708
709 709 if org_repo_scm == other_repo_scm:
710 710 log.debug('running diff between %s@%s and %s@%s'
711 711 % (org_repo.path, org_ref,
712 712 other_repo.path, other_ref))
713 713 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
714 714 ignore_whitespace=ignore_whitespace, context=context)
715 715 return _diff
716 716
717 717 return '' # FIXME: when is it ever relevant to return nothing?
@@ -1,390 +1,390
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.hooks
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Hooks runned by rhodecode
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27 import time
28 28 import binascii
29 29 import traceback
30 30 from inspect import isfunction
31 31
32 32 from mercurial.scmutil import revrange
33 33 from mercurial.node import nullrev
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils import action_logger
37 37 from rhodecode.lib.vcs.backends.base import EmptyChangeset
38 38 from rhodecode.lib.compat import json
39 39 from rhodecode.lib.exceptions import HTTPLockedRC
40 40 from rhodecode.lib.utils2 import safe_str, _extract_extras
41 41 from rhodecode.model.db import Repository, User
42 42
43 43
44 44 def _get_scm_size(alias, root_path):
45 45
46 46 if not alias.startswith('.'):
47 47 alias += '.'
48 48
49 49 size_scm, size_root = 0, 0
50 50 for path, dirs, files in os.walk(safe_str(root_path)):
51 51 if path.find(alias) != -1:
52 52 for f in files:
53 53 try:
54 54 size_scm += os.path.getsize(os.path.join(path, f))
55 55 except OSError:
56 56 pass
57 57 else:
58 58 for f in files:
59 59 try:
60 60 size_root += os.path.getsize(os.path.join(path, f))
61 61 except OSError:
62 62 pass
63 63
64 64 size_scm_f = h.format_byte_size(size_scm)
65 65 size_root_f = h.format_byte_size(size_root)
66 66 size_total_f = h.format_byte_size(size_root + size_scm)
67 67
68 68 return size_scm_f, size_root_f, size_total_f
69 69
70 70
71 71 def repo_size(ui, repo, hooktype=None, **kwargs):
72 72 """
73 73 Presents size of repository after push
74 74
75 75 :param ui:
76 76 :param repo:
77 77 :param hooktype:
78 78 """
79 79
80 80 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
81 81
82 82 last_cs = repo[len(repo) - 1]
83 83
84 84 msg = ('Repository size .hg:%s repo:%s total:%s\n'
85 85 'Last revision is now r%s:%s\n') % (
86 86 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
87 87 )
88 88
89 89 sys.stdout.write(msg)
90 90
91 91
92 92 def pre_push(ui, repo, **kwargs):
93 93 # pre push function, currently used to ban pushing when
94 94 # repository is locked
95 95 ex = _extract_extras()
96 96
97 97 usr = User.get_by_username(ex.username)
98 98 if ex.locked_by[0] and usr.user_id != int(ex.locked_by[0]):
99 99 locked_by = User.get(ex.locked_by[0]).username
100 100 # this exception is interpreted in git/hg middlewares and based
101 101 # on that proper return code is server to client
102 102 _http_ret = HTTPLockedRC(ex.repository, locked_by)
103 103 if str(_http_ret.code).startswith('2'):
104 104 #2xx Codes don't raise exceptions
105 105 sys.stdout.write(_http_ret.title)
106 106 else:
107 107 raise _http_ret
108 108
109 109
110 110 def pre_pull(ui, repo, **kwargs):
111 111 # pre push function, currently used to ban pushing when
112 112 # repository is locked
113 113 ex = _extract_extras()
114 114 if ex.locked_by[0]:
115 115 locked_by = User.get(ex.locked_by[0]).username
116 116 # this exception is interpreted in git/hg middlewares and based
117 117 # on that proper return code is server to client
118 118 _http_ret = HTTPLockedRC(ex.repository, locked_by)
119 119 if str(_http_ret.code).startswith('2'):
120 120 #2xx Codes don't raise exceptions
121 121 sys.stdout.write(_http_ret.title)
122 122 else:
123 123 raise _http_ret
124 124
125 125
126 126 def log_pull_action(ui, repo, **kwargs):
127 127 """
128 128 Logs user last pull action
129 129
130 130 :param ui:
131 131 :param repo:
132 132 """
133 133 ex = _extract_extras()
134 134
135 135 user = User.get_by_username(ex.username)
136 136 action = 'pull'
137 137 action_logger(user, action, ex.repository, ex.ip, commit=True)
138 138 # extension hook call
139 139 from rhodecode import EXTENSIONS
140 140 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
141 141 if isfunction(callback):
142 142 kw = {}
143 143 kw.update(ex)
144 144 callback(**kw)
145 145
146 if ex.make_lock is True:
146 if ex.make_lock:
147 147 Repository.lock(Repository.get_by_repo_name(ex.repository), user.user_id)
148 148 #msg = 'Made lock on repo `%s`' % repository
149 149 #sys.stdout.write(msg)
150 150
151 151 if ex.locked_by[0]:
152 152 locked_by = User.get(ex.locked_by[0]).username
153 153 _http_ret = HTTPLockedRC(ex.repository, locked_by)
154 154 if str(_http_ret.code).startswith('2'):
155 155 #2xx Codes don't raise exceptions
156 156 sys.stdout.write(_http_ret.title)
157 157 return 0
158 158
159 159
160 160 def log_push_action(ui, repo, **kwargs):
161 161 """
162 162 Maps user last push action to new changeset id, from mercurial
163 163
164 164 :param ui:
165 165 :param repo: repo object containing the `ui` object
166 166 """
167 167
168 168 ex = _extract_extras()
169 169
170 170 action = ex.action + ':%s'
171 171
172 172 if ex.scm == 'hg':
173 173 node = kwargs['node']
174 174
175 175 def get_revs(repo, rev_opt):
176 176 if rev_opt:
177 177 revs = revrange(repo, rev_opt)
178 178
179 179 if len(revs) == 0:
180 180 return (nullrev, nullrev)
181 181 return (max(revs), min(revs))
182 182 else:
183 183 return (len(repo) - 1, 0)
184 184
185 185 stop, start = get_revs(repo, [node + ':'])
186 186 h = binascii.hexlify
187 187 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
188 188 elif ex.scm == 'git':
189 189 revs = kwargs.get('_git_revs', [])
190 190 if '_git_revs' in kwargs:
191 191 kwargs.pop('_git_revs')
192 192
193 193 action = action % ','.join(revs)
194 194
195 195 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
196 196
197 197 # extension hook call
198 198 from rhodecode import EXTENSIONS
199 199 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
200 200 if isfunction(callback):
201 201 kw = {'pushed_revs': revs}
202 202 kw.update(ex)
203 203 callback(**kw)
204 204
205 if ex.make_lock is False:
205 if not ex.make_lock:
206 206 Repository.unlock(Repository.get_by_repo_name(ex.repository))
207 207 msg = 'Released lock on repo `%s`\n' % ex.repository
208 208 sys.stdout.write(msg)
209 209
210 210 if ex.locked_by[0]:
211 211 locked_by = User.get(ex.locked_by[0]).username
212 212 _http_ret = HTTPLockedRC(ex.repository, locked_by)
213 213 if str(_http_ret.code).startswith('2'):
214 214 #2xx Codes don't raise exceptions
215 215 sys.stdout.write(_http_ret.title)
216 216
217 217 return 0
218 218
219 219
220 220 def log_create_repository(repository_dict, created_by, **kwargs):
221 221 """
222 222 Post create repository Hook. This is a dummy function for admins to re-use
223 223 if needed. It's taken from rhodecode-extensions module and executed
224 224 if present
225 225
226 226 :param repository: dict dump of repository object
227 227 :param created_by: username who created repository
228 228
229 229 available keys of repository_dict:
230 230
231 231 'repo_type',
232 232 'description',
233 233 'private',
234 234 'created_on',
235 235 'enable_downloads',
236 236 'repo_id',
237 237 'user_id',
238 238 'enable_statistics',
239 239 'clone_uri',
240 240 'fork_id',
241 241 'group_id',
242 242 'repo_name'
243 243
244 244 """
245 245 from rhodecode import EXTENSIONS
246 246 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
247 247 if isfunction(callback):
248 248 kw = {}
249 249 kw.update(repository_dict)
250 250 kw.update({'created_by': created_by})
251 251 kw.update(kwargs)
252 252 return callback(**kw)
253 253
254 254 return 0
255 255
256 256
257 257 def log_delete_repository(repository_dict, deleted_by, **kwargs):
258 258 """
259 259 Post delete repository Hook. This is a dummy function for admins to re-use
260 260 if needed. It's taken from rhodecode-extensions module and executed
261 261 if present
262 262
263 263 :param repository: dict dump of repository object
264 264 :param deleted_by: username who deleted the repository
265 265
266 266 available keys of repository_dict:
267 267
268 268 'repo_type',
269 269 'description',
270 270 'private',
271 271 'created_on',
272 272 'enable_downloads',
273 273 'repo_id',
274 274 'user_id',
275 275 'enable_statistics',
276 276 'clone_uri',
277 277 'fork_id',
278 278 'group_id',
279 279 'repo_name'
280 280
281 281 """
282 282 from rhodecode import EXTENSIONS
283 283 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
284 284 if isfunction(callback):
285 285 kw = {}
286 286 kw.update(repository_dict)
287 287 kw.update({'deleted_by': deleted_by,
288 288 'deleted_on': time.time()})
289 289 kw.update(kwargs)
290 290 return callback(**kw)
291 291
292 292 return 0
293 293
294 294
295 295 handle_git_pre_receive = (lambda repo_path, revs, env:
296 296 handle_git_receive(repo_path, revs, env, hook_type='pre'))
297 297 handle_git_post_receive = (lambda repo_path, revs, env:
298 298 handle_git_receive(repo_path, revs, env, hook_type='post'))
299 299
300 300
301 301 def handle_git_receive(repo_path, revs, env, hook_type='post'):
302 302 """
303 303 A really hacky method that is runned by git post-receive hook and logs
304 304 an push action together with pushed revisions. It's executed by subprocess
305 305 thus needs all info to be able to create a on the fly pylons enviroment,
306 306 connect to database and run the logging code. Hacky as sh*t but works.
307 307
308 308 :param repo_path:
309 309 :type repo_path:
310 310 :param revs:
311 311 :type revs:
312 312 :param env:
313 313 :type env:
314 314 """
315 315 from paste.deploy import appconfig
316 316 from sqlalchemy import engine_from_config
317 317 from rhodecode.config.environment import load_environment
318 318 from rhodecode.model import init_model
319 319 from rhodecode.model.db import RhodeCodeUi
320 320 from rhodecode.lib.utils import make_ui
321 321 extras = _extract_extras(env)
322 322
323 323 path, ini_name = os.path.split(extras['config'])
324 324 conf = appconfig('config:%s' % ini_name, relative_to=path)
325 325 load_environment(conf.global_conf, conf.local_conf)
326 326
327 327 engine = engine_from_config(conf, 'sqlalchemy.db1.')
328 328 init_model(engine)
329 329
330 330 baseui = make_ui('db')
331 331 # fix if it's not a bare repo
332 332 if repo_path.endswith(os.sep + '.git'):
333 333 repo_path = repo_path[:-5]
334 334
335 335 repo = Repository.get_by_full_path(repo_path)
336 336 if not repo:
337 337 raise OSError('Repository %s not found in database'
338 338 % (safe_str(repo_path)))
339 339
340 340 _hooks = dict(baseui.configitems('hooks')) or {}
341 341
342 342 if hook_type == 'pre':
343 343 repo = repo.scm_instance
344 344 else:
345 345 #post push shouldn't use the cached instance never
346 346 repo = repo.scm_instance_no_cache()
347 347
348 348 if hook_type == 'pre':
349 349 pre_push(baseui, repo)
350 350
351 351 # if push hook is enabled via web interface
352 352 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
353 353
354 354 rev_data = []
355 355 for l in revs:
356 356 old_rev, new_rev, ref = l.split(' ')
357 357 _ref_data = ref.split('/')
358 358 if _ref_data[1] in ['tags', 'heads']:
359 359 rev_data.append({'old_rev': old_rev,
360 360 'new_rev': new_rev,
361 361 'ref': ref,
362 362 'type': _ref_data[1],
363 363 'name': _ref_data[2].strip()})
364 364
365 365 git_revs = []
366 366 for push_ref in rev_data:
367 367 _type = push_ref['type']
368 368 if _type == 'heads':
369 369 if push_ref['old_rev'] == EmptyChangeset().raw_id:
370 370 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
371 371 heads = repo.run_git_command(cmd)[0]
372 372 heads = heads.replace(push_ref['ref'], '')
373 373 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
374 374 heads.splitlines()))
375 375 cmd = (('log %(new_rev)s' % push_ref) +
376 376 ' --reverse --pretty=format:"%H" --not ' + heads)
377 377 git_revs += repo.run_git_command(cmd)[0].splitlines()
378 378
379 379 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
380 380 #delete branch case
381 381 git_revs += ['delete_branch=>%s' % push_ref['name']]
382 382 else:
383 383 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
384 384 ' --reverse --pretty=format:"%H"')
385 385 git_revs += repo.run_git_command(cmd)[0].splitlines()
386 386
387 387 elif _type == 'tags':
388 388 git_revs += ['tag=>%s' % push_ref['name']]
389 389
390 390 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,336 +1,336
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33 from dulwich.web import LimitedInputFilter, GunzipFilter
34 34 from rhodecode.lib.exceptions import HTTPLockedRC
35 35 from rhodecode.lib.hooks import pre_pull
36 36
37 37
38 38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
39 39
40 40 def handle(self):
41 41 write = lambda x: self.proto.write_sideband(1, x)
42 42
43 43 graph_walker = dulserver.ProtocolGraphWalker(self,
44 44 self.repo.object_store,
45 45 self.repo.get_peeled)
46 46 objects_iter = self.repo.fetch_objects(
47 47 graph_walker.determine_wants, graph_walker, self.progress,
48 48 get_tagged=self.get_tagged)
49 49
50 50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
51 51 # that the client still expects a 0-object pack in most cases.
52 52 if objects_iter is None:
53 53 return
54 54
55 55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
56 56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
57 57 objects_iter)
58 58 messages = []
59 59 messages.append('thank you for using rhodecode')
60 60
61 61 for msg in messages:
62 62 self.progress(msg + "\n")
63 63 # we are done
64 64 self.proto.write("0000")
65 65
66 66
67 67 dulserver.DEFAULT_HANDLERS = {
68 68 #git-ls-remote, git-clone, git-fetch and git-pull
69 69 'git-upload-pack': SimpleGitUploadPackHandler,
70 70 #git-push
71 71 'git-receive-pack': dulserver.ReceivePackHandler,
72 72 }
73 73
74 74 # not used for now until dulwich get's fixed
75 75 #from dulwich.repo import Repo
76 76 #from dulwich.web import make_wsgi_chain
77 77
78 78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
79 79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
80 80 HTTPBadRequest, HTTPNotAcceptable
81 81
82 82 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
83 83 _set_extras
84 84 from rhodecode.lib.base import BaseVCSController
85 85 from rhodecode.lib.auth import get_container_username
86 86 from rhodecode.lib.utils import is_valid_repo, make_ui
87 87 from rhodecode.lib.compat import json
88 88 from rhodecode.model.db import User, RhodeCodeUi
89 89
90 90 log = logging.getLogger(__name__)
91 91
92 92
93 93 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
94 94
95 95
96 96 def is_git(environ):
97 97 path_info = environ['PATH_INFO']
98 98 isgit_path = GIT_PROTO_PAT.match(path_info)
99 99 log.debug('pathinfo: %s detected as GIT %s' % (
100 100 path_info, isgit_path != None)
101 101 )
102 102 return isgit_path
103 103
104 104
105 105 class SimpleGit(BaseVCSController):
106 106
107 107 def _handle_request(self, environ, start_response):
108 108 if not is_git(environ):
109 109 return self.application(environ, start_response)
110 110 if not self._check_ssl(environ, start_response):
111 111 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
112 112
113 113 ip_addr = self._get_ip_addr(environ)
114 114 username = None
115 115 self._git_first_op = False
116 116 # skip passing error to error controller
117 117 environ['pylons.status_code_redirect'] = True
118 118
119 119 #======================================================================
120 120 # EXTRACT REPOSITORY NAME FROM ENV
121 121 #======================================================================
122 122 try:
123 123 repo_name = self.__get_repository(environ)
124 124 log.debug('Extracted repo name is %s' % repo_name)
125 125 except:
126 126 return HTTPInternalServerError()(environ, start_response)
127 127
128 128 # quick check if that dir exists...
129 if is_valid_repo(repo_name, self.basepath, 'git') is False:
129 if not is_valid_repo(repo_name, self.basepath, 'git'):
130 130 return HTTPNotFound()(environ, start_response)
131 131
132 132 #======================================================================
133 133 # GET ACTION PULL or PUSH
134 134 #======================================================================
135 135 action = self.__get_action(environ)
136 136
137 137 #======================================================================
138 138 # CHECK ANONYMOUS PERMISSION
139 139 #======================================================================
140 140 if action in ['pull', 'push']:
141 141 anonymous_user = self.__get_user('default')
142 142 username = anonymous_user.username
143 143 anonymous_perm = self._check_permission(action, anonymous_user,
144 144 repo_name, ip_addr)
145 145
146 if anonymous_perm is not True or anonymous_user.active is False:
147 if anonymous_perm is not True:
146 if not anonymous_perm or not anonymous_user.active:
147 if not anonymous_perm:
148 148 log.debug('Not enough credentials to access this '
149 149 'repository as anonymous user')
150 if anonymous_user.active is False:
150 if not anonymous_user.active:
151 151 log.debug('Anonymous access is disabled, running '
152 152 'authentication')
153 153 #==============================================================
154 154 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
155 155 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
156 156 #==============================================================
157 157
158 158 # Attempting to retrieve username from the container
159 159 username = get_container_username(environ, self.config)
160 160
161 161 # If not authenticated by the container, running basic auth
162 162 if not username:
163 163 self.authenticate.realm = \
164 164 safe_str(self.config['rhodecode_realm'])
165 165 result = self.authenticate(environ)
166 166 if isinstance(result, str):
167 167 AUTH_TYPE.update(environ, 'basic')
168 168 REMOTE_USER.update(environ, result)
169 169 username = result
170 170 else:
171 171 return result.wsgi_application(environ, start_response)
172 172
173 173 #==============================================================
174 174 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
175 175 #==============================================================
176 176 try:
177 177 user = self.__get_user(username)
178 178 if user is None or not user.active:
179 179 return HTTPForbidden()(environ, start_response)
180 180 username = user.username
181 181 except:
182 182 log.error(traceback.format_exc())
183 183 return HTTPInternalServerError()(environ, start_response)
184 184
185 185 #check permissions for this repository
186 186 perm = self._check_permission(action, user, repo_name, ip_addr)
187 if perm is not True:
187 if not perm:
188 188 return HTTPForbidden()(environ, start_response)
189 189
190 190 # extras are injected into UI object and later available
191 191 # in hooks executed by rhodecode
192 192 from rhodecode import CONFIG
193 193 server_url = get_server_url(environ)
194 194 extras = {
195 195 'ip': ip_addr,
196 196 'username': username,
197 197 'action': action,
198 198 'repository': repo_name,
199 199 'scm': 'git',
200 200 'config': CONFIG['__file__'],
201 201 'server_url': server_url,
202 202 'make_lock': None,
203 203 'locked_by': [None, None]
204 204 }
205 205
206 206 #===================================================================
207 207 # GIT REQUEST HANDLING
208 208 #===================================================================
209 209 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
210 210 log.debug('Repository path is %s' % repo_path)
211 211
212 212 # CHECK LOCKING only if it's not ANONYMOUS USER
213 213 if username != User.DEFAULT_USER:
214 214 log.debug('Checking locking on repository')
215 215 (make_lock,
216 216 locked,
217 217 locked_by) = self._check_locking_state(
218 218 environ=environ, action=action,
219 219 repo=repo_name, user_id=user.user_id
220 220 )
221 221 # store the make_lock for later evaluation in hooks
222 222 extras.update({'make_lock': make_lock,
223 223 'locked_by': locked_by})
224 224 # set the environ variables for this request
225 225 os.environ['RC_SCM_DATA'] = json.dumps(extras)
226 226 fix_PATH()
227 227 log.debug('HOOKS extras is %s' % extras)
228 228 baseui = make_ui('db')
229 229 self.__inject_extras(repo_path, baseui, extras)
230 230
231 231 try:
232 232 self._handle_githooks(repo_name, action, baseui, environ)
233 233 log.info('%s action on GIT repo "%s" by "%s" from %s' %
234 234 (action, repo_name, username, ip_addr))
235 235 app = self.__make_app(repo_name, repo_path, extras)
236 236 return app(environ, start_response)
237 237 except HTTPLockedRC, e:
238 238 _code = CONFIG.get('lock_ret_code')
239 239 log.debug('Repository LOCKED ret code %s!' % (_code))
240 240 return e(environ, start_response)
241 241 except Exception:
242 242 log.error(traceback.format_exc())
243 243 return HTTPInternalServerError()(environ, start_response)
244 244 finally:
245 245 # invalidate cache on push
246 246 if action == 'push':
247 247 self._invalidate_cache(repo_name)
248 248
249 249 def __make_app(self, repo_name, repo_path, extras):
250 250 """
251 251 Make an wsgi application using dulserver
252 252
253 253 :param repo_name: name of the repository
254 254 :param repo_path: full path to the repository
255 255 """
256 256
257 257 from rhodecode.lib.middleware.pygrack import make_wsgi_app
258 258 app = make_wsgi_app(
259 259 repo_root=safe_str(self.basepath),
260 260 repo_name=repo_name,
261 261 extras=extras,
262 262 )
263 263 app = GunzipFilter(LimitedInputFilter(app))
264 264 return app
265 265
266 266 def __get_repository(self, environ):
267 267 """
268 268 Get's repository name out of PATH_INFO header
269 269
270 270 :param environ: environ where PATH_INFO is stored
271 271 """
272 272 try:
273 273 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
274 274 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
275 275 except:
276 276 log.error(traceback.format_exc())
277 277 raise
278 278
279 279 return repo_name
280 280
281 281 def __get_user(self, username):
282 282 return User.get_by_username(username)
283 283
284 284 def __get_action(self, environ):
285 285 """
286 286 Maps git request commands into a pull or push command.
287 287
288 288 :param environ:
289 289 """
290 290 service = environ['QUERY_STRING'].split('=')
291 291
292 292 if len(service) > 1:
293 293 service_cmd = service[1]
294 294 mapping = {
295 295 'git-receive-pack': 'push',
296 296 'git-upload-pack': 'pull',
297 297 }
298 298 op = mapping[service_cmd]
299 299 self._git_stored_op = op
300 300 return op
301 301 else:
302 302 # try to fallback to stored variable as we don't know if the last
303 303 # operation is pull/push
304 304 op = getattr(self, '_git_stored_op', 'pull')
305 305 return op
306 306
307 307 def _handle_githooks(self, repo_name, action, baseui, environ):
308 308 """
309 309 Handles pull action, push is handled by post-receive hook
310 310 """
311 311 from rhodecode.lib.hooks import log_pull_action
312 312 service = environ['QUERY_STRING'].split('=')
313 313
314 314 if len(service) < 2:
315 315 return
316 316
317 317 from rhodecode.model.db import Repository
318 318 _repo = Repository.get_by_repo_name(repo_name)
319 319 _repo = _repo.scm_instance
320 320
321 321 _hooks = dict(baseui.configitems('hooks')) or {}
322 322 if action == 'pull':
323 323 # stupid git, emulate pre-pull hook !
324 324 pre_pull(ui=baseui, repo=_repo._repo)
325 325 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
326 326 log_pull_action(ui=baseui, repo=_repo._repo)
327 327
328 328 def __inject_extras(self, repo_path, baseui, extras={}):
329 329 """
330 330 Injects some extra params into baseui instance
331 331
332 332 :param baseui: baseui instance
333 333 :param extras: dict with extra params to put into baseui
334 334 """
335 335
336 336 _set_extras(extras)
@@ -1,287 +1,287
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from mercurial.error import RepoError
32 32 from mercurial.hgweb import hgweb_mod
33 33
34 34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 35 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
36 36 HTTPBadRequest, HTTPNotAcceptable
37 37
38 38 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
39 39 _set_extras
40 40 from rhodecode.lib.base import BaseVCSController
41 41 from rhodecode.lib.auth import get_container_username
42 42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
43 43 from rhodecode.lib.compat import json
44 44 from rhodecode.model.db import User
45 45 from rhodecode.lib.exceptions import HTTPLockedRC
46 46
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 def is_mercurial(environ):
52 52 """
53 53 Returns True if request's target is mercurial server - header
54 54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
55 55 """
56 56 http_accept = environ.get('HTTP_ACCEPT')
57 57 path_info = environ['PATH_INFO']
58 58 if http_accept and http_accept.startswith('application/mercurial'):
59 59 ishg_path = True
60 60 else:
61 61 ishg_path = False
62 62
63 63 log.debug('pathinfo: %s detected as HG %s' % (
64 64 path_info, ishg_path)
65 65 )
66 66 return ishg_path
67 67
68 68
69 69 class SimpleHg(BaseVCSController):
70 70
71 71 def _handle_request(self, environ, start_response):
72 72 if not is_mercurial(environ):
73 73 return self.application(environ, start_response)
74 74 if not self._check_ssl(environ, start_response):
75 75 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
76 76
77 77 ip_addr = self._get_ip_addr(environ)
78 78 username = None
79 79 # skip passing error to error controller
80 80 environ['pylons.status_code_redirect'] = True
81 81
82 82 #======================================================================
83 83 # EXTRACT REPOSITORY NAME FROM ENV
84 84 #======================================================================
85 85 try:
86 86 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
87 87 log.debug('Extracted repo name is %s' % repo_name)
88 88 except:
89 89 return HTTPInternalServerError()(environ, start_response)
90 90
91 91 # quick check if that dir exists...
92 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
92 if not is_valid_repo(repo_name, self.basepath, 'hg'):
93 93 return HTTPNotFound()(environ, start_response)
94 94
95 95 #======================================================================
96 96 # GET ACTION PULL or PUSH
97 97 #======================================================================
98 98 action = self.__get_action(environ)
99 99
100 100 #======================================================================
101 101 # CHECK ANONYMOUS PERMISSION
102 102 #======================================================================
103 103 if action in ['pull', 'push']:
104 104 anonymous_user = self.__get_user('default')
105 105 username = anonymous_user.username
106 106 anonymous_perm = self._check_permission(action, anonymous_user,
107 107 repo_name, ip_addr)
108 108
109 if anonymous_perm is not True or anonymous_user.active is False:
110 if anonymous_perm is not True:
109 if not anonymous_perm or not anonymous_user.active:
110 if not anonymous_perm:
111 111 log.debug('Not enough credentials to access this '
112 112 'repository as anonymous user')
113 if anonymous_user.active is False:
113 if not anonymous_user.active:
114 114 log.debug('Anonymous access is disabled, running '
115 115 'authentication')
116 116 #==============================================================
117 117 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
118 118 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
119 119 #==============================================================
120 120
121 121 # Attempting to retrieve username from the container
122 122 username = get_container_username(environ, self.config)
123 123
124 124 # If not authenticated by the container, running basic auth
125 125 if not username:
126 126 self.authenticate.realm = \
127 127 safe_str(self.config['rhodecode_realm'])
128 128 result = self.authenticate(environ)
129 129 if isinstance(result, str):
130 130 AUTH_TYPE.update(environ, 'basic')
131 131 REMOTE_USER.update(environ, result)
132 132 username = result
133 133 else:
134 134 return result.wsgi_application(environ, start_response)
135 135
136 136 #==============================================================
137 137 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
138 138 #==============================================================
139 139 try:
140 140 user = self.__get_user(username)
141 141 if user is None or not user.active:
142 142 return HTTPForbidden()(environ, start_response)
143 143 username = user.username
144 144 except:
145 145 log.error(traceback.format_exc())
146 146 return HTTPInternalServerError()(environ, start_response)
147 147
148 148 #check permissions for this repository
149 149 perm = self._check_permission(action, user, repo_name, ip_addr)
150 if perm is not True:
150 if not perm:
151 151 return HTTPForbidden()(environ, start_response)
152 152
153 153 # extras are injected into mercurial UI object and later available
154 154 # in hg hooks executed by rhodecode
155 155 from rhodecode import CONFIG
156 156 server_url = get_server_url(environ)
157 157 extras = {
158 158 'ip': ip_addr,
159 159 'username': username,
160 160 'action': action,
161 161 'repository': repo_name,
162 162 'scm': 'hg',
163 163 'config': CONFIG['__file__'],
164 164 'server_url': server_url,
165 165 'make_lock': None,
166 166 'locked_by': [None, None]
167 167 }
168 168 #======================================================================
169 169 # MERCURIAL REQUEST HANDLING
170 170 #======================================================================
171 171 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
172 172 log.debug('Repository path is %s' % repo_path)
173 173
174 174 # CHECK LOCKING only if it's not ANONYMOUS USER
175 175 if username != User.DEFAULT_USER:
176 176 log.debug('Checking locking on repository')
177 177 (make_lock,
178 178 locked,
179 179 locked_by) = self._check_locking_state(
180 180 environ=environ, action=action,
181 181 repo=repo_name, user_id=user.user_id
182 182 )
183 183 # store the make_lock for later evaluation in hooks
184 184 extras.update({'make_lock': make_lock,
185 185 'locked_by': locked_by})
186 186
187 187 # set the environ variables for this request
188 188 os.environ['RC_SCM_DATA'] = json.dumps(extras)
189 189 fix_PATH()
190 190 log.debug('HOOKS extras is %s' % extras)
191 191 baseui = make_ui('db')
192 192 self.__inject_extras(repo_path, baseui, extras)
193 193
194 194 try:
195 195 log.info('%s action on HG repo "%s" by "%s" from %s' %
196 196 (action, repo_name, username, ip_addr))
197 197 app = self.__make_app(repo_path, baseui, extras)
198 198 return app(environ, start_response)
199 199 except RepoError, e:
200 200 if str(e).find('not found') != -1:
201 201 return HTTPNotFound()(environ, start_response)
202 202 except HTTPLockedRC, e:
203 203 _code = CONFIG.get('lock_ret_code')
204 204 log.debug('Repository LOCKED ret code %s!' % (_code))
205 205 return e(environ, start_response)
206 206 except Exception:
207 207 log.error(traceback.format_exc())
208 208 return HTTPInternalServerError()(environ, start_response)
209 209 finally:
210 210 # invalidate cache on push
211 211 if action == 'push':
212 212 self._invalidate_cache(repo_name)
213 213
214 214 def __make_app(self, repo_name, baseui, extras):
215 215 """
216 216 Make an wsgi application using hgweb, and inject generated baseui
217 217 instance, additionally inject some extras into ui object
218 218 """
219 219 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
220 220
221 221 def __get_repository(self, environ):
222 222 """
223 223 Get's repository name out of PATH_INFO header
224 224
225 225 :param environ: environ where PATH_INFO is stored
226 226 """
227 227 try:
228 228 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
229 229 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
230 230 if repo_name.endswith('/'):
231 231 repo_name = repo_name.rstrip('/')
232 232 except:
233 233 log.error(traceback.format_exc())
234 234 raise
235 235
236 236 return repo_name
237 237
238 238 def __get_user(self, username):
239 239 return User.get_by_username(username)
240 240
241 241 def __get_action(self, environ):
242 242 """
243 243 Maps mercurial request commands into a clone,pull or push command.
244 244 This should always return a valid command string
245 245
246 246 :param environ:
247 247 """
248 248 mapping = {'changegroup': 'pull',
249 249 'changegroupsubset': 'pull',
250 250 'stream_out': 'pull',
251 251 'listkeys': 'pull',
252 252 'unbundle': 'push',
253 253 'pushkey': 'push', }
254 254 for qry in environ['QUERY_STRING'].split('&'):
255 255 if qry.startswith('cmd'):
256 256 cmd = qry.split('=')[-1]
257 257 if cmd in mapping:
258 258 return mapping[cmd]
259 259
260 260 return 'pull'
261 261
262 262 raise Exception('Unable to detect pull/push action !!'
263 263 'Are you using non standard command or client ?')
264 264
265 265 def __inject_extras(self, repo_path, baseui, extras={}):
266 266 """
267 267 Injects some extra params into baseui instance
268 268
269 269 also overwrites global settings with those takes from local hgrc file
270 270
271 271 :param baseui: baseui instance
272 272 :param extras: dict with extra params to put into baseui
273 273 """
274 274
275 275 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
276 276
277 277 # make our hgweb quiet so it doesn't print output
278 278 baseui.setconfig('ui', 'quiet', 'true')
279 279
280 280 repoui = make_ui('file', hgrc, False)
281 281
282 282 if repoui:
283 283 #overwrite our ui instance with the section from hgrc file
284 284 for section in ui_sections:
285 285 for k, v in repoui.configitems(section):
286 286 baseui.setconfig(section, k, v)
287 287 _set_extras(extras)
@@ -1,453 +1,453
1 1 # The code in this module is entirely lifted from the Lamson project
2 2 # (http://lamsonproject.org/). Its copyright is:
3 3
4 4 # Copyright (c) 2008, Zed A. Shaw
5 5 # All rights reserved.
6 6
7 7 # It is provided under this license:
8 8
9 9 # Redistribution and use in source and binary forms, with or without
10 10 # modification, are permitted provided that the following conditions are met:
11 11
12 12 # * Redistributions of source code must retain the above copyright notice, this
13 13 # list of conditions and the following disclaimer.
14 14
15 15 # * Redistributions in binary form must reproduce the above copyright notice,
16 16 # this list of conditions and the following disclaimer in the documentation
17 17 # and/or other materials provided with the distribution.
18 18
19 19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 20 # be used to endorse or promote products derived from this software without
21 21 # specific prior written permission.
22 22
23 23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 34 # POSSIBILITY OF SUCH DAMAGE.
35 35
36 36 import os
37 37 import mimetypes
38 38 import string
39 39 from email import encoders
40 40 from email.charset import Charset
41 41 from email.utils import parseaddr
42 42 from email.mime.base import MIMEBase
43 43
44 44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
45 45 DEFAULT_ENCODING = "utf-8"
46 46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
47 47
48 48
49 49 def normalize_header(header):
50 50 return string.capwords(header.lower(), '-')
51 51
52 52
53 53 class EncodingError(Exception):
54 54 """Thrown when there is an encoding error."""
55 55 pass
56 56
57 57
58 58 class MailBase(object):
59 59 """MailBase is used as the basis of lamson.mail and contains the basics of
60 60 encoding an email. You actually can do all your email processing with this
61 61 class, but it's more raw.
62 62 """
63 63 def __init__(self, items=()):
64 64 self.headers = dict(items)
65 65 self.parts = []
66 66 self.body = None
67 67 self.content_encoding = {'Content-Type': (None, {}),
68 68 'Content-Disposition': (None, {}),
69 69 'Content-Transfer-Encoding': (None, {})}
70 70
71 71 def __getitem__(self, key):
72 72 return self.headers.get(normalize_header(key), None)
73 73
74 74 def __len__(self):
75 75 return len(self.headers)
76 76
77 77 def __iter__(self):
78 78 return iter(self.headers)
79 79
80 80 def __contains__(self, key):
81 81 return normalize_header(key) in self.headers
82 82
83 83 def __setitem__(self, key, value):
84 84 self.headers[normalize_header(key)] = value
85 85
86 86 def __delitem__(self, key):
87 87 del self.headers[normalize_header(key)]
88 88
89 89 def __nonzero__(self):
90 90 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
91 91
92 92 def keys(self):
93 93 """Returns the sorted keys."""
94 94 return sorted(self.headers.keys())
95 95
96 96 def attach_file(self, filename, data, ctype, disposition):
97 97 """
98 98 A file attachment is a raw attachment with a disposition that
99 99 indicates the file name.
100 100 """
101 101 assert filename, "You can't attach a file without a filename."
102 102 ctype = ctype.lower()
103 103
104 104 part = MailBase()
105 105 part.body = data
106 106 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
107 107 part.content_encoding['Content-Disposition'] = (disposition,
108 108 {'filename': filename})
109 109 self.parts.append(part)
110 110
111 111 def attach_text(self, data, ctype):
112 112 """
113 113 This attaches a simpler text encoded part, which doesn't have a
114 114 filename.
115 115 """
116 116 ctype = ctype.lower()
117 117
118 118 part = MailBase()
119 119 part.body = data
120 120 part.content_encoding['Content-Type'] = (ctype, {})
121 121 self.parts.append(part)
122 122
123 123 def walk(self):
124 124 for p in self.parts:
125 125 yield p
126 126 for x in p.walk():
127 127 yield x
128 128
129 129
130 130 class MailResponse(object):
131 131 """
132 132 You are given MailResponse objects from the lamson.view methods, and
133 133 whenever you want to generate an email to send to someone. It has the
134 134 same basic functionality as MailRequest, but it is designed to be written
135 135 to, rather than read from (although you can do both).
136 136
137 137 You can easily set a Body or Html during creation or after by passing it
138 138 as __init__ parameters, or by setting those attributes.
139 139
140 140 You can initially set the From, To, and Subject, but they are headers so
141 141 use the dict notation to change them: msg['From'] = 'joe@test.com'.
142 142
143 143 The message is not fully crafted until right when you convert it with
144 144 MailResponse.to_message. This lets you change it and work with it, then
145 145 send it out when it's ready.
146 146 """
147 147 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
148 148 separator="; "):
149 149 self.Body = Body
150 150 self.Html = Html
151 151 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
152 152 self.multipart = self.Body and self.Html
153 153 self.attachments = []
154 154 self.separator = separator
155 155
156 156 def __contains__(self, key):
157 157 return self.base.__contains__(key)
158 158
159 159 def __getitem__(self, key):
160 160 return self.base.__getitem__(key)
161 161
162 162 def __setitem__(self, key, val):
163 163 return self.base.__setitem__(key, val)
164 164
165 165 def __delitem__(self, name):
166 166 del self.base[name]
167 167
168 168 def attach(self, filename=None, content_type=None, data=None,
169 169 disposition=None):
170 170 """
171 171
172 172 Simplifies attaching files from disk or data as files. To attach
173 173 simple text simple give data and a content_type. To attach a file,
174 174 give the data/content_type/filename/disposition combination.
175 175
176 176 For convenience, if you don't give data and only a filename, then it
177 177 will read that file's contents when you call to_message() later. If
178 178 you give data and filename then it will assume you've filled data
179 179 with what the file's contents are and filename is just the name to
180 180 use.
181 181 """
182 182
183 183 assert filename or data, ("You must give a filename or some data to "
184 184 "attach.")
185 185 assert data or os.path.exists(filename), ("File doesn't exist, and no "
186 186 "data given.")
187 187
188 188 self.multipart = True
189 189
190 190 if filename and not content_type:
191 191 content_type, encoding = mimetypes.guess_type(filename)
192 192
193 193 assert content_type, ("No content type given, and couldn't guess "
194 194 "from the filename: %r" % filename)
195 195
196 196 self.attachments.append({'filename': filename,
197 197 'content_type': content_type,
198 198 'data': data,
199 199 'disposition': disposition,})
200 200
201 201 def attach_part(self, part):
202 202 """
203 203 Attaches a raw MailBase part from a MailRequest (or anywhere)
204 204 so that you can copy it over.
205 205 """
206 206 self.multipart = True
207 207
208 208 self.attachments.append({'filename': None,
209 209 'content_type': None,
210 210 'data': None,
211 211 'disposition': None,
212 212 'part': part,
213 213 })
214 214
215 215 def attach_all_parts(self, mail_request):
216 216 """
217 217 Used for copying the attachment parts of a mail.MailRequest
218 218 object for mailing lists that need to maintain attachments.
219 219 """
220 220 for part in mail_request.all_parts():
221 221 self.attach_part(part)
222 222
223 223 self.base.content_encoding = mail_request.base.content_encoding.copy()
224 224
225 225 def clear(self):
226 226 """
227 227 Clears out the attachments so you can redo them. Use this to keep the
228 228 headers for a series of different messages with different attachments.
229 229 """
230 230 del self.attachments[:]
231 231 del self.base.parts[:]
232 232 self.multipart = False
233 233
234 234 def update(self, message):
235 235 """
236 236 Used to easily set a bunch of heading from another dict
237 237 like object.
238 238 """
239 239 for k in message.keys():
240 240 self.base[k] = message[k]
241 241
242 242 def __str__(self):
243 243 """
244 244 Converts to a string.
245 245 """
246 246 return self.to_message().as_string()
247 247
248 248 def _encode_attachment(self, filename=None, content_type=None, data=None,
249 249 disposition=None, part=None):
250 250 """
251 251 Used internally to take the attachments mentioned in self.attachments
252 252 and do the actual encoding in a lazy way when you call to_message.
253 253 """
254 254 if part:
255 255 self.base.parts.append(part)
256 256 elif filename:
257 257 if not data:
258 258 data = open(filename).read()
259 259
260 260 self.base.attach_file(filename, data, content_type,
261 261 disposition or 'attachment')
262 262 else:
263 263 self.base.attach_text(data, content_type)
264 264
265 265 ctype = self.base.content_encoding['Content-Type'][0]
266 266
267 267 if ctype and not ctype.startswith('multipart'):
268 268 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
269 269
270 270 def to_message(self):
271 271 """
272 272 Figures out all the required steps to finally craft the
273 273 message you need and return it. The resulting message
274 274 is also available as a self.base attribute.
275 275
276 276 What is returned is a Python email API message you can
277 277 use with those APIs. The self.base attribute is the raw
278 278 lamson.encoding.MailBase.
279 279 """
280 280 del self.base.parts[:]
281 281
282 282 if self.Body and self.Html:
283 283 self.multipart = True
284 284 self.base.content_encoding['Content-Type'] = (
285 285 'multipart/alternative', {})
286 286
287 287 if self.multipart:
288 288 self.base.body = None
289 289 if self.Body:
290 290 self.base.attach_text(self.Body, 'text/plain')
291 291
292 292 if self.Html:
293 293 self.base.attach_text(self.Html, 'text/html')
294 294
295 295 for args in self.attachments:
296 296 self._encode_attachment(**args)
297 297
298 298 elif self.Body:
299 299 self.base.body = self.Body
300 300 self.base.content_encoding['Content-Type'] = ('text/plain', {})
301 301
302 302 elif self.Html:
303 303 self.base.body = self.Html
304 304 self.base.content_encoding['Content-Type'] = ('text/html', {})
305 305
306 306 return to_message(self.base, separator=self.separator)
307 307
308 308 def all_parts(self):
309 309 """
310 310 Returns all the encoded parts. Only useful for debugging
311 311 or inspecting after calling to_message().
312 312 """
313 313 return self.base.parts
314 314
315 315 def keys(self):
316 316 return self.base.keys()
317 317
318 318
319 319 def to_message(mail, separator="; "):
320 320 """
321 321 Given a MailBase message, this will construct a MIMEPart
322 322 that is canonicalized for use with the Python email API.
323 323 """
324 324 ctype, params = mail.content_encoding['Content-Type']
325 325
326 326 if not ctype:
327 327 if mail.parts:
328 328 ctype = 'multipart/mixed'
329 329 else:
330 330 ctype = 'text/plain'
331 331 else:
332 332 if mail.parts:
333 333 assert ctype.startswith(("multipart", "message")), \
334 334 "Content type should be multipart or message, not %r" % ctype
335 335
336 336 # adjust the content type according to what it should be now
337 337 mail.content_encoding['Content-Type'] = (ctype, params)
338 338
339 339 try:
340 340 out = MIMEPart(ctype, **params)
341 341 except TypeError, exc: # pragma: no cover
342 342 raise EncodingError("Content-Type malformed, not allowed: %r; "
343 343 "%r (Python ERROR: %s" %
344 344 (ctype, params, exc.message))
345 345
346 346 for k in mail.keys():
347 347 if k in ADDRESS_HEADERS_WHITELIST:
348 348 out[k.encode('ascii')] = header_to_mime_encoding(
349 349 mail[k],
350 350 not_email=False,
351 351 separator=separator
352 352 )
353 353 else:
354 354 out[k.encode('ascii')] = header_to_mime_encoding(
355 355 mail[k],
356 356 not_email=True
357 357 )
358 358
359 359 out.extract_payload(mail)
360 360
361 361 # go through the children
362 362 for part in mail.parts:
363 363 out.attach(to_message(part))
364 364
365 365 return out
366 366
367 367
368 368 class MIMEPart(MIMEBase):
369 369 """
370 370 A reimplementation of nearly everything in email.mime to be more useful
371 371 for actually attaching things. Rather than one class for every type of
372 372 thing you'd encode, there's just this one, and it figures out how to
373 373 encode what you ask it.
374 374 """
375 375 def __init__(self, type, **params):
376 376 self.maintype, self.subtype = type.split('/')
377 377 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
378 378
379 379 def add_text(self, content):
380 380 # this is text, so encode it in canonical form
381 381 try:
382 382 encoded = content.encode('ascii')
383 383 charset = 'ascii'
384 384 except UnicodeError:
385 385 encoded = content.encode('utf-8')
386 386 charset = 'utf-8'
387 387
388 388 self.set_payload(encoded, charset=charset)
389 389
390 390 def extract_payload(self, mail):
391 391 if mail.body == None:
392 392 return # only None, '' is still ok
393 393
394 394 ctype, ctype_params = mail.content_encoding['Content-Type']
395 395 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
396 396
397 397 assert ctype, ("Extract payload requires that mail.content_encoding "
398 398 "have a valid Content-Type.")
399 399
400 400 if ctype.startswith("text/"):
401 401 self.add_text(mail.body)
402 402 else:
403 403 if cdisp:
404 404 # replicate the content-disposition settings
405 405 self.add_header('Content-Disposition', cdisp, **cdisp_params)
406 406
407 407 self.set_payload(mail.body)
408 408 encoders.encode_base64(self)
409 409
410 410 def __repr__(self):
411 411 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
412 412 self.subtype,
413 413 self.maintype,
414 414 self['Content-Type'],
415 415 self['Content-Disposition'],
416 416 self.is_multipart())
417 417
418 418
419 419 def header_to_mime_encoding(value, not_email=False, separator=", "):
420 420 if not value:
421 421 return ""
422 422
423 423 encoder = Charset(DEFAULT_ENCODING)
424 424 if type(value) == list:
425 425 return separator.join(properly_encode_header(
426 426 v, encoder, not_email) for v in value)
427 427 else:
428 428 return properly_encode_header(value, encoder, not_email)
429 429
430 430
431 431 def properly_encode_header(value, encoder, not_email):
432 432 """
433 433 The only thing special (weird) about this function is that it tries
434 434 to do a fast check to see if the header value has an email address in
435 435 it. Since random headers could have an email address, and email addresses
436 436 have weird special formatting rules, we have to check for it.
437 437
438 438 Normally this works fine, but in Librelist, we need to "obfuscate" email
439 439 addresses by changing the '@' to '-AT-'. This is where
440 440 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
441 441 to check if a header value has an email address. If you need to make this
442 442 check different, then change this.
443 443 """
444 444 try:
445 445 return value.encode("ascii")
446 446 except UnicodeEncodeError:
447 if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
447 if not not_email and VALUE_IS_EMAIL_ADDRESS(value):
448 448 # this could have an email address, make sure we don't screw it up
449 449 name, address = parseaddr(value)
450 450 return '"%s" <%s>' % (
451 451 encoder.header_encode(name.encode("utf-8")), address)
452 452
453 453 return encoder.header_encode(value.encode("utf-8"))
@@ -1,2039 +1,2039
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 return cls.query().all()
135 135
136 136 @classmethod
137 137 def delete(cls, id_):
138 138 obj = cls.query().get(id_)
139 139 Session().delete(obj)
140 140
141 141 def __repr__(self):
142 142 if hasattr(self, '__unicode__'):
143 143 # python repr needs to return str
144 144 return safe_str(self.__unicode__())
145 145 return '<DB:%s>' % (self.__class__.__name__)
146 146
147 147
148 148 class RhodeCodeSetting(Base, BaseModel):
149 149 __tablename__ = 'rhodecode_settings'
150 150 __table_args__ = (
151 151 UniqueConstraint('app_settings_name'),
152 152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
153 153 'mysql_charset': 'utf8'}
154 154 )
155 155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 156 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
158 158
159 159 def __init__(self, k='', v=''):
160 160 self.app_settings_name = k
161 161 self.app_settings_value = v
162 162
163 163 @validates('_app_settings_value')
164 164 def validate_settings_value(self, key, val):
165 165 assert type(val) == unicode
166 166 return val
167 167
168 168 @hybrid_property
169 169 def app_settings_value(self):
170 170 v = self._app_settings_value
171 171 if self.app_settings_name in ["ldap_active",
172 172 "default_repo_enable_statistics",
173 173 "default_repo_enable_locking",
174 174 "default_repo_private",
175 175 "default_repo_enable_downloads"]:
176 176 v = str2bool(v)
177 177 return v
178 178
179 179 @app_settings_value.setter
180 180 def app_settings_value(self, val):
181 181 """
182 182 Setter that will always make sure we use unicode in app_settings_value
183 183
184 184 :param val:
185 185 """
186 186 self._app_settings_value = safe_unicode(val)
187 187
188 188 def __unicode__(self):
189 189 return u"<%s('%s:%s')>" % (
190 190 self.__class__.__name__,
191 191 self.app_settings_name, self.app_settings_value
192 192 )
193 193
194 194 @classmethod
195 195 def get_by_name(cls, key):
196 196 return cls.query()\
197 197 .filter(cls.app_settings_name == key).scalar()
198 198
199 199 @classmethod
200 200 def get_by_name_or_create(cls, key):
201 201 res = cls.get_by_name(key)
202 202 if not res:
203 203 res = cls(key)
204 204 return res
205 205
206 206 @classmethod
207 207 def get_app_settings(cls, cache=False):
208 208
209 209 ret = cls.query()
210 210
211 211 if cache:
212 212 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
213 213
214 214 if not ret:
215 215 raise Exception('Could not get application settings !')
216 216 settings = {}
217 217 for each in ret:
218 218 settings['rhodecode_' + each.app_settings_name] = \
219 219 each.app_settings_value
220 220
221 221 return settings
222 222
223 223 @classmethod
224 224 def get_ldap_settings(cls, cache=False):
225 225 ret = cls.query()\
226 226 .filter(cls.app_settings_name.startswith('ldap_')).all()
227 227 fd = {}
228 228 for row in ret:
229 229 fd.update({row.app_settings_name: row.app_settings_value})
230 230
231 231 return fd
232 232
233 233 @classmethod
234 234 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
235 235 ret = cls.query()\
236 236 .filter(cls.app_settings_name.startswith('default_')).all()
237 237 fd = {}
238 238 for row in ret:
239 239 key = row.app_settings_name
240 240 if strip_prefix:
241 241 key = remove_prefix(key, prefix='default_')
242 242 fd.update({key: row.app_settings_value})
243 243
244 244 return fd
245 245
246 246
247 247 class RhodeCodeUi(Base, BaseModel):
248 248 __tablename__ = 'rhodecode_ui'
249 249 __table_args__ = (
250 250 UniqueConstraint('ui_key'),
251 251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
252 252 'mysql_charset': 'utf8'}
253 253 )
254 254
255 255 HOOK_UPDATE = 'changegroup.update'
256 256 HOOK_REPO_SIZE = 'changegroup.repo_size'
257 257 HOOK_PUSH = 'changegroup.push_logger'
258 258 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
259 259 HOOK_PULL = 'outgoing.pull_logger'
260 260 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
261 261
262 262 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
263 263 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
266 266 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
267 267
268 268 @classmethod
269 269 def get_by_key(cls, key):
270 270 return cls.query().filter(cls.ui_key == key).scalar()
271 271
272 272 @classmethod
273 273 def get_builtin_hooks(cls):
274 274 q = cls.query()
275 275 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
276 276 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
277 277 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
278 278 return q.all()
279 279
280 280 @classmethod
281 281 def get_custom_hooks(cls):
282 282 q = cls.query()
283 283 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
284 284 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
285 285 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
286 286 q = q.filter(cls.ui_section == 'hooks')
287 287 return q.all()
288 288
289 289 @classmethod
290 290 def get_repos_location(cls):
291 291 return cls.get_by_key('/').ui_value
292 292
293 293 @classmethod
294 294 def create_or_update_hook(cls, key, val):
295 295 new_ui = cls.get_by_key(key) or cls()
296 296 new_ui.ui_section = 'hooks'
297 297 new_ui.ui_active = True
298 298 new_ui.ui_key = key
299 299 new_ui.ui_value = val
300 300
301 301 Session().add(new_ui)
302 302
303 303 def __repr__(self):
304 304 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
305 305 self.ui_value)
306 306
307 307
308 308 class User(Base, BaseModel):
309 309 __tablename__ = 'users'
310 310 __table_args__ = (
311 311 UniqueConstraint('username'), UniqueConstraint('email'),
312 312 Index('u_username_idx', 'username'),
313 313 Index('u_email_idx', 'email'),
314 314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
315 315 'mysql_charset': 'utf8'}
316 316 )
317 317 DEFAULT_USER = 'default'
318 318 DEFAULT_PERMISSIONS = [
319 319 'hg.register.manual_activate', 'hg.create.repository',
320 320 'hg.fork.repository', 'repository.read', 'group.read'
321 321 ]
322 322 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
323 323 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
325 325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
326 326 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
327 327 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
331 331 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
333 333 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
334 334
335 335 user_log = relationship('UserLog')
336 336 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
337 337
338 338 repositories = relationship('Repository')
339 339 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
340 340 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
341 341
342 342 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
343 343 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
344 344
345 345 group_member = relationship('UserGroupMember', cascade='all')
346 346
347 347 notifications = relationship('UserNotification', cascade='all')
348 348 # notifications assigned to this user
349 349 user_created_notifications = relationship('Notification', cascade='all')
350 350 # comments created by this user
351 351 user_comments = relationship('ChangesetComment', cascade='all')
352 352 #extra emails for this user
353 353 user_emails = relationship('UserEmailMap', cascade='all')
354 354
355 355 @hybrid_property
356 356 def email(self):
357 357 return self._email
358 358
359 359 @email.setter
360 360 def email(self, val):
361 361 self._email = val.lower() if val else None
362 362
363 363 @property
364 364 def firstname(self):
365 365 # alias for future
366 366 return self.name
367 367
368 368 @property
369 369 def emails(self):
370 370 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
371 371 return [self.email] + [x.email for x in other]
372 372
373 373 @property
374 374 def ip_addresses(self):
375 375 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
376 376 return [x.ip_addr for x in ret]
377 377
378 378 @property
379 379 def username_and_name(self):
380 380 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
381 381
382 382 @property
383 383 def full_name(self):
384 384 return '%s %s' % (self.firstname, self.lastname)
385 385
386 386 @property
387 387 def full_name_or_username(self):
388 388 return ('%s %s' % (self.firstname, self.lastname)
389 389 if (self.firstname and self.lastname) else self.username)
390 390
391 391 @property
392 392 def full_contact(self):
393 393 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
394 394
395 395 @property
396 396 def short_contact(self):
397 397 return '%s %s' % (self.firstname, self.lastname)
398 398
399 399 @property
400 400 def is_admin(self):
401 401 return self.admin
402 402
403 403 @property
404 404 def AuthUser(self):
405 405 """
406 406 Returns instance of AuthUser for this user
407 407 """
408 408 from rhodecode.lib.auth import AuthUser
409 409 return AuthUser(user_id=self.user_id, api_key=self.api_key,
410 410 username=self.username)
411 411
412 412 def __unicode__(self):
413 413 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
414 414 self.user_id, self.username)
415 415
416 416 @classmethod
417 417 def get_by_username(cls, username, case_insensitive=False, cache=False):
418 418 if case_insensitive:
419 419 q = cls.query().filter(cls.username.ilike(username))
420 420 else:
421 421 q = cls.query().filter(cls.username == username)
422 422
423 423 if cache:
424 424 q = q.options(FromCache(
425 425 "sql_cache_short",
426 426 "get_user_%s" % _hash_key(username)
427 427 )
428 428 )
429 429 return q.scalar()
430 430
431 431 @classmethod
432 432 def get_by_api_key(cls, api_key, cache=False):
433 433 q = cls.query().filter(cls.api_key == api_key)
434 434
435 435 if cache:
436 436 q = q.options(FromCache("sql_cache_short",
437 437 "get_api_key_%s" % api_key))
438 438 return q.scalar()
439 439
440 440 @classmethod
441 441 def get_by_email(cls, email, case_insensitive=False, cache=False):
442 442 if case_insensitive:
443 443 q = cls.query().filter(cls.email.ilike(email))
444 444 else:
445 445 q = cls.query().filter(cls.email == email)
446 446
447 447 if cache:
448 448 q = q.options(FromCache("sql_cache_short",
449 449 "get_email_key_%s" % email))
450 450
451 451 ret = q.scalar()
452 452 if ret is None:
453 453 q = UserEmailMap.query()
454 454 # try fetching in alternate email map
455 455 if case_insensitive:
456 456 q = q.filter(UserEmailMap.email.ilike(email))
457 457 else:
458 458 q = q.filter(UserEmailMap.email == email)
459 459 q = q.options(joinedload(UserEmailMap.user))
460 460 if cache:
461 461 q = q.options(FromCache("sql_cache_short",
462 462 "get_email_map_key_%s" % email))
463 463 ret = getattr(q.scalar(), 'user', None)
464 464
465 465 return ret
466 466
467 467 @classmethod
468 468 def get_from_cs_author(cls, author):
469 469 """
470 470 Tries to get User objects out of commit author string
471 471
472 472 :param author:
473 473 """
474 474 from rhodecode.lib.helpers import email, author_name
475 475 # Valid email in the attribute passed, see if they're in the system
476 476 _email = email(author)
477 477 if _email:
478 478 user = cls.get_by_email(_email, case_insensitive=True)
479 479 if user:
480 480 return user
481 481 # Maybe we can match by username?
482 482 _author = author_name(author)
483 483 user = cls.get_by_username(_author, case_insensitive=True)
484 484 if user:
485 485 return user
486 486
487 487 def update_lastlogin(self):
488 488 """Update user lastlogin"""
489 489 self.last_login = datetime.datetime.now()
490 490 Session().add(self)
491 491 log.debug('updated user %s lastlogin' % self.username)
492 492
493 493 def get_api_data(self):
494 494 """
495 495 Common function for generating user related data for API
496 496 """
497 497 user = self
498 498 data = dict(
499 499 user_id=user.user_id,
500 500 username=user.username,
501 501 firstname=user.name,
502 502 lastname=user.lastname,
503 503 email=user.email,
504 504 emails=user.emails,
505 505 api_key=user.api_key,
506 506 active=user.active,
507 507 admin=user.admin,
508 508 ldap_dn=user.ldap_dn,
509 509 last_login=user.last_login,
510 510 ip_addresses=user.ip_addresses
511 511 )
512 512 return data
513 513
514 514 def __json__(self):
515 515 data = dict(
516 516 full_name=self.full_name,
517 517 full_name_or_username=self.full_name_or_username,
518 518 short_contact=self.short_contact,
519 519 full_contact=self.full_contact
520 520 )
521 521 data.update(self.get_api_data())
522 522 return data
523 523
524 524
525 525 class UserEmailMap(Base, BaseModel):
526 526 __tablename__ = 'user_email_map'
527 527 __table_args__ = (
528 528 Index('uem_email_idx', 'email'),
529 529 UniqueConstraint('email'),
530 530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
531 531 'mysql_charset': 'utf8'}
532 532 )
533 533 __mapper_args__ = {}
534 534
535 535 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
536 536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
537 537 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
538 538 user = relationship('User', lazy='joined')
539 539
540 540 @validates('_email')
541 541 def validate_email(self, key, email):
542 542 # check if this email is not main one
543 543 main_email = Session().query(User).filter(User.email == email).scalar()
544 544 if main_email is not None:
545 545 raise AttributeError('email %s is present is user table' % email)
546 546 return email
547 547
548 548 @hybrid_property
549 549 def email(self):
550 550 return self._email
551 551
552 552 @email.setter
553 553 def email(self, val):
554 554 self._email = val.lower() if val else None
555 555
556 556
557 557 class UserIpMap(Base, BaseModel):
558 558 __tablename__ = 'user_ip_map'
559 559 __table_args__ = (
560 560 UniqueConstraint('user_id', 'ip_addr'),
561 561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
562 562 'mysql_charset': 'utf8'}
563 563 )
564 564 __mapper_args__ = {}
565 565
566 566 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
567 567 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
568 568 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
569 569 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
570 570 user = relationship('User', lazy='joined')
571 571
572 572 @classmethod
573 573 def _get_ip_range(cls, ip_addr):
574 574 from rhodecode.lib import ipaddr
575 575 net = ipaddr.IPNetwork(address=ip_addr)
576 576 return [str(net.network), str(net.broadcast)]
577 577
578 578 def __json__(self):
579 579 return dict(
580 580 ip_addr=self.ip_addr,
581 581 ip_range=self._get_ip_range(self.ip_addr)
582 582 )
583 583
584 584
585 585 class UserLog(Base, BaseModel):
586 586 __tablename__ = 'user_logs'
587 587 __table_args__ = (
588 588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
589 589 'mysql_charset': 'utf8'},
590 590 )
591 591 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
592 592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
593 593 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
594 594 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
595 595 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 596 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 597 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
598 598 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 599
600 600 @property
601 601 def action_as_day(self):
602 602 return datetime.date(*self.action_date.timetuple()[:3])
603 603
604 604 user = relationship('User')
605 605 repository = relationship('Repository', cascade='')
606 606
607 607
608 608 class UserGroup(Base, BaseModel):
609 609 __tablename__ = 'users_groups'
610 610 __table_args__ = (
611 611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
612 612 'mysql_charset': 'utf8'},
613 613 )
614 614
615 615 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
616 616 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
617 617 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
618 618 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
619 619
620 620 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
621 621 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
622 622 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
623 623
624 624 def __unicode__(self):
625 625 return u'<userGroup(%s)>' % (self.users_group_name)
626 626
627 627 @classmethod
628 628 def get_by_group_name(cls, group_name, cache=False,
629 629 case_insensitive=False):
630 630 if case_insensitive:
631 631 q = cls.query().filter(cls.users_group_name.ilike(group_name))
632 632 else:
633 633 q = cls.query().filter(cls.users_group_name == group_name)
634 634 if cache:
635 635 q = q.options(FromCache(
636 636 "sql_cache_short",
637 637 "get_user_%s" % _hash_key(group_name)
638 638 )
639 639 )
640 640 return q.scalar()
641 641
642 642 @classmethod
643 643 def get(cls, users_group_id, cache=False):
644 644 users_group = cls.query()
645 645 if cache:
646 646 users_group = users_group.options(FromCache("sql_cache_short",
647 647 "get_users_group_%s" % users_group_id))
648 648 return users_group.get(users_group_id)
649 649
650 650 def get_api_data(self):
651 651 users_group = self
652 652
653 653 data = dict(
654 654 users_group_id=users_group.users_group_id,
655 655 group_name=users_group.users_group_name,
656 656 active=users_group.users_group_active,
657 657 )
658 658
659 659 return data
660 660
661 661
662 662 class UserGroupMember(Base, BaseModel):
663 663 __tablename__ = 'users_groups_members'
664 664 __table_args__ = (
665 665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
666 666 'mysql_charset': 'utf8'},
667 667 )
668 668
669 669 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
670 670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
671 671 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
672 672
673 673 user = relationship('User', lazy='joined')
674 674 users_group = relationship('UserGroup')
675 675
676 676 def __init__(self, gr_id='', u_id=''):
677 677 self.users_group_id = gr_id
678 678 self.user_id = u_id
679 679
680 680
681 681 class RepositoryField(Base, BaseModel):
682 682 __tablename__ = 'repositories_fields'
683 683 __table_args__ = (
684 684 UniqueConstraint('repository_id', 'field_key'), # no-multi field
685 685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
686 686 'mysql_charset': 'utf8'},
687 687 )
688 688 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
689 689
690 690 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
691 691 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
692 692 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
693 693 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
694 694 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
695 695 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
696 696 field_type = Column("field_type", String(256), nullable=False, unique=None)
697 697 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
698 698
699 699 repository = relationship('Repository')
700 700
701 701 @property
702 702 def field_key_prefixed(self):
703 703 return 'ex_%s' % self.field_key
704 704
705 705 @classmethod
706 706 def un_prefix_key(cls, key):
707 707 if key.startswith(cls.PREFIX):
708 708 return key[len(cls.PREFIX):]
709 709 return key
710 710
711 711 @classmethod
712 712 def get_by_key_name(cls, key, repo):
713 713 row = cls.query()\
714 714 .filter(cls.repository == repo)\
715 715 .filter(cls.field_key == key).scalar()
716 716 return row
717 717
718 718
719 719 class Repository(Base, BaseModel):
720 720 __tablename__ = 'repositories'
721 721 __table_args__ = (
722 722 UniqueConstraint('repo_name'),
723 723 Index('r_repo_name_idx', 'repo_name'),
724 724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
725 725 'mysql_charset': 'utf8'},
726 726 )
727 727
728 728 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
729 729 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
730 730 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
731 731 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
732 732 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
733 733 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
734 734 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
735 735 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
736 736 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
737 737 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 738 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
739 739 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
740 740 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
741 741 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
742 742 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
743 743
744 744 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
745 745 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
746 746
747 747 user = relationship('User')
748 748 fork = relationship('Repository', remote_side=repo_id)
749 749 group = relationship('RepoGroup')
750 750 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
751 751 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
752 752 stats = relationship('Statistics', cascade='all', uselist=False)
753 753
754 754 followers = relationship('UserFollowing',
755 755 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
756 756 cascade='all')
757 757 extra_fields = relationship('RepositoryField',
758 758 cascade="all, delete, delete-orphan")
759 759
760 760 logs = relationship('UserLog')
761 761 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
762 762
763 763 pull_requests_org = relationship('PullRequest',
764 764 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
765 765 cascade="all, delete, delete-orphan")
766 766
767 767 pull_requests_other = relationship('PullRequest',
768 768 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
769 769 cascade="all, delete, delete-orphan")
770 770
771 771 def __unicode__(self):
772 772 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
773 773 self.repo_name)
774 774
775 775 @hybrid_property
776 776 def locked(self):
777 777 # always should return [user_id, timelocked]
778 778 if self._locked:
779 779 _lock_info = self._locked.split(':')
780 780 return int(_lock_info[0]), _lock_info[1]
781 781 return [None, None]
782 782
783 783 @locked.setter
784 784 def locked(self, val):
785 785 if val and isinstance(val, (list, tuple)):
786 786 self._locked = ':'.join(map(str, val))
787 787 else:
788 788 self._locked = None
789 789
790 790 @hybrid_property
791 791 def changeset_cache(self):
792 792 from rhodecode.lib.vcs.backends.base import EmptyChangeset
793 793 dummy = EmptyChangeset().__json__()
794 794 if not self._changeset_cache:
795 795 return dummy
796 796 try:
797 797 return json.loads(self._changeset_cache)
798 798 except TypeError:
799 799 return dummy
800 800
801 801 @changeset_cache.setter
802 802 def changeset_cache(self, val):
803 803 try:
804 804 self._changeset_cache = json.dumps(val)
805 805 except:
806 806 log.error(traceback.format_exc())
807 807
808 808 @classmethod
809 809 def url_sep(cls):
810 810 return URL_SEP
811 811
812 812 @classmethod
813 813 def normalize_repo_name(cls, repo_name):
814 814 """
815 815 Normalizes os specific repo_name to the format internally stored inside
816 816 dabatabase using URL_SEP
817 817
818 818 :param cls:
819 819 :param repo_name:
820 820 """
821 821 return cls.url_sep().join(repo_name.split(os.sep))
822 822
823 823 @classmethod
824 824 def get_by_repo_name(cls, repo_name):
825 825 q = Session().query(cls).filter(cls.repo_name == repo_name)
826 826 q = q.options(joinedload(Repository.fork))\
827 827 .options(joinedload(Repository.user))\
828 828 .options(joinedload(Repository.group))
829 829 return q.scalar()
830 830
831 831 @classmethod
832 832 def get_by_full_path(cls, repo_full_path):
833 833 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
834 834 repo_name = cls.normalize_repo_name(repo_name)
835 835 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
836 836
837 837 @classmethod
838 838 def get_repo_forks(cls, repo_id):
839 839 return cls.query().filter(Repository.fork_id == repo_id)
840 840
841 841 @classmethod
842 842 def base_path(cls):
843 843 """
844 844 Returns base path when all repos are stored
845 845
846 846 :param cls:
847 847 """
848 848 q = Session().query(RhodeCodeUi)\
849 849 .filter(RhodeCodeUi.ui_key == cls.url_sep())
850 850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
851 851 return q.one().ui_value
852 852
853 853 @property
854 854 def forks(self):
855 855 """
856 856 Return forks of this repo
857 857 """
858 858 return Repository.get_repo_forks(self.repo_id)
859 859
860 860 @property
861 861 def parent(self):
862 862 """
863 863 Returns fork parent
864 864 """
865 865 return self.fork
866 866
867 867 @property
868 868 def just_name(self):
869 869 return self.repo_name.split(Repository.url_sep())[-1]
870 870
871 871 @property
872 872 def groups_with_parents(self):
873 873 groups = []
874 874 if self.group is None:
875 875 return groups
876 876
877 877 cur_gr = self.group
878 878 groups.insert(0, cur_gr)
879 879 while 1:
880 880 gr = getattr(cur_gr, 'parent_group', None)
881 881 cur_gr = cur_gr.parent_group
882 882 if gr is None:
883 883 break
884 884 groups.insert(0, gr)
885 885
886 886 return groups
887 887
888 888 @property
889 889 def groups_and_repo(self):
890 890 return self.groups_with_parents, self.just_name, self.repo_name
891 891
892 892 @LazyProperty
893 893 def repo_path(self):
894 894 """
895 895 Returns base full path for that repository means where it actually
896 896 exists on a filesystem
897 897 """
898 898 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
899 899 Repository.url_sep())
900 900 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
901 901 return q.one().ui_value
902 902
903 903 @property
904 904 def repo_full_path(self):
905 905 p = [self.repo_path]
906 906 # we need to split the name by / since this is how we store the
907 907 # names in the database, but that eventually needs to be converted
908 908 # into a valid system path
909 909 p += self.repo_name.split(Repository.url_sep())
910 910 return os.path.join(*p)
911 911
912 912 @property
913 913 def cache_keys(self):
914 914 """
915 915 Returns associated cache keys for that repo
916 916 """
917 917 return CacheInvalidation.query()\
918 918 .filter(CacheInvalidation.cache_args == self.repo_name)\
919 919 .order_by(CacheInvalidation.cache_key)\
920 920 .all()
921 921
922 922 def get_new_name(self, repo_name):
923 923 """
924 924 returns new full repository name based on assigned group and new new
925 925
926 926 :param group_name:
927 927 """
928 928 path_prefix = self.group.full_path_splitted if self.group else []
929 929 return Repository.url_sep().join(path_prefix + [repo_name])
930 930
931 931 @property
932 932 def _ui(self):
933 933 """
934 934 Creates an db based ui object for this repository
935 935 """
936 936 from rhodecode.lib.utils import make_ui
937 937 return make_ui('db', clear_session=False)
938 938
939 939 @classmethod
940 940 def is_valid(cls, repo_name):
941 941 """
942 942 returns True if given repo name is a valid filesystem repository
943 943
944 944 :param cls:
945 945 :param repo_name:
946 946 """
947 947 from rhodecode.lib.utils import is_valid_repo
948 948
949 949 return is_valid_repo(repo_name, cls.base_path())
950 950
951 951 def get_api_data(self):
952 952 """
953 953 Common function for generating repo api data
954 954
955 955 """
956 956 repo = self
957 957 data = dict(
958 958 repo_id=repo.repo_id,
959 959 repo_name=repo.repo_name,
960 960 repo_type=repo.repo_type,
961 961 clone_uri=repo.clone_uri,
962 962 private=repo.private,
963 963 created_on=repo.created_on,
964 964 description=repo.description,
965 965 landing_rev=repo.landing_rev,
966 966 owner=repo.user.username,
967 967 fork_of=repo.fork.repo_name if repo.fork else None,
968 968 enable_statistics=repo.enable_statistics,
969 969 enable_locking=repo.enable_locking,
970 970 enable_downloads=repo.enable_downloads,
971 971 last_changeset=repo.changeset_cache,
972 972 locked_by=User.get(self.locked[0]).get_api_data() \
973 973 if self.locked[0] else None,
974 974 locked_date=time_to_datetime(self.locked[1]) \
975 975 if self.locked[1] else None
976 976 )
977 977 rc_config = RhodeCodeSetting.get_app_settings()
978 978 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
979 979 if repository_fields:
980 980 for f in self.extra_fields:
981 981 data[f.field_key_prefixed] = f.field_value
982 982
983 983 return data
984 984
985 985 @classmethod
986 986 def lock(cls, repo, user_id):
987 987 repo.locked = [user_id, time.time()]
988 988 Session().add(repo)
989 989 Session().commit()
990 990
991 991 @classmethod
992 992 def unlock(cls, repo):
993 993 repo.locked = None
994 994 Session().add(repo)
995 995 Session().commit()
996 996
997 997 @classmethod
998 998 def getlock(cls, repo):
999 999 return repo.locked
1000 1000
1001 1001 @property
1002 1002 def last_db_change(self):
1003 1003 return self.updated_on
1004 1004
1005 1005 def clone_url(self, **override):
1006 1006 from pylons import url
1007 1007 from urlparse import urlparse
1008 1008 import urllib
1009 1009 parsed_url = urlparse(url('home', qualified=True))
1010 1010 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1011 1011 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1012 1012 args = {
1013 1013 'user': '',
1014 1014 'pass': '',
1015 1015 'scheme': parsed_url.scheme,
1016 1016 'netloc': parsed_url.netloc,
1017 1017 'prefix': decoded_path,
1018 1018 'path': self.repo_name
1019 1019 }
1020 1020
1021 1021 args.update(override)
1022 1022 return default_clone_uri % args
1023 1023
1024 1024 #==========================================================================
1025 1025 # SCM PROPERTIES
1026 1026 #==========================================================================
1027 1027
1028 1028 def get_changeset(self, rev=None):
1029 1029 return get_changeset_safe(self.scm_instance, rev)
1030 1030
1031 1031 def get_landing_changeset(self):
1032 1032 """
1033 1033 Returns landing changeset, or if that doesn't exist returns the tip
1034 1034 """
1035 1035 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1036 1036 return cs
1037 1037
1038 1038 def update_changeset_cache(self, cs_cache=None):
1039 1039 """
1040 1040 Update cache of last changeset for repository, keys should be::
1041 1041
1042 1042 short_id
1043 1043 raw_id
1044 1044 revision
1045 1045 message
1046 1046 date
1047 1047 author
1048 1048
1049 1049 :param cs_cache:
1050 1050 """
1051 1051 from rhodecode.lib.vcs.backends.base import BaseChangeset
1052 1052 if cs_cache is None:
1053 1053 cs_cache = EmptyChangeset()
1054 1054 # use no-cache version here
1055 1055 scm_repo = self.scm_instance_no_cache()
1056 1056 if scm_repo:
1057 1057 cs_cache = scm_repo.get_changeset()
1058 1058
1059 1059 if isinstance(cs_cache, BaseChangeset):
1060 1060 cs_cache = cs_cache.__json__()
1061 1061
1062 1062 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1063 1063 _default = datetime.datetime.fromtimestamp(0)
1064 1064 last_change = cs_cache.get('date') or _default
1065 1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1066 1066 self.updated_on = last_change
1067 1067 self.changeset_cache = cs_cache
1068 1068 Session().add(self)
1069 1069 Session().commit()
1070 1070 else:
1071 1071 log.debug('Skipping repo:%s already with latest changes' % self)
1072 1072
1073 1073 @property
1074 1074 def tip(self):
1075 1075 return self.get_changeset('tip')
1076 1076
1077 1077 @property
1078 1078 def author(self):
1079 1079 return self.tip.author
1080 1080
1081 1081 @property
1082 1082 def last_change(self):
1083 1083 return self.scm_instance.last_change
1084 1084
1085 1085 def get_comments(self, revisions=None):
1086 1086 """
1087 1087 Returns comments for this repository grouped by revisions
1088 1088
1089 1089 :param revisions: filter query by revisions only
1090 1090 """
1091 1091 cmts = ChangesetComment.query()\
1092 1092 .filter(ChangesetComment.repo == self)
1093 1093 if revisions:
1094 1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1095 1095 grouped = defaultdict(list)
1096 1096 for cmt in cmts.all():
1097 1097 grouped[cmt.revision].append(cmt)
1098 1098 return grouped
1099 1099
1100 1100 def statuses(self, revisions=None):
1101 1101 """
1102 1102 Returns statuses for this repository
1103 1103
1104 1104 :param revisions: list of revisions to get statuses for
1105 1105 :type revisions: list
1106 1106 """
1107 1107
1108 1108 statuses = ChangesetStatus.query()\
1109 1109 .filter(ChangesetStatus.repo == self)\
1110 1110 .filter(ChangesetStatus.version == 0)
1111 1111 if revisions:
1112 1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1113 1113 grouped = {}
1114 1114
1115 1115 #maybe we have open new pullrequest without a status ?
1116 1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1117 1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1118 1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1119 1119 for rev in pr.revisions:
1120 1120 pr_id = pr.pull_request_id
1121 1121 pr_repo = pr.other_repo.repo_name
1122 1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1123 1123
1124 1124 for stat in statuses.all():
1125 1125 pr_id = pr_repo = None
1126 1126 if stat.pull_request:
1127 1127 pr_id = stat.pull_request.pull_request_id
1128 1128 pr_repo = stat.pull_request.other_repo.repo_name
1129 1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1130 1130 pr_id, pr_repo]
1131 1131 return grouped
1132 1132
1133 1133 def _repo_size(self):
1134 1134 from rhodecode.lib import helpers as h
1135 1135 log.debug('calculating repository size...')
1136 1136 return h.format_byte_size(self.scm_instance.size)
1137 1137
1138 1138 #==========================================================================
1139 1139 # SCM CACHE INSTANCE
1140 1140 #==========================================================================
1141 1141
1142 1142 @property
1143 1143 def invalidate(self):
1144 1144 return CacheInvalidation.invalidate(self.repo_name)
1145 1145
1146 1146 def set_invalidate(self):
1147 1147 """
1148 1148 set a cache for invalidation for this instance
1149 1149 """
1150 1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1151 1151
1152 1152 def scm_instance_no_cache(self):
1153 1153 return self.__get_instance()
1154 1154
1155 1155 @LazyProperty
1156 1156 def scm_instance(self):
1157 1157 import rhodecode
1158 1158 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1159 1159 if full_cache:
1160 1160 return self.scm_instance_cached()
1161 1161 return self.__get_instance()
1162 1162
1163 1163 def scm_instance_cached(self, cache_map=None):
1164 1164 @cache_region('long_term')
1165 1165 def _c(repo_name):
1166 1166 return self.__get_instance()
1167 1167 rn = self.repo_name
1168 1168 log.debug('Getting cached instance of repo')
1169 1169
1170 1170 if cache_map:
1171 1171 # get using prefilled cache_map
1172 1172 invalidate_repo = cache_map[self.repo_name]
1173 1173 if invalidate_repo:
1174 1174 invalidate_repo = (None if invalidate_repo.cache_active
1175 1175 else invalidate_repo)
1176 1176 else:
1177 1177 # get from invalidate
1178 1178 invalidate_repo = self.invalidate
1179 1179
1180 1180 if invalidate_repo is not None:
1181 1181 region_invalidate(_c, None, rn)
1182 1182 # update our cache
1183 1183 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1184 1184 return _c(rn)
1185 1185
1186 1186 def __get_instance(self):
1187 1187 repo_full_path = self.repo_full_path
1188 1188 try:
1189 1189 alias = get_scm(repo_full_path)[0]
1190 1190 log.debug('Creating instance of %s repository from %s'
1191 1191 % (alias, repo_full_path))
1192 1192 backend = get_backend(alias)
1193 1193 except VCSError:
1194 1194 log.error(traceback.format_exc())
1195 1195 log.error('Perhaps this repository is in db and not in '
1196 1196 'filesystem run rescan repositories with '
1197 1197 '"destroy old data " option from admin panel')
1198 1198 return
1199 1199
1200 1200 if alias == 'hg':
1201 1201
1202 1202 repo = backend(safe_str(repo_full_path), create=False,
1203 1203 baseui=self._ui)
1204 1204 # skip hidden web repository
1205 1205 if repo._get_hidden():
1206 1206 return
1207 1207 else:
1208 1208 repo = backend(repo_full_path, create=False)
1209 1209
1210 1210 return repo
1211 1211
1212 1212
1213 1213 class RepoGroup(Base, BaseModel):
1214 1214 __tablename__ = 'groups'
1215 1215 __table_args__ = (
1216 1216 UniqueConstraint('group_name', 'group_parent_id'),
1217 1217 CheckConstraint('group_id != group_parent_id'),
1218 1218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1219 1219 'mysql_charset': 'utf8'},
1220 1220 )
1221 1221 __mapper_args__ = {'order_by': 'group_name'}
1222 1222
1223 1223 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1224 1224 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1225 1225 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1226 1226 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1227 1227 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1228 1228
1229 1229 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1230 1230 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1231 1231
1232 1232 parent_group = relationship('RepoGroup', remote_side=group_id)
1233 1233
1234 1234 def __init__(self, group_name='', parent_group=None):
1235 1235 self.group_name = group_name
1236 1236 self.parent_group = parent_group
1237 1237
1238 1238 def __unicode__(self):
1239 1239 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1240 1240 self.group_name)
1241 1241
1242 1242 @classmethod
1243 1243 def groups_choices(cls, groups=None, show_empty_group=True):
1244 1244 from webhelpers.html import literal as _literal
1245 1245 if not groups:
1246 1246 groups = cls.query().all()
1247 1247
1248 1248 repo_groups = []
1249 1249 if show_empty_group:
1250 1250 repo_groups = [('-1', '-- %s --' % _('top level'))]
1251 1251 sep = ' &raquo; '
1252 1252 _name = lambda k: _literal(sep.join(k))
1253 1253
1254 1254 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1255 1255 for x in groups])
1256 1256
1257 1257 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1258 1258 return repo_groups
1259 1259
1260 1260 @classmethod
1261 1261 def url_sep(cls):
1262 1262 return URL_SEP
1263 1263
1264 1264 @classmethod
1265 1265 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1266 1266 if case_insensitive:
1267 1267 gr = cls.query()\
1268 1268 .filter(cls.group_name.ilike(group_name))
1269 1269 else:
1270 1270 gr = cls.query()\
1271 1271 .filter(cls.group_name == group_name)
1272 1272 if cache:
1273 1273 gr = gr.options(FromCache(
1274 1274 "sql_cache_short",
1275 1275 "get_group_%s" % _hash_key(group_name)
1276 1276 )
1277 1277 )
1278 1278 return gr.scalar()
1279 1279
1280 1280 @property
1281 1281 def parents(self):
1282 1282 parents_recursion_limit = 5
1283 1283 groups = []
1284 1284 if self.parent_group is None:
1285 1285 return groups
1286 1286 cur_gr = self.parent_group
1287 1287 groups.insert(0, cur_gr)
1288 1288 cnt = 0
1289 1289 while 1:
1290 1290 cnt += 1
1291 1291 gr = getattr(cur_gr, 'parent_group', None)
1292 1292 cur_gr = cur_gr.parent_group
1293 1293 if gr is None:
1294 1294 break
1295 1295 if cnt == parents_recursion_limit:
1296 1296 # this will prevent accidental infinit loops
1297 1297 log.error('group nested more than %s' %
1298 1298 parents_recursion_limit)
1299 1299 break
1300 1300
1301 1301 groups.insert(0, gr)
1302 1302 return groups
1303 1303
1304 1304 @property
1305 1305 def children(self):
1306 1306 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1307 1307
1308 1308 @property
1309 1309 def name(self):
1310 1310 return self.group_name.split(RepoGroup.url_sep())[-1]
1311 1311
1312 1312 @property
1313 1313 def full_path(self):
1314 1314 return self.group_name
1315 1315
1316 1316 @property
1317 1317 def full_path_splitted(self):
1318 1318 return self.group_name.split(RepoGroup.url_sep())
1319 1319
1320 1320 @property
1321 1321 def repositories(self):
1322 1322 return Repository.query()\
1323 1323 .filter(Repository.group == self)\
1324 1324 .order_by(Repository.repo_name)
1325 1325
1326 1326 @property
1327 1327 def repositories_recursive_count(self):
1328 1328 cnt = self.repositories.count()
1329 1329
1330 1330 def children_count(group):
1331 1331 cnt = 0
1332 1332 for child in group.children:
1333 1333 cnt += child.repositories.count()
1334 1334 cnt += children_count(child)
1335 1335 return cnt
1336 1336
1337 1337 return cnt + children_count(self)
1338 1338
1339 1339 def _recursive_objects(self, include_repos=True):
1340 1340 all_ = []
1341 1341
1342 1342 def _get_members(root_gr):
1343 1343 if include_repos:
1344 1344 for r in root_gr.repositories:
1345 1345 all_.append(r)
1346 1346 childs = root_gr.children.all()
1347 1347 if childs:
1348 1348 for gr in childs:
1349 1349 all_.append(gr)
1350 1350 _get_members(gr)
1351 1351
1352 1352 _get_members(self)
1353 1353 return [self] + all_
1354 1354
1355 1355 def recursive_groups_and_repos(self):
1356 1356 """
1357 1357 Recursive return all groups, with repositories in those groups
1358 1358 """
1359 1359 return self._recursive_objects()
1360 1360
1361 1361 def recursive_groups(self):
1362 1362 """
1363 1363 Returns all children groups for this group including children of children
1364 1364 """
1365 1365 return self._recursive_objects(include_repos=False)
1366 1366
1367 1367 def get_new_name(self, group_name):
1368 1368 """
1369 1369 returns new full group name based on parent and new name
1370 1370
1371 1371 :param group_name:
1372 1372 """
1373 1373 path_prefix = (self.parent_group.full_path_splitted if
1374 1374 self.parent_group else [])
1375 1375 return RepoGroup.url_sep().join(path_prefix + [group_name])
1376 1376
1377 1377
1378 1378 class Permission(Base, BaseModel):
1379 1379 __tablename__ = 'permissions'
1380 1380 __table_args__ = (
1381 1381 Index('p_perm_name_idx', 'permission_name'),
1382 1382 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1383 1383 'mysql_charset': 'utf8'},
1384 1384 )
1385 1385 PERMS = [
1386 1386 ('repository.none', _('Repository no access')),
1387 1387 ('repository.read', _('Repository read access')),
1388 1388 ('repository.write', _('Repository write access')),
1389 1389 ('repository.admin', _('Repository admin access')),
1390 1390
1391 1391 ('group.none', _('Repository group no access')),
1392 1392 ('group.read', _('Repository group read access')),
1393 1393 ('group.write', _('Repository group write access')),
1394 1394 ('group.admin', _('Repository group admin access')),
1395 1395
1396 1396 ('hg.admin', _('RhodeCode Administrator')),
1397 1397 ('hg.create.none', _('Repository creation disabled')),
1398 1398 ('hg.create.repository', _('Repository creation enabled')),
1399 1399 ('hg.fork.none', _('Repository forking disabled')),
1400 1400 ('hg.fork.repository', _('Repository forking enabled')),
1401 1401 ('hg.register.none', _('Register disabled')),
1402 1402 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1403 1403 'with manual activation')),
1404 1404
1405 1405 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1406 1406 'with auto activation')),
1407 1407 ]
1408 1408
1409 1409 # defines which permissions are more important higher the more important
1410 1410 PERM_WEIGHTS = {
1411 1411 'repository.none': 0,
1412 1412 'repository.read': 1,
1413 1413 'repository.write': 3,
1414 1414 'repository.admin': 4,
1415 1415
1416 1416 'group.none': 0,
1417 1417 'group.read': 1,
1418 1418 'group.write': 3,
1419 1419 'group.admin': 4,
1420 1420
1421 1421 'hg.fork.none': 0,
1422 1422 'hg.fork.repository': 1,
1423 1423 'hg.create.none': 0,
1424 1424 'hg.create.repository':1
1425 1425 }
1426 1426
1427 1427 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1428 1428 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1429 1429 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1430 1430
1431 1431 def __unicode__(self):
1432 1432 return u"<%s('%s:%s')>" % (
1433 1433 self.__class__.__name__, self.permission_id, self.permission_name
1434 1434 )
1435 1435
1436 1436 @classmethod
1437 1437 def get_by_key(cls, key):
1438 1438 return cls.query().filter(cls.permission_name == key).scalar()
1439 1439
1440 1440 @classmethod
1441 1441 def get_default_perms(cls, default_user_id):
1442 1442 q = Session().query(UserRepoToPerm, Repository, cls)\
1443 1443 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1444 1444 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1445 1445 .filter(UserRepoToPerm.user_id == default_user_id)
1446 1446
1447 1447 return q.all()
1448 1448
1449 1449 @classmethod
1450 1450 def get_default_group_perms(cls, default_user_id):
1451 1451 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1452 1452 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1453 1453 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1454 1454 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1455 1455
1456 1456 return q.all()
1457 1457
1458 1458
1459 1459 class UserRepoToPerm(Base, BaseModel):
1460 1460 __tablename__ = 'repo_to_perm'
1461 1461 __table_args__ = (
1462 1462 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1463 1463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1464 1464 'mysql_charset': 'utf8'}
1465 1465 )
1466 1466 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1467 1467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1468 1468 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1469 1469 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1470 1470
1471 1471 user = relationship('User')
1472 1472 repository = relationship('Repository')
1473 1473 permission = relationship('Permission')
1474 1474
1475 1475 @classmethod
1476 1476 def create(cls, user, repository, permission):
1477 1477 n = cls()
1478 1478 n.user = user
1479 1479 n.repository = repository
1480 1480 n.permission = permission
1481 1481 Session().add(n)
1482 1482 return n
1483 1483
1484 1484 def __unicode__(self):
1485 1485 return u'<user:%s => %s >' % (self.user, self.repository)
1486 1486
1487 1487
1488 1488 class UserToPerm(Base, BaseModel):
1489 1489 __tablename__ = 'user_to_perm'
1490 1490 __table_args__ = (
1491 1491 UniqueConstraint('user_id', 'permission_id'),
1492 1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1493 1493 'mysql_charset': 'utf8'}
1494 1494 )
1495 1495 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1496 1496 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1497 1497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1498 1498
1499 1499 user = relationship('User')
1500 1500 permission = relationship('Permission', lazy='joined')
1501 1501
1502 1502
1503 1503 class UserGroupRepoToPerm(Base, BaseModel):
1504 1504 __tablename__ = 'users_group_repo_to_perm'
1505 1505 __table_args__ = (
1506 1506 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1507 1507 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1508 1508 'mysql_charset': 'utf8'}
1509 1509 )
1510 1510 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1511 1511 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1512 1512 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1513 1513 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1514 1514
1515 1515 users_group = relationship('UserGroup')
1516 1516 permission = relationship('Permission')
1517 1517 repository = relationship('Repository')
1518 1518
1519 1519 @classmethod
1520 1520 def create(cls, users_group, repository, permission):
1521 1521 n = cls()
1522 1522 n.users_group = users_group
1523 1523 n.repository = repository
1524 1524 n.permission = permission
1525 1525 Session().add(n)
1526 1526 return n
1527 1527
1528 1528 def __unicode__(self):
1529 1529 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1530 1530
1531 1531
1532 1532 class UserGroupToPerm(Base, BaseModel):
1533 1533 __tablename__ = 'users_group_to_perm'
1534 1534 __table_args__ = (
1535 1535 UniqueConstraint('users_group_id', 'permission_id',),
1536 1536 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1537 1537 'mysql_charset': 'utf8'}
1538 1538 )
1539 1539 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1540 1540 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1541 1541 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1542 1542
1543 1543 users_group = relationship('UserGroup')
1544 1544 permission = relationship('Permission')
1545 1545
1546 1546
1547 1547 class UserRepoGroupToPerm(Base, BaseModel):
1548 1548 __tablename__ = 'user_repo_group_to_perm'
1549 1549 __table_args__ = (
1550 1550 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1551 1551 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1552 1552 'mysql_charset': 'utf8'}
1553 1553 )
1554 1554
1555 1555 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1556 1556 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1557 1557 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1558 1558 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1559 1559
1560 1560 user = relationship('User')
1561 1561 group = relationship('RepoGroup')
1562 1562 permission = relationship('Permission')
1563 1563
1564 1564
1565 1565 class UserGroupRepoGroupToPerm(Base, BaseModel):
1566 1566 __tablename__ = 'users_group_repo_group_to_perm'
1567 1567 __table_args__ = (
1568 1568 UniqueConstraint('users_group_id', 'group_id'),
1569 1569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1570 1570 'mysql_charset': 'utf8'}
1571 1571 )
1572 1572
1573 1573 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1574 1574 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1575 1575 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1576 1576 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1577 1577
1578 1578 users_group = relationship('UserGroup')
1579 1579 permission = relationship('Permission')
1580 1580 group = relationship('RepoGroup')
1581 1581
1582 1582
1583 1583 class Statistics(Base, BaseModel):
1584 1584 __tablename__ = 'statistics'
1585 1585 __table_args__ = (
1586 1586 UniqueConstraint('repository_id'),
1587 1587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1588 1588 'mysql_charset': 'utf8'}
1589 1589 )
1590 1590 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1591 1591 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1592 1592 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1593 1593 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1594 1594 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1595 1595 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1596 1596
1597 1597 repository = relationship('Repository', single_parent=True)
1598 1598
1599 1599
1600 1600 class UserFollowing(Base, BaseModel):
1601 1601 __tablename__ = 'user_followings'
1602 1602 __table_args__ = (
1603 1603 UniqueConstraint('user_id', 'follows_repository_id'),
1604 1604 UniqueConstraint('user_id', 'follows_user_id'),
1605 1605 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1606 1606 'mysql_charset': 'utf8'}
1607 1607 )
1608 1608
1609 1609 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1610 1610 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1611 1611 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1612 1612 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1613 1613 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1614 1614
1615 1615 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1616 1616
1617 1617 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1618 1618 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1619 1619
1620 1620 @classmethod
1621 1621 def get_repo_followers(cls, repo_id):
1622 1622 return cls.query().filter(cls.follows_repo_id == repo_id)
1623 1623
1624 1624
1625 1625 class CacheInvalidation(Base, BaseModel):
1626 1626 __tablename__ = 'cache_invalidation'
1627 1627 __table_args__ = (
1628 1628 UniqueConstraint('cache_key'),
1629 1629 Index('key_idx', 'cache_key'),
1630 1630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1631 1631 'mysql_charset': 'utf8'},
1632 1632 )
1633 1633 # cache_id, not used
1634 1634 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 1635 # cache_key as created by _get_cache_key
1636 1636 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1637 1637 # cache_args is usually a repo_name, possibly with _README/_RSS/_ATOM suffix
1638 1638 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1639 1639 # instance sets cache_active True when it is caching, other instances set cache_active to False to invalidate
1640 1640 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1641 1641
1642 1642 def __init__(self, cache_key, cache_args=''):
1643 1643 self.cache_key = cache_key
1644 1644 self.cache_args = cache_args
1645 1645 self.cache_active = False
1646 1646
1647 1647 def __unicode__(self):
1648 1648 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1649 1649 self.cache_id, self.cache_key)
1650 1650
1651 1651 def get_prefix(self):
1652 1652 """
1653 1653 Guess prefix that might have been used in _get_cache_key to generate self.cache_key .
1654 1654 Only used for informational purposes in repo_edit.html .
1655 1655 """
1656 1656 _split = self.cache_key.split(self.cache_args, 1)
1657 1657 if len(_split) == 2:
1658 1658 return _split[0]
1659 1659 return ''
1660 1660
1661 1661 @classmethod
1662 1662 def _get_cache_key(cls, key):
1663 1663 """
1664 1664 Wrapper for generating a unique cache key for this instance and "key".
1665 1665 """
1666 1666 import rhodecode
1667 1667 prefix = rhodecode.CONFIG.get('instance_id', '')
1668 1668 return "%s%s" % (prefix, key)
1669 1669
1670 1670 @classmethod
1671 1671 def _get_or_create_inv_obj(cls, key, repo_name, commit=True):
1672 1672 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1673 1673 if not inv_obj:
1674 1674 try:
1675 1675 inv_obj = CacheInvalidation(key, repo_name)
1676 1676 Session().add(inv_obj)
1677 1677 if commit:
1678 1678 Session().commit()
1679 1679 except Exception:
1680 1680 log.error(traceback.format_exc())
1681 1681 Session().rollback()
1682 1682 return inv_obj
1683 1683
1684 1684 @classmethod
1685 1685 def invalidate(cls, key):
1686 1686 """
1687 1687 Returns Invalidation object if this given key should be invalidated
1688 1688 None otherwise. `cache_active = False` means that this cache
1689 1689 state is not valid and needs to be invalidated
1690 1690
1691 1691 :param key:
1692 1692 """
1693 1693 repo_name = key
1694 1694 repo_name = remove_suffix(repo_name, '_README')
1695 1695 repo_name = remove_suffix(repo_name, '_RSS')
1696 1696 repo_name = remove_suffix(repo_name, '_ATOM')
1697 1697
1698 1698 cache_key = cls._get_cache_key(key)
1699 1699 inv = cls._get_or_create_inv_obj(cache_key, repo_name)
1700 1700
1701 if inv and inv.cache_active is False:
1701 if inv and not inv.cache_active:
1702 1702 return inv
1703 1703
1704 1704 @classmethod
1705 1705 def set_invalidate(cls, key=None, repo_name=None):
1706 1706 """
1707 1707 Mark this Cache key for invalidation, either by key or whole
1708 1708 cache sets based on repo_name
1709 1709
1710 1710 :param key:
1711 1711 """
1712 1712 invalidated_keys = []
1713 1713 if key:
1714 1714 assert not repo_name
1715 1715 cache_key = cls._get_cache_key(key)
1716 1716 inv_objs = Session().query(cls).filter(cls.cache_key == cache_key).all()
1717 1717 else:
1718 1718 assert repo_name
1719 1719 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1720 1720
1721 1721 try:
1722 1722 for inv_obj in inv_objs:
1723 1723 inv_obj.cache_active = False
1724 1724 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1725 1725 % (inv_obj, key, safe_str(repo_name)))
1726 1726 invalidated_keys.append(inv_obj.cache_key)
1727 1727 Session().add(inv_obj)
1728 1728 Session().commit()
1729 1729 except Exception:
1730 1730 log.error(traceback.format_exc())
1731 1731 Session().rollback()
1732 1732 return invalidated_keys
1733 1733
1734 1734 @classmethod
1735 1735 def set_valid(cls, key):
1736 1736 """
1737 1737 Mark this cache key as active and currently cached
1738 1738
1739 1739 :param key:
1740 1740 """
1741 1741 inv_obj = cls.query().filter(cls.cache_key == key).scalar()
1742 1742 inv_obj.cache_active = True
1743 1743 Session().add(inv_obj)
1744 1744 Session().commit()
1745 1745
1746 1746 @classmethod
1747 1747 def get_cache_map(cls):
1748 1748
1749 1749 class cachemapdict(dict):
1750 1750
1751 1751 def __init__(self, *args, **kwargs):
1752 1752 self.fixkey = kwargs.pop('fixkey', False)
1753 1753 super(cachemapdict, self).__init__(*args, **kwargs)
1754 1754
1755 1755 def __getattr__(self, name):
1756 1756 cache_key = name
1757 1757 if self.fixkey:
1758 1758 cache_key = cls._get_cache_key(name)
1759 1759 if cache_key in self.__dict__:
1760 1760 return self.__dict__[cache_key]
1761 1761 else:
1762 1762 return self[cache_key]
1763 1763
1764 1764 def __getitem__(self, name):
1765 1765 cache_key = name
1766 1766 if self.fixkey:
1767 1767 cache_key = cls._get_cache_key(name)
1768 1768 try:
1769 1769 return super(cachemapdict, self).__getitem__(cache_key)
1770 1770 except KeyError:
1771 1771 return None
1772 1772
1773 1773 cache_map = cachemapdict(fixkey=True)
1774 1774 for obj in cls.query().all():
1775 1775 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1776 1776 return cache_map
1777 1777
1778 1778
1779 1779 class ChangesetComment(Base, BaseModel):
1780 1780 __tablename__ = 'changeset_comments'
1781 1781 __table_args__ = (
1782 1782 Index('cc_revision_idx', 'revision'),
1783 1783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1784 1784 'mysql_charset': 'utf8'},
1785 1785 )
1786 1786 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1787 1787 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1788 1788 revision = Column('revision', String(40), nullable=True)
1789 1789 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1790 1790 line_no = Column('line_no', Unicode(10), nullable=True)
1791 1791 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1792 1792 f_path = Column('f_path', Unicode(1000), nullable=True)
1793 1793 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1794 1794 text = Column('text', UnicodeText(25000), nullable=False)
1795 1795 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1796 1796 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1797 1797
1798 1798 author = relationship('User', lazy='joined')
1799 1799 repo = relationship('Repository')
1800 1800 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1801 1801 pull_request = relationship('PullRequest', lazy='joined')
1802 1802
1803 1803 @classmethod
1804 1804 def get_users(cls, revision=None, pull_request_id=None):
1805 1805 """
1806 1806 Returns user associated with this ChangesetComment. ie those
1807 1807 who actually commented
1808 1808
1809 1809 :param cls:
1810 1810 :param revision:
1811 1811 """
1812 1812 q = Session().query(User)\
1813 1813 .join(ChangesetComment.author)
1814 1814 if revision:
1815 1815 q = q.filter(cls.revision == revision)
1816 1816 elif pull_request_id:
1817 1817 q = q.filter(cls.pull_request_id == pull_request_id)
1818 1818 return q.all()
1819 1819
1820 1820
1821 1821 class ChangesetStatus(Base, BaseModel):
1822 1822 __tablename__ = 'changeset_statuses'
1823 1823 __table_args__ = (
1824 1824 Index('cs_revision_idx', 'revision'),
1825 1825 Index('cs_version_idx', 'version'),
1826 1826 UniqueConstraint('repo_id', 'revision', 'version'),
1827 1827 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1828 1828 'mysql_charset': 'utf8'}
1829 1829 )
1830 1830 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1831 1831 STATUS_APPROVED = 'approved'
1832 1832 STATUS_REJECTED = 'rejected'
1833 1833 STATUS_UNDER_REVIEW = 'under_review'
1834 1834
1835 1835 STATUSES = [
1836 1836 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1837 1837 (STATUS_APPROVED, _("Approved")),
1838 1838 (STATUS_REJECTED, _("Rejected")),
1839 1839 (STATUS_UNDER_REVIEW, _("Under Review")),
1840 1840 ]
1841 1841
1842 1842 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1843 1843 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1844 1844 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1845 1845 revision = Column('revision', String(40), nullable=False)
1846 1846 status = Column('status', String(128), nullable=False, default=DEFAULT)
1847 1847 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1848 1848 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1849 1849 version = Column('version', Integer(), nullable=False, default=0)
1850 1850 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1851 1851
1852 1852 author = relationship('User', lazy='joined')
1853 1853 repo = relationship('Repository')
1854 1854 comment = relationship('ChangesetComment', lazy='joined')
1855 1855 pull_request = relationship('PullRequest', lazy='joined')
1856 1856
1857 1857 def __unicode__(self):
1858 1858 return u"<%s('%s:%s')>" % (
1859 1859 self.__class__.__name__,
1860 1860 self.status, self.author
1861 1861 )
1862 1862
1863 1863 @classmethod
1864 1864 def get_status_lbl(cls, value):
1865 1865 return dict(cls.STATUSES).get(value)
1866 1866
1867 1867 @property
1868 1868 def status_lbl(self):
1869 1869 return ChangesetStatus.get_status_lbl(self.status)
1870 1870
1871 1871
1872 1872 class PullRequest(Base, BaseModel):
1873 1873 __tablename__ = 'pull_requests'
1874 1874 __table_args__ = (
1875 1875 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1876 1876 'mysql_charset': 'utf8'},
1877 1877 )
1878 1878
1879 1879 STATUS_NEW = u'new'
1880 1880 STATUS_OPEN = u'open'
1881 1881 STATUS_CLOSED = u'closed'
1882 1882
1883 1883 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1884 1884 title = Column('title', Unicode(256), nullable=True)
1885 1885 description = Column('description', UnicodeText(10240), nullable=True)
1886 1886 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1887 1887 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1888 1888 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1889 1889 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1890 1890 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1891 1891 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1892 1892 org_ref = Column('org_ref', Unicode(256), nullable=False)
1893 1893 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1894 1894 other_ref = Column('other_ref', Unicode(256), nullable=False)
1895 1895
1896 1896 @hybrid_property
1897 1897 def revisions(self):
1898 1898 return self._revisions.split(':')
1899 1899
1900 1900 @revisions.setter
1901 1901 def revisions(self, val):
1902 1902 self._revisions = ':'.join(val)
1903 1903
1904 1904 @property
1905 1905 def org_ref_parts(self):
1906 1906 return self.org_ref.split(':')
1907 1907
1908 1908 @property
1909 1909 def other_ref_parts(self):
1910 1910 return self.other_ref.split(':')
1911 1911
1912 1912 author = relationship('User', lazy='joined')
1913 1913 reviewers = relationship('PullRequestReviewers',
1914 1914 cascade="all, delete, delete-orphan")
1915 1915 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1916 1916 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1917 1917 statuses = relationship('ChangesetStatus')
1918 1918 comments = relationship('ChangesetComment',
1919 1919 cascade="all, delete, delete-orphan")
1920 1920
1921 1921 def is_closed(self):
1922 1922 return self.status == self.STATUS_CLOSED
1923 1923
1924 1924 @property
1925 1925 def last_review_status(self):
1926 1926 return self.statuses[-1].status if self.statuses else ''
1927 1927
1928 1928 def __json__(self):
1929 1929 return dict(
1930 1930 revisions=self.revisions
1931 1931 )
1932 1932
1933 1933
1934 1934 class PullRequestReviewers(Base, BaseModel):
1935 1935 __tablename__ = 'pull_request_reviewers'
1936 1936 __table_args__ = (
1937 1937 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1938 1938 'mysql_charset': 'utf8'},
1939 1939 )
1940 1940
1941 1941 def __init__(self, user=None, pull_request=None):
1942 1942 self.user = user
1943 1943 self.pull_request = pull_request
1944 1944
1945 1945 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1946 1946 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1947 1947 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1948 1948
1949 1949 user = relationship('User')
1950 1950 pull_request = relationship('PullRequest')
1951 1951
1952 1952
1953 1953 class Notification(Base, BaseModel):
1954 1954 __tablename__ = 'notifications'
1955 1955 __table_args__ = (
1956 1956 Index('notification_type_idx', 'type'),
1957 1957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1958 1958 'mysql_charset': 'utf8'},
1959 1959 )
1960 1960
1961 1961 TYPE_CHANGESET_COMMENT = u'cs_comment'
1962 1962 TYPE_MESSAGE = u'message'
1963 1963 TYPE_MENTION = u'mention'
1964 1964 TYPE_REGISTRATION = u'registration'
1965 1965 TYPE_PULL_REQUEST = u'pull_request'
1966 1966 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1967 1967
1968 1968 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1969 1969 subject = Column('subject', Unicode(512), nullable=True)
1970 1970 body = Column('body', UnicodeText(50000), nullable=True)
1971 1971 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1972 1972 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1973 1973 type_ = Column('type', Unicode(256))
1974 1974
1975 1975 created_by_user = relationship('User')
1976 1976 notifications_to_users = relationship('UserNotification', lazy='joined',
1977 1977 cascade="all, delete, delete-orphan")
1978 1978
1979 1979 @property
1980 1980 def recipients(self):
1981 1981 return [x.user for x in UserNotification.query()\
1982 1982 .filter(UserNotification.notification == self)\
1983 1983 .order_by(UserNotification.user_id.asc()).all()]
1984 1984
1985 1985 @classmethod
1986 1986 def create(cls, created_by, subject, body, recipients, type_=None):
1987 1987 if type_ is None:
1988 1988 type_ = Notification.TYPE_MESSAGE
1989 1989
1990 1990 notification = cls()
1991 1991 notification.created_by_user = created_by
1992 1992 notification.subject = subject
1993 1993 notification.body = body
1994 1994 notification.type_ = type_
1995 1995 notification.created_on = datetime.datetime.now()
1996 1996
1997 1997 for u in recipients:
1998 1998 assoc = UserNotification()
1999 1999 assoc.notification = notification
2000 2000 u.notifications.append(assoc)
2001 2001 Session().add(notification)
2002 2002 return notification
2003 2003
2004 2004 @property
2005 2005 def description(self):
2006 2006 from rhodecode.model.notification import NotificationModel
2007 2007 return NotificationModel().make_description(self)
2008 2008
2009 2009
2010 2010 class UserNotification(Base, BaseModel):
2011 2011 __tablename__ = 'user_to_notification'
2012 2012 __table_args__ = (
2013 2013 UniqueConstraint('user_id', 'notification_id'),
2014 2014 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2015 2015 'mysql_charset': 'utf8'}
2016 2016 )
2017 2017 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2018 2018 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2019 2019 read = Column('read', Boolean, default=False)
2020 2020 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2021 2021
2022 2022 user = relationship('User', lazy="joined")
2023 2023 notification = relationship('Notification', lazy="joined",
2024 2024 order_by=lambda: Notification.created_on.desc(),)
2025 2025
2026 2026 def mark_as_read(self):
2027 2027 self.read = True
2028 2028 Session().add(self)
2029 2029
2030 2030
2031 2031 class DbMigrateVersion(Base, BaseModel):
2032 2032 __tablename__ = 'db_migrate_version'
2033 2033 __table_args__ = (
2034 2034 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2035 2035 'mysql_charset': 'utf8'},
2036 2036 )
2037 2037 repository_id = Column('repository_id', String(250), primary_key=True)
2038 2038 repository_path = Column('repository_path', Text)
2039 2039 version = Column('version', Integer)
@@ -1,283 +1,283
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import tmpl_context as c
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import Notification, User, UserNotification
38 38 from rhodecode.model.meta import Session
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class NotificationModel(BaseModel):
44 44
45 45 cls = Notification
46 46
47 47 def __get_notification(self, notification):
48 48 if isinstance(notification, Notification):
49 49 return notification
50 50 elif isinstance(notification, (int, long)):
51 51 return Notification.get(notification)
52 52 else:
53 53 if notification:
54 54 raise Exception('notification must be int, long or Instance'
55 55 ' of Notification got %s' % type(notification))
56 56
57 57 def create(self, created_by, subject, body, recipients=None,
58 58 type_=Notification.TYPE_MESSAGE, with_email=True,
59 59 email_kwargs={}, email_subject=None):
60 60 """
61 61
62 62 Creates notification of given type
63 63
64 64 :param created_by: int, str or User instance. User who created this
65 65 notification
66 66 :param subject:
67 67 :param body:
68 68 :param recipients: list of int, str or User objects, when None
69 69 is given send to all admins
70 70 :param type_: type of notification
71 71 :param with_email: send email with this notification
72 72 :param email_kwargs: additional dict to pass as args to email template
73 73 :param email_subject: use given subject as email subject
74 74 """
75 75 from rhodecode.lib.celerylib import tasks, run_task
76 76
77 77 if recipients and not getattr(recipients, '__iter__', False):
78 78 raise Exception('recipients must be a list or iterable')
79 79
80 80 created_by_obj = self._get_user(created_by)
81 81
82 82 if recipients:
83 83 recipients_objs = []
84 84 for u in recipients:
85 85 obj = self._get_user(u)
86 86 if obj:
87 87 recipients_objs.append(obj)
88 88 recipients_objs = set(recipients_objs)
89 89 log.debug('sending notifications %s to %s' % (
90 90 type_, recipients_objs)
91 91 )
92 92 else:
93 93 # empty recipients means to all admins
94 94 recipients_objs = User.query().filter(User.admin == True).all()
95 95 log.debug('sending notifications %s to admins: %s' % (
96 96 type_, recipients_objs)
97 97 )
98 98 notif = Notification.create(
99 99 created_by=created_by_obj, subject=subject,
100 100 body=body, recipients=recipients_objs, type_=type_
101 101 )
102 102
103 if with_email is False:
103 if not with_email:
104 104 return notif
105 105
106 106 #don't send email to person who created this comment
107 107 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
108 108
109 109 # send email with notification to all other participants
110 110 for rec in rec_objs:
111 111 if not email_subject:
112 112 email_subject = NotificationModel()\
113 113 .make_description(notif, show_age=False)
114 114 type_ = type_
115 115 email_body = None # we set body to none, we just send HTML emails
116 116 ## this is passed into template
117 117 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
118 118 kwargs.update(email_kwargs)
119 119 email_body_html = EmailNotificationModel()\
120 120 .get_email_tmpl(type_, **kwargs)
121 121
122 122 run_task(tasks.send_email, rec.email, email_subject, email_body,
123 123 email_body_html)
124 124
125 125 return notif
126 126
127 127 def delete(self, user, notification):
128 128 # we don't want to remove actual notification just the assignment
129 129 try:
130 130 notification = self.__get_notification(notification)
131 131 user = self._get_user(user)
132 132 if notification and user:
133 133 obj = UserNotification.query()\
134 134 .filter(UserNotification.user == user)\
135 135 .filter(UserNotification.notification
136 136 == notification)\
137 137 .one()
138 138 Session().delete(obj)
139 139 return True
140 140 except Exception:
141 141 log.error(traceback.format_exc())
142 142 raise
143 143
144 144 def get_for_user(self, user, filter_=None):
145 145 """
146 146 Get mentions for given user, filter them if filter dict is given
147 147
148 148 :param user:
149 149 :param filter:
150 150 """
151 151 user = self._get_user(user)
152 152
153 153 q = UserNotification.query()\
154 154 .filter(UserNotification.user == user)\
155 155 .join((Notification, UserNotification.notification_id ==
156 156 Notification.notification_id))
157 157
158 158 if filter_:
159 159 q = q.filter(Notification.type_.in_(filter_))
160 160
161 161 return q.all()
162 162
163 163 def mark_read(self, user, notification):
164 164 try:
165 165 notification = self.__get_notification(notification)
166 166 user = self._get_user(user)
167 167 if notification and user:
168 168 obj = UserNotification.query()\
169 169 .filter(UserNotification.user == user)\
170 170 .filter(UserNotification.notification
171 171 == notification)\
172 172 .one()
173 173 obj.read = True
174 174 Session().add(obj)
175 175 return True
176 176 except Exception:
177 177 log.error(traceback.format_exc())
178 178 raise
179 179
180 180 def mark_all_read_for_user(self, user, filter_=None):
181 181 user = self._get_user(user)
182 182 q = UserNotification.query()\
183 183 .filter(UserNotification.user == user)\
184 184 .filter(UserNotification.read == False)\
185 185 .join((Notification, UserNotification.notification_id ==
186 186 Notification.notification_id))
187 187 if filter_:
188 188 q = q.filter(Notification.type_.in_(filter_))
189 189
190 190 # this is a little inefficient but sqlalchemy doesn't support
191 191 # update on joined tables :(
192 192 for obj in q.all():
193 193 obj.read = True
194 194 Session().add(obj)
195 195
196 196 def get_unread_cnt_for_user(self, user):
197 197 user = self._get_user(user)
198 198 return UserNotification.query()\
199 199 .filter(UserNotification.read == False)\
200 200 .filter(UserNotification.user == user).count()
201 201
202 202 def get_unread_for_user(self, user):
203 203 user = self._get_user(user)
204 204 return [x.notification for x in UserNotification.query()\
205 205 .filter(UserNotification.read == False)\
206 206 .filter(UserNotification.user == user).all()]
207 207
208 208 def get_user_notification(self, user, notification):
209 209 user = self._get_user(user)
210 210 notification = self.__get_notification(notification)
211 211
212 212 return UserNotification.query()\
213 213 .filter(UserNotification.notification == notification)\
214 214 .filter(UserNotification.user == user).scalar()
215 215
216 216 def make_description(self, notification, show_age=True):
217 217 """
218 218 Creates a human readable description based on properties
219 219 of notification object
220 220 """
221 221 #alias
222 222 _n = notification
223 223 _map = {
224 224 _n.TYPE_CHANGESET_COMMENT: _('commented on changeset at %(when)s'),
225 225 _n.TYPE_MESSAGE: _('sent message at %(when)s'),
226 226 _n.TYPE_MENTION: _('mentioned you at %(when)s'),
227 227 _n.TYPE_REGISTRATION: _('registered in RhodeCode at %(when)s'),
228 228 _n.TYPE_PULL_REQUEST: _('opened new pull request at %(when)s'),
229 229 _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request at %(when)s')
230 230 }
231 231
232 232 # action == _map string
233 233 tmpl = "%(user)s %(action)s "
234 234 if show_age:
235 235 when = h.age(notification.created_on)
236 236 else:
237 237 when = h.fmt_date(notification.created_on)
238 238
239 239 data = dict(
240 240 user=notification.created_by_user.username,
241 241 action=_map[notification.type_] % {'when': when},
242 242 )
243 243 return tmpl % data
244 244
245 245
246 246 class EmailNotificationModel(BaseModel):
247 247
248 248 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
249 249 TYPE_PASSWORD_RESET = 'password_link'
250 250 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
251 251 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
252 252 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
253 253 TYPE_DEFAULT = 'default'
254 254
255 255 def __init__(self):
256 256 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
257 257 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
258 258
259 259 self.email_types = {
260 260 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
261 261 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
262 262 self.TYPE_REGISTRATION: 'email_templates/registration.html',
263 263 self.TYPE_DEFAULT: 'email_templates/default.html',
264 264 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
265 265 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
266 266 }
267 267
268 268 def get_email_tmpl(self, type_, **kwargs):
269 269 """
270 270 return generated template for email based on given type
271 271
272 272 :param type_:
273 273 """
274 274
275 275 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
276 276 email_template = self._tmpl_lookup.get_template(base)
277 277 # translator and helpers inject
278 278 _kwargs = {'_': _,
279 279 'h': h,
280 280 'c': c}
281 281 _kwargs.update(kwargs)
282 282 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
283 283 return email_template.render(**_kwargs)
@@ -1,144 +1,144
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.permission
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 permissions model for RhodeCode
7 7
8 8 :created_on: Aug 20, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from sqlalchemy.exc import DatabaseError
30 30
31 31 from rhodecode.lib.caching_query import FromCache
32 32
33 33 from rhodecode.model import BaseModel
34 34 from rhodecode.model.db import User, Permission, UserToPerm, UserRepoToPerm,\
35 35 UserRepoGroupToPerm
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class PermissionModel(BaseModel):
41 41 """
42 42 Permissions model for RhodeCode
43 43 """
44 44
45 45 cls = Permission
46 46
47 47 def get_permission(self, permission_id, cache=False):
48 48 """
49 49 Get's permissions by id
50 50
51 51 :param permission_id: id of permission to get from database
52 52 :param cache: use Cache for this query
53 53 """
54 54 perm = self.sa.query(Permission)
55 55 if cache:
56 56 perm = perm.options(FromCache("sql_cache_short",
57 57 "get_permission_%s" % permission_id))
58 58 return perm.get(permission_id)
59 59
60 60 def get_permission_by_name(self, name, cache=False):
61 61 """
62 62 Get's permissions by given name
63 63
64 64 :param name: name to fetch
65 65 :param cache: Use cache for this query
66 66 """
67 67 perm = self.sa.query(Permission)\
68 68 .filter(Permission.permission_name == name)
69 69 if cache:
70 70 perm = perm.options(FromCache("sql_cache_short",
71 71 "get_permission_%s" % name))
72 72 return perm.scalar()
73 73
74 74 def update(self, form_result):
75 75 perm_user = self.sa.query(User)\
76 76 .filter(User.username ==
77 77 form_result['perm_user_name']).scalar()
78 78 u2p = self.sa.query(UserToPerm).filter(UserToPerm.user ==
79 79 perm_user).all()
80 80 if len(u2p) != len(User.DEFAULT_PERMISSIONS):
81 81 raise Exception('Defined: %s should be %s permissions for default'
82 82 ' user. This should not happen please verify'
83 83 ' your database' % (len(u2p), len(User.DEFAULT_PERMISSIONS)))
84 84
85 85 try:
86 86 # stage 1 change defaults
87 87 for p in u2p:
88 88 if p.permission.permission_name.startswith('repository.'):
89 89 p.permission = self.get_permission_by_name(
90 90 form_result['default_repo_perm'])
91 91 self.sa.add(p)
92 92
93 93 elif p.permission.permission_name.startswith('group.'):
94 94 p.permission = self.get_permission_by_name(
95 95 form_result['default_group_perm'])
96 96 self.sa.add(p)
97 97
98 98 elif p.permission.permission_name.startswith('hg.register.'):
99 99 p.permission = self.get_permission_by_name(
100 100 form_result['default_register'])
101 101 self.sa.add(p)
102 102
103 103 elif p.permission.permission_name.startswith('hg.create.'):
104 104 p.permission = self.get_permission_by_name(
105 105 form_result['default_create'])
106 106 self.sa.add(p)
107 107
108 108 elif p.permission.permission_name.startswith('hg.fork.'):
109 109 p.permission = self.get_permission_by_name(
110 110 form_result['default_fork'])
111 111 self.sa.add(p)
112 112
113 113 #stage 2 update all default permissions for repos if checked
114 114 if form_result['overwrite_default_repo'] == True:
115 115 _def_name = form_result['default_repo_perm'].split('repository.')[-1]
116 116 _def = self.get_permission_by_name('repository.' + _def_name)
117 117 # repos
118 118 for r2p in self.sa.query(UserRepoToPerm)\
119 119 .filter(UserRepoToPerm.user == perm_user)\
120 120 .all():
121 121
122 122 #don't reset PRIVATE repositories
123 if r2p.repository.private is False:
123 if not r2p.repository.private:
124 124 r2p.permission = _def
125 125 self.sa.add(r2p)
126 126
127 127 if form_result['overwrite_default_group'] == True:
128 128 _def_name = form_result['default_group_perm'].split('group.')[-1]
129 129 # groups
130 130 _def = self.get_permission_by_name('group.' + _def_name)
131 131 for g2p in self.sa.query(UserRepoGroupToPerm)\
132 132 .filter(UserRepoGroupToPerm.user == perm_user)\
133 133 .all():
134 134 g2p.permission = _def
135 135 self.sa.add(g2p)
136 136
137 137 # stage 3 set anonymous access
138 138 if perm_user.username == 'default':
139 139 perm_user.active = bool(form_result['anonymous'])
140 140 self.sa.add(perm_user)
141 141
142 142 except (DatabaseError,):
143 143 log.error(traceback.format_exc())
144 144 raise
@@ -1,763 +1,763
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import itertools
29 29 import collections
30 30 from pylons import url
31 31 from pylons.i18n.translation import _
32 32
33 33 from sqlalchemy.exc import DatabaseError
34 34 from sqlalchemy.orm import joinedload
35 35
36 36 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
37 37 from rhodecode.lib.caching_query import FromCache
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
40 40 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
41 41 Notification, RepoGroup, UserRepoGroupToPerm, UserGroupRepoGroupToPerm, \
42 42 UserEmailMap, UserIpMap
43 43 from rhodecode.lib.exceptions import DefaultUserException, \
44 44 UserOwnsReposException
45 45 from rhodecode.model.meta import Session
46 46
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50 PERM_WEIGHTS = Permission.PERM_WEIGHTS
51 51
52 52
53 53 class UserModel(BaseModel):
54 54 cls = User
55 55
56 56 def get(self, user_id, cache=False):
57 57 user = self.sa.query(User)
58 58 if cache:
59 59 user = user.options(FromCache("sql_cache_short",
60 60 "get_user_%s" % user_id))
61 61 return user.get(user_id)
62 62
63 63 def get_user(self, user):
64 64 return self._get_user(user)
65 65
66 66 def get_by_username(self, username, cache=False, case_insensitive=False):
67 67
68 68 if case_insensitive:
69 69 user = self.sa.query(User).filter(User.username.ilike(username))
70 70 else:
71 71 user = self.sa.query(User)\
72 72 .filter(User.username == username)
73 73 if cache:
74 74 user = user.options(FromCache("sql_cache_short",
75 75 "get_user_%s" % username))
76 76 return user.scalar()
77 77
78 78 def get_by_email(self, email, cache=False, case_insensitive=False):
79 79 return User.get_by_email(email, case_insensitive, cache)
80 80
81 81 def get_by_api_key(self, api_key, cache=False):
82 82 return User.get_by_api_key(api_key, cache)
83 83
84 84 def create(self, form_data):
85 85 from rhodecode.lib.auth import get_crypt_password
86 86 try:
87 87 new_user = User()
88 88 for k, v in form_data.items():
89 89 if k == 'password':
90 90 v = get_crypt_password(v)
91 91 if k == 'firstname':
92 92 k = 'name'
93 93 setattr(new_user, k, v)
94 94
95 95 new_user.api_key = generate_api_key(form_data['username'])
96 96 self.sa.add(new_user)
97 97 return new_user
98 98 except:
99 99 log.error(traceback.format_exc())
100 100 raise
101 101
102 102 def create_or_update(self, username, password, email, firstname='',
103 103 lastname='', active=True, admin=False, ldap_dn=None):
104 104 """
105 105 Creates a new instance if not found, or updates current one
106 106
107 107 :param username:
108 108 :param password:
109 109 :param email:
110 110 :param active:
111 111 :param firstname:
112 112 :param lastname:
113 113 :param active:
114 114 :param admin:
115 115 :param ldap_dn:
116 116 """
117 117
118 118 from rhodecode.lib.auth import get_crypt_password
119 119
120 120 log.debug('Checking for %s account in RhodeCode database' % username)
121 121 user = User.get_by_username(username, case_insensitive=True)
122 122 if user is None:
123 123 log.debug('creating new user %s' % username)
124 124 new_user = User()
125 125 edit = False
126 126 else:
127 127 log.debug('updating user %s' % username)
128 128 new_user = user
129 129 edit = True
130 130
131 131 try:
132 132 new_user.username = username
133 133 new_user.admin = admin
134 134 # set password only if creating an user or password is changed
135 if edit is False or user.password != password:
135 if not edit or user.password != password:
136 136 new_user.password = get_crypt_password(password)
137 137 new_user.api_key = generate_api_key(username)
138 138 new_user.email = email
139 139 new_user.active = active
140 140 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
141 141 new_user.name = firstname
142 142 new_user.lastname = lastname
143 143 self.sa.add(new_user)
144 144 return new_user
145 145 except (DatabaseError,):
146 146 log.error(traceback.format_exc())
147 147 raise
148 148
149 149 def create_for_container_auth(self, username, attrs):
150 150 """
151 151 Creates the given user if it's not already in the database
152 152
153 153 :param username:
154 154 :param attrs:
155 155 """
156 156 if self.get_by_username(username, case_insensitive=True) is None:
157 157
158 158 # autogenerate email for container account without one
159 159 generate_email = lambda usr: '%s@container_auth.account' % usr
160 160
161 161 try:
162 162 new_user = User()
163 163 new_user.username = username
164 164 new_user.password = None
165 165 new_user.api_key = generate_api_key(username)
166 166 new_user.email = attrs['email']
167 167 new_user.active = attrs.get('active', True)
168 168 new_user.name = attrs['name'] or generate_email(username)
169 169 new_user.lastname = attrs['lastname']
170 170
171 171 self.sa.add(new_user)
172 172 return new_user
173 173 except (DatabaseError,):
174 174 log.error(traceback.format_exc())
175 175 self.sa.rollback()
176 176 raise
177 177 log.debug('User %s already exists. Skipping creation of account'
178 178 ' for container auth.', username)
179 179 return None
180 180
181 181 def create_ldap(self, username, password, user_dn, attrs):
182 182 """
183 183 Checks if user is in database, if not creates this user marked
184 184 as ldap user
185 185
186 186 :param username:
187 187 :param password:
188 188 :param user_dn:
189 189 :param attrs:
190 190 """
191 191 from rhodecode.lib.auth import get_crypt_password
192 192 log.debug('Checking for such ldap account in RhodeCode database')
193 193 if self.get_by_username(username, case_insensitive=True) is None:
194 194
195 195 # autogenerate email for ldap account without one
196 196 generate_email = lambda usr: '%s@ldap.account' % usr
197 197
198 198 try:
199 199 new_user = User()
200 200 username = username.lower()
201 201 # add ldap account always lowercase
202 202 new_user.username = username
203 203 new_user.password = get_crypt_password(password)
204 204 new_user.api_key = generate_api_key(username)
205 205 new_user.email = attrs['email'] or generate_email(username)
206 206 new_user.active = attrs.get('active', True)
207 207 new_user.ldap_dn = safe_unicode(user_dn)
208 208 new_user.name = attrs['name']
209 209 new_user.lastname = attrs['lastname']
210 210
211 211 self.sa.add(new_user)
212 212 return new_user
213 213 except (DatabaseError,):
214 214 log.error(traceback.format_exc())
215 215 self.sa.rollback()
216 216 raise
217 217 log.debug('this %s user exists skipping creation of ldap account',
218 218 username)
219 219 return None
220 220
221 221 def create_registration(self, form_data):
222 222 from rhodecode.model.notification import NotificationModel
223 223
224 224 try:
225 225 form_data['admin'] = False
226 226 new_user = self.create(form_data)
227 227
228 228 self.sa.add(new_user)
229 229 self.sa.flush()
230 230
231 231 # notification to admins
232 232 subject = _('new user registration')
233 233 body = ('New user registration\n'
234 234 '---------------------\n'
235 235 '- Username: %s\n'
236 236 '- Full Name: %s\n'
237 237 '- Email: %s\n')
238 238 body = body % (new_user.username, new_user.full_name,
239 239 new_user.email)
240 240 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
241 241 kw = {'registered_user_url': edit_url}
242 242 NotificationModel().create(created_by=new_user, subject=subject,
243 243 body=body, recipients=None,
244 244 type_=Notification.TYPE_REGISTRATION,
245 245 email_kwargs=kw)
246 246
247 247 except:
248 248 log.error(traceback.format_exc())
249 249 raise
250 250
251 251 def update(self, user_id, form_data, skip_attrs=[]):
252 252 from rhodecode.lib.auth import get_crypt_password
253 253 try:
254 254 user = self.get(user_id, cache=False)
255 255 if user.username == 'default':
256 256 raise DefaultUserException(
257 257 _("You can't Edit this user since it's"
258 258 " crucial for entire application"))
259 259
260 260 for k, v in form_data.items():
261 261 if k in skip_attrs:
262 262 continue
263 263 if k == 'new_password' and v:
264 264 user.password = get_crypt_password(v)
265 265 user.api_key = generate_api_key(user.username)
266 266 else:
267 267 if k == 'firstname':
268 268 k = 'name'
269 269 setattr(user, k, v)
270 270 self.sa.add(user)
271 271 except:
272 272 log.error(traceback.format_exc())
273 273 raise
274 274
275 275 def update_user(self, user, **kwargs):
276 276 from rhodecode.lib.auth import get_crypt_password
277 277 try:
278 278 user = self._get_user(user)
279 279 if user.username == 'default':
280 280 raise DefaultUserException(
281 281 _("You can't Edit this user since it's"
282 282 " crucial for entire application")
283 283 )
284 284
285 285 for k, v in kwargs.items():
286 286 if k == 'password' and v:
287 287 v = get_crypt_password(v)
288 288 user.api_key = generate_api_key(user.username)
289 289
290 290 setattr(user, k, v)
291 291 self.sa.add(user)
292 292 return user
293 293 except:
294 294 log.error(traceback.format_exc())
295 295 raise
296 296
297 297 def delete(self, user):
298 298 user = self._get_user(user)
299 299
300 300 try:
301 301 if user.username == 'default':
302 302 raise DefaultUserException(
303 303 _(u"You can't remove this user since it's"
304 304 " crucial for entire application")
305 305 )
306 306 if user.repositories:
307 307 repos = [x.repo_name for x in user.repositories]
308 308 raise UserOwnsReposException(
309 309 _(u'user "%s" still owns %s repositories and cannot be '
310 310 'removed. Switch owners or remove those repositories. %s')
311 311 % (user.username, len(repos), ', '.join(repos))
312 312 )
313 313 self.sa.delete(user)
314 314 except:
315 315 log.error(traceback.format_exc())
316 316 raise
317 317
318 318 def reset_password_link(self, data):
319 319 from rhodecode.lib.celerylib import tasks, run_task
320 320 from rhodecode.model.notification import EmailNotificationModel
321 321 user_email = data['email']
322 322 try:
323 323 user = User.get_by_email(user_email)
324 324 if user:
325 325 log.debug('password reset user found %s' % user)
326 326 link = url('reset_password_confirmation', key=user.api_key,
327 327 qualified=True)
328 328 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
329 329 body = EmailNotificationModel().get_email_tmpl(reg_type,
330 330 **{'user': user.short_contact,
331 331 'reset_url': link})
332 332 log.debug('sending email')
333 333 run_task(tasks.send_email, user_email,
334 334 _("password reset link"), body, body)
335 335 log.info('send new password mail to %s' % user_email)
336 336 else:
337 337 log.debug("password reset email %s not found" % user_email)
338 338 except:
339 339 log.error(traceback.format_exc())
340 340 return False
341 341
342 342 return True
343 343
344 344 def reset_password(self, data):
345 345 from rhodecode.lib.celerylib import tasks, run_task
346 346 from rhodecode.lib import auth
347 347 user_email = data['email']
348 348 try:
349 349 try:
350 350 user = User.get_by_email(user_email)
351 351 new_passwd = auth.PasswordGenerator().gen_password(8,
352 352 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
353 353 if user:
354 354 user.password = auth.get_crypt_password(new_passwd)
355 355 user.api_key = auth.generate_api_key(user.username)
356 356 Session().add(user)
357 357 Session().commit()
358 358 log.info('change password for %s' % user_email)
359 359 if new_passwd is None:
360 360 raise Exception('unable to generate new password')
361 361 except:
362 362 log.error(traceback.format_exc())
363 363 Session().rollback()
364 364
365 365 run_task(tasks.send_email, user_email,
366 366 _('Your new password'),
367 367 _('Your new RhodeCode password:%s') % (new_passwd))
368 368 log.info('send new password mail to %s' % user_email)
369 369
370 370 except:
371 371 log.error('Failed to update user password')
372 372 log.error(traceback.format_exc())
373 373
374 374 return True
375 375
376 376 def fill_data(self, auth_user, user_id=None, api_key=None):
377 377 """
378 378 Fetches auth_user by user_id,or api_key if present.
379 379 Fills auth_user attributes with those taken from database.
380 380 Additionally set's is_authenitated if lookup fails
381 381 present in database
382 382
383 383 :param auth_user: instance of user to set attributes
384 384 :param user_id: user id to fetch by
385 385 :param api_key: api key to fetch by
386 386 """
387 387 if user_id is None and api_key is None:
388 388 raise Exception('You need to pass user_id or api_key')
389 389
390 390 try:
391 391 if api_key:
392 392 dbuser = self.get_by_api_key(api_key)
393 393 else:
394 394 dbuser = self.get(user_id)
395 395
396 396 if dbuser is not None and dbuser.active:
397 397 log.debug('filling %s data' % dbuser)
398 398 for k, v in dbuser.get_dict().items():
399 399 setattr(auth_user, k, v)
400 400 else:
401 401 return False
402 402
403 403 except:
404 404 log.error(traceback.format_exc())
405 405 auth_user.is_authenticated = False
406 406 return False
407 407
408 408 return True
409 409
410 410 def fill_perms(self, user, explicit=True, algo='higherwin'):
411 411 """
412 412 Fills user permission attribute with permissions taken from database
413 413 works for permissions given for repositories, and for permissions that
414 414 are granted to groups
415 415
416 416 :param user: user instance to fill his perms
417 417 :param explicit: In case there are permissions both for user and a group
418 418 that user is part of, explicit flag will defiine if user will
419 419 explicitly override permissions from group, if it's False it will
420 420 make decision based on the algo
421 421 :param algo: algorithm to decide what permission should be choose if
422 422 it's multiple defined, eg user in two different groups. It also
423 423 decides if explicit flag is turned off how to specify the permission
424 424 for case when user is in a group + have defined separate permission
425 425 """
426 426 RK = 'repositories'
427 427 GK = 'repositories_groups'
428 428 GLOBAL = 'global'
429 429 user.permissions[RK] = {}
430 430 user.permissions[GK] = {}
431 431 user.permissions[GLOBAL] = set()
432 432
433 433 def _choose_perm(new_perm, cur_perm):
434 434 new_perm_val = PERM_WEIGHTS[new_perm]
435 435 cur_perm_val = PERM_WEIGHTS[cur_perm]
436 436 if algo == 'higherwin':
437 437 if new_perm_val > cur_perm_val:
438 438 return new_perm
439 439 return cur_perm
440 440 elif algo == 'lowerwin':
441 441 if new_perm_val < cur_perm_val:
442 442 return new_perm
443 443 return cur_perm
444 444
445 445 #======================================================================
446 446 # fetch default permissions
447 447 #======================================================================
448 448 default_user = User.get_by_username('default', cache=True)
449 449 default_user_id = default_user.user_id
450 450
451 451 default_repo_perms = Permission.get_default_perms(default_user_id)
452 452 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
453 453
454 454 if user.is_admin:
455 455 #==================================================================
456 456 # admin user have all default rights for repositories
457 457 # and groups set to admin
458 458 #==================================================================
459 459 user.permissions[GLOBAL].add('hg.admin')
460 460
461 461 # repositories
462 462 for perm in default_repo_perms:
463 463 r_k = perm.UserRepoToPerm.repository.repo_name
464 464 p = 'repository.admin'
465 465 user.permissions[RK][r_k] = p
466 466
467 467 # repository groups
468 468 for perm in default_repo_groups_perms:
469 469 rg_k = perm.UserRepoGroupToPerm.group.group_name
470 470 p = 'group.admin'
471 471 user.permissions[GK][rg_k] = p
472 472 return user
473 473
474 474 #==================================================================
475 475 # SET DEFAULTS GLOBAL, REPOS, REPOS GROUPS
476 476 #==================================================================
477 477 uid = user.user_id
478 478
479 479 # default global permissions taken fron the default user
480 480 default_global_perms = self.sa.query(UserToPerm)\
481 481 .filter(UserToPerm.user_id == default_user_id)
482 482
483 483 for perm in default_global_perms:
484 484 user.permissions[GLOBAL].add(perm.permission.permission_name)
485 485
486 486 # defaults for repositories, taken from default user
487 487 for perm in default_repo_perms:
488 488 r_k = perm.UserRepoToPerm.repository.repo_name
489 489 if perm.Repository.private and not (perm.Repository.user_id == uid):
490 490 # disable defaults for private repos,
491 491 p = 'repository.none'
492 492 elif perm.Repository.user_id == uid:
493 493 # set admin if owner
494 494 p = 'repository.admin'
495 495 else:
496 496 p = perm.Permission.permission_name
497 497
498 498 user.permissions[RK][r_k] = p
499 499
500 500 # defaults for repository groups taken from default user permission
501 501 # on given group
502 502 for perm in default_repo_groups_perms:
503 503 rg_k = perm.UserRepoGroupToPerm.group.group_name
504 504 p = perm.Permission.permission_name
505 505 user.permissions[GK][rg_k] = p
506 506
507 507 #======================================================================
508 508 # !! OVERRIDE GLOBALS !! with user permissions if any found
509 509 #======================================================================
510 510 # those can be configured from groups or users explicitly
511 511 _configurable = set(['hg.fork.none', 'hg.fork.repository',
512 512 'hg.create.none', 'hg.create.repository'])
513 513
514 514 # USER GROUPS comes first
515 515 # user group global permissions
516 516 user_perms_from_users_groups = self.sa.query(UserGroupToPerm)\
517 517 .options(joinedload(UserGroupToPerm.permission))\
518 518 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
519 519 UserGroupMember.users_group_id))\
520 520 .filter(UserGroupMember.user_id == uid)\
521 521 .order_by(UserGroupToPerm.users_group_id)\
522 522 .all()
523 523 #need to group here by groups since user can be in more than one group
524 524 _grouped = [[x, list(y)] for x, y in
525 525 itertools.groupby(user_perms_from_users_groups,
526 526 lambda x:x.users_group)]
527 527 for gr, perms in _grouped:
528 528 # since user can be in multiple groups iterate over them and
529 529 # select the lowest permissions first (more explicit)
530 530 ##TODO: do this^^
531 531 if not gr.inherit_default_permissions:
532 532 # NEED TO IGNORE all configurable permissions and
533 533 # replace them with explicitly set
534 534 user.permissions[GLOBAL] = user.permissions[GLOBAL]\
535 535 .difference(_configurable)
536 536 for perm in perms:
537 537 user.permissions[GLOBAL].add(perm.permission.permission_name)
538 538
539 539 # user specific global permissions
540 540 user_perms = self.sa.query(UserToPerm)\
541 541 .options(joinedload(UserToPerm.permission))\
542 542 .filter(UserToPerm.user_id == uid).all()
543 543
544 544 if not user.inherit_default_permissions:
545 545 # NEED TO IGNORE all configurable permissions and
546 546 # replace them with explicitly set
547 547 user.permissions[GLOBAL] = user.permissions[GLOBAL]\
548 548 .difference(_configurable)
549 549
550 550 for perm in user_perms:
551 551 user.permissions[GLOBAL].add(perm.permission.permission_name)
552 552
553 553 #======================================================================
554 554 # !! PERMISSIONS FOR REPOSITORIES !!
555 555 #======================================================================
556 556 #======================================================================
557 557 # check if user is part of user groups for this repository and
558 558 # fill in his permission from it. _choose_perm decides of which
559 559 # permission should be selected based on selected method
560 560 #======================================================================
561 561
562 562 # user group for repositories permissions
563 563 user_repo_perms_from_users_groups = \
564 564 self.sa.query(UserGroupRepoToPerm, Permission, Repository,)\
565 565 .join((Repository, UserGroupRepoToPerm.repository_id ==
566 566 Repository.repo_id))\
567 567 .join((Permission, UserGroupRepoToPerm.permission_id ==
568 568 Permission.permission_id))\
569 569 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
570 570 UserGroupMember.users_group_id))\
571 571 .filter(UserGroupMember.user_id == uid)\
572 572 .all()
573 573
574 574 multiple_counter = collections.defaultdict(int)
575 575 for perm in user_repo_perms_from_users_groups:
576 576 r_k = perm.UserGroupRepoToPerm.repository.repo_name
577 577 multiple_counter[r_k] += 1
578 578 p = perm.Permission.permission_name
579 579 cur_perm = user.permissions[RK][r_k]
580 580
581 581 if perm.Repository.user_id == uid:
582 582 # set admin if owner
583 583 p = 'repository.admin'
584 584 else:
585 585 if multiple_counter[r_k] > 1:
586 586 p = _choose_perm(p, cur_perm)
587 587 user.permissions[RK][r_k] = p
588 588
589 589 # user explicit permissions for repositories, overrides any specified
590 590 # by the group permission
591 591 user_repo_perms = \
592 592 self.sa.query(UserRepoToPerm, Permission, Repository)\
593 593 .join((Repository, UserRepoToPerm.repository_id ==
594 594 Repository.repo_id))\
595 595 .join((Permission, UserRepoToPerm.permission_id ==
596 596 Permission.permission_id))\
597 597 .filter(UserRepoToPerm.user_id == uid)\
598 598 .all()
599 599
600 600 for perm in user_repo_perms:
601 601 r_k = perm.UserRepoToPerm.repository.repo_name
602 602 cur_perm = user.permissions[RK][r_k]
603 603 # set admin if owner
604 604 if perm.Repository.user_id == uid:
605 605 p = 'repository.admin'
606 606 else:
607 607 p = perm.Permission.permission_name
608 608 if not explicit:
609 609 p = _choose_perm(p, cur_perm)
610 610 user.permissions[RK][r_k] = p
611 611
612 612 #======================================================================
613 613 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
614 614 #======================================================================
615 615 #======================================================================
616 616 # check if user is part of user groups for this repository groups and
617 617 # fill in his permission from it. _choose_perm decides of which
618 618 # permission should be selected based on selected method
619 619 #======================================================================
620 620 # user group for repo groups permissions
621 621 user_repo_group_perms_from_users_groups = \
622 622 self.sa.query(UserGroupRepoGroupToPerm, Permission, RepoGroup)\
623 623 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
624 624 .join((Permission, UserGroupRepoGroupToPerm.permission_id
625 625 == Permission.permission_id))\
626 626 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
627 627 == UserGroupMember.users_group_id))\
628 628 .filter(UserGroupMember.user_id == uid)\
629 629 .all()
630 630
631 631 multiple_counter = collections.defaultdict(int)
632 632 for perm in user_repo_group_perms_from_users_groups:
633 633 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
634 634 multiple_counter[g_k] += 1
635 635 p = perm.Permission.permission_name
636 636 cur_perm = user.permissions[GK][g_k]
637 637 if multiple_counter[g_k] > 1:
638 638 p = _choose_perm(p, cur_perm)
639 639 user.permissions[GK][g_k] = p
640 640
641 641 # user explicit permissions for repository groups
642 642 user_repo_groups_perms = \
643 643 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
644 644 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
645 645 .join((Permission, UserRepoGroupToPerm.permission_id
646 646 == Permission.permission_id))\
647 647 .filter(UserRepoGroupToPerm.user_id == uid)\
648 648 .all()
649 649
650 650 for perm in user_repo_groups_perms:
651 651 rg_k = perm.UserRepoGroupToPerm.group.group_name
652 652 p = perm.Permission.permission_name
653 653 cur_perm = user.permissions[GK][rg_k]
654 654 if not explicit:
655 655 p = _choose_perm(p, cur_perm)
656 656 user.permissions[GK][rg_k] = p
657 657
658 658 return user
659 659
660 660 def has_perm(self, user, perm):
661 661 perm = self._get_perm(perm)
662 662 user = self._get_user(user)
663 663
664 664 return UserToPerm.query().filter(UserToPerm.user == user)\
665 665 .filter(UserToPerm.permission == perm).scalar() is not None
666 666
667 667 def grant_perm(self, user, perm):
668 668 """
669 669 Grant user global permissions
670 670
671 671 :param user:
672 672 :param perm:
673 673 """
674 674 user = self._get_user(user)
675 675 perm = self._get_perm(perm)
676 676 # if this permission is already granted skip it
677 677 _perm = UserToPerm.query()\
678 678 .filter(UserToPerm.user == user)\
679 679 .filter(UserToPerm.permission == perm)\
680 680 .scalar()
681 681 if _perm:
682 682 return
683 683 new = UserToPerm()
684 684 new.user = user
685 685 new.permission = perm
686 686 self.sa.add(new)
687 687
688 688 def revoke_perm(self, user, perm):
689 689 """
690 690 Revoke users global permissions
691 691
692 692 :param user:
693 693 :param perm:
694 694 """
695 695 user = self._get_user(user)
696 696 perm = self._get_perm(perm)
697 697
698 698 obj = UserToPerm.query()\
699 699 .filter(UserToPerm.user == user)\
700 700 .filter(UserToPerm.permission == perm)\
701 701 .scalar()
702 702 if obj:
703 703 self.sa.delete(obj)
704 704
705 705 def add_extra_email(self, user, email):
706 706 """
707 707 Adds email address to UserEmailMap
708 708
709 709 :param user:
710 710 :param email:
711 711 """
712 712 from rhodecode.model import forms
713 713 form = forms.UserExtraEmailForm()()
714 714 data = form.to_python(dict(email=email))
715 715 user = self._get_user(user)
716 716
717 717 obj = UserEmailMap()
718 718 obj.user = user
719 719 obj.email = data['email']
720 720 self.sa.add(obj)
721 721 return obj
722 722
723 723 def delete_extra_email(self, user, email_id):
724 724 """
725 725 Removes email address from UserEmailMap
726 726
727 727 :param user:
728 728 :param email_id:
729 729 """
730 730 user = self._get_user(user)
731 731 obj = UserEmailMap.query().get(email_id)
732 732 if obj:
733 733 self.sa.delete(obj)
734 734
735 735 def add_extra_ip(self, user, ip):
736 736 """
737 737 Adds ip address to UserIpMap
738 738
739 739 :param user:
740 740 :param ip:
741 741 """
742 742 from rhodecode.model import forms
743 743 form = forms.UserExtraIpForm()()
744 744 data = form.to_python(dict(ip=ip))
745 745 user = self._get_user(user)
746 746
747 747 obj = UserIpMap()
748 748 obj.user = user
749 749 obj.ip_addr = data['ip']
750 750 self.sa.add(obj)
751 751 return obj
752 752
753 753 def delete_extra_ip(self, user, ip_id):
754 754 """
755 755 Removes ip address from UserIpMap
756 756
757 757 :param user:
758 758 :param ip_id:
759 759 """
760 760 user = self._get_user(user)
761 761 obj = UserIpMap.query().get(ip_id)
762 762 if obj:
763 763 self.sa.delete(obj)
@@ -1,192 +1,192
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.users_group
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 user group model for RhodeCode
7 7
8 8 :created_on: Oct 1, 2011
9 9 :author: nvinot
10 10 :copyright: (C) 2011-2011 Nicolas Vinot <aeris@imirhil.fr>
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.model import BaseModel
31 31 from rhodecode.model.db import UserGroupMember, UserGroup,\
32 32 UserGroupRepoToPerm, Permission, UserGroupToPerm, User
33 33 from rhodecode.lib.exceptions import UserGroupsAssignedException
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class UserGroupModel(BaseModel):
39 39
40 40 cls = UserGroup
41 41
42 42 def __get_users_group(self, users_group):
43 43 return self._get_instance(UserGroup, users_group,
44 44 callback=UserGroup.get_by_group_name)
45 45
46 46 def get(self, users_group_id, cache=False):
47 47 return UserGroup.get(users_group_id)
48 48
49 49 def get_group(self, users_group):
50 50 return self.__get_users_group(users_group)
51 51
52 52 def get_by_name(self, name, cache=False, case_insensitive=False):
53 53 return UserGroup.get_by_group_name(name, cache, case_insensitive)
54 54
55 55 def create(self, name, active=True):
56 56 try:
57 57 new = UserGroup()
58 58 new.users_group_name = name
59 59 new.users_group_active = active
60 60 self.sa.add(new)
61 61 return new
62 62 except:
63 63 log.error(traceback.format_exc())
64 64 raise
65 65
66 66 def update(self, users_group, form_data):
67 67
68 68 try:
69 69 users_group = self.__get_users_group(users_group)
70 70
71 71 for k, v in form_data.items():
72 72 if k == 'users_group_members':
73 73 users_group.members = []
74 74 self.sa.flush()
75 75 members_list = []
76 76 if v:
77 77 v = [v] if isinstance(v, basestring) else v
78 78 for u_id in set(v):
79 79 member = UserGroupMember(users_group.users_group_id, u_id)
80 80 members_list.append(member)
81 81 setattr(users_group, 'members', members_list)
82 82 setattr(users_group, k, v)
83 83
84 84 self.sa.add(users_group)
85 85 except:
86 86 log.error(traceback.format_exc())
87 87 raise
88 88
89 89 def delete(self, users_group, force=False):
90 90 """
91 91 Deletes repos group, unless force flag is used
92 92 raises exception if there are members in that group, else deletes
93 93 group and users
94 94
95 95 :param users_group:
96 96 :param force:
97 97 """
98 98 try:
99 99 users_group = self.__get_users_group(users_group)
100 100
101 101 # check if this group is not assigned to repo
102 102 assigned_groups = UserGroupRepoToPerm.query()\
103 103 .filter(UserGroupRepoToPerm.users_group == users_group).all()
104 104
105 if assigned_groups and force is False:
105 if assigned_groups and not force:
106 106 raise UserGroupsAssignedException('RepoGroup assigned to %s' %
107 107 assigned_groups)
108 108
109 109 self.sa.delete(users_group)
110 110 except:
111 111 log.error(traceback.format_exc())
112 112 raise
113 113
114 114 def add_user_to_group(self, users_group, user):
115 115 users_group = self.__get_users_group(users_group)
116 116 user = self._get_user(user)
117 117
118 118 for m in users_group.members:
119 119 u = m.user
120 120 if u.user_id == user.user_id:
121 121 return True
122 122
123 123 try:
124 124 users_group_member = UserGroupMember()
125 125 users_group_member.user = user
126 126 users_group_member.users_group = users_group
127 127
128 128 users_group.members.append(users_group_member)
129 129 user.group_member.append(users_group_member)
130 130
131 131 self.sa.add(users_group_member)
132 132 return users_group_member
133 133 except:
134 134 log.error(traceback.format_exc())
135 135 raise
136 136
137 137 def remove_user_from_group(self, users_group, user):
138 138 users_group = self.__get_users_group(users_group)
139 139 user = self._get_user(user)
140 140
141 141 users_group_member = None
142 142 for m in users_group.members:
143 143 if m.user.user_id == user.user_id:
144 144 # Found this user's membership row
145 145 users_group_member = m
146 146 break
147 147
148 148 if users_group_member:
149 149 try:
150 150 self.sa.delete(users_group_member)
151 151 return True
152 152 except:
153 153 log.error(traceback.format_exc())
154 154 raise
155 155 else:
156 156 # User isn't in that group
157 157 return False
158 158
159 159 def has_perm(self, users_group, perm):
160 160 users_group = self.__get_users_group(users_group)
161 161 perm = self._get_perm(perm)
162 162
163 163 return UserGroupToPerm.query()\
164 164 .filter(UserGroupToPerm.users_group == users_group)\
165 165 .filter(UserGroupToPerm.permission == perm).scalar() is not None
166 166
167 167 def grant_perm(self, users_group, perm):
168 168 users_group = self.__get_users_group(users_group)
169 169 perm = self._get_perm(perm)
170 170
171 171 # if this permission is already granted skip it
172 172 _perm = UserGroupToPerm.query()\
173 173 .filter(UserGroupToPerm.users_group == users_group)\
174 174 .filter(UserGroupToPerm.permission == perm)\
175 175 .scalar()
176 176 if _perm:
177 177 return
178 178
179 179 new = UserGroupToPerm()
180 180 new.users_group = users_group
181 181 new.permission = perm
182 182 self.sa.add(new)
183 183
184 184 def revoke_perm(self, users_group, perm):
185 185 users_group = self.__get_users_group(users_group)
186 186 perm = self._get_perm(perm)
187 187
188 188 obj = UserGroupToPerm.query()\
189 189 .filter(UserGroupToPerm.users_group == users_group)\
190 190 .filter(UserGroupToPerm.permission == perm).scalar()
191 191 if obj:
192 192 self.sa.delete(obj)
@@ -1,816 +1,816
1 1 """
2 2 Set of generic validators
3 3 """
4 4 import os
5 5 import re
6 6 import formencode
7 7 import logging
8 8 from collections import defaultdict
9 9 from pylons.i18n.translation import _
10 10 from webhelpers.pylonslib.secure_form import authentication_token
11 11
12 12 from formencode.validators import (
13 13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 14 NotEmpty, IPAddress, CIDR
15 15 )
16 16 from rhodecode.lib.compat import OrderedSet
17 17 from rhodecode.lib import ipaddr
18 18 from rhodecode.lib.utils import repo_name_slug
19 19 from rhodecode.lib.utils2 import safe_int
20 20 from rhodecode.model.db import RepoGroup, Repository, UserGroup, User,\
21 21 ChangesetStatus
22 22 from rhodecode.lib.exceptions import LdapImportError
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.auth import HasReposGroupPermissionAny, HasPermissionAny
25 25
26 26 # silence warnings and pylint
27 27 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
28 28 NotEmpty, IPAddress, CIDR
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class UniqueList(formencode.FancyValidator):
34 34 """
35 35 Unique List !
36 36 """
37 37 messages = dict(
38 38 empty=_('Value cannot be an empty list'),
39 39 missing_value=_('Value cannot be an empty list'),
40 40 )
41 41
42 42 def _to_python(self, value, state):
43 43 if isinstance(value, list):
44 44 return value
45 45 elif isinstance(value, set):
46 46 return list(value)
47 47 elif isinstance(value, tuple):
48 48 return list(value)
49 49 elif value is None:
50 50 return []
51 51 else:
52 52 return [value]
53 53
54 54 def empty_value(self, value):
55 55 return []
56 56
57 57
58 58 class StateObj(object):
59 59 """
60 60 this is needed to translate the messages using _() in validators
61 61 """
62 62 _ = staticmethod(_)
63 63
64 64
65 65 def M(self, key, state=None, **kwargs):
66 66 """
67 67 returns string from self.message based on given key,
68 68 passed kw params are used to substitute %(named)s params inside
69 69 translated strings
70 70
71 71 :param msg:
72 72 :param state:
73 73 """
74 74 if state is None:
75 75 state = StateObj()
76 76 else:
77 77 state._ = staticmethod(_)
78 78 #inject validator into state object
79 79 return self.message(key, state, **kwargs)
80 80
81 81
82 82 def ValidUsername(edit=False, old_data={}):
83 83 class _validator(formencode.validators.FancyValidator):
84 84 messages = {
85 85 'username_exists': _(u'Username "%(username)s" already exists'),
86 86 'system_invalid_username':
87 87 _(u'Username "%(username)s" is forbidden'),
88 88 'invalid_username':
89 89 _(u'Username may only contain alphanumeric characters '
90 90 'underscores, periods or dashes and must begin with '
91 91 'alphanumeric character')
92 92 }
93 93
94 94 def validate_python(self, value, state):
95 95 if value in ['default', 'new_user']:
96 96 msg = M(self, 'system_invalid_username', state, username=value)
97 97 raise formencode.Invalid(msg, value, state)
98 98 #check if user is unique
99 99 old_un = None
100 100 if edit:
101 101 old_un = User.get(old_data.get('user_id')).username
102 102
103 103 if old_un != value or not edit:
104 104 if User.get_by_username(value, case_insensitive=True):
105 105 msg = M(self, 'username_exists', state, username=value)
106 106 raise formencode.Invalid(msg, value, state)
107 107
108 108 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
109 109 msg = M(self, 'invalid_username', state)
110 110 raise formencode.Invalid(msg, value, state)
111 111 return _validator
112 112
113 113
114 114 def ValidRepoUser():
115 115 class _validator(formencode.validators.FancyValidator):
116 116 messages = {
117 117 'invalid_username': _(u'Username %(username)s is not valid')
118 118 }
119 119
120 120 def validate_python(self, value, state):
121 121 try:
122 122 User.query().filter(User.active == True)\
123 123 .filter(User.username == value).one()
124 124 except Exception:
125 125 msg = M(self, 'invalid_username', state, username=value)
126 126 raise formencode.Invalid(msg, value, state,
127 127 error_dict=dict(username=msg)
128 128 )
129 129
130 130 return _validator
131 131
132 132
133 133 def ValidUserGroup(edit=False, old_data={}):
134 134 class _validator(formencode.validators.FancyValidator):
135 135 messages = {
136 136 'invalid_group': _(u'Invalid user group name'),
137 137 'group_exist': _(u'User group "%(usergroup)s" already exists'),
138 138 'invalid_usergroup_name':
139 139 _(u'user group name may only contain alphanumeric '
140 140 'characters underscores, periods or dashes and must begin '
141 141 'with alphanumeric character')
142 142 }
143 143
144 144 def validate_python(self, value, state):
145 145 if value in ['default']:
146 146 msg = M(self, 'invalid_group', state)
147 147 raise formencode.Invalid(msg, value, state,
148 148 error_dict=dict(users_group_name=msg)
149 149 )
150 150 #check if group is unique
151 151 old_ugname = None
152 152 if edit:
153 153 old_id = old_data.get('users_group_id')
154 154 old_ugname = UserGroup.get(old_id).users_group_name
155 155
156 156 if old_ugname != value or not edit:
157 157 is_existing_group = UserGroup.get_by_group_name(value,
158 158 case_insensitive=True)
159 159 if is_existing_group:
160 160 msg = M(self, 'group_exist', state, usergroup=value)
161 161 raise formencode.Invalid(msg, value, state,
162 162 error_dict=dict(users_group_name=msg)
163 163 )
164 164
165 165 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
166 166 msg = M(self, 'invalid_usergroup_name', state)
167 167 raise formencode.Invalid(msg, value, state,
168 168 error_dict=dict(users_group_name=msg)
169 169 )
170 170
171 171 return _validator
172 172
173 173
174 174 def ValidReposGroup(edit=False, old_data={}):
175 175 class _validator(formencode.validators.FancyValidator):
176 176 messages = {
177 177 'group_parent_id': _(u'Cannot assign this group as parent'),
178 178 'group_exists': _(u'Group "%(group_name)s" already exists'),
179 179 'repo_exists':
180 180 _(u'Repository with name "%(group_name)s" already exists')
181 181 }
182 182
183 183 def validate_python(self, value, state):
184 184 # TODO WRITE VALIDATIONS
185 185 group_name = value.get('group_name')
186 186 group_parent_id = value.get('group_parent_id')
187 187
188 188 # slugify repo group just in case :)
189 189 slug = repo_name_slug(group_name)
190 190
191 191 # check for parent of self
192 192 parent_of_self = lambda: (
193 193 old_data['group_id'] == int(group_parent_id)
194 194 if group_parent_id else False
195 195 )
196 196 if edit and parent_of_self():
197 197 msg = M(self, 'group_parent_id', state)
198 198 raise formencode.Invalid(msg, value, state,
199 199 error_dict=dict(group_parent_id=msg)
200 200 )
201 201
202 202 old_gname = None
203 203 if edit:
204 204 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
205 205
206 206 if old_gname != group_name or not edit:
207 207
208 208 # check group
209 209 gr = RepoGroup.query()\
210 210 .filter(RepoGroup.group_name == slug)\
211 211 .filter(RepoGroup.group_parent_id == group_parent_id)\
212 212 .scalar()
213 213
214 214 if gr:
215 215 msg = M(self, 'group_exists', state, group_name=slug)
216 216 raise formencode.Invalid(msg, value, state,
217 217 error_dict=dict(group_name=msg)
218 218 )
219 219
220 220 # check for same repo
221 221 repo = Repository.query()\
222 222 .filter(Repository.repo_name == slug)\
223 223 .scalar()
224 224
225 225 if repo:
226 226 msg = M(self, 'repo_exists', state, group_name=slug)
227 227 raise formencode.Invalid(msg, value, state,
228 228 error_dict=dict(group_name=msg)
229 229 )
230 230
231 231 return _validator
232 232
233 233
234 234 def ValidPassword():
235 235 class _validator(formencode.validators.FancyValidator):
236 236 messages = {
237 237 'invalid_password':
238 238 _(u'Invalid characters (non-ascii) in password')
239 239 }
240 240
241 241 def validate_python(self, value, state):
242 242 try:
243 243 (value or '').decode('ascii')
244 244 except UnicodeError:
245 245 msg = M(self, 'invalid_password', state)
246 246 raise formencode.Invalid(msg, value, state,)
247 247 return _validator
248 248
249 249
250 250 def ValidPasswordsMatch():
251 251 class _validator(formencode.validators.FancyValidator):
252 252 messages = {
253 253 'password_mismatch': _(u'Passwords do not match'),
254 254 }
255 255
256 256 def validate_python(self, value, state):
257 257
258 258 pass_val = value.get('password') or value.get('new_password')
259 259 if pass_val != value['password_confirmation']:
260 260 msg = M(self, 'password_mismatch', state)
261 261 raise formencode.Invalid(msg, value, state,
262 262 error_dict=dict(password_confirmation=msg)
263 263 )
264 264 return _validator
265 265
266 266
267 267 def ValidAuth():
268 268 class _validator(formencode.validators.FancyValidator):
269 269 messages = {
270 270 'invalid_password': _(u'invalid password'),
271 271 'invalid_username': _(u'invalid user name'),
272 272 'disabled_account': _(u'Your account is disabled')
273 273 }
274 274
275 275 def validate_python(self, value, state):
276 276 from rhodecode.lib.auth import authenticate
277 277
278 278 password = value['password']
279 279 username = value['username']
280 280
281 281 if not authenticate(username, password):
282 282 user = User.get_by_username(username)
283 if user and user.active is False:
283 if user and not user.active:
284 284 log.warning('user %s is disabled' % username)
285 285 msg = M(self, 'disabled_account', state)
286 286 raise formencode.Invalid(msg, value, state,
287 287 error_dict=dict(username=msg)
288 288 )
289 289 else:
290 290 log.warning('user %s failed to authenticate' % username)
291 291 msg = M(self, 'invalid_username', state)
292 292 msg2 = M(self, 'invalid_password', state)
293 293 raise formencode.Invalid(msg, value, state,
294 294 error_dict=dict(username=msg, password=msg2)
295 295 )
296 296 return _validator
297 297
298 298
299 299 def ValidAuthToken():
300 300 class _validator(formencode.validators.FancyValidator):
301 301 messages = {
302 302 'invalid_token': _(u'Token mismatch')
303 303 }
304 304
305 305 def validate_python(self, value, state):
306 306 if value != authentication_token():
307 307 msg = M(self, 'invalid_token', state)
308 308 raise formencode.Invalid(msg, value, state)
309 309 return _validator
310 310
311 311
312 312 def ValidRepoName(edit=False, old_data={}):
313 313 class _validator(formencode.validators.FancyValidator):
314 314 messages = {
315 315 'invalid_repo_name':
316 316 _(u'Repository name %(repo)s is disallowed'),
317 317 'repository_exists':
318 318 _(u'Repository named %(repo)s already exists'),
319 319 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
320 320 'exists in group "%(group)s"'),
321 321 'same_group_exists': _(u'Repository group with name "%(repo)s" '
322 322 'already exists')
323 323 }
324 324
325 325 def _to_python(self, value, state):
326 326 repo_name = repo_name_slug(value.get('repo_name', ''))
327 327 repo_group = value.get('repo_group')
328 328 if repo_group:
329 329 gr = RepoGroup.get(repo_group)
330 330 group_path = gr.full_path
331 331 group_name = gr.group_name
332 332 # value needs to be aware of group name in order to check
333 333 # db key This is an actual just the name to store in the
334 334 # database
335 335 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
336 336 else:
337 337 group_name = group_path = ''
338 338 repo_name_full = repo_name
339 339
340 340 value['repo_name'] = repo_name
341 341 value['repo_name_full'] = repo_name_full
342 342 value['group_path'] = group_path
343 343 value['group_name'] = group_name
344 344 return value
345 345
346 346 def validate_python(self, value, state):
347 347
348 348 repo_name = value.get('repo_name')
349 349 repo_name_full = value.get('repo_name_full')
350 350 group_path = value.get('group_path')
351 351 group_name = value.get('group_name')
352 352
353 353 if repo_name in [ADMIN_PREFIX, '']:
354 354 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
355 355 raise formencode.Invalid(msg, value, state,
356 356 error_dict=dict(repo_name=msg)
357 357 )
358 358
359 359 rename = old_data.get('repo_name') != repo_name_full
360 360 create = not edit
361 361 if rename or create:
362 362
363 363 if group_path != '':
364 364 if Repository.get_by_repo_name(repo_name_full):
365 365 msg = M(self, 'repository_in_group_exists', state,
366 366 repo=repo_name, group=group_name)
367 367 raise formencode.Invalid(msg, value, state,
368 368 error_dict=dict(repo_name=msg)
369 369 )
370 370 elif RepoGroup.get_by_group_name(repo_name_full):
371 371 msg = M(self, 'same_group_exists', state,
372 372 repo=repo_name)
373 373 raise formencode.Invalid(msg, value, state,
374 374 error_dict=dict(repo_name=msg)
375 375 )
376 376
377 377 elif Repository.get_by_repo_name(repo_name_full):
378 378 msg = M(self, 'repository_exists', state,
379 379 repo=repo_name)
380 380 raise formencode.Invalid(msg, value, state,
381 381 error_dict=dict(repo_name=msg)
382 382 )
383 383 return value
384 384 return _validator
385 385
386 386
387 387 def ValidForkName(*args, **kwargs):
388 388 return ValidRepoName(*args, **kwargs)
389 389
390 390
391 391 def SlugifyName():
392 392 class _validator(formencode.validators.FancyValidator):
393 393
394 394 def _to_python(self, value, state):
395 395 return repo_name_slug(value)
396 396
397 397 def validate_python(self, value, state):
398 398 pass
399 399
400 400 return _validator
401 401
402 402
403 403 def ValidCloneUri():
404 404 from rhodecode.lib.utils import make_ui
405 405
406 406 def url_handler(repo_type, url, ui=None):
407 407 if repo_type == 'hg':
408 408 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
409 409 from mercurial.httppeer import httppeer
410 410 if url.startswith('http'):
411 411 ## initially check if it's at least the proper URL
412 412 ## or does it pass basic auth
413 413 MercurialRepository._check_url(url)
414 414 httppeer(ui, url)._capabilities()
415 415 elif url.startswith('svn+http'):
416 416 from hgsubversion.svnrepo import svnremoterepo
417 417 svnremoterepo(ui, url).capabilities
418 418 elif url.startswith('git+http'):
419 419 raise NotImplementedError()
420 420 else:
421 421 raise Exception('clone from URI %s not allowed' % (url))
422 422
423 423 elif repo_type == 'git':
424 424 from rhodecode.lib.vcs.backends.git.repository import GitRepository
425 425 if url.startswith('http'):
426 426 ## initially check if it's at least the proper URL
427 427 ## or does it pass basic auth
428 428 GitRepository._check_url(url)
429 429 elif url.startswith('svn+http'):
430 430 raise NotImplementedError()
431 431 elif url.startswith('hg+http'):
432 432 raise NotImplementedError()
433 433 else:
434 434 raise Exception('clone from URI %s not allowed' % (url))
435 435
436 436 class _validator(formencode.validators.FancyValidator):
437 437 messages = {
438 438 'clone_uri': _(u'invalid clone url'),
439 439 'invalid_clone_uri': _(u'Invalid clone url, provide a '
440 440 'valid clone http(s)/svn+http(s) url')
441 441 }
442 442
443 443 def validate_python(self, value, state):
444 444 repo_type = value.get('repo_type')
445 445 url = value.get('clone_uri')
446 446
447 447 if not url:
448 448 pass
449 449 else:
450 450 try:
451 451 url_handler(repo_type, url, make_ui('db', clear_session=False))
452 452 except Exception:
453 453 log.exception('Url validation failed')
454 454 msg = M(self, 'clone_uri')
455 455 raise formencode.Invalid(msg, value, state,
456 456 error_dict=dict(clone_uri=msg)
457 457 )
458 458 return _validator
459 459
460 460
461 461 def ValidForkType(old_data={}):
462 462 class _validator(formencode.validators.FancyValidator):
463 463 messages = {
464 464 'invalid_fork_type': _(u'Fork have to be the same type as parent')
465 465 }
466 466
467 467 def validate_python(self, value, state):
468 468 if old_data['repo_type'] != value:
469 469 msg = M(self, 'invalid_fork_type', state)
470 470 raise formencode.Invalid(msg, value, state,
471 471 error_dict=dict(repo_type=msg)
472 472 )
473 473 return _validator
474 474
475 475
476 476 def CanWriteGroup(old_data=None):
477 477 class _validator(formencode.validators.FancyValidator):
478 478 messages = {
479 479 'permission_denied': _(u"You don't have permissions "
480 480 "to create repository in this group"),
481 481 'permission_denied_root': _(u"no permission to create repository "
482 482 "in root location")
483 483 }
484 484
485 485 def _to_python(self, value, state):
486 486 #root location
487 487 if value in [-1, "-1"]:
488 488 return None
489 489 return value
490 490
491 491 def validate_python(self, value, state):
492 492 gr = RepoGroup.get(value)
493 493 gr_name = gr.group_name if gr else None # None means ROOT location
494 494 val = HasReposGroupPermissionAny('group.write', 'group.admin')
495 495 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
496 496 forbidden = not val(gr_name, 'can write into group validator')
497 497 value_changed = True # old_data['repo_group'].get('group_id') != safe_int(value)
498 498 if value_changed: # do check if we changed the value
499 499 #parent group need to be existing
500 500 if gr and forbidden:
501 501 msg = M(self, 'permission_denied', state)
502 502 raise formencode.Invalid(msg, value, state,
503 503 error_dict=dict(repo_type=msg)
504 504 )
505 505 ## check if we can write to root location !
506 elif gr is None and can_create_repos() is False:
506 elif gr is None and not can_create_repos():
507 507 msg = M(self, 'permission_denied_root', state)
508 508 raise formencode.Invalid(msg, value, state,
509 509 error_dict=dict(repo_type=msg)
510 510 )
511 511
512 512 return _validator
513 513
514 514
515 515 def CanCreateGroup(can_create_in_root=False):
516 516 class _validator(formencode.validators.FancyValidator):
517 517 messages = {
518 518 'permission_denied': _(u"You don't have permissions "
519 519 "to create a group in this location")
520 520 }
521 521
522 522 def to_python(self, value, state):
523 523 #root location
524 524 if value in [-1, "-1"]:
525 525 return None
526 526 return value
527 527
528 528 def validate_python(self, value, state):
529 529 gr = RepoGroup.get(value)
530 530 gr_name = gr.group_name if gr else None # None means ROOT location
531 531
532 532 if can_create_in_root and gr is None:
533 533 #we can create in root, we're fine no validations required
534 534 return
535 535
536 forbidden_in_root = gr is None and can_create_in_root is False
536 forbidden_in_root = gr is None and not can_create_in_root
537 537 val = HasReposGroupPermissionAny('group.admin')
538 538 forbidden = not val(gr_name, 'can create group validator')
539 539 if forbidden_in_root or forbidden:
540 540 msg = M(self, 'permission_denied', state)
541 541 raise formencode.Invalid(msg, value, state,
542 542 error_dict=dict(group_parent_id=msg)
543 543 )
544 544
545 545 return _validator
546 546
547 547
548 548 def ValidPerms(type_='repo'):
549 549 if type_ == 'group':
550 550 EMPTY_PERM = 'group.none'
551 551 elif type_ == 'repo':
552 552 EMPTY_PERM = 'repository.none'
553 553
554 554 class _validator(formencode.validators.FancyValidator):
555 555 messages = {
556 556 'perm_new_member_name':
557 557 _(u'This username or user group name is not valid')
558 558 }
559 559
560 560 def to_python(self, value, state):
561 561 perms_update = OrderedSet()
562 562 perms_new = OrderedSet()
563 563 # build a list of permission to update and new permission to create
564 564
565 565 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
566 566 new_perms_group = defaultdict(dict)
567 567 for k, v in value.copy().iteritems():
568 568 if k.startswith('perm_new_member'):
569 569 del value[k]
570 570 _type, part = k.split('perm_new_member_')
571 571 args = part.split('_')
572 572 if len(args) == 1:
573 573 new_perms_group[args[0]]['perm'] = v
574 574 elif len(args) == 2:
575 575 _key, pos = args
576 576 new_perms_group[pos][_key] = v
577 577
578 578 # fill new permissions in order of how they were added
579 579 for k in sorted(map(int, new_perms_group.keys())):
580 580 perm_dict = new_perms_group[str(k)]
581 581 new_member = perm_dict.get('name')
582 582 new_perm = perm_dict.get('perm')
583 583 new_type = perm_dict.get('type')
584 584 if new_member and new_perm and new_type:
585 585 perms_new.add((new_member, new_perm, new_type))
586 586
587 587 for k, v in value.iteritems():
588 588 if k.startswith('u_perm_') or k.startswith('g_perm_'):
589 589 member = k[7:]
590 590 t = {'u': 'user',
591 591 'g': 'users_group'
592 592 }[k[0]]
593 593 if member == 'default':
594 594 if value.get('repo_private'):
595 595 # set none for default when updating to
596 596 # private repo
597 597 v = EMPTY_PERM
598 598 perms_update.add((member, v, t))
599 599 #always set NONE when private flag is set
600 600 if value.get('repo_private'):
601 601 perms_update.add(('default', EMPTY_PERM, 'user'))
602 602
603 603 value['perms_updates'] = list(perms_update)
604 604 value['perms_new'] = list(perms_new)
605 605
606 606 # update permissions
607 607 for k, v, t in perms_new:
608 608 try:
609 609 if t is 'user':
610 610 self.user_db = User.query()\
611 611 .filter(User.active == True)\
612 612 .filter(User.username == k).one()
613 613 if t is 'users_group':
614 614 self.user_db = UserGroup.query()\
615 615 .filter(UserGroup.users_group_active == True)\
616 616 .filter(UserGroup.users_group_name == k).one()
617 617
618 618 except Exception:
619 619 log.exception('Updated permission failed')
620 620 msg = M(self, 'perm_new_member_type', state)
621 621 raise formencode.Invalid(msg, value, state,
622 622 error_dict=dict(perm_new_member_name=msg)
623 623 )
624 624 return value
625 625 return _validator
626 626
627 627
628 628 def ValidSettings():
629 629 class _validator(formencode.validators.FancyValidator):
630 630 def _to_python(self, value, state):
631 631 # settings form for users that are not admin
632 632 # can't edit certain parameters, it's extra backup if they mangle
633 633 # with forms
634 634
635 635 forbidden_params = [
636 636 'user', 'repo_type', 'repo_enable_locking',
637 637 'repo_enable_downloads', 'repo_enable_statistics'
638 638 ]
639 639
640 640 for param in forbidden_params:
641 641 if param in value:
642 642 del value[param]
643 643 return value
644 644
645 645 def validate_python(self, value, state):
646 646 pass
647 647 return _validator
648 648
649 649
650 650 def ValidPath():
651 651 class _validator(formencode.validators.FancyValidator):
652 652 messages = {
653 653 'invalid_path': _(u'This is not a valid path')
654 654 }
655 655
656 656 def validate_python(self, value, state):
657 657 if not os.path.isdir(value):
658 658 msg = M(self, 'invalid_path', state)
659 659 raise formencode.Invalid(msg, value, state,
660 660 error_dict=dict(paths_root_path=msg)
661 661 )
662 662 return _validator
663 663
664 664
665 665 def UniqSystemEmail(old_data={}):
666 666 class _validator(formencode.validators.FancyValidator):
667 667 messages = {
668 668 'email_taken': _(u'This e-mail address is already taken')
669 669 }
670 670
671 671 def _to_python(self, value, state):
672 672 return value.lower()
673 673
674 674 def validate_python(self, value, state):
675 675 if (old_data.get('email') or '').lower() != value:
676 676 user = User.get_by_email(value, case_insensitive=True)
677 677 if user:
678 678 msg = M(self, 'email_taken', state)
679 679 raise formencode.Invalid(msg, value, state,
680 680 error_dict=dict(email=msg)
681 681 )
682 682 return _validator
683 683
684 684
685 685 def ValidSystemEmail():
686 686 class _validator(formencode.validators.FancyValidator):
687 687 messages = {
688 688 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
689 689 }
690 690
691 691 def _to_python(self, value, state):
692 692 return value.lower()
693 693
694 694 def validate_python(self, value, state):
695 695 user = User.get_by_email(value, case_insensitive=True)
696 696 if user is None:
697 697 msg = M(self, 'non_existing_email', state, email=value)
698 698 raise formencode.Invalid(msg, value, state,
699 699 error_dict=dict(email=msg)
700 700 )
701 701
702 702 return _validator
703 703
704 704
705 705 def LdapLibValidator():
706 706 class _validator(formencode.validators.FancyValidator):
707 707 messages = {
708 708
709 709 }
710 710
711 711 def validate_python(self, value, state):
712 712 try:
713 713 import ldap
714 714 ldap # pyflakes silence !
715 715 except ImportError:
716 716 raise LdapImportError()
717 717
718 718 return _validator
719 719
720 720
721 721 def AttrLoginValidator():
722 722 class _validator(formencode.validators.FancyValidator):
723 723 messages = {
724 724 'invalid_cn':
725 725 _(u'The LDAP Login attribute of the CN must be specified - '
726 726 'this is the name of the attribute that is equivalent '
727 727 'to "username"')
728 728 }
729 729
730 730 def validate_python(self, value, state):
731 731 if not value or not isinstance(value, (str, unicode)):
732 732 msg = M(self, 'invalid_cn', state)
733 733 raise formencode.Invalid(msg, value, state,
734 734 error_dict=dict(ldap_attr_login=msg)
735 735 )
736 736
737 737 return _validator
738 738
739 739
740 740 def NotReviewedRevisions(repo_id):
741 741 class _validator(formencode.validators.FancyValidator):
742 742 messages = {
743 743 'rev_already_reviewed':
744 744 _(u'Revisions %(revs)s are already part of pull request '
745 745 'or have set status')
746 746 }
747 747
748 748 def validate_python(self, value, state):
749 749 # check revisions if they are not reviewed, or a part of another
750 750 # pull request
751 751 statuses = ChangesetStatus.query()\
752 752 .filter(ChangesetStatus.revision.in_(value))\
753 753 .filter(ChangesetStatus.repo_id == repo_id)\
754 754 .all()
755 755
756 756 errors = []
757 757 for cs in statuses:
758 758 if cs.pull_request_id:
759 759 errors.append(['pull_req', cs.revision[:12]])
760 760 elif cs.status:
761 761 errors.append(['status', cs.revision[:12]])
762 762
763 763 if errors:
764 764 revs = ','.join([x[1] for x in errors])
765 765 msg = M(self, 'rev_already_reviewed', state, revs=revs)
766 766 raise formencode.Invalid(msg, value, state,
767 767 error_dict=dict(revisions=revs)
768 768 )
769 769
770 770 return _validator
771 771
772 772
773 773 def ValidIp():
774 774 class _validator(CIDR):
775 775 messages = dict(
776 776 badFormat=_('Please enter a valid IPv4 or IpV6 address'),
777 777 illegalBits=_('The network size (bits) must be within the range'
778 778 ' of 0-32 (not %(bits)r)'))
779 779
780 780 def to_python(self, value, state):
781 781 v = super(_validator, self).to_python(value, state)
782 782 v = v.strip()
783 783 net = ipaddr.IPNetwork(address=v)
784 784 if isinstance(net, ipaddr.IPv4Network):
785 785 #if IPv4 doesn't end with a mask, add /32
786 786 if '/' not in value:
787 787 v += '/32'
788 788 if isinstance(net, ipaddr.IPv6Network):
789 789 #if IPv6 doesn't end with a mask, add /128
790 790 if '/' not in value:
791 791 v += '/128'
792 792 return v
793 793
794 794 def validate_python(self, value, state):
795 795 try:
796 796 addr = value.strip()
797 797 #this raises an ValueError if address is not IpV4 or IpV6
798 798 ipaddr.IPNetwork(address=addr)
799 799 except ValueError:
800 800 raise formencode.Invalid(self.message('badFormat', state),
801 801 value, state)
802 802
803 803 return _validator
804 804
805 805
806 806 def FieldKey():
807 807 class _validator(formencode.validators.FancyValidator):
808 808 messages = dict(
809 809 badFormat=_('Key name can only consist of letters, '
810 810 'underscore, dash or numbers'),)
811 811
812 812 def validate_python(self, value, state):
813 813 if not re.match('[a-zA-Z0-9_-]+$', value):
814 814 raise formencode.Invalid(self.message('badFormat', state),
815 815 value, state)
816 816 return _validator
@@ -1,109 +1,109
1 1 <div id="node_history">
2 2 %if c.load_full_history:
3 3 <%include file='files_history_box.html'/>
4 4 %else:
5 5 <div style="padding-bottom:10px">
6 6 <span id="load_node_history" class="ui-btn">${_('Load file history')}</span>
7 7 </div>
8 8 %endif
9 9 </div>
10 10
11 11
12 12 <div id="body" class="codeblock">
13 13 <div class="code-header">
14 14 <div class="stats">
15 15 <div class="left img"><img src="${h.url('/images/icons/file.png')}"/></div>
16 16 <div class="left item"><pre class="tooltip" title="${h.tooltip(h.fmt_date(c.file_changeset.date))}">${h.link_to("r%s:%s" % (c.file_changeset.revision,h.short_id(c.file_changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file_changeset.raw_id))}</pre></div>
17 17 <div class="left item"><pre>${h.format_byte_size(c.file.size,binary=True)}</pre></div>
18 18 <div class="left item last"><pre>${c.file.mimetype}</pre></div>
19 19 <div class="buttons">
20 20 %if c.annotate:
21 21 ${h.link_to(_('show source'), h.url('files_home', repo_name=c.repo_name,revision=c.file_changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
22 22 %else:
23 23 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file_changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
24 24 %endif
25 25 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.file_changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
26 26 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.file_changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
27 27 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
28 % if c.on_branch_head and c.changeset.branch and c.file.is_binary is False:
28 % if c.on_branch_head and c.changeset.branch and not c.file.is_binary:
29 29 ${h.link_to(_('edit on branch:%s') % c.changeset.branch,h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.branch,f_path=c.f_path),class_="ui-btn")}
30 30 %else:
31 31 ${h.link_to(_('edit on branch:?'), '#', class_="ui-btn disabled tooltip", title=_('Editing files allowed only when on branch head revision'))}
32 32 % endif
33 33 % endif
34 34 </div>
35 35 </div>
36 36 <div class="author">
37 37 <div class="gravatar">
38 38 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.file_changeset.author),16)}"/>
39 39 </div>
40 40 <div title="${c.file_changeset.author}" class="user">${h.person(c.file_changeset.author)}</div>
41 41 </div>
42 42 <div class="commit">${h.urlify_commit(c.file_changeset.message,c.repo_name)}</div>
43 43 </div>
44 44 <div class="code-body">
45 45 %if c.file.is_binary:
46 46 ${_('Binary file (%s)') % c.file.mimetype}
47 47 %else:
48 48 % if c.file.size < c.cut_off_limit:
49 49 %if c.annotate:
50 50 ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
51 51 %else:
52 52 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
53 53 %endif
54 54 %else:
55 55 ${_('File is too big to display')} ${h.link_to(_('show as raw'),
56 56 h.url('files_raw_home',repo_name=c.repo_name,revision=c.file_changeset.raw_id,f_path=c.f_path))}
57 57 %endif
58 58 %endif
59 59 </div>
60 60 </div>
61 61
62 62 <script type="text/javascript">
63 63 YUE.onDOMReady(function(){
64 64 function highlight_lines(lines){
65 65 for(pos in lines){
66 66 YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
67 67 }
68 68 }
69 69 page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
70 70 if (page_highlights.length == 2){
71 71 highlight_ranges = page_highlights[1].split(",");
72 72
73 73 var h_lines = [];
74 74 for (pos in highlight_ranges){
75 75 var _range = highlight_ranges[pos].split('-');
76 76 if(_range.length == 2){
77 77 var start = parseInt(_range[0]);
78 78 var end = parseInt(_range[1]);
79 79 if (start < end){
80 80 for(var i=start;i<=end;i++){
81 81 h_lines.push(i);
82 82 }
83 83 }
84 84 }
85 85 else{
86 86 h_lines.push(parseInt(highlight_ranges[pos]));
87 87 }
88 88 }
89 89 highlight_lines(h_lines);
90 90 var _first_line= YUD.get('L'+h_lines[0]);
91 91 if(_first_line){
92 92 _first_line.scrollIntoView()
93 93 }
94 94 }
95 95
96 96 // select code link event
97 97 YUE.on('hlcode', 'mouseup', getSelectionLink);
98 98
99 99 //load history of file
100 100 YUE.on('load_node_history', 'click', function(e){
101 101 var _url = node_history_url.replace('__REV__','${c.file_changeset.raw_id}').replace('__FPATH__', '${c.f_path}');
102 102 ypjax(_url, 'node_history', function(o){
103 103 tooltip_activate();
104 104 })
105 105 });
106 106
107 107 });
108 108
109 109 </script>
@@ -1,340 +1,340
1 1 <%page args="parent" />
2 2 <div class="box">
3 3 <!-- box / title -->
4 4 <div class="title">
5 5 <h5>
6 6 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
7 7 </h5>
8 8 %if c.rhodecode_user.username != 'default':
9 9 <ul class="links">
10 10 %if h.HasPermissionAny('hg.admin','hg.create.repository')() or h.HasReposGroupPermissionAny('group.write', 'group.admin')(c.group.group_name if c.group else None):
11 11 <li>
12 12 %if c.group:
13 13 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
14 14 %if h.HasPermissionAny('hg.admin')() or h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
15 15 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group', parent_group=c.group.group_id))}</span>
16 16 %endif
17 17 %else:
18 18 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository'))}</span>
19 19 %if h.HasPermissionAny('hg.admin')():
20 20 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group'))}</span>
21 21 %endif
22 22 %endif
23 23 </li>
24 24 %endif
25 25 %if c.group and h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
26 26 <li>
27 27 <span>${h.link_to(_('Edit group'),h.url('edit_repos_group',group_name=c.group.group_name), title=_('You have admin right to this group, and can edit it'))}</span>
28 28 </li>
29 29 %endif
30 30 </ul>
31 31 %endif
32 32 </div>
33 33 <!-- end box / title -->
34 34 <div class="table">
35 35 % if c.groups:
36 36 <div id='groups_list_wrap' class="yui-skin-sam">
37 37 <table id="groups_list">
38 38 <thead>
39 39 <tr>
40 40 <th class="left"><a href="#">${_('Group name')}</a></th>
41 41 <th class="left"><a href="#">${_('Description')}</a></th>
42 42 ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
43 43 </tr>
44 44 </thead>
45 45
46 46 ## REPO GROUPS
47 47 % for gr in c.groups:
48 48 <tr>
49 49 <td>
50 50 <div style="white-space: nowrap">
51 51 <img class="icon" alt="${_('Repository group')}" src="${h.url('/images/icons/database_link.png')}"/>
52 52 ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
53 53 </div>
54 54 </td>
55 55 %if c.visual.stylify_metatags:
56 56 <td>${h.urlify_text(h.desc_stylize(gr.group_description))}</td>
57 57 %else:
58 58 <td>${gr.group_description}</td>
59 59 %endif
60 60 ## this is commented out since for multi nested repos can be HEAVY!
61 61 ## in number of executed queries during traversing uncomment at will
62 62 ##<td><b>${gr.repositories_recursive_count}</b></td>
63 63 </tr>
64 64 % endfor
65 65 </table>
66 66 </div>
67 67 <div id="group-user-paginator" style="padding: 0px 0px 0px 0px"></div>
68 68 <div style="height: 20px"></div>
69 69 % endif
70 70 <div id="welcome" style="display:none;text-align:center">
71 71 <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
72 72 </div>
73 73 <%cnt=0%>
74 74 <%namespace name="dt" file="/data_table/_dt_elements.html"/>
75 % if c.visual.lightweight_dashboard is False:
75 % if not c.visual.lightweight_dashboard:
76 76 ## old full detailed version
77 77 <div id='repos_list_wrap' class="yui-skin-sam">
78 78 <table id="repos_list">
79 79 <thead>
80 80 <tr>
81 81 <th class="left"></th>
82 82 <th class="left">${_('Name')}</th>
83 83 <th class="left">${_('Description')}</th>
84 84 <th class="left">${_('Last change')}</th>
85 85 <th class="left">${_('Tip')}</th>
86 86 <th class="left">${_('Owner')}</th>
87 87 <th class="left">${_('Atom')}</th>
88 88 </tr>
89 89 </thead>
90 90 <tbody>
91 91 %for cnt,repo in enumerate(c.repos_list):
92 92 <tr class="parity${(cnt+1)%2}">
93 93 ##QUICK MENU
94 94 <td class="quick_repo_menu">
95 95 ${dt.quick_menu(repo['name'])}
96 96 </td>
97 97 ##REPO NAME AND ICONS
98 98 <td class="reponame">
99 99 ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']),pageargs.get('short_repo_names'))}
100 100 </td>
101 101 ##DESCRIPTION
102 102 <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
103 103 %if c.visual.stylify_metatags:
104 104 ${h.urlify_text(h.desc_stylize(h.truncate(repo['description'],60)))}</span>
105 105 %else:
106 106 ${h.truncate(repo['description'],60)}</span>
107 107 %endif
108 108 </td>
109 109 ##LAST CHANGE DATE
110 110 <td>
111 111 ${dt.last_change(repo['last_change'])}
112 112 </td>
113 113 ##LAST REVISION
114 114 <td>
115 115 ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
116 116 </td>
117 117 ##
118 118 <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
119 119 <td>
120 120 ${dt.atom(repo['name'])}
121 121 </td>
122 122 </tr>
123 123 %endfor
124 124 </tbody>
125 125 </table>
126 126 </div>
127 127 % else:
128 128 ## lightweight version
129 129 <div class="yui-skin-sam" id="repos_list_wrap"></div>
130 130 <div id="user-paginator" style="padding: 0px 0px 0px 0px"></div>
131 131 % endif
132 132 </div>
133 133 </div>
134 % if c.visual.lightweight_dashboard is False:
134 % if not c.visual.lightweight_dashboard:
135 135 <script>
136 136 YUD.get('repo_count').innerHTML = ${cnt+1 if cnt else 0};
137 137
138 138 // groups table sorting
139 139 var myColumnDefs = [
140 140 {key:"name",label:"${_('Group name')}",sortable:true,
141 141 sortOptions: { sortFunction: groupNameSort }},
142 142 {key:"desc",label:"${_('Description')}",sortable:true},
143 143 ];
144 144
145 145 var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
146 146
147 147 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
148 148 myDataSource.responseSchema = {
149 149 fields: [
150 150 {key:"name"},
151 151 {key:"desc"},
152 152 ]
153 153 };
154 154
155 155 var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,{
156 156 sortedBy:{key:"name",dir:"asc"},
157 157 paginator: new YAHOO.widget.Paginator({
158 158 rowsPerPage: 50,
159 159 alwaysVisible: false,
160 160 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
161 161 pageLinks: 5,
162 162 containerClass: 'pagination-wh',
163 163 currentPageClass: 'pager_curpage',
164 164 pageLinkClass: 'pager_link',
165 165 nextPageLinkLabel: '&gt;',
166 166 previousPageLinkLabel: '&lt;',
167 167 firstPageLinkLabel: '&lt;&lt;',
168 168 lastPageLinkLabel: '&gt;&gt;',
169 169 containers:['group-user-paginator']
170 170 }),
171 171 MSG_SORTASC:"${_('Click to sort ascending')}",
172 172 MSG_SORTDESC:"${_('Click to sort descending')}"
173 173 });
174 174
175 175 // main table sorting
176 176 var myColumnDefs = [
177 177 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
178 178 {key:"name",label:"${_('Name')}",sortable:true,
179 179 sortOptions: { sortFunction: nameSort }},
180 180 {key:"desc",label:"${_('Description')}",sortable:true},
181 181 {key:"last_change",label:"${_('Last Change')}",sortable:true,
182 182 sortOptions: { sortFunction: ageSort }},
183 183 {key:"tip",label:"${_('Tip')}",sortable:true,
184 184 sortOptions: { sortFunction: revisionSort }},
185 185 {key:"owner",label:"${_('Owner')}",sortable:true},
186 186 {key:"atom",label:"",sortable:false},
187 187 ];
188 188
189 189 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
190 190
191 191 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
192 192
193 193 myDataSource.responseSchema = {
194 194 fields: [
195 195 {key:"menu"},
196 196 //{key:"raw_name"},
197 197 {key:"name"},
198 198 {key:"desc"},
199 199 {key:"last_change"},
200 200 {key:"tip"},
201 201 {key:"owner"},
202 202 {key:"atom"},
203 203 ]
204 204 };
205 205
206 206 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
207 207 {
208 208 sortedBy:{key:"name",dir:"asc"},
209 209 MSG_SORTASC:"${_('Click to sort ascending')}",
210 210 MSG_SORTDESC:"${_('Click to sort descending')}",
211 211 MSG_EMPTY:"${_('No records found.')}",
212 212 MSG_ERROR:"${_('Data error.')}",
213 213 MSG_LOADING:"${_('Loading...')}",
214 214 }
215 215 );
216 216 myDataTable.subscribe('postRenderEvent',function(oArgs) {
217 217 tooltip_activate();
218 218 quick_repo_menu();
219 219 var func = function(node){
220 220 return node.parentNode.parentNode.parentNode.parentNode;
221 221 }
222 222 q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
223 223 });
224 224
225 225 </script>
226 226 % else:
227 227 <script>
228 228 var data = ${c.data|n};
229 229 var myDataSource = new YAHOO.util.DataSource(data);
230 230 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
231 231
232 232 myDataSource.responseSchema = {
233 233 resultsList: "records",
234 234 fields: [
235 235 {key:"menu"},
236 236 {key:"raw_name"},
237 237 {key:"name"},
238 238 {key:"desc"},
239 239 {key:"last_change"},
240 240 {key:"last_changeset"},
241 241 {key:"owner"},
242 242 {key:"atom"},
243 243 ]
244 244 };
245 245 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
246 246 // This is the filter function
247 247 var data = res.results || [],
248 248 filtered = [],
249 249 i,l;
250 250
251 251 if (req) {
252 252 req = req.toLowerCase();
253 253 for (i = 0; i<data.length; i++) {
254 254 var pos = data[i].raw_name.toLowerCase().indexOf(req)
255 255 if (pos != -1) {
256 256 filtered.push(data[i]);
257 257 }
258 258 }
259 259 res.results = filtered;
260 260 }
261 261 YUD.get('repo_count').innerHTML = res.results.length;
262 262 return res;
263 263 }
264 264
265 265 // main table sorting
266 266 var myColumnDefs = [
267 267 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
268 268 {key:"name",label:"${_('Name')}",sortable:true,
269 269 sortOptions: { sortFunction: nameSort }},
270 270 {key:"desc",label:"${_('Description')}",sortable:true},
271 271 {key:"last_change",label:"${_('Last Change')}",sortable:true,
272 272 sortOptions: { sortFunction: ageSort }},
273 273 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
274 274 sortOptions: { sortFunction: revisionSort }},
275 275 {key:"owner",label:"${_('Owner')}",sortable:true},
276 276 {key:"atom",label:"",sortable:false},
277 277 ];
278 278
279 279 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
280 280 sortedBy:{key:"name",dir:"asc"},
281 281 paginator: new YAHOO.widget.Paginator({
282 282 rowsPerPage: ${c.visual.lightweight_dashboard_items},
283 283 alwaysVisible: false,
284 284 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
285 285 pageLinks: 5,
286 286 containerClass: 'pagination-wh',
287 287 currentPageClass: 'pager_curpage',
288 288 pageLinkClass: 'pager_link',
289 289 nextPageLinkLabel: '&gt;',
290 290 previousPageLinkLabel: '&lt;',
291 291 firstPageLinkLabel: '&lt;&lt;',
292 292 lastPageLinkLabel: '&gt;&gt;',
293 293 containers:['user-paginator']
294 294 }),
295 295
296 296 MSG_SORTASC:"${_('Click to sort ascending')}",
297 297 MSG_SORTDESC:"${_('Click to sort descending')}",
298 298 MSG_EMPTY:"${_('No repositories found.')}",
299 299 MSG_ERROR:"${_('Data error.')}",
300 300 MSG_LOADING:"${_('Loading...')}",
301 301 }
302 302 );
303 303 myDataTable.subscribe('postRenderEvent',function(oArgs) {
304 304 tooltip_activate();
305 305 quick_repo_menu();
306 306 });
307 307
308 308 var filterTimeout = null;
309 309
310 310 updateFilter = function () {
311 311 // Reset timeout
312 312 filterTimeout = null;
313 313
314 314 // Reset sort
315 315 var state = myDataTable.getState();
316 316 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
317 317
318 318 // Get filtered data
319 319 myDataSource.sendRequest(YUD.get('q_filter').value,{
320 320 success : myDataTable.onDataReturnInitializeTable,
321 321 failure : myDataTable.onDataReturnInitializeTable,
322 322 scope : myDataTable,
323 323 argument: state
324 324 });
325 325
326 326 };
327 327 YUE.on('q_filter','click',function(){
328 328 if(!YUD.hasClass('q_filter', 'loaded')){
329 329 YUD.get('q_filter').value = '';
330 330 //TODO: load here full list later to do search within groups
331 331 YUD.addClass('q_filter', 'loaded');
332 332 }
333 333 });
334 334
335 335 YUE.on('q_filter','keyup',function (e) {
336 336 clearTimeout(filterTimeout);
337 337 filterTimeout = setTimeout(updateFilter,600);
338 338 });
339 339 </script>
340 340 % endif
@@ -1,739 +1,739
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Summary') % c.repo_name} &middot; ${c.rhodecode_name}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('Summary')}
9 9 </%def>
10 10
11 11 <%def name="page_nav()">
12 12 ${self.menu('repositories')}
13 13 </%def>
14 14
15 15 <%def name="head_extra()">
16 16 <link href="${h.url('atom_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('repo %s ATOM feed') % c.repo_name}" type="application/atom+xml" />
17 17 <link href="${h.url('rss_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('repo %s RSS feed') % c.repo_name}" type="application/rss+xml" />
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 ${self.context_bar('summary')}
22 22 <%
23 23 summary = lambda n:{False:'summary-short'}.get(n)
24 24 %>
25 25 %if c.show_stats:
26 26 <div class="box box-left">
27 27 %else:
28 28 <div class="box">
29 29 %endif
30 30 <!-- box / title -->
31 31 <div class="title">
32 32 ${self.breadcrumbs()}
33 33 </div>
34 34 <!-- end box / title -->
35 35 <div class="form">
36 36 <div id="summary" class="fields">
37 37
38 38 <div class="field">
39 39 <div class="label-summary">
40 40 <label>${_('Name')}:</label>
41 41 </div>
42 42 <div class="input ${summary(c.show_stats)}">
43 43
44 44 ## locking icon
45 45 %if c.rhodecode_db_repo.enable_locking:
46 46 %if c.rhodecode_db_repo.locked[0]:
47 47 <span class="locking_locked tooltip" title="${_('Repository locked by %s') % h.person_by_id(c.rhodecode_db_repo.locked[0])}"></span>
48 48 %else:
49 49 <span class="locking_unlocked tooltip" title="${_('Repository unlocked')}"></span>
50 50 %endif
51 51 %endif
52 52 ##REPO TYPE
53 53 %if h.is_hg(c.dbrepo):
54 54 <img style="margin-bottom:2px" class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
55 55 %endif
56 56 %if h.is_git(c.dbrepo):
57 57 <img style="margin-bottom:2px" class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
58 58 %endif
59 59
60 60 ##PUBLIC/PRIVATE
61 61 %if c.dbrepo.private:
62 62 <img style="margin-bottom:2px" class="icon" title="${_('private repository')}" alt="${_('private repository')}" src="${h.url('/images/icons/lock.png')}"/>
63 63 %else:
64 64 <img style="margin-bottom:2px" class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="${h.url('/images/icons/lock_open.png')}"/>
65 65 %endif
66 66
67 67 ##REPO NAME
68 68 <span class="repo_name" title="${_('Non changable ID %s') % c.dbrepo.repo_id}">${h.repo_link(c.dbrepo.groups_and_repo)}</span>
69 69
70 70 ##FORK
71 71 %if c.dbrepo.fork:
72 72 <div style="margin-top:5px;clear:both">
73 73 <a href="${h.url('summary_home',repo_name=c.dbrepo.fork.repo_name)}"><img class="icon" alt="${_('public')}" title="${_('Fork of')} ${c.dbrepo.fork.repo_name}" src="${h.url('/images/icons/arrow_divide.png')}"/>
74 74 ${_('Fork of')} ${c.dbrepo.fork.repo_name}
75 75 </a>
76 76 </div>
77 77 %endif
78 78 ##REMOTE
79 79 %if c.dbrepo.clone_uri:
80 80 <div style="margin-top:5px;clear:both">
81 81 <a href="${h.url(str(h.hide_credentials(c.dbrepo.clone_uri)))}"><img class="icon" alt="${_('remote clone')}" title="${_('Clone from')} ${h.hide_credentials(c.dbrepo.clone_uri)}" src="${h.url('/images/icons/connect.png')}"/>
82 82 ${_('Clone from')} ${h.hide_credentials(c.dbrepo.clone_uri)}
83 83 </a>
84 84 </div>
85 85 %endif
86 86 </div>
87 87 </div>
88 88
89 89 <div class="field">
90 90 <div class="label-summary">
91 91 <label>${_('Description')}:</label>
92 92 </div>
93 93 %if c.visual.stylify_metatags:
94 94 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.desc_stylize(c.dbrepo.description))}</div>
95 95 %else:
96 96 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(c.dbrepo.description)}</div>
97 97 %endif
98 98 </div>
99 99
100 100 <div class="field">
101 101 <div class="label-summary">
102 102 <label>${_('Contact')}:</label>
103 103 </div>
104 104 <div class="input ${summary(c.show_stats)}">
105 105 <div class="gravatar">
106 106 <img alt="gravatar" src="${h.gravatar_url(c.dbrepo.user.email)}"/>
107 107 </div>
108 108 ${_('Username')}: ${c.dbrepo.user.username}<br/>
109 109 ${_('Name')}: ${c.dbrepo.user.name} ${c.dbrepo.user.lastname}<br/>
110 110 ${_('Email')}: <a href="mailto:${c.dbrepo.user.email}">${c.dbrepo.user.email}</a>
111 111 </div>
112 112 </div>
113 113
114 114 <div class="field">
115 115 <div class="label-summary">
116 116 <label>${_('Clone url')}:</label>
117 117 </div>
118 118 <div class="input ${summary(c.show_stats)}">
119 119 <input style="width:80%" type="text" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
120 120 <input style="display:none;width:80%" type="text" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}"/>
121 121 <div style="display:none" id="clone_by_name" class="ui-btn clone">${_('Show by Name')}</div>
122 122 <div id="clone_by_id" class="ui-btn clone">${_('Show by ID')}</div>
123 123 </div>
124 124 </div>
125 125
126 126 <div class="field">
127 127 <div class="label-summary">
128 128 <label>${_('Trending files')}:</label>
129 129 </div>
130 130 <div class="input ${summary(c.show_stats)}">
131 131 %if c.show_stats:
132 132 <div id="lang_stats"></div>
133 133 %else:
134 134 ${_('Statistics are disabled for this repository')}
135 135 %if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
136 136 ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")}
137 137 %endif
138 138 %endif
139 139 </div>
140 140 </div>
141 141
142 142 <div class="field">
143 143 <div class="label-summary">
144 144 <label>${_('Download')}:</label>
145 145 </div>
146 146 <div class="input ${summary(c.show_stats)}">
147 147 %if len(c.rhodecode_repo.revisions) == 0:
148 148 ${_('There are no downloads yet')}
149 %elif c.enable_downloads is False:
149 %elif not c.enable_downloads:
150 150 ${_('Downloads are disabled for this repository')}
151 151 %if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
152 152 ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")}
153 153 %endif
154 154 %else:
155 155 ${h.select('download_options',c.rhodecode_repo.get_changeset().raw_id,c.download_options)}
156 156 <span id="${'zip_link'}">${h.link_to(_('Download as zip'), h.url('files_archive_home',repo_name=c.dbrepo.repo_name,fname='tip.zip'),class_="archive_icon ui-btn")}</span>
157 157 <span style="vertical-align: bottom">
158 158 <input id="archive_subrepos" type="checkbox" name="subrepos" />
159 159 <label for="archive_subrepos" class="tooltip" title="${h.tooltip(_('Check this to download archive with subrepos'))}" >${_('with subrepos')}</label>
160 160 </span>
161 161 %endif
162 162 </div>
163 163 </div>
164 164 </div>
165 165 <div id="summary-menu-stats">
166 166 <ul>
167 167 <li>
168 168 <a class="followers" title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
169 169 ${_('Followers')}
170 170 <span style="float:right" id="current_followers_count">${c.repository_followers}</span>
171 171 </a>
172 172 </li>
173 173 <li>
174 174 <a class="forks" title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
175 175 ${_('Forks')}
176 176 <span style="float:right">${c.repository_forks}</span>
177 177 </a>
178 178 </li>
179 179
180 180 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
181 181 <li>
182 182 %if h.HasPermissionAll('hg.admin')('access settings on repository'):
183 183 ${h.link_to(_('Settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}
184 184 %else:
185 185 ${h.link_to(_('Settings'),h.url('repo_settings_home',repo_name=c.repo_name),class_='settings')}
186 186 %endif
187 187 </li>
188 188 %endif
189 189
190 190 <li>
191 191 %if c.rhodecode_user.username != 'default':
192 192 ${h.link_to(_('Feed'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key),class_='feed')}
193 193 %else:
194 194 ${h.link_to(_('Feed'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name),class_='feed')}
195 195 %endif
196 196 </li>
197 197
198 198 %if c.rhodecode_user.username != 'default':
199 199 <li class="repo_size">
200 200 <a href="#" class="repo-size" onclick="javascript:showRepoSize('repo_size_2','${c.dbrepo.repo_name}','${str(h.get_token())}')">${_('Repository Size')}</a>
201 201 <span id="repo_size_2"></span>
202 202 </li>
203 203 %endif
204 204 </ul>
205 205 </div>
206 206 </div>
207 207 </div>
208 208
209 209 %if c.show_stats:
210 210 <div class="box box-right" style="min-height:455px">
211 211 <!-- box / title -->
212 212 <div class="title">
213 213 <h5>${_('Commit activity by day / author')}</h5>
214 214 </div>
215 215
216 216 <div class="graph">
217 217 <div style="padding:0 10px 10px 17px;">
218 218 %if c.no_data:
219 219 ${c.no_data_msg}
220 220 %if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
221 221 ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")}
222 222 %endif
223 223 %else:
224 224 ${_('Stats gathered: ')} ${c.stats_percentage}%
225 225 %endif
226 226 </div>
227 227 <div id="commit_history" style="width:450px;height:300px;float:left"></div>
228 228 <div style="clear: both;height: 10px"></div>
229 229 <div id="overview" style="width:450px;height:100px;float:left"></div>
230 230
231 231 <div id="legend_data" style="clear:both;margin-top:10px;">
232 232 <div id="legend_container"></div>
233 233 <div id="legend_choices">
234 234 <table id="legend_choices_tables" class="noborder" style="font-size:smaller;color:#545454"></table>
235 235 </div>
236 236 </div>
237 237 </div>
238 238 </div>
239 239 %endif
240 240
241 241 <div class="box">
242 242 <div class="title">
243 243 <div class="breadcrumbs">
244 244 %if c.repo_changesets:
245 245 ${h.link_to(_('Latest changes'),h.url('changelog_home',repo_name=c.repo_name))}
246 246 %else:
247 247 ${_('Quick start')}
248 248 %endif
249 249 </div>
250 250 </div>
251 251 <div class="table">
252 252 <div id="shortlog_data">
253 253 <%include file='../shortlog/shortlog_data.html'/>
254 254 </div>
255 255 </div>
256 256 </div>
257 257
258 258 %if c.readme_data:
259 259 <div id="readme" class="anchor">
260 260 <div class="box" style="background-color: #FAFAFA">
261 261 <div class="title" title="${_("Readme file at revision '%s'" % c.rhodecode_db_repo.landing_rev)}">
262 262 <div class="breadcrumbs">
263 263 <a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a>
264 264 <a class="permalink" href="#readme" title="${_('Permalink to this readme')}">&para;</a>
265 265 </div>
266 266 </div>
267 267 <div class="readme">
268 268 <div class="readme_box">
269 269 ${c.readme_data|n}
270 270 </div>
271 271 </div>
272 272 </div>
273 273 </div>
274 274 %endif
275 275
276 276 <script type="text/javascript">
277 277 var clone_url = 'clone_url';
278 278 YUE.on(clone_url,'click',function(e){
279 279 if(YUD.hasClass(clone_url,'selected')){
280 280 return
281 281 }
282 282 else{
283 283 YUD.addClass(clone_url,'selected');
284 284 YUD.get(clone_url).select();
285 285 }
286 286 })
287 287
288 288 YUE.on('clone_by_name','click',function(e){
289 289 // show url by name and hide name button
290 290 YUD.setStyle('clone_url','display','');
291 291 YUD.setStyle('clone_by_name','display','none');
292 292
293 293 // hide url by id and show name button
294 294 YUD.setStyle('clone_by_id','display','');
295 295 YUD.setStyle('clone_url_id','display','none');
296 296
297 297 })
298 298 YUE.on('clone_by_id','click',function(e){
299 299
300 300 // show url by id and hide id button
301 301 YUD.setStyle('clone_by_id','display','none');
302 302 YUD.setStyle('clone_url_id','display','');
303 303
304 304 // hide url by name and show id button
305 305 YUD.setStyle('clone_by_name','display','');
306 306 YUD.setStyle('clone_url','display','none');
307 307 })
308 308
309 309
310 310 var tmpl_links = {};
311 311 %for cnt,archive in enumerate(c.rhodecode_repo._get_archives()):
312 312 tmpl_links["${archive['type']}"] = '${h.link_to('__NAME__', h.url('files_archive_home',repo_name=c.dbrepo.repo_name, fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_='archive_icon ui-btn')}';
313 313 %endfor
314 314
315 315 YUE.on(['download_options','archive_subrepos'],'change',function(e){
316 316 var sm = YUD.get('download_options');
317 317 var new_cs = sm.options[sm.selectedIndex];
318 318
319 319 for(k in tmpl_links){
320 320 var s = YUD.get(k+'_link');
321 321 if(s){
322 322 var title_tmpl = "${_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__')}";
323 323 title_tmpl= title_tmpl.replace('__CS_NAME__',new_cs.text);
324 324 title_tmpl = title_tmpl.replace('__CS_EXT__',k);
325 325
326 326 var url = tmpl_links[k].replace('__CS__',new_cs.value);
327 327 var subrepos = YUD.get('archive_subrepos').checked;
328 328 url = url.replace('__SUB__',subrepos);
329 329 url = url.replace('__NAME__',title_tmpl);
330 330 s.innerHTML = url
331 331 }
332 332 }
333 333 });
334 334 </script>
335 335 %if c.show_stats:
336 336 <script type="text/javascript">
337 337 var data = ${c.trending_languages|n};
338 338 var total = 0;
339 339 var no_data = true;
340 340 var tbl = document.createElement('table');
341 341 tbl.setAttribute('class','trending_language_tbl');
342 342 var cnt = 0;
343 343 for (var i=0;i<data.length;i++){
344 344 total+= data[i][1].count;
345 345 }
346 346 for (var i=0;i<data.length;i++){
347 347 cnt += 1;
348 348 no_data = false;
349 349
350 350 var hide = cnt>2;
351 351 var tr = document.createElement('tr');
352 352 if (hide){
353 353 tr.setAttribute('style','display:none');
354 354 tr.setAttribute('class','stats_hidden');
355 355 }
356 356 var k = data[i][0];
357 357 var obj = data[i][1];
358 358 var percentage = Math.round((obj.count/total*100),2);
359 359
360 360 var td1 = document.createElement('td');
361 361 td1.width = 150;
362 362 var trending_language_label = document.createElement('div');
363 363 trending_language_label.innerHTML = obj.desc+" ("+k+")";
364 364 td1.appendChild(trending_language_label);
365 365
366 366 var td2 = document.createElement('td');
367 367 td2.setAttribute('style','padding-right:14px !important');
368 368 var trending_language = document.createElement('div');
369 369 var nr_files = obj.count+" ${_('files')}";
370 370
371 371 trending_language.title = k+" "+nr_files;
372 372
373 373 if (percentage>22){
374 374 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
375 375 }
376 376 else{
377 377 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
378 378 }
379 379
380 380 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
381 381 trending_language.style.width=percentage+"%";
382 382 td2.appendChild(trending_language);
383 383
384 384 tr.appendChild(td1);
385 385 tr.appendChild(td2);
386 386 tbl.appendChild(tr);
387 387 if(cnt == 3){
388 388 var show_more = document.createElement('tr');
389 389 var td = document.createElement('td');
390 390 lnk = document.createElement('a');
391 391
392 392 lnk.href='#';
393 393 lnk.innerHTML = "${_('show more')}";
394 394 lnk.id='code_stats_show_more';
395 395 td.appendChild(lnk);
396 396
397 397 show_more.appendChild(td);
398 398 show_more.appendChild(document.createElement('td'));
399 399 tbl.appendChild(show_more);
400 400 }
401 401
402 402 }
403 403
404 404 YUD.get('lang_stats').appendChild(tbl);
405 405 YUE.on('code_stats_show_more','click',function(){
406 406 l = YUD.getElementsByClassName('stats_hidden')
407 407 for (e in l){
408 408 YUD.setStyle(l[e],'display','');
409 409 };
410 410 YUD.setStyle(YUD.get('code_stats_show_more'),
411 411 'display','none');
412 412 });
413 413 </script>
414 414 <script type="text/javascript">
415 415 /**
416 416 * Plots summary graph
417 417 *
418 418 * @class SummaryPlot
419 419 * @param {from} initial from for detailed graph
420 420 * @param {to} initial to for detailed graph
421 421 * @param {dataset}
422 422 * @param {overview_dataset}
423 423 */
424 424 function SummaryPlot(from,to,dataset,overview_dataset) {
425 425 var initial_ranges = {
426 426 "xaxis":{
427 427 "from":from,
428 428 "to":to,
429 429 },
430 430 };
431 431 var dataset = dataset;
432 432 var overview_dataset = [overview_dataset];
433 433 var choiceContainer = YUD.get("legend_choices");
434 434 var choiceContainerTable = YUD.get("legend_choices_tables");
435 435 var plotContainer = YUD.get('commit_history');
436 436 var overviewContainer = YUD.get('overview');
437 437
438 438 var plot_options = {
439 439 bars: {show:true,align:'center',lineWidth:4},
440 440 legend: {show:true, container:"legend_container"},
441 441 points: {show:true,radius:0,fill:false},
442 442 yaxis: {tickDecimals:0,},
443 443 xaxis: {
444 444 mode: "time",
445 445 timeformat: "%d/%m",
446 446 min:from,
447 447 max:to,
448 448 },
449 449 grid: {
450 450 hoverable: true,
451 451 clickable: true,
452 452 autoHighlight:true,
453 453 color: "#999"
454 454 },
455 455 //selection: {mode: "x"}
456 456 };
457 457 var overview_options = {
458 458 legend:{show:false},
459 459 bars: {show:true,barWidth: 2,},
460 460 shadowSize: 0,
461 461 xaxis: {mode: "time", timeformat: "%d/%m/%y",},
462 462 yaxis: {ticks: 3, min: 0,tickDecimals:0,},
463 463 grid: {color: "#999",},
464 464 selection: {mode: "x"}
465 465 };
466 466
467 467 /**
468 468 *get dummy data needed in few places
469 469 */
470 470 function getDummyData(label){
471 471 return {"label":label,
472 472 "data":[{"time":0,
473 473 "commits":0,
474 474 "added":0,
475 475 "changed":0,
476 476 "removed":0,
477 477 }],
478 478 "schema":["commits"],
479 479 "color":'#ffffff',
480 480 }
481 481 }
482 482
483 483 /**
484 484 * generate checkboxes accordindly to data
485 485 * @param keys
486 486 * @returns
487 487 */
488 488 function generateCheckboxes(data) {
489 489 //append checkboxes
490 490 var i = 0;
491 491 choiceContainerTable.innerHTML = '';
492 492 for(var pos in data) {
493 493
494 494 data[pos].color = i;
495 495 i++;
496 496 if(data[pos].label != ''){
497 497 choiceContainerTable.innerHTML +=
498 498 '<tr><td><input type="checkbox" id="id_user_{0}" name="{0}" checked="checked" /> \
499 499 <label for="id_user_{0}">{0}</label></td></tr>'.format(data[pos].label);
500 500 }
501 501 }
502 502 }
503 503
504 504 /**
505 505 * ToolTip show
506 506 */
507 507 function showTooltip(x, y, contents) {
508 508 var div=document.getElementById('tooltip');
509 509 if(!div) {
510 510 div = document.createElement('div');
511 511 div.id="tooltip";
512 512 div.style.position="absolute";
513 513 div.style.border='1px solid #fdd';
514 514 div.style.padding='2px';
515 515 div.style.backgroundColor='#fee';
516 516 document.body.appendChild(div);
517 517 }
518 518 YUD.setStyle(div, 'opacity', 0);
519 519 div.innerHTML = contents;
520 520 div.style.top=(y + 5) + "px";
521 521 div.style.left=(x + 5) + "px";
522 522
523 523 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
524 524 anim.animate();
525 525 }
526 526
527 527 /**
528 528 * This function will detect if selected period has some changesets
529 529 for this user if it does this data is then pushed for displaying
530 530 Additionally it will only display users that are selected by the checkbox
531 531 */
532 532 function getDataAccordingToRanges(ranges) {
533 533
534 534 var data = [];
535 535 var new_dataset = {};
536 536 var keys = [];
537 537 var max_commits = 0;
538 538 for(var key in dataset){
539 539
540 540 for(var ds in dataset[key].data){
541 541 commit_data = dataset[key].data[ds];
542 542 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
543 543
544 544 if(new_dataset[key] === undefined){
545 545 new_dataset[key] = {data:[],schema:["commits"],label:key};
546 546 }
547 547 new_dataset[key].data.push(commit_data);
548 548 }
549 549 }
550 550 if (new_dataset[key] !== undefined){
551 551 data.push(new_dataset[key]);
552 552 }
553 553 }
554 554
555 555 if (data.length > 0){
556 556 return data;
557 557 }
558 558 else{
559 559 //just return dummy data for graph to plot itself
560 560 return [getDummyData('')];
561 561 }
562 562 }
563 563
564 564 /**
565 565 * redraw using new checkbox data
566 566 */
567 567 function plotchoiced(e,args){
568 568 var cur_data = args[0];
569 569 var cur_ranges = args[1];
570 570
571 571 var new_data = [];
572 572 var inputs = choiceContainer.getElementsByTagName("input");
573 573
574 574 //show only checked labels
575 575 for(var i=0; i<inputs.length; i++) {
576 576 var checkbox_key = inputs[i].name;
577 577
578 578 if(inputs[i].checked){
579 579 for(var d in cur_data){
580 580 if(cur_data[d].label == checkbox_key){
581 581 new_data.push(cur_data[d]);
582 582 }
583 583 }
584 584 }
585 585 else{
586 586 //push dummy data to not hide the label
587 587 new_data.push(getDummyData(checkbox_key));
588 588 }
589 589 }
590 590
591 591 var new_options = YAHOO.lang.merge(plot_options, {
592 592 xaxis: {
593 593 min: cur_ranges.xaxis.from,
594 594 max: cur_ranges.xaxis.to,
595 595 mode:"time",
596 596 timeformat: "%d/%m",
597 597 },
598 598 });
599 599 if (!new_data){
600 600 new_data = [[0,1]];
601 601 }
602 602 // do the zooming
603 603 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
604 604
605 605 plot.subscribe("plotselected", plotselected);
606 606
607 607 //resubscribe plothover
608 608 plot.subscribe("plothover", plothover);
609 609
610 610 // don't fire event on the overview to prevent eternal loop
611 611 overview.setSelection(cur_ranges, true);
612 612
613 613 }
614 614
615 615 /**
616 616 * plot only selected items from overview
617 617 * @param ranges
618 618 * @returns
619 619 */
620 620 function plotselected(ranges,cur_data) {
621 621 //updates the data for new plot
622 622 var data = getDataAccordingToRanges(ranges);
623 623 generateCheckboxes(data);
624 624
625 625 var new_options = YAHOO.lang.merge(plot_options, {
626 626 xaxis: {
627 627 min: ranges.xaxis.from,
628 628 max: ranges.xaxis.to,
629 629 mode:"time",
630 630 timeformat: "%d/%m",
631 631 },
632 632 });
633 633 // do the zooming
634 634 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
635 635
636 636 plot.subscribe("plotselected", plotselected);
637 637
638 638 //resubscribe plothover
639 639 plot.subscribe("plothover", plothover);
640 640
641 641 // don't fire event on the overview to prevent eternal loop
642 642 overview.setSelection(ranges, true);
643 643
644 644 //resubscribe choiced
645 645 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
646 646 }
647 647
648 648 var previousPoint = null;
649 649
650 650 function plothover(o) {
651 651 var pos = o.pos;
652 652 var item = o.item;
653 653
654 654 //YUD.get("x").innerHTML = pos.x.toFixed(2);
655 655 //YUD.get("y").innerHTML = pos.y.toFixed(2);
656 656 if (item) {
657 657 if (previousPoint != item.datapoint) {
658 658 previousPoint = item.datapoint;
659 659
660 660 var tooltip = YUD.get("tooltip");
661 661 if(tooltip) {
662 662 tooltip.parentNode.removeChild(tooltip);
663 663 }
664 664 var x = item.datapoint.x.toFixed(2);
665 665 var y = item.datapoint.y.toFixed(2);
666 666
667 667 if (!item.series.label){
668 668 item.series.label = 'commits';
669 669 }
670 670 var d = new Date(x*1000);
671 671 var fd = d.toDateString()
672 672 var nr_commits = parseInt(y);
673 673
674 674 var cur_data = dataset[item.series.label].data[item.dataIndex];
675 675 var added = cur_data.added;
676 676 var changed = cur_data.changed;
677 677 var removed = cur_data.removed;
678 678
679 679 var nr_commits_suffix = " ${_('commits')} ";
680 680 var added_suffix = " ${_('files added')} ";
681 681 var changed_suffix = " ${_('files changed')} ";
682 682 var removed_suffix = " ${_('files removed')} ";
683 683
684 684
685 685 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
686 686 if(added==1){added_suffix=" ${_('file added')} ";}
687 687 if(changed==1){changed_suffix=" ${_('file changed')} ";}
688 688 if(removed==1){removed_suffix=" ${_('file removed')} ";}
689 689
690 690 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
691 691 +'<br/>'+
692 692 nr_commits + nr_commits_suffix+'<br/>'+
693 693 added + added_suffix +'<br/>'+
694 694 changed + changed_suffix + '<br/>'+
695 695 removed + removed_suffix + '<br/>');
696 696 }
697 697 }
698 698 else {
699 699 var tooltip = YUD.get("tooltip");
700 700
701 701 if(tooltip) {
702 702 tooltip.parentNode.removeChild(tooltip);
703 703 }
704 704 previousPoint = null;
705 705 }
706 706 }
707 707
708 708 /**
709 709 * MAIN EXECUTION
710 710 */
711 711
712 712 var data = getDataAccordingToRanges(initial_ranges);
713 713 generateCheckboxes(data);
714 714
715 715 //main plot
716 716 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
717 717
718 718 //overview
719 719 var overview = YAHOO.widget.Flot(overviewContainer,
720 720 overview_dataset, overview_options);
721 721
722 722 //show initial selection on overview
723 723 overview.setSelection(initial_ranges);
724 724
725 725 plot.subscribe("plotselected", plotselected);
726 726 plot.subscribe("plothover", plothover)
727 727
728 728 overview.subscribe("plotselected", function (ranges) {
729 729 plot.setSelection(ranges);
730 730 });
731 731
732 732 // user choices on overview
733 733 YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
734 734 }
735 735 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
736 736 </script>
737 737 %endif
738 738
739 739 </%def>
@@ -1,221 +1,221
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.tests.test_hg_operations
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Test suite for making push/pull operations
7 7
8 8 :created_on: Dec 30, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import shutil
29 29 import logging
30 30 from os.path import join as jn
31 31 from os.path import dirname as dn
32 32
33 33 from tempfile import _RandomNameSequence
34 34 from subprocess import Popen, PIPE
35 35
36 36 from paste.deploy import appconfig
37 37 from pylons import config
38 38 from sqlalchemy import engine_from_config
39 39
40 40 from rhodecode.lib.utils import add_cache
41 41 from rhodecode.model import init_model
42 42 from rhodecode.model import meta
43 43 from rhodecode.model.db import User, Repository
44 44 from rhodecode.lib.auth import get_crypt_password
45 45
46 46 from rhodecode.tests import TESTS_TMP_PATH, NEW_HG_REPO, HG_REPO
47 47 from rhodecode.config.environment import load_environment
48 48
49 49 rel_path = dn(dn(dn(dn(os.path.abspath(__file__)))))
50 50 conf = appconfig('config:rc.ini', relative_to=rel_path)
51 51 load_environment(conf.global_conf, conf.local_conf)
52 52
53 53 add_cache(conf)
54 54
55 55 USER = 'test_admin'
56 56 PASS = 'test12'
57 57 HOST = 'rc.local'
58 58 METHOD = 'pull'
59 59 DEBUG = True
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class Command(object):
64 64
65 65 def __init__(self, cwd):
66 66 self.cwd = cwd
67 67
68 68 def execute(self, cmd, *args):
69 69 """Runs command on the system with given ``args``.
70 70 """
71 71
72 72 command = cmd + ' ' + ' '.join(args)
73 73 log.debug('Executing %s' % command)
74 74 if DEBUG:
75 75 print command
76 76 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd)
77 77 stdout, stderr = p.communicate()
78 78 if DEBUG:
79 79 print stdout, stderr
80 80 return stdout, stderr
81 81
82 82
83 83 def get_session():
84 84 engine = engine_from_config(conf, 'sqlalchemy.db1.')
85 85 init_model(engine)
86 86 sa = meta.Session
87 87 return sa
88 88
89 89
90 90 def create_test_user(force=True):
91 91 print 'creating test user'
92 92 sa = get_session()
93 93
94 94 user = sa.query(User).filter(User.username == USER).scalar()
95 95
96 96 if force and user is not None:
97 97 print 'removing current user'
98 98 for repo in sa.query(Repository).filter(Repository.user == user).all():
99 99 sa.delete(repo)
100 100 sa.delete(user)
101 101 sa.commit()
102 102
103 103 if user is None or force:
104 104 print 'creating new one'
105 105 new_usr = User()
106 106 new_usr.username = USER
107 107 new_usr.password = get_crypt_password(PASS)
108 108 new_usr.email = 'mail@mail.com'
109 109 new_usr.name = 'test'
110 110 new_usr.lastname = 'lasttestname'
111 111 new_usr.active = True
112 112 new_usr.admin = True
113 113 sa.add(new_usr)
114 114 sa.commit()
115 115
116 116 print 'done'
117 117
118 118
119 119 def create_test_repo(force=True):
120 120 print 'creating test repo'
121 121 from rhodecode.model.repo import RepoModel
122 122 sa = get_session()
123 123
124 124 user = sa.query(User).filter(User.username == USER).scalar()
125 125 if user is None:
126 126 raise Exception('user not found')
127 127
128 128 repo = sa.query(Repository).filter(Repository.repo_name == HG_REPO).scalar()
129 129
130 130 if repo is None:
131 131 print 'repo not found creating'
132 132
133 133 form_data = {'repo_name': HG_REPO,
134 134 'repo_type': 'hg',
135 135 'private':False,
136 136 'clone_uri': '' }
137 137 rm = RepoModel(sa)
138 138 rm.base_path = '/home/hg'
139 139 rm.create(form_data, user)
140 140
141 141 print 'done'
142 142
143 143
144 144 def set_anonymous_access(enable=True):
145 145 sa = get_session()
146 146 user = sa.query(User).filter(User.username == 'default').one()
147 147 user.active = enable
148 148 sa.add(user)
149 149 sa.commit()
150 150
151 151
152 152 def get_anonymous_access():
153 153 sa = get_session()
154 154 return sa.query(User).filter(User.username == 'default').one().active
155 155
156 156
157 157 #==============================================================================
158 158 # TESTS
159 159 #==============================================================================
160 160 def test_clone_with_credentials(no_errors=False, repo=HG_REPO, method=METHOD,
161 161 seq=None, backend='hg'):
162 162 cwd = path = jn(TESTS_TMP_PATH, repo)
163 163
164 164 if seq == None:
165 165 seq = _RandomNameSequence().next()
166 166
167 167 try:
168 168 shutil.rmtree(path, ignore_errors=True)
169 169 os.makedirs(path)
170 170 #print 'made dirs %s' % jn(path)
171 171 except OSError:
172 172 raise
173 173
174 174 clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \
175 175 {'user': USER,
176 176 'pass': PASS,
177 177 'host': HOST,
178 178 'cloned_repo': repo, }
179 179
180 180 dest = path + seq
181 181 if method == 'pull':
182 182 stdout, stderr = Command(cwd).execute(backend, method, '--cwd', dest, clone_url)
183 183 else:
184 184 stdout, stderr = Command(cwd).execute(backend, method, clone_url, dest)
185 185 print stdout,'sdasdsadsa'
186 if no_errors is False:
186 if not no_errors:
187 187 if backend == 'hg':
188 188 assert """adding file changes""" in stdout, 'no messages about cloning'
189 189 assert """abort""" not in stderr , 'got error from clone'
190 190 elif backend == 'git':
191 191 assert """Cloning into""" in stdout, 'no messages about cloning'
192 192
193 193 if __name__ == '__main__':
194 194 try:
195 195 create_test_user(force=False)
196 196 seq = None
197 197 import time
198 198
199 199 try:
200 200 METHOD = sys.argv[3]
201 201 except:
202 202 pass
203 203
204 204 try:
205 205 backend = sys.argv[4]
206 206 except:
207 207 backend = 'hg'
208 208
209 209 if METHOD == 'pull':
210 210 seq = _RandomNameSequence().next()
211 211 test_clone_with_credentials(repo=sys.argv[1], method='clone',
212 212 seq=seq, backend=backend)
213 213 s = time.time()
214 214 for i in range(1, int(sys.argv[2]) + 1):
215 215 print 'take', i
216 216 test_clone_with_credentials(repo=sys.argv[1], method=METHOD,
217 217 seq=seq, backend=backend)
218 218 print 'time taken %.3f' % (time.time() - s)
219 219 except Exception, e:
220 220 raise
221 221 sys.exit('stop on %s' % e)
General Comments 0
You need to be logged in to leave comments. Login now