##// END OF EJS Templates
controllers: remove old auth_token checks - it was only partial CSRF protection
Mads Kiilerich -
r4990:959a9fa7 default
parent child Browse files
Show More
@@ -1,681 +1,674 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.repos
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Repositories controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 7, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 from formencode import htmlfill
32 32 from webob.exc import HTTPInternalServerError, HTTPForbidden, HTTPNotFound
33 33 from pylons import request, tmpl_context as c, url
34 34 from pylons.controllers.util import redirect
35 35 from pylons.i18n.translation import _
36 36 from sqlalchemy.sql.expression import func
37 37
38 38 from kallithea.lib import helpers as h
39 39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
40 40 HasRepoPermissionAllDecorator, NotAnonymous,HasPermissionAny, \
41 41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator
42 42 from kallithea.lib.base import BaseRepoController, render
43 43 from kallithea.lib.utils import action_logger, repo_name_slug, jsonify
44 from kallithea.lib.helpers import get_token
45 44 from kallithea.lib.vcs import RepositoryError
46 45 from kallithea.model.meta import Session
47 46 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup,\
48 47 Setting, RepositoryField
49 48 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
50 49 from kallithea.model.scm import ScmModel, RepoGroupList, RepoList
51 50 from kallithea.model.repo import RepoModel
52 51 from kallithea.lib.compat import json
53 52 from kallithea.lib.exceptions import AttachedForksError
54 53 from kallithea.lib.utils2 import safe_int
55 54
56 55 log = logging.getLogger(__name__)
57 56
58 57
59 58 class ReposController(BaseRepoController):
60 59 """
61 60 REST Controller styled on the Atom Publishing Protocol"""
62 61 # To properly map this controller, ensure your config/routing.py
63 62 # file has a resource setup:
64 63 # map.resource('repo', 'repos')
65 64
66 65 @LoginRequired()
67 66 def __before__(self):
68 67 super(ReposController, self).__before__()
69 68
70 69 def _load_repo(self, repo_name):
71 70 repo_obj = Repository.get_by_repo_name(repo_name)
72 71
73 72 if repo_obj is None:
74 73 h.not_mapped_error(repo_name)
75 74 return redirect(url('repos'))
76 75
77 76 return repo_obj
78 77
79 78 def __load_defaults(self, repo=None):
80 79 acl_groups = RepoGroupList(RepoGroup.query().all(),
81 80 perm_set=['group.write', 'group.admin'])
82 81 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
83 82 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
84 83
85 84 # in case someone no longer have a group.write access to a repository
86 85 # pre fill the list with this entry, we don't care if this is the same
87 86 # but it will allow saving repo data properly.
88 87
89 88 repo_group = None
90 89 if repo:
91 90 repo_group = repo.group
92 91 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
93 92 c.repo_groups_choices.append(unicode(repo_group.group_id))
94 93 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
95 94
96 95 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
97 96 c.landing_revs_choices = choices
98 97
99 98 def __load_data(self, repo_name=None):
100 99 """
101 100 Load defaults settings for edit, and update
102 101
103 102 :param repo_name:
104 103 """
105 104 c.repo_info = self._load_repo(repo_name)
106 105 self.__load_defaults(c.repo_info)
107 106
108 107 ##override defaults for exact repo info here git/hg etc
109 108 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
110 109 c.landing_revs_choices = choices
111 110 defaults = RepoModel()._get_defaults(repo_name)
112 111
113 112 return defaults
114 113
115 114 def index(self, format='html'):
116 115 """GET /repos: All items in the collection"""
117 116 # url('repos')
118 117 _list = Repository.query()\
119 118 .order_by(func.lower(Repository.repo_name))\
120 119 .all()
121 120
122 121 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
123 122 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
124 123 admin=True,
125 124 super_user_actions=True)
126 125 #json used to render the grid
127 126 c.data = json.dumps(repos_data)
128 127
129 128 return render('admin/repos/repos.html')
130 129
131 130 @NotAnonymous()
132 131 def create(self):
133 132 """
134 133 POST /repos: Create a new item"""
135 134 # url('repos')
136 135
137 136 self.__load_defaults()
138 137 form_result = {}
139 138 task_id = None
140 139 try:
141 140 # CanWriteToGroup validators checks permissions of this POST
142 141 form_result = RepoForm(repo_groups=c.repo_groups_choices,
143 142 landing_revs=c.landing_revs_choices)()\
144 143 .to_python(dict(request.POST))
145 144
146 145 # create is done sometimes async on celery, db transaction
147 146 # management is handled there.
148 147 task = RepoModel().create(form_result, self.authuser.user_id)
149 148 from celery.result import BaseAsyncResult
150 149 if isinstance(task, BaseAsyncResult):
151 150 task_id = task.task_id
152 151 except formencode.Invalid, errors:
153 152 return htmlfill.render(
154 153 render('admin/repos/repo_add.html'),
155 154 defaults=errors.value,
156 155 errors=errors.error_dict or {},
157 156 prefix_error=False,
158 157 force_defaults=False,
159 158 encoding="UTF-8")
160 159
161 160 except Exception:
162 161 log.error(traceback.format_exc())
163 162 msg = (_('Error creating repository %s')
164 163 % form_result.get('repo_name'))
165 164 h.flash(msg, category='error')
166 165 return redirect(url('home'))
167 166
168 167 return redirect(h.url('repo_creating_home',
169 168 repo_name=form_result['repo_name_full'],
170 169 task_id=task_id))
171 170
172 171 @NotAnonymous()
173 172 def create_repository(self):
174 173 """GET /_admin/create_repository: Form to create a new item"""
175 174 new_repo = request.GET.get('repo', '')
176 175 parent_group = request.GET.get('parent_group')
177 176 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
178 177 #you're not super admin nor have global create permissions,
179 178 #but maybe you have at least write permission to a parent group ?
180 179 _gr = RepoGroup.get(parent_group)
181 180 gr_name = _gr.group_name if _gr else None
182 181 # create repositories with write permission on group is set to true
183 182 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
184 183 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
185 184 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
186 185 if not (group_admin or (group_write and create_on_write)):
187 186 raise HTTPForbidden
188 187
189 188 acl_groups = RepoGroupList(RepoGroup.query().all(),
190 189 perm_set=['group.write', 'group.admin'])
191 190 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
192 191 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
193 192 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
194 193
195 194 c.new_repo = repo_name_slug(new_repo)
196 195
197 196 ## apply the defaults from defaults page
198 197 defaults = Setting.get_default_repo_settings(strip_prefix=True)
199 198 if parent_group:
200 199 defaults.update({'repo_group': parent_group})
201 200
202 201 return htmlfill.render(
203 202 render('admin/repos/repo_add.html'),
204 203 defaults=defaults,
205 204 errors={},
206 205 prefix_error=False,
207 206 encoding="UTF-8",
208 207 force_defaults=False)
209 208
210 209 @LoginRequired()
211 210 @NotAnonymous()
212 211 def repo_creating(self, repo_name):
213 212 c.repo = repo_name
214 213 c.task_id = request.GET.get('task_id')
215 214 if not c.repo:
216 215 raise HTTPNotFound()
217 216 return render('admin/repos/repo_creating.html')
218 217
219 218 @LoginRequired()
220 219 @NotAnonymous()
221 220 @jsonify
222 221 def repo_check(self, repo_name):
223 222 c.repo = repo_name
224 223 task_id = request.GET.get('task_id')
225 224
226 225 if task_id and task_id not in ['None']:
227 226 from kallithea import CELERY_ON
228 227 from celery.result import AsyncResult
229 228 if CELERY_ON:
230 229 task = AsyncResult(task_id)
231 230 if task.failed():
232 231 raise HTTPInternalServerError(task.traceback)
233 232
234 233 repo = Repository.get_by_repo_name(repo_name)
235 234 if repo and repo.repo_state == Repository.STATE_CREATED:
236 235 if repo.clone_uri:
237 236 clone_uri = repo.clone_uri_hidden
238 237 h.flash(_('Created repository %s from %s')
239 238 % (repo.repo_name, clone_uri), category='success')
240 239 else:
241 240 repo_url = h.link_to(repo.repo_name,
242 241 h.url('summary_home',
243 242 repo_name=repo.repo_name))
244 243 fork = repo.fork
245 244 if fork:
246 245 fork_name = fork.repo_name
247 246 h.flash(h.literal(_('Forked repository %s as %s')
248 247 % (fork_name, repo_url)), category='success')
249 248 else:
250 249 h.flash(h.literal(_('Created repository %s') % repo_url),
251 250 category='success')
252 251 return {'result': True}
253 252 return {'result': False}
254 253
255 254 @HasRepoPermissionAllDecorator('repository.admin')
256 255 def update(self, repo_name):
257 256 """
258 257 PUT /repos/repo_name: Update an existing item"""
259 258 # Forms posted to this method should contain a hidden field:
260 259 # <input type="hidden" name="_method" value="PUT" />
261 260 # Or using helpers:
262 261 # h.form(url('repo', repo_name=ID),
263 262 # method='put')
264 263 # url('repo', repo_name=ID)
265 264 c.repo_info = self._load_repo(repo_name)
266 265 c.active = 'settings'
267 266 c.repo_fields = RepositoryField.query()\
268 267 .filter(RepositoryField.repository == c.repo_info).all()
269 268 self.__load_defaults(c.repo_info)
270 269
271 270 repo_model = RepoModel()
272 271 changed_name = repo_name
273 272 #override the choices with extracted revisions !
274 273 choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo_name)
275 274 c.landing_revs_choices = choices
276 275 repo = Repository.get_by_repo_name(repo_name)
277 276 old_data = {
278 277 'repo_name': repo_name,
279 278 'repo_group': repo.group.get_dict() if repo.group else {},
280 279 'repo_type': repo.repo_type,
281 280 }
282 281 _form = RepoForm(edit=True, old_data=old_data,
283 282 repo_groups=c.repo_groups_choices,
284 283 landing_revs=c.landing_revs_choices)()
285 284
286 285 try:
287 286 form_result = _form.to_python(dict(request.POST))
288 287 repo = repo_model.update(repo_name, **form_result)
289 288 ScmModel().mark_for_invalidation(repo_name)
290 289 h.flash(_('Repository %s updated successfully') % repo_name,
291 290 category='success')
292 291 changed_name = repo.repo_name
293 292 action_logger(self.authuser, 'admin_updated_repo',
294 293 changed_name, self.ip_addr, self.sa)
295 294 Session().commit()
296 295 except formencode.Invalid, errors:
297 296 defaults = self.__load_data(repo_name)
298 297 defaults.update(errors.value)
299 298 return htmlfill.render(
300 299 render('admin/repos/repo_edit.html'),
301 300 defaults=defaults,
302 301 errors=errors.error_dict or {},
303 302 prefix_error=False,
304 303 encoding="UTF-8",
305 304 force_defaults=False)
306 305
307 306 except Exception:
308 307 log.error(traceback.format_exc())
309 308 h.flash(_('Error occurred during update of repository %s') \
310 309 % repo_name, category='error')
311 310 return redirect(url('edit_repo', repo_name=changed_name))
312 311
313 312 @HasRepoPermissionAllDecorator('repository.admin')
314 313 def delete(self, repo_name):
315 314 """
316 315 DELETE /repos/repo_name: Delete an existing item"""
317 316 # Forms posted to this method should contain a hidden field:
318 317 # <input type="hidden" name="_method" value="DELETE" />
319 318 # Or using helpers:
320 319 # h.form(url('repo', repo_name=ID),
321 320 # method='delete')
322 321 # url('repo', repo_name=ID)
323 322
324 323 repo_model = RepoModel()
325 324 repo = repo_model.get_by_repo_name(repo_name)
326 325 if not repo:
327 326 h.not_mapped_error(repo_name)
328 327 return redirect(url('repos'))
329 328 try:
330 329 _forks = repo.forks.count()
331 330 handle_forks = None
332 331 if _forks and request.POST.get('forks'):
333 332 do = request.POST['forks']
334 333 if do == 'detach_forks':
335 334 handle_forks = 'detach'
336 335 h.flash(_('Detached %s forks') % _forks, category='success')
337 336 elif do == 'delete_forks':
338 337 handle_forks = 'delete'
339 338 h.flash(_('Deleted %s forks') % _forks, category='success')
340 339 repo_model.delete(repo, forks=handle_forks)
341 340 action_logger(self.authuser, 'admin_deleted_repo',
342 341 repo_name, self.ip_addr, self.sa)
343 342 ScmModel().mark_for_invalidation(repo_name)
344 343 h.flash(_('Deleted repository %s') % repo_name, category='success')
345 344 Session().commit()
346 345 except AttachedForksError:
347 346 h.flash(_('Cannot delete %s it still contains attached forks')
348 347 % repo_name, category='warning')
349 348
350 349 except Exception:
351 350 log.error(traceback.format_exc())
352 351 h.flash(_('An error occurred during deletion of %s') % repo_name,
353 352 category='error')
354 353
355 354 if repo.group:
356 355 return redirect(url('repos_group_home', group_name=repo.group.group_name))
357 356 return redirect(url('repos'))
358 357
359 358 @HasPermissionAllDecorator('hg.admin')
360 359 def show(self, repo_name, format='html'):
361 360 """GET /repos/repo_name: Show a specific item"""
362 361 # url('repo', repo_name=ID)
363 362
364 363 @HasRepoPermissionAllDecorator('repository.admin')
365 364 def edit(self, repo_name):
366 365 """GET /repo_name/settings: Form to edit an existing item"""
367 366 # url('edit_repo', repo_name=ID)
368 367 defaults = self.__load_data(repo_name)
369 368 if 'clone_uri' in defaults:
370 369 del defaults['clone_uri']
371 370
372 371 c.repo_fields = RepositoryField.query()\
373 372 .filter(RepositoryField.repository == c.repo_info).all()
374 373 c.active = 'settings'
375 374 return htmlfill.render(
376 375 render('admin/repos/repo_edit.html'),
377 376 defaults=defaults,
378 377 encoding="UTF-8",
379 378 force_defaults=False)
380 379
381 380 @HasRepoPermissionAllDecorator('repository.admin')
382 381 def edit_permissions(self, repo_name):
383 382 """GET /repo_name/settings: Form to edit an existing item"""
384 383 # url('edit_repo', repo_name=ID)
385 384 c.repo_info = self._load_repo(repo_name)
386 385 repo_model = RepoModel()
387 386 c.users_array = repo_model.get_users_js()
388 387 c.user_groups_array = repo_model.get_user_groups_js()
389 388 c.active = 'permissions'
390 389 defaults = RepoModel()._get_defaults(repo_name)
391 390
392 391 return htmlfill.render(
393 392 render('admin/repos/repo_edit.html'),
394 393 defaults=defaults,
395 394 encoding="UTF-8",
396 395 force_defaults=False)
397 396
398 397 def edit_permissions_update(self, repo_name):
399 398 form = RepoPermsForm()().to_python(request.POST)
400 399 RepoModel()._update_permissions(repo_name, form['perms_new'],
401 400 form['perms_updates'])
402 401 #TODO: implement this
403 402 #action_logger(self.authuser, 'admin_changed_repo_permissions',
404 403 # repo_name, self.ip_addr, self.sa)
405 404 Session().commit()
406 405 h.flash(_('Repository permissions updated'), category='success')
407 406 return redirect(url('edit_repo_perms', repo_name=repo_name))
408 407
409 408 def edit_permissions_revoke(self, repo_name):
410 409 try:
411 410 obj_type = request.POST.get('obj_type')
412 411 obj_id = None
413 412 if obj_type == 'user':
414 413 obj_id = safe_int(request.POST.get('user_id'))
415 414 elif obj_type == 'user_group':
416 415 obj_id = safe_int(request.POST.get('user_group_id'))
417 416
418 417 if obj_type == 'user':
419 418 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
420 419 elif obj_type == 'user_group':
421 420 RepoModel().revoke_user_group_permission(
422 421 repo=repo_name, group_name=obj_id
423 422 )
424 423 #TODO: implement this
425 424 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
426 425 # repo_name, self.ip_addr, self.sa)
427 426 Session().commit()
428 427 except Exception:
429 428 log.error(traceback.format_exc())
430 429 h.flash(_('An error occurred during revoking of permission'),
431 430 category='error')
432 431 raise HTTPInternalServerError()
433 432
434 433 @HasRepoPermissionAllDecorator('repository.admin')
435 434 def edit_fields(self, repo_name):
436 435 """GET /repo_name/settings: Form to edit an existing item"""
437 436 # url('edit_repo', repo_name=ID)
438 437 c.repo_info = self._load_repo(repo_name)
439 438 c.repo_fields = RepositoryField.query()\
440 439 .filter(RepositoryField.repository == c.repo_info).all()
441 440 c.active = 'fields'
442 441 if request.POST:
443 442
444 443 return redirect(url('repo_edit_fields'))
445 444 return render('admin/repos/repo_edit.html')
446 445
447 446 @HasRepoPermissionAllDecorator('repository.admin')
448 447 def create_repo_field(self, repo_name):
449 448 try:
450 449 form_result = RepoFieldForm()().to_python(dict(request.POST))
451 450 new_field = RepositoryField()
452 451 new_field.repository = Repository.get_by_repo_name(repo_name)
453 452 new_field.field_key = form_result['new_field_key']
454 453 new_field.field_type = form_result['new_field_type'] # python type
455 454 new_field.field_value = form_result['new_field_value'] # set initial blank value
456 455 new_field.field_desc = form_result['new_field_desc']
457 456 new_field.field_label = form_result['new_field_label']
458 457 Session().add(new_field)
459 458 Session().commit()
460 459 except Exception, e:
461 460 log.error(traceback.format_exc())
462 461 msg = _('An error occurred during creation of field')
463 462 if isinstance(e, formencode.Invalid):
464 463 msg += ". " + e.msg
465 464 h.flash(msg, category='error')
466 465 return redirect(url('edit_repo_fields', repo_name=repo_name))
467 466
468 467 @HasRepoPermissionAllDecorator('repository.admin')
469 468 def delete_repo_field(self, repo_name, field_id):
470 469 field = RepositoryField.get_or_404(field_id)
471 470 try:
472 471 Session().delete(field)
473 472 Session().commit()
474 473 except Exception, e:
475 474 log.error(traceback.format_exc())
476 475 msg = _('An error occurred during removal of field')
477 476 h.flash(msg, category='error')
478 477 return redirect(url('edit_repo_fields', repo_name=repo_name))
479 478
480 479 @HasRepoPermissionAllDecorator('repository.admin')
481 480 def edit_advanced(self, repo_name):
482 481 """GET /repo_name/settings: Form to edit an existing item"""
483 482 # url('edit_repo', repo_name=ID)
484 483 c.repo_info = self._load_repo(repo_name)
485 484 c.default_user_id = User.get_default_user().user_id
486 485 c.in_public_journal = UserFollowing.query()\
487 486 .filter(UserFollowing.user_id == c.default_user_id)\
488 487 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
489 488
490 489 _repos = Repository.query().order_by(Repository.repo_name).all()
491 490 read_access_repos = RepoList(_repos)
492 491 c.repos_list = [(None, _('-- Not a fork --'))]
493 492 c.repos_list += [(x.repo_id, x.repo_name)
494 493 for x in read_access_repos
495 494 if x.repo_id != c.repo_info.repo_id]
496 495
497 496 defaults = {
498 497 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
499 498 }
500 499
501 500 c.active = 'advanced'
502 501 if request.POST:
503 502 return redirect(url('repo_edit_advanced'))
504 503 return htmlfill.render(
505 504 render('admin/repos/repo_edit.html'),
506 505 defaults=defaults,
507 506 encoding="UTF-8",
508 507 force_defaults=False)
509 508
510 509 @HasRepoPermissionAllDecorator('repository.admin')
511 510 def edit_advanced_journal(self, repo_name):
512 511 """
513 512 Sets this repository to be visible in public journal,
514 513 in other words asking default user to follow this repo
515 514
516 515 :param repo_name:
517 516 """
518 517
519 cur_token = request.POST.get('auth_token')
520 token = get_token()
521 if cur_token == token:
522 try:
523 repo_id = Repository.get_by_repo_name(repo_name).repo_id
524 user_id = User.get_default_user().user_id
525 self.scm_model.toggle_following_repo(repo_id, user_id)
526 h.flash(_('Updated repository visibility in public journal'),
527 category='success')
528 Session().commit()
529 except Exception:
530 h.flash(_('An error occurred during setting this'
531 ' repository in public journal'),
532 category='error')
533
534 else:
535 h.flash(_('Token mismatch'), category='error')
518 try:
519 repo_id = Repository.get_by_repo_name(repo_name).repo_id
520 user_id = User.get_default_user().user_id
521 self.scm_model.toggle_following_repo(repo_id, user_id)
522 h.flash(_('Updated repository visibility in public journal'),
523 category='success')
524 Session().commit()
525 except Exception:
526 h.flash(_('An error occurred during setting this'
527 ' repository in public journal'),
528 category='error')
536 529 return redirect(url('edit_repo_advanced', repo_name=repo_name))
537 530
538 531
539 532 @HasRepoPermissionAllDecorator('repository.admin')
540 533 def edit_advanced_fork(self, repo_name):
541 534 """
542 535 Mark given repository as a fork of another
543 536
544 537 :param repo_name:
545 538 """
546 539 try:
547 540 fork_id = request.POST.get('id_fork_of')
548 541 repo = ScmModel().mark_as_fork(repo_name, fork_id,
549 542 self.authuser.username)
550 543 fork = repo.fork.repo_name if repo.fork else _('Nothing')
551 544 Session().commit()
552 545 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
553 546 category='success')
554 547 except RepositoryError, e:
555 548 log.error(traceback.format_exc())
556 549 h.flash(str(e), category='error')
557 550 except Exception, e:
558 551 log.error(traceback.format_exc())
559 552 h.flash(_('An error occurred during this operation'),
560 553 category='error')
561 554
562 555 return redirect(url('edit_repo_advanced', repo_name=repo_name))
563 556
564 557 @HasRepoPermissionAllDecorator('repository.admin')
565 558 def edit_advanced_locking(self, repo_name):
566 559 """
567 560 Unlock repository when it is locked !
568 561
569 562 :param repo_name:
570 563 """
571 564 try:
572 565 repo = Repository.get_by_repo_name(repo_name)
573 566 if request.POST.get('set_lock'):
574 567 Repository.lock(repo, c.authuser.user_id)
575 568 h.flash(_('Locked repository'), category='success')
576 569 elif request.POST.get('set_unlock'):
577 570 Repository.unlock(repo)
578 571 h.flash(_('Unlocked repository'), category='success')
579 572 except Exception, e:
580 573 log.error(traceback.format_exc())
581 574 h.flash(_('An error occurred during unlocking'),
582 575 category='error')
583 576 return redirect(url('edit_repo_advanced', repo_name=repo_name))
584 577
585 578 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
586 579 def toggle_locking(self, repo_name):
587 580 """
588 581 Toggle locking of repository by simple GET call to url
589 582
590 583 :param repo_name:
591 584 """
592 585
593 586 try:
594 587 repo = Repository.get_by_repo_name(repo_name)
595 588
596 589 if repo.enable_locking:
597 590 if repo.locked[0]:
598 591 Repository.unlock(repo)
599 592 action = _('Unlocked')
600 593 else:
601 594 Repository.lock(repo, c.authuser.user_id)
602 595 action = _('Locked')
603 596
604 597 h.flash(_('Repository has been %s') % action,
605 598 category='success')
606 599 except Exception, e:
607 600 log.error(traceback.format_exc())
608 601 h.flash(_('An error occurred during unlocking'),
609 602 category='error')
610 603 return redirect(url('summary_home', repo_name=repo_name))
611 604
612 605 @HasRepoPermissionAllDecorator('repository.admin')
613 606 def edit_caches(self, repo_name):
614 607 """GET /repo_name/settings: Form to edit an existing item"""
615 608 # url('edit_repo', repo_name=ID)
616 609 c.repo_info = self._load_repo(repo_name)
617 610 c.active = 'caches'
618 611 if request.POST:
619 612 try:
620 613 ScmModel().mark_for_invalidation(repo_name, delete=True)
621 614 Session().commit()
622 615 h.flash(_('Cache invalidation successful'),
623 616 category='success')
624 617 except Exception, e:
625 618 log.error(traceback.format_exc())
626 619 h.flash(_('An error occurred during cache invalidation'),
627 620 category='error')
628 621
629 622 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
630 623 return render('admin/repos/repo_edit.html')
631 624
632 625 @HasRepoPermissionAllDecorator('repository.admin')
633 626 def edit_remote(self, repo_name):
634 627 """GET /repo_name/settings: Form to edit an existing item"""
635 628 # url('edit_repo', repo_name=ID)
636 629 c.repo_info = self._load_repo(repo_name)
637 630 c.active = 'remote'
638 631 if request.POST:
639 632 try:
640 633 ScmModel().pull_changes(repo_name, self.authuser.username)
641 634 h.flash(_('Pulled from remote location'), category='success')
642 635 except Exception, e:
643 636 log.error(traceback.format_exc())
644 637 h.flash(_('An error occurred during pull from remote location'),
645 638 category='error')
646 639 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
647 640 return render('admin/repos/repo_edit.html')
648 641
649 642 @HasRepoPermissionAllDecorator('repository.admin')
650 643 def edit_statistics(self, repo_name):
651 644 """GET /repo_name/settings: Form to edit an existing item"""
652 645 # url('edit_repo', repo_name=ID)
653 646 c.repo_info = self._load_repo(repo_name)
654 647 repo = c.repo_info.scm_instance
655 648
656 649 if c.repo_info.stats:
657 650 # this is on what revision we ended up so we add +1 for count
658 651 last_rev = c.repo_info.stats.stat_on_revision + 1
659 652 else:
660 653 last_rev = 0
661 654 c.stats_revision = last_rev
662 655
663 656 c.repo_last_rev = repo.count() if repo.revisions else 0
664 657
665 658 if last_rev == 0 or c.repo_last_rev == 0:
666 659 c.stats_percentage = 0
667 660 else:
668 661 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
669 662
670 663 c.active = 'statistics'
671 664 if request.POST:
672 665 try:
673 666 RepoModel().delete_stats(repo_name)
674 667 Session().commit()
675 668 except Exception, e:
676 669 log.error(traceback.format_exc())
677 670 h.flash(_('An error occurred during deletion of repository stats'),
678 671 category='error')
679 672 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
680 673
681 674 return render('admin/repos/repo_edit.html')
@@ -1,379 +1,374 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.journal
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Journal controller for pylons
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26
27 27 """
28 28
29 29 import logging
30 30 import traceback
31 31 from itertools import groupby
32 32
33 33 from sqlalchemy import or_
34 34 from sqlalchemy.orm import joinedload
35 35 from sqlalchemy.sql.expression import func
36 36
37 37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
38 38
39 39 from webob.exc import HTTPBadRequest
40 40 from pylons import request, tmpl_context as c, response, url
41 41 from pylons.i18n.translation import _
42 42
43 43 from kallithea.controllers.admin.admin import _journal_filter
44 44 from kallithea.model.db import UserLog, UserFollowing, Repository, User
45 45 from kallithea.model.meta import Session
46 46 from kallithea.model.repo import RepoModel
47 47 import kallithea.lib.helpers as h
48 48 from kallithea.lib.helpers import Page
49 49 from kallithea.lib.auth import LoginRequired, NotAnonymous
50 50 from kallithea.lib.base import BaseController, render
51 51 from kallithea.lib.utils2 import safe_int, AttributeDict
52 52 from kallithea.lib.compat import json
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class JournalController(BaseController):
58 58
59 59 def __before__(self):
60 60 super(JournalController, self).__before__()
61 61 self.language = 'en-us'
62 62 self.ttl = "5"
63 63 self.feed_nr = 20
64 64 c.search_term = request.GET.get('filter')
65 65
66 66 def _get_daily_aggregate(self, journal):
67 67 groups = []
68 68 for k, g in groupby(journal, lambda x: x.action_as_day):
69 69 user_group = []
70 70 #groupby username if it's a present value, else fallback to journal username
71 71 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
72 72 l = list(g2)
73 73 user_group.append((l[0].user, l))
74 74
75 75 groups.append((k, user_group,))
76 76
77 77 return groups
78 78
79 79 def _get_journal_data(self, following_repos):
80 80 repo_ids = [x.follows_repository.repo_id for x in following_repos
81 81 if x.follows_repository is not None]
82 82 user_ids = [x.follows_user.user_id for x in following_repos
83 83 if x.follows_user is not None]
84 84
85 85 filtering_criterion = None
86 86
87 87 if repo_ids and user_ids:
88 88 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
89 89 UserLog.user_id.in_(user_ids))
90 90 if repo_ids and not user_ids:
91 91 filtering_criterion = UserLog.repository_id.in_(repo_ids)
92 92 if not repo_ids and user_ids:
93 93 filtering_criterion = UserLog.user_id.in_(user_ids)
94 94 if filtering_criterion is not None:
95 95 journal = self.sa.query(UserLog)\
96 96 .options(joinedload(UserLog.user))\
97 97 .options(joinedload(UserLog.repository))
98 98 #filter
99 99 journal = _journal_filter(journal, c.search_term)
100 100 journal = journal.filter(filtering_criterion)\
101 101 .order_by(UserLog.action_date.desc())
102 102 else:
103 103 journal = []
104 104
105 105 return journal
106 106
107 107 def _atom_feed(self, repos, public=True):
108 108 journal = self._get_journal_data(repos)
109 109 if public:
110 110 _link = h.canonical_url('public_journal_atom')
111 111 _desc = '%s %s %s' % (c.site_name, _('public journal'),
112 112 'atom feed')
113 113 else:
114 114 _link = h.canonical_url('journal_atom')
115 115 _desc = '%s %s %s' % (c.site_name, _('journal'), 'atom feed')
116 116
117 117 feed = Atom1Feed(title=_desc,
118 118 link=_link,
119 119 description=_desc,
120 120 language=self.language,
121 121 ttl=self.ttl)
122 122
123 123 for entry in journal[:self.feed_nr]:
124 124 user = entry.user
125 125 if user is None:
126 126 #fix deleted users
127 127 user = AttributeDict({'short_contact': entry.username,
128 128 'email': '',
129 129 'full_contact': ''})
130 130 action, action_extra, ico = h.action_parser(entry, feed=True)
131 131 title = "%s - %s %s" % (user.short_contact, action(),
132 132 entry.repository.repo_name)
133 133 desc = action_extra()
134 134 _url = None
135 135 if entry.repository is not None:
136 136 _url = h.canonical_url('changelog_home',
137 137 repo_name=entry.repository.repo_name)
138 138
139 139 feed.add_item(title=title,
140 140 pubdate=entry.action_date,
141 141 link=_url or h.canonical_url(''),
142 142 author_email=user.email,
143 143 author_name=user.full_contact,
144 144 description=desc)
145 145
146 146 response.content_type = feed.mime_type
147 147 return feed.writeString('utf-8')
148 148
149 149 def _rss_feed(self, repos, public=True):
150 150 journal = self._get_journal_data(repos)
151 151 if public:
152 152 _link = h.canonical_url('public_journal_atom')
153 153 _desc = '%s %s %s' % (c.site_name, _('public journal'),
154 154 'rss feed')
155 155 else:
156 156 _link = h.canonical_url('journal_atom')
157 157 _desc = '%s %s %s' % (c.site_name, _('journal'), 'rss feed')
158 158
159 159 feed = Rss201rev2Feed(title=_desc,
160 160 link=_link,
161 161 description=_desc,
162 162 language=self.language,
163 163 ttl=self.ttl)
164 164
165 165 for entry in journal[:self.feed_nr]:
166 166 user = entry.user
167 167 if user is None:
168 168 #fix deleted users
169 169 user = AttributeDict({'short_contact': entry.username,
170 170 'email': '',
171 171 'full_contact': ''})
172 172 action, action_extra, ico = h.action_parser(entry, feed=True)
173 173 title = "%s - %s %s" % (user.short_contact, action(),
174 174 entry.repository.repo_name)
175 175 desc = action_extra()
176 176 _url = None
177 177 if entry.repository is not None:
178 178 _url = h.canonical_url('changelog_home',
179 179 repo_name=entry.repository.repo_name)
180 180
181 181 feed.add_item(title=title,
182 182 pubdate=entry.action_date,
183 183 link=_url or h.canonical_url(''),
184 184 author_email=user.email,
185 185 author_name=user.full_contact,
186 186 description=desc)
187 187
188 188 response.content_type = feed.mime_type
189 189 return feed.writeString('utf-8')
190 190
191 191 @LoginRequired()
192 192 @NotAnonymous()
193 193 def index(self):
194 194 # Return a rendered template
195 195 p = safe_int(request.GET.get('page', 1), 1)
196 196 c.user = User.get(self.authuser.user_id)
197 197 c.following = self.sa.query(UserFollowing)\
198 198 .filter(UserFollowing.user_id == self.authuser.user_id)\
199 199 .options(joinedload(UserFollowing.follows_repository))\
200 200 .all()
201 201
202 202 journal = self._get_journal_data(c.following)
203 203
204 204 def url_generator(**kw):
205 205 return url.current(filter=c.search_term, **kw)
206 206
207 207 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
208 208 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
209 209
210 210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 211 return render('journal/journal_data.html')
212 212
213 213 repos_list = Session().query(Repository)\
214 214 .filter(Repository.user_id ==
215 215 self.authuser.user_id)\
216 216 .order_by(func.lower(Repository.repo_name)).all()
217 217
218 218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
219 219 admin=True)
220 220 #json used to render the grid
221 221 c.data = json.dumps(repos_data)
222 222
223 223 watched_repos_data = []
224 224
225 225 ## watched repos
226 226 _render = RepoModel._render_datatable
227 227
228 228 def quick_menu(repo_name):
229 229 return _render('quick_menu', repo_name)
230 230
231 231 def repo_lnk(name, rtype, rstate, private, fork_of):
232 232 return _render('repo_name', name, rtype, rstate, private, fork_of,
233 233 short_name=False, admin=False)
234 234
235 235 def last_rev(repo_name, cs_cache):
236 236 return _render('revision', repo_name, cs_cache.get('revision'),
237 237 cs_cache.get('raw_id'), cs_cache.get('author'),
238 238 cs_cache.get('message'))
239 239
240 240 def desc(desc):
241 241 from pylons import tmpl_context as c
242 242 if c.visual.stylify_metatags:
243 243 return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
244 244 else:
245 245 return h.urlify_text(h.truncate(desc, 60))
246 246
247 247 def repo_actions(repo_name):
248 248 return _render('repo_actions', repo_name)
249 249
250 250 def owner_actions(user_id, username):
251 251 return _render('user_name', user_id, username)
252 252
253 253 def toogle_follow(repo_id):
254 254 return _render('toggle_follow', repo_id)
255 255
256 256 for entry in c.following:
257 257 repo = entry.follows_repository
258 258 cs_cache = repo.changeset_cache
259 259 row = {
260 260 "menu": quick_menu(repo.repo_name),
261 261 "raw_name": repo.repo_name.lower(),
262 262 "name": repo_lnk(repo.repo_name, repo.repo_type,
263 263 repo.repo_state, repo.private, repo.fork),
264 264 "last_changeset": last_rev(repo.repo_name, cs_cache),
265 265 "last_rev_raw": cs_cache.get('revision'),
266 266 "action": toogle_follow(repo.repo_id)
267 267 }
268 268
269 269 watched_repos_data.append(row)
270 270
271 271 c.watched_data = json.dumps({
272 272 "totalRecords": len(c.following),
273 273 "startIndex": 0,
274 274 "sort": "name",
275 275 "dir": "asc",
276 276 "records": watched_repos_data
277 277 })
278 278 return render('journal/journal.html')
279 279
280 280 @LoginRequired(api_access=True)
281 281 @NotAnonymous()
282 282 def journal_atom(self):
283 283 """
284 284 Produce an atom-1.0 feed via feedgenerator module
285 285 """
286 286 following = self.sa.query(UserFollowing)\
287 287 .filter(UserFollowing.user_id == self.authuser.user_id)\
288 288 .options(joinedload(UserFollowing.follows_repository))\
289 289 .all()
290 290 return self._atom_feed(following, public=False)
291 291
292 292 @LoginRequired(api_access=True)
293 293 @NotAnonymous()
294 294 def journal_rss(self):
295 295 """
296 296 Produce an rss feed via feedgenerator module
297 297 """
298 298 following = self.sa.query(UserFollowing)\
299 299 .filter(UserFollowing.user_id == self.authuser.user_id)\
300 300 .options(joinedload(UserFollowing.follows_repository))\
301 301 .all()
302 302 return self._rss_feed(following, public=False)
303 303
304 304 @LoginRequired()
305 305 @NotAnonymous()
306 306 def toggle_following(self):
307 cur_token = request.POST.get('auth_token')
308 token = h.get_token()
309 if cur_token == token:
307 user_id = request.POST.get('follows_user_id')
308 if user_id:
309 try:
310 self.scm_model.toggle_following_user(user_id,
311 self.authuser.user_id)
312 Session.commit()
313 return 'ok'
314 except Exception:
315 log.error(traceback.format_exc())
316 raise HTTPBadRequest()
310 317
311 user_id = request.POST.get('follows_user_id')
312 if user_id:
313 try:
314 self.scm_model.toggle_following_user(user_id,
315 self.authuser.user_id)
316 Session.commit()
317 return 'ok'
318 except Exception:
319 log.error(traceback.format_exc())
320 raise HTTPBadRequest()
318 repo_id = request.POST.get('follows_repo_id')
319 if repo_id:
320 try:
321 self.scm_model.toggle_following_repo(repo_id,
322 self.authuser.user_id)
323 Session.commit()
324 return 'ok'
325 except Exception:
326 log.error(traceback.format_exc())
327 raise HTTPBadRequest()
321 328
322 repo_id = request.POST.get('follows_repo_id')
323 if repo_id:
324 try:
325 self.scm_model.toggle_following_repo(repo_id,
326 self.authuser.user_id)
327 Session.commit()
328 return 'ok'
329 except Exception:
330 log.error(traceback.format_exc())
331 raise HTTPBadRequest()
332
333 log.debug('token mismatch %s vs %s' % (cur_token, token))
334 329 raise HTTPBadRequest()
335 330
336 331 @LoginRequired()
337 332 def public_journal(self):
338 333 # Return a rendered template
339 334 p = safe_int(request.GET.get('page', 1), 1)
340 335
341 336 c.following = self.sa.query(UserFollowing)\
342 337 .filter(UserFollowing.user_id == self.authuser.user_id)\
343 338 .options(joinedload(UserFollowing.follows_repository))\
344 339 .all()
345 340
346 341 journal = self._get_journal_data(c.following)
347 342
348 343 c.journal_pager = Page(journal, page=p, items_per_page=20)
349 344
350 345 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
351 346
352 347 if request.environ.get('HTTP_X_PARTIAL_XHR'):
353 348 return render('journal/journal_data.html')
354 349
355 350 return render('journal/public_journal.html')
356 351
357 352 @LoginRequired(api_access=True)
358 353 def public_journal_atom(self):
359 354 """
360 355 Produce an atom-1.0 feed via feedgenerator module
361 356 """
362 357 c.following = self.sa.query(UserFollowing)\
363 358 .filter(UserFollowing.user_id == self.authuser.user_id)\
364 359 .options(joinedload(UserFollowing.follows_repository))\
365 360 .all()
366 361
367 362 return self._atom_feed(c.following)
368 363
369 364 @LoginRequired(api_access=True)
370 365 def public_journal_rss(self):
371 366 """
372 367 Produce an rss2 feed via feedgenerator module
373 368 """
374 369 c.following = self.sa.query(UserFollowing)\
375 370 .filter(UserFollowing.user_id == self.authuser.user_id)\
376 371 .options(joinedload(UserFollowing.follows_repository))\
377 372 .all()
378 373
379 374 return self._rss_feed(c.following)
@@ -1,1469 +1,1452 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Helper functions
16 16
17 17 Consists of functions to typically be used within templates, but also
18 18 available to Controllers. This module is available to both as 'h'.
19 19 """
20 20 import random
21 21 import hashlib
22 22 import StringIO
23 23 import math
24 24 import logging
25 25 import re
26 26 import urlparse
27 27 import textwrap
28 28
29 29 from pygments.formatters.html import HtmlFormatter
30 30 from pygments import highlight as code_highlight
31 31 from pylons import url
32 32 from pylons.i18n.translation import _, ungettext
33 33 from hashlib import md5
34 34
35 35 from webhelpers.html import literal, HTML, escape
36 36 from webhelpers.html.tools import *
37 37 from webhelpers.html.builder import make_tag
38 38 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
39 39 end_form, file, form, hidden, image, javascript_link, link_to, \
40 40 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
41 41 submit, text, password, textarea, title, ul, xml_declaration, radio
42 42 from webhelpers.html.tools import auto_link, button_to, highlight, \
43 43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
44 44 from webhelpers.number import format_byte_size, format_bit_size
45 45 from webhelpers.pylonslib import Flash as _Flash
46 46 from webhelpers.pylonslib.secure_form import secure_form
47 47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
48 48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
49 49 replace_whitespace, urlify, truncate, wrap_paragraphs
50 50 from webhelpers.date import time_ago_in_words
51 51 from webhelpers.paginate import Page as _Page
52 52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
53 53 convert_boolean_attrs, NotGiven, _make_safe_id_component
54 54
55 55 from kallithea.lib.annotate import annotate_highlight
56 56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
57 57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
58 58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
59 59 safe_int
60 60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
61 61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
62 62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
63 63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
64 64 from kallithea.model.changeset_status import ChangesetStatusModel
65 65 from kallithea.model.db import URL_SEP, Permission
66 66
67 67 log = logging.getLogger(__name__)
68 68
69 69
70 70 def canonical_url(*args, **kargs):
71 71 '''Like url(x, qualified=True), but returns url that not only is qualified
72 72 but also canonical, as configured in canonical_url'''
73 73 from kallithea import CONFIG
74 74 try:
75 75 parts = CONFIG.get('canonical_url', '').split('://', 1)
76 76 kargs['host'] = parts[1].split('/', 1)[0]
77 77 kargs['protocol'] = parts[0]
78 78 except IndexError:
79 79 kargs['qualified'] = True
80 80 return url(*args, **kargs)
81 81
82 82 def canonical_hostname():
83 83 '''Return canonical hostname of system'''
84 84 from kallithea import CONFIG
85 85 try:
86 86 parts = CONFIG.get('canonical_url', '').split('://', 1)
87 87 return parts[1].split('/', 1)[0]
88 88 except IndexError:
89 89 parts = url('home', qualified=True).split('://', 1)
90 90 return parts[1].split('/', 1)[0]
91 91
92 92 def html_escape(text, html_escape_table=None):
93 93 """Produce entities within text."""
94 94 if not html_escape_table:
95 95 html_escape_table = {
96 96 "&": "&amp;",
97 97 '"': "&quot;",
98 98 "'": "&apos;",
99 99 ">": "&gt;",
100 100 "<": "&lt;",
101 101 }
102 102 return "".join(html_escape_table.get(c, c) for c in text)
103 103
104 104
105 105 def shorter(text, size=20):
106 106 postfix = '...'
107 107 if len(text) > size:
108 108 return text[:size - len(postfix)] + postfix
109 109 return text
110 110
111 111
112 112 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
113 113 """
114 114 Reset button
115 115 """
116 116 _set_input_attrs(attrs, type, name, value)
117 117 _set_id_attr(attrs, id, name)
118 118 convert_boolean_attrs(attrs, ["disabled"])
119 119 return HTML.input(**attrs)
120 120
121 121 reset = _reset
122 122 safeid = _make_safe_id_component
123 123
124 124
125 125 def FID(raw_id, path):
126 126 """
127 127 Creates a unique ID for filenode based on it's hash of path and revision
128 128 it's safe to use in urls
129 129
130 130 :param raw_id:
131 131 :param path:
132 132 """
133 133
134 134 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
135 135
136 136
137 def get_token():
138 """Return the current authentication token, creating one if one doesn't
139 already exist.
140 """
141 token_key = "_authentication_token"
142 from pylons import session
143 if not token_key in session:
144 try:
145 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
146 except AttributeError: # Python < 2.4
147 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
148 session[token_key] = token
149 if hasattr(session, 'save'):
150 session.save()
151 return session[token_key]
152
153
154 137 class _GetError(object):
155 138 """Get error from form_errors, and represent it as span wrapped error
156 139 message
157 140
158 141 :param field_name: field to fetch errors for
159 142 :param form_errors: form errors dict
160 143 """
161 144
162 145 def __call__(self, field_name, form_errors):
163 146 tmpl = """<span class="error_msg">%s</span>"""
164 147 if form_errors and field_name in form_errors:
165 148 return literal(tmpl % form_errors.get(field_name))
166 149
167 150 get_error = _GetError()
168 151
169 152
170 153 class _ToolTip(object):
171 154
172 155 def __call__(self, tooltip_title, trim_at=50):
173 156 """
174 157 Special function just to wrap our text into nice formatted
175 158 autowrapped text
176 159
177 160 :param tooltip_title:
178 161 """
179 162 tooltip_title = escape(tooltip_title)
180 163 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
181 164 return tooltip_title
182 165 tooltip = _ToolTip()
183 166
184 167
185 168 class _FilesBreadCrumbs(object):
186 169
187 170 def __call__(self, repo_name, rev, paths):
188 171 if isinstance(paths, str):
189 172 paths = safe_unicode(paths)
190 173 url_l = [link_to(repo_name, url('files_home',
191 174 repo_name=repo_name,
192 175 revision=rev, f_path=''),
193 176 class_='ypjax-link')]
194 177 paths_l = paths.split('/')
195 178 for cnt, p in enumerate(paths_l):
196 179 if p != '':
197 180 url_l.append(link_to(p,
198 181 url('files_home',
199 182 repo_name=repo_name,
200 183 revision=rev,
201 184 f_path='/'.join(paths_l[:cnt + 1])
202 185 ),
203 186 class_='ypjax-link'
204 187 )
205 188 )
206 189
207 190 return literal('/'.join(url_l))
208 191
209 192 files_breadcrumbs = _FilesBreadCrumbs()
210 193
211 194
212 195 class CodeHtmlFormatter(HtmlFormatter):
213 196 """
214 197 My code Html Formatter for source codes
215 198 """
216 199
217 200 def wrap(self, source, outfile):
218 201 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
219 202
220 203 def _wrap_code(self, source):
221 204 for cnt, it in enumerate(source):
222 205 i, t = it
223 206 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
224 207 yield i, t
225 208
226 209 def _wrap_tablelinenos(self, inner):
227 210 dummyoutfile = StringIO.StringIO()
228 211 lncount = 0
229 212 for t, line in inner:
230 213 if t:
231 214 lncount += 1
232 215 dummyoutfile.write(line)
233 216
234 217 fl = self.linenostart
235 218 mw = len(str(lncount + fl - 1))
236 219 sp = self.linenospecial
237 220 st = self.linenostep
238 221 la = self.lineanchors
239 222 aln = self.anchorlinenos
240 223 nocls = self.noclasses
241 224 if sp:
242 225 lines = []
243 226
244 227 for i in range(fl, fl + lncount):
245 228 if i % st == 0:
246 229 if i % sp == 0:
247 230 if aln:
248 231 lines.append('<a href="#%s%d" class="special">%*d</a>' %
249 232 (la, i, mw, i))
250 233 else:
251 234 lines.append('<span class="special">%*d</span>' % (mw, i))
252 235 else:
253 236 if aln:
254 237 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
255 238 else:
256 239 lines.append('%*d' % (mw, i))
257 240 else:
258 241 lines.append('')
259 242 ls = '\n'.join(lines)
260 243 else:
261 244 lines = []
262 245 for i in range(fl, fl + lncount):
263 246 if i % st == 0:
264 247 if aln:
265 248 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
266 249 else:
267 250 lines.append('%*d' % (mw, i))
268 251 else:
269 252 lines.append('')
270 253 ls = '\n'.join(lines)
271 254
272 255 # in case you wonder about the seemingly redundant <div> here: since the
273 256 # content in the other cell also is wrapped in a div, some browsers in
274 257 # some configurations seem to mess up the formatting...
275 258 if nocls:
276 259 yield 0, ('<table class="%stable">' % self.cssclass +
277 260 '<tr><td><div class="linenodiv" '
278 261 'style="background-color: #f0f0f0; padding-right: 10px">'
279 262 '<pre style="line-height: 125%">' +
280 263 ls + '</pre></div></td><td id="hlcode" class="code">')
281 264 else:
282 265 yield 0, ('<table class="%stable">' % self.cssclass +
283 266 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
284 267 ls + '</pre></div></td><td id="hlcode" class="code">')
285 268 yield 0, dummyoutfile.getvalue()
286 269 yield 0, '</td></tr></table>'
287 270
288 271
289 272 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
290 273
291 274 def _markup_whitespace(m):
292 275 groups = m.groups()
293 276 if groups[0]:
294 277 return '<u>\t</u>'
295 278 if groups[1]:
296 279 return ' <i></i>'
297 280
298 281 def markup_whitespace(s):
299 282 return _whitespace_re.sub(_markup_whitespace, s)
300 283
301 284 def pygmentize(filenode, **kwargs):
302 285 """
303 286 pygmentize function using pygments
304 287
305 288 :param filenode:
306 289 """
307 290 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
308 291 return literal(markup_whitespace(
309 292 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
310 293
311 294
312 295 def pygmentize_annotation(repo_name, filenode, **kwargs):
313 296 """
314 297 pygmentize function for annotation
315 298
316 299 :param filenode:
317 300 """
318 301
319 302 color_dict = {}
320 303
321 304 def gen_color(n=10000):
322 305 """generator for getting n of evenly distributed colors using
323 306 hsv color and golden ratio. It always return same order of colors
324 307
325 308 :returns: RGB tuple
326 309 """
327 310
328 311 def hsv_to_rgb(h, s, v):
329 312 if s == 0.0:
330 313 return v, v, v
331 314 i = int(h * 6.0) # XXX assume int() truncates!
332 315 f = (h * 6.0) - i
333 316 p = v * (1.0 - s)
334 317 q = v * (1.0 - s * f)
335 318 t = v * (1.0 - s * (1.0 - f))
336 319 i = i % 6
337 320 if i == 0:
338 321 return v, t, p
339 322 if i == 1:
340 323 return q, v, p
341 324 if i == 2:
342 325 return p, v, t
343 326 if i == 3:
344 327 return p, q, v
345 328 if i == 4:
346 329 return t, p, v
347 330 if i == 5:
348 331 return v, p, q
349 332
350 333 golden_ratio = 0.618033988749895
351 334 h = 0.22717784590367374
352 335
353 336 for _unused in xrange(n):
354 337 h += golden_ratio
355 338 h %= 1
356 339 HSV_tuple = [h, 0.95, 0.95]
357 340 RGB_tuple = hsv_to_rgb(*HSV_tuple)
358 341 yield map(lambda x: str(int(x * 256)), RGB_tuple)
359 342
360 343 cgenerator = gen_color()
361 344
362 345 def get_color_string(cs):
363 346 if cs in color_dict:
364 347 col = color_dict[cs]
365 348 else:
366 349 col = color_dict[cs] = cgenerator.next()
367 350 return "color: rgb(%s)! important;" % (', '.join(col))
368 351
369 352 def url_func(repo_name):
370 353
371 354 def _url_func(changeset):
372 355 author = changeset.author
373 356 date = changeset.date
374 357 message = tooltip(changeset.message)
375 358
376 359 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
377 360 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
378 361 "</b> %s<br/></div>")
379 362
380 363 tooltip_html = tooltip_html % (author, date, message)
381 364 lnk_format = show_id(changeset)
382 365 uri = link_to(
383 366 lnk_format,
384 367 url('changeset_home', repo_name=repo_name,
385 368 revision=changeset.raw_id),
386 369 style=get_color_string(changeset.raw_id),
387 370 class_='tooltip',
388 371 title=tooltip_html
389 372 )
390 373
391 374 uri += '\n'
392 375 return uri
393 376 return _url_func
394 377
395 378 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
396 379
397 380
398 381 def is_following_repo(repo_name, user_id):
399 382 from kallithea.model.scm import ScmModel
400 383 return ScmModel().is_following_repo(repo_name, user_id)
401 384
402 385 class _Message(object):
403 386 """A message returned by ``Flash.pop_messages()``.
404 387
405 388 Converting the message to a string returns the message text. Instances
406 389 also have the following attributes:
407 390
408 391 * ``message``: the message text.
409 392 * ``category``: the category specified when the message was created.
410 393 """
411 394
412 395 def __init__(self, category, message):
413 396 self.category = category
414 397 self.message = message
415 398
416 399 def __str__(self):
417 400 return self.message
418 401
419 402 __unicode__ = __str__
420 403
421 404 def __html__(self):
422 405 return escape(safe_unicode(self.message))
423 406
424 407 class Flash(_Flash):
425 408
426 409 def pop_messages(self):
427 410 """Return all accumulated messages and delete them from the session.
428 411
429 412 The return value is a list of ``Message`` objects.
430 413 """
431 414 from pylons import session
432 415 messages = session.pop(self.session_key, [])
433 416 session.save()
434 417 return [_Message(*m) for m in messages]
435 418
436 419 flash = Flash()
437 420
438 421 #==============================================================================
439 422 # SCM FILTERS available via h.
440 423 #==============================================================================
441 424 from kallithea.lib.vcs.utils import author_name, author_email
442 425 from kallithea.lib.utils2 import credentials_filter, age as _age
443 426 from kallithea.model.db import User, ChangesetStatus
444 427
445 428 age = lambda x, y=False: _age(x, y)
446 429 capitalize = lambda x: x.capitalize()
447 430 email = author_email
448 431 short_id = lambda x: x[:12]
449 432 hide_credentials = lambda x: ''.join(credentials_filter(x))
450 433
451 434
452 435 def show_id(cs):
453 436 """
454 437 Configurable function that shows ID
455 438 by default it's r123:fffeeefffeee
456 439
457 440 :param cs: changeset instance
458 441 """
459 442 from kallithea import CONFIG
460 443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
461 444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
462 445
463 446 raw_id = cs.raw_id[:def_len]
464 447 if show_rev:
465 448 return 'r%s:%s' % (cs.revision, raw_id)
466 449 else:
467 450 return '%s' % (raw_id)
468 451
469 452
470 453 def fmt_date(date):
471 454 if date:
472 455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
473 456
474 457 return ""
475 458
476 459
477 460 def is_git(repository):
478 461 if hasattr(repository, 'alias'):
479 462 _type = repository.alias
480 463 elif hasattr(repository, 'repo_type'):
481 464 _type = repository.repo_type
482 465 else:
483 466 _type = repository
484 467 return _type == 'git'
485 468
486 469
487 470 def is_hg(repository):
488 471 if hasattr(repository, 'alias'):
489 472 _type = repository.alias
490 473 elif hasattr(repository, 'repo_type'):
491 474 _type = repository.repo_type
492 475 else:
493 476 _type = repository
494 477 return _type == 'hg'
495 478
496 479
497 480 def user_or_none(author):
498 481 email = author_email(author)
499 482 if email is not None:
500 483 user = User.get_by_email(email, case_insensitive=True, cache=True)
501 484 if user is not None:
502 485 return user
503 486
504 487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
505 488 if user is not None:
506 489 return user
507 490
508 491 return None
509 492
510 493 def email_or_none(author):
511 494 if not author:
512 495 return None
513 496 user = user_or_none(author)
514 497 if user is not None:
515 498 return user.email # always use main email address - not necessarily the one used to find user
516 499
517 500 # extract email from the commit string
518 501 email = author_email(author)
519 502 if email:
520 503 return email
521 504
522 505 # No valid email, not a valid user in the system, none!
523 506 return None
524 507
525 508 def person(author, show_attr="username"):
526 509 """Find the user identified by 'author', return one of the users attributes,
527 510 default to the username attribute, None if there is no user"""
528 511 # attr to return from fetched user
529 512 person_getter = lambda usr: getattr(usr, show_attr)
530 513
531 514 # if author is already an instance use it for extraction
532 515 if isinstance(author, User):
533 516 return person_getter(author)
534 517
535 518 user = user_or_none(author)
536 519 if user is not None:
537 520 return person_getter(user)
538 521
539 522 # Still nothing? Just pass back the author name if any, else the email
540 523 return author_name(author) or email(author)
541 524
542 525
543 526 def person_by_id(id_, show_attr="username"):
544 527 # attr to return from fetched user
545 528 person_getter = lambda usr: getattr(usr, show_attr)
546 529
547 530 #maybe it's an ID ?
548 531 if str(id_).isdigit() or isinstance(id_, int):
549 532 id_ = int(id_)
550 533 user = User.get(id_)
551 534 if user is not None:
552 535 return person_getter(user)
553 536 return id_
554 537
555 538
556 539 def desc_stylize(value):
557 540 """
558 541 converts tags from value into html equivalent
559 542
560 543 :param value:
561 544 """
562 545 if not value:
563 546 return ''
564 547
565 548 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
566 549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
567 550 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
568 551 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
569 552 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
570 553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
571 554 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
572 555 '<div class="metatag" tag="lang">\\2</div>', value)
573 556 value = re.sub(r'\[([a-z]+)\]',
574 557 '<div class="metatag" tag="\\1">\\1</div>', value)
575 558
576 559 return value
577 560
578 561
579 562 def boolicon(value):
580 563 """Returns boolean value of a value, represented as small html image of true/false
581 564 icons
582 565
583 566 :param value: value
584 567 """
585 568
586 569 if value:
587 570 return HTML.tag('i', class_="icon-ok")
588 571 else:
589 572 return HTML.tag('i', class_="icon-minus-circled")
590 573
591 574
592 575 def action_parser(user_log, feed=False, parse_cs=False):
593 576 """
594 577 This helper will action_map the specified string action into translated
595 578 fancy names with icons and links
596 579
597 580 :param user_log: user log instance
598 581 :param feed: use output for feeds (no html and fancy icons)
599 582 :param parse_cs: parse Changesets into VCS instances
600 583 """
601 584
602 585 action = user_log.action
603 586 action_params = ' '
604 587
605 588 x = action.split(':')
606 589
607 590 if len(x) > 1:
608 591 action, action_params = x
609 592
610 593 def get_cs_links():
611 594 revs_limit = 3 # display this amount always
612 595 revs_top_limit = 50 # show upto this amount of changesets hidden
613 596 revs_ids = action_params.split(',')
614 597 deleted = user_log.repository is None
615 598 if deleted:
616 599 return ','.join(revs_ids)
617 600
618 601 repo_name = user_log.repository.repo_name
619 602
620 603 def lnk(rev, repo_name):
621 604 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
622 605 lazy_cs = True
623 606 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
624 607 lazy_cs = False
625 608 lbl = '?'
626 609 if rev.op == 'delete_branch':
627 610 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
628 611 title = ''
629 612 elif rev.op == 'tag':
630 613 lbl = '%s' % _('Created tag: %s') % rev.ref_name
631 614 title = ''
632 615 _url = '#'
633 616
634 617 else:
635 618 lbl = '%s' % (rev.short_id[:8])
636 619 _url = url('changeset_home', repo_name=repo_name,
637 620 revision=rev.raw_id)
638 621 title = tooltip(rev.message)
639 622 else:
640 623 ## changeset cannot be found/striped/removed etc.
641 624 lbl = ('%s' % rev)[:12]
642 625 _url = '#'
643 626 title = _('Changeset not found')
644 627 if parse_cs:
645 628 return link_to(lbl, _url, title=title, class_='tooltip')
646 629 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
647 630 class_='lazy-cs' if lazy_cs else '')
648 631
649 632 def _get_op(rev_txt):
650 633 _op = None
651 634 _name = rev_txt
652 635 if len(rev_txt.split('=>')) == 2:
653 636 _op, _name = rev_txt.split('=>')
654 637 return _op, _name
655 638
656 639 revs = []
657 640 if len(filter(lambda v: v != '', revs_ids)) > 0:
658 641 repo = None
659 642 for rev in revs_ids[:revs_top_limit]:
660 643 _op, _name = _get_op(rev)
661 644
662 645 # we want parsed changesets, or new log store format is bad
663 646 if parse_cs:
664 647 try:
665 648 if repo is None:
666 649 repo = user_log.repository.scm_instance
667 650 _rev = repo.get_changeset(rev)
668 651 revs.append(_rev)
669 652 except ChangesetDoesNotExistError:
670 653 log.error('cannot find revision %s in this repo' % rev)
671 654 revs.append(rev)
672 655 continue
673 656 else:
674 657 _rev = AttributeDict({
675 658 'short_id': rev[:12],
676 659 'raw_id': rev,
677 660 'message': '',
678 661 'op': _op,
679 662 'ref_name': _name
680 663 })
681 664 revs.append(_rev)
682 665 cs_links = [" " + ', '.join(
683 666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
684 667 )]
685 668 _op1, _name1 = _get_op(revs_ids[0])
686 669 _op2, _name2 = _get_op(revs_ids[-1])
687 670
688 671 _rev = '%s...%s' % (_name1, _name2)
689 672
690 673 compare_view = (
691 674 ' <div class="compare_view tooltip" title="%s">'
692 675 '<a href="%s">%s</a> </div>' % (
693 676 _('Show all combined changesets %s->%s') % (
694 677 revs_ids[0][:12], revs_ids[-1][:12]
695 678 ),
696 679 url('changeset_home', repo_name=repo_name,
697 680 revision=_rev
698 681 ),
699 682 _('compare view')
700 683 )
701 684 )
702 685
703 686 # if we have exactly one more than normally displayed
704 687 # just display it, takes less space than displaying
705 688 # "and 1 more revisions"
706 689 if len(revs_ids) == revs_limit + 1:
707 690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
708 691
709 692 # hidden-by-default ones
710 693 if len(revs_ids) > revs_limit + 1:
711 694 uniq_id = revs_ids[0]
712 695 html_tmpl = (
713 696 '<span> %s <a class="show_more" id="_%s" '
714 697 'href="#more">%s</a> %s</span>'
715 698 )
716 699 if not feed:
717 700 cs_links.append(html_tmpl % (
718 701 _('and'),
719 702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
720 703 _('revisions')
721 704 )
722 705 )
723 706
724 707 if not feed:
725 708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
726 709 else:
727 710 html_tmpl = '<span id="%s"> %s </span>'
728 711
729 712 morelinks = ', '.join(
730 713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
731 714 )
732 715
733 716 if len(revs_ids) > revs_top_limit:
734 717 morelinks += ', ...'
735 718
736 719 cs_links.append(html_tmpl % (uniq_id, morelinks))
737 720 if len(revs) > 1:
738 721 cs_links.append(compare_view)
739 722 return ''.join(cs_links)
740 723
741 724 def get_fork_name():
742 725 repo_name = action_params
743 726 _url = url('summary_home', repo_name=repo_name)
744 727 return _('fork name %s') % link_to(action_params, _url)
745 728
746 729 def get_user_name():
747 730 user_name = action_params
748 731 return user_name
749 732
750 733 def get_users_group():
751 734 group_name = action_params
752 735 return group_name
753 736
754 737 def get_pull_request():
755 738 pull_request_id = action_params
756 739 deleted = user_log.repository is None
757 740 if deleted:
758 741 repo_name = user_log.repository_name
759 742 else:
760 743 repo_name = user_log.repository.repo_name
761 744 return link_to(_('Pull request #%s') % pull_request_id,
762 745 url('pullrequest_show', repo_name=repo_name,
763 746 pull_request_id=pull_request_id))
764 747
765 748 def get_archive_name():
766 749 archive_name = action_params
767 750 return archive_name
768 751
769 752 # action : translated str, callback(extractor), icon
770 753 action_map = {
771 754 'user_deleted_repo': (_('[deleted] repository'),
772 755 None, 'icon-trashcan'),
773 756 'user_created_repo': (_('[created] repository'),
774 757 None, 'icon-plus'),
775 758 'user_created_fork': (_('[created] repository as fork'),
776 759 None, 'icon-fork'),
777 760 'user_forked_repo': (_('[forked] repository'),
778 761 get_fork_name, 'icon-fork'),
779 762 'user_updated_repo': (_('[updated] repository'),
780 763 None, 'icon-pencil'),
781 764 'user_downloaded_archive': (_('[downloaded] archive from repository'),
782 765 get_archive_name, 'icon-download-cloud'),
783 766 'admin_deleted_repo': (_('[delete] repository'),
784 767 None, 'icon-trashcan'),
785 768 'admin_created_repo': (_('[created] repository'),
786 769 None, 'icon-plus'),
787 770 'admin_forked_repo': (_('[forked] repository'),
788 771 None, 'icon-fork'),
789 772 'admin_updated_repo': (_('[updated] repository'),
790 773 None, 'icon-pencil'),
791 774 'admin_created_user': (_('[created] user'),
792 775 get_user_name, 'icon-user'),
793 776 'admin_updated_user': (_('[updated] user'),
794 777 get_user_name, 'icon-user'),
795 778 'admin_created_users_group': (_('[created] user group'),
796 779 get_users_group, 'icon-pencil'),
797 780 'admin_updated_users_group': (_('[updated] user group'),
798 781 get_users_group, 'icon-pencil'),
799 782 'user_commented_revision': (_('[commented] on revision in repository'),
800 783 get_cs_links, 'icon-comment'),
801 784 'user_commented_pull_request': (_('[commented] on pull request for'),
802 785 get_pull_request, 'icon-comment'),
803 786 'user_closed_pull_request': (_('[closed] pull request for'),
804 787 get_pull_request, 'icon-ok'),
805 788 'push': (_('[pushed] into'),
806 789 get_cs_links, 'icon-move-up'),
807 790 'push_local': (_('[committed via Kallithea] into repository'),
808 791 get_cs_links, 'icon-pencil'),
809 792 'push_remote': (_('[pulled from remote] into repository'),
810 793 get_cs_links, 'icon-move-up'),
811 794 'pull': (_('[pulled] from'),
812 795 None, 'icon-move-down'),
813 796 'started_following_repo': (_('[started following] repository'),
814 797 None, 'icon-heart'),
815 798 'stopped_following_repo': (_('[stopped following] repository'),
816 799 None, 'icon-heart-empty'),
817 800 }
818 801
819 802 action_str = action_map.get(action, action)
820 803 if feed:
821 804 action = action_str[0].replace('[', '').replace(']', '')
822 805 else:
823 806 action = action_str[0]\
824 807 .replace('[', '<span class="journal_highlight">')\
825 808 .replace(']', '</span>')
826 809
827 810 action_params_func = lambda: ""
828 811
829 812 if callable(action_str[1]):
830 813 action_params_func = action_str[1]
831 814
832 815 def action_parser_icon():
833 816 action = user_log.action
834 817 action_params = None
835 818 x = action.split(':')
836 819
837 820 if len(x) > 1:
838 821 action, action_params = x
839 822
840 823 tmpl = """<i class="%s" alt="%s"></i>"""
841 824 ico = action_map.get(action, ['', '', ''])[2]
842 825 return literal(tmpl % (ico, action))
843 826
844 827 # returned callbacks we need to call to get
845 828 return [lambda: literal(action), action_params_func, action_parser_icon]
846 829
847 830
848 831
849 832 #==============================================================================
850 833 # PERMS
851 834 #==============================================================================
852 835 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
853 836 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
854 837 HasRepoGroupPermissionAny
855 838
856 839
857 840 #==============================================================================
858 841 # GRAVATAR URL
859 842 #==============================================================================
860 843 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
861 844 """return html element of the gravatar
862 845
863 846 This method will return an <img> with the resolution double the size (for
864 847 retina screens) of the image. If the url returned from gravatar_url is
865 848 empty then we fallback to using an icon.
866 849
867 850 """
868 851 src = gravatar_url(email_address, size*2, ssl_enabled)
869 852
870 853 # here it makes sense to use style="width: ..." (instead of, say, a
871 854 # stylesheet) because we using this to generate a high-res (retina) size
872 855 tmpl = """<img alt="gravatar" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>"""
873 856
874 857 # if src is empty then there was no gravatar, so we use a font icon
875 858 if not src:
876 859 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
877 860
878 861 tmpl = tmpl.format(cls=cls, size=size, src=src)
879 862 return literal(tmpl)
880 863
881 864 def gravatar_url(email_address, size=30, ssl_enabled=True):
882 865 # doh, we need to re-import those to mock it later
883 866 from pylons import url
884 867 from pylons import tmpl_context as c
885 868
886 869 _def = 'anonymous@kallithea-scm.org' # default gravatar
887 870 _use_gravatar = c.visual.use_gravatar
888 871 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
889 872
890 873 email_address = email_address or _def
891 874
892 875 if not _use_gravatar or not email_address or email_address == _def:
893 876 return ""
894 877
895 878 if _use_gravatar:
896 879 _md5 = lambda s: hashlib.md5(s).hexdigest()
897 880
898 881 tmpl = _gravatar_url
899 882 parsed_url = urlparse.urlparse(url.current(qualified=True))
900 883 tmpl = tmpl.replace('{email}', email_address)\
901 884 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
902 885 .replace('{netloc}', parsed_url.netloc)\
903 886 .replace('{scheme}', parsed_url.scheme)\
904 887 .replace('{size}', safe_str(size))
905 888 return tmpl
906 889
907 890 class Page(_Page):
908 891 """
909 892 Custom pager to match rendering style with YUI paginator
910 893 """
911 894
912 895 def _get_pos(self, cur_page, max_page, items):
913 896 edge = (items / 2) + 1
914 897 if (cur_page <= edge):
915 898 radius = max(items / 2, items - cur_page)
916 899 elif (max_page - cur_page) < edge:
917 900 radius = (items - 1) - (max_page - cur_page)
918 901 else:
919 902 radius = items / 2
920 903
921 904 left = max(1, (cur_page - (radius)))
922 905 right = min(max_page, cur_page + (radius))
923 906 return left, cur_page, right
924 907
925 908 def _range(self, regexp_match):
926 909 """
927 910 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
928 911
929 912 Arguments:
930 913
931 914 regexp_match
932 915 A "re" (regular expressions) match object containing the
933 916 radius of linked pages around the current page in
934 917 regexp_match.group(1) as a string
935 918
936 919 This function is supposed to be called as a callable in
937 920 re.sub.
938 921
939 922 """
940 923 radius = int(regexp_match.group(1))
941 924
942 925 # Compute the first and last page number within the radius
943 926 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
944 927 # -> leftmost_page = 5
945 928 # -> rightmost_page = 9
946 929 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
947 930 self.last_page,
948 931 (radius * 2) + 1)
949 932 nav_items = []
950 933
951 934 # Create a link to the first page (unless we are on the first page
952 935 # or there would be no need to insert '..' spacers)
953 936 if self.page != self.first_page and self.first_page < leftmost_page:
954 937 nav_items.append(self._pagerlink(self.first_page, self.first_page))
955 938
956 939 # Insert dots if there are pages between the first page
957 940 # and the currently displayed page range
958 941 if leftmost_page - self.first_page > 1:
959 942 # Wrap in a SPAN tag if nolink_attr is set
960 943 text = '..'
961 944 if self.dotdot_attr:
962 945 text = HTML.span(c=text, **self.dotdot_attr)
963 946 nav_items.append(text)
964 947
965 948 for thispage in xrange(leftmost_page, rightmost_page + 1):
966 949 # Highlight the current page number and do not use a link
967 950 if thispage == self.page:
968 951 text = '%s' % (thispage,)
969 952 # Wrap in a SPAN tag if nolink_attr is set
970 953 if self.curpage_attr:
971 954 text = HTML.span(c=text, **self.curpage_attr)
972 955 nav_items.append(text)
973 956 # Otherwise create just a link to that page
974 957 else:
975 958 text = '%s' % (thispage,)
976 959 nav_items.append(self._pagerlink(thispage, text))
977 960
978 961 # Insert dots if there are pages between the displayed
979 962 # page numbers and the end of the page range
980 963 if self.last_page - rightmost_page > 1:
981 964 text = '..'
982 965 # Wrap in a SPAN tag if nolink_attr is set
983 966 if self.dotdot_attr:
984 967 text = HTML.span(c=text, **self.dotdot_attr)
985 968 nav_items.append(text)
986 969
987 970 # Create a link to the very last page (unless we are on the last
988 971 # page or there would be no need to insert '..' spacers)
989 972 if self.page != self.last_page and rightmost_page < self.last_page:
990 973 nav_items.append(self._pagerlink(self.last_page, self.last_page))
991 974
992 975 #_page_link = url.current()
993 976 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
994 977 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
995 978 return self.separator.join(nav_items)
996 979
997 980 def pager(self, format='~2~', page_param='page', partial_param='partial',
998 981 show_if_single_page=False, separator=' ', onclick=None,
999 982 symbol_first='<<', symbol_last='>>',
1000 983 symbol_previous='<', symbol_next='>',
1001 984 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1002 985 curpage_attr={'class': 'pager_curpage'},
1003 986 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1004 987
1005 988 self.curpage_attr = curpage_attr
1006 989 self.separator = separator
1007 990 self.pager_kwargs = kwargs
1008 991 self.page_param = page_param
1009 992 self.partial_param = partial_param
1010 993 self.onclick = onclick
1011 994 self.link_attr = link_attr
1012 995 self.dotdot_attr = dotdot_attr
1013 996
1014 997 # Don't show navigator if there is no more than one page
1015 998 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1016 999 return ''
1017 1000
1018 1001 from string import Template
1019 1002 # Replace ~...~ in token format by range of pages
1020 1003 result = re.sub(r'~(\d+)~', self._range, format)
1021 1004
1022 1005 # Interpolate '%' variables
1023 1006 result = Template(result).safe_substitute({
1024 1007 'first_page': self.first_page,
1025 1008 'last_page': self.last_page,
1026 1009 'page': self.page,
1027 1010 'page_count': self.page_count,
1028 1011 'items_per_page': self.items_per_page,
1029 1012 'first_item': self.first_item,
1030 1013 'last_item': self.last_item,
1031 1014 'item_count': self.item_count,
1032 1015 'link_first': self.page > self.first_page and \
1033 1016 self._pagerlink(self.first_page, symbol_first) or '',
1034 1017 'link_last': self.page < self.last_page and \
1035 1018 self._pagerlink(self.last_page, symbol_last) or '',
1036 1019 'link_previous': self.previous_page and \
1037 1020 self._pagerlink(self.previous_page, symbol_previous) \
1038 1021 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1039 1022 'link_next': self.next_page and \
1040 1023 self._pagerlink(self.next_page, symbol_next) \
1041 1024 or HTML.span(symbol_next, class_="yui-pg-next")
1042 1025 })
1043 1026
1044 1027 return literal(result)
1045 1028
1046 1029
1047 1030 #==============================================================================
1048 1031 # REPO PAGER, PAGER FOR REPOSITORY
1049 1032 #==============================================================================
1050 1033 class RepoPage(Page):
1051 1034
1052 1035 def __init__(self, collection, page=1, items_per_page=20,
1053 1036 item_count=None, url=None, **kwargs):
1054 1037
1055 1038 """Create a "RepoPage" instance. special pager for paging
1056 1039 repository
1057 1040 """
1058 1041 self._url_generator = url
1059 1042
1060 1043 # Safe the kwargs class-wide so they can be used in the pager() method
1061 1044 self.kwargs = kwargs
1062 1045
1063 1046 # Save a reference to the collection
1064 1047 self.original_collection = collection
1065 1048
1066 1049 self.collection = collection
1067 1050
1068 1051 # The self.page is the number of the current page.
1069 1052 # The first page has the number 1!
1070 1053 try:
1071 1054 self.page = int(page) # make it int() if we get it as a string
1072 1055 except (ValueError, TypeError):
1073 1056 self.page = 1
1074 1057
1075 1058 self.items_per_page = items_per_page
1076 1059
1077 1060 # Unless the user tells us how many items the collections has
1078 1061 # we calculate that ourselves.
1079 1062 if item_count is not None:
1080 1063 self.item_count = item_count
1081 1064 else:
1082 1065 self.item_count = len(self.collection)
1083 1066
1084 1067 # Compute the number of the first and last available page
1085 1068 if self.item_count > 0:
1086 1069 self.first_page = 1
1087 1070 self.page_count = int(math.ceil(float(self.item_count) /
1088 1071 self.items_per_page))
1089 1072 self.last_page = self.first_page + self.page_count - 1
1090 1073
1091 1074 # Make sure that the requested page number is the range of
1092 1075 # valid pages
1093 1076 if self.page > self.last_page:
1094 1077 self.page = self.last_page
1095 1078 elif self.page < self.first_page:
1096 1079 self.page = self.first_page
1097 1080
1098 1081 # Note: the number of items on this page can be less than
1099 1082 # items_per_page if the last page is not full
1100 1083 self.first_item = max(0, (self.item_count) - (self.page *
1101 1084 items_per_page))
1102 1085 self.last_item = ((self.item_count - 1) - items_per_page *
1103 1086 (self.page - 1))
1104 1087
1105 1088 self.items = list(self.collection[self.first_item:self.last_item + 1])
1106 1089
1107 1090 # Links to previous and next page
1108 1091 if self.page > self.first_page:
1109 1092 self.previous_page = self.page - 1
1110 1093 else:
1111 1094 self.previous_page = None
1112 1095
1113 1096 if self.page < self.last_page:
1114 1097 self.next_page = self.page + 1
1115 1098 else:
1116 1099 self.next_page = None
1117 1100
1118 1101 # No items available
1119 1102 else:
1120 1103 self.first_page = None
1121 1104 self.page_count = 0
1122 1105 self.last_page = None
1123 1106 self.first_item = None
1124 1107 self.last_item = None
1125 1108 self.previous_page = None
1126 1109 self.next_page = None
1127 1110 self.items = []
1128 1111
1129 1112 # This is a subclass of the 'list' type. Initialise the list now.
1130 1113 list.__init__(self, reversed(self.items))
1131 1114
1132 1115
1133 1116 def changed_tooltip(nodes):
1134 1117 """
1135 1118 Generates a html string for changed nodes in changeset page.
1136 1119 It limits the output to 30 entries
1137 1120
1138 1121 :param nodes: LazyNodesGenerator
1139 1122 """
1140 1123 if nodes:
1141 1124 pref = ': <br/> '
1142 1125 suf = ''
1143 1126 if len(nodes) > 30:
1144 1127 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1145 1128 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1146 1129 for x in nodes[:30]]) + suf)
1147 1130 else:
1148 1131 return ': ' + _('No Files')
1149 1132
1150 1133
1151 1134 def repo_link(groups_and_repos):
1152 1135 """
1153 1136 Makes a breadcrumbs link to repo within a group
1154 1137 joins &raquo; on each group to create a fancy link
1155 1138
1156 1139 ex::
1157 1140 group >> subgroup >> repo
1158 1141
1159 1142 :param groups_and_repos:
1160 1143 :param last_url:
1161 1144 """
1162 1145 groups, just_name, repo_name = groups_and_repos
1163 1146 last_url = url('summary_home', repo_name=repo_name)
1164 1147 last_link = link_to(just_name, last_url)
1165 1148
1166 1149 def make_link(group):
1167 1150 return link_to(group.name,
1168 1151 url('repos_group_home', group_name=group.group_name))
1169 1152 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1170 1153
1171 1154
1172 1155 def fancy_file_stats(stats):
1173 1156 """
1174 1157 Displays a fancy two colored bar for number of added/deleted
1175 1158 lines of code on file
1176 1159
1177 1160 :param stats: two element list of added/deleted lines of code
1178 1161 """
1179 1162 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1180 1163 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1181 1164
1182 1165 def cgen(l_type, a_v, d_v):
1183 1166 mapping = {'tr': 'top-right-rounded-corner-mid',
1184 1167 'tl': 'top-left-rounded-corner-mid',
1185 1168 'br': 'bottom-right-rounded-corner-mid',
1186 1169 'bl': 'bottom-left-rounded-corner-mid'}
1187 1170 map_getter = lambda x: mapping[x]
1188 1171
1189 1172 if l_type == 'a' and d_v:
1190 1173 #case when added and deleted are present
1191 1174 return ' '.join(map(map_getter, ['tl', 'bl']))
1192 1175
1193 1176 if l_type == 'a' and not d_v:
1194 1177 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1195 1178
1196 1179 if l_type == 'd' and a_v:
1197 1180 return ' '.join(map(map_getter, ['tr', 'br']))
1198 1181
1199 1182 if l_type == 'd' and not a_v:
1200 1183 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1201 1184
1202 1185 a, d = stats['added'], stats['deleted']
1203 1186 width = 100
1204 1187
1205 1188 if stats['binary']:
1206 1189 #binary mode
1207 1190 lbl = ''
1208 1191 bin_op = 1
1209 1192
1210 1193 if BIN_FILENODE in stats['ops']:
1211 1194 lbl = 'bin+'
1212 1195
1213 1196 if NEW_FILENODE in stats['ops']:
1214 1197 lbl += _('new file')
1215 1198 bin_op = NEW_FILENODE
1216 1199 elif MOD_FILENODE in stats['ops']:
1217 1200 lbl += _('mod')
1218 1201 bin_op = MOD_FILENODE
1219 1202 elif DEL_FILENODE in stats['ops']:
1220 1203 lbl += _('del')
1221 1204 bin_op = DEL_FILENODE
1222 1205 elif RENAMED_FILENODE in stats['ops']:
1223 1206 lbl += _('rename')
1224 1207 bin_op = RENAMED_FILENODE
1225 1208
1226 1209 #chmod can go with other operations
1227 1210 if CHMOD_FILENODE in stats['ops']:
1228 1211 _org_lbl = _('chmod')
1229 1212 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1230 1213
1231 1214 #import ipdb;ipdb.set_trace()
1232 1215 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1233 1216 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1234 1217 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1235 1218
1236 1219 t = stats['added'] + stats['deleted']
1237 1220 unit = float(width) / (t or 1)
1238 1221
1239 1222 # needs > 9% of width to be visible or 0 to be hidden
1240 1223 a_p = max(9, unit * a) if a > 0 else 0
1241 1224 d_p = max(9, unit * d) if d > 0 else 0
1242 1225 p_sum = a_p + d_p
1243 1226
1244 1227 if p_sum > width:
1245 1228 #adjust the percentage to be == 100% since we adjusted to 9
1246 1229 if a_p > d_p:
1247 1230 a_p = a_p - (p_sum - width)
1248 1231 else:
1249 1232 d_p = d_p - (p_sum - width)
1250 1233
1251 1234 a_v = a if a > 0 else ''
1252 1235 d_v = d if d > 0 else ''
1253 1236
1254 1237 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1255 1238 cgen('a', a_v, d_v), a_p, a_v
1256 1239 )
1257 1240 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1258 1241 cgen('d', a_v, d_v), d_p, d_v
1259 1242 )
1260 1243 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1261 1244
1262 1245
1263 1246 def urlify_text(text_, safe=True):
1264 1247 """
1265 1248 Extract urls from text and make html links out of them
1266 1249
1267 1250 :param text_:
1268 1251 """
1269 1252
1270 1253 def url_func(match_obj):
1271 1254 url_full = match_obj.groups()[0]
1272 1255 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1273 1256 _newtext = url_re.sub(url_func, text_)
1274 1257 if safe:
1275 1258 return literal(_newtext)
1276 1259 return _newtext
1277 1260
1278 1261
1279 1262 def urlify_changesets(text_, repository):
1280 1263 """
1281 1264 Extract revision ids from changeset and make link from them
1282 1265
1283 1266 :param text_:
1284 1267 :param repository: repo name to build the URL with
1285 1268 """
1286 1269 from pylons import url # doh, we need to re-import url to mock it later
1287 1270
1288 1271 def url_func(match_obj):
1289 1272 rev = match_obj.group(0)
1290 1273 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1291 1274 'url': url('changeset_home', repo_name=repository, revision=rev),
1292 1275 'rev': rev,
1293 1276 }
1294 1277
1295 1278 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1296 1279
1297 1280 def linkify_others(t, l):
1298 1281 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1299 1282 links = []
1300 1283 for e in urls.split(t):
1301 1284 if not urls.match(e):
1302 1285 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1303 1286 else:
1304 1287 links.append(e)
1305 1288
1306 1289 return ''.join(links)
1307 1290
1308 1291 def urlify_commit(text_, repository, link_=None):
1309 1292 """
1310 1293 Parses given text message and makes proper links.
1311 1294 issues are linked to given issue-server, and rest is a changeset link
1312 1295 if link_ is given, in other case it's a plain text
1313 1296
1314 1297 :param text_:
1315 1298 :param repository:
1316 1299 :param link_: changeset link
1317 1300 """
1318 1301 def escaper(string):
1319 1302 return string.replace('<', '&lt;').replace('>', '&gt;')
1320 1303
1321 1304 # urlify changesets - extract revisions and make link out of them
1322 1305 newtext = urlify_changesets(escaper(text_), repository)
1323 1306
1324 1307 # extract http/https links and make them real urls
1325 1308 newtext = urlify_text(newtext, safe=False)
1326 1309
1327 1310 newtext = urlify_issues(newtext, repository, link_)
1328 1311
1329 1312 return literal(newtext)
1330 1313
1331 1314 def urlify_issues(newtext, repository, link_=None):
1332 1315 from kallithea import CONFIG as conf
1333 1316
1334 1317 # allow multiple issue servers to be used
1335 1318 valid_indices = [
1336 1319 x.group(1)
1337 1320 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1338 1321 if x and 'issue_server_link%s' % x.group(1) in conf
1339 1322 and 'issue_prefix%s' % x.group(1) in conf
1340 1323 ]
1341 1324
1342 1325 if valid_indices:
1343 1326 log.debug('found issue server suffixes `%s` during valuation of: %s'
1344 1327 % (','.join(valid_indices), newtext))
1345 1328
1346 1329 for pattern_index in valid_indices:
1347 1330 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1348 1331 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1349 1332 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1350 1333
1351 1334 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1352 1335 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1353 1336 ISSUE_PREFIX))
1354 1337
1355 1338 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1356 1339
1357 1340 def url_func(match_obj):
1358 1341 pref = ''
1359 1342 if match_obj.group().startswith(' '):
1360 1343 pref = ' '
1361 1344
1362 1345 issue_id = ''.join(match_obj.groups())
1363 1346 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1364 1347 if repository:
1365 1348 issue_url = issue_url.replace('{repo}', repository)
1366 1349 repo_name = repository.split(URL_SEP)[-1]
1367 1350 issue_url = issue_url.replace('{repo_name}', repo_name)
1368 1351
1369 1352 return (
1370 1353 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1371 1354 '%(issue-prefix)s%(id-repr)s'
1372 1355 '</a>'
1373 1356 ) % {
1374 1357 'pref': pref,
1375 1358 'cls': 'issue-tracker-link',
1376 1359 'url': issue_url,
1377 1360 'id-repr': issue_id,
1378 1361 'issue-prefix': ISSUE_PREFIX,
1379 1362 'serv': ISSUE_SERVER_LNK,
1380 1363 }
1381 1364 newtext = URL_PAT.sub(url_func, newtext)
1382 1365 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1383 1366
1384 1367 # if we actually did something above
1385 1368 if link_:
1386 1369 # wrap not links into final link => link_
1387 1370 newtext = linkify_others(newtext, link_)
1388 1371 return newtext
1389 1372
1390 1373
1391 1374 def rst(source):
1392 1375 return literal('<div class="rst-block">%s</div>' %
1393 1376 MarkupRenderer.rst(source))
1394 1377
1395 1378
1396 1379 def rst_w_mentions(source):
1397 1380 """
1398 1381 Wrapped rst renderer with @mention highlighting
1399 1382
1400 1383 :param source:
1401 1384 """
1402 1385 return literal('<div class="rst-block">%s</div>' %
1403 1386 MarkupRenderer.rst_with_mentions(source))
1404 1387
1405 1388 def short_ref(ref_type, ref_name):
1406 1389 if ref_type == 'rev':
1407 1390 return short_id(ref_name)
1408 1391 return ref_name
1409 1392
1410 1393 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1411 1394 """
1412 1395 Return full markup for a href to changeset_home for a changeset.
1413 1396 If ref_type is branch it will link to changelog.
1414 1397 ref_name is shortened if ref_type is 'rev'.
1415 1398 if rev is specified show it too, explicitly linking to that revision.
1416 1399 """
1417 1400 txt = short_ref(ref_type, ref_name)
1418 1401 if ref_type == 'branch':
1419 1402 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1420 1403 else:
1421 1404 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1422 1405 l = link_to(repo_name + '#' + txt, u)
1423 1406 if rev and ref_type != 'rev':
1424 1407 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1425 1408 return l
1426 1409
1427 1410 def changeset_status(repo, revision):
1428 1411 return ChangesetStatusModel().get_status(repo, revision)
1429 1412
1430 1413
1431 1414 def changeset_status_lbl(changeset_status):
1432 1415 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1433 1416
1434 1417
1435 1418 def get_permission_name(key):
1436 1419 return dict(Permission.PERMS).get(key)
1437 1420
1438 1421
1439 1422 def journal_filter_help():
1440 1423 return _(textwrap.dedent('''
1441 1424 Example filter terms:
1442 1425 repository:vcs
1443 1426 username:developer
1444 1427 action:*push*
1445 1428 ip:127.0.0.1
1446 1429 date:20120101
1447 1430 date:[20120101100000 TO 20120102]
1448 1431
1449 1432 Generate wildcards using '*' character:
1450 1433 "repository:vcs*" - search everything starting with 'vcs'
1451 1434 "repository:*vcs*" - search for repository containing 'vcs'
1452 1435
1453 1436 Optional AND / OR operators in queries
1454 1437 "repository:vcs OR repository:test"
1455 1438 "username:test AND repository:test*"
1456 1439 '''))
1457 1440
1458 1441
1459 1442 def not_mapped_error(repo_name):
1460 1443 flash(_('%s repository is not mapped to db perhaps'
1461 1444 ' it was created or renamed from the filesystem'
1462 1445 ' please run the application again'
1463 1446 ' in order to rescan repositories') % repo_name, category='error')
1464 1447
1465 1448
1466 1449 def ip_range(ip_addr):
1467 1450 from kallithea.model.db import UserIpMap
1468 1451 s, e = UserIpMap._get_ip_range(ip_addr)
1469 1452 return '%s - %s' % (s, e)
@@ -1,2158 +1,2154 b''
1 1 /**
2 2 Kallithea JS Files
3 3 **/
4 4 'use strict';
5 5
6 6 if (typeof console == "undefined" || typeof console.log == "undefined"){
7 7 console = { log: function() {} }
8 8 }
9 9
10 10 /**
11 11 * INJECT .format function into String
12 12 * Usage: "My name is {0} {1}".format("Johny","Bravo")
13 13 * Return "My name is Johny Bravo"
14 14 * Inspired by https://gist.github.com/1049426
15 15 */
16 16 String.prototype.format = function() {
17 17 function format() {
18 18 var str = this;
19 19 var len = arguments.length+1;
20 20 var safe = undefined;
21 21 var arg = undefined;
22 22
23 23 // For each {0} {1} {n...} replace with the argument in that position. If
24 24 // the argument is an object or an array it will be stringified to JSON.
25 25 for (var i=0; i < len; arg = arguments[i++]) {
26 26 safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
27 27 str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
28 28 }
29 29 return str;
30 30 }
31 31
32 32 // Save a reference of what may already exist under the property native.
33 33 // Allows for doing something like: if("".format.native) { /* use native */ }
34 34 format.native = String.prototype.format;
35 35
36 36 // Replace the prototype property
37 37 return format;
38 38
39 39 }();
40 40
41 41 String.prototype.strip = function(char) {
42 42 if(char === undefined){
43 43 char = '\\s';
44 44 }
45 45 return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
46 46 }
47 47
48 48 String.prototype.lstrip = function(char) {
49 49 if(char === undefined){
50 50 char = '\\s';
51 51 }
52 52 return this.replace(new RegExp('^'+char+'+'),'');
53 53 }
54 54
55 55 String.prototype.rstrip = function(char) {
56 56 if(char === undefined){
57 57 char = '\\s';
58 58 }
59 59 return this.replace(new RegExp(''+char+'+$'),'');
60 60 }
61 61
62 62 /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill
63 63 under MIT license / public domain, see
64 64 https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
65 65 if(!Array.prototype.indexOf) {
66 66 Array.prototype.indexOf = function (searchElement, fromIndex) {
67 67 if ( this === undefined || this === null ) {
68 68 throw new TypeError( '"this" is null or not defined' );
69 69 }
70 70
71 71 var length = this.length >>> 0; // Hack to convert object.length to a UInt32
72 72
73 73 fromIndex = +fromIndex || 0;
74 74
75 75 if (Math.abs(fromIndex) === Infinity) {
76 76 fromIndex = 0;
77 77 }
78 78
79 79 if (fromIndex < 0) {
80 80 fromIndex += length;
81 81 if (fromIndex < 0) {
82 82 fromIndex = 0;
83 83 }
84 84 }
85 85
86 86 for (;fromIndex < length; fromIndex++) {
87 87 if (this[fromIndex] === searchElement) {
88 88 return fromIndex;
89 89 }
90 90 }
91 91
92 92 return -1;
93 93 };
94 94 }
95 95
96 96 /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Compatibility
97 97 under MIT license / public domain, see
98 98 https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
99 99 if (!Array.prototype.filter)
100 100 {
101 101 Array.prototype.filter = function(fun /*, thisArg */)
102 102 {
103 103 if (this === void 0 || this === null)
104 104 throw new TypeError();
105 105
106 106 var t = Object(this);
107 107 var len = t.length >>> 0;
108 108 if (typeof fun !== "function")
109 109 throw new TypeError();
110 110
111 111 var res = [];
112 112 var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
113 113 for (var i = 0; i < len; i++)
114 114 {
115 115 if (i in t)
116 116 {
117 117 var val = t[i];
118 118
119 119 // NOTE: Technically this should Object.defineProperty at
120 120 // the next index, as push can be affected by
121 121 // properties on Object.prototype and Array.prototype.
122 122 // But that method's new, and collisions should be
123 123 // rare, so use the more-compatible alternative.
124 124 if (fun.call(thisArg, val, i, t))
125 125 res.push(val);
126 126 }
127 127 }
128 128
129 129 return res;
130 130 };
131 131 }
132 132
133 133 /**
134 134 * A customized version of PyRoutes.JS from https://pypi.python.org/pypi/pyroutes.js/
135 135 * which is copyright Stephane Klein and was made available under the BSD License.
136 136 *
137 137 * Usage pyroutes.url('mark_error_fixed',{"error_id":error_id}) // /mark_error_fixed/<error_id>
138 138 */
139 139 var pyroutes = (function() {
140 140 // access global map defined in special file pyroutes
141 141 var matchlist = PROUTES_MAP;
142 142 var sprintf = (function() {
143 143 function get_type(variable) {
144 144 return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
145 145 }
146 146 function str_repeat(input, multiplier) {
147 147 for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
148 148 return output.join('');
149 149 }
150 150
151 151 var str_format = function() {
152 152 if (!str_format.cache.hasOwnProperty(arguments[0])) {
153 153 str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
154 154 }
155 155 return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
156 156 };
157 157
158 158 str_format.format = function(parse_tree, argv) {
159 159 var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
160 160 for (i = 0; i < tree_length; i++) {
161 161 node_type = get_type(parse_tree[i]);
162 162 if (node_type === 'string') {
163 163 output.push(parse_tree[i]);
164 164 }
165 165 else if (node_type === 'array') {
166 166 match = parse_tree[i]; // convenience purposes only
167 167 if (match[2]) { // keyword argument
168 168 arg = argv[cursor];
169 169 for (k = 0; k < match[2].length; k++) {
170 170 if (!arg.hasOwnProperty(match[2][k])) {
171 171 throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
172 172 }
173 173 arg = arg[match[2][k]];
174 174 }
175 175 }
176 176 else if (match[1]) { // positional argument (explicit)
177 177 arg = argv[match[1]];
178 178 }
179 179 else { // positional argument (implicit)
180 180 arg = argv[cursor++];
181 181 }
182 182
183 183 if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
184 184 throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
185 185 }
186 186 switch (match[8]) {
187 187 case 'b': arg = arg.toString(2); break;
188 188 case 'c': arg = String.fromCharCode(arg); break;
189 189 case 'd': arg = parseInt(arg, 10); break;
190 190 case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
191 191 case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
192 192 case 'o': arg = arg.toString(8); break;
193 193 case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
194 194 case 'u': arg = Math.abs(arg); break;
195 195 case 'x': arg = arg.toString(16); break;
196 196 case 'X': arg = arg.toString(16).toUpperCase(); break;
197 197 }
198 198 arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
199 199 pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
200 200 pad_length = match[6] - String(arg).length;
201 201 pad = match[6] ? str_repeat(pad_character, pad_length) : '';
202 202 output.push(match[5] ? arg + pad : pad + arg);
203 203 }
204 204 }
205 205 return output.join('');
206 206 };
207 207
208 208 str_format.cache = {};
209 209
210 210 str_format.parse = function(fmt) {
211 211 var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
212 212 while (_fmt) {
213 213 if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
214 214 parse_tree.push(match[0]);
215 215 }
216 216 else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
217 217 parse_tree.push('%');
218 218 }
219 219 else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
220 220 if (match[2]) {
221 221 arg_names |= 1;
222 222 var field_list = [], replacement_field = match[2], field_match = [];
223 223 if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
224 224 field_list.push(field_match[1]);
225 225 while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
226 226 if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
227 227 field_list.push(field_match[1]);
228 228 }
229 229 else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
230 230 field_list.push(field_match[1]);
231 231 }
232 232 else {
233 233 throw('[sprintf] huh?');
234 234 }
235 235 }
236 236 }
237 237 else {
238 238 throw('[sprintf] huh?');
239 239 }
240 240 match[2] = field_list;
241 241 }
242 242 else {
243 243 arg_names |= 2;
244 244 }
245 245 if (arg_names === 3) {
246 246 throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
247 247 }
248 248 parse_tree.push(match);
249 249 }
250 250 else {
251 251 throw('[sprintf] huh?');
252 252 }
253 253 _fmt = _fmt.substring(match[0].length);
254 254 }
255 255 return parse_tree;
256 256 };
257 257
258 258 return str_format;
259 259 })();
260 260
261 261 var vsprintf = function(fmt, argv) {
262 262 argv.unshift(fmt);
263 263 return sprintf.apply(null, argv);
264 264 };
265 265 return {
266 266 'url': function(route_name, params) {
267 267 var result = route_name;
268 268 if (typeof(params) != 'object'){
269 269 params = {};
270 270 }
271 271 if (matchlist.hasOwnProperty(route_name)) {
272 272 var route = matchlist[route_name];
273 273 // param substitution
274 274 for(var i=0; i < route[1].length; i++) {
275 275 if (!params.hasOwnProperty(route[1][i]))
276 276 throw new Error(route[1][i] + ' missing in "' + route_name + '" route generation');
277 277 }
278 278 result = sprintf(route[0], params);
279 279
280 280 var ret = [];
281 281 //extra params => GET
282 282 for(var param in params){
283 283 if (route[1].indexOf(param) == -1){
284 284 ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
285 285 }
286 286 }
287 287 var _parts = ret.join("&");
288 288 if(_parts){
289 289 result = result +'?'+ _parts
290 290 }
291 291 }
292 292
293 293 return result;
294 294 },
295 295 'register': function(route_name, route_tmpl, req_params) {
296 296 if (typeof(req_params) != 'object') {
297 297 req_params = [];
298 298 }
299 299 var keys = [];
300 300 for (var i=0; i < req_params.length; i++) {
301 301 keys.push(req_params[i])
302 302 }
303 303 matchlist[route_name] = [
304 304 unescape(route_tmpl),
305 305 keys
306 306 ]
307 307 },
308 308 '_routes': function(){
309 309 return matchlist;
310 310 }
311 311 }
312 312 })();
313 313
314 314
315 315 /**
316 316 * GLOBAL YUI Shortcuts
317 317 */
318 318 var YUD = YAHOO.util.Dom;
319 319 var YUE = YAHOO.util.Event;
320 320
321 321 /* Invoke all functions in callbacks */
322 322 var _run_callbacks = function(callbacks){
323 323 if (callbacks !== undefined){
324 324 var _l = callbacks.length;
325 325 for (var i=0;i<_l;i++){
326 326 var func = callbacks[i];
327 327 if(typeof(func)=='function'){
328 328 try{
329 329 func();
330 330 }catch (err){};
331 331 }
332 332 }
333 333 }
334 334 }
335 335
336 336 /**
337 337 * turns objects into GET query string
338 338 */
339 339 var _toQueryString = function(o) {
340 340 if(typeof o !== 'object') {
341 341 return false;
342 342 }
343 343 var _p, _qs = [];
344 344 for(_p in o) {
345 345 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
346 346 }
347 347 return _qs.join('&');
348 348 };
349 349
350 350 /**
351 351 * Load HTML into DOM using Ajax
352 352 *
353 353 * @param $target: load html async and place it (or an error message) here
354 354 * @param success: success callback function
355 355 * @param args: query parameters to pass to url
356 356 */
357 357 function asynchtml(url, $target, success, args){
358 358 if(args===undefined){
359 359 args=null;
360 360 }
361 361 $target.html(_TM['Loading ...']).css('opacity','0.3');
362 362
363 363 return $.ajax({url: url, data: args, headers: {'X-PARTIAL-XHR': '1'}, cache: false, dataType: 'html'})
364 364 .done(function(html) {
365 365 $target.html(html);
366 366 $target.css('opacity','1.0');
367 367 //execute the given original callback
368 368 if (success !== undefined && success) {
369 369 success();
370 370 }
371 371 })
372 372 .fail(function(jqXHR, textStatus, errorThrown) {
373 373 if (textStatus == "abort")
374 374 return;
375 375 console.log('Ajax failure: ' + textStatus);
376 376 $target.html('<span class="error_red">ERROR: {0}</span>'.format(textStatus));
377 377 $target.css('opacity','1.0');
378 378 })
379 379 ;
380 380 };
381 381
382 382 var ajaxGET = function(url,success) {
383 383 return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
384 384 .done(success)
385 385 .fail(function(jqXHR, textStatus, errorThrown) {
386 386 if (textStatus == "abort")
387 387 return;
388 388 alert("Ajax GET error: " + textStatus);
389 389 })
390 390 ;
391 391 };
392 392
393 393 var ajaxPOST = function(url, postData, success, failure) {
394 394 var postData = _toQueryString(postData);
395 395 if(failure === undefined) {
396 396 failure = function(jqXHR, textStatus, errorThrown) {
397 397 if (textStatus != "abort")
398 398 alert("Error posting to server: " + textStatus);
399 399 };
400 400 }
401 401 return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
402 402 .done(success)
403 403 .fail(failure);
404 404 };
405 405
406 406
407 407 /**
408 408 * activate .show_more links
409 409 * the .show_more must have an id that is the the id of an element to hide prefixed with _
410 410 * the parentnode will be displayed
411 411 */
412 412 var show_more_event = function(){
413 413 $('.show_more').click(function(e){
414 414 var el = e.currentTarget;
415 415 $('#' + el.id.substring(1)).hide();
416 416 $(el.parentNode).show();
417 417 });
418 418 };
419 419
420 420 /**
421 421 * activate .lazy-cs mouseover for showing changeset tooltip
422 422 */
423 423 var show_changeset_tooltip = function(){
424 424 $('.lazy-cs').mouseover(function(e){
425 425 var $target = $(e.currentTarget);
426 426 var rid = $target.attr('raw_id');
427 427 var repo_name = $target.attr('repo_name');
428 428 if(rid && !$target.hasClass('tooltip')){
429 429 _show_tooltip(e, _TM['loading ...']);
430 430 var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": rid});
431 431 ajaxGET(url, function(json){
432 432 $target.addClass('tooltip')
433 433 _show_tooltip(e, json['message']);
434 434 _activate_tooltip($target);
435 435 });
436 436 }
437 437 });
438 438 };
439 439
440 440 var _onSuccessFollow = function(target){
441 441 var $target = $(target);
442 442 var $f_cnt = $('#current_followers_count');
443 443 if($target.hasClass('follow')){
444 444 $target.attr('class', 'following');
445 445 $target.attr('title', _TM['Stop following this repository']);
446 446 if($f_cnt.html()){
447 447 var cnt = Number($f_cnt.html())+1;
448 448 $f_cnt.html(cnt);
449 449 }
450 450 }
451 451 else{
452 452 $target.attr('class', 'follow');
453 453 $target.attr('title', _TM['Start following this repository']);
454 454 if($f_cnt.html()){
455 455 var cnt = Number($f_cnt.html())-1;
456 456 $f_cnt.html(cnt);
457 457 }
458 458 }
459 459 }
460 460
461 var toggleFollowingRepo = function(target, follows_repo_id, token, user_id){
461 var toggleFollowingRepo = function(target, follows_repo_id){
462 462 var args = 'follows_repo_id=' + follows_repo_id;
463 args += '&amp;auth_token=' + token;
464 if(user_id != undefined){
465 args +="&amp;user_id=" + user_id;
466 }
467 463 $.post(TOGGLE_FOLLOW_URL, args, function(data){
468 464 _onSuccessFollow(target);
469 465 });
470 466 return false;
471 467 };
472 468
473 var showRepoSize = function(target, repo_name, token){
474 var args = 'auth_token=' + token;
469 var showRepoSize = function(target, repo_name){
470 var args = '';
475 471
476 472 if(!$("#" + target).hasClass('loaded')){
477 473 $("#" + target).html(_TM['Loading ...']);
478 474 var url = pyroutes.url('repo_size', {"repo_name":repo_name});
479 475 $.post(url, args, function(data) {
480 476 $("#" + target).html(data);
481 477 $("#" + target).addClass('loaded');
482 478 });
483 479 }
484 480 return false;
485 481 };
486 482
487 483 /**
488 484 * tooltips
489 485 */
490 486
491 487 var tooltip_activate = function(){
492 488 $(document).ready(_init_tooltip);
493 489 };
494 490
495 491 var _activate_tooltip = function($tt){
496 492 $tt.mouseover(_show_tooltip);
497 493 $tt.mousemove(_move_tooltip);
498 494 $tt.mouseout(_close_tooltip);
499 495 };
500 496
501 497 var _init_tooltip = function(){
502 498 var $tipBox = $('#tip-box');
503 499 if(!$tipBox.length){
504 500 $tipBox = $('<div id="tip-box"></div>')
505 501 $(document.body).append($tipBox);
506 502 }
507 503
508 504 $tipBox.hide();
509 505 $tipBox.css('position', 'absolute');
510 506 $tipBox.css('max-width', '600px');
511 507
512 508 _activate_tooltip($('.tooltip'));
513 509 };
514 510
515 511 var _show_tooltip = function(e, tipText){
516 512 e.stopImmediatePropagation();
517 513 var el = e.currentTarget;
518 514 if(tipText){
519 515 // just use it
520 516 } else if(el.tagName.toLowerCase() === 'img'){
521 517 tipText = el.alt ? el.alt : '';
522 518 } else {
523 519 tipText = el.title ? el.title : '';
524 520 }
525 521
526 522 if(tipText !== ''){
527 523 // save org title
528 524 $(el).attr('tt_title', tipText);
529 525 // reset title to not show org tooltips
530 526 $(el).attr('title', '');
531 527
532 528 var $tipBox = $('#tip-box');
533 529 $tipBox.html(tipText);
534 530 $tipBox.css('display', 'block');
535 531 }
536 532 };
537 533
538 534 var _move_tooltip = function(e){
539 535 e.stopImmediatePropagation();
540 536 var $tipBox = $('#tip-box');
541 537 $tipBox.css('top', (e.pageY + 15) + 'px');
542 538 $tipBox.css('left', (e.pageX + 15) + 'px');
543 539 };
544 540
545 541 var _close_tooltip = function(e){
546 542 e.stopImmediatePropagation();
547 543 var $tipBox = $('#tip-box');
548 544 $tipBox.hide();
549 545 var el = e.currentTarget;
550 546 $(el).attr('title', $(el).attr('tt_title'));
551 547 };
552 548
553 549 /**
554 550 * Quick filter widget
555 551 *
556 552 * @param target: filter input target
557 553 * @param nodes: list of nodes in html we want to filter.
558 554 * @param display_element function that takes current node from nodes and
559 555 * does hide or show based on the node
560 556 */
561 557 var q_filter = (function() {
562 558 var _namespace = {};
563 559 var namespace = function (target) {
564 560 if (!(target in _namespace)) {
565 561 _namespace[target] = {};
566 562 }
567 563 return _namespace[target];
568 564 };
569 565 return function (target, $nodes, display_element) {
570 566 var $nodes = $nodes;
571 567 var $q_filter_field = $('#' + target);
572 568 var F = namespace(target);
573 569
574 570 $q_filter_field.keyup(function (e) {
575 571 clearTimeout(F.filterTimeout);
576 572 F.filterTimeout = setTimeout(F.updateFilter, 600);
577 573 });
578 574
579 575 F.filterTimeout = null;
580 576
581 577 F.updateFilter = function () {
582 578 // Reset timeout
583 579 F.filterTimeout = null;
584 580
585 581 var obsolete = [];
586 582
587 583 var req = $q_filter_field.val().toLowerCase();
588 584
589 585 var showing = 0;
590 586 $nodes.each(function () {
591 587 var n = this;
592 588 var target_element = display_element(n);
593 589 if (req && n.innerHTML.toLowerCase().indexOf(req) == -1) {
594 590 $(target_element).hide();
595 591 }
596 592 else {
597 593 $(target_element).show();
598 594 showing += 1;
599 595 }
600 596 });
601 597
602 598 $('#repo_count').html(showing);
603 599 /* FIXME: don't hardcode */
604 600 }
605 601 }
606 602 })();
607 603
608 604 /* return jQuery expression with a tr with body in 3rd column and class cls and id named after the body */
609 605 var _table_tr = function(cls, body){
610 606 // like: <div class="comment" id="comment-8" line="o92"><div class="comment-wrapp">...
611 607 // except new inlines which are different ...
612 608 var comment_id = ($(body).attr('id') || 'comment-new').split('comment-')[1];
613 609 var tr_id = 'comment-tr-{0}'.format(comment_id);
614 610 return $(('<tr id="{0}" class="{1}">'+
615 611 '<td class="lineno-inline new-inline"></td>'+
616 612 '<td class="lineno-inline old-inline"></td>'+
617 613 '<td>{2}</td>'+
618 614 '</tr>').format(tr_id, cls, body));
619 615 };
620 616
621 617 /** return jQuery expression with new inline form based on template **/
622 618 var _createInlineForm = function(parent_tr, f_path, line) {
623 619 var $tmpl = $('#comment-inline-form-template').html().format(f_path, line);
624 620 var $form = _table_tr('comment-form-inline', $tmpl)
625 621
626 622 // create event for hide button
627 623 $form.find('.hide-inline-form').click(function(e) {
628 624 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
629 625 if($(newtr).next().hasClass('inline-comments-button')){
630 626 $(newtr).next().show();
631 627 }
632 628 $(newtr).remove();
633 629 $(parent_tr).removeClass('form-open');
634 630 $(parent_tr).removeClass('hl-comment');
635 631 });
636 632
637 633 return $form
638 634 };
639 635
640 636 /**
641 637 * Inject inline comment for an given TR. This tr should always be a .line .
642 638 * The form will be inject after any comments.
643 639 */
644 640 var injectInlineForm = function(tr){
645 641 var $tr = $(tr);
646 642 if(!$tr.hasClass('line')){
647 643 return
648 644 }
649 645 var submit_url = AJAX_COMMENT_URL;
650 646 var $td = $tr.find('.code');
651 647 if($tr.hasClass('form-open') || $tr.hasClass('context') || $td.hasClass('no-comment')){
652 648 return
653 649 }
654 650 $tr.addClass('form-open hl-comment');
655 651 var $node = $tr.parent().parent().parent().find('.full_f_path');
656 652 var f_path = $node.attr('path');
657 653 var lineno = _getLineNo(tr);
658 654 var $form = _createInlineForm(tr, f_path, lineno, submit_url);
659 655
660 656 var $parent = $tr;
661 657 while ($parent.next().hasClass('inline-comments')){
662 658 var $parent = $parent.next();
663 659 }
664 660 $form.insertAfter($parent);
665 661 var $overlay = $form.find('.submitting-overlay');
666 662 var $inlineform = $form.find('.inline-form');
667 663
668 664 $form.submit(function(e){
669 665 e.preventDefault();
670 666
671 667 if(lineno === undefined){
672 668 alert('Error submitting, line ' + lineno + ' not found.');
673 669 return;
674 670 }
675 671 if(f_path === undefined){
676 672 alert('Error submitting, file path ' + f_path + ' not found.');
677 673 return;
678 674 }
679 675
680 676 var text = $('#text_'+lineno).val();
681 677 if(text == ""){
682 678 return;
683 679 }
684 680
685 681 $overlay.show();
686 682
687 683 var success = function(json_data){
688 684 $tr.removeClass('form-open');
689 685 $form.remove();
690 686 _renderInlineComment(json_data);
691 687 };
692 688 var postData = {
693 689 'text': text,
694 690 'f_path': f_path,
695 691 'line': lineno
696 692 };
697 693 ajaxPOST(submit_url, postData, success);
698 694 });
699 695
700 696 $('#preview-btn_'+lineno).click(function(e){
701 697 var text = $('#text_'+lineno).val();
702 698 if(!text){
703 699 return
704 700 }
705 701 $('#preview-box_'+lineno).addClass('unloaded');
706 702 $('#preview-box_'+lineno).html(_TM['Loading ...']);
707 703 $('#edit-container_'+lineno).hide();
708 704 $('#edit-btn_'+lineno).show();
709 705 $('#preview-container_'+lineno).show();
710 706 $('#preview-btn_'+lineno).hide();
711 707
712 708 var url = pyroutes.url('changeset_comment_preview', {'repo_name': REPO_NAME});
713 709 var post_data = {'text': text};
714 710 ajaxPOST(url, post_data, function(html){
715 711 $('#preview-box_'+lineno).html(html);
716 712 $('#preview-box_'+lineno).removeClass('unloaded');
717 713 })
718 714 })
719 715 $('#edit-btn_'+lineno).click(function(e){
720 716 $('#edit-container_'+lineno).show();
721 717 $('#edit-btn_'+lineno).hide();
722 718 $('#preview-container_'+lineno).hide();
723 719 $('#preview-btn_'+lineno).show();
724 720 })
725 721
726 722 setTimeout(function(){
727 723 // callbacks
728 724 tooltip_activate();
729 725 MentionsAutoComplete('text_'+lineno, 'mentions_container_'+lineno,
730 726 _USERS_AC_DATA, _GROUPS_AC_DATA);
731 727 $('#text_'+lineno).focus();
732 728 },10)
733 729 };
734 730
735 731 var deleteComment = function(comment_id){
736 732 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id);
737 733 var postData = {'_method':'delete'};
738 734 var success = function(o){
739 735 var $deleted = $('#comment-tr-'+comment_id);
740 736 var $prev = $deleted.prev('tr');
741 737 $deleted.remove();
742 738 _placeAddButton($prev);
743 739 }
744 740 ajaxPOST(url,postData,success);
745 741 }
746 742
747 743 var _getLineNo = function(tr) {
748 744 var line;
749 745 var o = $(tr).children()[0].id.split('_');
750 746 var n = $(tr).children()[1].id.split('_');
751 747
752 748 if (n.length >= 2) {
753 749 line = n[n.length-1];
754 750 } else if (o.length >= 2) {
755 751 line = o[o.length-1];
756 752 }
757 753
758 754 return line
759 755 };
760 756
761 757 var _placeAddButton = function($line_tr){
762 758 var $tr = $line_tr;
763 759 while ($tr.next().hasClass('inline-comments')){
764 760 $tr.find('.add-comment').remove();
765 761 $tr = $tr.next();
766 762 }
767 763 $tr.find('.add-comment').remove();
768 764 var label = TRANSLATION_MAP['Add Another Comment'];
769 765 var $html_el = $('<div class="add-comment"><span class="btn btn-mini">{0}</span></div>'.format(label));
770 766 $html_el.click(function(e) {
771 767 injectInlineForm($line_tr);
772 768 });
773 769 $tr.find('.comment').after($html_el);
774 770 };
775 771
776 772 /**
777 773 * Places the inline comment into the changeset block in proper line position
778 774 */
779 775 var _placeInline = function(target_id, lineno, html){
780 776 var $td = $("#{0}_{1}".format(target_id, lineno));
781 777 if (!$td.length){
782 778 return false;
783 779 }
784 780
785 781 // check if there are comments already !
786 782 var $line_tr = $td.parent(); // the tr
787 783 var $after_tr = $line_tr;
788 784 while ($after_tr.next().hasClass('inline-comments')){
789 785 $after_tr = $after_tr.next();
790 786 }
791 787 // put in the comment at the bottom
792 788 var $tr = _table_tr('inline-comments', html)
793 789 $tr.find('div.comment').addClass('inline-comment');
794 790 $after_tr.after($tr);
795 791
796 792 // scan nodes, and attach add button to last one
797 793 _placeAddButton($line_tr);
798 794 return true;
799 795 }
800 796
801 797 /**
802 798 * make a single inline comment and place it inside
803 799 */
804 800 var _renderInlineComment = function(json_data){
805 801 var html = json_data['rendered_text'];
806 802 var lineno = json_data['line_no'];
807 803 var target_id = json_data['target_id'];
808 804 return _placeInline(target_id, lineno, html);
809 805 }
810 806
811 807 /**
812 808 * Iterates over all the inlines, and places them inside proper blocks of data
813 809 */
814 810 var renderInlineComments = function(file_comments){
815 811 for (var f in file_comments){
816 812 // holding all comments for a FILE
817 813 var box = file_comments[f];
818 814
819 815 var target_id = $(box).attr('target_id');
820 816 // actual comments with line numbers
821 817 var comments = box.children;
822 818 var obsolete_comments = [];
823 819 for(var i=0; i<comments.length; i++){
824 820 var data = {
825 821 'rendered_text': comments[i].outerHTML,
826 822 'line_no': $(comments[i]).attr('line'),
827 823 'target_id': target_id
828 824 }
829 825 if (_renderInlineComment(data)) {
830 826 obsolete_comments.push(comments[i]);
831 827 $(comments[i]).hide();
832 828 }else{
833 829 var txt = document.createTextNode(
834 830 "Comment to " + YUD.getAttribute(comments[i].parentNode,'path') +
835 831 " line " + data.line_no +
836 832 " which is outside the diff context:");
837 833 comments[i].insertBefore(txt, comments[i].firstChild);
838 834 }
839 835 }
840 836 // now remove all the obsolete comments that have been copied to their
841 837 // respective locations.
842 838 for (var i=0; i < obsolete_comments.length; i++) {
843 839 obsolete_comments[i].parentNode.removeChild(obsolete_comments[i]);
844 840 }
845 841
846 842 $(box).show();
847 843 }
848 844 }
849 845
850 846 /**
851 847 * Double link comments
852 848 */
853 849 var linkInlineComments = function(firstlinks, comments){
854 850 var $comments = $(comments);
855 851 if ($comments.length > 0) {
856 852 $(firstlinks).html('<a href="#{0}">First comment</a>'.format($comments.attr('id')));
857 853 }
858 854 if ($comments.length <= 1) {
859 855 return;
860 856 }
861 857
862 858 $comments.each(function(i, e){
863 859 var prev = '';
864 860 if (i > 0){
865 861 var prev_anchor = YUD.getAttribute(comments.item(i-1),'id');
866 862 prev = '<a href="#{0}">Previous comment</a>'.format(prev_anchor);
867 863 }
868 864 var next = '';
869 865 if (i+1 < comments.length){
870 866 var next_anchor = YUD.getAttribute(comments.item(i+1),'id');
871 867 next = '<a href="#{0}">Next comment</a>'.format(next_anchor);
872 868 }
873 869 var $div = $(('<div class="prev-next-comment">'+
874 870 '<div class="prev-comment">{0}</div>'+
875 871 '<div class="next-comment">{1}</div>').format(prev, next));
876 872 $div.prependTo(this);
877 873 });
878 874 }
879 875
880 876 /* activate files.html stuff */
881 877 var fileBrowserListeners = function(current_url, node_list_url, url_base){
882 878 var current_url_branch = "?branch=__BRANCH__";
883 879
884 880 $('#stay_at_branch').on('click',function(e){
885 881 if(e.currentTarget.checked){
886 882 var uri = current_url_branch;
887 883 uri = uri.replace('__BRANCH__',e.currentTarget.value);
888 884 window.location = uri;
889 885 }
890 886 else{
891 887 window.location = current_url;
892 888 }
893 889 })
894 890
895 891 var $node_filter = $('#node_filter');
896 892
897 893 var filterTimeout = null;
898 894 var nodes = null;
899 895
900 896 var initFilter = function(){
901 897 $('#node_filter_box_loading').show();
902 898 $('#search_activate_id').hide();
903 899 $('#add_node_id').hide();
904 900 $.ajax({url: node_list_url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
905 901 .done(function(json) {
906 902 nodes = json.nodes;
907 903 $('#node_filter_box_loading').hide();
908 904 $('#node_filter_box').show();
909 905 $node_filter.focus();
910 906 if($node_filter.hasClass('init')){
911 907 $node_filter.val('');
912 908 $node_filter.removeClass('init');
913 909 }
914 910 })
915 911 .fail(function() {
916 912 console.log('failed to load');
917 913 })
918 914 ;
919 915 }
920 916
921 917 var updateFilter = function(e) {
922 918 return function(){
923 919 // Reset timeout
924 920 filterTimeout = null;
925 921 var query = e.currentTarget.value.toLowerCase();
926 922 var match = [];
927 923 var matches = 0;
928 924 var matches_max = 20;
929 925 if (query != ""){
930 926 for(var i=0;i<nodes.length;i++){
931 927 var pos = nodes[i].name.toLowerCase().indexOf(query)
932 928 if(query && pos != -1){
933 929 matches++
934 930 //show only certain amount to not kill browser
935 931 if (matches > matches_max){
936 932 break;
937 933 }
938 934
939 935 var n = nodes[i].name;
940 936 var t = nodes[i].type;
941 937 var n_hl = n.substring(0,pos)
942 938 +"<b>{0}</b>".format(n.substring(pos,pos+query.length))
943 939 +n.substring(pos+query.length)
944 940 var new_url = url_base.replace('__FPATH__',n);
945 941 match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
946 942 }
947 943 if(match.length >= matches_max){
948 944 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
949 945 break;
950 946 }
951 947 }
952 948 }
953 949 if(query != ""){
954 950 $('#tbody').hide();
955 951 $('#tbody_filtered').show();
956 952
957 953 if (match.length==0){
958 954 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
959 955 }
960 956
961 957 $('#tbody_filtered').html(match.join(""));
962 958 }
963 959 else{
964 960 $('#tbody').show();
965 961 $('#tbody_filtered').hide();
966 962 }
967 963 }
968 964 };
969 965
970 966 $('#filter_activate').click(function(){
971 967 initFilter();
972 968 });
973 969 $node_filter.click(function(){
974 970 if($node_filter.hasClass('init')){
975 971 $node_filter.val('');
976 972 $node_filter.removeClass('init');
977 973 }
978 974 });
979 975 $node_filter.keyup(function(e){
980 976 clearTimeout(filterTimeout);
981 977 filterTimeout = setTimeout(updateFilter(e),600);
982 978 });
983 979 };
984 980
985 981
986 982 var initCodeMirror = function(textarea_id, resetUrl){
987 983 var myCodeMirror = CodeMirror.fromTextArea($('#' + textarea_id)[0], {
988 984 mode: "null",
989 985 lineNumbers: true,
990 986 indentUnit: 4,
991 987 autofocus: true
992 988 });
993 989 CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
994 990
995 991 $('#reset').click(function(e){
996 992 window.location=resetUrl;
997 993 });
998 994
999 995 $('#file_enable').click(function(){
1000 996 $('#editor_container').show();
1001 997 $('#upload_file_container').hide();
1002 998 $('#filename_container').show();
1003 999 $('#set_mode_header').show();
1004 1000 });
1005 1001
1006 1002 $('#upload_file_enable').click(function(){
1007 1003 $('#editor_container').hide();
1008 1004 $('#upload_file_container').show();
1009 1005 $('#filename_container').hide();
1010 1006 $('#set_mode_header').hide();
1011 1007 });
1012 1008
1013 1009 return myCodeMirror
1014 1010 };
1015 1011
1016 1012 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
1017 1013 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
1018 1014 }
1019 1015
1020 1016
1021 1017 var _getIdentNode = function(n){
1022 1018 //iterate thrugh nodes until matching interesting node
1023 1019
1024 1020 if (typeof n == 'undefined'){
1025 1021 return -1
1026 1022 }
1027 1023
1028 1024 if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
1029 1025 return n
1030 1026 }
1031 1027 else{
1032 1028 return _getIdentNode(n.parentNode);
1033 1029 }
1034 1030 };
1035 1031
1036 1032 /* generate links for multi line selects that can be shown by files.html page_highlights.
1037 1033 * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
1038 1034 var getSelectionLink = function(e) {
1039 1035 //get selection from start/to nodes
1040 1036 if (typeof window.getSelection != "undefined") {
1041 1037 s = window.getSelection();
1042 1038
1043 1039 var from = _getIdentNode(s.anchorNode);
1044 1040 var till = _getIdentNode(s.focusNode);
1045 1041
1046 1042 var f_int = parseInt(from.id.replace('L',''));
1047 1043 var t_int = parseInt(till.id.replace('L',''));
1048 1044
1049 1045 var yoffset = 35;
1050 1046 var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
1051 1047 if (ranges[0] > ranges[1]){
1052 1048 //highlight from bottom
1053 1049 yoffset = -yoffset;
1054 1050 ranges = [ranges[1], ranges[0]];
1055 1051 }
1056 1052 var $hl_div = $('div#linktt');
1057 1053 // if we select more than 2 lines
1058 1054 if (ranges[0] != ranges[1]){
1059 1055 if ($hl_div.length) {
1060 1056 $hl_div.html('');
1061 1057 } else {
1062 1058 $hl_div = $('<div id="linktt" class="hl-tip-box">');
1063 1059 $('body').prepend($hl_div);
1064 1060 }
1065 1061
1066 1062 $hl_div.append($('<a>').html(_TM['Selection link']).attr('href', location.href.substring(0, location.href.indexOf('#')) + '#L' + ranges[0] + '-'+ranges[1]));
1067 1063 var xy = $(till).offset();
1068 1064 $hl_div.css('top', (xy.top + yoffset) + 'px').css('left', xy.left + 'px');
1069 1065 $hl_div.show();
1070 1066 }
1071 1067 else{
1072 1068 $hl_div.hide();
1073 1069 }
1074 1070 }
1075 1071 };
1076 1072
1077 1073 var deleteNotification = function(url, notification_id, callbacks){
1078 1074 var success = function(o){
1079 1075 $("#notification_"+notification_id).remove();
1080 1076 _run_callbacks(callbacks);
1081 1077 };
1082 1078 var failure = function(o){
1083 1079 alert("deleteNotification failure");
1084 1080 };
1085 1081 var postData = {'_method': 'delete'};
1086 1082 var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
1087 1083 ajaxPOST(sUrl, postData, success, failure);
1088 1084 };
1089 1085
1090 1086 var readNotification = function(url, notification_id, callbacks){
1091 1087 var success = function(o){
1092 1088 var $obj = $("#notification_"+notification_id);
1093 1089 $obj.removeClass('unread');
1094 1090 $obj.find('.read-notification').remove();
1095 1091 _run_callbacks(callbacks);
1096 1092 };
1097 1093 var failure = function(o){
1098 1094 alert("readNotification failure");
1099 1095 };
1100 1096 var postData = {'_method': 'put'};
1101 1097 var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
1102 1098 ajaxPOST(sUrl, postData, success, failure);
1103 1099 };
1104 1100
1105 1101 /** MEMBERS AUTOCOMPLETE WIDGET **/
1106 1102
1107 1103 var _MembersAutoComplete = function (divid, cont, users_list, groups_list) {
1108 1104 var myUsers = users_list;
1109 1105 var myGroups = groups_list;
1110 1106
1111 1107 // Define a custom search function for the DataSource of users
1112 1108 var matchUsers = function (sQuery) {
1113 1109 // Case insensitive matching
1114 1110 var query = sQuery.toLowerCase();
1115 1111 var i = 0;
1116 1112 var l = myUsers.length;
1117 1113 var matches = [];
1118 1114
1119 1115 // Match against each name of each contact
1120 1116 for (; i < l; i++) {
1121 1117 var contact = myUsers[i];
1122 1118 if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
1123 1119 ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
1124 1120 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
1125 1121 matches[matches.length] = contact;
1126 1122 }
1127 1123 }
1128 1124 return matches;
1129 1125 };
1130 1126
1131 1127 // Define a custom search function for the DataSource of userGroups
1132 1128 var matchGroups = function (sQuery) {
1133 1129 // Case insensitive matching
1134 1130 var query = sQuery.toLowerCase();
1135 1131 var i = 0;
1136 1132 var l = myGroups.length;
1137 1133 var matches = [];
1138 1134
1139 1135 // Match against each name of each contact
1140 1136 for (; i < l; i++) {
1141 1137 var matched_group = myGroups[i];
1142 1138 if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
1143 1139 matches[matches.length] = matched_group;
1144 1140 }
1145 1141 }
1146 1142 return matches;
1147 1143 };
1148 1144
1149 1145 //match all
1150 1146 var matchAll = function (sQuery) {
1151 1147 var u = matchUsers(sQuery);
1152 1148 var g = matchGroups(sQuery);
1153 1149 return u.concat(g);
1154 1150 };
1155 1151
1156 1152 // DataScheme for members
1157 1153 var memberDS = new YAHOO.util.FunctionDataSource(matchAll);
1158 1154 memberDS.responseSchema = {
1159 1155 fields: ["id", "fname", "lname", "nname", "grname", "grmembers", "gravatar_lnk", "gravatar_size"]
1160 1156 };
1161 1157
1162 1158 // DataScheme for owner
1163 1159 var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
1164 1160 ownerDS.responseSchema = {
1165 1161 fields: ["id", "fname", "lname", "nname", "gravatar_lnk", "gravatar_size"]
1166 1162 };
1167 1163
1168 1164 // Instantiate AutoComplete for perms
1169 1165 var membersAC = new YAHOO.widget.AutoComplete(divid, cont, memberDS);
1170 1166 membersAC.useShadow = false;
1171 1167 membersAC.resultTypeList = false;
1172 1168 membersAC.animVert = false;
1173 1169 membersAC.animHoriz = false;
1174 1170 membersAC.animSpeed = 0.1;
1175 1171
1176 1172 // Instantiate AutoComplete for owner
1177 1173 var ownerAC = new YAHOO.widget.AutoComplete("user", "owner_container", ownerDS);
1178 1174 ownerAC.useShadow = false;
1179 1175 ownerAC.resultTypeList = false;
1180 1176 ownerAC.animVert = false;
1181 1177 ownerAC.animHoriz = false;
1182 1178 ownerAC.animSpeed = 0.1;
1183 1179
1184 1180 // Helper highlight function for the formatter
1185 1181 var highlightMatch = function (full, snippet, matchindex) {
1186 1182 return full.substring(0, matchindex)
1187 1183 + "<span class='match'>"
1188 1184 + full.substr(matchindex, snippet.length)
1189 1185 + "</span>" + full.substring(matchindex + snippet.length);
1190 1186 };
1191 1187
1192 1188 // Custom formatter to highlight the matching letters
1193 1189 var custom_formatter = function (oResultData, sQuery, sResultMatch) {
1194 1190 var query = sQuery.toLowerCase();
1195 1191 var _gravatar = function(res, em, size, group){
1196 1192 var elem = '<img alt="gravatar" class="perm-gravatar-ac" style="width: {0}px; height: {0}px" src="{1}"/>'.format(size, em);
1197 1193 if (!em) {
1198 1194 elem = '<i class="icon-user perm-gravatar-ac" style="font-size: {0}px;"></i>'.format(size);
1199 1195 }
1200 1196 if (group !== undefined){
1201 1197 elem = '<i class="perm-gravatar-ac icon-users"></i>'
1202 1198 }
1203 1199 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'
1204 1200 return tmpl.format(elem,res)
1205 1201 }
1206 1202 // group
1207 1203 if (oResultData.grname != undefined) {
1208 1204 var grname = oResultData.grname;
1209 1205 var grmembers = oResultData.grmembers;
1210 1206 var grnameMatchIndex = grname.toLowerCase().indexOf(query);
1211 1207 var grprefix = "{0}: ".format(_TM['Group']);
1212 1208 var grsuffix = " (" + grmembers + " )";
1213 1209 var grsuffix = " ({0} {1})".format(grmembers, _TM['members']);
1214 1210
1215 1211 if (grnameMatchIndex > -1) {
1216 1212 return _gravatar(grprefix + highlightMatch(grname, query, grnameMatchIndex) + grsuffix,null,null,true);
1217 1213 }
1218 1214 return _gravatar(grprefix + oResultData.grname + grsuffix, null, null, true);
1219 1215 // Users
1220 1216 } else if (oResultData.nname != undefined) {
1221 1217 var fname = oResultData.fname || "";
1222 1218 var lname = oResultData.lname || "";
1223 1219 var nname = oResultData.nname;
1224 1220
1225 1221 // Guard against null value
1226 1222 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
1227 1223 lnameMatchIndex = lname.toLowerCase().indexOf(query),
1228 1224 nnameMatchIndex = nname.toLowerCase().indexOf(query),
1229 1225 displayfname, displaylname, displaynname;
1230 1226
1231 1227 if (fnameMatchIndex > -1) {
1232 1228 displayfname = highlightMatch(fname, query, fnameMatchIndex);
1233 1229 } else {
1234 1230 displayfname = fname;
1235 1231 }
1236 1232
1237 1233 if (lnameMatchIndex > -1) {
1238 1234 displaylname = highlightMatch(lname, query, lnameMatchIndex);
1239 1235 } else {
1240 1236 displaylname = lname;
1241 1237 }
1242 1238
1243 1239 if (nnameMatchIndex > -1) {
1244 1240 displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
1245 1241 } else {
1246 1242 displaynname = nname ? "(" + nname + ")" : "";
1247 1243 }
1248 1244
1249 1245 return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk, oResultData.gravatar_size);
1250 1246 } else {
1251 1247 return '';
1252 1248 }
1253 1249 };
1254 1250 membersAC.formatResult = custom_formatter;
1255 1251 ownerAC.formatResult = custom_formatter;
1256 1252
1257 1253 var myHandler = function (sType, aArgs) {
1258 1254 var nextId = divid.split('perm_new_member_name_')[1];
1259 1255 var myAC = aArgs[0]; // reference back to the AC instance
1260 1256 var elLI = aArgs[1]; // reference to the selected LI element
1261 1257 var oData = aArgs[2]; // object literal of selected item's result data
1262 1258 //fill the autocomplete with value
1263 1259 if (oData.nname != undefined) {
1264 1260 //users
1265 1261 myAC.getInputEl().value = oData.nname;
1266 1262 $('#perm_new_member_type_'+nextId).val('user');
1267 1263 } else {
1268 1264 //groups
1269 1265 myAC.getInputEl().value = oData.grname;
1270 1266 $('#perm_new_member_type_'+nextId).val('users_group');
1271 1267 }
1272 1268 };
1273 1269
1274 1270 membersAC.itemSelectEvent.subscribe(myHandler);
1275 1271 if(ownerAC.itemSelectEvent){
1276 1272 ownerAC.itemSelectEvent.subscribe(myHandler);
1277 1273 }
1278 1274
1279 1275 return {
1280 1276 memberDS: memberDS,
1281 1277 ownerDS: ownerDS,
1282 1278 membersAC: membersAC,
1283 1279 ownerAC: ownerAC
1284 1280 };
1285 1281 }
1286 1282
1287 1283 var MentionsAutoComplete = function (divid, cont, users_list, groups_list) {
1288 1284 var myUsers = users_list;
1289 1285 var myGroups = groups_list;
1290 1286
1291 1287 // Define a custom search function for the DataSource of users
1292 1288 var matchUsers = function (sQuery) {
1293 1289 var org_sQuery = sQuery;
1294 1290 if(this.mentionQuery == null){
1295 1291 return []
1296 1292 }
1297 1293 sQuery = this.mentionQuery;
1298 1294 // Case insensitive matching
1299 1295 var query = sQuery.toLowerCase();
1300 1296 var i = 0;
1301 1297 var l = myUsers.length;
1302 1298 var matches = [];
1303 1299
1304 1300 // Match against each name of each contact
1305 1301 for (; i < l; i++) {
1306 1302 var contact = myUsers[i];
1307 1303 if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
1308 1304 ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
1309 1305 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
1310 1306 matches[matches.length] = contact;
1311 1307 }
1312 1308 }
1313 1309 return matches
1314 1310 };
1315 1311
1316 1312 //match all
1317 1313 var matchAll = function (sQuery) {
1318 1314 return matchUsers(sQuery);
1319 1315 };
1320 1316
1321 1317 // DataScheme for owner
1322 1318 var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
1323 1319
1324 1320 ownerDS.responseSchema = {
1325 1321 fields: ["id", "fname", "lname", "nname", "gravatar_lnk", "gravatar_size"]
1326 1322 };
1327 1323
1328 1324 // Instantiate AutoComplete for mentions
1329 1325 var ownerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
1330 1326 ownerAC.useShadow = false;
1331 1327 ownerAC.resultTypeList = false;
1332 1328 ownerAC.suppressInputUpdate = true;
1333 1329 ownerAC.animVert = false;
1334 1330 ownerAC.animHoriz = false;
1335 1331 ownerAC.animSpeed = 0.1;
1336 1332
1337 1333 // Helper highlight function for the formatter
1338 1334 var highlightMatch = function (full, snippet, matchindex) {
1339 1335 return full.substring(0, matchindex)
1340 1336 + "<span class='match'>"
1341 1337 + full.substr(matchindex, snippet.length)
1342 1338 + "</span>" + full.substring(matchindex + snippet.length);
1343 1339 };
1344 1340
1345 1341 // Custom formatter to highlight the matching letters
1346 1342 ownerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
1347 1343 var org_sQuery = sQuery;
1348 1344 if(this.dataSource.mentionQuery != null){
1349 1345 sQuery = this.dataSource.mentionQuery;
1350 1346 }
1351 1347
1352 1348 var query = sQuery.toLowerCase();
1353 1349 var _gravatar = function(res, em, size, group){
1354 1350 var elem = '<img alt="gravatar" class="perm-gravatar-ac" style="width: {0}px; height: {0}px" src="{1}"/>'.format(size, em);
1355 1351 if (!em) {
1356 1352 elem = '<i class="icon-user perm-gravatar-ac" style="font-size: {0}px;"></i>'.format(size);
1357 1353 }
1358 1354 if (group !== undefined){
1359 1355 elem = '<i class="perm-gravatar-ac icon-users"></i>'
1360 1356 }
1361 1357 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'
1362 1358 return tmpl.format(elem,res)
1363 1359 }
1364 1360 if (oResultData.nname != undefined) {
1365 1361 var fname = oResultData.fname || "";
1366 1362 var lname = oResultData.lname || "";
1367 1363 var nname = oResultData.nname;
1368 1364
1369 1365 // Guard against null value
1370 1366 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
1371 1367 lnameMatchIndex = lname.toLowerCase().indexOf(query),
1372 1368 nnameMatchIndex = nname.toLowerCase().indexOf(query),
1373 1369 displayfname, displaylname, displaynname;
1374 1370
1375 1371 if (fnameMatchIndex > -1) {
1376 1372 displayfname = highlightMatch(fname, query, fnameMatchIndex);
1377 1373 } else {
1378 1374 displayfname = fname;
1379 1375 }
1380 1376
1381 1377 if (lnameMatchIndex > -1) {
1382 1378 displaylname = highlightMatch(lname, query, lnameMatchIndex);
1383 1379 } else {
1384 1380 displaylname = lname;
1385 1381 }
1386 1382
1387 1383 if (nnameMatchIndex > -1) {
1388 1384 displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
1389 1385 } else {
1390 1386 displaynname = nname ? "(" + nname + ")" : "";
1391 1387 }
1392 1388
1393 1389 return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk, oResultData.gravatar_size);
1394 1390 } else {
1395 1391 return '';
1396 1392 }
1397 1393 };
1398 1394
1399 1395 if(ownerAC.itemSelectEvent){
1400 1396 ownerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
1401 1397 var myAC = aArgs[0]; // reference back to the AC instance
1402 1398 var elLI = aArgs[1]; // reference to the selected LI element
1403 1399 var oData = aArgs[2]; // object literal of selected item's result data
1404 1400 //fill the autocomplete with value
1405 1401 if (oData.nname != undefined) {
1406 1402 //users
1407 1403 //Replace the mention name with replaced
1408 1404 var re = new RegExp();
1409 1405 var org = myAC.getInputEl().value;
1410 1406 var chunks = myAC.dataSource.chunks
1411 1407 // replace middle chunk(the search term) with actuall match
1412 1408 chunks[1] = chunks[1].replace('@'+myAC.dataSource.mentionQuery,
1413 1409 '@'+oData.nname+' ');
1414 1410 myAC.getInputEl().value = chunks.join('')
1415 1411 myAC.getInputEl().focus(); // Y U NO WORK !?
1416 1412 } else {
1417 1413 //groups
1418 1414 myAC.getInputEl().value = oData.grname;
1419 1415 $('#perm_new_member_type').val('users_group');
1420 1416 }
1421 1417 });
1422 1418 }
1423 1419
1424 1420 // in this keybuffer we will gather current value of search !
1425 1421 // since we need to get this just when someone does `@` then we do the
1426 1422 // search
1427 1423 ownerAC.dataSource.chunks = [];
1428 1424 ownerAC.dataSource.mentionQuery = null;
1429 1425
1430 1426 ownerAC.get_mention = function(msg, max_pos) {
1431 1427 var org = msg;
1432 1428 // Must match utils2.py MENTIONS_REGEX.
1433 1429 // Only matching on string up to cursor, so it must end with $
1434 1430 var re = new RegExp('(?:^|[^a-zA-Z0-9])@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])$')
1435 1431 var chunks = [];
1436 1432
1437 1433 // cut first chunk until current pos
1438 1434 var to_max = msg.substr(0, max_pos);
1439 1435 var at_pos = Math.max(0,to_max.lastIndexOf('@')-1);
1440 1436 var msg2 = to_max.substr(at_pos);
1441 1437
1442 1438 chunks.push(org.substr(0,at_pos))// prefix chunk
1443 1439 chunks.push(msg2) // search chunk
1444 1440 chunks.push(org.substr(max_pos)) // postfix chunk
1445 1441
1446 1442 // clean up msg2 for filtering and regex match
1447 1443 var msg2 = msg2.lstrip(' ').lstrip('\n');
1448 1444
1449 1445 if(re.test(msg2)){
1450 1446 var unam = re.exec(msg2)[1];
1451 1447 return [unam, chunks];
1452 1448 }
1453 1449 return [null, null];
1454 1450 };
1455 1451
1456 1452 var $divid = $('#'+divid);
1457 1453 $divid.keyup(function(e){
1458 1454 var currentMessage = $divid.val();
1459 1455 var currentCaretPosition = $divid[0].selectionStart;
1460 1456
1461 1457 var unam = ownerAC.get_mention(currentMessage, currentCaretPosition);
1462 1458 var curr_search = null;
1463 1459 if(unam[0]){
1464 1460 curr_search = unam[0];
1465 1461 }
1466 1462
1467 1463 ownerAC.dataSource.chunks = unam[1];
1468 1464 ownerAC.dataSource.mentionQuery = curr_search;
1469 1465 });
1470 1466 }
1471 1467
1472 1468 var addReviewMember = function(id,fname,lname,nname,gravatar_link,gravatar_size){
1473 1469 var displayname = "{0} {1} ({2})".format(fname, lname, nname);
1474 1470 var gravatarelm = '<img alt="gravatar" style="width: {0}px; height: {0}px" src="{1}"/>'.format(gravatar_size, gravatar_link);
1475 1471 if (!gravatar_link)
1476 1472 gravatarelm = '<i class="icon-user" style="font-size: {0}px;"></i>'.format(gravatar_size);
1477 1473 var element = (
1478 1474 ' <li id="reviewer_{2}">\n'+
1479 1475 ' <div class="reviewers_member">\n'+
1480 1476 ' <div class="reviewer_status tooltip" title="not_reviewed">\n'+
1481 1477 ' <i class="icon-circle changeset-status-not_reviewed"></i>\n'+
1482 1478 ' </div>\n'+
1483 1479 ' <div class="reviewer_gravatar gravatar">{0}</div>\n'+
1484 1480 ' <div style="float:left;">{1}</div>\n'+
1485 1481 ' <input type="hidden" value="{2}" name="review_members" />\n'+
1486 1482 ' <div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">\n'+
1487 1483 ' <i class="icon-minus-circled"></i>\n'+
1488 1484 ' </div> (add not saved)\n'+
1489 1485 ' </div>\n'+
1490 1486 ' </li>\n'
1491 1487 ).format(gravatarelm, displayname, id);
1492 1488 // check if we don't have this ID already in
1493 1489 var ids = [];
1494 1490 $('#review_members').find('li').each(function() {
1495 1491 ids.push(this.id);
1496 1492 });
1497 1493 if(ids.indexOf('reviewer_'+id) == -1){
1498 1494 //only add if it's not there
1499 1495 $('#review_members').append(element);
1500 1496 }
1501 1497 }
1502 1498
1503 1499 var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
1504 1500 var $li = $('#reviewer_{0}'.format(reviewer_id));
1505 1501 $li.find('div div').css("text-decoration", "line-through");
1506 1502 $li.find('input').attr('name', 'review_members_removed');
1507 1503 $li.find('.reviewer_member_remove').replaceWith('&nbsp;(remove not saved)');
1508 1504 }
1509 1505
1510 1506 /* activate auto completion of users and groups ... but only used for users as PR reviewers */
1511 1507 var PullRequestAutoComplete = function (divid, cont, users_list, groups_list) {
1512 1508 var myUsers = users_list;
1513 1509 var myGroups = groups_list;
1514 1510
1515 1511 // Define a custom search function for the DataSource of users
1516 1512 var matchUsers = function (sQuery) {
1517 1513 // Case insensitive matching
1518 1514 var query = sQuery.toLowerCase();
1519 1515 var i = 0;
1520 1516 var l = myUsers.length;
1521 1517 var matches = [];
1522 1518
1523 1519 // Match against each name of each contact
1524 1520 for (; i < l; i++) {
1525 1521 var contact = myUsers[i];
1526 1522 if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
1527 1523 ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
1528 1524 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
1529 1525 matches[matches.length] = contact;
1530 1526 }
1531 1527 }
1532 1528 return matches;
1533 1529 };
1534 1530
1535 1531 // Define a custom search function for the DataSource of userGroups
1536 1532 var matchGroups = function (sQuery) {
1537 1533 // Case insensitive matching
1538 1534 var query = sQuery.toLowerCase();
1539 1535 var i = 0;
1540 1536 var l = myGroups.length;
1541 1537 var matches = [];
1542 1538
1543 1539 // Match against each name of each contact
1544 1540 for (; i < l; i++) {
1545 1541 matched_group = myGroups[i];
1546 1542 if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
1547 1543 matches[matches.length] = matched_group;
1548 1544 }
1549 1545 }
1550 1546 return matches;
1551 1547 };
1552 1548
1553 1549 //match all
1554 1550 var matchAll = function (sQuery) {
1555 1551 return matchUsers(sQuery);
1556 1552 };
1557 1553
1558 1554 // DataScheme for owner
1559 1555 var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
1560 1556
1561 1557 ownerDS.responseSchema = {
1562 1558 fields: ["id", "fname", "lname", "nname", "gravatar_lnk", "gravatar_size"]
1563 1559 };
1564 1560
1565 1561 // Instantiate AutoComplete for mentions
1566 1562 var reviewerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
1567 1563 reviewerAC.useShadow = false;
1568 1564 reviewerAC.resultTypeList = false;
1569 1565 reviewerAC.suppressInputUpdate = true;
1570 1566 reviewerAC.animVert = false;
1571 1567 reviewerAC.animHoriz = false;
1572 1568 reviewerAC.animSpeed = 0.1;
1573 1569
1574 1570 // Helper highlight function for the formatter
1575 1571 var highlightMatch = function (full, snippet, matchindex) {
1576 1572 return full.substring(0, matchindex)
1577 1573 + "<span class='match'>"
1578 1574 + full.substr(matchindex, snippet.length)
1579 1575 + "</span>" + full.substring(matchindex + snippet.length);
1580 1576 };
1581 1577
1582 1578 // Custom formatter to highlight the matching letters
1583 1579 reviewerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
1584 1580 var org_sQuery = sQuery;
1585 1581 if(this.dataSource.mentionQuery != null){
1586 1582 sQuery = this.dataSource.mentionQuery;
1587 1583 }
1588 1584
1589 1585 var query = sQuery.toLowerCase();
1590 1586 var _gravatar = function(res, em, size, group){
1591 1587 var elem = '<img alt="gravatar" class="perm-gravatar-ac" style="width: {0}px; height: {0}px" src="{1}"/>'.format(size, em);
1592 1588 if (!em) {
1593 1589 elem = '<i class="icon-user perm-gravatar-ac" style="font-size: {0}px;"></i>'.format(size);
1594 1590 }
1595 1591 if (group !== undefined){
1596 1592 elem = '<i class="perm-gravatar-ac icon-users"></i>'
1597 1593 }
1598 1594 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'
1599 1595 return tmpl.format(elem,res)
1600 1596 }
1601 1597 if (oResultData.nname != undefined) {
1602 1598 var fname = oResultData.fname || "";
1603 1599 var lname = oResultData.lname || "";
1604 1600 var nname = oResultData.nname;
1605 1601
1606 1602 // Guard against null value
1607 1603 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
1608 1604 lnameMatchIndex = lname.toLowerCase().indexOf(query),
1609 1605 nnameMatchIndex = nname.toLowerCase().indexOf(query),
1610 1606 displayfname, displaylname, displaynname;
1611 1607
1612 1608 if (fnameMatchIndex > -1) {
1613 1609 displayfname = highlightMatch(fname, query, fnameMatchIndex);
1614 1610 } else {
1615 1611 displayfname = fname;
1616 1612 }
1617 1613
1618 1614 if (lnameMatchIndex > -1) {
1619 1615 displaylname = highlightMatch(lname, query, lnameMatchIndex);
1620 1616 } else {
1621 1617 displaylname = lname;
1622 1618 }
1623 1619
1624 1620 if (nnameMatchIndex > -1) {
1625 1621 displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
1626 1622 } else {
1627 1623 displaynname = nname ? "(" + nname + ")" : "";
1628 1624 }
1629 1625
1630 1626 return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk, oResultData.gravatar_size);
1631 1627 } else {
1632 1628 return '';
1633 1629 }
1634 1630 };
1635 1631
1636 1632 //members cache to catch duplicates
1637 1633 reviewerAC.dataSource.cache = [];
1638 1634 // hack into select event
1639 1635 if(reviewerAC.itemSelectEvent){
1640 1636 reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
1641 1637
1642 1638 var myAC = aArgs[0]; // reference back to the AC instance
1643 1639 var elLI = aArgs[1]; // reference to the selected LI element
1644 1640 var oData = aArgs[2]; // object literal of selected item's result data
1645 1641
1646 1642 //fill the autocomplete with value
1647 1643 if (oData.nname != undefined) {
1648 1644 addReviewMember(oData.id, oData.fname, oData.lname, oData.nname,
1649 1645 oData.gravatar_lnk, oData.gravatar_size);
1650 1646 myAC.dataSource.cache.push(oData.id);
1651 1647 $('#user').val('');
1652 1648 }
1653 1649 });
1654 1650 }
1655 1651 }
1656 1652
1657 1653 /**
1658 1654 * Activate .quick_repo_menu
1659 1655 */
1660 1656 var quick_repo_menu = function(){
1661 1657 $(".quick_repo_menu").mouseenter(function(e) {
1662 1658 var $menu = $(e.currentTarget).children().first().children().first();
1663 1659 if($menu.hasClass('hidden')){
1664 1660 $menu.removeClass('hidden').addClass('active');
1665 1661 $(e.currentTarget).removeClass('hidden').addClass('active');
1666 1662 }
1667 1663 })
1668 1664 $(".quick_repo_menu").mouseleave(function(e) {
1669 1665 var $menu = $(e.currentTarget).children().first().children().first();
1670 1666 if($menu.hasClass('active')){
1671 1667 $menu.removeClass('active').addClass('hidden');
1672 1668 $(e.currentTarget).removeClass('active').addClass('hidden');
1673 1669 }
1674 1670 })
1675 1671 };
1676 1672
1677 1673
1678 1674 /**
1679 1675 * TABLE SORTING
1680 1676 */
1681 1677
1682 1678 var revisionSort = function(a, b, desc, field) {
1683 1679 var a_ = parseInt(a.getData('last_rev_raw') || 0);
1684 1680 var b_ = parseInt(b.getData('last_rev_raw') || 0);
1685 1681
1686 1682 return YAHOO.util.Sort.compare(a_, b_, desc);
1687 1683 };
1688 1684
1689 1685 var ageSort = function(a, b, desc, field) {
1690 1686 // data is like: <span class="tooltip" date="2014-06-04 18:18:55.325474" title="Wed, 04 Jun 2014 18:18:55">1 day and 23 hours ago</span>
1691 1687 var a_ = $(a.getData(field)).attr('date');
1692 1688 var b_ = $(b.getData(field)).attr('date');
1693 1689
1694 1690 return YAHOO.util.Sort.compare(a_, b_, desc);
1695 1691 };
1696 1692
1697 1693 var lastLoginSort = function(a, b, desc, field) {
1698 1694 var a_ = parseFloat(a.getData('last_login_raw') || 0);
1699 1695 var b_ = parseFloat(b.getData('last_login_raw') || 0);
1700 1696
1701 1697 return YAHOO.util.Sort.compare(a_, b_, desc);
1702 1698 };
1703 1699
1704 1700 var nameSort = function(a, b, desc, field) {
1705 1701 var a_ = a.getData('raw_name') || 0;
1706 1702 var b_ = b.getData('raw_name') || 0;
1707 1703
1708 1704 return YAHOO.util.Sort.compare(a_, b_, desc);
1709 1705 };
1710 1706
1711 1707 var dateSort = function(a, b, desc, field) {
1712 1708 var a_ = parseFloat(a.getData('raw_date') || 0);
1713 1709 var b_ = parseFloat(b.getData('raw_date') || 0);
1714 1710
1715 1711 return YAHOO.util.Sort.compare(a_, b_, desc);
1716 1712 };
1717 1713
1718 1714 var addPermAction = function(_html, users_list, groups_list){
1719 1715 var $last_node = $('.last_new_member').last(); // empty tr between last and add
1720 1716 var next_id = $('.new_members').length;
1721 1717 $last_node.before($('<tr class="new_members">').append(_html.format(next_id)));
1722 1718 _MembersAutoComplete("perm_new_member_name_"+next_id,
1723 1719 "perm_container_"+next_id, users_list, groups_list);
1724 1720 }
1725 1721
1726 1722 function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
1727 1723 var success = function (o) {
1728 1724 $('#' + field_id).remove();
1729 1725 };
1730 1726 var failure = function (o) {
1731 1727 alert(_TM['Failed to revoke permission'] + ": " + o.status);
1732 1728 };
1733 1729 var query_params = {
1734 1730 '_method': 'delete'
1735 1731 }
1736 1732 // put extra data into POST
1737 1733 if (extra_data !== undefined && (typeof extra_data === 'object')){
1738 1734 for(var k in extra_data){
1739 1735 query_params[k] = extra_data[k];
1740 1736 }
1741 1737 }
1742 1738
1743 1739 if (obj_type=='user'){
1744 1740 query_params['user_id'] = obj_id;
1745 1741 query_params['obj_type'] = 'user';
1746 1742 }
1747 1743 else if (obj_type=='user_group'){
1748 1744 query_params['user_group_id'] = obj_id;
1749 1745 query_params['obj_type'] = 'user_group';
1750 1746 }
1751 1747
1752 1748 ajaxPOST(url, query_params, success, failure);
1753 1749 };
1754 1750
1755 1751 /* Multi selectors */
1756 1752
1757 1753 var MultiSelectWidget = function(selected_id, available_id, form_id){
1758 1754 var $availableselect = $('#' + available_id);
1759 1755 var $selectedselect = $('#' + selected_id);
1760 1756
1761 1757 //fill available only with those not in selected
1762 1758 var $selectedoptions = $selectedselect.children('option');
1763 1759 $availableselect.children('option').filter(function(i, e){
1764 1760 for(var j = 0, node; node = $selectedoptions[j]; j++){
1765 1761 if(node.value == e.value){
1766 1762 return true;
1767 1763 }
1768 1764 }
1769 1765 return false;
1770 1766 }).remove();
1771 1767
1772 1768 $('#add_element').click(function(e){
1773 1769 $selectedselect.append($availableselect.children('option:selected'));
1774 1770 });
1775 1771 $('#remove_element').click(function(e){
1776 1772 $availableselect.append($selectedselect.children('option:selected'));
1777 1773 });
1778 1774
1779 1775 $('#'+form_id).submit(function(){
1780 1776 $selectedselect.children('option').each(function(i, e){
1781 1777 e.selected = 'selected';
1782 1778 });
1783 1779 });
1784 1780 }
1785 1781
1786 1782 // custom paginator
1787 1783 var YUI_paginator = function(links_per_page, containers){
1788 1784
1789 1785 (function () {
1790 1786
1791 1787 var Paginator = YAHOO.widget.Paginator,
1792 1788 l = YAHOO.lang,
1793 1789 setId = YAHOO.util.Dom.generateId;
1794 1790
1795 1791 Paginator.ui.MyFirstPageLink = function (p) {
1796 1792 this.paginator = p;
1797 1793
1798 1794 p.subscribe('recordOffsetChange',this.update,this,true);
1799 1795 p.subscribe('rowsPerPageChange',this.update,this,true);
1800 1796 p.subscribe('totalRecordsChange',this.update,this,true);
1801 1797 p.subscribe('destroy',this.destroy,this,true);
1802 1798
1803 1799 // TODO: make this work
1804 1800 p.subscribe('firstPageLinkLabelChange',this.update,this,true);
1805 1801 p.subscribe('firstPageLinkClassChange',this.update,this,true);
1806 1802 };
1807 1803
1808 1804 Paginator.ui.MyFirstPageLink.init = function (p) {
1809 1805 p.setAttributeConfig('firstPageLinkLabel', {
1810 1806 value : 1,
1811 1807 validator : l.isString
1812 1808 });
1813 1809 p.setAttributeConfig('firstPageLinkClass', {
1814 1810 value : 'yui-pg-first',
1815 1811 validator : l.isString
1816 1812 });
1817 1813 p.setAttributeConfig('firstPageLinkTitle', {
1818 1814 value : 'First Page',
1819 1815 validator : l.isString
1820 1816 });
1821 1817 };
1822 1818
1823 1819 // Instance members and methods
1824 1820 Paginator.ui.MyFirstPageLink.prototype = {
1825 1821 current : null,
1826 1822 leftmost_page: null,
1827 1823 rightmost_page: null,
1828 1824 link : null,
1829 1825 span : null,
1830 1826 dotdot : null,
1831 1827 getPos : function(cur_page, max_page, items){
1832 1828 var edge = parseInt(items / 2) + 1;
1833 1829 if (cur_page <= edge){
1834 1830 var radius = Math.max(parseInt(items / 2), items - cur_page);
1835 1831 }
1836 1832 else if ((max_page - cur_page) < edge) {
1837 1833 var radius = (items - 1) - (max_page - cur_page);
1838 1834 }
1839 1835 else{
1840 1836 var radius = parseInt(items / 2);
1841 1837 }
1842 1838
1843 1839 var left = Math.max(1, (cur_page - (radius)))
1844 1840 var right = Math.min(max_page, cur_page + (radius))
1845 1841 return [left, cur_page, right]
1846 1842 },
1847 1843 render : function (id_base) {
1848 1844 var p = this.paginator,
1849 1845 c = p.get('firstPageLinkClass'),
1850 1846 label = p.get('firstPageLinkLabel'),
1851 1847 title = p.get('firstPageLinkTitle');
1852 1848
1853 1849 this.link = document.createElement('a');
1854 1850 this.span = document.createElement('span');
1855 1851 $(this.span).hide();
1856 1852
1857 1853 var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
1858 1854 this.leftmost_page = _pos[0];
1859 1855 this.rightmost_page = _pos[2];
1860 1856
1861 1857 setId(this.link, id_base + '-first-link');
1862 1858 this.link.href = '#';
1863 1859 this.link.className = c;
1864 1860 this.link.innerHTML = label;
1865 1861 this.link.title = title;
1866 1862 YUE.on(this.link,'click',this.onClick,this,true);
1867 1863
1868 1864 setId(this.span, id_base + '-first-span');
1869 1865 this.span.className = c;
1870 1866 this.span.innerHTML = label;
1871 1867
1872 1868 this.current = p.getCurrentPage() > 1 ? this.link : this.span;
1873 1869 return this.current;
1874 1870 },
1875 1871 update : function (e) {
1876 1872 var p = this.paginator;
1877 1873 var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
1878 1874 this.leftmost_page = _pos[0];
1879 1875 this.rightmost_page = _pos[2];
1880 1876
1881 1877 if (e && e.prevValue === e.newValue) {
1882 1878 return;
1883 1879 }
1884 1880
1885 1881 var par = this.current ? this.current.parentNode : null;
1886 1882 if (this.leftmost_page > 1) {
1887 1883 if (par && this.current === this.span) {
1888 1884 par.replaceChild(this.link,this.current);
1889 1885 this.current = this.link;
1890 1886 }
1891 1887 } else {
1892 1888 if (par && this.current === this.link) {
1893 1889 par.replaceChild(this.span,this.current);
1894 1890 this.current = this.span;
1895 1891 }
1896 1892 }
1897 1893 },
1898 1894 destroy : function () {
1899 1895 YUE.purgeElement(this.link);
1900 1896 this.current.parentNode.removeChild(this.current);
1901 1897 this.link = this.span = null;
1902 1898 },
1903 1899 onClick : function (e) {
1904 1900 YUE.stopEvent(e);
1905 1901 this.paginator.setPage(1);
1906 1902 }
1907 1903 };
1908 1904
1909 1905 })();
1910 1906
1911 1907 (function () {
1912 1908
1913 1909 var Paginator = YAHOO.widget.Paginator,
1914 1910 l = YAHOO.lang,
1915 1911 setId = YAHOO.util.Dom.generateId;
1916 1912
1917 1913 Paginator.ui.MyLastPageLink = function (p) {
1918 1914 this.paginator = p;
1919 1915
1920 1916 p.subscribe('recordOffsetChange',this.update,this,true);
1921 1917 p.subscribe('rowsPerPageChange',this.update,this,true);
1922 1918 p.subscribe('totalRecordsChange',this.update,this,true);
1923 1919 p.subscribe('destroy',this.destroy,this,true);
1924 1920
1925 1921 // TODO: make this work
1926 1922 p.subscribe('lastPageLinkLabelChange',this.update,this,true);
1927 1923 p.subscribe('lastPageLinkClassChange', this.update,this,true);
1928 1924 };
1929 1925
1930 1926 Paginator.ui.MyLastPageLink.init = function (p) {
1931 1927 p.setAttributeConfig('lastPageLinkLabel', {
1932 1928 value : -1,
1933 1929 validator : l.isString
1934 1930 });
1935 1931 p.setAttributeConfig('lastPageLinkClass', {
1936 1932 value : 'yui-pg-last',
1937 1933 validator : l.isString
1938 1934 });
1939 1935 p.setAttributeConfig('lastPageLinkTitle', {
1940 1936 value : 'Last Page',
1941 1937 validator : l.isString
1942 1938 });
1943 1939
1944 1940 };
1945 1941
1946 1942 Paginator.ui.MyLastPageLink.prototype = {
1947 1943
1948 1944 current : null,
1949 1945 leftmost_page: null,
1950 1946 rightmost_page: null,
1951 1947 link : null,
1952 1948 span : null,
1953 1949 dotdot : null,
1954 1950 na : null,
1955 1951 getPos : function(cur_page, max_page, items){
1956 1952 var edge = parseInt(items / 2) + 1;
1957 1953 if (cur_page <= edge){
1958 1954 var radius = Math.max(parseInt(items / 2), items - cur_page);
1959 1955 }
1960 1956 else if ((max_page - cur_page) < edge) {
1961 1957 var radius = (items - 1) - (max_page - cur_page);
1962 1958 }
1963 1959 else{
1964 1960 var radius = parseInt(items / 2);
1965 1961 }
1966 1962
1967 1963 var left = Math.max(1, (cur_page - (radius)))
1968 1964 var right = Math.min(max_page, cur_page + (radius))
1969 1965 return [left, cur_page, right]
1970 1966 },
1971 1967 render : function (id_base) {
1972 1968 var p = this.paginator,
1973 1969 c = p.get('lastPageLinkClass'),
1974 1970 label = p.get('lastPageLinkLabel'),
1975 1971 last = p.getTotalPages(),
1976 1972 title = p.get('lastPageLinkTitle');
1977 1973
1978 1974 var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
1979 1975 this.leftmost_page = _pos[0];
1980 1976 this.rightmost_page = _pos[2];
1981 1977
1982 1978 this.link = document.createElement('a');
1983 1979 this.span = document.createElement('span');
1984 1980 $(this.span).hide();
1985 1981
1986 1982 this.na = this.span.cloneNode(false);
1987 1983
1988 1984 setId(this.link, id_base + '-last-link');
1989 1985 this.link.href = '#';
1990 1986 this.link.className = c;
1991 1987 this.link.innerHTML = label;
1992 1988 this.link.title = title;
1993 1989 YUE.on(this.link,'click',this.onClick,this,true);
1994 1990
1995 1991 setId(this.span, id_base + '-last-span');
1996 1992 this.span.className = c;
1997 1993 this.span.innerHTML = label;
1998 1994
1999 1995 setId(this.na, id_base + '-last-na');
2000 1996
2001 1997 if (this.rightmost_page < p.getTotalPages()){
2002 1998 this.current = this.link;
2003 1999 }
2004 2000 else{
2005 2001 this.current = this.span;
2006 2002 }
2007 2003
2008 2004 this.current.innerHTML = p.getTotalPages();
2009 2005 return this.current;
2010 2006 },
2011 2007
2012 2008 update : function (e) {
2013 2009 var p = this.paginator;
2014 2010
2015 2011 var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
2016 2012 this.leftmost_page = _pos[0];
2017 2013 this.rightmost_page = _pos[2];
2018 2014
2019 2015 if (e && e.prevValue === e.newValue) {
2020 2016 return;
2021 2017 }
2022 2018
2023 2019 var par = this.current ? this.current.parentNode : null,
2024 2020 after = this.link;
2025 2021 if (par) {
2026 2022
2027 2023 // only show the last page if the rightmost one is
2028 2024 // lower, so we don't have doubled entries at the end
2029 2025 if (!(this.rightmost_page < p.getTotalPages())){
2030 2026 after = this.span
2031 2027 }
2032 2028
2033 2029 if (this.current !== after) {
2034 2030 par.replaceChild(after,this.current);
2035 2031 this.current = after;
2036 2032 }
2037 2033 }
2038 2034 this.current.innerHTML = this.paginator.getTotalPages();
2039 2035
2040 2036 },
2041 2037 destroy : function () {
2042 2038 YUE.purgeElement(this.link);
2043 2039 this.current.parentNode.removeChild(this.current);
2044 2040 this.link = this.span = null;
2045 2041 },
2046 2042 onClick : function (e) {
2047 2043 YUE.stopEvent(e);
2048 2044 this.paginator.setPage(this.paginator.getTotalPages());
2049 2045 }
2050 2046 };
2051 2047
2052 2048 })();
2053 2049
2054 2050 var pagi = new YAHOO.widget.Paginator({
2055 2051 rowsPerPage: links_per_page,
2056 2052 alwaysVisible: false,
2057 2053 template : "{PreviousPageLink} {MyFirstPageLink} {PageLinks} {MyLastPageLink} {NextPageLink}",
2058 2054 pageLinks: 5,
2059 2055 containerClass: 'pagination-wh',
2060 2056 currentPageClass: 'pager_curpage',
2061 2057 pageLinkClass: 'pager_link',
2062 2058 nextPageLinkLabel: '&gt;',
2063 2059 previousPageLinkLabel: '&lt;',
2064 2060 containers:containers
2065 2061 })
2066 2062
2067 2063 return pagi
2068 2064 }
2069 2065
2070 2066 var YUI_datatable = function(data, fields, columns, countnode, sortkey, rows){
2071 2067 var myDataSource = new YAHOO.util.DataSource(data);
2072 2068 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
2073 2069 myDataSource.responseSchema = {
2074 2070 resultsList: "records",
2075 2071 fields: fields
2076 2072 };
2077 2073 myDataSource.doBeforeCallback = function(req, raw, res, cb) {
2078 2074 // This is the filter function
2079 2075 var data = res.results || [],
2080 2076 filtered = [],
2081 2077 i, l;
2082 2078
2083 2079 if (req) {
2084 2080 req = req.toLowerCase();
2085 2081 for (i = 0; i<data.length; i++) {
2086 2082 var pos = data[i].raw_name.toLowerCase().indexOf(req)
2087 2083 if (pos != -1) {
2088 2084 filtered.push(data[i]);
2089 2085 }
2090 2086 }
2091 2087 res.results = filtered;
2092 2088 }
2093 2089 $(countnode).html(res.results.length);
2094 2090 return res;
2095 2091 }
2096 2092
2097 2093 var myDataTable = new YAHOO.widget.DataTable("datatable_list_wrap", columns, myDataSource, {
2098 2094 sortedBy: {key:sortkey, dir:"asc"},
2099 2095 paginator: YUI_paginator(rows !== undefined && rows ? rows : 25, ['user-paginator']),
2100 2096 MSG_SORTASC: _TM['MSG_SORTASC'],
2101 2097 MSG_SORTDESC: _TM['MSG_SORTDESC'],
2102 2098 MSG_EMPTY: _TM['MSG_EMPTY'],
2103 2099 MSG_ERROR: _TM['MSG_ERROR'],
2104 2100 MSG_LOADING: _TM['MSG_LOADING']
2105 2101 });
2106 2102 myDataTable.subscribe('postRenderEvent',function(oArgs) {
2107 2103 tooltip_activate();
2108 2104 quick_repo_menu();
2109 2105 });
2110 2106
2111 2107 var filterTimeout = null;
2112 2108 var $q_filter = $('#q_filter');
2113 2109
2114 2110 var updateFilter = function () {
2115 2111 // Reset timeout
2116 2112 filterTimeout = null;
2117 2113
2118 2114 // Reset sort
2119 2115 var state = myDataTable.getState();
2120 2116 state.sortedBy = {key:sortkey, dir:YAHOO.widget.DataTable.CLASS_ASC};
2121 2117
2122 2118 // Get filtered data
2123 2119 myDataSource.sendRequest($q_filter.val(), {
2124 2120 success : myDataTable.onDataReturnInitializeTable,
2125 2121 failure : myDataTable.onDataReturnInitializeTable,
2126 2122 scope : myDataTable,
2127 2123 argument: state});
2128 2124 };
2129 2125
2130 2126 $q_filter.click(function(){
2131 2127 if(!$q_filter.hasClass('loaded')){
2132 2128 //TODO: load here full list later to do search within groups
2133 2129 $q_filter.addClass('loaded');
2134 2130 }
2135 2131 });
2136 2132
2137 2133 $q_filter.keyup(function (e) {
2138 2134 clearTimeout(filterTimeout);
2139 2135 filterTimeout = setTimeout(updateFilter, 600);
2140 2136 });
2141 2137 }
2142 2138
2143 2139 // global hooks after DOM is loaded
2144 2140
2145 2141 $(document).ready(function(){
2146 2142 $('.diff-collapse-button').click(function(e) {
2147 2143 var $button = $(e.currentTarget);
2148 2144 var $target = $('#' + $button.attr('target'));
2149 2145 if($target.hasClass('hidden')){
2150 2146 $target.removeClass('hidden');
2151 2147 $button.html("&uarr; {0} &uarr;".format(_TM['Collapse Diff']));
2152 2148 }
2153 2149 else if(!$target.hasClass('hidden')){
2154 2150 $target.addClass('hidden');
2155 2151 $button.html("&darr; {0} &darr;".format(_TM['Expand Diff']));
2156 2152 }
2157 2153 });
2158 2154 });
@@ -1,95 +1,94 b''
1 1 <h3>${_('Parent')}</h3>
2 2 ${h.form(url('edit_repo_advanced_fork', repo_name=c.repo_info.repo_name), method='put')}
3 3 <div class="form">
4 4 ${h.select('id_fork_of','',c.repos_list,class_="medium")}
5 5 ${h.submit('set_as_fork_%s' % c.repo_info.repo_name,_('Set'),class_="btn btn-small")}
6 6 <div class="field" style="border:none;color:#888">
7 7 <ul>
8 8 <li>${_('''Manually set this repository as a fork of another from the list.''')}</li>
9 9 </ul>
10 10 </div>
11 11 </div>
12 12 ${h.end_form()}
13 13
14 14 <script>
15 15 $(document).ready(function(){
16 16 $("#id_fork_of").select2({
17 17 'dropdownAutoWidth': true
18 18 });
19 19 })
20 20 </script>
21 21
22 22 <h3>${_('Public Journal Visibility')}</h3>
23 23 ${h.form(url('edit_repo_advanced_journal', repo_name=c.repo_info.repo_name), method='put')}
24 24 <div class="form">
25 ${h.hidden('auth_token',str(h.get_token()))}
26 25 <div class="field">
27 26 %if c.in_public_journal:
28 27 <button class="btn btn-small" type="submit">
29 28 <i class="icon-minus"></i>
30 29 ${_('Remove from public journal')}
31 30 </button>
32 31 %else:
33 32 <button class="btn btn-small" type="submit">
34 33 <i class="icon-plus"></i>
35 34 ${_('Add to Public Journal')}
36 35 </button>
37 36 %endif
38 37 </div>
39 38 <div class="field" style="border:none;color:#888">
40 39 <ul>
41 40 <li>${_('All actions done in this repository will be visible to everyone in the public journal.')}</li>
42 41 </ul>
43 42 </div>
44 43 </div>
45 44 ${h.end_form()}
46 45
47 46 <h3>${_('Change Locking')}</h3>
48 47 ${h.form(url('edit_repo_advanced_locking', repo_name=c.repo_info.repo_name), method='put')}
49 48 <div class="form">
50 49 %if c.repo_info.locked[0]:
51 50 ${h.hidden('set_unlock', '1')}
52 51 <button class="btn btn-small" type="submit"
53 52 onclick="return confirm('${_('Confirm to unlock repository.')}');">
54 53 <i class="icon-lock-open-alt"></i>
55 54 ${_('Unlock Repository')}
56 55 </button>
57 56 ${'Locked by %s on %s' % (h.person_by_id(c.repo_info.locked[0]),h.fmt_date(h.time_to_datetime(c.repo_info.locked[1])))}
58 57 %else:
59 58 ${h.hidden('set_lock', '1')}
60 59 <button class="btn btn-small" type="submit"
61 60 onclick="return confirm('${_('Confirm to lock repository.')}');">
62 61 <i class="icon-lock"></i>
63 62 ${_('Lock Repository')}
64 63 </button>
65 64 ${_('Repository is not locked')}
66 65 %endif
67 66 <div class="field" style="border:none;color:#888">
68 67 <ul>
69 68 <li>${_('Force locking on the repository. Works only when anonymous access is disabled. Triggering a pull locks the repository. The user who is pulling locks the repository; only the user who pulled and locked it can unlock it by doing a push.')}
70 69 </li>
71 70 </ul>
72 71 </div>
73 72 </div>
74 73 ${h.end_form()}
75 74
76 75 <h3>${_('Delete')}</h3>
77 76 ${h.form(url('repo', repo_name=c.repo_name),method='delete')}
78 77 <div class="form">
79 78 <button class="btn btn-small btn-danger" type="submit"
80 79 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
81 80 <i class="icon-minus-circled"></i>
82 81 ${_('Delete this Repository')}
83 82 </button>
84 83 %if c.repo_info.forks.count():
85 84 ${ungettext('This repository has %s fork', 'This repository has %s forks', c.repo_info.forks.count()) % c.repo_info.forks.count()}
86 85 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
87 86 <input type="radio" name="forks" value="delete_forks" /> <label for="forks">${_('Delete forks')}</label>
88 87 %endif
89 88 <div class="field" style="border:none;color:#888">
90 89 <ul>
91 90 <li>${_('The deleted repository will be moved away and hidden until the administrator expires it. The administrator can both permanently delete it or restore it.')}</li>
92 91 </ul>
93 92 </div>
94 93 </div>
95 94 ${h.end_form()}
@@ -1,596 +1,596 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- CONTENT -->
5 5 <div id="content">
6 6 ${self.flash_msg()}
7 7 <div id="main">
8 8 ${next.main()}
9 9 </div>
10 10 </div>
11 11 <!-- END CONTENT -->
12 12
13 13 <!-- FOOTER -->
14 14 <div id="footer">
15 15 <div id="footer-inner" class="title">
16 16 <div>
17 17 <p class="footer-link">
18 18 ${_('Server instance: %s') % c.instance_id if c.instance_id else ''}
19 19 </p>
20 20 <p class="footer-link-right">
21 21 This site is powered by
22 22 %if c.visual.show_version:
23 23 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a> ${c.kallithea_version},
24 24 %else:
25 25 <a href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
26 26 %endif
27 27 which is
28 28 <a href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2015 by various authors &amp; licensed under GPLv3</a>.
29 29 %if c.issues_url:
30 30 &ndash; <a href="${c.issues_url}" target="_blank">${_('Support')}</a>
31 31 %endif
32 32 </p>
33 33 </div>
34 34 </div>
35 35 </div>
36 36
37 37 <!-- END FOOTER -->
38 38
39 39 ### MAKO DEFS ###
40 40
41 41 <%block name="branding_title">
42 42 %if c.site_name:
43 43 &middot; ${c.site_name}
44 44 %endif
45 45 </%block>
46 46
47 47 <%def name="flash_msg()">
48 48 <%include file="/base/flash_msg.html"/>
49 49 </%def>
50 50
51 51 <%def name="breadcrumbs()">
52 52 <div class="breadcrumbs">
53 53 ${self.breadcrumbs_links()}
54 54 </div>
55 55 </%def>
56 56
57 57 <%def name="admin_menu()">
58 58 <ul class="admin_menu">
59 59 <li><a href="${h.url('admin_home')}"><i class="icon-book"></i> ${_('Admin Journal')}</a></li>
60 60 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
61 61 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
62 62 <li><a href="${h.url('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
63 63 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
64 64 <li><a href="${h.url('admin_permissions')}"><i class="icon-block"></i> ${_('Permissions')}</a></li>
65 65 <li><a href="${h.url('auth_home')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
66 66 <li><a href="${h.url('defaults')}"><i class="icon-wrench"></i> ${_('Defaults')}</a></li>
67 67 <li class="last"><a href="${h.url('admin_settings')}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
68 68 </ul>
69 69
70 70 </%def>
71 71
72 72
73 73 ## admin menu used for people that have some admin resources
74 74 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
75 75 <ul>
76 76 %if repositories:
77 77 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
78 78 %endif
79 79 %if repository_groups:
80 80 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
81 81 %endif
82 82 %if user_groups:
83 83 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
84 84 %endif
85 85 </ul>
86 86 </%def>
87 87
88 88 <%def name="repotag(repo)">
89 89 %if h.is_hg(repo):
90 90 <span class="repotag" title="${_('Mercurial repository')}">hg</span>
91 91 %endif
92 92 %if h.is_git(repo):
93 93 <span class="repotag" title="${_('Git repository')}">git</span>
94 94 %endif
95 95 </%def>
96 96
97 97 <%def name="repo_context_bar(current=None, rev=None)">
98 98 <% rev = None if rev == 'tip' else rev %>
99 99 <%
100 100 def follow_class():
101 101 if c.repository_following:
102 102 return h.literal('following')
103 103 else:
104 104 return h.literal('follow')
105 105 %>
106 106 <%
107 107 def is_current(selected):
108 108 if selected == current:
109 109 return h.literal('class="current"')
110 110 %>
111 111
112 112 <!--- CONTEXT BAR -->
113 113 <div id="context-bar" class="box">
114 114 <h2>
115 115 ${repotag(c.db_repo)}
116 116
117 117 ## public/private
118 118 %if c.db_repo.private:
119 119 <i class="icon-keyhole-circled"></i>
120 120 %else:
121 121 <i class="icon-globe"></i>
122 122 %endif
123 123 ${h.repo_link(c.db_repo.groups_and_repo)}
124 124
125 125 %if current == 'createfork':
126 126 - ${_('Create Fork')}
127 127 %endif
128 128 </h2>
129 129 <!--
130 130 <div id="breadcrumbs">
131 131 ${h.link_to(_(u'Repositories'),h.url('home'))}
132 132 &raquo;
133 133 ${h.repo_link(c.db_repo.groups_and_repo)}
134 134 </div>
135 135 -->
136 136 <ul id="context-pages" class="horizontal-list">
137 137 <li ${is_current('summary')}><a href="${h.url('summary_home', repo_name=c.repo_name)}"><i class="icon-doc-text"></i> ${_('Summary')}</a></li>
138 138 %if rev:
139 139 <li ${is_current('changelog')}><a href="${h.url('changelog_file_home', repo_name=c.repo_name, revision=rev, f_path='')}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
140 140 %else:
141 141 <li ${is_current('changelog')}><a href="${h.url('changelog_home', repo_name=c.repo_name)}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
142 142 %endif
143 143 <li ${is_current('files')}><a href="${h.url('files_home', repo_name=c.repo_name, revision=rev or 'tip')}"><i class="icon-doc-inv"></i> ${_('Files')}</a></li>
144 144 <li ${is_current('switch-to')}>
145 145 <a href="#" id="branch_tag_switcher_2" class="dropdown"><i class="icon-exchange"></i> ${_('Switch To')}</a>
146 146 <ul id="switch_to_list_2" class="switch_to submenu">
147 147 <li><a href="#">${_('Loading...')}</a></li>
148 148 </ul>
149 149 </li>
150 150 <li ${is_current('options')}>
151 151 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
152 152 <a href="${h.url('edit_repo',repo_name=c.repo_name)}" class="dropdown"><i class="icon-wrench"></i> ${_('Options')}</a>
153 153 %else:
154 154 <a href="#" class="dropdown"><i class="icon-wrench"></i> ${_('Options')}</a>
155 155 %endif
156 156 <ul>
157 157 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
158 158 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
159 159 %endif
160 160 %if c.db_repo.fork:
161 161 <li><a href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1], other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}">
162 162 <i class="icon-git-compare"></i> ${_('Compare Fork')}</a></li>
163 163 %endif
164 164 <li><a href="${h.url('compare_home',repo_name=c.repo_name)}"><i class="icon-git-compare"></i> ${_('Compare')}</a></li>
165 165
166 166 <li><a href="${h.url('search_repo',repo_name=c.repo_name)}"><i class="icon-search"></i> ${_('Search')}</a></li>
167 167
168 168 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.db_repo.enable_locking:
169 169 %if c.db_repo.locked[0]:
170 170 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock"></i> ${_('Unlock')}</a></li>
171 171 %else:
172 172 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock-open-alt"></i> ${_('Lock')}</li>
173 173 %endif
174 174 %endif
175 175 ## TODO: this check feels wrong, it would be better to have a check for permissions
176 176 ## also it feels like a job for the controller
177 177 %if c.authuser.username != 'default':
178 178 <li>
179 <a class="${follow_class()}" onclick="javascript:toggleFollowingRepo(this,${c.db_repo.repo_id},'${str(h.get_token())}');">
179 <a class="${follow_class()}" onclick="javascript:toggleFollowingRepo(this,${c.db_repo.repo_id});">
180 180 <span class="show-follow"><i class="icon-heart-empty"></i> ${_('Follow')}</span>
181 181 <span class="show-following"><i class="icon-heart"></i> ${_('Unfollow')}</span>
182 182 </a>
183 183 </li>
184 184 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Fork')}</a></li>
185 185 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Create Pull Request')}</a></li>
186 186 %endif
187 187 </ul>
188 188 </li>
189 189 <li ${is_current('showpullrequest')}>
190 190 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}"> <i class="icon-git-pull-request"></i> ${_('Pull Requests')}
191 191 %if c.repository_pull_requests:
192 192 <span>${c.repository_pull_requests}</span>
193 193 %endif
194 194 </a>
195 195 </li>
196 196 </ul>
197 197 </div>
198 198 <script type="text/javascript">
199 199 YUE.on('branch_tag_switcher_2','mouseover',function(){
200 200 var $branch_tag_switcher_2 = $('#branch_tag_switcher_2');
201 201 var loaded = $branch_tag_switcher_2.hasClass('loaded');
202 202 if(!loaded){
203 203 $branch_tag_switcher_2.addClass('loaded');
204 204 asynchtml("${h.url('branch_tag_switcher',repo_name=c.repo_name)}", $('#switch_to_list_2'));
205 205 }
206 206 return false;
207 207 });
208 208 </script>
209 209 <!--- END CONTEXT BAR -->
210 210 </%def>
211 211
212 212 <%def name="menu(current=None)">
213 213 <%
214 214 def is_current(selected):
215 215 if selected == current:
216 216 return h.literal('class="current"')
217 217 %>
218 218
219 219 <ul id="quick" class="horizontal-list">
220 220 <!-- repo switcher -->
221 221 <li ${is_current('repositories')}>
222 222 <input id="repo_switcher" name="repo_switcher" type="hidden">
223 223 </li>
224 224
225 225 ##ROOT MENU
226 226 %if c.authuser.username != 'default':
227 227 <li ${is_current('journal')}>
228 228 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
229 229 <i class="icon-book"></i> ${_('Journal')}
230 230 </a>
231 231 </li>
232 232 %else:
233 233 <li ${is_current('journal')}>
234 234 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
235 235 <i class="icon-book"></i> ${_('Public journal')}
236 236 </a>
237 237 </li>
238 238 %endif
239 239 <li ${is_current('gists')}>
240 240 <a class="menu_link childs" title="${_('Show public gists')}" href="${h.url('gists')}">
241 241 <i class="icon-clippy"></i> ${_('Gists')}
242 242 </a>
243 243 <ul class="admin_menu">
244 244 <li><a href="${h.url('new_gist', public=1)}"><i class="icon-paste"></i> ${_('Create New Gist')}</a></li>
245 245 <li><a href="${h.url('gists')}"><i class="icon-globe"></i> ${_('All Public Gists')}</a></li>
246 246 %if c.authuser.username != 'default':
247 247 <li><a href="${h.url('gists', public=1)}"><i class="icon-user"></i> ${_('My Public Gists')}</a></li>
248 248 <li><a href="${h.url('gists', private=1)}"><i class="icon-keyhole-circled"></i> ${_('My Private Gists')}</a></li>
249 249 %endif
250 250 </ul>
251 251 </li>
252 252 <li ${is_current('search')}>
253 253 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
254 254 <i class="icon-search"></i> ${_('Search')}
255 255 </a>
256 256 </li>
257 257 % if h.HasPermissionAll('hg.admin')('access admin main page'):
258 258 <li ${is_current('admin')}>
259 259 <a class="menu_link childs" title="${_('Admin')}" href="${h.url('admin_home')}">
260 260 <i class="icon-gear"></i> ${_('Admin')}
261 261 </a>
262 262 ${admin_menu()}
263 263 </li>
264 264 % elif c.authuser.repositories_admin or c.authuser.repository_groups_admin or c.authuser.user_groups_admin:
265 265 <li ${is_current('admin')}>
266 266 <a class="menu_link childs" title="${_('Admin')}">
267 267 <i class="icon-gear"></i> ${_('Admin')}
268 268 </a>
269 269 ${admin_menu_simple(c.authuser.repositories_admin,
270 270 c.authuser.repository_groups_admin,
271 271 c.authuser.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
272 272 </li>
273 273 % endif
274 274
275 275 <li ${is_current('my_pullrequests')}>
276 276 <a class="menu_link" title="${_('My Pull Requests')}" href="${h.url('my_pullrequests')}">
277 277 <i class="icon-git-pull-request"></i> ${_('My Pull Requests')}
278 278 %if c.my_pr_count != 0:
279 279 <span class="menu_link_notifications">${c.my_pr_count}</span>
280 280 %endif
281 281 </a>
282 282 </li>
283 283
284 284 ## USER MENU
285 285 <li>
286 286 <a class="menu_link childs" id="quick_login_link">
287 287 <span class="icon">
288 288 ${h.gravatar(c.authuser.email, size=20)}
289 289 </span>
290 290 %if c.authuser.username != 'default':
291 291 <span class="menu_link_user">${c.authuser.username}</span>
292 292 %if c.unread_notifications != 0:
293 293 <span class="menu_link_notifications">${c.unread_notifications}</span>
294 294 %endif
295 295 %else:
296 296 <span>${_('Not Logged In')}</span>
297 297 %endif
298 298 </a>
299 299
300 300 <div class="user-menu">
301 301 <div id="quick_login">
302 302 %if c.authuser.username == 'default' or c.authuser.user_id is None:
303 303 <h4>${_('Login to Your Account')}</h4>
304 304 ${h.form(h.url('login_home',came_from=h.url.current()))}
305 305 <div class="form">
306 306 <div class="fields">
307 307 <div class="field">
308 308 <div class="label">
309 309 <label for="username">${_('Username')}:</label>
310 310 </div>
311 311 <div class="input">
312 312 ${h.text('username',class_='focus')}
313 313 </div>
314 314
315 315 </div>
316 316 <div class="field">
317 317 <div class="label">
318 318 <label for="password">${_('Password')}:</label>
319 319 </div>
320 320 <div class="input">
321 321 ${h.password('password',class_='focus')}
322 322 </div>
323 323
324 324 </div>
325 325 <div class="buttons">
326 326 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
327 327 <div class="register">
328 328 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
329 329 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
330 330 %endif
331 331 </div>
332 332 <div class="submit">
333 333 ${h.submit('sign_in',_('Log In'),class_="btn btn-mini")}
334 334 </div>
335 335 </div>
336 336 </div>
337 337 </div>
338 338 ${h.end_form()}
339 339 %else:
340 340 <div class="links_left">
341 341 <div class="big_gravatar">
342 342 ${h.gravatar(c.authuser.email, size=48)}
343 343 </div>
344 344 <div class="full_name">${c.authuser.full_name_or_username}</div>
345 345 <div class="email">${c.authuser.email}</div>
346 346 </div>
347 347 <div class="links_right">
348 348 <ol class="links">
349 349 <li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
350 350 <li>${h.link_to(_(u'My Account'),h.url('my_account'))}</li>
351 351 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
352 352 </ol>
353 353 </div>
354 354 %endif
355 355 </div>
356 356 </div>
357 357 </li>
358 358
359 359 <script type="text/javascript">
360 360 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
361 361 var cache = {}
362 362 /*format the look of items in the list*/
363 363 var format = function(state){
364 364 if (!state.id){
365 365 return state.text; // optgroup
366 366 }
367 367 var obj_dict = state.obj;
368 368 var tmpl = '';
369 369
370 370 if(obj_dict && state.type == 'repo'){
371 371 tmpl += '<span class="repo-icons">';
372 372 if(obj_dict['repo_type'] === 'hg'){
373 373 tmpl += '<span class="repotag">hg</span> ';
374 374 }
375 375 else if(obj_dict['repo_type'] === 'git'){
376 376 tmpl += '<span class="repotag">git</span> ';
377 377 }
378 378 if(obj_dict['private']){
379 379 tmpl += '<i class="icon-keyhole-circled"></i> ';
380 380 }
381 381 else if(visual_show_public_icon){
382 382 tmpl += '<i class="icon-globe"></i> ';
383 383 }
384 384 tmpl += '</span>';
385 385 }
386 386 if(obj_dict && state.type == 'group'){
387 387 tmpl += '<i class="icon-folder"></i> ';
388 388 }
389 389 tmpl += state.text;
390 390 return tmpl;
391 391 }
392 392
393 393 $("#repo_switcher").select2({
394 394 placeholder: '<i class="icon-database"></i> ${_('Repositories')}',
395 395 dropdownAutoWidth: true,
396 396 formatResult: format,
397 397 formatSelection: format,
398 398 formatNoMatches: function(term){
399 399 return "${_('No matches found')}";
400 400 },
401 401 containerCssClass: "repo-switcher",
402 402 dropdownCssClass: "repo-switcher-dropdown",
403 403 escapeMarkup: function(m){
404 404 // don't escape our custom placeholder
405 405 if(m.substr(0,29) == '<i class="icon-database"></i>'){
406 406 return m;
407 407 }
408 408
409 409 return Select2.util.escapeMarkup(m);
410 410 },
411 411 query: function(query){
412 412 var key = 'cache';
413 413 var cached = cache[key] ;
414 414 if(cached) {
415 415 var data = {results: []};
416 416 //filter results
417 417 $.each(cached.results, function(){
418 418 var section = this.text;
419 419 var children = [];
420 420 $.each(this.children, function(){
421 421 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
422 422 children.push({'id': this.id, 'text': this.text, 'type': this.type, 'obj': this.obj})
423 423 }
424 424 })
425 425 if(children.length !== 0){
426 426 data.results.push({'text': section, 'children': children})
427 427 }
428 428
429 429 });
430 430 query.callback(data);
431 431 }else{
432 432 $.ajax({
433 433 url: "${h.url('repo_switcher_data')}",
434 434 data: {},
435 435 dataType: 'json',
436 436 type: 'GET',
437 437 success: function(data) {
438 438 cache[key] = data;
439 439 query.callback({results: data.results});
440 440 }
441 441 })
442 442 }
443 443 }
444 444 });
445 445
446 446 $("#repo_switcher").on('select2-selecting', function(e){
447 447 e.preventDefault();
448 448 window.location = pyroutes.url('summary_home', {'repo_name': e.val})
449 449 })
450 450
451 451 ## Global mouse bindings ##
452 452
453 453 // general help "?"
454 454 Mousetrap.bind(['?'], function(e) {
455 455 $('#help_kb').modal({})
456 456 });
457 457
458 458 // / open the quick filter
459 459 Mousetrap.bind(['/'], function(e) {
460 460 $("#repo_switcher").select2("open");
461 461
462 462 // return false to prevent default browser behavior
463 463 // and stop event from bubbling
464 464 return false;
465 465 });
466 466
467 467 // ctrl/command+b, show the the main bar
468 468 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
469 469 if($('#header-inner').hasClass('hover') && $('#content').hasClass('hover')){
470 470 $('#header-inner').removeClass('hover');
471 471 $('#content').removeClass('hover');
472 472 }
473 473 else{
474 474 $('#header-inner').addClass('hover');
475 475 $('#content').addClass('hover');
476 476 }
477 477 return false;
478 478 });
479 479
480 480 // general nav g + action
481 481 Mousetrap.bind(['g h'], function(e) {
482 482 window.location = pyroutes.url('home');
483 483 });
484 484 Mousetrap.bind(['g g'], function(e) {
485 485 window.location = pyroutes.url('gists', {'private':1});
486 486 });
487 487 Mousetrap.bind(['g G'], function(e) {
488 488 window.location = pyroutes.url('gists', {'public':1});
489 489 });
490 490 Mousetrap.bind(['n g'], function(e) {
491 491 window.location = pyroutes.url('new_gist');
492 492 });
493 493 Mousetrap.bind(['n r'], function(e) {
494 494 window.location = pyroutes.url('new_repo');
495 495 });
496 496
497 497 % if hasattr(c, 'repo_name') and hasattr(c, 'db_repo'):
498 498 // nav in repo context
499 499 Mousetrap.bind(['g s'], function(e) {
500 500 window.location = pyroutes.url('summary_home', {'repo_name': REPO_NAME});
501 501 });
502 502 Mousetrap.bind(['g c'], function(e) {
503 503 window.location = pyroutes.url('changelog_home', {'repo_name': REPO_NAME});
504 504 });
505 505 Mousetrap.bind(['g F'], function(e) {
506 506 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': '', 'search': '1'});
507 507 });
508 508 Mousetrap.bind(['g f'], function(e) {
509 509 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.db_repo.landing_rev[1]}', 'f_path': ''});
510 510 });
511 511 Mousetrap.bind(['g o'], function(e) {
512 512 window.location = pyroutes.url('edit_repo', {'repo_name': REPO_NAME});
513 513 });
514 514 Mousetrap.bind(['g O'], function(e) {
515 515 window.location = pyroutes.url('edit_repo_perms', {'repo_name': REPO_NAME});
516 516 });
517 517 % endif
518 518
519 519 </script>
520 520 </%def>
521 521
522 522 %if 0:
523 523 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
524 524 <div class="modal-dialog">
525 525 <div class="modal-content">
526 526 <div class="modal-header">
527 527 <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="icon-cancel-circled"></i></button>
528 528 <h4 class="modal-title">${_('Keyboard shortcuts')}</h4>
529 529 </div>
530 530 <div class="modal-body">
531 531 <div class="row">
532 532 <div class="col-md-5">
533 533 <table class="keyboard-mappings">
534 534 <tbody>
535 535 <tr>
536 536 <th></th>
537 537 <th>${_('Site-wide shortcuts')}</th>
538 538 </tr>
539 539 <%
540 540 elems = [
541 541 ('/', 'Open quick search box'),
542 542 ('ctrl/cmd+b', 'Show main settings bar'),
543 543 ('g h', 'Goto home page'),
544 544 ('g g', 'Goto my private gists page'),
545 545 ('g G', 'Goto my public gists page'),
546 546 ('n r', 'New repository page'),
547 547 ('n g', 'New gist page')
548 548 ]
549 549 %>
550 550 %for key, desc in elems:
551 551 <tr>
552 552 <td class="keys">
553 553 <span class="key">${key}</span>
554 554 </td>
555 555 <td>${desc}</td>
556 556 </tr>
557 557 %endfor
558 558 </tbody>
559 559 </table>
560 560 </div>
561 561 <div class="col-md-offset-5">
562 562 <table class="keyboard-mappings">
563 563 <tbody>
564 564 <tr>
565 565 <th></th>
566 566 <th>${_('Repositories')}</th>
567 567 </tr>
568 568 <%
569 569 elems = [
570 570 ('g s', 'Goto summary page'),
571 571 ('g c', 'Goto changelog page'),
572 572 ('g f', 'Goto files page'),
573 573 ('g F', 'Goto files page with file search activated'),
574 574 ('g o', 'Goto repository settings'),
575 575 ('g O', 'Goto repository permissions settings')
576 576 ]
577 577 %>
578 578 %for key, desc in elems:
579 579 <tr>
580 580 <td class="keys">
581 581 <span class="key">${key}</span>
582 582 </td>
583 583 <td>${desc}</td>
584 584 </tr>
585 585 %endfor
586 586 </tbody>
587 587 </table>
588 588 </div>
589 589 </div>
590 590 </div>
591 591 <div class="modal-footer">
592 592 </div>
593 593 </div><!-- /.modal-content -->
594 594 </div><!-- /.modal-dialog -->
595 595 </div><!-- /.modal -->
596 596 %endif
@@ -1,217 +1,217 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 4
5 5 <%namespace name="base" file="/base/base.html"/>
6 6
7 7 <%def name="quick_menu(repo_name)">
8 8 <ul class="menu_items hidden">
9 9 ##<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
10 10
11 11 <li style="border-top:1px solid #577632; margin-left: 21px; padding-left: -99px;"></li>
12 12 <li>
13 13 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
14 14 <span class="icon">
15 15 <i class="icon-doc-text-inv"></i>
16 16 </span>
17 17 <span>${_('Summary')}</span>
18 18 </a>
19 19 </li>
20 20 <li>
21 21 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
22 22 <span class="icon">
23 23 <i class="icon-clock"></i>
24 24 </span>
25 25 <span>${_('Changelog')}</span>
26 26 </a>
27 27 </li>
28 28 <li>
29 29 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
30 30 <span class="icon">
31 31 <i class="icon-docs"></i>
32 32 </span>
33 33 <span>${_('Files')}</span>
34 34 </a>
35 35 </li>
36 36 <li>
37 37 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
38 38 <span class="icon">
39 39 <i class="icon-fork"></i>
40 40 </span>
41 41 <span>${_('Fork')}</span>
42 42 </a>
43 43 </li>
44 44 <li>
45 45 <a title="${_('Settings')}" href="${h.url('edit_repo',repo_name=repo_name)}">
46 46 <span class="icon">
47 47 <i class="icon-gear"></i>
48 48 </span>
49 49 <span>${_('Settings')}</span>
50 50 </a>
51 51 </li>
52 52 </ul>
53 53 </%def>
54 54
55 55 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
56 56 <%
57 57 def get_name(name,short_name=short_name):
58 58 if short_name:
59 59 return name.split('/')[-1]
60 60 else:
61 61 return name
62 62 %>
63 63 <div class="dt_repo ${'dt_repo_pending' if rstate == 'repo_state_pending' else ''}">
64 64 ##NAME
65 65 <a href="${h.url('edit_repo' if admin else 'summary_home', repo_name=name)}">
66 66
67 67 ##TYPE OF REPO
68 68 ${base.repotag(rtype)}
69 69
70 70 ##PRIVATE/PUBLIC
71 71 %if private and c.visual.show_private_icon:
72 72 <i class="icon-keyhole-circled" title="${_('Private repository')}"></i>
73 73 %elif not private and c.visual.show_public_icon:
74 74 <i class="icon-globe" title="${_('Public repository')}"></i>
75 75 %else:
76 76 <span style="margin: 0px 8px 0px 8px"></span>
77 77 %endif
78 78 <span class="dt_repo_name">${get_name(name)}</span>
79 79 </a>
80 80 %if fork_of:
81 81 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-fork"></i></a>
82 82 %endif
83 83 %if rstate == 'repo_state_pending':
84 84 <i class="icon-wrench" title="${_('Repository creation in progress...')}"></i>
85 85 %endif
86 86 </div>
87 87 </%def>
88 88
89 89 <%def name="last_change(last_change)">
90 90 <span class="tooltip" date="${last_change}" title="${h.tooltip(h.fmt_date(last_change))}">${h.age(last_change)}</span>
91 91 </%def>
92 92
93 93 <%def name="revision(name,rev,tip,author,last_msg)">
94 94 <div>
95 95 %if rev >= 0:
96 96 <a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip revision-link" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a>
97 97 %else:
98 98 ${_('No changesets yet')}
99 99 %endif
100 100 </div>
101 101 </%def>
102 102
103 103 <%def name="rss(name)">
104 104 %if c.authuser.username != 'default':
105 105 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i></a>
106 106 %else:
107 107 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-squared"></i></a>
108 108 %endif
109 109 </%def>
110 110
111 111 <%def name="atom(name)">
112 112 %if c.authuser.username != 'default':
113 113 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i></a>
114 114 %else:
115 115 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-squared"></i></a>
116 116 %endif
117 117 </%def>
118 118
119 119 <%def name="repo_actions(repo_name, super_user=True)">
120 120 <div>
121 121 <div style="float:left; margin-right:5px;" class="grid_edit">
122 122 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('edit')}">
123 123 <i class="icon-pencil"></i> ${h.submit('edit_%s' % repo_name,_('edit'),class_="action_button")}
124 124 </a>
125 125 </div>
126 126 <div style="float:left" class="grid_delete">
127 127 ${h.form(h.url('repo', repo_name=repo_name),method='delete')}
128 128 <i class="icon-minus-circled" style="color:#FF4444"></i>
129 129 ${h.submit('remove_%s' % repo_name,_('delete'),class_="action_button",
130 130 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
131 131 ${h.end_form()}
132 132 </div>
133 133 </div>
134 134 </%def>
135 135
136 136 <%def name="repo_state(repo_state)">
137 137 <div>
138 138 %if repo_state == 'repo_state_pending':
139 139 <div class="btn btn-mini btn-info disabled">${_('Creating')}</div>
140 140 %elif repo_state == 'repo_state_created':
141 141 <div class="btn btn-mini btn-success disabled">${_('Created')}</div>
142 142 %else:
143 143 <div class="btn btn-mini btn-danger disabled" title="${repo_state}">invalid</div>
144 144 %endif
145 145 </div>
146 146 </%def>
147 147
148 148 <%def name="user_actions(user_id, username)">
149 149 <div style="float:left" class="grid_edit">
150 150 <a href="${h.url('edit_user',id=user_id)}" title="${_('edit')}">
151 151 <i class="icon-pencil"></i> ${h.submit('edit_%s' % username,_('edit'),class_="action_button")}
152 152 </a>
153 153 </div>
154 154 <div style="float:left" class="grid_delete">
155 155 ${h.form(h.url('delete_user', id=user_id),method='delete')}
156 156 <i class="icon-minus-circled" style="color:#FF4444"></i>
157 157 ${h.submit('remove_',_('delete'),id="remove_user_%s" % user_id, class_="action_button",
158 158 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
159 159 ${h.end_form()}
160 160 </div>
161 161 </%def>
162 162
163 163 <%def name="user_group_actions(user_group_id, user_group_name)">
164 164 <div style="float:left" class="grid_edit">
165 165 <a href="${h.url('edit_users_group', id=user_group_id)}" title="${_('Edit')}">
166 166 <i class="icon-pencil"></i>
167 167 ${h.submit('edit_%s' % user_group_name,_('edit'),class_="action_button", id_="submit_user_group_edit")}
168 168 </a>
169 169 </div>
170 170 <div style="float:left" class="grid_delete">
171 171 ${h.form(h.url('users_group', id=user_group_id),method='delete')}
172 172 <i class="icon-minus-circled" style="color:#FF4444"></i>
173 173 ${h.submit('remove_',_('delete'),id="remove_group_%s" % user_group_id, class_="action_button",
174 174 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
175 175 ${h.end_form()}
176 176 </div>
177 177 </%def>
178 178
179 179 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
180 180 <div style="float:left" class="grid_edit">
181 181 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">
182 182 <i class="icon-pencil"></i>
183 183 ${h.submit('edit_%s' % repo_group_name, _('edit'),class_="action_button")}
184 184 </a>
185 185 </div>
186 186 <div style="float:left" class="grid_delete">
187 187 ${h.form(h.url('repos_group', group_name=repo_group_name),method='delete')}
188 188 <i class="icon-minus-circled" style="color:#FF4444"></i>
189 189 ${h.submit('remove_%s' % repo_group_name,_('delete'),class_="action_button",
190 190 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
191 191 ${h.end_form()}
192 192 </div>
193 193 </%def>
194 194
195 195 <%def name="user_name(user_id, username)">
196 196 ${h.link_to(username,h.url('edit_user', id=user_id))}
197 197 </%def>
198 198
199 199 <%def name="repo_group_name(repo_group_name, children_groups)">
200 200 <div style="white-space: nowrap">
201 201 <a href="${h.url('repos_group_home',group_name=repo_group_name)}">
202 202 <i class="icon-folder" title="${_('Repository group')}"></i> ${h.literal(' &raquo; '.join(children_groups))}</a>
203 203 </div>
204 204 </%def>
205 205
206 206 <%def name="user_group_name(user_group_id, user_group_name)">
207 207 <div style="white-space: nowrap">
208 208 <a href="${h.url('edit_users_group', id=user_group_id)}">
209 209 <i class="icon-users" title="${_('User group')}"></i> ${user_group_name}</a>
210 210 </div>
211 211 </%def>
212 212
213 213 <%def name="toggle_follow(repo_id)">
214 214 <span id="follow_toggle_${repo_id}" class="following" title="${_('Stop following this repository')}"
215 onclick="javascript:toggleFollowingRepo(this, ${repo_id},'${str(h.get_token())}')">
215 onclick="javascript:toggleFollowingRepo(this, ${repo_id})">
216 216 </span>
217 217 </%def>
@@ -1,400 +1,400 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%block name="title">
4 4 ${_('%s Summary') % c.repo_name}
5 5 </%block>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('Summary')}
9 9
10 10 ## locking icon
11 11 %if c.db_repo.enable_locking:
12 12 %if c.db_repo.locked[0]:
13 13 <span class="locking_locked tooltip icon-block" title="${_('Repository locked by %s') % h.person_by_id(c.db_repo.locked[0])}"></span>
14 14 %else:
15 15 <span class="locking_unlocked tooltip icon-ok" title="${_('Repository unlocked')}"></span>
16 16 %endif
17 17 %endif
18 18
19 19 ##FORK
20 20 %if c.db_repo.fork:
21 21 <span>
22 22 - <i class="icon-fork"></i> ${_('Fork of')} "<a href="${h.url('summary_home',repo_name=c.db_repo.fork.repo_name)}">${c.db_repo.fork.repo_name}</a>"
23 23 </span>
24 24 %endif
25 25
26 26 ##REMOTE
27 27 %if c.db_repo.clone_uri:
28 28 <span>
29 29 - <i class="icon-fork"></i> ${_('Clone from')} "<a href="${h.url(str(h.hide_credentials(c.db_repo.clone_uri)))}">${h.hide_credentials(c.db_repo.clone_uri)}</a>"
30 30 <span>
31 31 %endif
32 32 </%def>
33 33
34 34 <%block name="header_menu">
35 35 ${self.menu('repositories')}
36 36 </%block>
37 37
38 38 <%block name="head_extra">
39 39 <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
40 40 <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
41 41
42 42 <script>
43 43 redirect_hash_branch = function(){
44 44 var branch = window.location.hash.replace(/^#(.*)/, '$1');
45 45 if (branch){
46 46 window.location = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}"
47 47 .replace('__BRANCH__',branch);
48 48 }
49 49 }
50 50 redirect_hash_branch();
51 51 window.onhashchange = function() {
52 52 redirect_hash_branch();
53 53 };
54 54 </script>
55 55 </%block>
56 56
57 57 <%def name="main()">
58 58 ${self.repo_context_bar('summary')}
59 59 <%
60 60 summary = lambda n:{False:'summary-short'}.get(n)
61 61 %>
62 62 <div class="box">
63 63 <!-- box / title -->
64 64 <div class="title">
65 65 ${self.breadcrumbs()}
66 66 </div>
67 67 <!-- end box / title -->
68 68 <div class="form">
69 69 <div id="summary" class="fields">
70 70 <div class="field">
71 71 <div class="label-summary">
72 72 <label>${_('Clone URL')}:</label>
73 73 </div>
74 74 <div class="input ${summary(c.show_stats)}">
75 75 ${self.repotag(c.db_repo)}
76 76 <input style="width:80%" type="text" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
77 77 <input style="display:none;width:80%" type="text" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}"/>
78 78 <div style="display:none" id="clone_by_name" class="btn btn-small">${_('Show by Name')}</div>
79 79 <div id="clone_by_id" class="btn btn-small">${_('Show by ID')}</div>
80 80 </div>
81 81 </div>
82 82
83 83 <div class="field">
84 84 <div class="label-summary">
85 85 <label>${_('Description')}:</label>
86 86 </div>
87 87 %if c.visual.stylify_metatags:
88 88 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.desc_stylize(c.db_repo.description))}</div>
89 89 %else:
90 90 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(c.db_repo.description)}</div>
91 91 %endif
92 92 </div>
93 93
94 94 <div class="field">
95 95 <div class="label-summary">
96 96 <label>${_('Trending files')}:</label>
97 97 </div>
98 98 <div class="input ${summary(c.show_stats)}">
99 99 %if c.show_stats:
100 100 <div id="lang_stats"></div>
101 101 %else:
102 102 ${_('Statistics are disabled for this repository')}
103 103 %if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
104 104 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'),class_="btn btn-mini")}
105 105 %endif
106 106 %endif
107 107 </div>
108 108 </div>
109 109
110 110 <div class="field">
111 111 <div class="label-summary">
112 112 <label>${_('Download')}:</label>
113 113 </div>
114 114 <div class="input ${summary(c.show_stats)}">
115 115 %if len(c.db_repo_scm_instance.revisions) == 0:
116 116 ${_('There are no downloads yet')}
117 117 %elif not c.enable_downloads:
118 118 ${_('Downloads are disabled for this repository')}
119 119 %if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
120 120 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'),class_="btn btn-mini")}
121 121 %endif
122 122 %else:
123 123 <span id="${'zip_link'}">
124 124 <a class="btn btn-small" href="${h.url('files_archive_home',repo_name=c.db_repo.repo_name,fname='tip.zip')}"><i class="icon-file-zip"></i> ${_('Download as zip')}</a>
125 125 </span>
126 126 ${h.hidden('download_options')}
127 127 <span style="vertical-align: bottom">
128 128 <input id="archive_subrepos" type="checkbox" name="subrepos" />
129 129 <label for="archive_subrepos" class="tooltip" title="${h.tooltip(_('Check this to download archive with subrepos'))}" >${_('with subrepos')}</label>
130 130 </span>
131 131 %endif
132 132 </div>
133 133 </div>
134 134 </div>
135 135 <div id="summary-menu-stats">
136 136 <ul>
137 137 <li>
138 138 <a title="${_('Owner')} ${c.db_repo.user.email}">
139 139 <i class="icon-user"></i> ${c.db_repo.user.username}
140 140 <div class="gravatar" style="float: right; margin: 0px 0px 0px 0px" title="${c.db_repo.user.name} ${c.db_repo.user.lastname}">
141 141 ${h.gravatar(c.db_repo.user.email, size=18)}
142 142 </div>
143 143 </a>
144 144 </li>
145 145 <li>
146 146 <a title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
147 147 <i class="icon-heart"></i> ${_('Followers')}
148 148 <span class="stats-bullet" id="current_followers_count">${c.repository_followers}</span>
149 149 </a>
150 150 </li>
151 151 <li>
152 152 <a title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
153 153 <i class="icon-fork"></i> ${_('Forks')}
154 154 <span class="stats-bullet">${c.repository_forks}</span>
155 155 </a>
156 156 </li>
157 157
158 158 %if c.authuser.username != 'default':
159 159 <li class="repo_size">
160 <a href="#" onclick="javascript:showRepoSize('repo_size_2','${c.db_repo.repo_name}','${str(h.get_token())}')"><i class="icon-ruler"></i> ${_('Repository Size')}</a>
160 <a href="#" onclick="javascript:showRepoSize('repo_size_2','${c.db_repo.repo_name}')"><i class="icon-ruler"></i> ${_('Repository Size')}</a>
161 161 <span class="stats-bullet" id="repo_size_2"></span>
162 162 </li>
163 163 %endif
164 164
165 165 <li>
166 166 %if c.authuser.username != 'default':
167 167 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
168 168 %else:
169 169 <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name)}"><i class="icon-rss-squared"></i> ${_('Feed')}</a>
170 170 %endif
171 171 </li>
172 172
173 173 %if c.show_stats:
174 174 <li>
175 175 <a title="${_('Statistics')}" href="${h.url('repo_stats_home',repo_name=c.repo_name)}">
176 176 <i class="icon-graph"></i> ${_('Statistics')}
177 177 </a>
178 178 </li>
179 179 %endif
180 180 </ul>
181 181 </div>
182 182 </div>
183 183 </div>
184 184
185 185
186 186 <div class="box">
187 187 <div class="title">
188 188 <div class="breadcrumbs">
189 189 %if c.repo_changesets:
190 190 ${h.link_to(_('Latest Changes'),h.url('changelog_home',repo_name=c.repo_name))}
191 191 %else:
192 192 ${_('Quick Start')}
193 193 %endif
194 194 </div>
195 195 </div>
196 196 <div class="table">
197 197 <div id="shortlog_data">
198 198 <%include file='../changelog/changelog_summary_data.html'/>
199 199 </div>
200 200 </div>
201 201 </div>
202 202
203 203 %if c.readme_data:
204 204 <div id="readme" class="anchor">
205 205 <div class="box" style="background-color: #FAFAFA">
206 206 <div class="title" title="${_('Readme file from revision %s:%s') % (c.db_repo.landing_rev[0], c.db_repo.landing_rev[1])}">
207 207 <div class="breadcrumbs">
208 208 <a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a>
209 209 </div>
210 210 </div>
211 211 <div class="readme">
212 212 <div class="readme_box">
213 213 ${c.readme_data|n}
214 214 </div>
215 215 </div>
216 216 </div>
217 217 </div>
218 218 %endif
219 219
220 220 <script type="text/javascript">
221 221 $(document).ready(function(){
222 222 var $clone_url = $('#clone_url');
223 223 var $clone_url_id = $('#clone_url_id');
224 224 var $clone_by_name = $('#clone_by_name');
225 225 var $clone_by_id = $('#clone_by_id');
226 226 $clone_url.click(function(e){
227 227 if($clone_url.hasClass('selected')){
228 228 return ;
229 229 }else{
230 230 $clone_url.addClass('selected');
231 231 $clone_url.select();
232 232 }
233 233 });
234 234
235 235 $clone_by_name.click(function(e){
236 236 // show url by name and hide name button
237 237 $clone_url.show();
238 238 $clone_by_name.hide();
239 239
240 240 // hide url by id and show name button
241 241 $clone_by_id.show();
242 242 $clone_url_id.hide();
243 243 });
244 244
245 245 $clone_by_id.click(function(e){
246 246 // show url by id and hide id button
247 247 $clone_by_id.hide();
248 248 $clone_url_id.show();
249 249
250 250 // hide url by name and show id button
251 251 $clone_by_name.show();
252 252 $clone_url.hide();
253 253 });
254 254
255 255 var cache = {}
256 256 $("#download_options").select2({
257 257 placeholder: _TM['Select changeset'],
258 258 dropdownAutoWidth: true,
259 259 query: function(query){
260 260 var key = 'cache';
261 261 var cached = cache[key] ;
262 262 if(cached) {
263 263 var data = {results: []};
264 264 //filter results
265 265 $.each(cached.results, function(){
266 266 var section = this.text;
267 267 var children = [];
268 268 $.each(this.children, function(){
269 269 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
270 270 children.push({'id': this.id, 'text': this.text})
271 271 }
272 272 })
273 273 data.results.push({'text': section, 'children': children})
274 274 });
275 275 query.callback(data);
276 276 }else{
277 277 $.ajax({
278 278 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.repo_name}'}),
279 279 data: {},
280 280 dataType: 'json',
281 281 type: 'GET',
282 282 success: function(data) {
283 283 cache[key] = data;
284 284 query.callback({results: data.results});
285 285 }
286 286 })
287 287 }
288 288 }
289 289 });
290 290 // on change of download options
291 291 $('#download_options').change(function(e){
292 292 var new_cs = e.added
293 293
294 294 for(k in tmpl_links){
295 295 var s = $('#'+k+'_link');
296 296 if(s){
297 297 var title_tmpl = "${_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__')}";
298 298 title_tmpl= title_tmpl.replace('__CS_NAME__',new_cs.text);
299 299 title_tmpl = title_tmpl.replace('__CS_EXT__',k);
300 300 title_tmpl = '<i class="icon-file-zip"></i> '+ title_tmpl;
301 301 var url = tmpl_links[k].replace('__CS__',new_cs.id);
302 302 var subrepos = $('#archive_subrepos').is(':checked');
303 303 url = url.replace('__SUB__',subrepos);
304 304 url = url.replace('__NAME__',title_tmpl);
305 305
306 306 s.html(url)
307 307 }
308 308 }
309 309 });
310 310
311 311 var tmpl_links = {};
312 312 %for cnt,archive in enumerate(c.db_repo_scm_instance._get_archives()):
313 313 tmpl_links["${archive['type']}"] = '${h.link_to('__NAME__', h.url('files_archive_home',repo_name=c.db_repo.repo_name, fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_='btn btn-small')}';
314 314 %endfor
315 315 })
316 316 </script>
317 317
318 318 %if c.show_stats:
319 319 <script type="text/javascript">
320 320 $(document).ready(function(){
321 321 var data = ${c.trending_languages|n};
322 322 var total = 0;
323 323 var no_data = true;
324 324 var tbl = document.createElement('table');
325 325 tbl.setAttribute('class','trending_language_tbl');
326 326 var cnt = 0;
327 327 for (var i=0;i<data.length;i++){
328 328 total+= data[i][1].count;
329 329 }
330 330 for (var i=0;i<data.length;i++){
331 331 cnt += 1;
332 332 no_data = false;
333 333
334 334 var hide = cnt>2;
335 335 var tr = document.createElement('tr');
336 336 if (hide){
337 337 tr.setAttribute('style','display:none');
338 338 tr.setAttribute('class','stats_hidden');
339 339 }
340 340 var k = data[i][0];
341 341 var obj = data[i][1];
342 342 var percentage = Math.round((obj.count/total*100),2);
343 343
344 344 var td1 = document.createElement('td');
345 345 td1.width = 150;
346 346 var trending_language_label = document.createElement('div');
347 347 trending_language_label.innerHTML = obj.desc+" ("+k+")";
348 348 td1.appendChild(trending_language_label);
349 349
350 350 var td2 = document.createElement('td');
351 351 td2.setAttribute('style','padding-right:14px !important');
352 352 var trending_language = document.createElement('div');
353 353 var nr_files = obj.count+" ${_('files')}";
354 354
355 355 trending_language.title = k+" "+nr_files;
356 356
357 357 if (percentage>22){
358 358 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
359 359 }
360 360 else{
361 361 trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
362 362 }
363 363
364 364 trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
365 365 trending_language.style.width=percentage+"%";
366 366 td2.appendChild(trending_language);
367 367
368 368 tr.appendChild(td1);
369 369 tr.appendChild(td2);
370 370 tbl.appendChild(tr);
371 371 if(cnt == 3){
372 372 var show_more = document.createElement('tr');
373 373 var td = document.createElement('td');
374 374 lnk = document.createElement('a');
375 375
376 376 lnk.href='#';
377 377 lnk.innerHTML = "${_('Show more')}";
378 378 lnk.id='code_stats_show_more';
379 379 td.appendChild(lnk);
380 380
381 381 show_more.appendChild(td);
382 382 show_more.appendChild(document.createElement('td'));
383 383 tbl.appendChild(show_more);
384 384 }
385 385
386 386 }
387 387 if (data.length == 0) {
388 388 tbl.innerHTML = "<tr><td>${_('No data ready yet')}</td></tr>";
389 389 }
390 390
391 391 $('#lang_stats').append(tbl);
392 392 $('#code_stats_show_more').click(function(){
393 393 $('.stats_hidden').show();
394 394 $('#code_stats_show_more').hide();
395 395 });
396 396 });
397 397 </script>
398 398 %endif
399 399
400 400 </%def>
@@ -1,39 +1,38 b''
1 1 from kallithea.tests import *
2 2 import datetime
3 3
4 4
5 5 class TestJournalController(TestController):
6 6
7 7 def test_index(self):
8 8 self.log_user()
9 9 response = self.app.get(url(controller='journal', action='index'))
10 10
11 11 response.mustcontain("""<div class="journal_day">%s</div>""" % datetime.date.today())
12 12
13 13 def test_stop_following_repository(self):
14 14 session = self.log_user()
15 15 # usr = Session().query(User).filter(User.username == 'test_admin').one()
16 16 # repo = Session().query(Repository).filter(Repository.repo_name == HG_REPO).one()
17 17 #
18 18 # followings = Session().query(UserFollowing)\
19 19 # .filter(UserFollowing.user == usr)\
20 20 # .filter(UserFollowing.follows_repository == repo).all()
21 21 #
22 22 # assert len(followings) == 1, 'Not following any repository'
23 23 #
24 24 # response = self.app.post(url(controller='journal',
25 25 # action='toggle_following'),
26 # {'auth_token':get_token(session),
27 # 'follows_repo_id':repo.repo_id})
26 # {'follows_repo_id':repo.repo_id})
28 27
29 28 def test_start_following_repository(self):
30 29 self.log_user()
31 30 response = self.app.get(url(controller='journal', action='index'),)
32 31
33 32 def test_public_journal_atom(self):
34 33 self.log_user()
35 34 response = self.app.get(url(controller='journal', action='public_journal_atom'),)
36 35
37 36 def test_public_journal_rss(self):
38 37 self.log_user()
39 38 response = self.app.get(url(controller='journal', action='public_journal_rss'),)
General Comments 0
You need to be logged in to leave comments. Login now