##// END OF EJS Templates
Use lightweight revisions in journal by default
marcink -
r2958:c0a6a2e6 beta
parent child Browse files
Show More
@@ -1,513 +1,508
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 settings controller for rhodecode admin
7 7
8 8 :created_on: Jul 14, 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 import pkg_resources
30 30 import platform
31 31
32 32 from sqlalchemy import func
33 33 from formencode import htmlfill
34 34 from pylons import request, session, tmpl_context as c, url, config
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
40 40 HasPermissionAnyDecorator, NotAnonymous
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.lib.celerylib import tasks, run_task
43 43 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
44 44 set_rhodecode_config, repo_name_slug, check_git_version
45 45 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
46 46 RhodeCodeSetting, PullRequest, PullRequestReviewers
47 47 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm
49 49 from rhodecode.model.scm import ScmModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.db import User
52 52 from rhodecode.model.notification import EmailNotificationModel
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.lib.utils2 import str2bool
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class SettingsController(BaseController):
60 60 """REST Controller styled on the Atom Publishing Protocol"""
61 61 # To properly map this controller, ensure your config/routing.py
62 62 # file has a resource setup:
63 63 # map.resource('setting', 'settings', controller='admin/settings',
64 64 # path_prefix='/admin', name_prefix='admin_')
65 65
66 66 @LoginRequired()
67 67 def __before__(self):
68 68 c.admin_user = session.get('admin_user')
69 69 c.admin_username = session.get('admin_username')
70 70 c.modules = sorted([(p.project_name, p.version)
71 71 for p in pkg_resources.working_set]
72 72 + [('git', check_git_version())],
73 73 key=lambda k: k[0].lower())
74 74 c.py_version = platform.python_version()
75 75 c.platform = platform.platform()
76 76 super(SettingsController, self).__before__()
77 77
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 def index(self, format='html'):
80 80 """GET /admin/settings: All items in the collection"""
81 81 # url('admin_settings')
82 82
83 83 defaults = RhodeCodeSetting.get_app_settings()
84 84 defaults.update(self._get_hg_ui_settings())
85 85
86 86 return htmlfill.render(
87 87 render('admin/settings/settings.html'),
88 88 defaults=defaults,
89 89 encoding="UTF-8",
90 90 force_defaults=False
91 91 )
92 92
93 93 @HasPermissionAllDecorator('hg.admin')
94 94 def create(self):
95 95 """POST /admin/settings: Create a new item"""
96 96 # url('admin_settings')
97 97
98 98 @HasPermissionAllDecorator('hg.admin')
99 99 def new(self, format='html'):
100 100 """GET /admin/settings/new: Form to create a new item"""
101 101 # url('admin_new_setting')
102 102
103 103 @HasPermissionAllDecorator('hg.admin')
104 104 def update(self, setting_id):
105 105 """PUT /admin/settings/setting_id: Update an existing item"""
106 106 # Forms posted to this method should contain a hidden field:
107 107 # <input type="hidden" name="_method" value="PUT" />
108 108 # Or using helpers:
109 109 # h.form(url('admin_setting', setting_id=ID),
110 110 # method='put')
111 111 # url('admin_setting', setting_id=ID)
112 112
113 113 if setting_id == 'mapping':
114 114 rm_obsolete = request.POST.get('destroy', False)
115 115 log.debug('Rescanning directories with destroy=%s' % rm_obsolete)
116 116 initial = ScmModel().repo_scan()
117 117 log.debug('invalidating all repositories')
118 118 for repo_name in initial.keys():
119 119 invalidate_cache('get_repo_cached_%s' % repo_name)
120 120
121 121 added, removed = repo2db_mapper(initial, rm_obsolete)
122 122
123 123 h.flash(_('Repositories successfully'
124 124 ' rescanned added: %s,removed: %s') % (added, removed),
125 125 category='success')
126 126
127 127 if setting_id == 'whoosh':
128 128 repo_location = self._get_hg_ui_settings()['paths_root_path']
129 129 full_index = request.POST.get('full_index', False)
130 130 run_task(tasks.whoosh_index, repo_location, full_index)
131 131 h.flash(_('Whoosh reindex task scheduled'), category='success')
132 132
133 133 if setting_id == 'global':
134 134
135 135 application_form = ApplicationSettingsForm()()
136 136 try:
137 137 form_result = application_form.to_python(dict(request.POST))
138 138 except formencode.Invalid, errors:
139 139 return htmlfill.render(
140 140 render('admin/settings/settings.html'),
141 141 defaults=errors.value,
142 142 errors=errors.error_dict or {},
143 143 prefix_error=False,
144 144 encoding="UTF-8"
145 145 )
146 146
147 147 try:
148 148 sett1 = RhodeCodeSetting.get_by_name_or_create('title')
149 149 sett1.app_settings_value = form_result['rhodecode_title']
150 150 Session().add(sett1)
151 151
152 152 sett2 = RhodeCodeSetting.get_by_name_or_create('realm')
153 153 sett2.app_settings_value = form_result['rhodecode_realm']
154 154 Session().add(sett2)
155 155
156 156 sett3 = RhodeCodeSetting.get_by_name_or_create('ga_code')
157 157 sett3.app_settings_value = form_result['rhodecode_ga_code']
158 158 Session().add(sett3)
159 159
160 160 Session().commit()
161 161 set_rhodecode_config(config)
162 162 h.flash(_('Updated application settings'), category='success')
163 163
164 164 except Exception:
165 165 log.error(traceback.format_exc())
166 166 h.flash(_('error occurred during updating '
167 167 'application settings'),
168 168 category='error')
169 169
170 170 if setting_id == 'visual':
171 171
172 172 application_form = ApplicationVisualisationForm()()
173 173 try:
174 174 form_result = application_form.to_python(dict(request.POST))
175 175 except formencode.Invalid, errors:
176 176 return htmlfill.render(
177 177 render('admin/settings/settings.html'),
178 178 defaults=errors.value,
179 179 errors=errors.error_dict or {},
180 180 prefix_error=False,
181 181 encoding="UTF-8"
182 182 )
183 183
184 184 try:
185 185 sett1 = RhodeCodeSetting.get_by_name_or_create('show_public_icon')
186 186 sett1.app_settings_value = \
187 187 form_result['rhodecode_show_public_icon']
188 188 Session().add(sett1)
189 189
190 190 sett2 = RhodeCodeSetting.get_by_name_or_create('show_private_icon')
191 191 sett2.app_settings_value = \
192 192 form_result['rhodecode_show_private_icon']
193 193 Session().add(sett2)
194 194
195 195 sett3 = RhodeCodeSetting.get_by_name_or_create('stylify_metatags')
196 196 sett3.app_settings_value = \
197 197 form_result['rhodecode_stylify_metatags']
198 198 Session().add(sett3)
199 199
200 200 sett4 = RhodeCodeSetting.get_by_name_or_create('lightweight_dashboard')
201 201 sett4.app_settings_value = \
202 202 form_result['rhodecode_lightweight_dashboard']
203 203 Session().add(sett4)
204 204
205 sett5 = RhodeCodeSetting.get_by_name_or_create('lightweight_journal')
206 sett5.app_settings_value = \
207 form_result['rhodecode_lightweight_journal']
208 Session().add(sett5)
209
210 205 Session().commit()
211 206 set_rhodecode_config(config)
212 207 h.flash(_('Updated visualisation settings'),
213 208 category='success')
214 209
215 210 except Exception:
216 211 log.error(traceback.format_exc())
217 212 h.flash(_('error occurred during updating '
218 213 'visualisation settings'),
219 214 category='error')
220 215
221 216 if setting_id == 'vcs':
222 217 application_form = ApplicationUiSettingsForm()()
223 218 try:
224 219 form_result = application_form.to_python(dict(request.POST))
225 220 except formencode.Invalid, errors:
226 221 return htmlfill.render(
227 222 render('admin/settings/settings.html'),
228 223 defaults=errors.value,
229 224 errors=errors.error_dict or {},
230 225 prefix_error=False,
231 226 encoding="UTF-8"
232 227 )
233 228
234 229 try:
235 230 # fix namespaces for hooks and extensions
236 231 _f = lambda s: s.replace('.', '_')
237 232
238 233 sett = RhodeCodeUi.get_by_key('push_ssl')
239 234 sett.ui_value = form_result['web_push_ssl']
240 235 Session().add(sett)
241 236
242 237 sett = RhodeCodeUi.get_by_key('/')
243 238 sett.ui_value = form_result['paths_root_path']
244 239 Session().add(sett)
245 240
246 241 #HOOKS
247 242 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_UPDATE)
248 243 sett.ui_active = form_result[_f('hooks_%s' %
249 244 RhodeCodeUi.HOOK_UPDATE)]
250 245 Session().add(sett)
251 246
252 247 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_REPO_SIZE)
253 248 sett.ui_active = form_result[_f('hooks_%s' %
254 249 RhodeCodeUi.HOOK_REPO_SIZE)]
255 250 Session().add(sett)
256 251
257 252 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PUSH)
258 253 sett.ui_active = form_result[_f('hooks_%s' %
259 254 RhodeCodeUi.HOOK_PUSH)]
260 255 Session().add(sett)
261 256
262 257 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PULL)
263 258 sett.ui_active = form_result[_f('hooks_%s' %
264 259 RhodeCodeUi.HOOK_PULL)]
265 260
266 261 Session().add(sett)
267 262
268 263 ## EXTENSIONS
269 264 sett = RhodeCodeUi.get_by_key('largefiles')
270 265 if not sett:
271 266 #make one if it's not there !
272 267 sett = RhodeCodeUi()
273 268 sett.ui_key = 'largefiles'
274 269 sett.ui_section = 'extensions'
275 270 sett.ui_active = form_result[_f('extensions_largefiles')]
276 271 Session().add(sett)
277 272
278 273 sett = RhodeCodeUi.get_by_key('hgsubversion')
279 274 if not sett:
280 275 #make one if it's not there !
281 276 sett = RhodeCodeUi()
282 277 sett.ui_key = 'hgsubversion'
283 278 sett.ui_section = 'extensions'
284 279
285 280 sett.ui_active = form_result[_f('extensions_hgsubversion')]
286 281 Session().add(sett)
287 282
288 283 # sett = RhodeCodeUi.get_by_key('hggit')
289 284 # if not sett:
290 285 # #make one if it's not there !
291 286 # sett = RhodeCodeUi()
292 287 # sett.ui_key = 'hggit'
293 288 # sett.ui_section = 'extensions'
294 289 #
295 290 # sett.ui_active = form_result[_f('extensions_hggit')]
296 291 # Session().add(sett)
297 292
298 293 Session().commit()
299 294
300 295 h.flash(_('Updated VCS settings'), category='success')
301 296
302 297 except Exception:
303 298 log.error(traceback.format_exc())
304 299 h.flash(_('error occurred during updating '
305 300 'application settings'), category='error')
306 301
307 302 if setting_id == 'hooks':
308 303 ui_key = request.POST.get('new_hook_ui_key')
309 304 ui_value = request.POST.get('new_hook_ui_value')
310 305 try:
311 306
312 307 if ui_value and ui_key:
313 308 RhodeCodeUi.create_or_update_hook(ui_key, ui_value)
314 309 h.flash(_('Added new hook'),
315 310 category='success')
316 311
317 312 # check for edits
318 313 update = False
319 314 _d = request.POST.dict_of_lists()
320 315 for k, v in zip(_d.get('hook_ui_key', []),
321 316 _d.get('hook_ui_value_new', [])):
322 317 RhodeCodeUi.create_or_update_hook(k, v)
323 318 update = True
324 319
325 320 if update:
326 321 h.flash(_('Updated hooks'), category='success')
327 322 Session().commit()
328 323 except Exception:
329 324 log.error(traceback.format_exc())
330 325 h.flash(_('error occurred during hook creation'),
331 326 category='error')
332 327
333 328 return redirect(url('admin_edit_setting', setting_id='hooks'))
334 329
335 330 if setting_id == 'email':
336 331 test_email = request.POST.get('test_email')
337 332 test_email_subj = 'RhodeCode TestEmail'
338 333 test_email_body = 'RhodeCode Email test'
339 334
340 335 test_email_html_body = EmailNotificationModel()\
341 336 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
342 337 body=test_email_body)
343 338
344 339 recipients = [test_email] if [test_email] else None
345 340
346 341 run_task(tasks.send_email, recipients, test_email_subj,
347 342 test_email_body, test_email_html_body)
348 343
349 344 h.flash(_('Email task created'), category='success')
350 345 return redirect(url('admin_settings'))
351 346
352 347 @HasPermissionAllDecorator('hg.admin')
353 348 def delete(self, setting_id):
354 349 """DELETE /admin/settings/setting_id: Delete an existing item"""
355 350 # Forms posted to this method should contain a hidden field:
356 351 # <input type="hidden" name="_method" value="DELETE" />
357 352 # Or using helpers:
358 353 # h.form(url('admin_setting', setting_id=ID),
359 354 # method='delete')
360 355 # url('admin_setting', setting_id=ID)
361 356 if setting_id == 'hooks':
362 357 hook_id = request.POST.get('hook_id')
363 358 RhodeCodeUi.delete(hook_id)
364 359 Session().commit()
365 360
366 361 @HasPermissionAllDecorator('hg.admin')
367 362 def show(self, setting_id, format='html'):
368 363 """
369 364 GET /admin/settings/setting_id: Show a specific item"""
370 365 # url('admin_setting', setting_id=ID)
371 366
372 367 @HasPermissionAllDecorator('hg.admin')
373 368 def edit(self, setting_id, format='html'):
374 369 """
375 370 GET /admin/settings/setting_id/edit: Form to
376 371 edit an existing item"""
377 372 # url('admin_edit_setting', setting_id=ID)
378 373
379 374 c.hooks = RhodeCodeUi.get_builtin_hooks()
380 375 c.custom_hooks = RhodeCodeUi.get_custom_hooks()
381 376
382 377 return htmlfill.render(
383 378 render('admin/settings/hooks.html'),
384 379 defaults={},
385 380 encoding="UTF-8",
386 381 force_defaults=False
387 382 )
388 383
389 384 @NotAnonymous()
390 385 def my_account(self):
391 386 """
392 387 GET /_admin/my_account Displays info about my account
393 388 """
394 389 # url('admin_settings_my_account')
395 390
396 391 c.user = User.get(self.rhodecode_user.user_id)
397 392 all_repos = Session().query(Repository)\
398 393 .filter(Repository.user_id == c.user.user_id)\
399 394 .order_by(func.lower(Repository.repo_name)).all()
400 395
401 396 c.user_repos = ScmModel().get_repos(all_repos)
402 397
403 398 if c.user.username == 'default':
404 399 h.flash(_("You can't edit this user since it's"
405 400 " crucial for entire application"), category='warning')
406 401 return redirect(url('users'))
407 402
408 403 defaults = c.user.get_dict()
409 404
410 405 c.form = htmlfill.render(
411 406 render('admin/users/user_edit_my_account_form.html'),
412 407 defaults=defaults,
413 408 encoding="UTF-8",
414 409 force_defaults=False
415 410 )
416 411 return render('admin/users/user_edit_my_account.html')
417 412
418 413 @NotAnonymous()
419 414 def my_account_update(self):
420 415 """PUT /_admin/my_account_update: Update an existing item"""
421 416 # Forms posted to this method should contain a hidden field:
422 417 # <input type="hidden" name="_method" value="PUT" />
423 418 # Or using helpers:
424 419 # h.form(url('admin_settings_my_account_update'),
425 420 # method='put')
426 421 # url('admin_settings_my_account_update', id=ID)
427 422 uid = self.rhodecode_user.user_id
428 423 email = self.rhodecode_user.email
429 424 _form = UserForm(edit=True,
430 425 old_data={'user_id': uid, 'email': email})()
431 426 form_result = {}
432 427 try:
433 428 form_result = _form.to_python(dict(request.POST))
434 429 UserModel().update_my_account(uid, form_result)
435 430 h.flash(_('Your account was updated successfully'),
436 431 category='success')
437 432 Session().commit()
438 433 except formencode.Invalid, errors:
439 434 c.user = User.get(self.rhodecode_user.user_id)
440 435
441 436 c.form = htmlfill.render(
442 437 render('admin/users/user_edit_my_account_form.html'),
443 438 defaults=errors.value,
444 439 errors=errors.error_dict or {},
445 440 prefix_error=False,
446 441 encoding="UTF-8")
447 442 return render('admin/users/user_edit_my_account.html')
448 443 except Exception:
449 444 log.error(traceback.format_exc())
450 445 h.flash(_('error occurred during update of user %s') \
451 446 % form_result.get('username'), category='error')
452 447
453 448 return redirect(url('my_account'))
454 449
455 450 @NotAnonymous()
456 451 def my_account_my_repos(self):
457 452 all_repos = Session().query(Repository)\
458 453 .filter(Repository.user_id == self.rhodecode_user.user_id)\
459 454 .order_by(func.lower(Repository.repo_name))\
460 455 .all()
461 456 c.user_repos = ScmModel().get_repos(all_repos)
462 457 return render('admin/users/user_edit_my_account_repos.html')
463 458
464 459 @NotAnonymous()
465 460 def my_account_my_pullrequests(self):
466 461 c.my_pull_requests = PullRequest.query()\
467 462 .filter(PullRequest.user_id==
468 463 self.rhodecode_user.user_id)\
469 464 .all()
470 465 c.participate_in_pull_requests = \
471 466 [x.pull_request for x in PullRequestReviewers.query()\
472 467 .filter(PullRequestReviewers.user_id==
473 468 self.rhodecode_user.user_id)\
474 469 .all()]
475 470 return render('admin/users/user_edit_my_account_pullrequests.html')
476 471
477 472 @NotAnonymous()
478 473 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
479 474 def create_repository(self):
480 475 """GET /_admin/create_repository: Form to create a new item"""
481 476
482 477 c.repo_groups = RepoGroup.groups_choices(check_perms=True)
483 478 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
484 479 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
485 480
486 481 new_repo = request.GET.get('repo', '')
487 482 c.new_repo = repo_name_slug(new_repo)
488 483
489 484 return render('admin/repos/repo_add_create_repository.html')
490 485
491 486 def _get_hg_ui_settings(self):
492 487 ret = RhodeCodeUi.query().all()
493 488
494 489 if not ret:
495 490 raise Exception('Could not get application ui settings !')
496 491 settings = {}
497 492 for each in ret:
498 493 k = each.ui_key
499 494 v = each.ui_value
500 495 if k == '/':
501 496 k = 'root_path'
502 497
503 498 if k == 'push_ssl':
504 499 v = str2bool(v)
505 500
506 501 if k.find('.') != -1:
507 502 k = k.replace('.', '_')
508 503
509 504 if each.ui_section in ['hooks', 'extensions']:
510 505 v = each.ui_active
511 506
512 507 settings[each.ui_section + '_' + k] = v
513 508 return settings
@@ -1,324 +1,323
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
21 21 safe_str
22 22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 24 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
25 25 from rhodecode.model import meta
26 26
27 27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 28 from rhodecode.model.notification import NotificationModel
29 29 from rhodecode.model.scm import ScmModel
30 30 from rhodecode.model.meta import Session
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def _get_ip_addr(environ):
36 36 proxy_key = 'HTTP_X_REAL_IP'
37 37 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
38 38 def_key = 'REMOTE_ADDR'
39 39
40 40 ip = environ.get(proxy_key2)
41 41 if ip:
42 42 return ip
43 43
44 44 ip = environ.get(proxy_key)
45 45
46 46 if ip:
47 47 return ip
48 48
49 49 ip = environ.get(def_key, '0.0.0.0')
50 50 return ip
51 51
52 52
53 53 def _get_access_path(environ):
54 54 path = environ.get('PATH_INFO')
55 55 org_req = environ.get('pylons.original_request')
56 56 if org_req:
57 57 path = org_req.environ.get('PATH_INFO')
58 58 return path
59 59
60 60
61 61 class BasicAuth(AuthBasicAuthenticator):
62 62
63 63 def __init__(self, realm, authfunc, auth_http_code=None):
64 64 self.realm = realm
65 65 self.authfunc = authfunc
66 66 self._rc_auth_http_code = auth_http_code
67 67
68 68 def build_authentication(self):
69 69 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
70 70 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
71 71 # return 403 if alternative http return code is specified in
72 72 # RhodeCode config
73 73 return HTTPForbidden(headers=head)
74 74 return HTTPUnauthorized(headers=head)
75 75
76 76 def authenticate(self, environ):
77 77 authorization = AUTHORIZATION(environ)
78 78 if not authorization:
79 79 return self.build_authentication()
80 80 (authmeth, auth) = authorization.split(' ', 1)
81 81 if 'basic' != authmeth.lower():
82 82 return self.build_authentication()
83 83 auth = auth.strip().decode('base64')
84 84 _parts = auth.split(':', 1)
85 85 if len(_parts) == 2:
86 86 username, password = _parts
87 87 if self.authfunc(environ, username, password):
88 88 return username
89 89 return self.build_authentication()
90 90
91 91 __call__ = authenticate
92 92
93 93
94 94 class BaseVCSController(object):
95 95
96 96 def __init__(self, application, config):
97 97 self.application = application
98 98 self.config = config
99 99 # base path of repo locations
100 100 self.basepath = self.config['base_path']
101 101 #authenticate this mercurial request using authfunc
102 102 self.authenticate = BasicAuth('', authfunc,
103 103 config.get('auth_ret_code'))
104 104 self.ipaddr = '0.0.0.0'
105 105
106 106 def _handle_request(self, environ, start_response):
107 107 raise NotImplementedError()
108 108
109 109 def _get_by_id(self, repo_name):
110 110 """
111 111 Get's a special pattern _<ID> from clone url and tries to replace it
112 112 with a repository_name for support of _<ID> non changable urls
113 113
114 114 :param repo_name:
115 115 """
116 116 try:
117 117 data = repo_name.split('/')
118 118 if len(data) >= 2:
119 119 by_id = data[1].split('_')
120 120 if len(by_id) == 2 and by_id[1].isdigit():
121 121 _repo_name = Repository.get(by_id[1]).repo_name
122 122 data[1] = _repo_name
123 123 except:
124 124 log.debug('Failed to extract repo_name from id %s' % (
125 125 traceback.format_exc()
126 126 )
127 127 )
128 128
129 129 return '/'.join(data)
130 130
131 131 def _invalidate_cache(self, repo_name):
132 132 """
133 133 Set's cache for this repository for invalidation on next access
134 134
135 135 :param repo_name: full repo name, also a cache key
136 136 """
137 137 invalidate_cache('get_repo_cached_%s' % repo_name)
138 138
139 139 def _check_permission(self, action, user, repo_name):
140 140 """
141 141 Checks permissions using action (push/pull) user and repository
142 142 name
143 143
144 144 :param action: push or pull action
145 145 :param user: user instance
146 146 :param repo_name: repository name
147 147 """
148 148 if action == 'push':
149 149 if not HasPermissionAnyMiddleware('repository.write',
150 150 'repository.admin')(user,
151 151 repo_name):
152 152 return False
153 153
154 154 else:
155 155 #any other action need at least read permission
156 156 if not HasPermissionAnyMiddleware('repository.read',
157 157 'repository.write',
158 158 'repository.admin')(user,
159 159 repo_name):
160 160 return False
161 161
162 162 return True
163 163
164 164 def _get_ip_addr(self, environ):
165 165 return _get_ip_addr(environ)
166 166
167 167 def _check_ssl(self, environ, start_response):
168 168 """
169 169 Checks the SSL check flag and returns False if SSL is not present
170 170 and required True otherwise
171 171 """
172 172 org_proto = environ['wsgi._org_proto']
173 173 #check if we have SSL required ! if not it's a bad request !
174 174 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
175 175 if require_ssl and org_proto == 'http':
176 176 log.debug('proto is %s and SSL is required BAD REQUEST !'
177 177 % org_proto)
178 178 return False
179 179 return True
180 180
181 181 def _check_locking_state(self, environ, action, repo, user_id):
182 182 """
183 183 Checks locking on this repository, if locking is enabled and lock is
184 184 present returns a tuple of make_lock, locked, locked_by.
185 185 make_lock can have 3 states None (do nothing) True, make lock
186 186 False release lock, This value is later propagated to hooks, which
187 187 do the locking. Think about this as signals passed to hooks what to do.
188 188
189 189 """
190 190 locked = False # defines that locked error should be thrown to user
191 191 make_lock = None
192 192 repo = Repository.get_by_repo_name(repo)
193 193 user = User.get(user_id)
194 194
195 195 # this is kind of hacky, but due to how mercurial handles client-server
196 196 # server see all operation on changeset; bookmarks, phases and
197 197 # obsolescence marker in different transaction, we don't want to check
198 198 # locking on those
199 199 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
200 200 locked_by = repo.locked
201 201 if repo and repo.enable_locking and not obsolete_call:
202 202 if action == 'push':
203 203 #check if it's already locked !, if it is compare users
204 204 user_id, _date = repo.locked
205 205 if user.user_id == user_id:
206 206 log.debug('Got push from user %s, now unlocking' % (user))
207 207 # unlock if we have push from user who locked
208 208 make_lock = False
209 209 else:
210 210 # we're not the same user who locked, ban with 423 !
211 211 locked = True
212 212 if action == 'pull':
213 213 if repo.locked[0] and repo.locked[1]:
214 214 locked = True
215 215 else:
216 216 log.debug('Setting lock on repo %s by %s' % (repo, user))
217 217 make_lock = True
218 218
219 219 else:
220 220 log.debug('Repository %s do not have locking enabled' % (repo))
221 221 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
222 222 % (make_lock, locked, locked_by))
223 223 return make_lock, locked, locked_by
224 224
225 225 def __call__(self, environ, start_response):
226 226 start = time.time()
227 227 try:
228 228 return self._handle_request(environ, start_response)
229 229 finally:
230 230 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
231 231 log.debug('Request time: %.3fs' % (time.time() - start))
232 232 meta.Session.remove()
233 233
234 234
235 235 class BaseController(WSGIController):
236 236
237 237 def __before__(self):
238 238 c.rhodecode_version = __version__
239 239 c.rhodecode_instanceid = config.get('instance_id')
240 240 c.rhodecode_name = config.get('rhodecode_title')
241 241 c.use_gravatar = str2bool(config.get('use_gravatar'))
242 242 c.ga_code = config.get('rhodecode_ga_code')
243 243 # Visual options
244 244 c.visual = AttributeDict({})
245 245 rc_config = RhodeCodeSetting.get_app_settings()
246 246
247 247 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
248 248 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
249 249 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
250 250 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
251 c.visual.lightweight_journal = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
252 251
253 252 c.repo_name = get_repo_slug(request)
254 253 c.backends = BACKENDS.keys()
255 254 c.unread_notifications = NotificationModel()\
256 255 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
257 256 self.cut_off_limit = int(config.get('cut_off_limit'))
258 257
259 258 self.sa = meta.Session
260 259 self.scm_model = ScmModel(self.sa)
261 260 self.ip_addr = ''
262 261
263 262 def __call__(self, environ, start_response):
264 263 """Invoke the Controller"""
265 264 # WSGIController.__call__ dispatches to the Controller method
266 265 # the request is routed to. This routing information is
267 266 # available in environ['pylons.routes_dict']
268 267 start = time.time()
269 268 try:
270 269 self.ip_addr = _get_ip_addr(environ)
271 270 # make sure that we update permissions each time we call controller
272 271 api_key = request.GET.get('api_key')
273 272 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
274 273 user_id = cookie_store.get('user_id', None)
275 274 username = get_container_username(environ, config)
276 275 auth_user = AuthUser(user_id, api_key, username)
277 276 request.user = auth_user
278 277 self.rhodecode_user = c.rhodecode_user = auth_user
279 278 if not self.rhodecode_user.is_authenticated and \
280 279 self.rhodecode_user.user_id is not None:
281 280 self.rhodecode_user.set_authenticated(
282 281 cookie_store.get('is_authenticated')
283 282 )
284 283 log.info('IP: %s User: %s accessed %s' % (
285 284 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
286 285 )
287 286 return WSGIController.__call__(self, environ, start_response)
288 287 finally:
289 288 log.info('IP: %s Request to %s time: %.3fs' % (
290 289 _get_ip_addr(environ),
291 290 safe_unicode(_get_access_path(environ)), time.time() - start)
292 291 )
293 292 meta.Session.remove()
294 293
295 294
296 295 class BaseRepoController(BaseController):
297 296 """
298 297 Base class for controllers responsible for loading all needed data for
299 298 repository loaded items are
300 299
301 300 c.rhodecode_repo: instance of scm repository
302 301 c.rhodecode_db_repo: instance of db
303 302 c.repository_followers: number of followers
304 303 c.repository_forks: number of forks
305 304 """
306 305
307 306 def __before__(self):
308 307 super(BaseRepoController, self).__before__()
309 308 if c.repo_name:
310 309
311 310 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
312 311 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
313 312 # update last change according to VCS data
314 313 dbr.update_last_change(c.rhodecode_repo.last_change)
315 314 if c.rhodecode_repo is None:
316 315 log.error('%s this repository is present in database but it '
317 316 'cannot be created as an scm instance', c.repo_name)
318 317
319 318 redirect(url('home'))
320 319
321 320 # some globals counter for menu
322 321 c.repository_followers = self.scm_model.get_followers(dbr)
323 322 c.repository_forks = self.scm_model.get_forks(dbr)
324 323 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
@@ -1,1109 +1,1107
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12 import re
13 13 import urlparse
14 14
15 15 from datetime import datetime
16 16 from pygments.formatters.html import HtmlFormatter
17 17 from pygments import highlight as code_highlight
18 18 from pylons import url, request, config
19 19 from pylons.i18n.translation import _, ungettext
20 20 from hashlib import md5
21 21
22 22 from webhelpers.html import literal, HTML, escape
23 23 from webhelpers.html.tools import *
24 24 from webhelpers.html.builder import make_tag
25 25 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
26 26 end_form, file, form, hidden, image, javascript_link, link_to, \
27 27 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
28 28 submit, text, password, textarea, title, ul, xml_declaration, radio
29 29 from webhelpers.html.tools import auto_link, button_to, highlight, \
30 30 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
31 31 from webhelpers.number import format_byte_size, format_bit_size
32 32 from webhelpers.pylonslib import Flash as _Flash
33 33 from webhelpers.pylonslib.secure_form import secure_form
34 34 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
35 35 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
36 36 replace_whitespace, urlify, truncate, wrap_paragraphs
37 37 from webhelpers.date import time_ago_in_words
38 38 from webhelpers.paginate import Page
39 39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 40 convert_boolean_attrs, NotGiven, _make_safe_id_component
41 41
42 42 from rhodecode.lib.annotate import annotate_highlight
43 43 from rhodecode.lib.utils import repo_name_slug
44 44 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 45 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
46 46 from rhodecode.lib.markup_renderer import MarkupRenderer
47 47 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
48 48 from rhodecode.lib.vcs.backends.base import BaseChangeset
49 49 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.db import URL_SEP, Permission
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 html_escape_table = {
57 57 "&": "&amp;",
58 58 '"': "&quot;",
59 59 "'": "&apos;",
60 60 ">": "&gt;",
61 61 "<": "&lt;",
62 62 }
63 63
64 64
65 65 def html_escape(text):
66 66 """Produce entities within text."""
67 67 return "".join(html_escape_table.get(c,c) for c in text)
68 68
69 69
70 70 def shorter(text, size=20):
71 71 postfix = '...'
72 72 if len(text) > size:
73 73 return text[:size - len(postfix)] + postfix
74 74 return text
75 75
76 76
77 77 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
78 78 """
79 79 Reset button
80 80 """
81 81 _set_input_attrs(attrs, type, name, value)
82 82 _set_id_attr(attrs, id, name)
83 83 convert_boolean_attrs(attrs, ["disabled"])
84 84 return HTML.input(**attrs)
85 85
86 86 reset = _reset
87 87 safeid = _make_safe_id_component
88 88
89 89
90 90 def FID(raw_id, path):
91 91 """
92 92 Creates a uniqe ID for filenode based on it's hash of path and revision
93 93 it's safe to use in urls
94 94
95 95 :param raw_id:
96 96 :param path:
97 97 """
98 98
99 99 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
100 100
101 101
102 102 def get_token():
103 103 """Return the current authentication token, creating one if one doesn't
104 104 already exist.
105 105 """
106 106 token_key = "_authentication_token"
107 107 from pylons import session
108 108 if not token_key in session:
109 109 try:
110 110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
111 111 except AttributeError: # Python < 2.4
112 112 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
113 113 session[token_key] = token
114 114 if hasattr(session, 'save'):
115 115 session.save()
116 116 return session[token_key]
117 117
118 118
119 119 class _GetError(object):
120 120 """Get error from form_errors, and represent it as span wrapped error
121 121 message
122 122
123 123 :param field_name: field to fetch errors for
124 124 :param form_errors: form errors dict
125 125 """
126 126
127 127 def __call__(self, field_name, form_errors):
128 128 tmpl = """<span class="error_msg">%s</span>"""
129 129 if form_errors and field_name in form_errors:
130 130 return literal(tmpl % form_errors.get(field_name))
131 131
132 132 get_error = _GetError()
133 133
134 134
135 135 class _ToolTip(object):
136 136
137 137 def __call__(self, tooltip_title, trim_at=50):
138 138 """
139 139 Special function just to wrap our text into nice formatted
140 140 autowrapped text
141 141
142 142 :param tooltip_title:
143 143 """
144 144 tooltip_title = escape(tooltip_title)
145 145 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
146 146 return tooltip_title
147 147 tooltip = _ToolTip()
148 148
149 149
150 150 class _FilesBreadCrumbs(object):
151 151
152 152 def __call__(self, repo_name, rev, paths):
153 153 if isinstance(paths, str):
154 154 paths = safe_unicode(paths)
155 155 url_l = [link_to(repo_name, url('files_home',
156 156 repo_name=repo_name,
157 157 revision=rev, f_path=''),
158 158 class_='ypjax-link')]
159 159 paths_l = paths.split('/')
160 160 for cnt, p in enumerate(paths_l):
161 161 if p != '':
162 162 url_l.append(link_to(p,
163 163 url('files_home',
164 164 repo_name=repo_name,
165 165 revision=rev,
166 166 f_path='/'.join(paths_l[:cnt + 1])
167 167 ),
168 168 class_='ypjax-link'
169 169 )
170 170 )
171 171
172 172 return literal('/'.join(url_l))
173 173
174 174 files_breadcrumbs = _FilesBreadCrumbs()
175 175
176 176
177 177 class CodeHtmlFormatter(HtmlFormatter):
178 178 """
179 179 My code Html Formatter for source codes
180 180 """
181 181
182 182 def wrap(self, source, outfile):
183 183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184 184
185 185 def _wrap_code(self, source):
186 186 for cnt, it in enumerate(source):
187 187 i, t = it
188 188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 189 yield i, t
190 190
191 191 def _wrap_tablelinenos(self, inner):
192 192 dummyoutfile = StringIO.StringIO()
193 193 lncount = 0
194 194 for t, line in inner:
195 195 if t:
196 196 lncount += 1
197 197 dummyoutfile.write(line)
198 198
199 199 fl = self.linenostart
200 200 mw = len(str(lncount + fl - 1))
201 201 sp = self.linenospecial
202 202 st = self.linenostep
203 203 la = self.lineanchors
204 204 aln = self.anchorlinenos
205 205 nocls = self.noclasses
206 206 if sp:
207 207 lines = []
208 208
209 209 for i in range(fl, fl + lncount):
210 210 if i % st == 0:
211 211 if i % sp == 0:
212 212 if aln:
213 213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 214 (la, i, mw, i))
215 215 else:
216 216 lines.append('<span class="special">%*d</span>' % (mw, i))
217 217 else:
218 218 if aln:
219 219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 220 else:
221 221 lines.append('%*d' % (mw, i))
222 222 else:
223 223 lines.append('')
224 224 ls = '\n'.join(lines)
225 225 else:
226 226 lines = []
227 227 for i in range(fl, fl + lncount):
228 228 if i % st == 0:
229 229 if aln:
230 230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 231 else:
232 232 lines.append('%*d' % (mw, i))
233 233 else:
234 234 lines.append('')
235 235 ls = '\n'.join(lines)
236 236
237 237 # in case you wonder about the seemingly redundant <div> here: since the
238 238 # content in the other cell also is wrapped in a div, some browsers in
239 239 # some configurations seem to mess up the formatting...
240 240 if nocls:
241 241 yield 0, ('<table class="%stable">' % self.cssclass +
242 242 '<tr><td><div class="linenodiv" '
243 243 'style="background-color: #f0f0f0; padding-right: 10px">'
244 244 '<pre style="line-height: 125%">' +
245 245 ls + '</pre></div></td><td id="hlcode" class="code">')
246 246 else:
247 247 yield 0, ('<table class="%stable">' % self.cssclass +
248 248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 249 ls + '</pre></div></td><td id="hlcode" class="code">')
250 250 yield 0, dummyoutfile.getvalue()
251 251 yield 0, '</td></tr></table>'
252 252
253 253
254 254 def pygmentize(filenode, **kwargs):
255 255 """pygmentize function using pygments
256 256
257 257 :param filenode:
258 258 """
259 259
260 260 return literal(code_highlight(filenode.content,
261 261 filenode.lexer, CodeHtmlFormatter(**kwargs)))
262 262
263 263
264 264 def pygmentize_annotation(repo_name, filenode, **kwargs):
265 265 """
266 266 pygmentize function for annotation
267 267
268 268 :param filenode:
269 269 """
270 270
271 271 color_dict = {}
272 272
273 273 def gen_color(n=10000):
274 274 """generator for getting n of evenly distributed colors using
275 275 hsv color and golden ratio. It always return same order of colors
276 276
277 277 :returns: RGB tuple
278 278 """
279 279
280 280 def hsv_to_rgb(h, s, v):
281 281 if s == 0.0:
282 282 return v, v, v
283 283 i = int(h * 6.0) # XXX assume int() truncates!
284 284 f = (h * 6.0) - i
285 285 p = v * (1.0 - s)
286 286 q = v * (1.0 - s * f)
287 287 t = v * (1.0 - s * (1.0 - f))
288 288 i = i % 6
289 289 if i == 0:
290 290 return v, t, p
291 291 if i == 1:
292 292 return q, v, p
293 293 if i == 2:
294 294 return p, v, t
295 295 if i == 3:
296 296 return p, q, v
297 297 if i == 4:
298 298 return t, p, v
299 299 if i == 5:
300 300 return v, p, q
301 301
302 302 golden_ratio = 0.618033988749895
303 303 h = 0.22717784590367374
304 304
305 305 for _ in xrange(n):
306 306 h += golden_ratio
307 307 h %= 1
308 308 HSV_tuple = [h, 0.95, 0.95]
309 309 RGB_tuple = hsv_to_rgb(*HSV_tuple)
310 310 yield map(lambda x: str(int(x * 256)), RGB_tuple)
311 311
312 312 cgenerator = gen_color()
313 313
314 314 def get_color_string(cs):
315 315 if cs in color_dict:
316 316 col = color_dict[cs]
317 317 else:
318 318 col = color_dict[cs] = cgenerator.next()
319 319 return "color: rgb(%s)! important;" % (', '.join(col))
320 320
321 321 def url_func(repo_name):
322 322
323 323 def _url_func(changeset):
324 324 author = changeset.author
325 325 date = changeset.date
326 326 message = tooltip(changeset.message)
327 327
328 328 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
329 329 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
330 330 "</b> %s<br/></div>")
331 331
332 332 tooltip_html = tooltip_html % (author, date, message)
333 333 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
334 334 short_id(changeset.raw_id))
335 335 uri = link_to(
336 336 lnk_format,
337 337 url('changeset_home', repo_name=repo_name,
338 338 revision=changeset.raw_id),
339 339 style=get_color_string(changeset.raw_id),
340 340 class_='tooltip',
341 341 title=tooltip_html
342 342 )
343 343
344 344 uri += '\n'
345 345 return uri
346 346 return _url_func
347 347
348 348 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
349 349
350 350
351 351 def is_following_repo(repo_name, user_id):
352 352 from rhodecode.model.scm import ScmModel
353 353 return ScmModel().is_following_repo(repo_name, user_id)
354 354
355 355 flash = _Flash()
356 356
357 357 #==============================================================================
358 358 # SCM FILTERS available via h.
359 359 #==============================================================================
360 360 from rhodecode.lib.vcs.utils import author_name, author_email
361 361 from rhodecode.lib.utils2 import credentials_filter, age as _age
362 362 from rhodecode.model.db import User, ChangesetStatus
363 363
364 364 age = lambda x: _age(x)
365 365 capitalize = lambda x: x.capitalize()
366 366 email = author_email
367 367 short_id = lambda x: x[:12]
368 368 hide_credentials = lambda x: ''.join(credentials_filter(x))
369 369
370 370
371 371 def fmt_date(date):
372 372 if date:
373 373 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
374 374 return date.strftime(_fmt).decode('utf8')
375 375
376 376 return ""
377 377
378 378
379 379 def is_git(repository):
380 380 if hasattr(repository, 'alias'):
381 381 _type = repository.alias
382 382 elif hasattr(repository, 'repo_type'):
383 383 _type = repository.repo_type
384 384 else:
385 385 _type = repository
386 386 return _type == 'git'
387 387
388 388
389 389 def is_hg(repository):
390 390 if hasattr(repository, 'alias'):
391 391 _type = repository.alias
392 392 elif hasattr(repository, 'repo_type'):
393 393 _type = repository.repo_type
394 394 else:
395 395 _type = repository
396 396 return _type == 'hg'
397 397
398 398
399 399 def email_or_none(author):
400 400 # extract email from the commit string
401 401 _email = email(author)
402 402 if _email != '':
403 403 # check it against RhodeCode database, and use the MAIN email for this
404 404 # user
405 405 user = User.get_by_email(_email, case_insensitive=True, cache=True)
406 406 if user is not None:
407 407 return user.email
408 408 return _email
409 409
410 410 # See if it contains a username we can get an email from
411 411 user = User.get_by_username(author_name(author), case_insensitive=True,
412 412 cache=True)
413 413 if user is not None:
414 414 return user.email
415 415
416 416 # No valid email, not a valid user in the system, none!
417 417 return None
418 418
419 419
420 420 def person(author, show_attr="username_and_name"):
421 421 # attr to return from fetched user
422 422 person_getter = lambda usr: getattr(usr, show_attr)
423 423
424 424 # Valid email in the attribute passed, see if they're in the system
425 425 _email = email(author)
426 426 if _email != '':
427 427 user = User.get_by_email(_email, case_insensitive=True, cache=True)
428 428 if user is not None:
429 429 return person_getter(user)
430 430 return _email
431 431
432 432 # Maybe it's a username?
433 433 _author = author_name(author)
434 434 user = User.get_by_username(_author, case_insensitive=True,
435 435 cache=True)
436 436 if user is not None:
437 437 return person_getter(user)
438 438
439 439 # Still nothing? Just pass back the author name then
440 440 return _author
441 441
442 442
443 443 def person_by_id(id_, show_attr="username_and_name"):
444 444 # attr to return from fetched user
445 445 person_getter = lambda usr: getattr(usr, show_attr)
446 446
447 447 #maybe it's an ID ?
448 448 if str(id_).isdigit() or isinstance(id_, int):
449 449 id_ = int(id_)
450 450 user = User.get(id_)
451 451 if user is not None:
452 452 return person_getter(user)
453 453 return id_
454 454
455 455
456 456 def desc_stylize(value):
457 457 """
458 458 converts tags from value into html equivalent
459 459
460 460 :param value:
461 461 """
462 462 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
463 463 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
464 464 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 465 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
466 466 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
467 467 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
468 468 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/]*)\]',
469 469 '<div class="metatag" tag="lang">\\2</div>', value)
470 470 value = re.sub(r'\[([a-z]+)\]',
471 471 '<div class="metatag" tag="\\1">\\1</div>', value)
472 472
473 473 return value
474 474
475 475
476 476 def bool2icon(value):
477 477 """Returns True/False values represented as small html image of true/false
478 478 icons
479 479
480 480 :param value: bool value
481 481 """
482 482
483 483 if value is True:
484 484 return HTML.tag('img', src=url("/images/icons/accept.png"),
485 485 alt=_('True'))
486 486
487 487 if value is False:
488 488 return HTML.tag('img', src=url("/images/icons/cancel.png"),
489 489 alt=_('False'))
490 490
491 491 return value
492 492
493 493
494 def action_parser(user_log, feed=False, parse_cs=True):
494 def action_parser(user_log, feed=False, parse_cs=False):
495 495 """
496 496 This helper will action_map the specified string action into translated
497 497 fancy names with icons and links
498 498
499 499 :param user_log: user log instance
500 500 :param feed: use output for feeds (no html and fancy icons)
501 501 :param parse_cs: parse Changesets into VCS instances
502 502 """
503 from pylons import tmpl_context as c
504 if c.visual.lightweight_journal:
505 parse_cs = False
503
506 504 action = user_log.action
507 505 action_params = ' '
508 506
509 507 x = action.split(':')
510 508
511 509 if len(x) > 1:
512 510 action, action_params = x
513 511
514 512 def get_cs_links():
515 513 revs_limit = 3 # display this amount always
516 514 revs_top_limit = 50 # show upto this amount of changesets hidden
517 515 revs_ids = action_params.split(',')
518 516 deleted = user_log.repository is None
519 517 if deleted:
520 518 return ','.join(revs_ids)
521 519
522 520 repo_name = user_log.repository.repo_name
523 521
524 522 def lnk(rev, repo_name):
525
526 523 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
527 if rev.revision:
528 lbl = 'r%s:%s' % (rev.revision, rev.short_id)
529 else:
530 lbl = '%s' % (rev.short_id)
524 lbl = '%s' % (rev.short_id[:8])
531 525 _url = url('changeset_home', repo_name=repo_name,
532 526 revision=rev.raw_id)
533 title = tooltip(rev.message) if parse_cs else ''
527 title = tooltip(rev.message)
534 528 else:
529 ## changeset cannot be found/striped/removed etc.
535 530 lbl = ('%s' % rev)[:12]
536 531 _url = '#'
537 532 title = _('Changeset not found')
538
539 return link_to(lbl, _url, title=title,
540 class_='tooltip' if parse_cs else '',)
533 if parse_cs:
534 return link_to(lbl, _url, title=title, class_='tooltip')
535 return link_to(lbl, _url, raw_id=rev.raw_id, class_='journal-cs')
541 536
542 537 revs = []
543 538 if len(filter(lambda v: v != '', revs_ids)) > 0:
544 if parse_cs:
545 repo = user_log.repository.scm_instance
539 repo = None
546 540 for rev in revs_ids[:revs_top_limit]:
541 # we want parsed changesets, or new log store format is bad
547 542 if parse_cs:
548 543 try:
549 rev = repo.get_changeset(rev)
550 revs.append(rev)
544 if repo is None:
545 repo = user_log.repository.scm_instance
546 _rev = repo.get_changeset(rev)
547 revs.append(_rev)
551 548 except ChangesetDoesNotExistError:
552 549 log.error('cannot find revision %s in this repo' % rev)
553 550 revs.append(rev)
554 551 continue
555 552 else:
556 rev = AttributeDict({
553 _rev = AttributeDict({
557 554 'short_id': rev[:12],
558 555 'raw_id': rev,
556 'message': '',
559 557 })
560 revs.append(rev)
558 revs.append(_rev)
561 559 cs_links = []
562 560 cs_links.append(" " + ', '.join(
563 561 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
564 562 )
565 563 )
566 564
567 565 compare_view = (
568 566 ' <div class="compare_view tooltip" title="%s">'
569 567 '<a href="%s">%s</a> </div>' % (
570 568 _('Show all combined changesets %s->%s') % (
571 569 revs_ids[0][:12], revs_ids[-1][:12]
572 570 ),
573 571 url('changeset_home', repo_name=repo_name,
574 572 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
575 573 ),
576 574 _('compare view')
577 575 )
578 576 )
579 577
580 578 # if we have exactly one more than normally displayed
581 579 # just display it, takes less space than displaying
582 580 # "and 1 more revisions"
583 581 if len(revs_ids) == revs_limit + 1:
584 582 rev = revs[revs_limit]
585 583 cs_links.append(", " + lnk(rev, repo_name))
586 584
587 585 # hidden-by-default ones
588 586 if len(revs_ids) > revs_limit + 1:
589 587 uniq_id = revs_ids[0]
590 588 html_tmpl = (
591 589 '<span> %s <a class="show_more" id="_%s" '
592 590 'href="#more">%s</a> %s</span>'
593 591 )
594 592 if not feed:
595 593 cs_links.append(html_tmpl % (
596 594 _('and'),
597 595 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
598 596 _('revisions')
599 597 )
600 598 )
601 599
602 600 if not feed:
603 601 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
604 602 else:
605 603 html_tmpl = '<span id="%s"> %s </span>'
606 604
607 605 morelinks = ', '.join(
608 606 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
609 607 )
610 608
611 609 if len(revs_ids) > revs_top_limit:
612 610 morelinks += ', ...'
613 611
614 612 cs_links.append(html_tmpl % (uniq_id, morelinks))
615 613 if len(revs) > 1:
616 614 cs_links.append(compare_view)
617 615 return ''.join(cs_links)
618 616
619 617 def get_fork_name():
620 618 repo_name = action_params
621 619 _url = url('summary_home', repo_name=repo_name)
622 620 return _('fork name %s') % link_to(action_params, _url)
623 621
624 622 def get_user_name():
625 623 user_name = action_params
626 624 return user_name
627 625
628 626 def get_users_group():
629 627 group_name = action_params
630 628 return group_name
631 629
632 630 def get_pull_request():
633 631 pull_request_id = action_params
634 632 repo_name = user_log.repository.repo_name
635 633 return link_to(_('Pull request #%s') % pull_request_id,
636 634 url('pullrequest_show', repo_name=repo_name,
637 635 pull_request_id=pull_request_id))
638 636
639 637 # action : translated str, callback(extractor), icon
640 638 action_map = {
641 639 'user_deleted_repo': (_('[deleted] repository'),
642 640 None, 'database_delete.png'),
643 641 'user_created_repo': (_('[created] repository'),
644 642 None, 'database_add.png'),
645 643 'user_created_fork': (_('[created] repository as fork'),
646 644 None, 'arrow_divide.png'),
647 645 'user_forked_repo': (_('[forked] repository'),
648 646 get_fork_name, 'arrow_divide.png'),
649 647 'user_updated_repo': (_('[updated] repository'),
650 648 None, 'database_edit.png'),
651 649 'admin_deleted_repo': (_('[delete] repository'),
652 650 None, 'database_delete.png'),
653 651 'admin_created_repo': (_('[created] repository'),
654 652 None, 'database_add.png'),
655 653 'admin_forked_repo': (_('[forked] repository'),
656 654 None, 'arrow_divide.png'),
657 655 'admin_updated_repo': (_('[updated] repository'),
658 656 None, 'database_edit.png'),
659 657 'admin_created_user': (_('[created] user'),
660 658 get_user_name, 'user_add.png'),
661 659 'admin_updated_user': (_('[updated] user'),
662 660 get_user_name, 'user_edit.png'),
663 661 'admin_created_users_group': (_('[created] users group'),
664 662 get_users_group, 'group_add.png'),
665 663 'admin_updated_users_group': (_('[updated] users group'),
666 664 get_users_group, 'group_edit.png'),
667 665 'user_commented_revision': (_('[commented] on revision in repository'),
668 666 get_cs_links, 'comment_add.png'),
669 667 'user_commented_pull_request': (_('[commented] on pull request for'),
670 668 get_pull_request, 'comment_add.png'),
671 669 'user_closed_pull_request': (_('[closed] pull request for'),
672 670 get_pull_request, 'tick.png'),
673 671 'push': (_('[pushed] into'),
674 672 get_cs_links, 'script_add.png'),
675 673 'push_local': (_('[committed via RhodeCode] into repository'),
676 674 get_cs_links, 'script_edit.png'),
677 675 'push_remote': (_('[pulled from remote] into repository'),
678 676 get_cs_links, 'connect.png'),
679 677 'pull': (_('[pulled] from'),
680 678 None, 'down_16.png'),
681 679 'started_following_repo': (_('[started following] repository'),
682 680 None, 'heart_add.png'),
683 681 'stopped_following_repo': (_('[stopped following] repository'),
684 682 None, 'heart_delete.png'),
685 683 }
686 684
687 685 action_str = action_map.get(action, action)
688 686 if feed:
689 687 action = action_str[0].replace('[', '').replace(']', '')
690 688 else:
691 689 action = action_str[0]\
692 690 .replace('[', '<span class="journal_highlight">')\
693 691 .replace(']', '</span>')
694 692
695 693 action_params_func = lambda: ""
696 694
697 695 if callable(action_str[1]):
698 696 action_params_func = action_str[1]
699 697
700 698 def action_parser_icon():
701 699 action = user_log.action
702 700 action_params = None
703 701 x = action.split(':')
704 702
705 703 if len(x) > 1:
706 704 action, action_params = x
707 705
708 706 tmpl = """<img src="%s%s" alt="%s"/>"""
709 707 ico = action_map.get(action, ['', '', ''])[2]
710 708 return literal(tmpl % ((url('/images/icons/')), ico, action))
711 709
712 710 # returned callbacks we need to call to get
713 711 return [lambda: literal(action), action_params_func, action_parser_icon]
714 712
715 713
716 714
717 715 #==============================================================================
718 716 # PERMS
719 717 #==============================================================================
720 718 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
721 719 HasRepoPermissionAny, HasRepoPermissionAll
722 720
723 721
724 722 #==============================================================================
725 723 # GRAVATAR URL
726 724 #==============================================================================
727 725
728 726 def gravatar_url(email_address, size=30):
729 727 from pylons import url ## doh, we need to re-import url to mock it later
730 728 if(str2bool(config['app_conf'].get('use_gravatar')) and
731 729 config['app_conf'].get('alternative_gravatar_url')):
732 730 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
733 731 parsed_url = urlparse.urlparse(url.current(qualified=True))
734 732 tmpl = tmpl.replace('{email}', email_address)\
735 733 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
736 734 .replace('{netloc}', parsed_url.netloc)\
737 735 .replace('{scheme}', parsed_url.scheme)\
738 736 .replace('{size}', str(size))
739 737 return tmpl
740 738
741 739 if (not str2bool(config['app_conf'].get('use_gravatar')) or
742 740 not email_address or email_address == 'anonymous@rhodecode.org'):
743 741 f = lambda a, l: min(l, key=lambda x: abs(x - a))
744 742 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
745 743
746 744 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
747 745 default = 'identicon'
748 746 baseurl_nossl = "http://www.gravatar.com/avatar/"
749 747 baseurl_ssl = "https://secure.gravatar.com/avatar/"
750 748 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
751 749
752 750 if isinstance(email_address, unicode):
753 751 #hashlib crashes on unicode items
754 752 email_address = safe_str(email_address)
755 753 # construct the url
756 754 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
757 755 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
758 756
759 757 return gravatar_url
760 758
761 759
762 760 #==============================================================================
763 761 # REPO PAGER, PAGER FOR REPOSITORY
764 762 #==============================================================================
765 763 class RepoPage(Page):
766 764
767 765 def __init__(self, collection, page=1, items_per_page=20,
768 766 item_count=None, url=None, **kwargs):
769 767
770 768 """Create a "RepoPage" instance. special pager for paging
771 769 repository
772 770 """
773 771 self._url_generator = url
774 772
775 773 # Safe the kwargs class-wide so they can be used in the pager() method
776 774 self.kwargs = kwargs
777 775
778 776 # Save a reference to the collection
779 777 self.original_collection = collection
780 778
781 779 self.collection = collection
782 780
783 781 # The self.page is the number of the current page.
784 782 # The first page has the number 1!
785 783 try:
786 784 self.page = int(page) # make it int() if we get it as a string
787 785 except (ValueError, TypeError):
788 786 self.page = 1
789 787
790 788 self.items_per_page = items_per_page
791 789
792 790 # Unless the user tells us how many items the collections has
793 791 # we calculate that ourselves.
794 792 if item_count is not None:
795 793 self.item_count = item_count
796 794 else:
797 795 self.item_count = len(self.collection)
798 796
799 797 # Compute the number of the first and last available page
800 798 if self.item_count > 0:
801 799 self.first_page = 1
802 800 self.page_count = int(math.ceil(float(self.item_count) /
803 801 self.items_per_page))
804 802 self.last_page = self.first_page + self.page_count - 1
805 803
806 804 # Make sure that the requested page number is the range of
807 805 # valid pages
808 806 if self.page > self.last_page:
809 807 self.page = self.last_page
810 808 elif self.page < self.first_page:
811 809 self.page = self.first_page
812 810
813 811 # Note: the number of items on this page can be less than
814 812 # items_per_page if the last page is not full
815 813 self.first_item = max(0, (self.item_count) - (self.page *
816 814 items_per_page))
817 815 self.last_item = ((self.item_count - 1) - items_per_page *
818 816 (self.page - 1))
819 817
820 818 self.items = list(self.collection[self.first_item:self.last_item + 1])
821 819
822 820 # Links to previous and next page
823 821 if self.page > self.first_page:
824 822 self.previous_page = self.page - 1
825 823 else:
826 824 self.previous_page = None
827 825
828 826 if self.page < self.last_page:
829 827 self.next_page = self.page + 1
830 828 else:
831 829 self.next_page = None
832 830
833 831 # No items available
834 832 else:
835 833 self.first_page = None
836 834 self.page_count = 0
837 835 self.last_page = None
838 836 self.first_item = None
839 837 self.last_item = None
840 838 self.previous_page = None
841 839 self.next_page = None
842 840 self.items = []
843 841
844 842 # This is a subclass of the 'list' type. Initialise the list now.
845 843 list.__init__(self, reversed(self.items))
846 844
847 845
848 846 def changed_tooltip(nodes):
849 847 """
850 848 Generates a html string for changed nodes in changeset page.
851 849 It limits the output to 30 entries
852 850
853 851 :param nodes: LazyNodesGenerator
854 852 """
855 853 if nodes:
856 854 pref = ': <br/> '
857 855 suf = ''
858 856 if len(nodes) > 30:
859 857 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
860 858 return literal(pref + '<br/> '.join([safe_unicode(x.path)
861 859 for x in nodes[:30]]) + suf)
862 860 else:
863 861 return ': ' + _('No Files')
864 862
865 863
866 864 def repo_link(groups_and_repos):
867 865 """
868 866 Makes a breadcrumbs link to repo within a group
869 867 joins &raquo; on each group to create a fancy link
870 868
871 869 ex::
872 870 group >> subgroup >> repo
873 871
874 872 :param groups_and_repos:
875 873 """
876 874 groups, repo_name = groups_and_repos
877 875
878 876 if not groups:
879 877 return repo_name
880 878 else:
881 879 def make_link(group):
882 880 return link_to(group.name, url('repos_group_home',
883 881 group_name=group.group_name))
884 882 return literal(' &raquo; '.join(map(make_link, groups)) + \
885 883 " &raquo; " + repo_name)
886 884
887 885
888 886 def fancy_file_stats(stats):
889 887 """
890 888 Displays a fancy two colored bar for number of added/deleted
891 889 lines of code on file
892 890
893 891 :param stats: two element list of added/deleted lines of code
894 892 """
895 893
896 894 a, d, t = stats[0], stats[1], stats[0] + stats[1]
897 895 width = 100
898 896 unit = float(width) / (t or 1)
899 897
900 898 # needs > 9% of width to be visible or 0 to be hidden
901 899 a_p = max(9, unit * a) if a > 0 else 0
902 900 d_p = max(9, unit * d) if d > 0 else 0
903 901 p_sum = a_p + d_p
904 902
905 903 if p_sum > width:
906 904 #adjust the percentage to be == 100% since we adjusted to 9
907 905 if a_p > d_p:
908 906 a_p = a_p - (p_sum - width)
909 907 else:
910 908 d_p = d_p - (p_sum - width)
911 909
912 910 a_v = a if a > 0 else ''
913 911 d_v = d if d > 0 else ''
914 912
915 913 def cgen(l_type):
916 914 mapping = {'tr': 'top-right-rounded-corner-mid',
917 915 'tl': 'top-left-rounded-corner-mid',
918 916 'br': 'bottom-right-rounded-corner-mid',
919 917 'bl': 'bottom-left-rounded-corner-mid'}
920 918 map_getter = lambda x: mapping[x]
921 919
922 920 if l_type == 'a' and d_v:
923 921 #case when added and deleted are present
924 922 return ' '.join(map(map_getter, ['tl', 'bl']))
925 923
926 924 if l_type == 'a' and not d_v:
927 925 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
928 926
929 927 if l_type == 'd' and a_v:
930 928 return ' '.join(map(map_getter, ['tr', 'br']))
931 929
932 930 if l_type == 'd' and not a_v:
933 931 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
934 932
935 933 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
936 934 cgen('a'), a_p, a_v
937 935 )
938 936 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
939 937 cgen('d'), d_p, d_v
940 938 )
941 939 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
942 940
943 941
944 942 def urlify_text(text_):
945 943 import re
946 944
947 945 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
948 946 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
949 947
950 948 def url_func(match_obj):
951 949 url_full = match_obj.groups()[0]
952 950 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
953 951
954 952 return literal(url_pat.sub(url_func, text_))
955 953
956 954
957 955 def urlify_changesets(text_, repository):
958 956 """
959 957 Extract revision ids from changeset and make link from them
960 958
961 959 :param text_:
962 960 :param repository:
963 961 """
964 962 import re
965 963 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
966 964
967 965 def url_func(match_obj):
968 966 rev = match_obj.groups()[0]
969 967 pref = ''
970 968 if match_obj.group().startswith(' '):
971 969 pref = ' '
972 970 tmpl = (
973 971 '%(pref)s<a class="%(cls)s" href="%(url)s">'
974 972 '%(rev)s'
975 973 '</a>'
976 974 )
977 975 return tmpl % {
978 976 'pref': pref,
979 977 'cls': 'revision-link',
980 978 'url': url('changeset_home', repo_name=repository, revision=rev),
981 979 'rev': rev,
982 980 }
983 981
984 982 newtext = URL_PAT.sub(url_func, text_)
985 983
986 984 return newtext
987 985
988 986
989 987 def urlify_commit(text_, repository=None, link_=None):
990 988 """
991 989 Parses given text message and makes proper links.
992 990 issues are linked to given issue-server, and rest is a changeset link
993 991 if link_ is given, in other case it's a plain text
994 992
995 993 :param text_:
996 994 :param repository:
997 995 :param link_: changeset link
998 996 """
999 997 import re
1000 998 import traceback
1001 999
1002 1000 def escaper(string):
1003 1001 return string.replace('<', '&lt;').replace('>', '&gt;')
1004 1002
1005 1003 def linkify_others(t, l):
1006 1004 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1007 1005 links = []
1008 1006 for e in urls.split(t):
1009 1007 if not urls.match(e):
1010 1008 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1011 1009 else:
1012 1010 links.append(e)
1013 1011
1014 1012 return ''.join(links)
1015 1013
1016 1014 # urlify changesets - extrac revisions and make link out of them
1017 1015 newtext = urlify_changesets(escaper(text_), repository)
1018 1016
1019 1017 try:
1020 1018 conf = config['app_conf']
1021 1019
1022 1020 # allow multiple issue servers to be used
1023 1021 valid_indices = [
1024 1022 x.group(1)
1025 1023 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1026 1024 if x and 'issue_server_link%s' % x.group(1) in conf
1027 1025 and 'issue_prefix%s' % x.group(1) in conf
1028 1026 ]
1029 1027
1030 1028 log.debug('found issue server suffixes `%s` during valuation of: %s'
1031 1029 % (','.join(valid_indices), newtext))
1032 1030
1033 1031 for pattern_index in valid_indices:
1034 1032 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1035 1033 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1036 1034 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1037 1035
1038 1036 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1039 1037 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1040 1038 ISSUE_PREFIX))
1041 1039
1042 1040 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1043 1041
1044 1042 def url_func(match_obj):
1045 1043 pref = ''
1046 1044 if match_obj.group().startswith(' '):
1047 1045 pref = ' '
1048 1046
1049 1047 issue_id = ''.join(match_obj.groups())
1050 1048 tmpl = (
1051 1049 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1052 1050 '%(issue-prefix)s%(id-repr)s'
1053 1051 '</a>'
1054 1052 )
1055 1053 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1056 1054 if repository:
1057 1055 url = url.replace('{repo}', repository)
1058 1056 repo_name = repository.split(URL_SEP)[-1]
1059 1057 url = url.replace('{repo_name}', repo_name)
1060 1058
1061 1059 return tmpl % {
1062 1060 'pref': pref,
1063 1061 'cls': 'issue-tracker-link',
1064 1062 'url': url,
1065 1063 'id-repr': issue_id,
1066 1064 'issue-prefix': ISSUE_PREFIX,
1067 1065 'serv': ISSUE_SERVER_LNK,
1068 1066 }
1069 1067 newtext = URL_PAT.sub(url_func, newtext)
1070 1068 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1071 1069
1072 1070 # if we actually did something above
1073 1071 if link_:
1074 1072 # wrap not links into final link => link_
1075 1073 newtext = linkify_others(newtext, link_)
1076 1074
1077 1075 return literal(newtext)
1078 1076 except:
1079 1077 log.error(traceback.format_exc())
1080 1078 pass
1081 1079
1082 1080 return newtext
1083 1081
1084 1082
1085 1083 def rst(source):
1086 1084 return literal('<div class="rst-block">%s</div>' %
1087 1085 MarkupRenderer.rst(source))
1088 1086
1089 1087
1090 1088 def rst_w_mentions(source):
1091 1089 """
1092 1090 Wrapped rst renderer with @mention highlighting
1093 1091
1094 1092 :param source:
1095 1093 """
1096 1094 return literal('<div class="rst-block">%s</div>' %
1097 1095 MarkupRenderer.rst_with_mentions(source))
1098 1096
1099 1097
1100 1098 def changeset_status(repo, revision):
1101 1099 return ChangesetStatusModel().get_status(repo, revision)
1102 1100
1103 1101
1104 1102 def changeset_status_lbl(changeset_status):
1105 1103 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1106 1104
1107 1105
1108 1106 def get_permission_name(key):
1109 1107 return dict(Permission.PERMS).get(key)
@@ -1,422 +1,422
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 from inspect import isfunction
30 30
31 31 from mercurial.scmutil import revrange
32 32 from mercurial.node import nullrev
33 33
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.utils import action_logger
36 36 from rhodecode.lib.vcs.backends.base import EmptyChangeset
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.exceptions import HTTPLockedRC
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str, datetime_to_time
40 40 from rhodecode.model.db import Repository, User
41 41
42 42
43 43 def _get_scm_size(alias, root_path):
44 44
45 45 if not alias.startswith('.'):
46 46 alias += '.'
47 47
48 48 size_scm, size_root = 0, 0
49 49 for path, dirs, files in os.walk(root_path):
50 50 if path.find(alias) != -1:
51 51 for f in files:
52 52 try:
53 53 size_scm += os.path.getsize(os.path.join(path, f))
54 54 except OSError:
55 55 pass
56 56 else:
57 57 for f in files:
58 58 try:
59 59 size_root += os.path.getsize(os.path.join(path, f))
60 60 except OSError:
61 61 pass
62 62
63 63 size_scm_f = h.format_byte_size(size_scm)
64 64 size_root_f = h.format_byte_size(size_root)
65 65 size_total_f = h.format_byte_size(size_root + size_scm)
66 66
67 67 return size_scm_f, size_root_f, size_total_f
68 68
69 69
70 70 def repo_size(ui, repo, hooktype=None, **kwargs):
71 71 """
72 72 Presents size of repository after push
73 73
74 74 :param ui:
75 75 :param repo:
76 76 :param hooktype:
77 77 """
78 78
79 79 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
80 80
81 81 last_cs = repo[len(repo) - 1]
82 82
83 83 msg = ('Repository size .hg:%s repo:%s total:%s\n'
84 84 'Last revision is now r%s:%s\n') % (
85 85 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
86 86 )
87 87
88 88 sys.stdout.write(msg)
89 89
90 90
91 91 def pre_push(ui, repo, **kwargs):
92 92 # pre push function, currently used to ban pushing when
93 93 # repository is locked
94 94 try:
95 95 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
96 96 except:
97 97 rc_extras = {}
98 98 extras = dict(repo.ui.configitems('rhodecode_extras'))
99 99
100 100 if 'username' in extras:
101 101 username = extras['username']
102 102 repository = extras['repository']
103 103 scm = extras['scm']
104 104 locked_by = extras['locked_by']
105 105 elif 'username' in rc_extras:
106 106 username = rc_extras['username']
107 107 repository = rc_extras['repository']
108 108 scm = rc_extras['scm']
109 109 locked_by = rc_extras['locked_by']
110 110 else:
111 111 raise Exception('Missing data in repo.ui and os.environ')
112 112
113 113 usr = User.get_by_username(username)
114 114 if locked_by[0] and usr.user_id != int(locked_by[0]):
115 115 locked_by = User.get(locked_by[0]).username
116 116 raise HTTPLockedRC(repository, locked_by)
117 117
118 118
119 119 def pre_pull(ui, repo, **kwargs):
120 120 # pre push function, currently used to ban pushing when
121 121 # repository is locked
122 122 try:
123 123 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
124 124 except:
125 125 rc_extras = {}
126 126 extras = dict(repo.ui.configitems('rhodecode_extras'))
127 127 if 'username' in extras:
128 128 username = extras['username']
129 129 repository = extras['repository']
130 130 scm = extras['scm']
131 131 locked_by = extras['locked_by']
132 132 elif 'username' in rc_extras:
133 133 username = rc_extras['username']
134 134 repository = rc_extras['repository']
135 135 scm = rc_extras['scm']
136 136 locked_by = rc_extras['locked_by']
137 137 else:
138 138 raise Exception('Missing data in repo.ui and os.environ')
139 139
140 140 if locked_by[0]:
141 141 locked_by = User.get(locked_by[0]).username
142 142 raise HTTPLockedRC(repository, locked_by)
143 143
144 144
145 145 def log_pull_action(ui, repo, **kwargs):
146 146 """
147 147 Logs user last pull action
148 148
149 149 :param ui:
150 150 :param repo:
151 151 """
152 152 try:
153 153 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
154 154 except:
155 155 rc_extras = {}
156 156 extras = dict(repo.ui.configitems('rhodecode_extras'))
157 157 if 'username' in extras:
158 158 username = extras['username']
159 159 repository = extras['repository']
160 160 scm = extras['scm']
161 161 make_lock = extras['make_lock']
162 162 elif 'username' in rc_extras:
163 163 username = rc_extras['username']
164 164 repository = rc_extras['repository']
165 165 scm = rc_extras['scm']
166 166 make_lock = rc_extras['make_lock']
167 167 else:
168 168 raise Exception('Missing data in repo.ui and os.environ')
169 169 user = User.get_by_username(username)
170 170 action = 'pull'
171 171 action_logger(user, action, repository, extras['ip'], commit=True)
172 172 # extension hook call
173 173 from rhodecode import EXTENSIONS
174 174 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
175 175
176 176 if isfunction(callback):
177 177 kw = {}
178 178 kw.update(extras)
179 179 callback(**kw)
180 180
181 181 if make_lock is True:
182 182 Repository.lock(Repository.get_by_repo_name(repository), user.user_id)
183 183 #msg = 'Made lock on repo `%s`' % repository
184 184 #sys.stdout.write(msg)
185 185
186 186 return 0
187 187
188 188
189 189 def log_push_action(ui, repo, **kwargs):
190 190 """
191 191 Maps user last push action to new changeset id, from mercurial
192 192
193 193 :param ui:
194 194 :param repo: repo object containing the `ui` object
195 195 """
196 196
197 197 try:
198 198 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
199 199 except:
200 200 rc_extras = {}
201 201
202 202 extras = dict(repo.ui.configitems('rhodecode_extras'))
203 203 if 'username' in extras:
204 204 username = extras['username']
205 205 repository = extras['repository']
206 206 scm = extras['scm']
207 207 make_lock = extras['make_lock']
208 208 elif 'username' in rc_extras:
209 209 username = rc_extras['username']
210 210 repository = rc_extras['repository']
211 211 scm = rc_extras['scm']
212 212 make_lock = rc_extras['make_lock']
213 213 else:
214 214 raise Exception('Missing data in repo.ui and os.environ')
215 215
216 216 action = 'push' + ':%s'
217 217
218 218 if scm == 'hg':
219 219 node = kwargs['node']
220 220
221 221 def get_revs(repo, rev_opt):
222 222 if rev_opt:
223 223 revs = revrange(repo, rev_opt)
224 224
225 225 if len(revs) == 0:
226 226 return (nullrev, nullrev)
227 227 return (max(revs), min(revs))
228 228 else:
229 229 return (len(repo) - 1, 0)
230 230
231 231 stop, start = get_revs(repo, [node + ':'])
232 232 h = binascii.hexlify
233 233 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
234 234 elif scm == 'git':
235 235 revs = kwargs.get('_git_revs', [])
236 236 if '_git_revs' in kwargs:
237 237 kwargs.pop('_git_revs')
238 238
239 239 action = action % ','.join(revs)
240 240
241 241 action_logger(username, action, repository, extras['ip'], commit=True)
242 242
243 243 # extension hook call
244 244 from rhodecode import EXTENSIONS
245 245 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
246 246 if isfunction(callback):
247 247 kw = {'pushed_revs': revs}
248 248 kw.update(extras)
249 249 callback(**kw)
250 250
251 251 if make_lock is False:
252 252 Repository.unlock(Repository.get_by_repo_name(repository))
253 253 msg = 'Released lock on repo `%s`\n' % repository
254 254 sys.stdout.write(msg)
255 255
256 256 return 0
257 257
258 258
259 259 def log_create_repository(repository_dict, created_by, **kwargs):
260 260 """
261 261 Post create repository Hook. This is a dummy function for admins to re-use
262 262 if needed. It's taken from rhodecode-extensions module and executed
263 263 if present
264 264
265 265 :param repository: dict dump of repository object
266 266 :param created_by: username who created repository
267 267
268 268 available keys of repository_dict:
269 269
270 270 'repo_type',
271 271 'description',
272 272 'private',
273 273 'created_on',
274 274 'enable_downloads',
275 275 'repo_id',
276 276 'user_id',
277 277 'enable_statistics',
278 278 'clone_uri',
279 279 'fork_id',
280 280 'group_id',
281 281 'repo_name'
282 282
283 283 """
284 284 from rhodecode import EXTENSIONS
285 285 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
286 286 if isfunction(callback):
287 287 kw = {}
288 288 kw.update(repository_dict)
289 289 kw.update({'created_by': created_by})
290 290 kw.update(kwargs)
291 291 return callback(**kw)
292 292
293 293 return 0
294 294
295 295
296 296 def log_delete_repository(repository_dict, deleted_by, **kwargs):
297 297 """
298 298 Post delete repository Hook. This is a dummy function for admins to re-use
299 299 if needed. It's taken from rhodecode-extensions module and executed
300 300 if present
301 301
302 302 :param repository: dict dump of repository object
303 303 :param deleted_by: username who deleted the repository
304 304
305 305 available keys of repository_dict:
306 306
307 307 'repo_type',
308 308 'description',
309 309 'private',
310 310 'created_on',
311 311 'enable_downloads',
312 312 'repo_id',
313 313 'user_id',
314 314 'enable_statistics',
315 315 'clone_uri',
316 316 'fork_id',
317 317 'group_id',
318 318 'repo_name'
319 319
320 320 """
321 321 from rhodecode import EXTENSIONS
322 322 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
323 323 if isfunction(callback):
324 324 kw = {}
325 325 kw.update(repository_dict)
326 326 kw.update({'deleted_by': deleted_by,
327 327 'deleted_on': time.time()})
328 328 kw.update(kwargs)
329 329 return callback(**kw)
330 330
331 331 return 0
332 332
333 333
334 334 handle_git_pre_receive = (lambda repo_path, revs, env:
335 335 handle_git_receive(repo_path, revs, env, hook_type='pre'))
336 336 handle_git_post_receive = (lambda repo_path, revs, env:
337 337 handle_git_receive(repo_path, revs, env, hook_type='post'))
338 338
339 339
340 340 def handle_git_receive(repo_path, revs, env, hook_type='post'):
341 341 """
342 342 A really hacky method that is runned by git post-receive hook and logs
343 343 an push action together with pushed revisions. It's executed by subprocess
344 344 thus needs all info to be able to create a on the fly pylons enviroment,
345 345 connect to database and run the logging code. Hacky as sh*t but works.
346 346
347 347 :param repo_path:
348 348 :type repo_path:
349 349 :param revs:
350 350 :type revs:
351 351 :param env:
352 352 :type env:
353 353 """
354 354 from paste.deploy import appconfig
355 355 from sqlalchemy import engine_from_config
356 356 from rhodecode.config.environment import load_environment
357 357 from rhodecode.model import init_model
358 358 from rhodecode.model.db import RhodeCodeUi
359 359 from rhodecode.lib.utils import make_ui
360 360 extras = json.loads(env['RHODECODE_EXTRAS'])
361 361
362 362 path, ini_name = os.path.split(extras['config'])
363 363 conf = appconfig('config:%s' % ini_name, relative_to=path)
364 364 load_environment(conf.global_conf, conf.local_conf)
365 365
366 366 engine = engine_from_config(conf, 'sqlalchemy.db1.')
367 367 init_model(engine)
368 368
369 369 baseui = make_ui('db')
370 370 # fix if it's not a bare repo
371 371 if repo_path.endswith(os.sep + '.git'):
372 372 repo_path = repo_path[:-5]
373 373
374 374 repo = Repository.get_by_full_path(repo_path)
375 375 if not repo:
376 376 raise OSError('Repository %s not found in database'
377 377 % (safe_str(repo_path)))
378 378
379 379 _hooks = dict(baseui.configitems('hooks')) or {}
380 380
381 381 for k, v in extras.items():
382 382 baseui.setconfig('rhodecode_extras', k, v)
383 383 repo = repo.scm_instance
384 384 repo.ui = baseui
385 385
386 386 if hook_type == 'pre':
387 387 pre_push(baseui, repo)
388 388
389 389 # if push hook is enabled via web interface
390 390 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
391 391
392 392 rev_data = []
393 393 for l in revs:
394 394 old_rev, new_rev, ref = l.split(' ')
395 395 _ref_data = ref.split('/')
396 396 if _ref_data[1] in ['tags', 'heads']:
397 397 rev_data.append({'old_rev': old_rev,
398 398 'new_rev': new_rev,
399 399 'ref': ref,
400 400 'type': _ref_data[1],
401 401 'name': _ref_data[2].strip()})
402 402
403 403 git_revs = []
404 404 for push_ref in rev_data:
405 405 _type = push_ref['type']
406 406 if _type == 'heads':
407 407 if push_ref['old_rev'] == EmptyChangeset().raw_id:
408 408 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
409 409 heads = repo.run_git_command(cmd)[0]
410 410 heads = heads.replace(push_ref['ref'], '')
411 411 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
412 412 heads.splitlines()))
413 413 cmd = (('log %(new_rev)s' % push_ref) +
414 414 ' --reverse --pretty=format:"%H" --not ' + heads)
415 415 else:
416 416 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
417 417 ' --reverse --pretty=format:"%H"')
418 418 git_revs += repo.run_git_command(cmd)[0].splitlines()
419 419 elif _type == 'tags':
420 420 git_revs += [push_ref['name']]
421 421
422 422 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,342 +1,338
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Settings administration')} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_('Admin'),h.url('admin_home'))} &raquo; ${_('Settings')}
10 10 </%def>
11 11
12 12 <%def name="page_nav()">
13 13 ${self.menu('admin')}
14 14 </%def>
15 15
16 16 <%def name="main()">
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 <!-- end box / title -->
23 23
24 24 <h3>${_('Remap and rescan repositories')}</h3>
25 25 ${h.form(url('admin_setting', setting_id='mapping'),method='put')}
26 26 <div class="form">
27 27 <!-- fields -->
28 28
29 29 <div class="fields">
30 30 <div class="field">
31 31 <div class="label label-checkbox">
32 32 <label for="destroy">${_('rescan option')}:</label>
33 33 </div>
34 34 <div class="checkboxes">
35 35 <div class="checkbox">
36 36 ${h.checkbox('destroy',True)}
37 37 <label for="destroy">
38 38 <span class="tooltip" title="${h.tooltip(_('In case a repository was deleted from filesystem and there are leftovers in the database check this option to scan obsolete data in database and remove it.'))}">
39 39 ${_('destroy old data')}</span> </label>
40 40 </div>
41 41 <span class="help-block">${_('Rescan repositories location for new repositories. Also deletes obsolete if `destroy` flag is checked ')}</span>
42 42 </div>
43 43 </div>
44 44
45 45 <div class="buttons">
46 46 ${h.submit('rescan',_('Rescan repositories'),class_="ui-btn large")}
47 47 </div>
48 48 </div>
49 49 </div>
50 50 ${h.end_form()}
51 51
52 52 <h3>${_('Whoosh indexing')}</h3>
53 53 ${h.form(url('admin_setting', setting_id='whoosh'),method='put')}
54 54 <div class="form">
55 55 <!-- fields -->
56 56
57 57 <div class="fields">
58 58 <div class="field">
59 59 <div class="label label-checkbox">
60 60 <label>${_('index build option')}:</label>
61 61 </div>
62 62 <div class="checkboxes">
63 63 <div class="checkbox">
64 64 ${h.checkbox('full_index',True)}
65 65 <label for="full_index">${_('build from scratch')}</label>
66 66 </div>
67 67 </div>
68 68 </div>
69 69
70 70 <div class="buttons">
71 71 ${h.submit('reindex',_('Reindex'),class_="ui-btn large")}
72 72 </div>
73 73 </div>
74 74 </div>
75 75 ${h.end_form()}
76 76
77 77 <h3>${_('Global application settings')}</h3>
78 78 ${h.form(url('admin_setting', setting_id='global'),method='put')}
79 79 <div class="form">
80 80 <!-- fields -->
81 81
82 82 <div class="fields">
83 83
84 84 <div class="field">
85 85 <div class="label">
86 86 <label for="rhodecode_title">${_('Application name')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 ${h.text('rhodecode_title',size=30)}
90 90 </div>
91 91 </div>
92 92
93 93 <div class="field">
94 94 <div class="label">
95 95 <label for="rhodecode_realm">${_('Realm text')}:</label>
96 96 </div>
97 97 <div class="input">
98 98 ${h.text('rhodecode_realm',size=30)}
99 99 </div>
100 100 </div>
101 101
102 102 <div class="field">
103 103 <div class="label">
104 104 <label for="rhodecode_ga_code">${_('GA code')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 ${h.text('rhodecode_ga_code',size=30)}
108 108 </div>
109 109 </div>
110 110
111 111 <div class="buttons">
112 112 ${h.submit('save',_('Save settings'),class_="ui-btn large")}
113 113 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
114 114 </div>
115 115 </div>
116 116 </div>
117 117 ${h.end_form()}
118 118
119 119 <h3>${_('Visualisation settings')}</h3>
120 120 ${h.form(url('admin_setting', setting_id='visual'),method='put')}
121 121 <div class="form">
122 122 <!-- fields -->
123 123
124 124 <div class="fields">
125 125 <div class="field">
126 126 <div class="label label-checkbox">
127 127 <label>${_('General')}:</label>
128 128 </div>
129 129 <div class="checkboxes">
130 130 <div class="checkbox">
131 131 ${h.checkbox('rhodecode_lightweight_dashboard','True')}
132 132 <label for="rhodecode_lightweight_dashboard">${_('Use lightweight dashboard')}</label>
133 133 </div>
134 <div class="checkbox">
135 ${h.checkbox('rhodecode_lightweight_journal','True')}
136 <label for="rhodecode_lightweight_journal">${_('Use lightweight journal')}</label>
137 </div>
138 134 </div>
139 135 </div>
140 136
141 137 <div class="field">
142 138 <div class="label label-checkbox">
143 139 <label>${_('Icons')}:</label>
144 140 </div>
145 141 <div class="checkboxes">
146 142 <div class="checkbox">
147 143 ${h.checkbox('rhodecode_show_public_icon','True')}
148 144 <label for="rhodecode_show_public_icon">${_('Show public repo icon on repositories')}</label>
149 145 </div>
150 146 <div class="checkbox">
151 147 ${h.checkbox('rhodecode_show_private_icon','True')}
152 148 <label for="rhodecode_show_private_icon">${_('Show private repo icon on repositories')}</label>
153 149 </div>
154 150 </div>
155 151 </div>
156 152
157 153 <div class="field">
158 154 <div class="label label-checkbox">
159 155 <label>${_('Meta-Tagging')}:</label>
160 156 </div>
161 157 <div class="checkboxes">
162 158 <div class="checkbox">
163 159 ${h.checkbox('rhodecode_stylify_metatags','True')}
164 160 <label for="rhodecode_stylify_metatags">${_('Stylify recognised metatags:')}</label>
165 161 </div>
166 162 <div style="padding-left: 20px;">
167 163 <ul> <!-- Fix style here -->
168 164 <li>[featured] <span class="metatag" tag="featured">featured</span></li>
169 165 <li>[stale] <span class="metatag" tag="stale">stale</span></li>
170 166 <li>[dead] <span class="metatag" tag="dead">dead</span></li>
171 167 <li>[lang =&gt; lang] <span class="metatag" tag="lang" >lang</span></li>
172 168 <li>[license =&gt; License] <span class="metatag" tag="license"><a href="http://www.opensource.org/licenses/License" >License</a></span></li>
173 169 <li>[requires =&gt; Repo] <span class="metatag" tag="requires" >requires =&gt; <a href="#" >Repo</a></span></li>
174 170 <li>[recommends =&gt; Repo] <span class="metatag" tag="recommends" >recommends =&gt; <a href="#" >Repo</a></span></li>
175 171 <li>[see =&gt; URI] <span class="metatag" tag="see">see =&gt; <a href="#">URI</a> </span></li>
176 172 </ul>
177 173 </div>
178 174 </div>
179 175 </div>
180 176
181 177 <div class="buttons">
182 178 ${h.submit('save',_('Save settings'),class_="ui-btn large")}
183 179 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
184 180 </div>
185 181
186 182 </div>
187 183 </div>
188 184 ${h.end_form()}
189 185
190 186
191 187 <h3>${_('VCS settings')}</h3>
192 188 ${h.form(url('admin_setting', setting_id='vcs'),method='put')}
193 189 <div class="form">
194 190 <!-- fields -->
195 191
196 192 <div class="fields">
197 193
198 194 <div class="field">
199 195 <div class="label label-checkbox">
200 196 <label>${_('Web')}:</label>
201 197 </div>
202 198 <div class="checkboxes">
203 199 <div class="checkbox">
204 200 ${h.checkbox('web_push_ssl', 'True')}
205 201 <label for="web_push_ssl">${_('require ssl for vcs operations')}</label>
206 202 </div>
207 203 <span class="help-block">${_('RhodeCode will require SSL for pushing or pulling. If SSL is missing it will return HTTP Error 406: Not Acceptable')}</span>
208 204 </div>
209 205 </div>
210 206
211 207 <div class="field">
212 208 <div class="label label-checkbox">
213 209 <label>${_('Hooks')}:</label>
214 210 </div>
215 211 <div class="checkboxes">
216 212 <div class="checkbox">
217 213 ${h.checkbox('hooks_changegroup_update','True')}
218 214 <label for="hooks_changegroup_update">${_('Update repository after push (hg update)')}</label>
219 215 </div>
220 216 <div class="checkbox">
221 217 ${h.checkbox('hooks_changegroup_repo_size','True')}
222 218 <label for="hooks_changegroup_repo_size">${_('Show repository size after push')}</label>
223 219 </div>
224 220 <div class="checkbox">
225 221 ${h.checkbox('hooks_changegroup_push_logger','True')}
226 222 <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label>
227 223 </div>
228 224 <div class="checkbox">
229 225 ${h.checkbox('hooks_outgoing_pull_logger','True')}
230 226 <label for="hooks_outgoing_pull_logger">${_('Log user pull commands')}</label>
231 227 </div>
232 228 </div>
233 229 <div class="input" style="margin-top:10px">
234 230 ${h.link_to(_('advanced setup'),url('admin_edit_setting',setting_id='hooks'),class_="ui-btn")}
235 231 </div>
236 232 </div>
237 233 <div class="field">
238 234 <div class="label label-checkbox">
239 235 <label>${_('Mercurial Extensions')}:</label>
240 236 </div>
241 237 <div class="checkboxes">
242 238 <div class="checkbox">
243 239 ${h.checkbox('extensions_largefiles','True')}
244 240 <label for="extensions_hgsubversion">${_('largefiles extensions')}</label>
245 241 </div>
246 242 <div class="checkbox">
247 243 ${h.checkbox('extensions_hgsubversion','True')}
248 244 <label for="extensions_hgsubversion">${_('hgsubversion extensions')}</label>
249 245 </div>
250 246 <span class="help-block">${_('Requires hgsubversion library installed. Allows clonning from svn remote locations')}</span>
251 247 ##<div class="checkbox">
252 248 ## ${h.checkbox('extensions_hggit','True')}
253 249 ## <label for="extensions_hggit">${_('hg-git extensions')}</label>
254 250 ##</div>
255 251 ##<span class="help-block">${_('Requires hg-git library installed. Allows clonning from git remote locations')}</span>
256 252 </div>
257 253 </div>
258 254 <div class="field">
259 255 <div class="label">
260 256 <label for="paths_root_path">${_('Repositories location')}:</label>
261 257 </div>
262 258 <div class="input">
263 259 ${h.text('paths_root_path',size=30,readonly="readonly")}
264 260 <span id="path_unlock" class="tooltip"
265 261 title="${h.tooltip(_('This a crucial application setting. If you are really sure you need to change this, you must restart application in order to make this setting take effect. Click this label to unlock.'))}">
266 262 ${_('unlock')}</span>
267 263 <span class="help-block">${_('Location where repositories are stored. After changing this value a restart, and rescan is required')}</span>
268 264 </div>
269 265 </div>
270 266
271 267 <div class="buttons">
272 268 ${h.submit('save',_('Save settings'),class_="ui-btn large")}
273 269 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
274 270 </div>
275 271 </div>
276 272 </div>
277 273 ${h.end_form()}
278 274
279 275 <script type="text/javascript">
280 276 YAHOO.util.Event.onDOMReady(function(){
281 277 YAHOO.util.Event.addListener('path_unlock','click',function(){
282 278 YAHOO.util.Dom.get('paths_root_path').removeAttribute('readonly');
283 279 });
284 280 });
285 281 </script>
286 282
287 283 <h3>${_('Test Email')}</h3>
288 284 ${h.form(url('admin_setting', setting_id='email'),method='put')}
289 285 <div class="form">
290 286 <!-- fields -->
291 287
292 288 <div class="fields">
293 289 <div class="field">
294 290 <div class="label">
295 291 <label for="test_email">${_('Email to')}:</label>
296 292 </div>
297 293 <div class="input">
298 294 ${h.text('test_email',size=30)}
299 295 </div>
300 296 </div>
301 297
302 298 <div class="buttons">
303 299 ${h.submit('send',_('Send'),class_="ui-btn large")}
304 300 </div>
305 301 </div>
306 302 </div>
307 303 ${h.end_form()}
308 304
309 305 <h3>${_('System Info and Packages')}</h3>
310 306 <div class="form">
311 307 <div>
312 308 <h5 id="expand_modules" style="cursor: pointer">&darr; ${_('show')} &darr;</h5>
313 309 </div>
314 310 <div id="expand_modules_table" style="display:none">
315 311 <h5>Python - ${c.py_version}</h5>
316 312 <h5>System - ${c.platform}</h5>
317 313
318 314 <table class="table" style="margin:0px 0px 0px 20px">
319 315 <colgroup>
320 316 <col style="width:220px">
321 317 </colgroup>
322 318 <tbody>
323 319 %for key, value in c.modules:
324 320 <tr>
325 321 <th style="text-align: right;padding-right:5px;">${key}</th>
326 322 <td>${value}</td>
327 323 </tr>
328 324 %endfor
329 325 </tbody>
330 326 </table>
331 327 </div>
332 328 </div>
333 329
334 330 <script type="text/javascript">
335 331 YUE.on('expand_modules','click',function(e){
336 332 YUD.setStyle('expand_modules_table','display','');
337 333 YUD.setStyle('expand_modules','display','none');
338 334 })
339 335 </script>
340 336
341 337 </div>
342 338 </%def>
@@ -1,199 +1,207
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_(u'Home'),h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 %if c.pull_request.is_closed():
23 23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 24 %endif
25 25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
26 26
27 27 <div class="form">
28 28 <div id="summary" class="fields">
29 29 <div class="field">
30 30 <div class="label-summary">
31 31 <label>${_('Status')}:</label>
32 32 </div>
33 33 <div class="input">
34 34 <div class="changeset-status-container" style="float:none;clear:both">
35 35 %if c.current_changeset_status:
36 36 <div title="${_('Pull request status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
37 37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
38 38 %endif
39 39 </div>
40 40 </div>
41 41 </div>
42 42 <div class="field">
43 43 <div class="label-summary">
44 44 <label>${_('Still not reviewed by')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 % if len(c.pull_request_pending_reviewers) > 0:
48 48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
49 49 %else:
50 50 <div>${_('pull request was reviewed by all reviewers')}</div>
51 51 %endif
52 52 </div>
53 53 </div>
54 54 </div>
55 55 </div>
56 56 <div style="white-space:pre-wrap;padding:3px 3px 5px 20px">${h.literal(c.pull_request.description)}</div>
57 57 <div style="padding:4px 4px 10px 20px">
58 58 <div>${_('Created on')}: ${h.fmt_date(c.pull_request.created_on)}</div>
59 59 </div>
60 60
61 61 <div style="min-height:160px">
62 62 ##DIFF
63 63 <div class="table" style="float:left;clear:none">
64 64 <div id="body" class="diffblock">
65 65 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
66 66 </div>
67 67 <div id="changeset_compare_view_content">
68 68 ##CS
69 69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
70 70 <%include file="/compare/compare_cs.html" />
71 71
72 72 ## FILES
73 <div id="affected_files">
74 % if c.files:
73 75 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
74 76 <div class="cs_files">
75 77 %for fid, change, f, stat in c.files:
76 78 <div class="cs_${change}">
77 79 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
78 80 <div class="changes">${h.fancy_file_stats(stat)}</div>
79 81 </div>
80 82 %endfor
81 83 </div>
84 %else:
85 <div class="ui-btn" style="text-align: center;margin-top:5px">${_('Click to load diff details')}</div>
86 %endif
87 </div>
82 88 </div>
83 89 </div>
84 90 ## REVIEWERS
85 91 <div style="float:left; border-left:1px dashed #eee">
86 92 <h4>${_('Pull request reviewers')}</h4>
87 93 <div id="reviewers" style="padding:0px 0px 0px 15px">
88 94 ## members goes here !
89 95 <div class="group_members_wrap">
90 96 <ul id="review_members" class="group_members">
91 97 %for member,status in c.pull_request_reviewers:
92 98 <li id="reviewer_${member.user_id}">
93 99 <div class="reviewers_member">
94 100 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
95 101 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
96 102 </div>
97 103 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
98 104 <div style="float:left">${member.full_name} (${_('owner')})</div>
99 105 <input type="hidden" value="${member.user_id}" name="review_members" />
100 106 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id):
101 107 <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
102 108 %endif
103 109 </div>
104 110 </li>
105 111 %endfor
106 112 </ul>
107 113 </div>
108 114 %if not c.pull_request.is_closed():
109 115 <div class='ac'>
110 116 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
111 117 <div class="reviewer_ac">
112 118 ${h.text('user', class_='yui-ac-input')}
113 119 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
114 120 <div id="reviewers_container"></div>
115 121 </div>
116 122 <div style="padding:0px 10px">
117 123 <span id="update_pull_request" class="ui-btn xsmall">${_('save')}</span>
118 124 </div>
119 125 %endif
120 126 </div>
121 127 %endif
122 128 </div>
123 129 </div>
124 130 </div>
125 131 <script>
126 132 var _USERS_AC_DATA = ${c.users_array|n};
127 133 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
128 134 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
129 135 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
130 136 AJAX_UPDATE_PULLREQUEST = "${url('pullrequest_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}"
131 137 </script>
132 138
133 139 ## diff block
140 <div id="diff_block_container" style="clear:both;">
134 141 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
135 142 %for fid, change, f, stat in c.files:
136 143 ${diff_block.diff_block_simple([c.changes[fid]])}
137 144 %endfor
145 </div>
138 146
139 147 ## template for inline comment form
140 148 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
141 149 ${comment.comment_inline_form()}
142 150
143 151 ## render comments and inlines
144 152 ${comment.generate_comments()}
145 153
146 154 % if not c.pull_request.is_closed():
147 155 ## main comment form and it status
148 156 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
149 157 pull_request_id=c.pull_request.pull_request_id),
150 158 c.current_changeset_status,
151 159 close_btn=True)}
152 160 %endif
153 161
154 162 <script type="text/javascript">
155 163 YUE.onDOMReady(function(){
156 164 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
157 165
158 166 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
159 167 var show = 'none';
160 168 var target = e.currentTarget;
161 169 if(target.checked){
162 170 var show = ''
163 171 }
164 172 var boxid = YUD.getAttribute(target,'id_for');
165 173 var comments = YUQ('#{0} .inline-comments'.format(boxid));
166 174 for(c in comments){
167 175 YUD.setStyle(comments[c],'display',show);
168 176 }
169 177 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
170 178 for(c in btns){
171 179 YUD.setStyle(btns[c],'display',show);
172 180 }
173 181 })
174 182
175 183 YUE.on(YUQ('.line'),'click',function(e){
176 184 var tr = e.currentTarget;
177 185 injectInlineForm(tr);
178 186 });
179 187
180 188 // inject comments into they proper positions
181 189 var file_comments = YUQ('.inline-comment-placeholder');
182 190 renderInlineComments(file_comments);
183 191
184 192 YUE.on(YUD.get('update_pull_request'),'click',function(e){
185 193
186 194 var reviewers_ids = [];
187 195 var ids = YUQ('#review_members input');
188 196 for(var i=0; i<ids.length;i++){
189 197 var id = ids[i].value
190 198 reviewers_ids.push(id);
191 199 }
192 200 updateReviewers(reviewers_ids);
193 201 })
194 202 })
195 203 </script>
196 204
197 205 </div>
198 206
199 207 </%def>
General Comments 0
You need to be logged in to leave comments. Login now