##// END OF EJS Templates
datagrids: save permanently the state if sorting for pull-request grids.
milka -
r4558:c3119a6d default
parent child Browse files
Show More
@@ -1,823 +1,826 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29 from pyramid.view import view_config
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, NotAnonymous, CSRFRequired,
38 38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 39 from rhodecode.lib.channelstream import (
40 40 channelstream_request, ChannelstreamException)
41 41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43 from rhodecode.model.comment import CommentsModel
44 44 from rhodecode.model.db import (
45 45 IntegrityError, or_, in_filter_generator,
46 46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 47 PullRequest, UserBookmark, RepoGroup)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.model.validation_schema.schemas import user_schema
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class MyAccountView(BaseAppView, DataGridAppView):
58 58 ALLOW_SCOPED_TOKENS = False
59 59 """
60 60 This view has alternative version inside EE, if modified please take a look
61 61 in there as well.
62 62 """
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 c.user = c.auth_user.get_instance()
67 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 @view_config(
74 74 route_name='my_account_profile', request_method='GET',
75 75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 76 def my_account_profile(self):
77 77 c = self.load_default_context()
78 78 c.active = 'profile'
79 79 c.extern_type = c.user.extern_type
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 @view_config(
85 85 route_name='my_account_password', request_method='GET',
86 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 87 def my_account_password(self):
88 88 c = self.load_default_context()
89 89 c.active = 'password'
90 90 c.extern_type = c.user.extern_type
91 91
92 92 schema = user_schema.ChangePasswordSchema().bind(
93 93 username=c.user.username)
94 94
95 95 form = forms.Form(
96 96 schema,
97 97 action=h.route_path('my_account_password_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 c.form = form
101 101 return self._get_template_context(c)
102 102
103 103 @LoginRequired()
104 104 @NotAnonymous()
105 105 @CSRFRequired()
106 106 @view_config(
107 107 route_name='my_account_password_update', request_method='POST',
108 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 109 def my_account_password_update(self):
110 110 _ = self.request.translate
111 111 c = self.load_default_context()
112 112 c.active = 'password'
113 113 c.extern_type = c.user.extern_type
114 114
115 115 schema = user_schema.ChangePasswordSchema().bind(
116 116 username=c.user.username)
117 117
118 118 form = forms.Form(
119 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 120
121 121 if c.extern_type != 'rhodecode':
122 122 raise HTTPFound(self.request.route_path('my_account_password'))
123 123
124 124 controls = self.request.POST.items()
125 125 try:
126 126 valid_data = form.validate(controls)
127 127 UserModel().update_user(c.user.user_id, **valid_data)
128 128 c.user.update_userdata(force_password_change=False)
129 129 Session().commit()
130 130 except forms.ValidationFailure as e:
131 131 c.form = e
132 132 return self._get_template_context(c)
133 133
134 134 except Exception:
135 135 log.exception("Exception updating password")
136 136 h.flash(_('Error occurred during update of user password'),
137 137 category='error')
138 138 else:
139 139 instance = c.auth_user.get_instance()
140 140 self.session.setdefault('rhodecode_user', {}).update(
141 141 {'password': md5(instance.password)})
142 142 self.session.save()
143 143 h.flash(_("Successfully updated password"), category='success')
144 144
145 145 raise HTTPFound(self.request.route_path('my_account_password'))
146 146
147 147 @LoginRequired()
148 148 @NotAnonymous()
149 149 @view_config(
150 150 route_name='my_account_auth_tokens', request_method='GET',
151 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 152 def my_account_auth_tokens(self):
153 153 _ = self.request.translate
154 154
155 155 c = self.load_default_context()
156 156 c.active = 'auth_tokens'
157 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 158 c.role_values = [
159 159 (x, AuthTokenModel.cls._get_role_name(x))
160 160 for x in AuthTokenModel.cls.ROLES]
161 161 c.role_options = [(c.role_values, _("Role"))]
162 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 163 c.user.user_id, show_expired=True)
164 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 165 return self._get_template_context(c)
166 166
167 167 @LoginRequired()
168 168 @NotAnonymous()
169 169 @CSRFRequired()
170 170 @view_config(
171 171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 172 renderer='json_ext')
173 173 def my_account_auth_tokens_view(self):
174 174 _ = self.request.translate
175 175 c = self.load_default_context()
176 176
177 177 auth_token_id = self.request.POST.get('auth_token_id')
178 178
179 179 if auth_token_id:
180 180 token = UserApiKeys.get_or_404(auth_token_id)
181 181 if token.user.user_id != c.user.user_id:
182 182 raise HTTPNotFound()
183 183
184 184 return {
185 185 'auth_token': token.api_key
186 186 }
187 187
188 188 def maybe_attach_token_scope(self, token):
189 189 # implemented in EE edition
190 190 pass
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 @CSRFRequired()
195 195 @view_config(
196 196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 197 def my_account_auth_tokens_add(self):
198 198 _ = self.request.translate
199 199 c = self.load_default_context()
200 200
201 201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 202 description = self.request.POST.get('description')
203 203 role = self.request.POST.get('role')
204 204
205 205 token = UserModel().add_auth_token(
206 206 user=c.user.user_id,
207 207 lifetime_minutes=lifetime, role=role, description=description,
208 208 scope_callback=self.maybe_attach_token_scope)
209 209 token_data = token.get_api_data()
210 210
211 211 audit_logger.store_web(
212 212 'user.edit.token.add', action_data={
213 213 'data': {'token': token_data, 'user': 'self'}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 219
220 220 @LoginRequired()
221 221 @NotAnonymous()
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 225 def my_account_auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 del_auth_token = self.request.POST.get('del_auth_token')
230 230
231 231 if del_auth_token:
232 232 token = UserApiKeys.get_or_404(del_auth_token)
233 233 token_data = token.get_api_data()
234 234
235 235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 236 audit_logger.store_web(
237 237 'user.edit.token.delete', action_data={
238 238 'data': {'token': token_data, 'user': 'self'}},
239 239 user=self._rhodecode_user,)
240 240 Session().commit()
241 241 h.flash(_("Auth token successfully deleted"), category='success')
242 242
243 243 return HTTPFound(h.route_path('my_account_auth_tokens'))
244 244
245 245 @LoginRequired()
246 246 @NotAnonymous()
247 247 @view_config(
248 248 route_name='my_account_emails', request_method='GET',
249 249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 250 def my_account_emails(self):
251 251 _ = self.request.translate
252 252
253 253 c = self.load_default_context()
254 254 c.active = 'emails'
255 255
256 256 c.user_email_map = UserEmailMap.query()\
257 257 .filter(UserEmailMap.user == c.user).all()
258 258
259 259 schema = user_schema.AddEmailSchema().bind(
260 260 username=c.user.username, user_emails=c.user.emails)
261 261
262 262 form = forms.RcForm(schema,
263 263 action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 c.form = form
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @NotAnonymous()
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='my_account_emails_add', request_method='POST',
274 274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 275 def my_account_emails_add(self):
276 276 _ = self.request.translate
277 277 c = self.load_default_context()
278 278 c.active = 'emails'
279 279
280 280 schema = user_schema.AddEmailSchema().bind(
281 281 username=c.user.username, user_emails=c.user.emails)
282 282
283 283 form = forms.RcForm(
284 284 schema, action=h.route_path('my_account_emails_add'),
285 285 buttons=(forms.buttons.save, forms.buttons.reset))
286 286
287 287 controls = self.request.POST.items()
288 288 try:
289 289 valid_data = form.validate(controls)
290 290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 291 audit_logger.store_web(
292 292 'user.edit.email.add', action_data={
293 293 'data': {'email': valid_data['email'], 'user': 'self'}},
294 294 user=self._rhodecode_user,)
295 295 Session().commit()
296 296 except formencode.Invalid as error:
297 297 h.flash(h.escape(error.error_dict['email']), category='error')
298 298 except forms.ValidationFailure as e:
299 299 c.user_email_map = UserEmailMap.query() \
300 300 .filter(UserEmailMap.user == c.user).all()
301 301 c.form = e
302 302 return self._get_template_context(c)
303 303 except Exception:
304 304 log.exception("Exception adding email")
305 305 h.flash(_('Error occurred during adding email'),
306 306 category='error')
307 307 else:
308 308 h.flash(_("Successfully added email"), category='success')
309 309
310 310 raise HTTPFound(self.request.route_path('my_account_emails'))
311 311
312 312 @LoginRequired()
313 313 @NotAnonymous()
314 314 @CSRFRequired()
315 315 @view_config(
316 316 route_name='my_account_emails_delete', request_method='POST')
317 317 def my_account_emails_delete(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320
321 321 del_email_id = self.request.POST.get('del_email_id')
322 322 if del_email_id:
323 323 email = UserEmailMap.get_or_404(del_email_id).email
324 324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 325 audit_logger.store_web(
326 326 'user.edit.email.delete', action_data={
327 327 'data': {'email': email, 'user': 'self'}},
328 328 user=self._rhodecode_user,)
329 329 Session().commit()
330 330 h.flash(_("Email successfully deleted"),
331 331 category='success')
332 332 return HTTPFound(h.route_path('my_account_emails'))
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='my_account_notifications_test_channelstream',
339 339 request_method='POST', renderer='json_ext')
340 340 def my_account_notifications_test_channelstream(self):
341 341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 342 self._rhodecode_user.username, datetime.datetime.now())
343 343 payload = {
344 344 # 'channel': 'broadcast',
345 345 'type': 'message',
346 346 'timestamp': datetime.datetime.utcnow(),
347 347 'user': 'system',
348 348 'pm_users': [self._rhodecode_user.username],
349 349 'message': {
350 350 'message': message,
351 351 'level': 'info',
352 352 'topic': '/notifications'
353 353 }
354 354 }
355 355
356 356 registry = self.request.registry
357 357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 358 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 359
360 360 try:
361 361 channelstream_request(channelstream_config, [payload], '/message')
362 362 except ChannelstreamException as e:
363 363 log.exception('Failed to send channelstream data')
364 364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 365 return {"response": 'Channelstream data sent. '
366 366 'You should see a new live message now.'}
367 367
368 368 def _load_my_repos_data(self, watched=False):
369 369
370 370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371 371
372 372 if watched:
373 373 # repos user watch
374 374 repo_list = Session().query(
375 375 Repository
376 376 ) \
377 377 .join(
378 378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 379 ) \
380 380 .filter(
381 381 UserFollowing.user_id == self._rhodecode_user.user_id
382 382 ) \
383 383 .filter(or_(
384 384 # generate multiple IN to fix limitation problems
385 385 *in_filter_generator(Repository.repo_id, allowed_ids))
386 386 ) \
387 387 .order_by(Repository.repo_name) \
388 388 .all()
389 389
390 390 else:
391 391 # repos user is owner of
392 392 repo_list = Session().query(
393 393 Repository
394 394 ) \
395 395 .filter(
396 396 Repository.user_id == self._rhodecode_user.user_id
397 397 ) \
398 398 .filter(or_(
399 399 # generate multiple IN to fix limitation problems
400 400 *in_filter_generator(Repository.repo_id, allowed_ids))
401 401 ) \
402 402 .order_by(Repository.repo_name) \
403 403 .all()
404 404
405 405 _render = self.request.get_partial_renderer(
406 406 'rhodecode:templates/data_table/_dt_elements.mako')
407 407
408 408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 410 short_name=False, admin=False)
411 411
412 412 repos_data = []
413 413 for repo in repo_list:
414 414 row = {
415 415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 416 repo.private, repo.archived, repo.fork),
417 417 "name_raw": repo.repo_name.lower(),
418 418 }
419 419
420 420 repos_data.append(row)
421 421
422 422 # json used to render the grid
423 423 return json.dumps(repos_data)
424 424
425 425 @LoginRequired()
426 426 @NotAnonymous()
427 427 @view_config(
428 428 route_name='my_account_repos', request_method='GET',
429 429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 430 def my_account_repos(self):
431 431 c = self.load_default_context()
432 432 c.active = 'repos'
433 433
434 434 # json used to render the grid
435 435 c.data = self._load_my_repos_data()
436 436 return self._get_template_context(c)
437 437
438 438 @LoginRequired()
439 439 @NotAnonymous()
440 440 @view_config(
441 441 route_name='my_account_watched', request_method='GET',
442 442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 443 def my_account_watched(self):
444 444 c = self.load_default_context()
445 445 c.active = 'watched'
446 446
447 447 # json used to render the grid
448 448 c.data = self._load_my_repos_data(watched=True)
449 449 return self._get_template_context(c)
450 450
451 451 @LoginRequired()
452 452 @NotAnonymous()
453 453 @view_config(
454 454 route_name='my_account_bookmarks', request_method='GET',
455 455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 456 def my_account_bookmarks(self):
457 457 c = self.load_default_context()
458 458 c.active = 'bookmarks'
459 459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 460 self._rhodecode_db_user.user_id, cache=False)
461 461 return self._get_template_context(c)
462 462
463 463 def _process_bookmark_entry(self, entry, user_id):
464 464 position = safe_int(entry.get('position'))
465 465 cur_position = safe_int(entry.get('cur_position'))
466 466 if position is None:
467 467 return
468 468
469 469 # check if this is an existing entry
470 470 is_new = False
471 471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472 472
473 473 if db_entry and str2bool(entry.get('remove')):
474 474 log.debug('Marked bookmark %s for deletion', db_entry)
475 475 Session().delete(db_entry)
476 476 return
477 477
478 478 if not db_entry:
479 479 # new
480 480 db_entry = UserBookmark()
481 481 is_new = True
482 482
483 483 should_save = False
484 484 default_redirect_url = ''
485 485
486 486 # save repo
487 487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 488 repo = Repository.get(entry['bookmark_repo'])
489 489 perm_check = HasRepoPermissionAny(
490 490 'repository.read', 'repository.write', 'repository.admin')
491 491 if repo and perm_check(repo_name=repo.repo_name):
492 492 db_entry.repository = repo
493 493 should_save = True
494 494 default_redirect_url = '${repo_url}'
495 495 # save repo group
496 496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 498 perm_check = HasRepoGroupPermissionAny(
499 499 'group.read', 'group.write', 'group.admin')
500 500
501 501 if repo_group and perm_check(group_name=repo_group.group_name):
502 502 db_entry.repository_group = repo_group
503 503 should_save = True
504 504 default_redirect_url = '${repo_group_url}'
505 505 # save generic info
506 506 elif entry.get('title') and entry.get('redirect_url'):
507 507 should_save = True
508 508
509 509 if should_save:
510 510 # mark user and position
511 511 db_entry.user_id = user_id
512 512 db_entry.position = position
513 513 db_entry.title = entry.get('title')
514 514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516 516
517 517 Session().add(db_entry)
518 518
519 519 @LoginRequired()
520 520 @NotAnonymous()
521 521 @CSRFRequired()
522 522 @view_config(
523 523 route_name='my_account_bookmarks_update', request_method='POST')
524 524 def my_account_bookmarks_update(self):
525 525 _ = self.request.translate
526 526 c = self.load_default_context()
527 527 c.active = 'bookmarks'
528 528
529 529 controls = peppercorn.parse(self.request.POST.items())
530 530 user_id = c.user.user_id
531 531
532 532 # validate positions
533 533 positions = {}
534 534 for entry in controls.get('bookmarks', []):
535 535 position = safe_int(entry['position'])
536 536 if position is None:
537 537 continue
538 538
539 539 if position in positions:
540 540 h.flash(_("Position {} is defined twice. "
541 541 "Please correct this error.").format(position), category='error')
542 542 return HTTPFound(h.route_path('my_account_bookmarks'))
543 543
544 544 entry['position'] = position
545 545 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 546 positions[position] = entry
547 547
548 548 try:
549 549 for entry in positions.values():
550 550 self._process_bookmark_entry(entry, user_id)
551 551
552 552 Session().commit()
553 553 h.flash(_("Update Bookmarks"), category='success')
554 554 except IntegrityError:
555 555 h.flash(_("Failed to update bookmarks. "
556 556 "Make sure an unique position is used."), category='error')
557 557
558 558 return HTTPFound(h.route_path('my_account_bookmarks'))
559 559
560 560 @LoginRequired()
561 561 @NotAnonymous()
562 562 @view_config(
563 563 route_name='my_account_goto_bookmark', request_method='GET',
564 564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 565 def my_account_goto_bookmark(self):
566 566
567 567 bookmark_id = self.request.matchdict['bookmark_id']
568 568 user_bookmark = UserBookmark().query()\
569 569 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 570 .filter(UserBookmark.position == bookmark_id).scalar()
571 571
572 572 redirect_url = h.route_path('my_account_bookmarks')
573 573 if not user_bookmark:
574 574 raise HTTPFound(redirect_url)
575 575
576 576 # repository set
577 577 if user_bookmark.repository:
578 578 repo_name = user_bookmark.repository.repo_name
579 579 base_redirect_url = h.route_path(
580 580 'repo_summary', repo_name=repo_name)
581 581 if user_bookmark.redirect_url and \
582 582 '${repo_url}' in user_bookmark.redirect_url:
583 583 redirect_url = string.Template(user_bookmark.redirect_url)\
584 584 .safe_substitute({'repo_url': base_redirect_url})
585 585 else:
586 586 redirect_url = base_redirect_url
587 587 # repository group set
588 588 elif user_bookmark.repository_group:
589 589 repo_group_name = user_bookmark.repository_group.group_name
590 590 base_redirect_url = h.route_path(
591 591 'repo_group_home', repo_group_name=repo_group_name)
592 592 if user_bookmark.redirect_url and \
593 593 '${repo_group_url}' in user_bookmark.redirect_url:
594 594 redirect_url = string.Template(user_bookmark.redirect_url)\
595 595 .safe_substitute({'repo_group_url': base_redirect_url})
596 596 else:
597 597 redirect_url = base_redirect_url
598 598 # custom URL set
599 599 elif user_bookmark.redirect_url:
600 600 server_url = h.route_url('home').rstrip('/')
601 601 redirect_url = string.Template(user_bookmark.redirect_url) \
602 602 .safe_substitute({'server_url': server_url})
603 603
604 604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 605 raise HTTPFound(redirect_url)
606 606
607 607 @LoginRequired()
608 608 @NotAnonymous()
609 609 @view_config(
610 610 route_name='my_account_perms', request_method='GET',
611 611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 612 def my_account_perms(self):
613 613 c = self.load_default_context()
614 614 c.active = 'perms'
615 615
616 616 c.perm_user = c.auth_user
617 617 return self._get_template_context(c)
618 618
619 619 @LoginRequired()
620 620 @NotAnonymous()
621 621 @view_config(
622 622 route_name='my_account_notifications', request_method='GET',
623 623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 624 def my_notifications(self):
625 625 c = self.load_default_context()
626 626 c.active = 'notifications'
627 627
628 628 return self._get_template_context(c)
629 629
630 630 @LoginRequired()
631 631 @NotAnonymous()
632 632 @CSRFRequired()
633 633 @view_config(
634 634 route_name='my_account_notifications_toggle_visibility',
635 635 request_method='POST', renderer='json_ext')
636 636 def my_notifications_toggle_visibility(self):
637 637 user = self._rhodecode_db_user
638 638 new_status = not user.user_data.get('notification_status', True)
639 639 user.update_userdata(notification_status=new_status)
640 640 Session().commit()
641 641 return user.user_data['notification_status']
642 642
643 643 @LoginRequired()
644 644 @NotAnonymous()
645 645 @view_config(
646 646 route_name='my_account_edit',
647 647 request_method='GET',
648 648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 649 def my_account_edit(self):
650 650 c = self.load_default_context()
651 651 c.active = 'profile_edit'
652 652 c.extern_type = c.user.extern_type
653 653 c.extern_name = c.user.extern_name
654 654
655 655 schema = user_schema.UserProfileSchema().bind(
656 656 username=c.user.username, user_emails=c.user.emails)
657 657 appstruct = {
658 658 'username': c.user.username,
659 659 'email': c.user.email,
660 660 'firstname': c.user.firstname,
661 661 'lastname': c.user.lastname,
662 662 'description': c.user.description,
663 663 }
664 664 c.form = forms.RcForm(
665 665 schema, appstruct=appstruct,
666 666 action=h.route_path('my_account_update'),
667 667 buttons=(forms.buttons.save, forms.buttons.reset))
668 668
669 669 return self._get_template_context(c)
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @CSRFRequired()
674 674 @view_config(
675 675 route_name='my_account_update',
676 676 request_method='POST',
677 677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 678 def my_account_update(self):
679 679 _ = self.request.translate
680 680 c = self.load_default_context()
681 681 c.active = 'profile_edit'
682 682 c.perm_user = c.auth_user
683 683 c.extern_type = c.user.extern_type
684 684 c.extern_name = c.user.extern_name
685 685
686 686 schema = user_schema.UserProfileSchema().bind(
687 687 username=c.user.username, user_emails=c.user.emails)
688 688 form = forms.RcForm(
689 689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690 690
691 691 controls = self.request.POST.items()
692 692 try:
693 693 valid_data = form.validate(controls)
694 694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 695 'new_password', 'password_confirmation']
696 696 if c.extern_type != "rhodecode":
697 697 # forbid updating username for external accounts
698 698 skip_attrs.append('username')
699 699 old_email = c.user.email
700 700 UserModel().update_user(
701 701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 702 **valid_data)
703 703 if old_email != valid_data['email']:
704 704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
705 .filter(UserEmailMap.user == c.user)\
706 .filter(UserEmailMap.email == valid_data['email'])\
707 .first()
706 708 old.email = old_email
707 709 h.flash(_('Your account was updated successfully'), category='success')
708 710 Session().commit()
709 711 except forms.ValidationFailure as e:
710 712 c.form = e
711 713 return self._get_template_context(c)
712 714 except Exception:
713 715 log.exception("Exception updating user")
714 716 h.flash(_('Error occurred during update of user'),
715 717 category='error')
716 718 raise HTTPFound(h.route_path('my_account_profile'))
717 719
718 720 def _get_pull_requests_list(self, statuses):
719 721 draw, start, limit = self._extract_chunk(self.request)
720 722 search_q, order_by, order_dir = self._extract_ordering(self.request)
723
721 724 _render = self.request.get_partial_renderer(
722 725 'rhodecode:templates/data_table/_dt_elements.mako')
723 726
724 727 pull_requests = PullRequestModel().get_im_participating_in(
725 728 user_id=self._rhodecode_user.user_id,
726 729 statuses=statuses, query=search_q,
727 730 offset=start, length=limit, order_by=order_by,
728 731 order_dir=order_dir)
729 732
730 733 pull_requests_total_count = PullRequestModel().count_im_participating_in(
731 734 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732 735
733 736 data = []
734 737 comments_model = CommentsModel()
735 738 for pr in pull_requests:
736 739 repo_id = pr.target_repo_id
737 740 comments_count = comments_model.get_all_comments(
738 741 repo_id, pull_request=pr, include_drafts=False, count_only=True)
739 742 owned = pr.user_id == self._rhodecode_user.user_id
740 743
741 744 data.append({
742 745 'target_repo': _render('pullrequest_target_repo',
743 746 pr.target_repo.repo_name),
744 747 'name': _render('pullrequest_name',
745 748 pr.pull_request_id, pr.pull_request_state,
746 749 pr.work_in_progress, pr.target_repo.repo_name,
747 750 short=True),
748 751 'name_raw': pr.pull_request_id,
749 752 'status': _render('pullrequest_status',
750 753 pr.calculated_review_status()),
751 754 'title': _render('pullrequest_title', pr.title, pr.description),
752 755 'description': h.escape(pr.description),
753 756 'updated_on': _render('pullrequest_updated_on',
754 757 h.datetime_to_time(pr.updated_on),
755 758 pr.versions_count),
756 759 'updated_on_raw': h.datetime_to_time(pr.updated_on),
757 760 'created_on': _render('pullrequest_updated_on',
758 761 h.datetime_to_time(pr.created_on)),
759 762 'created_on_raw': h.datetime_to_time(pr.created_on),
760 763 'state': pr.pull_request_state,
761 764 'author': _render('pullrequest_author',
762 765 pr.author.full_contact, ),
763 766 'author_raw': pr.author.full_name,
764 767 'comments': _render('pullrequest_comments', comments_count),
765 768 'comments_raw': comments_count,
766 769 'closed': pr.is_closed(),
767 770 'owned': owned
768 771 })
769 772
770 773 # json used to render the grid
771 774 data = ({
772 775 'draw': draw,
773 776 'data': data,
774 777 'recordsTotal': pull_requests_total_count,
775 778 'recordsFiltered': pull_requests_total_count,
776 779 })
777 780 return data
778 781
779 782 @LoginRequired()
780 783 @NotAnonymous()
781 784 @view_config(
782 785 route_name='my_account_pullrequests',
783 786 request_method='GET',
784 787 renderer='rhodecode:templates/admin/my_account/my_account.mako')
785 788 def my_account_pullrequests(self):
786 789 c = self.load_default_context()
787 790 c.active = 'pullrequests'
788 791 req_get = self.request.GET
789 792
790 793 c.closed = str2bool(req_get.get('pr_show_closed'))
791 794
792 795 return self._get_template_context(c)
793 796
794 797 @LoginRequired()
795 798 @NotAnonymous()
796 799 @view_config(
797 800 route_name='my_account_pullrequests_data',
798 801 request_method='GET', renderer='json_ext')
799 802 def my_account_pullrequests_data(self):
800 803 self.load_default_context()
801 804 req_get = self.request.GET
802 805 closed = str2bool(req_get.get('closed'))
803 806
804 807 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
805 808 if closed:
806 809 statuses += [PullRequest.STATUS_CLOSED]
807 810
808 811 data = self._get_pull_requests_list(statuses=statuses)
809 812 return data
810 813
811 814 @LoginRequired()
812 815 @NotAnonymous()
813 816 @view_config(
814 817 route_name='my_account_user_group_membership',
815 818 request_method='GET',
816 819 renderer='rhodecode:templates/admin/my_account/my_account.mako')
817 820 def my_account_user_group_membership(self):
818 821 c = self.load_default_context()
819 822 c.active = 'user_group_membership'
820 823 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
821 824 for group in self._rhodecode_db_user.group_member]
822 825 c.user_groups = json.dumps(groups)
823 826 return self._get_template_context(c)
@@ -1,706 +1,716 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
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 Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 container.show();
80 80 if (!container.hasClass('loaded')) {
81 81 $.ajax({url: url})
82 82 .complete(function (data) {
83 83 var responseJSON = data.responseJSON;
84 84 container.addClass('loaded');
85 85 container.html(responseJSON.size);
86 86 callback(responseJSON.code_stats)
87 87 })
88 88 .fail(function (data) {
89 89 console.log('failed to load repo stats');
90 90 });
91 91 }
92 92
93 93 };
94 94
95 95 var showRepoStats = function(target, data){
96 96 var container = $('#' + target);
97 97
98 98 if (container.hasClass('loaded')) {
99 99 return
100 100 }
101 101
102 102 var total = 0;
103 103 var no_data = true;
104 104 var tbl = document.createElement('table');
105 105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106 106
107 107 $.each(data, function(key, val){
108 108 total += val.count;
109 109 });
110 110
111 111 var sortedStats = [];
112 112 for (var obj in data){
113 113 sortedStats.push([obj, data[obj]])
114 114 }
115 115 var sortedData = sortedStats.sort(function (a, b) {
116 116 return b[1].count - a[1].count
117 117 });
118 118 var cnt = 0;
119 119 $.each(sortedData, function(idx, val){
120 120 cnt += 1;
121 121 no_data = false;
122 122
123 123 var tr = document.createElement('tr');
124 124
125 125 var key = val[0];
126 126 var obj = {"desc": val[1].desc, "count": val[1].count};
127 127
128 128 // meta language names
129 129 var td1 = document.createElement('td');
130 130 var trending_language_label = document.createElement('div');
131 131 trending_language_label.innerHTML = obj.desc;
132 132 td1.appendChild(trending_language_label);
133 133
134 134 // extensions
135 135 var td2 = document.createElement('td');
136 136 var extension = document.createElement('div');
137 137 extension.innerHTML = ".{0}".format(key)
138 138 td2.appendChild(extension);
139 139
140 140 // number of files
141 141 var td3 = document.createElement('td');
142 142 var file_count = document.createElement('div');
143 143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 144 var label = _ngettext('file', 'files', obj.count);
145 145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 146 td3.appendChild(file_count);
147 147
148 148 // percentage
149 149 var td4 = document.createElement('td');
150 150 td4.setAttribute("class", 'trending_language');
151 151
152 152 var percentage = document.createElement('div');
153 153 percentage.setAttribute('class', 'lang-bar');
154 154 percentage.innerHTML = "&nbsp;";
155 155 percentage.style.width = percentage_num + '%';
156 156 td4.appendChild(percentage);
157 157
158 158 tr.appendChild(td1);
159 159 tr.appendChild(td2);
160 160 tr.appendChild(td3);
161 161 tr.appendChild(td4);
162 162 tbl.appendChild(tr);
163 163
164 164 });
165 165
166 166 $(container).html(tbl);
167 167 $(container).addClass('loaded');
168 168
169 169 $('#code_stats_show_more').on('click', function (e) {
170 170 e.preventDefault();
171 171 $('.stats_hidden').each(function (idx) {
172 172 $(this).css("display", "");
173 173 });
174 174 $('#code_stats_show_more').hide();
175 175 });
176 176
177 177 };
178 178
179 179 // returns a node from given html;
180 180 var fromHTML = function(html){
181 181 var _html = document.createElement('element');
182 182 _html.innerHTML = html;
183 183 return _html;
184 184 };
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207
208 208 var clipboardActivate = function() {
209 209 /*
210 210 *
211 211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 212 * */
213 213 var clipboard = new ClipboardJS('.clipboard-action');
214 214
215 215 clipboard.on('success', function(e) {
216 216 var callback = function () {
217 217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 218 };
219 219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 220 e.clearSelection();
221 221 });
222 222 };
223 223
224 224 var tooltipActivate = function () {
225 225 var delay = 50;
226 226 var animation = 'fade';
227 227 var theme = 'tooltipster-shadow';
228 228 var debug = false;
229 229
230 230 $('.tooltip').tooltipster({
231 231 debug: debug,
232 232 theme: theme,
233 233 animation: animation,
234 234 delay: delay,
235 235 contentCloning: true,
236 236 contentAsHTML: true,
237 237
238 238 functionBefore: function (instance, helper) {
239 239 var $origin = $(helper.origin);
240 240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 241 instance.content(data);
242 242 }
243 243 });
244 244 var hovercardCache = {};
245 245
246 246 var loadHoverCard = function (url, altHovercard, callback) {
247 247 var id = url;
248 248
249 249 if (hovercardCache[id] !== undefined) {
250 250 callback(hovercardCache[id]);
251 251 return true;
252 252 }
253 253
254 254 hovercardCache[id] = undefined;
255 255 $.get(url, function (data) {
256 256 hovercardCache[id] = data;
257 257 callback(hovercardCache[id]);
258 258 return true;
259 259 }).fail(function (data, textStatus, errorThrown) {
260 260
261 261 if (parseInt(data.status) === 404) {
262 262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 263 } else {
264 264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 265 }
266 266 callback(msg);
267 267 return false
268 268 });
269 269 };
270 270
271 271 $('.tooltip-hovercard').tooltipster({
272 272 debug: debug,
273 273 theme: theme,
274 274 animation: animation,
275 275 delay: delay,
276 276 interactive: true,
277 277 contentCloning: true,
278 278
279 279 trigger: 'custom',
280 280 triggerOpen: {
281 281 mouseenter: true,
282 282 },
283 283 triggerClose: {
284 284 mouseleave: true,
285 285 originClick: true,
286 286 touchleave: true
287 287 },
288 288 content: _gettext('Loading...'),
289 289 contentAsHTML: true,
290 290 updateAnimation: null,
291 291
292 292 functionBefore: function (instance, helper) {
293 293
294 294 var $origin = $(helper.origin);
295 295
296 296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 297 if ($origin.data('loaded') !== true) {
298 298 var hovercardUrl = $origin.data('hovercardUrl');
299 299 var altHovercard = $origin.data('hovercardAlt');
300 300
301 301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 302 var urlLoad = true;
303 303 if (hovercardUrl.substr(0, 12) === 'pyroutes.url') {
304 304 hovercardUrl = eval(hovercardUrl)
305 305 } else if (hovercardUrl.substr(0, 11) === 'javascript:') {
306 306 var jsFunc = hovercardUrl.substr(11);
307 307 urlLoad = false;
308 308 loaded = true;
309 309 instance.content(eval(jsFunc))
310 310 }
311 311
312 312 if (urlLoad) {
313 313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
314 314 instance.content(data);
315 315 })
316 316 }
317 317
318 318 } else {
319 319 if ($origin.data('hovercardAltHtml')) {
320 320 var data = atob($origin.data('hovercardAltHtml'));
321 321 } else {
322 322 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
323 323 }
324 324 var loaded = true;
325 325 instance.content(data);
326 326 }
327 327
328 328 // to remember that the data has been loaded
329 329 $origin.data('loaded', loaded);
330 330 }
331 331 }
332 332 })
333 333 };
334 334
335 335 // Formatting values in a Select2 dropdown of commit references
336 336 var formatSelect2SelectionRefs = function(commit_ref){
337 337 var tmpl = '';
338 338 if (!commit_ref.text || commit_ref.type === 'sha'){
339 339 return commit_ref.text;
340 340 }
341 341 if (commit_ref.type === 'branch'){
342 342 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
343 343 } else if (commit_ref.type === 'tag'){
344 344 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
345 345 } else if (commit_ref.type === 'book'){
346 346 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
347 347 }
348 348 return tmpl.concat(escapeHtml(commit_ref.text));
349 349 };
350 350
351 351 // takes a given html element and scrolls it down offset pixels
352 352 function offsetScroll(element, offset) {
353 353 setTimeout(function() {
354 354 var location = element.offset().top;
355 355 // some browsers use body, some use html
356 356 $('html, body').animate({ scrollTop: (location - offset) });
357 357 }, 100);
358 358 }
359 359
360 360 // scroll an element `percent`% from the top of page in `time` ms
361 361 function scrollToElement(element, percent, time) {
362 362 percent = (percent === undefined ? 25 : percent);
363 363 time = (time === undefined ? 100 : time);
364 364
365 365 var $element = $(element);
366 366 if ($element.length == 0) {
367 367 throw('Cannot scroll to {0}'.format(element))
368 368 }
369 369 var elOffset = $element.offset().top;
370 370 var elHeight = $element.height();
371 371 var windowHeight = $(window).height();
372 372 var offset = elOffset;
373 373 if (elHeight < windowHeight) {
374 374 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
375 375 }
376 376 setTimeout(function() {
377 377 $('html, body').animate({ scrollTop: offset});
378 378 }, time);
379 379 }
380 380
381 381 /**
382 382 * global hooks after DOM is loaded
383 383 */
384 384 $(document).ready(function() {
385 385 firefoxAnchorFix();
386 386
387 387 $('.navigation a.menulink').on('click', function(e){
388 388 var menuitem = $(this).parent('li');
389 389 if (menuitem.hasClass('open')) {
390 390 menuitem.removeClass('open');
391 391 } else {
392 392 menuitem.addClass('open');
393 393 $(document).on('click', function(event) {
394 394 if (!$(event.target).closest(menuitem).length) {
395 395 menuitem.removeClass('open');
396 396 }
397 397 });
398 398 }
399 399 });
400 400
401 401 $('body').on('click', '.cb-lineno a', function(event) {
402 402 function sortNumber(a,b) {
403 403 return a - b;
404 404 }
405 405
406 406 var lineNo = $(this).data('lineNo');
407 407 var lineName = $(this).attr('name');
408 408
409 409 if (lineNo) {
410 410 var prevLine = $('.cb-line-selected a').data('lineNo');
411 411
412 412 // on shift, we do a range selection, if we got previous line
413 413 if (event.shiftKey && prevLine !== undefined) {
414 414 var prevLine = parseInt(prevLine);
415 415 var nextLine = parseInt(lineNo);
416 416 var pos = [prevLine, nextLine].sort(sortNumber);
417 417 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
418 418
419 419 // single click
420 420 } else {
421 421 var nextLine = parseInt(lineNo);
422 422 var pos = [nextLine, nextLine];
423 423 var anchor = '#L{0}'.format(pos[0]);
424 424
425 425 }
426 426 // highlight
427 427 var range = [];
428 428 for (var i = pos[0]; i <= pos[1]; i++) {
429 429 range.push(i);
430 430 }
431 431 // clear old selected lines
432 432 $('.cb-line-selected').removeClass('cb-line-selected');
433 433
434 434 $.each(range, function (i, lineNo) {
435 435 var line_td = $('td.cb-lineno#L' + lineNo);
436 436
437 437 if (line_td.length) {
438 438 line_td.addClass('cb-line-selected'); // line number td
439 439 line_td.prev().addClass('cb-line-selected'); // line data
440 440 line_td.next().addClass('cb-line-selected'); // line content
441 441 }
442 442 });
443 443
444 444 } else if (lineName !== undefined) { // lineName only occurs in diffs
445 445 // clear old selected lines
446 446 $('td.cb-line-selected').removeClass('cb-line-selected');
447 447 var anchor = '#{0}'.format(lineName);
448 448 var diffmode = templateContext.session_attrs.diffmode || "sideside";
449 449
450 450 if (diffmode === "unified") {
451 451 $(this).closest('tr').find('td').addClass('cb-line-selected');
452 452 } else {
453 453 var activeTd = $(this).closest('td');
454 454 activeTd.addClass('cb-line-selected');
455 455 activeTd.next('td').addClass('cb-line-selected');
456 456 }
457 457
458 458 }
459 459
460 460 // Replace URL without jumping to it if browser supports.
461 461 // Default otherwise
462 462 if (history.pushState && anchor !== undefined) {
463 463 var new_location = location.href.rstrip('#');
464 464 if (location.hash) {
465 465 // location without hash
466 466 new_location = new_location.replace(location.hash, "");
467 467 }
468 468
469 469 // Make new anchor url
470 470 new_location = new_location + anchor;
471 471 history.pushState(true, document.title, new_location);
472 472
473 473 return false;
474 474 }
475 475
476 476 });
477 477
478 478 $('.collapse_file').on('click', function(e) {
479 479 e.stopPropagation();
480 480 if ($(e.target).is('a')) { return; }
481 481 var node = $(e.delegateTarget).first();
482 482 var icon = $($(node.children().first()).children().first());
483 483 var id = node.attr('fid');
484 484 var target = $('#'+id);
485 485 var tr = $('#tr_'+id);
486 486 var diff = $('#diff_'+id);
487 487 if(node.hasClass('expand_file')){
488 488 node.removeClass('expand_file');
489 489 icon.removeClass('expand_file_icon');
490 490 node.addClass('collapse_file');
491 491 icon.addClass('collapse_file_icon');
492 492 diff.show();
493 493 tr.show();
494 494 target.show();
495 495 } else {
496 496 node.removeClass('collapse_file');
497 497 icon.removeClass('collapse_file_icon');
498 498 node.addClass('expand_file');
499 499 icon.addClass('expand_file_icon');
500 500 diff.hide();
501 501 tr.hide();
502 502 target.hide();
503 503 }
504 504 });
505 505
506 506 $('#expand_all_files').click(function() {
507 507 $('.expand_file').each(function() {
508 508 var node = $(this);
509 509 var icon = $($(node.children().first()).children().first());
510 510 var id = $(this).attr('fid');
511 511 var target = $('#'+id);
512 512 var tr = $('#tr_'+id);
513 513 var diff = $('#diff_'+id);
514 514 node.removeClass('expand_file');
515 515 icon.removeClass('expand_file_icon');
516 516 node.addClass('collapse_file');
517 517 icon.addClass('collapse_file_icon');
518 518 diff.show();
519 519 tr.show();
520 520 target.show();
521 521 });
522 522 });
523 523
524 524 $('#collapse_all_files').click(function() {
525 525 $('.collapse_file').each(function() {
526 526 var node = $(this);
527 527 var icon = $($(node.children().first()).children().first());
528 528 var id = $(this).attr('fid');
529 529 var target = $('#'+id);
530 530 var tr = $('#tr_'+id);
531 531 var diff = $('#diff_'+id);
532 532 node.removeClass('collapse_file');
533 533 icon.removeClass('collapse_file_icon');
534 534 node.addClass('expand_file');
535 535 icon.addClass('expand_file_icon');
536 536 diff.hide();
537 537 tr.hide();
538 538 target.hide();
539 539 });
540 540 });
541 541
542 542 // Mouse over behavior for comments and line selection
543 543
544 544 // Select the line that comes from the url anchor
545 545 // At the time of development, Chrome didn't seem to support jquery's :target
546 546 // element, so I had to scroll manually
547 547
548 548 if (location.hash) {
549 549 var result = splitDelimitedHash(location.hash);
550 550
551 551 var loc = result.loc;
552 552
553 553 if (loc.length > 1) {
554 554
555 555 var highlightable_line_tds = [];
556 556
557 557 // source code line format
558 558 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
559 559
560 560 // multi-line HL, for files
561 561 if (page_highlights.length > 1) {
562 562 var highlight_ranges = page_highlights[1].split(",");
563 563 var h_lines = [];
564 564 for (var pos in highlight_ranges) {
565 565 var _range = highlight_ranges[pos].split('-');
566 566 if (_range.length === 2) {
567 567 var start = parseInt(_range[0]);
568 568 var end = parseInt(_range[1]);
569 569 if (start < end) {
570 570 for (var i = start; i <= end; i++) {
571 571 h_lines.push(i);
572 572 }
573 573 }
574 574 } else {
575 575 h_lines.push(parseInt(highlight_ranges[pos]));
576 576 }
577 577 }
578 578 for (pos in h_lines) {
579 579 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
580 580 if (line_td.length) {
581 581 highlightable_line_tds.push(line_td);
582 582 }
583 583 }
584 584 }
585 585
586 586 // now check a direct id reference of line in diff / pull-request page)
587 587 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
588 588 highlightable_line_tds.push($(loc));
589 589 }
590 590
591 591 // mark diff lines as selected
592 592 $.each(highlightable_line_tds, function (i, $td) {
593 593 $td.addClass('cb-line-selected'); // line number td
594 594 $td.prev().addClass('cb-line-selected'); // line data
595 595 $td.next().addClass('cb-line-selected'); // line content
596 596 });
597 597
598 598 if (highlightable_line_tds.length > 0) {
599 599 var $first_line_td = highlightable_line_tds[0];
600 600 scrollToElement($first_line_td);
601 601 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
602 602 td: $first_line_td,
603 603 remainder: result.remainder
604 604 });
605 605 } else {
606 606 // case for direct anchor to comments
607 607 var $line = $(loc);
608 608
609 609 if ($line.hasClass('comment-general')) {
610 610 $line.show();
611 611 } else if ($line.hasClass('comment-inline')) {
612 612 $line.show();
613 613 var $cb = $line.closest('.cb');
614 614 $cb.removeClass('cb-collapsed')
615 615 }
616 616 if ($line.length > 0) {
617 617 $line.addClass('comment-selected-hl');
618 618 offsetScroll($line, 70);
619 619 }
620 620 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
621 621 $line.nextAll('.cb-comment-add-button').trigger('click');
622 622 }
623 623 }
624 624
625 625 }
626 626 }
627 627 collapsableContent();
628 628 });
629 629
630 630 var feedLifetimeOptions = function(query, initialData){
631 631 var data = {results: []};
632 632 var isQuery = typeof query.term !== 'undefined';
633 633
634 634 var section = _gettext('Lifetime');
635 635 var children = [];
636 636
637 637 //filter results
638 638 $.each(initialData.results, function(idx, value) {
639 639
640 640 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
641 641 children.push({
642 642 'id': this.id,
643 643 'text': this.text
644 644 })
645 645 }
646 646
647 647 });
648 648 data.results.push({
649 649 'text': section,
650 650 'children': children
651 651 });
652 652
653 653 if (isQuery) {
654 654
655 655 var now = moment.utc();
656 656
657 657 var parseQuery = function(entry, now){
658 658 var fmt = 'DD/MM/YYYY H:mm';
659 659 var parsed = moment.utc(entry, fmt);
660 660 var diffInMin = parsed.diff(now, 'minutes');
661 661
662 662 if (diffInMin > 0){
663 663 return {
664 664 id: diffInMin,
665 665 text: parsed.format(fmt)
666 666 }
667 667 } else {
668 668 return {
669 669 id: undefined,
670 670 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
671 671 }
672 672 }
673 673
674 674
675 675 };
676 676
677 677 data.results.push({
678 678 'text': _gettext('Specified expiration date'),
679 679 'children': [{
680 680 'id': parseQuery(query.term, now).id,
681 681 'text': parseQuery(query.term, now).text
682 682 }]
683 683 });
684 684 }
685 685
686 686 query.callback(data);
687 687 };
688 688
689 689 /*
690 690 * Retrievew via templateContext.session_attrs.key
691 691 * */
692 692 var storeUserSessionAttr = function (key, val) {
693 693
694 694 var postData = {
695 695 'key': key,
696 696 'val': val,
697 697 'csrf_token': CSRF_TOKEN
698 698 };
699 699
700 700 var success = function(o) {
701 701 return true
702 702 };
703 703
704 704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
705 705 return false;
706 706 };
707
708
709 var getUserSessionAttr = function(key) {
710 var storeKey = templateContext.session_attrs;
711 var val = storeKey[key]
712 if (val !== undefined) {
713 return JSON.parse(val)
714 }
715 return null
716 }
@@ -1,144 +1,151 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 <div style="height: 35px">
6 6 <%
7 7 selected_filter = 'all'
8 8 if c.closed:
9 9 selected_filter = 'all_closed'
10 10 %>
11 11
12 12 <ul class="button-links">
13 13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
14 14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
15 15 </ul>
16 16
17 17 <div class="grid-quick-filter">
18 18 <ul class="grid-filter-box">
19 19 <li class="grid-filter-box-icon">
20 20 <i class="icon-search"></i>
21 21 </li>
22 22 <li class="grid-filter-box-input">
23 23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
24 24 </li>
25 25 </ul>
26 26 </div>
27 27 </div>
28 28 </div>
29 29 </div>
30 30
31 31 <div class="panel panel-default">
32 32 <div class="panel-heading">
33 33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 34 </div>
35 35 <div class="panel-body panel-body-min-height">
36 36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 37 </div>
38 38 </div>
39 39
40 40 <script type="text/javascript">
41 41 $(document).ready(function () {
42 42
43 43 var $pullRequestListTable = $('#pull_request_list_table');
44 44
45 45 // participating object list
46 46 $pullRequestListTable.DataTable({
47 47 processing: true,
48 48 serverSide: true,
49 stateSave: true,
50 stateDuration: -1,
49 51 ajax: {
50 52 "url": "${h.route_path('my_account_pullrequests_data')}",
51 53 "data": function (d) {
52 54 d.closed = "${c.closed}";
53 55 },
54 56 "dataSrc": function (json) {
55 57 return json.data;
56 58 }
57 59 },
58 60
59 61 dom: 'rtp',
60 62 pageLength: ${c.visual.dashboard_items},
61 63 order: [[1, "desc"]],
62 64 columns: [
63 65 {
64 66 data: {
65 67 "_": "status",
66 68 "sort": "status"
67 69 }, title: "", className: "td-status", orderable: false
68 70 },
69 71 {
70 72 data: {
71 73 "_": "name",
72 74 "sort": "name_raw"
73 75 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
74 76 },
75 77 {
76 78 data: {
77 79 "_": "title",
78 80 "sort": "title"
79 81 }, title: "${_('Title')}", className: "td-description"
80 82 },
81 83 {
82 84 data: {
83 85 "_": "author",
84 86 "sort": "author_raw"
85 87 }, title: "${_('Author')}", className: "td-user", orderable: false
86 88 },
87 89 {
88 90 data: {
89 91 "_": "comments",
90 92 "sort": "comments_raw"
91 93 }, title: "", className: "td-comments", orderable: false
92 94 },
93 95 {
94 96 data: {
95 97 "_": "updated_on",
96 98 "sort": "updated_on_raw"
97 99 }, title: "${_('Last Update')}", className: "td-time"
98 100 },
99 101 {
100 102 data: {
101 103 "_": "target_repo",
102 104 "sort": "target_repo"
103 105 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
104 106 },
105 107 ],
106 108 language: {
107 109 paginate: DEFAULT_GRID_PAGINATION,
108 110 sProcessing: _gettext('loading...'),
109 111 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
110 112 },
111 113 "drawCallback": function (settings, json) {
112 114 timeagoActivate();
113 115 tooltipActivate();
114 116 },
115 117 "createdRow": function (row, data, index) {
116 118 if (data['closed']) {
117 119 $(row).addClass('closed');
118 120 }
119 121 if (data['owned']) {
120 122 $(row).addClass('owned');
121 123 }
124 },
125 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
122 128 }
123 129 });
124 130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
125 131 $pullRequestListTable.css('opacity', 1);
126 132 });
127 133
128 134 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
129 135 $pullRequestListTable.css('opacity', 0.3);
130 136 });
131 137
138
132 139 // filter
133 140 $('#q_filter').on('keyup',
134 141 $.debounce(250, function () {
135 142 $pullRequestListTable.DataTable().search(
136 143 $('#q_filter').val()
137 144 ).draw();
138 145 })
139 146 );
140 147
141 148 });
142 149
143 150
144 151 </script>
@@ -1,140 +1,146 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Pull Requests').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='showpullrequest')}
19 19 </%def>
20 20
21 21
22 22 <%def name="main()">
23 23
24 24 <div class="box">
25 25 <div class="title">
26 26 <ul class="button-links">
27 27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 33 </ul>
34 34
35 35 <ul class="links">
36 36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 37 <li>
38 38 <span>
39 39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 40 ${_('Open new Pull Request')}
41 41 </a>
42 42 </span>
43 43 </li>
44 44 % endif
45 45
46 46 <li>
47 47 <div class="grid-quick-filter">
48 48 <ul class="grid-filter-box">
49 49 <li class="grid-filter-box-icon">
50 50 <i class="icon-search"></i>
51 51 </li>
52 52 <li class="grid-filter-box-input">
53 53 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 54 </li>
55 55 </ul>
56 56 </div>
57 57 </li>
58 58
59 59 </ul>
60 60
61 61 </div>
62 62
63 63 <div class="main-content-full-width">
64 64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 65 </div>
66 66
67 67 </div>
68 68
69 69 <script type="text/javascript">
70 70 $(document).ready(function() {
71 71 var $pullRequestListTable = $('#pull_request_list_table');
72 72
73 73 // object list
74 74 $pullRequestListTable.DataTable({
75 75 processing: true,
76 76 serverSide: true,
77 stateSave: true,
78 stateDuration: -1,
77 79 ajax: {
78 80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
79 81 "data": function (d) {
80 82 d.source = "${c.source}";
81 83 d.closed = "${c.closed}";
82 84 d.my = "${c.my}";
83 85 d.awaiting_review = "${c.awaiting_review}";
84 86 d.awaiting_my_review = "${c.awaiting_my_review}";
85 87 }
86 88 },
87 89 dom: 'rtp',
88 90 pageLength: ${c.visual.dashboard_items},
89 91 order: [[ 1, "desc" ]],
90 92 columns: [
91 93 { data: {"_": "status",
92 94 "sort": "status"}, title: "", className: "td-status", orderable: false},
93 95 { data: {"_": "name",
94 96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
95 97 { data: {"_": "title",
96 98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
97 99 { data: {"_": "author",
98 100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
99 101 { data: {"_": "comments",
100 102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
101 103 { data: {"_": "updated_on",
102 104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
103 105 ],
104 106 language: {
105 107 paginate: DEFAULT_GRID_PAGINATION,
106 108 sProcessing: _gettext('loading...'),
107 109 emptyTable: _gettext("No pull requests available yet.")
108 110 },
109 111 "drawCallback": function( settings, json ) {
110 112 timeagoActivate();
111 113 tooltipActivate();
112 114 },
113 115 "createdRow": function ( row, data, index ) {
114 116 if (data['closed']) {
115 117 $(row).addClass('closed');
116 118 }
119 },
120 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
117 123 }
118 124 });
119 125
120 126 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
121 127 $pullRequestListTable.css('opacity', 1);
122 128 });
123 129
124 130 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
125 131 $pullRequestListTable.css('opacity', 0.3);
126 132 });
127 133
128 134 // filter
129 135 $('#q_filter').on('keyup',
130 136 $.debounce(250, function() {
131 137 $pullRequestListTable.DataTable().search(
132 138 $('#q_filter').val()
133 139 ).draw();
134 140 })
135 141 );
136 142
137 143 });
138 144
139 145 </script>
140 146 </%def>
General Comments 0
You need to be logged in to leave comments. Login now