##// END OF EJS Templates
validating choices for landing_rev
marcink -
r2460:12fa0c19 beta
parent child Browse files
Show More
@@ -1,437 +1,442 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repositories controller for RhodeCode
7 7
8 8 :created_on: Apr 7, 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 from formencode import htmlfill
30 30
31 31 from paste.httpexceptions import HTTPInternalServerError
32 32 from pylons import request, session, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.exc import IntegrityError
36 36
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
39 39 HasPermissionAnyDecorator, HasRepoPermissionAllDecorator
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
42 42 from rhodecode.lib.helpers import get_token
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup
45 45 from rhodecode.model.forms import RepoForm
46 46 from rhodecode.model.scm import ScmModel
47 47 from rhodecode.model.repo import RepoModel
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ReposController(BaseController):
53 53 """
54 54 REST Controller styled on the Atom Publishing Protocol"""
55 55 # To properly map this controller, ensure your config/routing.py
56 56 # file has a resource setup:
57 57 # map.resource('repo', 'repos')
58 58
59 59 @LoginRequired()
60 60 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
61 61 def __before__(self):
62 62 c.admin_user = session.get('admin_user')
63 63 c.admin_username = session.get('admin_username')
64 64 super(ReposController, self).__before__()
65 65
66 66 def __load_defaults(self):
67 67 c.repo_groups = RepoGroup.groups_choices()
68 68 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
69 69
70 70 repo_model = RepoModel()
71 71 c.users_array = repo_model.get_users_js()
72 72 c.users_groups_array = repo_model.get_users_groups_js()
73 c.landing_revs = ScmModel().get_repo_landing_revs()
73 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
74 c.landing_revs_choices = choices
74 75
75 76 def __load_data(self, repo_name=None):
76 77 """
77 78 Load defaults settings for edit, and update
78 79
79 80 :param repo_name:
80 81 """
81 82 self.__load_defaults()
82 83
83 84 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
84 85 repo = db_repo.scm_instance
85 86
86 87 if c.repo_info is None:
87 88 h.flash(_('%s repository is not mapped to db perhaps'
88 89 ' it was created or renamed from the filesystem'
89 90 ' please run the application again'
90 91 ' in order to rescan repositories') % repo_name,
91 92 category='error')
92 93
93 94 return redirect(url('repos'))
94 95
95 c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
96 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
97 c.landing_revs_choices = choices
98
96 99 c.default_user_id = User.get_by_username('default').user_id
97 100 c.in_public_journal = UserFollowing.query()\
98 101 .filter(UserFollowing.user_id == c.default_user_id)\
99 102 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
100 103
101 104 if c.repo_info.stats:
102 105 # this is on what revision we ended up so we add +1 for count
103 106 last_rev = c.repo_info.stats.stat_on_revision + 1
104 107 else:
105 108 last_rev = 0
106 109 c.stats_revision = last_rev
107 110
108 111 c.repo_last_rev = repo.count() if repo.revisions else 0
109 112
110 113 if last_rev == 0 or c.repo_last_rev == 0:
111 114 c.stats_percentage = 0
112 115 else:
113 116 c.stats_percentage = '%.2f' % ((float((last_rev)) /
114 117 c.repo_last_rev) * 100)
115 118
116 119 defaults = RepoModel()._get_defaults(repo_name)
117 120
118 121 c.repos_list = [('', _('--REMOVE FORK--'))]
119 122 c.repos_list += [(x.repo_id, x.repo_name) for x in
120 123 Repository.query().order_by(Repository.repo_name).all()]
121 124
122 125 return defaults
123 126
124 127 @HasPermissionAllDecorator('hg.admin')
125 128 def index(self, format='html'):
126 129 """GET /repos: All items in the collection"""
127 130 # url('repos')
128 131
129 132 c.repos_list = ScmModel().get_repos(Repository.query()
130 133 .order_by(Repository.repo_name)
131 134 .all(), sort_key='name_sort')
132 135 return render('admin/repos/repos.html')
133 136
134 137 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
135 138 def create(self):
136 139 """
137 140 POST /repos: Create a new item"""
138 141 # url('repos')
139 142
140 143 self.__load_defaults()
141 144 form_result = {}
142 145 try:
143 form_result = RepoForm(repo_groups=c.repo_groups_choices)()\
146 form_result = RepoForm(repo_groups=c.repo_groups_choices,
147 landing_revs=c.landing_revs_choices)()\
144 148 .to_python(dict(request.POST))
145 149 RepoModel().create(form_result, self.rhodecode_user)
146 150 if form_result['clone_uri']:
147 151 h.flash(_('created repository %s from %s') \
148 152 % (form_result['repo_name'], form_result['clone_uri']),
149 153 category='success')
150 154 else:
151 155 h.flash(_('created repository %s') % form_result['repo_name'],
152 156 category='success')
153 157
154 158 if request.POST.get('user_created'):
155 159 # created by regular non admin user
156 160 action_logger(self.rhodecode_user, 'user_created_repo',
157 161 form_result['repo_name_full'], self.ip_addr,
158 162 self.sa)
159 163 else:
160 164 action_logger(self.rhodecode_user, 'admin_created_repo',
161 165 form_result['repo_name_full'], self.ip_addr,
162 166 self.sa)
163 167 Session.commit()
164 168 except formencode.Invalid, errors:
165 169
166 170 c.new_repo = errors.value['repo_name']
167 171
168 172 if request.POST.get('user_created'):
169 173 r = render('admin/repos/repo_add_create_repository.html')
170 174 else:
171 175 r = render('admin/repos/repo_add.html')
172 176
173 177 return htmlfill.render(
174 178 r,
175 179 defaults=errors.value,
176 180 errors=errors.error_dict or {},
177 181 prefix_error=False,
178 182 encoding="UTF-8")
179 183
180 184 except Exception:
181 185 log.error(traceback.format_exc())
182 186 msg = _('error occurred during creation of repository %s') \
183 187 % form_result.get('repo_name')
184 188 h.flash(msg, category='error')
185 189 if request.POST.get('user_created'):
186 190 return redirect(url('home'))
187 191 return redirect(url('repos'))
188 192
189 193 @HasPermissionAllDecorator('hg.admin')
190 194 def new(self, format='html'):
191 195 """GET /repos/new: Form to create a new item"""
192 196 new_repo = request.GET.get('repo', '')
193 197 c.new_repo = repo_name_slug(new_repo)
194 198 self.__load_defaults()
195 199 return render('admin/repos/repo_add.html')
196 200
197 201 @HasPermissionAllDecorator('hg.admin')
198 202 def update(self, repo_name):
199 203 """
200 204 PUT /repos/repo_name: Update an existing item"""
201 205 # Forms posted to this method should contain a hidden field:
202 206 # <input type="hidden" name="_method" value="PUT" />
203 207 # Or using helpers:
204 208 # h.form(url('repo', repo_name=ID),
205 209 # method='put')
206 210 # url('repo', repo_name=ID)
207 211 self.__load_defaults()
208 212 repo_model = RepoModel()
209 213 changed_name = repo_name
210 214 _form = RepoForm(edit=True, old_data={'repo_name': repo_name},
211 repo_groups=c.repo_groups_choices)()
215 repo_groups=c.repo_groups_choices,
216 landing_revs=c.landing_revs_choices)()
212 217 try:
213 218 form_result = _form.to_python(dict(request.POST))
214 219 repo = repo_model.update(repo_name, form_result)
215 220 invalidate_cache('get_repo_cached_%s' % repo_name)
216 221 h.flash(_('Repository %s updated successfully' % repo_name),
217 222 category='success')
218 223 changed_name = repo.repo_name
219 224 action_logger(self.rhodecode_user, 'admin_updated_repo',
220 225 changed_name, self.ip_addr, self.sa)
221 226 Session.commit()
222 227 except formencode.Invalid, errors:
223 228 defaults = self.__load_data(repo_name)
224 229 defaults.update(errors.value)
225 230 return htmlfill.render(
226 231 render('admin/repos/repo_edit.html'),
227 232 defaults=defaults,
228 233 errors=errors.error_dict or {},
229 234 prefix_error=False,
230 235 encoding="UTF-8")
231 236
232 237 except Exception:
233 238 log.error(traceback.format_exc())
234 239 h.flash(_('error occurred during update of repository %s') \
235 240 % repo_name, category='error')
236 241 return redirect(url('edit_repo', repo_name=changed_name))
237 242
238 243 @HasPermissionAllDecorator('hg.admin')
239 244 def delete(self, repo_name):
240 245 """
241 246 DELETE /repos/repo_name: Delete an existing item"""
242 247 # Forms posted to this method should contain a hidden field:
243 248 # <input type="hidden" name="_method" value="DELETE" />
244 249 # Or using helpers:
245 250 # h.form(url('repo', repo_name=ID),
246 251 # method='delete')
247 252 # url('repo', repo_name=ID)
248 253
249 254 repo_model = RepoModel()
250 255 repo = repo_model.get_by_repo_name(repo_name)
251 256 if not repo:
252 257 h.flash(_('%s repository is not mapped to db perhaps'
253 258 ' it was moved or renamed from the filesystem'
254 259 ' please run the application again'
255 260 ' in order to rescan repositories') % repo_name,
256 261 category='error')
257 262
258 263 return redirect(url('repos'))
259 264 try:
260 265 action_logger(self.rhodecode_user, 'admin_deleted_repo',
261 266 repo_name, self.ip_addr, self.sa)
262 267 repo_model.delete(repo)
263 268 invalidate_cache('get_repo_cached_%s' % repo_name)
264 269 h.flash(_('deleted repository %s') % repo_name, category='success')
265 270 Session.commit()
266 271 except IntegrityError, e:
267 272 if e.message.find('repositories_fork_id_fkey') != -1:
268 273 log.error(traceback.format_exc())
269 274 h.flash(_('Cannot delete %s it still contains attached '
270 275 'forks') % repo_name,
271 276 category='warning')
272 277 else:
273 278 log.error(traceback.format_exc())
274 279 h.flash(_('An error occurred during '
275 280 'deletion of %s') % repo_name,
276 281 category='error')
277 282
278 283 except Exception, e:
279 284 log.error(traceback.format_exc())
280 285 h.flash(_('An error occurred during deletion of %s') % repo_name,
281 286 category='error')
282 287
283 288 return redirect(url('repos'))
284 289
285 290 @HasRepoPermissionAllDecorator('repository.admin')
286 291 def delete_perm_user(self, repo_name):
287 292 """
288 293 DELETE an existing repository permission user
289 294
290 295 :param repo_name:
291 296 """
292 297 try:
293 298 RepoModel().revoke_user_permission(repo=repo_name,
294 299 user=request.POST['user_id'])
295 300 Session.commit()
296 301 except Exception:
297 302 log.error(traceback.format_exc())
298 303 h.flash(_('An error occurred during deletion of repository user'),
299 304 category='error')
300 305 raise HTTPInternalServerError()
301 306
302 307 @HasRepoPermissionAllDecorator('repository.admin')
303 308 def delete_perm_users_group(self, repo_name):
304 309 """
305 310 DELETE an existing repository permission users group
306 311
307 312 :param repo_name:
308 313 """
309 314
310 315 try:
311 316 RepoModel().revoke_users_group_permission(
312 317 repo=repo_name, group_name=request.POST['users_group_id']
313 318 )
314 319 Session.commit()
315 320 except Exception:
316 321 log.error(traceback.format_exc())
317 322 h.flash(_('An error occurred during deletion of repository'
318 323 ' users groups'),
319 324 category='error')
320 325 raise HTTPInternalServerError()
321 326
322 327 @HasPermissionAllDecorator('hg.admin')
323 328 def repo_stats(self, repo_name):
324 329 """
325 330 DELETE an existing repository statistics
326 331
327 332 :param repo_name:
328 333 """
329 334
330 335 try:
331 336 RepoModel().delete_stats(repo_name)
332 337 Session.commit()
333 338 except Exception, e:
334 339 h.flash(_('An error occurred during deletion of repository stats'),
335 340 category='error')
336 341 return redirect(url('edit_repo', repo_name=repo_name))
337 342
338 343 @HasPermissionAllDecorator('hg.admin')
339 344 def repo_cache(self, repo_name):
340 345 """
341 346 INVALIDATE existing repository cache
342 347
343 348 :param repo_name:
344 349 """
345 350
346 351 try:
347 352 ScmModel().mark_for_invalidation(repo_name)
348 353 Session.commit()
349 354 except Exception, e:
350 355 h.flash(_('An error occurred during cache invalidation'),
351 356 category='error')
352 357 return redirect(url('edit_repo', repo_name=repo_name))
353 358
354 359 @HasPermissionAllDecorator('hg.admin')
355 360 def repo_public_journal(self, repo_name):
356 361 """
357 362 Set's this repository to be visible in public journal,
358 363 in other words assing default user to follow this repo
359 364
360 365 :param repo_name:
361 366 """
362 367
363 368 cur_token = request.POST.get('auth_token')
364 369 token = get_token()
365 370 if cur_token == token:
366 371 try:
367 372 repo_id = Repository.get_by_repo_name(repo_name).repo_id
368 373 user_id = User.get_by_username('default').user_id
369 374 self.scm_model.toggle_following_repo(repo_id, user_id)
370 375 h.flash(_('Updated repository visibility in public journal'),
371 376 category='success')
372 377 Session.commit()
373 378 except:
374 379 h.flash(_('An error occurred during setting this'
375 380 ' repository in public journal'),
376 381 category='error')
377 382
378 383 else:
379 384 h.flash(_('Token mismatch'), category='error')
380 385 return redirect(url('edit_repo', repo_name=repo_name))
381 386
382 387 @HasPermissionAllDecorator('hg.admin')
383 388 def repo_pull(self, repo_name):
384 389 """
385 390 Runs task to update given repository with remote changes,
386 391 ie. make pull on remote location
387 392
388 393 :param repo_name:
389 394 """
390 395 try:
391 396 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
392 397 h.flash(_('Pulled from remote location'), category='success')
393 398 except Exception, e:
394 399 h.flash(_('An error occurred during pull from remote location'),
395 400 category='error')
396 401
397 402 return redirect(url('edit_repo', repo_name=repo_name))
398 403
399 404 @HasPermissionAllDecorator('hg.admin')
400 405 def repo_as_fork(self, repo_name):
401 406 """
402 407 Mark given repository as a fork of another
403 408
404 409 :param repo_name:
405 410 """
406 411 try:
407 412 fork_id = request.POST.get('id_fork_of')
408 413 repo = ScmModel().mark_as_fork(repo_name, fork_id,
409 414 self.rhodecode_user.username)
410 415 fork = repo.fork.repo_name if repo.fork else _('Nothing')
411 416 Session.commit()
412 417 h.flash(_('Marked repo %s as fork of %s' % (repo_name,fork)),
413 418 category='success')
414 419 except Exception, e:
415 420 raise
416 421 h.flash(_('An error occurred during this operation'),
417 422 category='error')
418 423
419 424 return redirect(url('edit_repo', repo_name=repo_name))
420 425
421 426 @HasPermissionAllDecorator('hg.admin')
422 427 def show(self, repo_name, format='html'):
423 428 """GET /repos/repo_name: Show a specific item"""
424 429 # url('repo', repo_name=ID)
425 430
426 431 @HasPermissionAllDecorator('hg.admin')
427 432 def edit(self, repo_name, format='html'):
428 433 """GET /repos/repo_name/edit: Form to edit an existing item"""
429 434 # url('edit_repo', repo_name=ID)
430 435 defaults = self.__load_data(repo_name)
431 436
432 437 return htmlfill.render(
433 438 render('admin/repos/repo_edit.html'),
434 439 defaults=defaults,
435 440 encoding="UTF-8",
436 441 force_defaults=False
437 442 )
@@ -1,158 +1,162 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Settings controller for rhodecode
7 7
8 8 :created_on: Jun 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 logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31
32 32 from pylons import tmpl_context as c, request, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 import rhodecode.lib.helpers as h
37 37
38 38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
39 39 from rhodecode.lib.base import BaseRepoController, render
40 40 from rhodecode.lib.utils import invalidate_cache, action_logger
41 41
42 42 from rhodecode.model.forms import RepoSettingsForm
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.db import RepoGroup
45 45 from rhodecode.model.meta import Session
46 from rhodecode.model.scm import ScmModel
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 class SettingsController(BaseRepoController):
51 52
52 53 @LoginRequired()
53 54 def __before__(self):
54 55 super(SettingsController, self).__before__()
55 56
56 57 def __load_defaults(self):
57 58 c.repo_groups = RepoGroup.groups_choices()
58 59 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59 60
60 61 repo_model = RepoModel()
61 62 c.users_array = repo_model.get_users_js()
62 63 c.users_groups_array = repo_model.get_users_groups_js()
64 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
65 c.landing_revs_choices = choices
63 66
64 67 @HasRepoPermissionAllDecorator('repository.admin')
65 68 def index(self, repo_name):
66 69 repo_model = RepoModel()
67 70 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
68 71 if not repo:
69 72 h.flash(_('%s repository is not mapped to db perhaps'
70 73 ' it was created or renamed from the file system'
71 74 ' please run the application again'
72 75 ' in order to rescan repositories') % repo_name,
73 76 category='error')
74 77
75 78 return redirect(url('home'))
76 79
77 80 self.__load_defaults()
78 81
79 82 defaults = RepoModel()._get_defaults(repo_name)
80 83
81 84 return htmlfill.render(
82 85 render('settings/repo_settings.html'),
83 86 defaults=defaults,
84 87 encoding="UTF-8",
85 88 force_defaults=False
86 89 )
87 90
88 91 @HasRepoPermissionAllDecorator('repository.admin')
89 92 def update(self, repo_name):
90 93 repo_model = RepoModel()
91 94 changed_name = repo_name
92 95
93 96 self.__load_defaults()
94 97
95 98 _form = RepoSettingsForm(edit=True,
96 99 old_data={'repo_name': repo_name},
97 repo_groups=c.repo_groups_choices)()
100 repo_groups=c.repo_groups_choices,
101 landing_revs=c.landing_revs_choices)()
98 102 try:
99 103 form_result = _form.to_python(dict(request.POST))
100 104
101 105 repo_model.update(repo_name, form_result)
102 106 invalidate_cache('get_repo_cached_%s' % repo_name)
103 107 h.flash(_('Repository %s updated successfully' % repo_name),
104 108 category='success')
105 109 changed_name = form_result['repo_name_full']
106 110 action_logger(self.rhodecode_user, 'user_updated_repo',
107 111 changed_name, self.ip_addr, self.sa)
108 112 Session.commit()
109 113 except formencode.Invalid, errors:
110 114 c.repo_info = repo_model.get_by_repo_name(repo_name)
111 115 c.users_array = repo_model.get_users_js()
112 116 errors.value.update({'user': c.repo_info.user.username})
113 117 return htmlfill.render(
114 118 render('settings/repo_settings.html'),
115 119 defaults=errors.value,
116 120 errors=errors.error_dict or {},
117 121 prefix_error=False,
118 122 encoding="UTF-8")
119 123 except Exception:
120 124 log.error(traceback.format_exc())
121 125 h.flash(_('error occurred during update of repository %s') \
122 126 % repo_name, category='error')
123 127
124 128 return redirect(url('repo_settings_home', repo_name=changed_name))
125 129
126 130 @HasRepoPermissionAllDecorator('repository.admin')
127 131 def delete(self, repo_name):
128 132 """DELETE /repos/repo_name: Delete an existing item"""
129 133 # Forms posted to this method should contain a hidden field:
130 134 # <input type="hidden" name="_method" value="DELETE" />
131 135 # Or using helpers:
132 136 # h.form(url('repo_settings_delete', repo_name=ID),
133 137 # method='delete')
134 138 # url('repo_settings_delete', repo_name=ID)
135 139
136 140 repo_model = RepoModel()
137 141 repo = repo_model.get_by_repo_name(repo_name)
138 142 if not repo:
139 143 h.flash(_('%s repository is not mapped to db perhaps'
140 144 ' it was moved or renamed from the filesystem'
141 145 ' please run the application again'
142 146 ' in order to rescan repositories') % repo_name,
143 147 category='error')
144 148
145 149 return redirect(url('home'))
146 150 try:
147 151 action_logger(self.rhodecode_user, 'user_deleted_repo',
148 152 repo_name, self.ip_addr, self.sa)
149 153 repo_model.delete(repo)
150 154 invalidate_cache('get_repo_cached_%s' % repo_name)
151 155 h.flash(_('deleted repository %s') % repo_name, category='success')
152 156 Session.commit()
153 157 except Exception:
154 158 log.error(traceback.format_exc())
155 159 h.flash(_('An error occurred during deletion of %s') % repo_name,
156 160 category='error')
157 161
158 162 return redirect(url('home'))
@@ -1,774 +1,774 b''
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 import os
23 23 import re
24 24 import logging
25 25 import traceback
26 26
27 27 import formencode
28 28 from formencode import All
29 29 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
30 30 Email, Bool, StringBoolean, Set
31 31
32 32 from pylons.i18n.translation import _
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34
35 35 from rhodecode.config.routing import ADMIN_PREFIX
36 36 from rhodecode.lib.utils import repo_name_slug
37 37 from rhodecode.lib.auth import authenticate, get_crypt_password
38 38 from rhodecode.lib.exceptions import LdapImportError
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode import BACKENDS
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 #this is needed to translate the messages using _() in validators
46 46 class State_obj(object):
47 47 _ = staticmethod(_)
48 48
49 49
50 50 #==============================================================================
51 51 # VALIDATORS
52 52 #==============================================================================
53 53 class ValidAuthToken(formencode.validators.FancyValidator):
54 54 messages = {'invalid_token': _('Token mismatch')}
55 55
56 56 def validate_python(self, value, state):
57 57
58 58 if value != authentication_token():
59 59 raise formencode.Invalid(
60 60 self.message('invalid_token',
61 61 state, search_number=value),
62 62 value,
63 63 state
64 64 )
65 65
66 66
67 67 def ValidUsername(edit, old_data):
68 68 class _ValidUsername(formencode.validators.FancyValidator):
69 69
70 70 def validate_python(self, value, state):
71 71 if value in ['default', 'new_user']:
72 72 raise formencode.Invalid(_('Invalid username'), value, state)
73 73 #check if user is unique
74 74 old_un = None
75 75 if edit:
76 76 old_un = User.get(old_data.get('user_id')).username
77 77
78 78 if old_un != value or not edit:
79 79 if User.get_by_username(value, case_insensitive=True):
80 80 raise formencode.Invalid(_('This username already '
81 81 'exists') , value, state)
82 82
83 83 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
84 84 raise formencode.Invalid(
85 85 _('Username may only contain alphanumeric characters '
86 86 'underscores, periods or dashes and must begin with '
87 87 'alphanumeric character'),
88 88 value,
89 89 state
90 90 )
91 91
92 92 return _ValidUsername
93 93
94 94
95 95 def ValidUsersGroup(edit, old_data):
96 96
97 97 class _ValidUsersGroup(formencode.validators.FancyValidator):
98 98
99 99 def validate_python(self, value, state):
100 100 if value in ['default']:
101 101 raise formencode.Invalid(_('Invalid group name'), value, state)
102 102 #check if group is unique
103 103 old_ugname = None
104 104 if edit:
105 105 old_ugname = UsersGroup.get(
106 106 old_data.get('users_group_id')).users_group_name
107 107
108 108 if old_ugname != value or not edit:
109 109 if UsersGroup.get_by_group_name(value, cache=False,
110 110 case_insensitive=True):
111 111 raise formencode.Invalid(_('This users group '
112 112 'already exists'), value,
113 113 state)
114 114
115 115 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
116 116 raise formencode.Invalid(
117 117 _('RepoGroup name may only contain alphanumeric characters '
118 118 'underscores, periods or dashes and must begin with '
119 119 'alphanumeric character'),
120 120 value,
121 121 state
122 122 )
123 123
124 124 return _ValidUsersGroup
125 125
126 126
127 127 def ValidReposGroup(edit, old_data):
128 128 class _ValidReposGroup(formencode.validators.FancyValidator):
129 129
130 130 def validate_python(self, value, state):
131 131 # TODO WRITE VALIDATIONS
132 132 group_name = value.get('group_name')
133 133 group_parent_id = value.get('group_parent_id')
134 134
135 135 # slugify repo group just in case :)
136 136 slug = repo_name_slug(group_name)
137 137
138 138 # check for parent of self
139 139 parent_of_self = lambda: (
140 140 old_data['group_id'] == int(group_parent_id)
141 141 if group_parent_id else False
142 142 )
143 143 if edit and parent_of_self():
144 144 e_dict = {
145 145 'group_parent_id': _('Cannot assign this group as parent')
146 146 }
147 147 raise formencode.Invalid('', value, state,
148 148 error_dict=e_dict)
149 149
150 150 old_gname = None
151 151 if edit:
152 152 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
153 153
154 154 if old_gname != group_name or not edit:
155 155
156 156 # check group
157 157 gr = RepoGroup.query()\
158 158 .filter(RepoGroup.group_name == slug)\
159 159 .filter(RepoGroup.group_parent_id == group_parent_id)\
160 160 .scalar()
161 161
162 162 if gr:
163 163 e_dict = {
164 164 'group_name': _('This group already exists')
165 165 }
166 166 raise formencode.Invalid('', value, state,
167 167 error_dict=e_dict)
168 168
169 169 # check for same repo
170 170 repo = Repository.query()\
171 171 .filter(Repository.repo_name == slug)\
172 172 .scalar()
173 173
174 174 if repo:
175 175 e_dict = {
176 176 'group_name': _('Repository with this name already exists')
177 177 }
178 178 raise formencode.Invalid('', value, state,
179 179 error_dict=e_dict)
180 180
181 181 return _ValidReposGroup
182 182
183 183
184 184 class ValidPassword(formencode.validators.FancyValidator):
185 185
186 186 def to_python(self, value, state):
187 187
188 188 if not value:
189 189 return
190 190
191 191 if value.get('password'):
192 192 try:
193 193 value['password'] = get_crypt_password(value['password'])
194 194 except UnicodeEncodeError:
195 195 e_dict = {'password': _('Invalid characters in password')}
196 196 raise formencode.Invalid('', value, state, error_dict=e_dict)
197 197
198 198 if value.get('password_confirmation'):
199 199 try:
200 200 value['password_confirmation'] = \
201 201 get_crypt_password(value['password_confirmation'])
202 202 except UnicodeEncodeError:
203 203 e_dict = {
204 204 'password_confirmation': _('Invalid characters in password')
205 205 }
206 206 raise formencode.Invalid('', value, state, error_dict=e_dict)
207 207
208 208 if value.get('new_password'):
209 209 try:
210 210 value['new_password'] = \
211 211 get_crypt_password(value['new_password'])
212 212 except UnicodeEncodeError:
213 213 e_dict = {'new_password': _('Invalid characters in password')}
214 214 raise formencode.Invalid('', value, state, error_dict=e_dict)
215 215
216 216 return value
217 217
218 218
219 219 class ValidPasswordsMatch(formencode.validators.FancyValidator):
220 220
221 221 def validate_python(self, value, state):
222 222
223 223 pass_val = value.get('password') or value.get('new_password')
224 224 if pass_val != value['password_confirmation']:
225 225 e_dict = {'password_confirmation':
226 226 _('Passwords do not match')}
227 227 raise formencode.Invalid('', value, state, error_dict=e_dict)
228 228
229 229
230 230 class ValidAuth(formencode.validators.FancyValidator):
231 231 messages = {
232 232 'invalid_password':_('invalid password'),
233 233 'invalid_login':_('invalid user name'),
234 234 'disabled_account':_('Your account is disabled')
235 235 }
236 236
237 237 # error mapping
238 238 e_dict = {'username': messages['invalid_login'],
239 239 'password': messages['invalid_password']}
240 240 e_dict_disable = {'username': messages['disabled_account']}
241 241
242 242 def validate_python(self, value, state):
243 243 password = value['password']
244 244 username = value['username']
245 245 user = User.get_by_username(username)
246 246
247 247 if authenticate(username, password):
248 248 return value
249 249 else:
250 250 if user and user.active is False:
251 251 log.warning('user %s is disabled' % username)
252 252 raise formencode.Invalid(
253 253 self.message('disabled_account',
254 254 state=State_obj),
255 255 value, state,
256 256 error_dict=self.e_dict_disable
257 257 )
258 258 else:
259 259 log.warning('user %s failed to authenticate' % username)
260 260 raise formencode.Invalid(
261 261 self.message('invalid_password',
262 262 state=State_obj), value, state,
263 263 error_dict=self.e_dict
264 264 )
265 265
266 266
267 267 class ValidRepoUser(formencode.validators.FancyValidator):
268 268
269 269 def to_python(self, value, state):
270 270 try:
271 271 User.query().filter(User.active == True)\
272 272 .filter(User.username == value).one()
273 273 except Exception:
274 274 raise formencode.Invalid(_('This username is not valid'),
275 275 value, state)
276 276 return value
277 277
278 278
279 279 def ValidRepoName(edit, old_data):
280 280 class _ValidRepoName(formencode.validators.FancyValidator):
281 281 def to_python(self, value, state):
282 282
283 283 repo_name = value.get('repo_name')
284 284
285 285 slug = repo_name_slug(repo_name)
286 286 if slug in [ADMIN_PREFIX, '']:
287 287 e_dict = {'repo_name': _('This repository name is disallowed')}
288 288 raise formencode.Invalid('', value, state, error_dict=e_dict)
289 289
290 290 if value.get('repo_group'):
291 291 gr = RepoGroup.get(value.get('repo_group'))
292 292 group_path = gr.full_path
293 293 # value needs to be aware of group name in order to check
294 294 # db key This is an actual just the name to store in the
295 295 # database
296 296 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
297 297
298 298 else:
299 299 group_path = ''
300 300 repo_name_full = repo_name
301 301
302 302 value['repo_name_full'] = repo_name_full
303 303 rename = old_data.get('repo_name') != repo_name_full
304 304 create = not edit
305 305 if rename or create:
306 306
307 307 if group_path != '':
308 308 if Repository.get_by_repo_name(repo_name_full):
309 309 e_dict = {
310 310 'repo_name': _('This repository already exists in '
311 311 'a group "%s"') % gr.group_name
312 312 }
313 313 raise formencode.Invalid('', value, state,
314 314 error_dict=e_dict)
315 315 elif RepoGroup.get_by_group_name(repo_name_full):
316 316 e_dict = {
317 317 'repo_name': _('There is a group with this name '
318 318 'already "%s"') % repo_name_full
319 319 }
320 320 raise formencode.Invalid('', value, state,
321 321 error_dict=e_dict)
322 322
323 323 elif Repository.get_by_repo_name(repo_name_full):
324 324 e_dict = {'repo_name': _('This repository '
325 325 'already exists')}
326 326 raise formencode.Invalid('', value, state,
327 327 error_dict=e_dict)
328 328
329 329 return value
330 330
331 331 return _ValidRepoName
332 332
333 333
334 334 def ValidForkName(*args, **kwargs):
335 335 return ValidRepoName(*args, **kwargs)
336 336
337 337
338 338 def SlugifyName():
339 339 class _SlugifyName(formencode.validators.FancyValidator):
340 340
341 341 def to_python(self, value, state):
342 342 return repo_name_slug(value)
343 343
344 344 return _SlugifyName
345 345
346 346
347 347 def ValidCloneUri():
348 348 from rhodecode.lib.utils import make_ui
349 349
350 350 def url_handler(repo_type, url, proto, ui=None):
351 351 if repo_type == 'hg':
352 352 from mercurial.httprepo import httprepository, httpsrepository
353 353 if proto == 'https':
354 354 httpsrepository(make_ui('db'), url).capabilities
355 355 elif proto == 'http':
356 356 httprepository(make_ui('db'), url).capabilities
357 357 elif repo_type == 'git':
358 358 #TODO: write a git url validator
359 359 pass
360 360
361 361 class _ValidCloneUri(formencode.validators.FancyValidator):
362 362
363 363 def to_python(self, value, state):
364 364
365 365 repo_type = value.get('repo_type')
366 366 url = value.get('clone_uri')
367 367 e_dict = {'clone_uri': _('invalid clone url')}
368 368
369 369 if not url:
370 370 pass
371 371 elif url.startswith('https'):
372 372 try:
373 373 url_handler(repo_type, url, 'https', make_ui('db'))
374 374 except Exception:
375 375 log.error(traceback.format_exc())
376 376 raise formencode.Invalid('', value, state, error_dict=e_dict)
377 377 elif url.startswith('http'):
378 378 try:
379 379 url_handler(repo_type, url, 'http', make_ui('db'))
380 380 except Exception:
381 381 log.error(traceback.format_exc())
382 382 raise formencode.Invalid('', value, state, error_dict=e_dict)
383 383 else:
384 384 e_dict = {'clone_uri': _('Invalid clone url, provide a '
385 385 'valid clone http\s url')}
386 386 raise formencode.Invalid('', value, state, error_dict=e_dict)
387 387
388 388 return value
389 389
390 390 return _ValidCloneUri
391 391
392 392
393 393 def ValidForkType(old_data):
394 394 class _ValidForkType(formencode.validators.FancyValidator):
395 395
396 396 def to_python(self, value, state):
397 397 if old_data['repo_type'] != value:
398 398 raise formencode.Invalid(_('Fork have to be the same '
399 399 'type as original'), value, state)
400 400
401 401 return value
402 402 return _ValidForkType
403 403
404 404
405 405 def ValidPerms(type_='repo'):
406 406 if type_ == 'group':
407 407 EMPTY_PERM = 'group.none'
408 408 elif type_ == 'repo':
409 409 EMPTY_PERM = 'repository.none'
410 410
411 411 class _ValidPerms(formencode.validators.FancyValidator):
412 412 messages = {
413 413 'perm_new_member_name':
414 414 _('This username or users group name is not valid')
415 415 }
416 416
417 417 def to_python(self, value, state):
418 418 perms_update = []
419 419 perms_new = []
420 420 # build a list of permission to update and new permission to create
421 421 for k, v in value.items():
422 422 # means new added member to permissions
423 423 if k.startswith('perm_new_member'):
424 424 new_perm = value.get('perm_new_member', False)
425 425 new_member = value.get('perm_new_member_name', False)
426 426 new_type = value.get('perm_new_member_type')
427 427
428 428 if new_member and new_perm:
429 429 if (new_member, new_perm, new_type) not in perms_new:
430 430 perms_new.append((new_member, new_perm, new_type))
431 431 elif k.startswith('u_perm_') or k.startswith('g_perm_'):
432 432 member = k[7:]
433 433 t = {'u': 'user',
434 434 'g': 'users_group'
435 435 }[k[0]]
436 436 if member == 'default':
437 437 if value.get('private'):
438 438 # set none for default when updating to private repo
439 439 v = EMPTY_PERM
440 440 perms_update.append((member, v, t))
441 441
442 442 value['perms_updates'] = perms_update
443 443 value['perms_new'] = perms_new
444 444
445 445 # update permissions
446 446 for k, v, t in perms_new:
447 447 try:
448 448 if t is 'user':
449 449 self.user_db = User.query()\
450 450 .filter(User.active == True)\
451 451 .filter(User.username == k).one()
452 452 if t is 'users_group':
453 453 self.user_db = UsersGroup.query()\
454 454 .filter(UsersGroup.users_group_active == True)\
455 455 .filter(UsersGroup.users_group_name == k).one()
456 456
457 457 except Exception:
458 458 msg = self.message('perm_new_member_name',
459 459 state=State_obj)
460 460 raise formencode.Invalid(
461 461 msg, value, state, error_dict={'perm_new_member_name': msg}
462 462 )
463 463 return value
464 464 return _ValidPerms
465 465
466 466
467 467 class ValidSettings(formencode.validators.FancyValidator):
468 468
469 469 def to_python(self, value, state):
470 470 # settings form can't edit user
471 471 if 'user' in value:
472 472 del['value']['user']
473 473 return value
474 474
475 475
476 476 class ValidPath(formencode.validators.FancyValidator):
477 477 def to_python(self, value, state):
478 478
479 479 if not os.path.isdir(value):
480 480 msg = _('This is not a valid path')
481 481 raise formencode.Invalid(msg, value, state,
482 482 error_dict={'paths_root_path': msg})
483 483 return value
484 484
485 485
486 486 def UniqSystemEmail(old_data):
487 487 class _UniqSystemEmail(formencode.validators.FancyValidator):
488 488 def to_python(self, value, state):
489 489 value = value.lower()
490 490 if (old_data.get('email') or '').lower() != value:
491 491 user = User.get_by_email(value, case_insensitive=True)
492 492 if user:
493 493 raise formencode.Invalid(
494 494 _("This e-mail address is already taken"), value, state
495 495 )
496 496 return value
497 497
498 498 return _UniqSystemEmail
499 499
500 500
501 501 class ValidSystemEmail(formencode.validators.FancyValidator):
502 502 def to_python(self, value, state):
503 503 value = value.lower()
504 504 user = User.get_by_email(value, case_insensitive=True)
505 505 if user is None:
506 506 raise formencode.Invalid(
507 507 _("This e-mail address doesn't exist."), value, state
508 508 )
509 509
510 510 return value
511 511
512 512
513 513 class LdapLibValidator(formencode.validators.FancyValidator):
514 514
515 515 def to_python(self, value, state):
516 516
517 517 try:
518 518 import ldap
519 519 except ImportError:
520 520 raise LdapImportError
521 521 return value
522 522
523 523
524 524 class AttrLoginValidator(formencode.validators.FancyValidator):
525 525
526 526 def to_python(self, value, state):
527 527
528 528 if not value or not isinstance(value, (str, unicode)):
529 529 raise formencode.Invalid(
530 530 _("The LDAP Login attribute of the CN must be specified - "
531 531 "this is the name of the attribute that is equivalent "
532 532 "to 'username'"), value, state
533 533 )
534 534
535 535 return value
536 536
537 537
538 538 #==============================================================================
539 539 # FORMS
540 540 #==============================================================================
541 541 class LoginForm(formencode.Schema):
542 542 allow_extra_fields = True
543 543 filter_extra_fields = True
544 544 username = UnicodeString(
545 545 strip=True,
546 546 min=1,
547 547 not_empty=True,
548 548 messages={
549 549 'empty': _('Please enter a login'),
550 550 'tooShort': _('Enter a value %(min)i characters long or more')}
551 551 )
552 552
553 553 password = UnicodeString(
554 554 strip=False,
555 555 min=3,
556 556 not_empty=True,
557 557 messages={
558 558 'empty': _('Please enter a password'),
559 559 'tooShort': _('Enter %(min)i characters or more')}
560 560 )
561 561
562 562 remember = StringBoolean(if_missing=False)
563 563
564 564 chained_validators = [ValidAuth]
565 565
566 566
567 567 def UserForm(edit=False, old_data={}):
568 568 class _UserForm(formencode.Schema):
569 569 allow_extra_fields = True
570 570 filter_extra_fields = True
571 571 username = All(UnicodeString(strip=True, min=1, not_empty=True),
572 572 ValidUsername(edit, old_data))
573 573 if edit:
574 574 new_password = All(UnicodeString(strip=False, min=6, not_empty=False))
575 575 password_confirmation = All(UnicodeString(strip=False, min=6,
576 576 not_empty=False))
577 577 admin = StringBoolean(if_missing=False)
578 578 else:
579 579 password = All(UnicodeString(strip=False, min=6, not_empty=True))
580 580 password_confirmation = All(UnicodeString(strip=False, min=6,
581 581 not_empty=False))
582 582
583 583 active = StringBoolean(if_missing=False)
584 584 name = UnicodeString(strip=True, min=1, not_empty=False)
585 585 lastname = UnicodeString(strip=True, min=1, not_empty=False)
586 586 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
587 587
588 588 chained_validators = [ValidPasswordsMatch, ValidPassword]
589 589
590 590 return _UserForm
591 591
592 592
593 593 def UsersGroupForm(edit=False, old_data={}, available_members=[]):
594 594 class _UsersGroupForm(formencode.Schema):
595 595 allow_extra_fields = True
596 596 filter_extra_fields = True
597 597
598 598 users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
599 599 ValidUsersGroup(edit, old_data))
600 600
601 601 users_group_active = StringBoolean(if_missing=False)
602 602
603 603 if edit:
604 604 users_group_members = OneOf(available_members, hideList=False,
605 605 testValueList=True,
606 606 if_missing=None, not_empty=False)
607 607
608 608 return _UsersGroupForm
609 609
610 610
611 611 def ReposGroupForm(edit=False, old_data={}, available_groups=[]):
612 612 class _ReposGroupForm(formencode.Schema):
613 613 allow_extra_fields = True
614 614 filter_extra_fields = False
615 615
616 616 group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
617 617 SlugifyName())
618 618 group_description = UnicodeString(strip=True, min=1,
619 619 not_empty=True)
620 620 group_parent_id = OneOf(available_groups, hideList=False,
621 621 testValueList=True,
622 622 if_missing=None, not_empty=False)
623 623
624 624 chained_validators = [ValidReposGroup(edit, old_data), ValidPerms('group')]
625 625
626 626 return _ReposGroupForm
627 627
628 628
629 629 def RegisterForm(edit=False, old_data={}):
630 630 class _RegisterForm(formencode.Schema):
631 631 allow_extra_fields = True
632 632 filter_extra_fields = True
633 633 username = All(ValidUsername(edit, old_data),
634 634 UnicodeString(strip=True, min=1, not_empty=True))
635 635 password = All(UnicodeString(strip=False, min=6, not_empty=True))
636 636 password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=True))
637 637 active = StringBoolean(if_missing=False)
638 638 name = UnicodeString(strip=True, min=1, not_empty=False)
639 639 lastname = UnicodeString(strip=True, min=1, not_empty=False)
640 640 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
641 641
642 642 chained_validators = [ValidPasswordsMatch, ValidPassword]
643 643
644 644 return _RegisterForm
645 645
646 646
647 647 def PasswordResetForm():
648 648 class _PasswordResetForm(formencode.Schema):
649 649 allow_extra_fields = True
650 650 filter_extra_fields = True
651 651 email = All(ValidSystemEmail(), Email(not_empty=True))
652 652 return _PasswordResetForm
653 653
654 654
655 655 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
656 repo_groups=[]):
656 repo_groups=[], landing_revs=[]):
657 657 class _RepoForm(formencode.Schema):
658 658 allow_extra_fields = True
659 659 filter_extra_fields = False
660 660 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
661 661 SlugifyName())
662 662 clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False))
663 663 repo_group = OneOf(repo_groups, hideList=True)
664 664 repo_type = OneOf(supported_backends)
665 665 description = UnicodeString(strip=True, min=1, not_empty=False)
666 666 private = StringBoolean(if_missing=False)
667 667 enable_statistics = StringBoolean(if_missing=False)
668 668 enable_downloads = StringBoolean(if_missing=False)
669 landing_rev = UnicodeString(strip=True, min=1, not_empty=True)
669 landing_rev = OneOf(landing_revs, hideList=True)
670 670
671 671 if edit:
672 672 #this is repo owner
673 673 user = All(UnicodeString(not_empty=True), ValidRepoUser)
674 674
675 675 chained_validators = [ValidCloneUri()(),
676 676 ValidRepoName(edit, old_data),
677 677 ValidPerms()]
678 678 return _RepoForm
679 679
680 680
681 def RepoForkForm(edit=False, old_data={},
682 supported_backends=BACKENDS.keys(), repo_groups=[]):
681 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
682 repo_groups=[]):
683 683 class _RepoForkForm(formencode.Schema):
684 684 allow_extra_fields = True
685 685 filter_extra_fields = False
686 686 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
687 687 SlugifyName())
688 688 repo_group = OneOf(repo_groups, hideList=True)
689 689 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
690 690 description = UnicodeString(strip=True, min=1, not_empty=True)
691 691 private = StringBoolean(if_missing=False)
692 692 copy_permissions = StringBoolean(if_missing=False)
693 693 update_after_clone = StringBoolean(if_missing=False)
694 694 fork_parent_id = UnicodeString()
695 695 chained_validators = [ValidForkName(edit, old_data)]
696 696
697 697 return _RepoForkForm
698 698
699 699
700 def RepoSettingsForm(edit=False, old_data={},
701 supported_backends=BACKENDS.keys(), repo_groups=[]):
700 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
701 repo_groups=[], landing_revs=[]):
702 702 class _RepoForm(formencode.Schema):
703 703 allow_extra_fields = True
704 704 filter_extra_fields = False
705 705 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
706 706 SlugifyName())
707 707 description = UnicodeString(strip=True, min=1, not_empty=True)
708 708 repo_group = OneOf(repo_groups, hideList=True)
709 709 private = StringBoolean(if_missing=False)
710 landing_rev = UnicodeString(strip=True, min=1, not_empty=True)
710 landing_rev = OneOf(landing_revs, hideList=True)
711 711 chained_validators = [ValidRepoName(edit, old_data), ValidPerms(),
712 712 ValidSettings]
713 713 return _RepoForm
714 714
715 715
716 716 def ApplicationSettingsForm():
717 717 class _ApplicationSettingsForm(formencode.Schema):
718 718 allow_extra_fields = True
719 719 filter_extra_fields = False
720 720 rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
721 721 rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
722 722 rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
723 723
724 724 return _ApplicationSettingsForm
725 725
726 726
727 727 def ApplicationUiSettingsForm():
728 728 class _ApplicationUiSettingsForm(formencode.Schema):
729 729 allow_extra_fields = True
730 730 filter_extra_fields = False
731 731 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
732 732 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
733 733 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
734 734 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
735 735 hooks_changegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
736 736 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
737 737
738 738 return _ApplicationUiSettingsForm
739 739
740 740
741 741 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
742 742 class _DefaultPermissionsForm(formencode.Schema):
743 743 allow_extra_fields = True
744 744 filter_extra_fields = True
745 745 overwrite_default = StringBoolean(if_missing=False)
746 746 anonymous = OneOf(['True', 'False'], if_missing=False)
747 747 default_perm = OneOf(perms_choices)
748 748 default_register = OneOf(register_choices)
749 749 default_create = OneOf(create_choices)
750 750
751 751 return _DefaultPermissionsForm
752 752
753 753
754 754 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
755 755 class _LdapSettingsForm(formencode.Schema):
756 756 allow_extra_fields = True
757 757 filter_extra_fields = True
758 758 #pre_validators = [LdapLibValidator]
759 759 ldap_active = StringBoolean(if_missing=False)
760 760 ldap_host = UnicodeString(strip=True,)
761 761 ldap_port = Number(strip=True,)
762 762 ldap_tls_kind = OneOf(tls_kind_choices)
763 763 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
764 764 ldap_dn_user = UnicodeString(strip=True,)
765 765 ldap_dn_pass = UnicodeString(strip=True,)
766 766 ldap_base_dn = UnicodeString(strip=True,)
767 767 ldap_filter = UnicodeString(strip=True,)
768 768 ldap_search_scope = OneOf(search_scope_choices)
769 769 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
770 770 ldap_attr_firstname = UnicodeString(strip=True,)
771 771 ldap_attr_lastname = UnicodeString(strip=True,)
772 772 ldap_attr_email = UnicodeString(strip=True,)
773 773
774 774 return _LdapSettingsForm
@@ -1,505 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm 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 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29 import cStringIO
30 30
31 31 from sqlalchemy import func
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib.vcs import get_backend
35 35 from rhodecode.lib.vcs.exceptions import RepositoryError
36 36 from rhodecode.lib.vcs.utils.lazy import LazyProperty
37 37 from rhodecode.lib.vcs.nodes import FileNode
38 38
39 39 from rhodecode import BACKENDS
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.lib.utils2 import safe_str, safe_unicode
42 42 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
43 43 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
44 44 action_logger, EmptyChangeset, REMOVED_REPO_PAT
45 45 from rhodecode.model import BaseModel
46 46 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
47 47 UserFollowing, UserLog, User, RepoGroup
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class UserTemp(object):
53 53 def __init__(self, user_id):
54 54 self.user_id = user_id
55 55
56 56 def __repr__(self):
57 57 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
58 58
59 59
60 60 class RepoTemp(object):
61 61 def __init__(self, repo_id):
62 62 self.repo_id = repo_id
63 63
64 64 def __repr__(self):
65 65 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
66 66
67 67
68 68 class CachedRepoList(object):
69 69
70 70 def __init__(self, db_repo_list, repos_path, order_by=None):
71 71 self.db_repo_list = db_repo_list
72 72 self.repos_path = repos_path
73 73 self.order_by = order_by
74 74 self.reversed = (order_by or '').startswith('-')
75 75
76 76 def __len__(self):
77 77 return len(self.db_repo_list)
78 78
79 79 def __repr__(self):
80 80 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
81 81
82 82 def __iter__(self):
83 83 # pre-propagated cache_map to save executing select statements
84 84 # for each repo
85 85 cache_map = CacheInvalidation.get_cache_map()
86 86
87 87 for dbr in self.db_repo_list:
88 88 scmr = dbr.scm_instance_cached(cache_map)
89 89 # check permission at this level
90 90 if not HasRepoPermissionAny(
91 91 'repository.read', 'repository.write', 'repository.admin'
92 92 )(dbr.repo_name, 'get repo check'):
93 93 continue
94 94
95 95 if scmr is None:
96 96 log.error(
97 97 '%s this repository is present in database but it '
98 98 'cannot be created as an scm instance' % dbr.repo_name
99 99 )
100 100 continue
101 101
102 102 last_change = scmr.last_change
103 103 tip = h.get_changeset_safe(scmr, 'tip')
104 104
105 105 tmp_d = {}
106 106 tmp_d['name'] = dbr.repo_name
107 107 tmp_d['name_sort'] = tmp_d['name'].lower()
108 108 tmp_d['description'] = dbr.description
109 109 tmp_d['description_sort'] = tmp_d['description']
110 110 tmp_d['last_change'] = last_change
111 111 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
112 112 tmp_d['tip'] = tip.raw_id
113 113 tmp_d['tip_sort'] = tip.revision
114 114 tmp_d['rev'] = tip.revision
115 115 tmp_d['contact'] = dbr.user.full_contact
116 116 tmp_d['contact_sort'] = tmp_d['contact']
117 117 tmp_d['owner_sort'] = tmp_d['contact']
118 118 tmp_d['repo_archives'] = list(scmr._get_archives())
119 119 tmp_d['last_msg'] = tip.message
120 120 tmp_d['author'] = tip.author
121 121 tmp_d['dbrepo'] = dbr.get_dict()
122 122 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
123 123 yield tmp_d
124 124
125 125
126 126 class GroupList(object):
127 127
128 128 def __init__(self, db_repo_group_list):
129 129 self.db_repo_group_list = db_repo_group_list
130 130
131 131 def __len__(self):
132 132 return len(self.db_repo_group_list)
133 133
134 134 def __repr__(self):
135 135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 for dbgr in self.db_repo_group_list:
139 139 # check permission at this level
140 140 if not HasReposGroupPermissionAny(
141 141 'group.read', 'group.write', 'group.admin'
142 142 )(dbgr.group_name, 'get group repo check'):
143 143 continue
144 144
145 145 yield dbgr
146 146
147 147
148 148 class ScmModel(BaseModel):
149 149 """
150 150 Generic Scm Model
151 151 """
152 152
153 153 def __get_repo(self, instance):
154 154 cls = Repository
155 155 if isinstance(instance, cls):
156 156 return instance
157 157 elif isinstance(instance, int) or str(instance).isdigit():
158 158 return cls.get(instance)
159 159 elif isinstance(instance, basestring):
160 160 return cls.get_by_repo_name(instance)
161 161 elif instance:
162 162 raise Exception('given object must be int, basestr or Instance'
163 163 ' of %s got %s' % (type(cls), type(instance)))
164 164
165 165 @LazyProperty
166 166 def repos_path(self):
167 167 """
168 168 Get's the repositories root path from database
169 169 """
170 170
171 171 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
172 172
173 173 return q.ui_value
174 174
175 175 def repo_scan(self, repos_path=None):
176 176 """
177 177 Listing of repositories in given path. This path should not be a
178 178 repository itself. Return a dictionary of repository objects
179 179
180 180 :param repos_path: path to directory containing repositories
181 181 """
182 182
183 183 if repos_path is None:
184 184 repos_path = self.repos_path
185 185
186 186 log.info('scanning for repositories in %s' % repos_path)
187 187
188 188 baseui = make_ui('db')
189 189 repos = {}
190 190
191 191 for name, path in get_filesystem_repos(repos_path, recursive=True):
192 192 # skip removed repos
193 193 if REMOVED_REPO_PAT.match(name):
194 194 continue
195 195
196 196 # name need to be decomposed and put back together using the /
197 197 # since this is internal storage separator for rhodecode
198 198 name = Repository.url_sep().join(name.split(os.sep))
199 199
200 200 try:
201 201 if name in repos:
202 202 raise RepositoryError('Duplicate repository name %s '
203 203 'found in %s' % (name, path))
204 204 else:
205 205
206 206 klass = get_backend(path[0])
207 207
208 208 if path[0] == 'hg' and path[0] in BACKENDS.keys():
209 209 repos[name] = klass(safe_str(path[1]), baseui=baseui)
210 210
211 211 if path[0] == 'git' and path[0] in BACKENDS.keys():
212 212 repos[name] = klass(path[1])
213 213 except OSError:
214 214 continue
215 215
216 216 return repos
217 217
218 218 def get_repos(self, all_repos=None, sort_key=None):
219 219 """
220 220 Get all repos from db and for each repo create it's
221 221 backend instance and fill that backed with information from database
222 222
223 223 :param all_repos: list of repository names as strings
224 224 give specific repositories list, good for filtering
225 225 """
226 226 if all_repos is None:
227 227 all_repos = self.sa.query(Repository)\
228 228 .filter(Repository.group_id == None)\
229 229 .order_by(func.lower(Repository.repo_name)).all()
230 230
231 231 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
232 232 order_by=sort_key)
233 233
234 234 return repo_iter
235 235
236 236 def get_repos_groups(self, all_groups=None):
237 237 if all_groups is None:
238 238 all_groups = RepoGroup.query()\
239 239 .filter(RepoGroup.group_parent_id == None).all()
240 240 group_iter = GroupList(all_groups)
241 241
242 242 return group_iter
243 243
244 244 def mark_for_invalidation(self, repo_name):
245 245 """
246 246 Puts cache invalidation task into db for
247 247 further global cache invalidation
248 248
249 249 :param repo_name: this repo that should invalidation take place
250 250 """
251 251 CacheInvalidation.set_invalidate(repo_name)
252 252
253 253 def toggle_following_repo(self, follow_repo_id, user_id):
254 254
255 255 f = self.sa.query(UserFollowing)\
256 256 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
257 257 .filter(UserFollowing.user_id == user_id).scalar()
258 258
259 259 if f is not None:
260 260 try:
261 261 self.sa.delete(f)
262 262 action_logger(UserTemp(user_id),
263 263 'stopped_following_repo',
264 264 RepoTemp(follow_repo_id))
265 265 return
266 266 except:
267 267 log.error(traceback.format_exc())
268 268 raise
269 269
270 270 try:
271 271 f = UserFollowing()
272 272 f.user_id = user_id
273 273 f.follows_repo_id = follow_repo_id
274 274 self.sa.add(f)
275 275
276 276 action_logger(UserTemp(user_id),
277 277 'started_following_repo',
278 278 RepoTemp(follow_repo_id))
279 279 except:
280 280 log.error(traceback.format_exc())
281 281 raise
282 282
283 283 def toggle_following_user(self, follow_user_id, user_id):
284 284 f = self.sa.query(UserFollowing)\
285 285 .filter(UserFollowing.follows_user_id == follow_user_id)\
286 286 .filter(UserFollowing.user_id == user_id).scalar()
287 287
288 288 if f is not None:
289 289 try:
290 290 self.sa.delete(f)
291 291 return
292 292 except:
293 293 log.error(traceback.format_exc())
294 294 raise
295 295
296 296 try:
297 297 f = UserFollowing()
298 298 f.user_id = user_id
299 299 f.follows_user_id = follow_user_id
300 300 self.sa.add(f)
301 301 except:
302 302 log.error(traceback.format_exc())
303 303 raise
304 304
305 305 def is_following_repo(self, repo_name, user_id, cache=False):
306 306 r = self.sa.query(Repository)\
307 307 .filter(Repository.repo_name == repo_name).scalar()
308 308
309 309 f = self.sa.query(UserFollowing)\
310 310 .filter(UserFollowing.follows_repository == r)\
311 311 .filter(UserFollowing.user_id == user_id).scalar()
312 312
313 313 return f is not None
314 314
315 315 def is_following_user(self, username, user_id, cache=False):
316 316 u = User.get_by_username(username)
317 317
318 318 f = self.sa.query(UserFollowing)\
319 319 .filter(UserFollowing.follows_user == u)\
320 320 .filter(UserFollowing.user_id == user_id).scalar()
321 321
322 322 return f is not None
323 323
324 324 def get_followers(self, repo_id):
325 325 if not isinstance(repo_id, int):
326 326 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
327 327
328 328 return self.sa.query(UserFollowing)\
329 329 .filter(UserFollowing.follows_repo_id == repo_id).count()
330 330
331 331 def get_forks(self, repo_id):
332 332 if not isinstance(repo_id, int):
333 333 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
334 334
335 335 return self.sa.query(Repository)\
336 336 .filter(Repository.fork_id == repo_id).count()
337 337
338 338 def mark_as_fork(self, repo, fork, user):
339 339 repo = self.__get_repo(repo)
340 340 fork = self.__get_repo(fork)
341 341 repo.fork = fork
342 342 self.sa.add(repo)
343 343 return repo
344 344
345 345 def pull_changes(self, repo_name, username):
346 346 dbrepo = Repository.get_by_repo_name(repo_name)
347 347 clone_uri = dbrepo.clone_uri
348 348 if not clone_uri:
349 349 raise Exception("This repository doesn't have a clone uri")
350 350
351 351 repo = dbrepo.scm_instance
352 352 try:
353 353 extras = {
354 354 'ip': '',
355 355 'username': username,
356 356 'action': 'push_remote',
357 357 'repository': repo_name,
358 358 'scm': repo.alias,
359 359 }
360 360
361 361 # inject ui extra param to log this action via push logger
362 362 for k, v in extras.items():
363 363 repo._repo.ui.setconfig('rhodecode_extras', k, v)
364 364 if repo.alias == 'git':
365 365 repo.fetch(clone_uri)
366 366 else:
367 367 repo.pull(clone_uri)
368 368 self.mark_for_invalidation(repo_name)
369 369 except:
370 370 log.error(traceback.format_exc())
371 371 raise
372 372
373 373 def commit_change(self, repo, repo_name, cs, user, author, message,
374 374 content, f_path):
375 375
376 376 if repo.alias == 'hg':
377 377 from rhodecode.lib.vcs.backends.hg import \
378 378 MercurialInMemoryChangeset as IMC
379 379 elif repo.alias == 'git':
380 380 from rhodecode.lib.vcs.backends.git import \
381 381 GitInMemoryChangeset as IMC
382 382
383 383 # decoding here will force that we have proper encoded values
384 384 # in any other case this will throw exceptions and deny commit
385 385 content = safe_str(content)
386 386 path = safe_str(f_path)
387 387 # message and author needs to be unicode
388 388 # proper backend should then translate that into required type
389 389 message = safe_unicode(message)
390 390 author = safe_unicode(author)
391 391 m = IMC(repo)
392 392 m.change(FileNode(path, content))
393 393 tip = m.commit(message=message,
394 394 author=author,
395 395 parents=[cs], branch=cs.branch)
396 396
397 397 new_cs = tip.short_id
398 398 action = 'push_local:%s' % new_cs
399 399
400 400 action_logger(user, action, repo_name)
401 401
402 402 self.mark_for_invalidation(repo_name)
403 403
404 404 def create_node(self, repo, repo_name, cs, user, author, message, content,
405 405 f_path):
406 406 if repo.alias == 'hg':
407 407 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
408 408 elif repo.alias == 'git':
409 409 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
410 410 # decoding here will force that we have proper encoded values
411 411 # in any other case this will throw exceptions and deny commit
412 412
413 413 if isinstance(content, (basestring,)):
414 414 content = safe_str(content)
415 415 elif isinstance(content, (file, cStringIO.OutputType,)):
416 416 content = content.read()
417 417 else:
418 418 raise Exception('Content is of unrecognized type %s' % (
419 419 type(content)
420 420 ))
421 421
422 422 message = safe_unicode(message)
423 423 author = safe_unicode(author)
424 424 path = safe_str(f_path)
425 425 m = IMC(repo)
426 426
427 427 if isinstance(cs, EmptyChangeset):
428 428 # EmptyChangeset means we we're editing empty repository
429 429 parents = None
430 430 else:
431 431 parents = [cs]
432 432
433 433 m.add(FileNode(path, content=content))
434 434 tip = m.commit(message=message,
435 435 author=author,
436 436 parents=parents, branch=cs.branch)
437 437 new_cs = tip.short_id
438 438 action = 'push_local:%s' % new_cs
439 439
440 440 action_logger(user, action, repo_name)
441 441
442 442 self.mark_for_invalidation(repo_name)
443 443
444 444 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
445 445 """
446 446 recursive walk in root dir and return a set of all path in that dir
447 447 based on repository walk function
448 448
449 449 :param repo_name: name of repository
450 450 :param revision: revision for which to list nodes
451 451 :param root_path: root path to list
452 452 :param flat: return as a list, if False returns a dict with decription
453 453
454 454 """
455 455 _files = list()
456 456 _dirs = list()
457 457 try:
458 458 _repo = self.__get_repo(repo_name)
459 459 changeset = _repo.scm_instance.get_changeset(revision)
460 460 root_path = root_path.lstrip('/')
461 461 for topnode, dirs, files in changeset.walk(root_path):
462 462 for f in files:
463 463 _files.append(f.path if flat else {"name": f.path,
464 464 "type": "file"})
465 465 for d in dirs:
466 466 _dirs.append(d.path if flat else {"name": d.path,
467 467 "type": "dir"})
468 468 except RepositoryError:
469 469 log.debug(traceback.format_exc())
470 470 raise
471 471
472 472 return _dirs, _files
473 473
474 474 def get_unread_journal(self):
475 475 return self.sa.query(UserLog).count()
476 476
477 477 def get_repo_landing_revs(self, repo=None):
478 478 """
479 479 Generates select option with tags branches and bookmarks (for hg only)
480 480 grouped by type
481 481
482 482 :param repo:
483 483 :type repo:
484 484 """
485
485 486 hist_l = []
487 choices = []
486 488 repo = self.__get_repo(repo)
487 489 hist_l.append(['tip', _('latest tip')])
490 choices.append('tip')
488 491 if not repo:
489 return hist_l
492 return choices, hist_l
490 493
491 494 repo = repo.scm_instance
495
492 496 branches_group = ([(k, k) for k, v in
493 497 repo.branches.iteritems()], _("Branches"))
494 498 hist_l.append(branches_group)
499 choices.extend([x[0] for x in branches_group[0]])
495 500
496 501 if repo.alias == 'hg':
497 502 bookmarks_group = ([(k, k) for k, v in
498 503 repo.bookmarks.iteritems()], _("Bookmarks"))
499 504 hist_l.append(bookmarks_group)
505 choices.extend([x[0] for x in bookmarks_group[0]])
500 506
501 507 tags_group = ([(k, k) for k, v in
502 508 repo.tags.iteritems()], _("Tags"))
503 509 hist_l.append(tags_group)
510 choices.extend([x[0] for x in tags_group[0]])
504 511
505 return hist_l
512 return choices, hist_l
General Comments 0
You need to be logged in to leave comments. Login now