##// END OF EJS Templates
pull-requests: added WIP markers in display, and improved a bit ui when creating a pull-request....
marcink -
r4102:24c7dd99 default
parent child Browse files
Show More
@@ -1,761 +1,762 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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
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 LoginRequired, NotAnonymous, CSRFRequired, \
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, joinedload,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.scm import RepoList
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.user_group import UserGroupModel
53 53 from rhodecode.model.validation_schema.schemas import user_schema
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MyAccountView(BaseAppView, DataGridAppView):
59 59 ALLOW_SCOPED_TOKENS = False
60 60 """
61 61 This view has alternative version inside EE, if modified please take a look
62 62 in there as well.
63 63 """
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.user = c.auth_user.get_instance()
68 68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
69 69
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 @view_config(
75 75 route_name='my_account_profile', request_method='GET',
76 76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
77 77 def my_account_profile(self):
78 78 c = self.load_default_context()
79 79 c.active = 'profile'
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 def maybe_attach_token_scope(self, token):
168 168 # implemented in EE edition
169 169 pass
170 170
171 171 @LoginRequired()
172 172 @NotAnonymous()
173 173 @CSRFRequired()
174 174 @view_config(
175 175 route_name='my_account_auth_tokens_add', request_method='POST',)
176 176 def my_account_auth_tokens_add(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179
180 180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
181 181 description = self.request.POST.get('description')
182 182 role = self.request.POST.get('role')
183 183
184 184 token = UserModel().add_auth_token(
185 185 user=c.user.user_id,
186 186 lifetime_minutes=lifetime, role=role, description=description,
187 187 scope_callback=self.maybe_attach_token_scope)
188 188 token_data = token.get_api_data()
189 189
190 190 audit_logger.store_web(
191 191 'user.edit.token.add', action_data={
192 192 'data': {'token': token_data, 'user': 'self'}},
193 193 user=self._rhodecode_user, )
194 194 Session().commit()
195 195
196 196 h.flash(_("Auth token successfully created"), category='success')
197 197 return HTTPFound(h.route_path('my_account_auth_tokens'))
198 198
199 199 @LoginRequired()
200 200 @NotAnonymous()
201 201 @CSRFRequired()
202 202 @view_config(
203 203 route_name='my_account_auth_tokens_delete', request_method='POST')
204 204 def my_account_auth_tokens_delete(self):
205 205 _ = self.request.translate
206 206 c = self.load_default_context()
207 207
208 208 del_auth_token = self.request.POST.get('del_auth_token')
209 209
210 210 if del_auth_token:
211 211 token = UserApiKeys.get_or_404(del_auth_token)
212 212 token_data = token.get_api_data()
213 213
214 214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
215 215 audit_logger.store_web(
216 216 'user.edit.token.delete', action_data={
217 217 'data': {'token': token_data, 'user': 'self'}},
218 218 user=self._rhodecode_user,)
219 219 Session().commit()
220 220 h.flash(_("Auth token successfully deleted"), category='success')
221 221
222 222 return HTTPFound(h.route_path('my_account_auth_tokens'))
223 223
224 224 @LoginRequired()
225 225 @NotAnonymous()
226 226 @view_config(
227 227 route_name='my_account_emails', request_method='GET',
228 228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 229 def my_account_emails(self):
230 230 _ = self.request.translate
231 231
232 232 c = self.load_default_context()
233 233 c.active = 'emails'
234 234
235 235 c.user_email_map = UserEmailMap.query()\
236 236 .filter(UserEmailMap.user == c.user).all()
237 237
238 238 schema = user_schema.AddEmailSchema().bind(
239 239 username=c.user.username, user_emails=c.user.emails)
240 240
241 241 form = forms.RcForm(schema,
242 242 action=h.route_path('my_account_emails_add'),
243 243 buttons=(forms.buttons.save, forms.buttons.reset))
244 244
245 245 c.form = form
246 246 return self._get_template_context(c)
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 @CSRFRequired()
251 251 @view_config(
252 252 route_name='my_account_emails_add', request_method='POST',
253 253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
254 254 def my_account_emails_add(self):
255 255 _ = self.request.translate
256 256 c = self.load_default_context()
257 257 c.active = 'emails'
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(
263 263 schema, action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 controls = self.request.POST.items()
267 267 try:
268 268 valid_data = form.validate(controls)
269 269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
270 270 audit_logger.store_web(
271 271 'user.edit.email.add', action_data={
272 272 'data': {'email': valid_data['email'], 'user': 'self'}},
273 273 user=self._rhodecode_user,)
274 274 Session().commit()
275 275 except formencode.Invalid as error:
276 276 h.flash(h.escape(error.error_dict['email']), category='error')
277 277 except forms.ValidationFailure as e:
278 278 c.user_email_map = UserEmailMap.query() \
279 279 .filter(UserEmailMap.user == c.user).all()
280 280 c.form = e
281 281 return self._get_template_context(c)
282 282 except Exception:
283 283 log.exception("Exception adding email")
284 284 h.flash(_('Error occurred during adding email'),
285 285 category='error')
286 286 else:
287 287 h.flash(_("Successfully added email"), category='success')
288 288
289 289 raise HTTPFound(self.request.route_path('my_account_emails'))
290 290
291 291 @LoginRequired()
292 292 @NotAnonymous()
293 293 @CSRFRequired()
294 294 @view_config(
295 295 route_name='my_account_emails_delete', request_method='POST')
296 296 def my_account_emails_delete(self):
297 297 _ = self.request.translate
298 298 c = self.load_default_context()
299 299
300 300 del_email_id = self.request.POST.get('del_email_id')
301 301 if del_email_id:
302 302 email = UserEmailMap.get_or_404(del_email_id).email
303 303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
304 304 audit_logger.store_web(
305 305 'user.edit.email.delete', action_data={
306 306 'data': {'email': email, 'user': 'self'}},
307 307 user=self._rhodecode_user,)
308 308 Session().commit()
309 309 h.flash(_("Email successfully deleted"),
310 310 category='success')
311 311 return HTTPFound(h.route_path('my_account_emails'))
312 312
313 313 @LoginRequired()
314 314 @NotAnonymous()
315 315 @CSRFRequired()
316 316 @view_config(
317 317 route_name='my_account_notifications_test_channelstream',
318 318 request_method='POST', renderer='json_ext')
319 319 def my_account_notifications_test_channelstream(self):
320 320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
321 321 self._rhodecode_user.username, datetime.datetime.now())
322 322 payload = {
323 323 # 'channel': 'broadcast',
324 324 'type': 'message',
325 325 'timestamp': datetime.datetime.utcnow(),
326 326 'user': 'system',
327 327 'pm_users': [self._rhodecode_user.username],
328 328 'message': {
329 329 'message': message,
330 330 'level': 'info',
331 331 'topic': '/notifications'
332 332 }
333 333 }
334 334
335 335 registry = self.request.registry
336 336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
337 337 channelstream_config = rhodecode_plugins.get('channelstream', {})
338 338
339 339 try:
340 340 channelstream_request(channelstream_config, [payload], '/message')
341 341 except ChannelstreamException as e:
342 342 log.exception('Failed to send channelstream data')
343 343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
344 344 return {"response": 'Channelstream data sent. '
345 345 'You should see a new live message now.'}
346 346
347 347 def _load_my_repos_data(self, watched=False):
348 348 if watched:
349 349 admin = False
350 350 follows_repos = Session().query(UserFollowing)\
351 351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
352 352 .options(joinedload(UserFollowing.follows_repository))\
353 353 .all()
354 354 repo_list = [x.follows_repository for x in follows_repos]
355 355 else:
356 356 admin = True
357 357 repo_list = Repository.get_all_repos(
358 358 user_id=self._rhodecode_user.user_id)
359 359 repo_list = RepoList(repo_list, perm_set=[
360 360 'repository.read', 'repository.write', 'repository.admin'])
361 361
362 362 repos_data = RepoModel().get_repos_as_dict(
363 363 repo_list=repo_list, admin=admin, short_name=False)
364 364 # json used to render the grid
365 365 return json.dumps(repos_data)
366 366
367 367 @LoginRequired()
368 368 @NotAnonymous()
369 369 @view_config(
370 370 route_name='my_account_repos', request_method='GET',
371 371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 372 def my_account_repos(self):
373 373 c = self.load_default_context()
374 374 c.active = 'repos'
375 375
376 376 # json used to render the grid
377 377 c.data = self._load_my_repos_data()
378 378 return self._get_template_context(c)
379 379
380 380 @LoginRequired()
381 381 @NotAnonymous()
382 382 @view_config(
383 383 route_name='my_account_watched', request_method='GET',
384 384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
385 385 def my_account_watched(self):
386 386 c = self.load_default_context()
387 387 c.active = 'watched'
388 388
389 389 # json used to render the grid
390 390 c.data = self._load_my_repos_data(watched=True)
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @NotAnonymous()
395 395 @view_config(
396 396 route_name='my_account_bookmarks', request_method='GET',
397 397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 398 def my_account_bookmarks(self):
399 399 c = self.load_default_context()
400 400 c.active = 'bookmarks'
401 401 return self._get_template_context(c)
402 402
403 403 def _process_bookmark_entry(self, entry, user_id):
404 404 position = safe_int(entry.get('position'))
405 405 cur_position = safe_int(entry.get('cur_position'))
406 406 if position is None or cur_position is None:
407 407 return
408 408
409 409 # check if this is an existing entry
410 410 is_new = False
411 411 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
412 412
413 413 if db_entry and str2bool(entry.get('remove')):
414 414 log.debug('Marked bookmark %s for deletion', db_entry)
415 415 Session().delete(db_entry)
416 416 return
417 417
418 418 if not db_entry:
419 419 # new
420 420 db_entry = UserBookmark()
421 421 is_new = True
422 422
423 423 should_save = False
424 424 default_redirect_url = ''
425 425
426 426 # save repo
427 427 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
428 428 repo = Repository.get(entry['bookmark_repo'])
429 429 perm_check = HasRepoPermissionAny(
430 430 'repository.read', 'repository.write', 'repository.admin')
431 431 if repo and perm_check(repo_name=repo.repo_name):
432 432 db_entry.repository = repo
433 433 should_save = True
434 434 default_redirect_url = '${repo_url}'
435 435 # save repo group
436 436 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
437 437 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
438 438 perm_check = HasRepoGroupPermissionAny(
439 439 'group.read', 'group.write', 'group.admin')
440 440
441 441 if repo_group and perm_check(group_name=repo_group.group_name):
442 442 db_entry.repository_group = repo_group
443 443 should_save = True
444 444 default_redirect_url = '${repo_group_url}'
445 445 # save generic info
446 446 elif entry.get('title') and entry.get('redirect_url'):
447 447 should_save = True
448 448
449 449 if should_save:
450 450 # mark user and position
451 451 db_entry.user_id = user_id
452 452 db_entry.position = position
453 453 db_entry.title = entry.get('title')
454 454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455 455 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
456 456
457 457 Session().add(db_entry)
458 458
459 459 @LoginRequired()
460 460 @NotAnonymous()
461 461 @CSRFRequired()
462 462 @view_config(
463 463 route_name='my_account_bookmarks_update', request_method='POST')
464 464 def my_account_bookmarks_update(self):
465 465 _ = self.request.translate
466 466 c = self.load_default_context()
467 467 c.active = 'bookmarks'
468 468
469 469 controls = peppercorn.parse(self.request.POST.items())
470 470 user_id = c.user.user_id
471 471
472 472 # validate positions
473 473 positions = {}
474 474 for entry in controls.get('bookmarks', []):
475 475 position = safe_int(entry['position'])
476 476 if position is None:
477 477 continue
478 478
479 479 if position in positions:
480 480 h.flash(_("Position {} is defined twice. "
481 481 "Please correct this error.").format(position), category='error')
482 482 return HTTPFound(h.route_path('my_account_bookmarks'))
483 483
484 484 entry['position'] = position
485 485 entry['cur_position'] = safe_int(entry.get('cur_position'))
486 486 positions[position] = entry
487 487
488 488 try:
489 489 for entry in positions.values():
490 490 self._process_bookmark_entry(entry, user_id)
491 491
492 492 Session().commit()
493 493 h.flash(_("Update Bookmarks"), category='success')
494 494 except IntegrityError:
495 495 h.flash(_("Failed to update bookmarks. "
496 496 "Make sure an unique position is used."), category='error')
497 497
498 498 return HTTPFound(h.route_path('my_account_bookmarks'))
499 499
500 500 @LoginRequired()
501 501 @NotAnonymous()
502 502 @view_config(
503 503 route_name='my_account_goto_bookmark', request_method='GET',
504 504 renderer='rhodecode:templates/admin/my_account/my_account.mako')
505 505 def my_account_goto_bookmark(self):
506 506
507 507 bookmark_id = self.request.matchdict['bookmark_id']
508 508 user_bookmark = UserBookmark().query()\
509 509 .filter(UserBookmark.user_id == self.request.user.user_id) \
510 510 .filter(UserBookmark.position == bookmark_id).scalar()
511 511
512 512 redirect_url = h.route_path('my_account_bookmarks')
513 513 if not user_bookmark:
514 514 raise HTTPFound(redirect_url)
515 515
516 516 # repository set
517 517 if user_bookmark.repository:
518 518 repo_name = user_bookmark.repository.repo_name
519 519 base_redirect_url = h.route_path(
520 520 'repo_summary', repo_name=repo_name)
521 521 if user_bookmark.redirect_url and \
522 522 '${repo_url}' in user_bookmark.redirect_url:
523 523 redirect_url = string.Template(user_bookmark.redirect_url)\
524 524 .safe_substitute({'repo_url': base_redirect_url})
525 525 else:
526 526 redirect_url = base_redirect_url
527 527 # repository group set
528 528 elif user_bookmark.repository_group:
529 529 repo_group_name = user_bookmark.repository_group.group_name
530 530 base_redirect_url = h.route_path(
531 531 'repo_group_home', repo_group_name=repo_group_name)
532 532 if user_bookmark.redirect_url and \
533 533 '${repo_group_url}' in user_bookmark.redirect_url:
534 534 redirect_url = string.Template(user_bookmark.redirect_url)\
535 535 .safe_substitute({'repo_group_url': base_redirect_url})
536 536 else:
537 537 redirect_url = base_redirect_url
538 538 # custom URL set
539 539 elif user_bookmark.redirect_url:
540 540 server_url = h.route_url('home').rstrip('/')
541 541 redirect_url = string.Template(user_bookmark.redirect_url) \
542 542 .safe_substitute({'server_url': server_url})
543 543
544 544 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
545 545 raise HTTPFound(redirect_url)
546 546
547 547 @LoginRequired()
548 548 @NotAnonymous()
549 549 @view_config(
550 550 route_name='my_account_perms', request_method='GET',
551 551 renderer='rhodecode:templates/admin/my_account/my_account.mako')
552 552 def my_account_perms(self):
553 553 c = self.load_default_context()
554 554 c.active = 'perms'
555 555
556 556 c.perm_user = c.auth_user
557 557 return self._get_template_context(c)
558 558
559 559 @LoginRequired()
560 560 @NotAnonymous()
561 561 @view_config(
562 562 route_name='my_account_notifications', request_method='GET',
563 563 renderer='rhodecode:templates/admin/my_account/my_account.mako')
564 564 def my_notifications(self):
565 565 c = self.load_default_context()
566 566 c.active = 'notifications'
567 567
568 568 return self._get_template_context(c)
569 569
570 570 @LoginRequired()
571 571 @NotAnonymous()
572 572 @CSRFRequired()
573 573 @view_config(
574 574 route_name='my_account_notifications_toggle_visibility',
575 575 request_method='POST', renderer='json_ext')
576 576 def my_notifications_toggle_visibility(self):
577 577 user = self._rhodecode_db_user
578 578 new_status = not user.user_data.get('notification_status', True)
579 579 user.update_userdata(notification_status=new_status)
580 580 Session().commit()
581 581 return user.user_data['notification_status']
582 582
583 583 @LoginRequired()
584 584 @NotAnonymous()
585 585 @view_config(
586 586 route_name='my_account_edit',
587 587 request_method='GET',
588 588 renderer='rhodecode:templates/admin/my_account/my_account.mako')
589 589 def my_account_edit(self):
590 590 c = self.load_default_context()
591 591 c.active = 'profile_edit'
592 592 c.extern_type = c.user.extern_type
593 593 c.extern_name = c.user.extern_name
594 594
595 595 schema = user_schema.UserProfileSchema().bind(
596 596 username=c.user.username, user_emails=c.user.emails)
597 597 appstruct = {
598 598 'username': c.user.username,
599 599 'email': c.user.email,
600 600 'firstname': c.user.firstname,
601 601 'lastname': c.user.lastname,
602 602 'description': c.user.description,
603 603 }
604 604 c.form = forms.RcForm(
605 605 schema, appstruct=appstruct,
606 606 action=h.route_path('my_account_update'),
607 607 buttons=(forms.buttons.save, forms.buttons.reset))
608 608
609 609 return self._get_template_context(c)
610 610
611 611 @LoginRequired()
612 612 @NotAnonymous()
613 613 @CSRFRequired()
614 614 @view_config(
615 615 route_name='my_account_update',
616 616 request_method='POST',
617 617 renderer='rhodecode:templates/admin/my_account/my_account.mako')
618 618 def my_account_update(self):
619 619 _ = self.request.translate
620 620 c = self.load_default_context()
621 621 c.active = 'profile_edit'
622 622 c.perm_user = c.auth_user
623 623 c.extern_type = c.user.extern_type
624 624 c.extern_name = c.user.extern_name
625 625
626 626 schema = user_schema.UserProfileSchema().bind(
627 627 username=c.user.username, user_emails=c.user.emails)
628 628 form = forms.RcForm(
629 629 schema, buttons=(forms.buttons.save, forms.buttons.reset))
630 630
631 631 controls = self.request.POST.items()
632 632 try:
633 633 valid_data = form.validate(controls)
634 634 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
635 635 'new_password', 'password_confirmation']
636 636 if c.extern_type != "rhodecode":
637 637 # forbid updating username for external accounts
638 638 skip_attrs.append('username')
639 639 old_email = c.user.email
640 640 UserModel().update_user(
641 641 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
642 642 **valid_data)
643 643 if old_email != valid_data['email']:
644 644 old = UserEmailMap.query() \
645 645 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
646 646 old.email = old_email
647 647 h.flash(_('Your account was updated successfully'), category='success')
648 648 Session().commit()
649 649 except forms.ValidationFailure as e:
650 650 c.form = e
651 651 return self._get_template_context(c)
652 652 except Exception:
653 653 log.exception("Exception updating user")
654 654 h.flash(_('Error occurred during update of user'),
655 655 category='error')
656 656 raise HTTPFound(h.route_path('my_account_profile'))
657 657
658 658 def _get_pull_requests_list(self, statuses):
659 659 draw, start, limit = self._extract_chunk(self.request)
660 660 search_q, order_by, order_dir = self._extract_ordering(self.request)
661 661 _render = self.request.get_partial_renderer(
662 662 'rhodecode:templates/data_table/_dt_elements.mako')
663 663
664 664 pull_requests = PullRequestModel().get_im_participating_in(
665 665 user_id=self._rhodecode_user.user_id,
666 666 statuses=statuses,
667 667 offset=start, length=limit, order_by=order_by,
668 668 order_dir=order_dir)
669 669
670 670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
671 671 user_id=self._rhodecode_user.user_id, statuses=statuses)
672 672
673 673 data = []
674 674 comments_model = CommentsModel()
675 675 for pr in pull_requests:
676 676 repo_id = pr.target_repo_id
677 677 comments = comments_model.get_all_comments(
678 678 repo_id, pull_request=pr)
679 679 owned = pr.user_id == self._rhodecode_user.user_id
680 680
681 681 data.append({
682 682 'target_repo': _render('pullrequest_target_repo',
683 683 pr.target_repo.repo_name),
684 684 'name': _render('pullrequest_name',
685 pr.pull_request_id, pr.target_repo.repo_name,
685 pr.pull_request_id, pr.work_in_progress,
686 pr.target_repo.repo_name,
686 687 short=True),
687 688 'name_raw': pr.pull_request_id,
688 689 'status': _render('pullrequest_status',
689 690 pr.calculated_review_status()),
690 691 'title': _render('pullrequest_title', pr.title, pr.description),
691 692 'description': h.escape(pr.description),
692 693 'updated_on': _render('pullrequest_updated_on',
693 694 h.datetime_to_time(pr.updated_on)),
694 695 'updated_on_raw': h.datetime_to_time(pr.updated_on),
695 696 'created_on': _render('pullrequest_updated_on',
696 697 h.datetime_to_time(pr.created_on)),
697 698 'created_on_raw': h.datetime_to_time(pr.created_on),
698 699 'state': pr.pull_request_state,
699 700 'author': _render('pullrequest_author',
700 701 pr.author.full_contact, ),
701 702 'author_raw': pr.author.full_name,
702 703 'comments': _render('pullrequest_comments', len(comments)),
703 704 'comments_raw': len(comments),
704 705 'closed': pr.is_closed(),
705 706 'owned': owned
706 707 })
707 708
708 709 # json used to render the grid
709 710 data = ({
710 711 'draw': draw,
711 712 'data': data,
712 713 'recordsTotal': pull_requests_total_count,
713 714 'recordsFiltered': pull_requests_total_count,
714 715 })
715 716 return data
716 717
717 718 @LoginRequired()
718 719 @NotAnonymous()
719 720 @view_config(
720 721 route_name='my_account_pullrequests',
721 722 request_method='GET',
722 723 renderer='rhodecode:templates/admin/my_account/my_account.mako')
723 724 def my_account_pullrequests(self):
724 725 c = self.load_default_context()
725 726 c.active = 'pullrequests'
726 727 req_get = self.request.GET
727 728
728 729 c.closed = str2bool(req_get.get('pr_show_closed'))
729 730
730 731 return self._get_template_context(c)
731 732
732 733 @LoginRequired()
733 734 @NotAnonymous()
734 735 @view_config(
735 736 route_name='my_account_pullrequests_data',
736 737 request_method='GET', renderer='json_ext')
737 738 def my_account_pullrequests_data(self):
738 739 self.load_default_context()
739 740 req_get = self.request.GET
740 741 closed = str2bool(req_get.get('closed'))
741 742
742 743 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
743 744 if closed:
744 745 statuses += [PullRequest.STATUS_CLOSED]
745 746
746 747 data = self._get_pull_requests_list(statuses=statuses)
747 748 return data
748 749
749 750 @LoginRequired()
750 751 @NotAnonymous()
751 752 @view_config(
752 753 route_name='my_account_user_group_membership',
753 754 request_method='GET',
754 755 renderer='rhodecode:templates/admin/my_account/my_account.mako')
755 756 def my_account_user_group_membership(self):
756 757 c = self.load_default_context()
757 758 c.active = 'user_group_membership'
758 759 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
759 760 for group in self._rhodecode_db_user.group_member]
760 761 c.user_groups = json.dumps(groups)
761 762 return self._get_template_context(c)
@@ -1,1481 +1,1482 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 # backward compat., we use for OLD PRs a plain renderer
64 64 c.renderer = 'plain'
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.work_in_progress,
112 pr.target_repo.repo_name),
112 113 'name_raw': pr.pull_request_id,
113 114 'status': _render('pullrequest_status',
114 115 pr.calculated_review_status()),
115 116 'title': _render('pullrequest_title', pr.title, pr.description),
116 117 'description': h.escape(pr.description),
117 118 'updated_on': _render('pullrequest_updated_on',
118 119 h.datetime_to_time(pr.updated_on)),
119 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 121 'created_on': _render('pullrequest_updated_on',
121 122 h.datetime_to_time(pr.created_on)),
122 123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 124 'state': pr.pull_request_state,
124 125 'author': _render('pullrequest_author',
125 126 pr.author.full_contact, ),
126 127 'author_raw': pr.author.full_name,
127 128 'comments': _render('pullrequest_comments', len(comments)),
128 129 'comments_raw': len(comments),
129 130 'closed': pr.is_closed(),
130 131 })
131 132
132 133 data = ({
133 134 'draw': draw,
134 135 'data': data,
135 136 'recordsTotal': pull_requests_total_count,
136 137 'recordsFiltered': pull_requests_total_count,
137 138 })
138 139 return data
139 140
140 141 @LoginRequired()
141 142 @HasRepoPermissionAnyDecorator(
142 143 'repository.read', 'repository.write', 'repository.admin')
143 144 @view_config(
144 145 route_name='pullrequest_show_all', request_method='GET',
145 146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 147 def pull_request_list(self):
147 148 c = self.load_default_context()
148 149
149 150 req_get = self.request.GET
150 151 c.source = str2bool(req_get.get('source'))
151 152 c.closed = str2bool(req_get.get('closed'))
152 153 c.my = str2bool(req_get.get('my'))
153 154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 156
156 157 c.active = 'open'
157 158 if c.my:
158 159 c.active = 'my'
159 160 if c.closed:
160 161 c.active = 'closed'
161 162 if c.awaiting_review and not c.source:
162 163 c.active = 'awaiting'
163 164 if c.source and not c.awaiting_review:
164 165 c.active = 'source'
165 166 if c.awaiting_my_review:
166 167 c.active = 'awaiting_my'
167 168
168 169 return self._get_template_context(c)
169 170
170 171 @LoginRequired()
171 172 @HasRepoPermissionAnyDecorator(
172 173 'repository.read', 'repository.write', 'repository.admin')
173 174 @view_config(
174 175 route_name='pullrequest_show_all_data', request_method='GET',
175 176 renderer='json_ext', xhr=True)
176 177 def pull_request_list_data(self):
177 178 self.load_default_context()
178 179
179 180 # additional filters
180 181 req_get = self.request.GET
181 182 source = str2bool(req_get.get('source'))
182 183 closed = str2bool(req_get.get('closed'))
183 184 my = str2bool(req_get.get('my'))
184 185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 187
187 188 filter_type = 'awaiting_review' if awaiting_review \
188 189 else 'awaiting_my_review' if awaiting_my_review \
189 190 else None
190 191
191 192 opened_by = None
192 193 if my:
193 194 opened_by = [self._rhodecode_user.user_id]
194 195
195 196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 197 if closed:
197 198 statuses = [PullRequest.STATUS_CLOSED]
198 199
199 200 data = self._get_pull_requests_list(
200 201 repo_name=self.db_repo_name, source=source,
201 202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 203
203 204 return data
204 205
205 206 def _is_diff_cache_enabled(self, target_repo):
206 207 caching_enabled = self._get_general_setting(
207 208 target_repo, 'rhodecode_diff_cache')
208 209 log.debug('Diff caching enabled: %s', caching_enabled)
209 210 return caching_enabled
210 211
211 212 def _get_diffset(self, source_repo_name, source_repo,
212 213 source_ref_id, target_ref_id,
213 214 target_commit, source_commit, diff_limit, file_limit,
214 215 fulldiff, hide_whitespace_changes, diff_context):
215 216
216 217 vcs_diff = PullRequestModel().get_diff(
217 218 source_repo, source_ref_id, target_ref_id,
218 219 hide_whitespace_changes, diff_context)
219 220
220 221 diff_processor = diffs.DiffProcessor(
221 222 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 223 file_limit=file_limit, show_full_diff=fulldiff)
223 224
224 225 _parsed = diff_processor.prepare()
225 226
226 227 diffset = codeblocks.DiffSet(
227 228 repo_name=self.db_repo_name,
228 229 source_repo_name=source_repo_name,
229 230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 232 )
232 233 diffset = self.path_filter.render_patchset_filtered(
233 234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 235
235 236 return diffset
236 237
237 238 def _get_range_diffset(self, source_scm, source_repo,
238 239 commit1, commit2, diff_limit, file_limit,
239 240 fulldiff, hide_whitespace_changes, diff_context):
240 241 vcs_diff = source_scm.get_diff(
241 242 commit1, commit2,
242 243 ignore_whitespace=hide_whitespace_changes,
243 244 context=diff_context)
244 245
245 246 diff_processor = diffs.DiffProcessor(
246 247 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 248 file_limit=file_limit, show_full_diff=fulldiff)
248 249
249 250 _parsed = diff_processor.prepare()
250 251
251 252 diffset = codeblocks.DiffSet(
252 253 repo_name=source_repo.repo_name,
253 254 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 255 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 256
256 257 diffset = self.path_filter.render_patchset_filtered(
257 258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 259
259 260 return diffset
260 261
261 262 @LoginRequired()
262 263 @HasRepoPermissionAnyDecorator(
263 264 'repository.read', 'repository.write', 'repository.admin')
264 265 @view_config(
265 266 route_name='pullrequest_show', request_method='GET',
266 267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 268 def pull_request_show(self):
268 269 _ = self.request.translate
269 270 c = self.load_default_context()
270 271
271 272 pull_request = PullRequest.get_or_404(
272 273 self.request.matchdict['pull_request_id'])
273 274 pull_request_id = pull_request.pull_request_id
274 275
275 276 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 277 log.debug('show: forbidden because pull request is in state %s',
277 278 pull_request.pull_request_state)
278 279 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 280 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 281 pull_request.pull_request_state)
281 282 h.flash(msg, category='error')
282 283 raise HTTPFound(h.route_path('pullrequest_show_all',
283 284 repo_name=self.db_repo_name))
284 285
285 286 version = self.request.GET.get('version')
286 287 from_version = self.request.GET.get('from_version') or version
287 288 merge_checks = self.request.GET.get('merge_checks')
288 289 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
289 290
290 291 # fetch global flags of ignore ws or context lines
291 292 diff_context = diffs.get_diff_context(self.request)
292 293 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
293 294
294 295 force_refresh = str2bool(self.request.GET.get('force_refresh'))
295 296
296 297 (pull_request_latest,
297 298 pull_request_at_ver,
298 299 pull_request_display_obj,
299 300 at_version) = PullRequestModel().get_pr_version(
300 301 pull_request_id, version=version)
301 302 pr_closed = pull_request_latest.is_closed()
302 303
303 304 if pr_closed and (version or from_version):
304 305 # not allow to browse versions
305 306 raise HTTPFound(h.route_path(
306 307 'pullrequest_show', repo_name=self.db_repo_name,
307 308 pull_request_id=pull_request_id))
308 309
309 310 versions = pull_request_display_obj.versions()
310 311 # used to store per-commit range diffs
311 312 c.changes = collections.OrderedDict()
312 313 c.range_diff_on = self.request.GET.get('range-diff') == "1"
313 314
314 315 c.at_version = at_version
315 316 c.at_version_num = (at_version
316 317 if at_version and at_version != 'latest'
317 318 else None)
318 319 c.at_version_pos = ChangesetComment.get_index_from_version(
319 320 c.at_version_num, versions)
320 321
321 322 (prev_pull_request_latest,
322 323 prev_pull_request_at_ver,
323 324 prev_pull_request_display_obj,
324 325 prev_at_version) = PullRequestModel().get_pr_version(
325 326 pull_request_id, version=from_version)
326 327
327 328 c.from_version = prev_at_version
328 329 c.from_version_num = (prev_at_version
329 330 if prev_at_version and prev_at_version != 'latest'
330 331 else None)
331 332 c.from_version_pos = ChangesetComment.get_index_from_version(
332 333 c.from_version_num, versions)
333 334
334 335 # define if we're in COMPARE mode or VIEW at version mode
335 336 compare = at_version != prev_at_version
336 337
337 338 # pull_requests repo_name we opened it against
338 339 # ie. target_repo must match
339 340 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
340 341 raise HTTPNotFound()
341 342
342 343 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
343 344 pull_request_at_ver)
344 345
345 346 c.pull_request = pull_request_display_obj
346 347 c.renderer = pull_request_at_ver.description_renderer or c.renderer
347 348 c.pull_request_latest = pull_request_latest
348 349
349 350 if compare or (at_version and not at_version == 'latest'):
350 351 c.allowed_to_change_status = False
351 352 c.allowed_to_update = False
352 353 c.allowed_to_merge = False
353 354 c.allowed_to_delete = False
354 355 c.allowed_to_comment = False
355 356 c.allowed_to_close = False
356 357 else:
357 358 can_change_status = PullRequestModel().check_user_change_status(
358 359 pull_request_at_ver, self._rhodecode_user)
359 360 c.allowed_to_change_status = can_change_status and not pr_closed
360 361
361 362 c.allowed_to_update = PullRequestModel().check_user_update(
362 363 pull_request_latest, self._rhodecode_user) and not pr_closed
363 364 c.allowed_to_merge = PullRequestModel().check_user_merge(
364 365 pull_request_latest, self._rhodecode_user) and not pr_closed
365 366 c.allowed_to_delete = PullRequestModel().check_user_delete(
366 367 pull_request_latest, self._rhodecode_user) and not pr_closed
367 368 c.allowed_to_comment = not pr_closed
368 369 c.allowed_to_close = c.allowed_to_merge and not pr_closed
369 370
370 371 c.forbid_adding_reviewers = False
371 372 c.forbid_author_to_review = False
372 373 c.forbid_commit_author_to_review = False
373 374
374 375 if pull_request_latest.reviewer_data and \
375 376 'rules' in pull_request_latest.reviewer_data:
376 377 rules = pull_request_latest.reviewer_data['rules'] or {}
377 378 try:
378 379 c.forbid_adding_reviewers = rules.get(
379 380 'forbid_adding_reviewers')
380 381 c.forbid_author_to_review = rules.get(
381 382 'forbid_author_to_review')
382 383 c.forbid_commit_author_to_review = rules.get(
383 384 'forbid_commit_author_to_review')
384 385 except Exception:
385 386 pass
386 387
387 388 # check merge capabilities
388 389 _merge_check = MergeCheck.validate(
389 390 pull_request_latest, auth_user=self._rhodecode_user,
390 391 translator=self.request.translate,
391 392 force_shadow_repo_refresh=force_refresh)
392 393 c.pr_merge_errors = _merge_check.error_details
393 394 c.pr_merge_possible = not _merge_check.failed
394 395 c.pr_merge_message = _merge_check.merge_msg
395 396
396 397 c.pr_merge_info = MergeCheck.get_merge_conditions(
397 398 pull_request_latest, translator=self.request.translate)
398 399
399 400 c.pull_request_review_status = _merge_check.review_status
400 401 if merge_checks:
401 402 self.request.override_renderer = \
402 403 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
403 404 return self._get_template_context(c)
404 405
405 406 comments_model = CommentsModel()
406 407
407 408 # reviewers and statuses
408 409 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
409 410 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
410 411
411 412 # GENERAL COMMENTS with versions #
412 413 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
413 414 q = q.order_by(ChangesetComment.comment_id.asc())
414 415 general_comments = q
415 416
416 417 # pick comments we want to render at current version
417 418 c.comment_versions = comments_model.aggregate_comments(
418 419 general_comments, versions, c.at_version_num)
419 420 c.comments = c.comment_versions[c.at_version_num]['until']
420 421
421 422 # INLINE COMMENTS with versions #
422 423 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
423 424 q = q.order_by(ChangesetComment.comment_id.asc())
424 425 inline_comments = q
425 426
426 427 c.inline_versions = comments_model.aggregate_comments(
427 428 inline_comments, versions, c.at_version_num, inline=True)
428 429
429 430 # TODOs
430 431 c.unresolved_comments = CommentsModel() \
431 432 .get_pull_request_unresolved_todos(pull_request)
432 433 c.resolved_comments = CommentsModel() \
433 434 .get_pull_request_resolved_todos(pull_request)
434 435
435 436 # inject latest version
436 437 latest_ver = PullRequest.get_pr_display_object(
437 438 pull_request_latest, pull_request_latest)
438 439
439 440 c.versions = versions + [latest_ver]
440 441
441 442 # if we use version, then do not show later comments
442 443 # than current version
443 444 display_inline_comments = collections.defaultdict(
444 445 lambda: collections.defaultdict(list))
445 446 for co in inline_comments:
446 447 if c.at_version_num:
447 448 # pick comments that are at least UPTO given version, so we
448 449 # don't render comments for higher version
449 450 should_render = co.pull_request_version_id and \
450 451 co.pull_request_version_id <= c.at_version_num
451 452 else:
452 453 # showing all, for 'latest'
453 454 should_render = True
454 455
455 456 if should_render:
456 457 display_inline_comments[co.f_path][co.line_no].append(co)
457 458
458 459 # load diff data into template context, if we use compare mode then
459 460 # diff is calculated based on changes between versions of PR
460 461
461 462 source_repo = pull_request_at_ver.source_repo
462 463 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
463 464
464 465 target_repo = pull_request_at_ver.target_repo
465 466 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
466 467
467 468 if compare:
468 469 # in compare switch the diff base to latest commit from prev version
469 470 target_ref_id = prev_pull_request_display_obj.revisions[0]
470 471
471 472 # despite opening commits for bookmarks/branches/tags, we always
472 473 # convert this to rev to prevent changes after bookmark or branch change
473 474 c.source_ref_type = 'rev'
474 475 c.source_ref = source_ref_id
475 476
476 477 c.target_ref_type = 'rev'
477 478 c.target_ref = target_ref_id
478 479
479 480 c.source_repo = source_repo
480 481 c.target_repo = target_repo
481 482
482 483 c.commit_ranges = []
483 484 source_commit = EmptyCommit()
484 485 target_commit = EmptyCommit()
485 486 c.missing_requirements = False
486 487
487 488 source_scm = source_repo.scm_instance()
488 489 target_scm = target_repo.scm_instance()
489 490
490 491 shadow_scm = None
491 492 try:
492 493 shadow_scm = pull_request_latest.get_shadow_repo()
493 494 except Exception:
494 495 log.debug('Failed to get shadow repo', exc_info=True)
495 496 # try first the existing source_repo, and then shadow
496 497 # repo if we can obtain one
497 498 commits_source_repo = source_scm or shadow_scm
498 499
499 500 c.commits_source_repo = commits_source_repo
500 501 c.ancestor = None # set it to None, to hide it from PR view
501 502
502 503 # empty version means latest, so we keep this to prevent
503 504 # double caching
504 505 version_normalized = version or 'latest'
505 506 from_version_normalized = from_version or 'latest'
506 507
507 508 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
508 509 cache_file_path = diff_cache_exist(
509 510 cache_path, 'pull_request', pull_request_id, version_normalized,
510 511 from_version_normalized, source_ref_id, target_ref_id,
511 512 hide_whitespace_changes, diff_context, c.fulldiff)
512 513
513 514 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
514 515 force_recache = self.get_recache_flag()
515 516
516 517 cached_diff = None
517 518 if caching_enabled:
518 519 cached_diff = load_cached_diff(cache_file_path)
519 520
520 521 has_proper_commit_cache = (
521 522 cached_diff and cached_diff.get('commits')
522 523 and len(cached_diff.get('commits', [])) == 5
523 524 and cached_diff.get('commits')[0]
524 525 and cached_diff.get('commits')[3])
525 526
526 527 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
527 528 diff_commit_cache = \
528 529 (ancestor_commit, commit_cache, missing_requirements,
529 530 source_commit, target_commit) = cached_diff['commits']
530 531 else:
531 532 diff_commit_cache = \
532 533 (ancestor_commit, commit_cache, missing_requirements,
533 534 source_commit, target_commit) = self.get_commits(
534 535 commits_source_repo,
535 536 pull_request_at_ver,
536 537 source_commit,
537 538 source_ref_id,
538 539 source_scm,
539 540 target_commit,
540 541 target_ref_id,
541 542 target_scm)
542 543
543 544 # register our commit range
544 545 for comm in commit_cache.values():
545 546 c.commit_ranges.append(comm)
546 547
547 548 c.missing_requirements = missing_requirements
548 549 c.ancestor_commit = ancestor_commit
549 550 c.statuses = source_repo.statuses(
550 551 [x.raw_id for x in c.commit_ranges])
551 552
552 553 # auto collapse if we have more than limit
553 554 collapse_limit = diffs.DiffProcessor._collapse_commits_over
554 555 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
555 556 c.compare_mode = compare
556 557
557 558 # diff_limit is the old behavior, will cut off the whole diff
558 559 # if the limit is applied otherwise will just hide the
559 560 # big files from the front-end
560 561 diff_limit = c.visual.cut_off_limit_diff
561 562 file_limit = c.visual.cut_off_limit_file
562 563
563 564 c.missing_commits = False
564 565 if (c.missing_requirements
565 566 or isinstance(source_commit, EmptyCommit)
566 567 or source_commit == target_commit):
567 568
568 569 c.missing_commits = True
569 570 else:
570 571 c.inline_comments = display_inline_comments
571 572
572 573 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
573 574 if not force_recache and has_proper_diff_cache:
574 575 c.diffset = cached_diff['diff']
575 576 (ancestor_commit, commit_cache, missing_requirements,
576 577 source_commit, target_commit) = cached_diff['commits']
577 578 else:
578 579 c.diffset = self._get_diffset(
579 580 c.source_repo.repo_name, commits_source_repo,
580 581 source_ref_id, target_ref_id,
581 582 target_commit, source_commit,
582 583 diff_limit, file_limit, c.fulldiff,
583 584 hide_whitespace_changes, diff_context)
584 585
585 586 # save cached diff
586 587 if caching_enabled:
587 588 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
588 589
589 590 c.limited_diff = c.diffset.limited_diff
590 591
591 592 # calculate removed files that are bound to comments
592 593 comment_deleted_files = [
593 594 fname for fname in display_inline_comments
594 595 if fname not in c.diffset.file_stats]
595 596
596 597 c.deleted_files_comments = collections.defaultdict(dict)
597 598 for fname, per_line_comments in display_inline_comments.items():
598 599 if fname in comment_deleted_files:
599 600 c.deleted_files_comments[fname]['stats'] = 0
600 601 c.deleted_files_comments[fname]['comments'] = list()
601 602 for lno, comments in per_line_comments.items():
602 603 c.deleted_files_comments[fname]['comments'].extend(comments)
603 604
604 605 # maybe calculate the range diff
605 606 if c.range_diff_on:
606 607 # TODO(marcink): set whitespace/context
607 608 context_lcl = 3
608 609 ign_whitespace_lcl = False
609 610
610 611 for commit in c.commit_ranges:
611 612 commit2 = commit
612 613 commit1 = commit.first_parent
613 614
614 615 range_diff_cache_file_path = diff_cache_exist(
615 616 cache_path, 'diff', commit.raw_id,
616 617 ign_whitespace_lcl, context_lcl, c.fulldiff)
617 618
618 619 cached_diff = None
619 620 if caching_enabled:
620 621 cached_diff = load_cached_diff(range_diff_cache_file_path)
621 622
622 623 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
623 624 if not force_recache and has_proper_diff_cache:
624 625 diffset = cached_diff['diff']
625 626 else:
626 627 diffset = self._get_range_diffset(
627 628 source_scm, source_repo,
628 629 commit1, commit2, diff_limit, file_limit,
629 630 c.fulldiff, ign_whitespace_lcl, context_lcl
630 631 )
631 632
632 633 # save cached diff
633 634 if caching_enabled:
634 635 cache_diff(range_diff_cache_file_path, diffset, None)
635 636
636 637 c.changes[commit.raw_id] = diffset
637 638
638 639 # this is a hack to properly display links, when creating PR, the
639 640 # compare view and others uses different notation, and
640 641 # compare_commits.mako renders links based on the target_repo.
641 642 # We need to swap that here to generate it properly on the html side
642 643 c.target_repo = c.source_repo
643 644
644 645 c.commit_statuses = ChangesetStatus.STATUSES
645 646
646 647 c.show_version_changes = not pr_closed
647 648 if c.show_version_changes:
648 649 cur_obj = pull_request_at_ver
649 650 prev_obj = prev_pull_request_at_ver
650 651
651 652 old_commit_ids = prev_obj.revisions
652 653 new_commit_ids = cur_obj.revisions
653 654 commit_changes = PullRequestModel()._calculate_commit_id_changes(
654 655 old_commit_ids, new_commit_ids)
655 656 c.commit_changes_summary = commit_changes
656 657
657 658 # calculate the diff for commits between versions
658 659 c.commit_changes = []
659 660 mark = lambda cs, fw: list(
660 661 h.itertools.izip_longest([], cs, fillvalue=fw))
661 662 for c_type, raw_id in mark(commit_changes.added, 'a') \
662 663 + mark(commit_changes.removed, 'r') \
663 664 + mark(commit_changes.common, 'c'):
664 665
665 666 if raw_id in commit_cache:
666 667 commit = commit_cache[raw_id]
667 668 else:
668 669 try:
669 670 commit = commits_source_repo.get_commit(raw_id)
670 671 except CommitDoesNotExistError:
671 672 # in case we fail extracting still use "dummy" commit
672 673 # for display in commit diff
673 674 commit = h.AttributeDict(
674 675 {'raw_id': raw_id,
675 676 'message': 'EMPTY or MISSING COMMIT'})
676 677 c.commit_changes.append([c_type, commit])
677 678
678 679 # current user review statuses for each version
679 680 c.review_versions = {}
680 681 if self._rhodecode_user.user_id in allowed_reviewers:
681 682 for co in general_comments:
682 683 if co.author.user_id == self._rhodecode_user.user_id:
683 684 status = co.status_change
684 685 if status:
685 686 _ver_pr = status[0].comment.pull_request_version_id
686 687 c.review_versions[_ver_pr] = status[0]
687 688
688 689 return self._get_template_context(c)
689 690
690 691 def get_commits(
691 692 self, commits_source_repo, pull_request_at_ver, source_commit,
692 693 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
693 694 commit_cache = collections.OrderedDict()
694 695 missing_requirements = False
695 696 try:
696 697 pre_load = ["author", "date", "message", "branch", "parents"]
697 698 show_revs = pull_request_at_ver.revisions
698 699 for rev in show_revs:
699 700 comm = commits_source_repo.get_commit(
700 701 commit_id=rev, pre_load=pre_load)
701 702 commit_cache[comm.raw_id] = comm
702 703
703 704 # Order here matters, we first need to get target, and then
704 705 # the source
705 706 target_commit = commits_source_repo.get_commit(
706 707 commit_id=safe_str(target_ref_id))
707 708
708 709 source_commit = commits_source_repo.get_commit(
709 710 commit_id=safe_str(source_ref_id))
710 711 except CommitDoesNotExistError:
711 712 log.warning(
712 713 'Failed to get commit from `{}` repo'.format(
713 714 commits_source_repo), exc_info=True)
714 715 except RepositoryRequirementError:
715 716 log.warning(
716 717 'Failed to get all required data from repo', exc_info=True)
717 718 missing_requirements = True
718 719 ancestor_commit = None
719 720 try:
720 721 ancestor_id = source_scm.get_common_ancestor(
721 722 source_commit.raw_id, target_commit.raw_id, target_scm)
722 723 ancestor_commit = source_scm.get_commit(ancestor_id)
723 724 except Exception:
724 725 ancestor_commit = None
725 726 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
726 727
727 728 def assure_not_empty_repo(self):
728 729 _ = self.request.translate
729 730
730 731 try:
731 732 self.db_repo.scm_instance().get_commit()
732 733 except EmptyRepositoryError:
733 734 h.flash(h.literal(_('There are no commits yet')),
734 735 category='warning')
735 736 raise HTTPFound(
736 737 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
737 738
738 739 @LoginRequired()
739 740 @NotAnonymous()
740 741 @HasRepoPermissionAnyDecorator(
741 742 'repository.read', 'repository.write', 'repository.admin')
742 743 @view_config(
743 744 route_name='pullrequest_new', request_method='GET',
744 745 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
745 746 def pull_request_new(self):
746 747 _ = self.request.translate
747 748 c = self.load_default_context()
748 749
749 750 self.assure_not_empty_repo()
750 751 source_repo = self.db_repo
751 752
752 753 commit_id = self.request.GET.get('commit')
753 754 branch_ref = self.request.GET.get('branch')
754 755 bookmark_ref = self.request.GET.get('bookmark')
755 756
756 757 try:
757 758 source_repo_data = PullRequestModel().generate_repo_data(
758 759 source_repo, commit_id=commit_id,
759 760 branch=branch_ref, bookmark=bookmark_ref,
760 761 translator=self.request.translate)
761 762 except CommitDoesNotExistError as e:
762 763 log.exception(e)
763 764 h.flash(_('Commit does not exist'), 'error')
764 765 raise HTTPFound(
765 766 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
766 767
767 768 default_target_repo = source_repo
768 769
769 770 if source_repo.parent and c.has_origin_repo_read_perm:
770 771 parent_vcs_obj = source_repo.parent.scm_instance()
771 772 if parent_vcs_obj and not parent_vcs_obj.is_empty():
772 773 # change default if we have a parent repo
773 774 default_target_repo = source_repo.parent
774 775
775 776 target_repo_data = PullRequestModel().generate_repo_data(
776 777 default_target_repo, translator=self.request.translate)
777 778
778 779 selected_source_ref = source_repo_data['refs']['selected_ref']
779 780 title_source_ref = ''
780 781 if selected_source_ref:
781 782 title_source_ref = selected_source_ref.split(':', 2)[1]
782 783 c.default_title = PullRequestModel().generate_pullrequest_title(
783 784 source=source_repo.repo_name,
784 785 source_ref=title_source_ref,
785 786 target=default_target_repo.repo_name
786 787 )
787 788
788 789 c.default_repo_data = {
789 790 'source_repo_name': source_repo.repo_name,
790 791 'source_refs_json': json.dumps(source_repo_data),
791 792 'target_repo_name': default_target_repo.repo_name,
792 793 'target_refs_json': json.dumps(target_repo_data),
793 794 }
794 795 c.default_source_ref = selected_source_ref
795 796
796 797 return self._get_template_context(c)
797 798
798 799 @LoginRequired()
799 800 @NotAnonymous()
800 801 @HasRepoPermissionAnyDecorator(
801 802 'repository.read', 'repository.write', 'repository.admin')
802 803 @view_config(
803 804 route_name='pullrequest_repo_refs', request_method='GET',
804 805 renderer='json_ext', xhr=True)
805 806 def pull_request_repo_refs(self):
806 807 self.load_default_context()
807 808 target_repo_name = self.request.matchdict['target_repo_name']
808 809 repo = Repository.get_by_repo_name(target_repo_name)
809 810 if not repo:
810 811 raise HTTPNotFound()
811 812
812 813 target_perm = HasRepoPermissionAny(
813 814 'repository.read', 'repository.write', 'repository.admin')(
814 815 target_repo_name)
815 816 if not target_perm:
816 817 raise HTTPNotFound()
817 818
818 819 return PullRequestModel().generate_repo_data(
819 820 repo, translator=self.request.translate)
820 821
821 822 @LoginRequired()
822 823 @NotAnonymous()
823 824 @HasRepoPermissionAnyDecorator(
824 825 'repository.read', 'repository.write', 'repository.admin')
825 826 @view_config(
826 827 route_name='pullrequest_repo_targets', request_method='GET',
827 828 renderer='json_ext', xhr=True)
828 829 def pullrequest_repo_targets(self):
829 830 _ = self.request.translate
830 831 filter_query = self.request.GET.get('query')
831 832
832 833 # get the parents
833 834 parent_target_repos = []
834 835 if self.db_repo.parent:
835 836 parents_query = Repository.query() \
836 837 .order_by(func.length(Repository.repo_name)) \
837 838 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
838 839
839 840 if filter_query:
840 841 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
841 842 parents_query = parents_query.filter(
842 843 Repository.repo_name.ilike(ilike_expression))
843 844 parents = parents_query.limit(20).all()
844 845
845 846 for parent in parents:
846 847 parent_vcs_obj = parent.scm_instance()
847 848 if parent_vcs_obj and not parent_vcs_obj.is_empty():
848 849 parent_target_repos.append(parent)
849 850
850 851 # get other forks, and repo itself
851 852 query = Repository.query() \
852 853 .order_by(func.length(Repository.repo_name)) \
853 854 .filter(
854 855 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
855 856 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
856 857 ) \
857 858 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
858 859
859 860 if filter_query:
860 861 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
861 862 query = query.filter(Repository.repo_name.ilike(ilike_expression))
862 863
863 864 limit = max(20 - len(parent_target_repos), 5) # not less then 5
864 865 target_repos = query.limit(limit).all()
865 866
866 867 all_target_repos = target_repos + parent_target_repos
867 868
868 869 repos = []
869 870 # This checks permissions to the repositories
870 871 for obj in ScmModel().get_repos(all_target_repos):
871 872 repos.append({
872 873 'id': obj['name'],
873 874 'text': obj['name'],
874 875 'type': 'repo',
875 876 'repo_id': obj['dbrepo']['repo_id'],
876 877 'repo_type': obj['dbrepo']['repo_type'],
877 878 'private': obj['dbrepo']['private'],
878 879
879 880 })
880 881
881 882 data = {
882 883 'more': False,
883 884 'results': [{
884 885 'text': _('Repositories'),
885 886 'children': repos
886 887 }] if repos else []
887 888 }
888 889 return data
889 890
890 891 @LoginRequired()
891 892 @NotAnonymous()
892 893 @HasRepoPermissionAnyDecorator(
893 894 'repository.read', 'repository.write', 'repository.admin')
894 895 @CSRFRequired()
895 896 @view_config(
896 897 route_name='pullrequest_create', request_method='POST',
897 898 renderer=None)
898 899 def pull_request_create(self):
899 900 _ = self.request.translate
900 901 self.assure_not_empty_repo()
901 902 self.load_default_context()
902 903
903 904 controls = peppercorn.parse(self.request.POST.items())
904 905
905 906 try:
906 907 form = PullRequestForm(
907 908 self.request.translate, self.db_repo.repo_id)()
908 909 _form = form.to_python(controls)
909 910 except formencode.Invalid as errors:
910 911 if errors.error_dict.get('revisions'):
911 912 msg = 'Revisions: %s' % errors.error_dict['revisions']
912 913 elif errors.error_dict.get('pullrequest_title'):
913 914 msg = errors.error_dict.get('pullrequest_title')
914 915 else:
915 916 msg = _('Error creating pull request: {}').format(errors)
916 917 log.exception(msg)
917 918 h.flash(msg, 'error')
918 919
919 920 # would rather just go back to form ...
920 921 raise HTTPFound(
921 922 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
922 923
923 924 source_repo = _form['source_repo']
924 925 source_ref = _form['source_ref']
925 926 target_repo = _form['target_repo']
926 927 target_ref = _form['target_ref']
927 928 commit_ids = _form['revisions'][::-1]
928 929
929 930 # find the ancestor for this pr
930 931 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
931 932 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
932 933
933 934 if not (source_db_repo or target_db_repo):
934 935 h.flash(_('source_repo or target repo not found'), category='error')
935 936 raise HTTPFound(
936 937 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
937 938
938 939 # re-check permissions again here
939 940 # source_repo we must have read permissions
940 941
941 942 source_perm = HasRepoPermissionAny(
942 943 'repository.read', 'repository.write', 'repository.admin')(
943 944 source_db_repo.repo_name)
944 945 if not source_perm:
945 946 msg = _('Not Enough permissions to source repo `{}`.'.format(
946 947 source_db_repo.repo_name))
947 948 h.flash(msg, category='error')
948 949 # copy the args back to redirect
949 950 org_query = self.request.GET.mixed()
950 951 raise HTTPFound(
951 952 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
952 953 _query=org_query))
953 954
954 955 # target repo we must have read permissions, and also later on
955 956 # we want to check branch permissions here
956 957 target_perm = HasRepoPermissionAny(
957 958 'repository.read', 'repository.write', 'repository.admin')(
958 959 target_db_repo.repo_name)
959 960 if not target_perm:
960 961 msg = _('Not Enough permissions to target repo `{}`.'.format(
961 962 target_db_repo.repo_name))
962 963 h.flash(msg, category='error')
963 964 # copy the args back to redirect
964 965 org_query = self.request.GET.mixed()
965 966 raise HTTPFound(
966 967 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
967 968 _query=org_query))
968 969
969 970 source_scm = source_db_repo.scm_instance()
970 971 target_scm = target_db_repo.scm_instance()
971 972
972 973 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
973 974 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
974 975
975 976 ancestor = source_scm.get_common_ancestor(
976 977 source_commit.raw_id, target_commit.raw_id, target_scm)
977 978
978 979 # recalculate target ref based on ancestor
979 980 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
980 981 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
981 982
982 983 get_default_reviewers_data, validate_default_reviewers = \
983 984 PullRequestModel().get_reviewer_functions()
984 985
985 986 # recalculate reviewers logic, to make sure we can validate this
986 987 reviewer_rules = get_default_reviewers_data(
987 988 self._rhodecode_db_user, source_db_repo,
988 989 source_commit, target_db_repo, target_commit)
989 990
990 991 given_reviewers = _form['review_members']
991 992 reviewers = validate_default_reviewers(
992 993 given_reviewers, reviewer_rules)
993 994
994 995 pullrequest_title = _form['pullrequest_title']
995 996 title_source_ref = source_ref.split(':', 2)[1]
996 997 if not pullrequest_title:
997 998 pullrequest_title = PullRequestModel().generate_pullrequest_title(
998 999 source=source_repo,
999 1000 source_ref=title_source_ref,
1000 1001 target=target_repo
1001 1002 )
1002 1003
1003 1004 description = _form['pullrequest_desc']
1004 1005 description_renderer = _form['description_renderer']
1005 1006
1006 1007 try:
1007 1008 pull_request = PullRequestModel().create(
1008 1009 created_by=self._rhodecode_user.user_id,
1009 1010 source_repo=source_repo,
1010 1011 source_ref=source_ref,
1011 1012 target_repo=target_repo,
1012 1013 target_ref=target_ref,
1013 1014 revisions=commit_ids,
1014 1015 reviewers=reviewers,
1015 1016 title=pullrequest_title,
1016 1017 description=description,
1017 1018 description_renderer=description_renderer,
1018 1019 reviewer_data=reviewer_rules,
1019 1020 auth_user=self._rhodecode_user
1020 1021 )
1021 1022 Session().commit()
1022 1023
1023 1024 h.flash(_('Successfully opened new pull request'),
1024 1025 category='success')
1025 1026 except Exception:
1026 1027 msg = _('Error occurred during creation of this pull request.')
1027 1028 log.exception(msg)
1028 1029 h.flash(msg, category='error')
1029 1030
1030 1031 # copy the args back to redirect
1031 1032 org_query = self.request.GET.mixed()
1032 1033 raise HTTPFound(
1033 1034 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1034 1035 _query=org_query))
1035 1036
1036 1037 raise HTTPFound(
1037 1038 h.route_path('pullrequest_show', repo_name=target_repo,
1038 1039 pull_request_id=pull_request.pull_request_id))
1039 1040
1040 1041 @LoginRequired()
1041 1042 @NotAnonymous()
1042 1043 @HasRepoPermissionAnyDecorator(
1043 1044 'repository.read', 'repository.write', 'repository.admin')
1044 1045 @CSRFRequired()
1045 1046 @view_config(
1046 1047 route_name='pullrequest_update', request_method='POST',
1047 1048 renderer='json_ext')
1048 1049 def pull_request_update(self):
1049 1050 pull_request = PullRequest.get_or_404(
1050 1051 self.request.matchdict['pull_request_id'])
1051 1052 _ = self.request.translate
1052 1053
1053 1054 self.load_default_context()
1054 1055 redirect_url = None
1055 1056
1056 1057 if pull_request.is_closed():
1057 1058 log.debug('update: forbidden because pull request is closed')
1058 1059 msg = _(u'Cannot update closed pull requests.')
1059 1060 h.flash(msg, category='error')
1060 1061 return {'response': True,
1061 1062 'redirect_url': redirect_url}
1062 1063
1063 1064 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1064 1065 log.debug('update: forbidden because pull request is in state %s',
1065 1066 pull_request.pull_request_state)
1066 1067 msg = _(u'Cannot update pull requests in state other than `{}`. '
1067 1068 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1068 1069 pull_request.pull_request_state)
1069 1070 h.flash(msg, category='error')
1070 1071 return {'response': True,
1071 1072 'redirect_url': redirect_url}
1072 1073
1073 1074 # only owner or admin can update it
1074 1075 allowed_to_update = PullRequestModel().check_user_update(
1075 1076 pull_request, self._rhodecode_user)
1076 1077 if allowed_to_update:
1077 1078 controls = peppercorn.parse(self.request.POST.items())
1078 1079 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1079 1080
1080 1081 if 'review_members' in controls:
1081 1082 self._update_reviewers(
1082 1083 pull_request, controls['review_members'],
1083 1084 pull_request.reviewer_data)
1084 1085 elif str2bool(self.request.POST.get('update_commits', 'false')):
1085 1086 self._update_commits(pull_request)
1086 1087 if force_refresh:
1087 1088 redirect_url = h.route_path(
1088 1089 'pullrequest_show', repo_name=self.db_repo_name,
1089 1090 pull_request_id=pull_request.pull_request_id,
1090 1091 _query={"force_refresh": 1})
1091 1092 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1092 1093 self._edit_pull_request(pull_request)
1093 1094 else:
1094 1095 raise HTTPBadRequest()
1095 1096
1096 1097 return {'response': True,
1097 1098 'redirect_url': redirect_url}
1098 1099 raise HTTPForbidden()
1099 1100
1100 1101 def _edit_pull_request(self, pull_request):
1101 1102 _ = self.request.translate
1102 1103
1103 1104 try:
1104 1105 PullRequestModel().edit(
1105 1106 pull_request,
1106 1107 self.request.POST.get('title'),
1107 1108 self.request.POST.get('description'),
1108 1109 self.request.POST.get('description_renderer'),
1109 1110 self._rhodecode_user)
1110 1111 except ValueError:
1111 1112 msg = _(u'Cannot update closed pull requests.')
1112 1113 h.flash(msg, category='error')
1113 1114 return
1114 1115 else:
1115 1116 Session().commit()
1116 1117
1117 1118 msg = _(u'Pull request title & description updated.')
1118 1119 h.flash(msg, category='success')
1119 1120 return
1120 1121
1121 1122 def _update_commits(self, pull_request):
1122 1123 _ = self.request.translate
1123 1124
1124 1125 with pull_request.set_state(PullRequest.STATE_UPDATING):
1125 1126 resp = PullRequestModel().update_commits(pull_request)
1126 1127
1127 1128 if resp.executed:
1128 1129
1129 1130 if resp.target_changed and resp.source_changed:
1130 1131 changed = 'target and source repositories'
1131 1132 elif resp.target_changed and not resp.source_changed:
1132 1133 changed = 'target repository'
1133 1134 elif not resp.target_changed and resp.source_changed:
1134 1135 changed = 'source repository'
1135 1136 else:
1136 1137 changed = 'nothing'
1137 1138
1138 1139 msg = _(u'Pull request updated to "{source_commit_id}" with '
1139 1140 u'{count_added} added, {count_removed} removed commits. '
1140 1141 u'Source of changes: {change_source}')
1141 1142 msg = msg.format(
1142 1143 source_commit_id=pull_request.source_ref_parts.commit_id,
1143 1144 count_added=len(resp.changes.added),
1144 1145 count_removed=len(resp.changes.removed),
1145 1146 change_source=changed)
1146 1147 h.flash(msg, category='success')
1147 1148
1148 1149 channel = '/repo${}$/pr/{}'.format(
1149 1150 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1150 1151 message = msg + (
1151 1152 ' - <a onclick="window.location.reload()">'
1152 1153 '<strong>{}</strong></a>'.format(_('Reload page')))
1153 1154 channelstream.post_message(
1154 1155 channel, message, self._rhodecode_user.username,
1155 1156 registry=self.request.registry)
1156 1157 else:
1157 1158 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1158 1159 warning_reasons = [
1159 1160 UpdateFailureReason.NO_CHANGE,
1160 1161 UpdateFailureReason.WRONG_REF_TYPE,
1161 1162 ]
1162 1163 category = 'warning' if resp.reason in warning_reasons else 'error'
1163 1164 h.flash(msg, category=category)
1164 1165
1165 1166 @LoginRequired()
1166 1167 @NotAnonymous()
1167 1168 @HasRepoPermissionAnyDecorator(
1168 1169 'repository.read', 'repository.write', 'repository.admin')
1169 1170 @CSRFRequired()
1170 1171 @view_config(
1171 1172 route_name='pullrequest_merge', request_method='POST',
1172 1173 renderer='json_ext')
1173 1174 def pull_request_merge(self):
1174 1175 """
1175 1176 Merge will perform a server-side merge of the specified
1176 1177 pull request, if the pull request is approved and mergeable.
1177 1178 After successful merging, the pull request is automatically
1178 1179 closed, with a relevant comment.
1179 1180 """
1180 1181 pull_request = PullRequest.get_or_404(
1181 1182 self.request.matchdict['pull_request_id'])
1182 1183 _ = self.request.translate
1183 1184
1184 1185 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1185 1186 log.debug('show: forbidden because pull request is in state %s',
1186 1187 pull_request.pull_request_state)
1187 1188 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1188 1189 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1189 1190 pull_request.pull_request_state)
1190 1191 h.flash(msg, category='error')
1191 1192 raise HTTPFound(
1192 1193 h.route_path('pullrequest_show',
1193 1194 repo_name=pull_request.target_repo.repo_name,
1194 1195 pull_request_id=pull_request.pull_request_id))
1195 1196
1196 1197 self.load_default_context()
1197 1198
1198 1199 with pull_request.set_state(PullRequest.STATE_UPDATING):
1199 1200 check = MergeCheck.validate(
1200 1201 pull_request, auth_user=self._rhodecode_user,
1201 1202 translator=self.request.translate)
1202 1203 merge_possible = not check.failed
1203 1204
1204 1205 for err_type, error_msg in check.errors:
1205 1206 h.flash(error_msg, category=err_type)
1206 1207
1207 1208 if merge_possible:
1208 1209 log.debug("Pre-conditions checked, trying to merge.")
1209 1210 extras = vcs_operation_context(
1210 1211 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1211 1212 username=self._rhodecode_db_user.username, action='push',
1212 1213 scm=pull_request.target_repo.repo_type)
1213 1214 with pull_request.set_state(PullRequest.STATE_UPDATING):
1214 1215 self._merge_pull_request(
1215 1216 pull_request, self._rhodecode_db_user, extras)
1216 1217 else:
1217 1218 log.debug("Pre-conditions failed, NOT merging.")
1218 1219
1219 1220 raise HTTPFound(
1220 1221 h.route_path('pullrequest_show',
1221 1222 repo_name=pull_request.target_repo.repo_name,
1222 1223 pull_request_id=pull_request.pull_request_id))
1223 1224
1224 1225 def _merge_pull_request(self, pull_request, user, extras):
1225 1226 _ = self.request.translate
1226 1227 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1227 1228
1228 1229 if merge_resp.executed:
1229 1230 log.debug("The merge was successful, closing the pull request.")
1230 1231 PullRequestModel().close_pull_request(
1231 1232 pull_request.pull_request_id, user)
1232 1233 Session().commit()
1233 1234 msg = _('Pull request was successfully merged and closed.')
1234 1235 h.flash(msg, category='success')
1235 1236 else:
1236 1237 log.debug(
1237 1238 "The merge was not successful. Merge response: %s", merge_resp)
1238 1239 msg = merge_resp.merge_status_message
1239 1240 h.flash(msg, category='error')
1240 1241
1241 1242 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1242 1243 _ = self.request.translate
1243 1244
1244 1245 get_default_reviewers_data, validate_default_reviewers = \
1245 1246 PullRequestModel().get_reviewer_functions()
1246 1247
1247 1248 try:
1248 1249 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1249 1250 except ValueError as e:
1250 1251 log.error('Reviewers Validation: {}'.format(e))
1251 1252 h.flash(e, category='error')
1252 1253 return
1253 1254
1254 1255 old_calculated_status = pull_request.calculated_review_status()
1255 1256 PullRequestModel().update_reviewers(
1256 1257 pull_request, reviewers, self._rhodecode_user)
1257 1258 h.flash(_('Pull request reviewers updated.'), category='success')
1258 1259 Session().commit()
1259 1260
1260 1261 # trigger status changed if change in reviewers changes the status
1261 1262 calculated_status = pull_request.calculated_review_status()
1262 1263 if old_calculated_status != calculated_status:
1263 1264 PullRequestModel().trigger_pull_request_hook(
1264 1265 pull_request, self._rhodecode_user, 'review_status_change',
1265 1266 data={'status': calculated_status})
1266 1267
1267 1268 @LoginRequired()
1268 1269 @NotAnonymous()
1269 1270 @HasRepoPermissionAnyDecorator(
1270 1271 'repository.read', 'repository.write', 'repository.admin')
1271 1272 @CSRFRequired()
1272 1273 @view_config(
1273 1274 route_name='pullrequest_delete', request_method='POST',
1274 1275 renderer='json_ext')
1275 1276 def pull_request_delete(self):
1276 1277 _ = self.request.translate
1277 1278
1278 1279 pull_request = PullRequest.get_or_404(
1279 1280 self.request.matchdict['pull_request_id'])
1280 1281 self.load_default_context()
1281 1282
1282 1283 pr_closed = pull_request.is_closed()
1283 1284 allowed_to_delete = PullRequestModel().check_user_delete(
1284 1285 pull_request, self._rhodecode_user) and not pr_closed
1285 1286
1286 1287 # only owner can delete it !
1287 1288 if allowed_to_delete:
1288 1289 PullRequestModel().delete(pull_request, self._rhodecode_user)
1289 1290 Session().commit()
1290 1291 h.flash(_('Successfully deleted pull request'),
1291 1292 category='success')
1292 1293 raise HTTPFound(h.route_path('pullrequest_show_all',
1293 1294 repo_name=self.db_repo_name))
1294 1295
1295 1296 log.warning('user %s tried to delete pull request without access',
1296 1297 self._rhodecode_user)
1297 1298 raise HTTPNotFound()
1298 1299
1299 1300 @LoginRequired()
1300 1301 @NotAnonymous()
1301 1302 @HasRepoPermissionAnyDecorator(
1302 1303 'repository.read', 'repository.write', 'repository.admin')
1303 1304 @CSRFRequired()
1304 1305 @view_config(
1305 1306 route_name='pullrequest_comment_create', request_method='POST',
1306 1307 renderer='json_ext')
1307 1308 def pull_request_comment_create(self):
1308 1309 _ = self.request.translate
1309 1310
1310 1311 pull_request = PullRequest.get_or_404(
1311 1312 self.request.matchdict['pull_request_id'])
1312 1313 pull_request_id = pull_request.pull_request_id
1313 1314
1314 1315 if pull_request.is_closed():
1315 1316 log.debug('comment: forbidden because pull request is closed')
1316 1317 raise HTTPForbidden()
1317 1318
1318 1319 allowed_to_comment = PullRequestModel().check_user_comment(
1319 1320 pull_request, self._rhodecode_user)
1320 1321 if not allowed_to_comment:
1321 1322 log.debug(
1322 1323 'comment: forbidden because pull request is from forbidden repo')
1323 1324 raise HTTPForbidden()
1324 1325
1325 1326 c = self.load_default_context()
1326 1327
1327 1328 status = self.request.POST.get('changeset_status', None)
1328 1329 text = self.request.POST.get('text')
1329 1330 comment_type = self.request.POST.get('comment_type')
1330 1331 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1331 1332 close_pull_request = self.request.POST.get('close_pull_request')
1332 1333
1333 1334 # the logic here should work like following, if we submit close
1334 1335 # pr comment, use `close_pull_request_with_comment` function
1335 1336 # else handle regular comment logic
1336 1337
1337 1338 if close_pull_request:
1338 1339 # only owner or admin or person with write permissions
1339 1340 allowed_to_close = PullRequestModel().check_user_update(
1340 1341 pull_request, self._rhodecode_user)
1341 1342 if not allowed_to_close:
1342 1343 log.debug('comment: forbidden because not allowed to close '
1343 1344 'pull request %s', pull_request_id)
1344 1345 raise HTTPForbidden()
1345 1346
1346 1347 # This also triggers `review_status_change`
1347 1348 comment, status = PullRequestModel().close_pull_request_with_comment(
1348 1349 pull_request, self._rhodecode_user, self.db_repo, message=text,
1349 1350 auth_user=self._rhodecode_user)
1350 1351 Session().flush()
1351 1352
1352 1353 PullRequestModel().trigger_pull_request_hook(
1353 1354 pull_request, self._rhodecode_user, 'comment',
1354 1355 data={'comment': comment})
1355 1356
1356 1357 else:
1357 1358 # regular comment case, could be inline, or one with status.
1358 1359 # for that one we check also permissions
1359 1360
1360 1361 allowed_to_change_status = PullRequestModel().check_user_change_status(
1361 1362 pull_request, self._rhodecode_user)
1362 1363
1363 1364 if status and allowed_to_change_status:
1364 1365 message = (_('Status change %(transition_icon)s %(status)s')
1365 1366 % {'transition_icon': '>',
1366 1367 'status': ChangesetStatus.get_status_lbl(status)})
1367 1368 text = text or message
1368 1369
1369 1370 comment = CommentsModel().create(
1370 1371 text=text,
1371 1372 repo=self.db_repo.repo_id,
1372 1373 user=self._rhodecode_user.user_id,
1373 1374 pull_request=pull_request,
1374 1375 f_path=self.request.POST.get('f_path'),
1375 1376 line_no=self.request.POST.get('line'),
1376 1377 status_change=(ChangesetStatus.get_status_lbl(status)
1377 1378 if status and allowed_to_change_status else None),
1378 1379 status_change_type=(status
1379 1380 if status and allowed_to_change_status else None),
1380 1381 comment_type=comment_type,
1381 1382 resolves_comment_id=resolves_comment_id,
1382 1383 auth_user=self._rhodecode_user
1383 1384 )
1384 1385
1385 1386 if allowed_to_change_status:
1386 1387 # calculate old status before we change it
1387 1388 old_calculated_status = pull_request.calculated_review_status()
1388 1389
1389 1390 # get status if set !
1390 1391 if status:
1391 1392 ChangesetStatusModel().set_status(
1392 1393 self.db_repo.repo_id,
1393 1394 status,
1394 1395 self._rhodecode_user.user_id,
1395 1396 comment,
1396 1397 pull_request=pull_request
1397 1398 )
1398 1399
1399 1400 Session().flush()
1400 1401 # this is somehow required to get access to some relationship
1401 1402 # loaded on comment
1402 1403 Session().refresh(comment)
1403 1404
1404 1405 PullRequestModel().trigger_pull_request_hook(
1405 1406 pull_request, self._rhodecode_user, 'comment',
1406 1407 data={'comment': comment})
1407 1408
1408 1409 # we now calculate the status of pull request, and based on that
1409 1410 # calculation we set the commits status
1410 1411 calculated_status = pull_request.calculated_review_status()
1411 1412 if old_calculated_status != calculated_status:
1412 1413 PullRequestModel().trigger_pull_request_hook(
1413 1414 pull_request, self._rhodecode_user, 'review_status_change',
1414 1415 data={'status': calculated_status})
1415 1416
1416 1417 Session().commit()
1417 1418
1418 1419 data = {
1419 1420 'target_id': h.safeid(h.safe_unicode(
1420 1421 self.request.POST.get('f_path'))),
1421 1422 }
1422 1423 if comment:
1423 1424 c.co = comment
1424 1425 rendered_comment = render(
1425 1426 'rhodecode:templates/changeset/changeset_comment_block.mako',
1426 1427 self._get_template_context(c), self.request)
1427 1428
1428 1429 data.update(comment.get_dict())
1429 1430 data.update({'rendered_text': rendered_comment})
1430 1431
1431 1432 return data
1432 1433
1433 1434 @LoginRequired()
1434 1435 @NotAnonymous()
1435 1436 @HasRepoPermissionAnyDecorator(
1436 1437 'repository.read', 'repository.write', 'repository.admin')
1437 1438 @CSRFRequired()
1438 1439 @view_config(
1439 1440 route_name='pullrequest_comment_delete', request_method='POST',
1440 1441 renderer='json_ext')
1441 1442 def pull_request_comment_delete(self):
1442 1443 pull_request = PullRequest.get_or_404(
1443 1444 self.request.matchdict['pull_request_id'])
1444 1445
1445 1446 comment = ChangesetComment.get_or_404(
1446 1447 self.request.matchdict['comment_id'])
1447 1448 comment_id = comment.comment_id
1448 1449
1449 1450 if pull_request.is_closed():
1450 1451 log.debug('comment: forbidden because pull request is closed')
1451 1452 raise HTTPForbidden()
1452 1453
1453 1454 if not comment:
1454 1455 log.debug('Comment with id:%s not found, skipping', comment_id)
1455 1456 # comment already deleted in another call probably
1456 1457 return True
1457 1458
1458 1459 if comment.pull_request.is_closed():
1459 1460 # don't allow deleting comments on closed pull request
1460 1461 raise HTTPForbidden()
1461 1462
1462 1463 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1463 1464 super_admin = h.HasPermissionAny('hg.admin')()
1464 1465 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1465 1466 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1466 1467 comment_repo_admin = is_repo_admin and is_repo_comment
1467 1468
1468 1469 if super_admin or comment_owner or comment_repo_admin:
1469 1470 old_calculated_status = comment.pull_request.calculated_review_status()
1470 1471 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1471 1472 Session().commit()
1472 1473 calculated_status = comment.pull_request.calculated_review_status()
1473 1474 if old_calculated_status != calculated_status:
1474 1475 PullRequestModel().trigger_pull_request_hook(
1475 1476 comment.pull_request, self._rhodecode_user, 'review_status_change',
1476 1477 data={'status': calculated_status})
1477 1478 return True
1478 1479 else:
1479 1480 log.warning('No permissions for user %s to delete comment_id: %s',
1480 1481 self._rhodecode_db_user, comment_id)
1481 1482 raise HTTPNotFound()
@@ -1,470 +1,474 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 <%def name="metatags_help()">
7 7 <table>
8 8 <%
9 9 example_tags = [
10 10 ('state','[stable]'),
11 11 ('state','[stale]'),
12 12 ('state','[featured]'),
13 13 ('state','[dev]'),
14 14 ('state','[dead]'),
15 15 ('state','[deprecated]'),
16 16
17 17 ('label','[personal]'),
18 18 ('generic','[v2.0.0]'),
19 19
20 20 ('lang','[lang =&gt; JavaScript]'),
21 21 ('license','[license =&gt; LicenseName]'),
22 22
23 23 ('ref','[requires =&gt; RepoName]'),
24 24 ('ref','[recommends =&gt; GroupName]'),
25 25 ('ref','[conflicts =&gt; SomeName]'),
26 26 ('ref','[base =&gt; SomeName]'),
27 27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 28 ('see','[see =&gt; http://rhodecode.com]'),
29 29 ]
30 30 %>
31 31 % for tag_type, tag in example_tags:
32 32 <tr>
33 33 <td>${tag|n}</td>
34 34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 35 </tr>
36 36 % endfor
37 37 </table>
38 38 </%def>
39 39
40 40 <%def name="render_description(description, stylify_metatags)">
41 41 <%
42 42 tags = []
43 43 if stylify_metatags:
44 44 tags, description = h.extract_metatags(description)
45 45 %>
46 46 % for tag_type, tag in tags:
47 47 ${h.style_metatag(tag_type, tag)|n,trim}
48 48 % endfor
49 49 <code style="white-space: pre-wrap">${description}</code>
50 50 </%def>
51 51
52 52 ## REPOSITORY RENDERERS
53 53 <%def name="quick_menu(repo_name)">
54 54 <i class="icon-more"></i>
55 55 <div class="menu_items_container hidden">
56 56 <ul class="menu_items">
57 57 <li>
58 58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 59 <span>${_('Summary')}</span>
60 60 </a>
61 61 </li>
62 62 <li>
63 63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 64 <span>${_('Commits')}</span>
65 65 </a>
66 66 </li>
67 67 <li>
68 68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 69 <span>${_('Files')}</span>
70 70 </a>
71 71 </li>
72 72 <li>
73 73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 74 <span>${_('Fork')}</span>
75 75 </a>
76 76 </li>
77 77 </ul>
78 78 </div>
79 79 </%def>
80 80
81 81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
82 82 <%
83 83 def get_name(name,short_name=short_name):
84 84 if short_name:
85 85 return name.split('/')[-1]
86 86 else:
87 87 return name
88 88 %>
89 89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 90 ##NAME
91 91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92 92
93 93 ##TYPE OF REPO
94 94 %if h.is_hg(rtype):
95 95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 96 %elif h.is_git(rtype):
97 97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 98 %elif h.is_svn(rtype):
99 99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 100 %endif
101 101
102 102 ##PRIVATE/PUBLIC
103 103 %if private is True and c.visual.show_private_icon:
104 104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 105 %elif private is False and c.visual.show_public_icon:
106 106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 107 %else:
108 108 <span></span>
109 109 %endif
110 110 ${get_name(name)}
111 111 </a>
112 112 %if fork_of:
113 113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
114 114 %endif
115 115 %if rstate == 'repo_state_pending':
116 116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 117 (${_('creating...')})
118 118 </span>
119 119 %endif
120 120
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_desc(description, stylify_metatags)">
125 125 <%
126 126 tags, description = h.extract_metatags(description)
127 127 %>
128 128
129 129 <div class="truncate-wrap">
130 130 % if stylify_metatags:
131 131 % for tag_type, tag in tags:
132 132 ${h.style_metatag(tag_type, tag)|n}
133 133 % endfor
134 134 % endif
135 135 ${description}
136 136 </div>
137 137
138 138 </%def>
139 139
140 140 <%def name="last_change(last_change)">
141 141 ${h.age_component(last_change, time_is_local=True)}
142 142 </%def>
143 143
144 144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 145 <div>
146 146 %if rev >= 0:
147 147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 148 %else:
149 149 ${_('No commits yet')}
150 150 %endif
151 151 </div>
152 152 </%def>
153 153
154 154 <%def name="rss(name)">
155 155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 157 %else:
158 158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 159 %endif
160 160 </%def>
161 161
162 162 <%def name="atom(name)">
163 163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 165 %else:
166 166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 167 %endif
168 168 </%def>
169 169
170 170 <%def name="repo_actions(repo_name, super_user=True)">
171 171 <div>
172 172 <div class="grid_edit">
173 173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 174 Edit
175 175 </a>
176 176 </div>
177 177 <div class="grid_delete">
178 178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 179 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
180 180 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
181 181 ${h.end_form()}
182 182 </div>
183 183 </div>
184 184 </%def>
185 185
186 186 <%def name="repo_state(repo_state)">
187 187 <div>
188 188 %if repo_state == 'repo_state_pending':
189 189 <div class="tag tag4">${_('Creating')}</div>
190 190 %elif repo_state == 'repo_state_created':
191 191 <div class="tag tag1">${_('Created')}</div>
192 192 %else:
193 193 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
194 194 %endif
195 195 </div>
196 196 </%def>
197 197
198 198
199 199 ## REPO GROUP RENDERERS
200 200 <%def name="quick_repo_group_menu(repo_group_name)">
201 201 <i class="icon-more"></i>
202 202 <div class="menu_items_container hidden">
203 203 <ul class="menu_items">
204 204 <li>
205 205 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
206 206 </li>
207 207
208 208 </ul>
209 209 </div>
210 210 </%def>
211 211
212 212 <%def name="repo_group_name(repo_group_name, children_groups=None)">
213 213 <div>
214 214 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
215 215 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
216 216 %if children_groups:
217 217 ${h.literal(' &raquo; '.join(children_groups))}
218 218 %else:
219 219 ${repo_group_name}
220 220 %endif
221 221 </a>
222 222 </div>
223 223 </%def>
224 224
225 225 <%def name="repo_group_desc(description, personal, stylify_metatags)">
226 226
227 227 <%
228 228 if stylify_metatags:
229 229 tags, description = h.extract_metatags(description)
230 230 %>
231 231
232 232 <div class="truncate-wrap">
233 233 % if personal:
234 234 <div class="metatag" tag="personal">${_('personal')}</div>
235 235 % endif
236 236
237 237 % if stylify_metatags:
238 238 % for tag_type, tag in tags:
239 239 ${h.style_metatag(tag_type, tag)|n}
240 240 % endfor
241 241 % endif
242 242 ${description}
243 243 </div>
244 244
245 245 </%def>
246 246
247 247 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
248 248 <div class="grid_edit">
249 249 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
250 250 </div>
251 251 <div class="grid_delete">
252 252 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
253 253 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
254 254 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)+"');")}
255 255 ${h.end_form()}
256 256 </div>
257 257 </%def>
258 258
259 259
260 260 <%def name="user_actions(user_id, username)">
261 261 <div class="grid_edit">
262 262 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
263 263 ${_('Edit')}
264 264 </a>
265 265 </div>
266 266 <div class="grid_delete">
267 267 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
268 268 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
269 269 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
270 270 ${h.end_form()}
271 271 </div>
272 272 </%def>
273 273
274 274 <%def name="user_group_actions(user_group_id, user_group_name)">
275 275 <div class="grid_edit">
276 276 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
277 277 </div>
278 278 <div class="grid_delete">
279 279 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
280 280 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
281 281 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
282 282 ${h.end_form()}
283 283 </div>
284 284 </%def>
285 285
286 286
287 287 <%def name="user_name(user_id, username)">
288 288 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
289 289 </%def>
290 290
291 291 <%def name="user_profile(username)">
292 292 ${base.gravatar_with_user(username, 16, tooltip=True)}
293 293 </%def>
294 294
295 295 <%def name="user_group_name(user_group_name)">
296 296 <div>
297 297 <i class="icon-user-group" title="${_('User group')}"></i>
298 298 ${h.link_to_group(user_group_name)}
299 299 </div>
300 300 </%def>
301 301
302 302
303 303 ## GISTS
304 304
305 305 <%def name="gist_gravatar(full_contact)">
306 306 <div class="gist_gravatar">
307 307 ${base.gravatar(full_contact, 30)}
308 308 </div>
309 309 </%def>
310 310
311 311 <%def name="gist_access_id(gist_access_id, full_contact)">
312 312 <div>
313 313 <b>
314 314 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
315 315 </b>
316 316 </div>
317 317 </%def>
318 318
319 319 <%def name="gist_author(full_contact, created_on, expires)">
320 320 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
321 321 </%def>
322 322
323 323
324 324 <%def name="gist_created(created_on)">
325 325 <div class="created">
326 326 ${h.age_component(created_on, time_is_local=True)}
327 327 </div>
328 328 </%def>
329 329
330 330 <%def name="gist_expires(expires)">
331 331 <div class="created">
332 332 %if expires == -1:
333 333 ${_('never')}
334 334 %else:
335 335 ${h.age_component(h.time_to_utcdatetime(expires))}
336 336 %endif
337 337 </div>
338 338 </%def>
339 339
340 340 <%def name="gist_type(gist_type)">
341 341 %if gist_type == 'public':
342 342 <span class="tag tag-gist-public disabled">${_('Public Gist')}</span>
343 343 %else:
344 344 <span class="tag tag-gist-private disabled">${_('Private Gist')}</span>
345 345 %endif
346 346 </%def>
347 347
348 348 <%def name="gist_description(gist_description)">
349 349 ${gist_description}
350 350 </%def>
351 351
352 352
353 353 ## PULL REQUESTS GRID RENDERERS
354 354
355 355 <%def name="pullrequest_target_repo(repo_name)">
356 356 <div class="truncate">
357 357 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
358 358 </div>
359 359 </%def>
360 360
361 361 <%def name="pullrequest_status(status)">
362 362 <i class="icon-circle review-status-${status}"></i>
363 363 </%def>
364 364
365 365 <%def name="pullrequest_title(title, description)">
366 366 ${title}
367 367 </%def>
368 368
369 369 <%def name="pullrequest_comments(comments_nr)">
370 370 <i class="icon-comment"></i> ${comments_nr}
371 371 </%def>
372 372
373 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
373 <%def name="pullrequest_name(pull_request_id, is_wip, target_repo_name, short=False)">
374 374 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
375 % if is_wip:
376 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
377 % endif
378
375 379 % if short:
376 380 !${pull_request_id}
377 381 % else:
378 382 ${_('Pull request !{}').format(pull_request_id)}
379 383 % endif
380 384 </a>
381 385 </%def>
382 386
383 387 <%def name="pullrequest_updated_on(updated_on)">
384 388 ${h.age_component(h.time_to_utcdatetime(updated_on))}
385 389 </%def>
386 390
387 391 <%def name="pullrequest_author(full_contact)">
388 392 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
389 393 </%def>
390 394
391 395
392 396 ## ARTIFACT RENDERERS
393 397 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
394 398 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
395 399 ${artifact_display_name or '_EMPTY_NAME_'}
396 400 </a>
397 401 </%def>
398 402
399 403 <%def name="repo_artifact_uid(repo_name, file_uid)">
400 404 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
401 405 </%def>
402 406
403 407 <%def name="repo_artifact_sha256(artifact_sha256)">
404 408 <div class="code">${h.shorter(artifact_sha256, 12)}</div>
405 409 </%def>
406 410
407 411 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
408 412 ## <div class="grid_edit">
409 413 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
410 414 ## </div>
411 415 <div class="grid_edit">
412 416 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
413 417 </div>
414 418 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
415 419 <div class="grid_delete">
416 420 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
417 421 ${h.submit('remove_',_('Delete'),id="remove_artifact_%s" % file_store_id, class_="btn btn-link btn-danger",
418 422 onclick="return confirm('"+_('Confirm to delete this artifact: %s') % file_uid+"');")}
419 423 ${h.end_form()}
420 424 </div>
421 425 % endif
422 426 </%def>
423 427
424 428 <%def name="markup_form(form_id, form_text='', help_text=None)">
425 429
426 430 <div class="markup-form">
427 431 <div class="markup-form-area">
428 432 <div class="markup-form-area-header">
429 433 <ul class="nav-links clearfix">
430 434 <li class="active">
431 435 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
432 436 </li>
433 437 <li class="">
434 438 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
435 439 </li>
436 440 </ul>
437 441 </div>
438 442
439 443 <div class="markup-form-area-write" style="display: block;">
440 444 <div id="edit-container_${form_id}">
441 445 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
442 446 </div>
443 447 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
444 448 <div id="preview-box_${form_id}" class="preview-box"></div>
445 449 </div>
446 450 </div>
447 451
448 452 <div class="markup-form-area-footer">
449 453 <div class="toolbar">
450 454 <div class="toolbar-text">
451 455 ${(_('Parsed using %s syntax') % (
452 456 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
453 457 )
454 458 )|n}
455 459 </div>
456 460 </div>
457 461 </div>
458 462 </div>
459 463
460 464 <div class="markup-form-footer">
461 465 % if help_text:
462 466 <span class="help-block">${help_text}</span>
463 467 % endif
464 468 </div>
465 469 </div>
466 470 <script type="text/javascript">
467 471 new MarkupForm('${form_id}');
468 472 </script>
469 473
470 474 </%def>
@@ -1,540 +1,543 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('New pull request')}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()"></%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="menu_bar_subnav()">
15 15 ${self.repo_menu(active='showpullrequest')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 <div class="box">
20 20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
21 21
22 22 <div class="box pr-summary">
23 23
24 24 <div class="summary-details block-left">
25 25
26 26
27 27 <div class="pr-details-title">
28 28 ${_('New pull request')}
29 29 </div>
30 30
31 31 <div class="form" style="padding-top: 10px">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 <p class="help-block">
44 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
45 </p>
43 46 </div>
44 47
45 48 <div class="field">
46 49 <div class="label label-textarea">
47 50 <label for="pullrequest_desc">${_('Description')}:</label>
48 51 </div>
49 <div class="textarea text-area editor">
52 <div class="textarea text-area">
50 53 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
51 54 ${dt.markup_form('pullrequest_desc')}
52 55 </div>
53 56 </div>
54 57
55 58 <div class="field">
56 59 <div class="label label-textarea">
57 60 <label for="commit_flow">${_('Commit flow')}:</label>
58 61 </div>
59 62
60 63 ## TODO: johbo: Abusing the "content" class here to get the
61 64 ## desired effect. Should be replaced by a proper solution.
62 65
63 66 ##ORG
64 67 <div class="content">
65 68 <strong>${_('Source repository')}:</strong>
66 69 ${c.rhodecode_db_repo.description}
67 70 </div>
68 71 <div class="content">
69 72 ${h.hidden('source_repo')}
70 73 ${h.hidden('source_ref')}
71 74 </div>
72 75
73 76 ##OTHER, most Probably the PARENT OF THIS FORK
74 77 <div class="content">
75 78 ## filled with JS
76 79 <div id="target_repo_desc"></div>
77 80 </div>
78 81
79 82 <div class="content">
80 83 ${h.hidden('target_repo')}
81 84 ${h.hidden('target_ref')}
82 85 <span id="target_ref_loading" style="display: none">
83 86 ${_('Loading refs...')}
84 87 </span>
85 88 </div>
86 89 </div>
87 90
88 91 <div class="field">
89 92 <div class="label label-textarea">
90 93 <label for="pullrequest_submit"></label>
91 94 </div>
92 95 <div class="input">
93 96 <div class="pr-submit-button">
94 97 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
95 98 </div>
96 99 <div id="pr_open_message"></div>
97 100 </div>
98 101 </div>
99 102
100 103 <div class="pr-spacing-container"></div>
101 104 </div>
102 105 </div>
103 106 </div>
104 107 <div>
105 108 ## AUTHOR
106 109 <div class="reviewers-title block-right">
107 110 <div class="pr-details-title">
108 111 ${_('Author of this pull request')}
109 112 </div>
110 113 </div>
111 114 <div class="block-right pr-details-content reviewers">
112 115 <ul class="group_members">
113 116 <li>
114 117 ${self.gravatar_with_user(c.rhodecode_user.email, 16, tooltip=True)}
115 118 </li>
116 119 </ul>
117 120 </div>
118 121
119 122 ## REVIEW RULES
120 123 <div id="review_rules" style="display: none" class="reviewers-title block-right">
121 124 <div class="pr-details-title">
122 125 ${_('Reviewer rules')}
123 126 </div>
124 127 <div class="pr-reviewer-rules">
125 128 ## review rules will be appended here, by default reviewers logic
126 129 </div>
127 130 </div>
128 131
129 132 ## REVIEWERS
130 133 <div class="reviewers-title block-right">
131 134 <div class="pr-details-title">
132 135 ${_('Pull request reviewers')}
133 136 <span class="calculate-reviewers"> - ${_('loading...')}</span>
134 137 </div>
135 138 </div>
136 139 <div id="reviewers" class="block-right pr-details-content reviewers">
137 140 ## members goes here, filled via JS based on initial selection !
138 141 <input type="hidden" name="__start__" value="review_members:sequence">
139 142 <ul id="review_members" class="group_members"></ul>
140 143 <input type="hidden" name="__end__" value="review_members:sequence">
141 144 <div id="add_reviewer_input" class='ac'>
142 145 <div class="reviewer_ac">
143 146 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
144 147 <div id="reviewers_container"></div>
145 148 </div>
146 149 </div>
147 150 </div>
148 151 </div>
149 152 </div>
150 153 <div class="box">
151 154 <div>
152 155 ## overview pulled by ajax
153 156 <div id="pull_request_overview"></div>
154 157 </div>
155 158 </div>
156 159 ${h.end_form()}
157 160 </div>
158 161
159 162 <script type="text/javascript">
160 163 $(function(){
161 164 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
162 165 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
163 166 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
164 167 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
165 168
166 169 var $pullRequestForm = $('#pull_request_form');
167 170 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
168 171 var $sourceRepo = $('#source_repo', $pullRequestForm);
169 172 var $targetRepo = $('#target_repo', $pullRequestForm);
170 173 var $sourceRef = $('#source_ref', $pullRequestForm);
171 174 var $targetRef = $('#target_ref', $pullRequestForm);
172 175
173 176 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
174 177 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
175 178
176 179 var targetRepo = function() { return $targetRepo.eq(0).val() };
177 180 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
178 181
179 182 var calculateContainerWidth = function() {
180 183 var maxWidth = 0;
181 184 var repoSelect2Containers = ['#source_repo', '#target_repo'];
182 185 $.each(repoSelect2Containers, function(idx, value) {
183 186 $(value).select2('container').width('auto');
184 187 var curWidth = $(value).select2('container').width();
185 188 if (maxWidth <= curWidth) {
186 189 maxWidth = curWidth;
187 190 }
188 191 $.each(repoSelect2Containers, function(idx, value) {
189 192 $(value).select2('container').width(maxWidth + 10);
190 193 });
191 194 });
192 195 };
193 196
194 197 var initRefSelection = function(selectedRef) {
195 198 return function(element, callback) {
196 199 // translate our select2 id into a text, it's a mapping to show
197 200 // simple label when selecting by internal ID.
198 201 var id, refData;
199 202 if (selectedRef === undefined || selectedRef === null) {
200 203 id = element.val();
201 204 refData = element.val().split(':');
202 205
203 206 if (refData.length !== 3){
204 207 refData = ["", "", ""]
205 208 }
206 209 } else {
207 210 id = selectedRef;
208 211 refData = selectedRef.split(':');
209 212 }
210 213
211 214 var text = refData[1];
212 215 if (refData[0] === 'rev') {
213 216 text = text.substring(0, 12);
214 217 }
215 218
216 219 var data = {id: id, text: text};
217 220 callback(data);
218 221 };
219 222 };
220 223
221 224 var formatRefSelection = function(data, container, escapeMarkup) {
222 225 var prefix = '';
223 226 var refData = data.id.split(':');
224 227 if (refData[0] === 'branch') {
225 228 prefix = '<i class="icon-branch"></i>';
226 229 }
227 230 else if (refData[0] === 'book') {
228 231 prefix = '<i class="icon-bookmark"></i>';
229 232 }
230 233 else if (refData[0] === 'tag') {
231 234 prefix = '<i class="icon-tag"></i>';
232 235 }
233 236
234 237 var originalOption = data.element;
235 238 return prefix + escapeMarkup(data.text);
236 239 };formatSelection:
237 240
238 241 // custom code mirror
239 242 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
240 243
241 244 reviewersController = new ReviewersController();
242 245
243 246 var queryTargetRepo = function(self, query) {
244 247 // cache ALL results if query is empty
245 248 var cacheKey = query.term || '__';
246 249 var cachedData = self.cachedDataSource[cacheKey];
247 250
248 251 if (cachedData) {
249 252 query.callback({results: cachedData.results});
250 253 } else {
251 254 $.ajax({
252 255 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
253 256 data: {query: query.term},
254 257 dataType: 'json',
255 258 type: 'GET',
256 259 success: function(data) {
257 260 self.cachedDataSource[cacheKey] = data;
258 261 query.callback({results: data.results});
259 262 },
260 263 error: function(data, textStatus, errorThrown) {
261 264 alert(
262 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
263 266 }
264 267 });
265 268 }
266 269 };
267 270
268 271 var queryTargetRefs = function(initialData, query) {
269 272 var data = {results: []};
270 273 // filter initialData
271 274 $.each(initialData, function() {
272 275 var section = this.text;
273 276 var children = [];
274 277 $.each(this.children, function() {
275 278 if (query.term.length === 0 ||
276 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
277 280 children.push({'id': this.id, 'text': this.text})
278 281 }
279 282 });
280 283 data.results.push({'text': section, 'children': children})
281 284 });
282 285 query.callback({results: data.results});
283 286 };
284 287
285 288 var loadRepoRefDiffPreview = function() {
286 289
287 290 var url_data = {
288 291 'repo_name': targetRepo(),
289 292 'target_repo': sourceRepo(),
290 293 'source_ref': targetRef()[2],
291 294 'source_ref_type': 'rev',
292 295 'target_ref': sourceRef()[2],
293 296 'target_ref_type': 'rev',
294 297 'merge': true,
295 298 '_': Date.now() // bypass browser caching
296 299 }; // gather the source/target ref and repo here
297 300
298 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
299 302 prButtonLock(true, "${_('Please select source and target')}");
300 303 return;
301 304 }
302 305 var url = pyroutes.url('repo_compare', url_data);
303 306
304 307 // lock PR button, so we cannot send PR before it's calculated
305 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
306 309
307 310 if (loadRepoRefDiffPreview._currentRequest) {
308 311 loadRepoRefDiffPreview._currentRequest.abort();
309 312 }
310 313
311 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
312 315 .error(function(data, textStatus, errorThrown) {
313 316 if (textStatus !== 'abort') {
314 317 alert(
315 318 "Error while processing request.\nError code {0} ({1}).".format(
316 319 data.status, data.statusText));
317 320 }
318 321
319 322 })
320 323 .done(function(data) {
321 324 loadRepoRefDiffPreview._currentRequest = null;
322 325 $('#pull_request_overview').html(data);
323 326
324 327 var commitElements = $(data).find('tr[commit_id]');
325 328
326 329 var prTitleAndDesc = getTitleAndDescription(
327 330 sourceRef()[1], commitElements, 5);
328 331
329 332 var title = prTitleAndDesc[0];
330 333 var proposedDescription = prTitleAndDesc[1];
331 334
332 335 var useGeneratedTitle = (
333 336 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 337 $('#pullrequest_title').val() === "");
335 338
336 339 if (title && useGeneratedTitle) {
337 340 // use generated title if we haven't specified our own
338 341 $('#pullrequest_title').val(title);
339 342 $('#pullrequest_title').addClass('autogenerated-title');
340 343
341 344 }
342 345
343 346 var useGeneratedDescription = (
344 347 !codeMirrorInstance._userDefinedValue ||
345 348 codeMirrorInstance.getValue() === "");
346 349
347 350 if (proposedDescription && useGeneratedDescription) {
348 351 // set proposed content, if we haven't defined our own,
349 352 // or we don't have description written
350 353 codeMirrorInstance._userDefinedValue = false; // reset state
351 354 codeMirrorInstance.setValue(proposedDescription);
352 355 }
353 356
354 357 // refresh our codeMirror so events kicks in and it's change aware
355 358 codeMirrorInstance.refresh();
356 359
357 360 var msg = '';
358 361 if (commitElements.length === 1) {
359 362 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
360 363 } else {
361 364 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
362 365 }
363 366
364 367 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
365 368
366 369 if (commitElements.length) {
367 370 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
368 371 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
369 372 }
370 373 else {
371 374 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
372 375 }
373 376
374 377
375 378 });
376 379 };
377 380
378 381 var Select2Box = function(element, overrides) {
379 382 var globalDefaults = {
380 383 dropdownAutoWidth: true,
381 384 containerCssClass: "drop-menu",
382 385 dropdownCssClass: "drop-menu-dropdown"
383 386 };
384 387
385 388 var initSelect2 = function(defaultOptions) {
386 389 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
387 390 element.select2(options);
388 391 };
389 392
390 393 return {
391 394 initRef: function() {
392 395 var defaultOptions = {
393 396 minimumResultsForSearch: 5,
394 397 formatSelection: formatRefSelection
395 398 };
396 399
397 400 initSelect2(defaultOptions);
398 401 },
399 402
400 403 initRepo: function(defaultValue, readOnly) {
401 404 var defaultOptions = {
402 405 initSelection : function (element, callback) {
403 406 var data = {id: defaultValue, text: defaultValue};
404 407 callback(data);
405 408 }
406 409 };
407 410
408 411 initSelect2(defaultOptions);
409 412
410 413 element.select2('val', defaultSourceRepo);
411 414 if (readOnly === true) {
412 415 element.select2('readonly', true);
413 416 }
414 417 }
415 418 };
416 419 };
417 420
418 421 var initTargetRefs = function(refsData, selectedRef) {
419 422
420 423 Select2Box($targetRef, {
421 424 placeholder: "${_('Select commit reference')}",
422 425 query: function(query) {
423 426 queryTargetRefs(refsData, query);
424 427 },
425 428 initSelection : initRefSelection(selectedRef)
426 429 }).initRef();
427 430
428 431 if (!(selectedRef === undefined)) {
429 432 $targetRef.select2('val', selectedRef);
430 433 }
431 434 };
432 435
433 436 var targetRepoChanged = function(repoData) {
434 437 // generate new DESC of target repo displayed next to select
435 438 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
436 439 $('#target_repo_desc').html(
437 440 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
438 441 );
439 442
440 443 // generate dynamic select2 for refs.
441 444 initTargetRefs(repoData['refs']['select2_refs'],
442 445 repoData['refs']['selected_ref']);
443 446
444 447 };
445 448
446 449 var sourceRefSelect2 = Select2Box($sourceRef, {
447 450 placeholder: "${_('Select commit reference')}",
448 451 query: function(query) {
449 452 var initialData = defaultSourceRepoData['refs']['select2_refs'];
450 453 queryTargetRefs(initialData, query)
451 454 },
452 455 initSelection: initRefSelection()
453 456 }
454 457 );
455 458
456 459 var sourceRepoSelect2 = Select2Box($sourceRepo, {
457 460 query: function(query) {}
458 461 });
459 462
460 463 var targetRepoSelect2 = Select2Box($targetRepo, {
461 464 cachedDataSource: {},
462 465 query: $.debounce(250, function(query) {
463 466 queryTargetRepo(this, query);
464 467 }),
465 468 formatResult: formatRepoResult
466 469 });
467 470
468 471 sourceRefSelect2.initRef();
469 472
470 473 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
471 474
472 475 targetRepoSelect2.initRepo(defaultTargetRepo, false);
473 476
474 477 $sourceRef.on('change', function(e){
475 478 loadRepoRefDiffPreview();
476 479 reviewersController.loadDefaultReviewers(
477 480 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 481 });
479 482
480 483 $targetRef.on('change', function(e){
481 484 loadRepoRefDiffPreview();
482 485 reviewersController.loadDefaultReviewers(
483 486 sourceRepo(), sourceRef(), targetRepo(), targetRef());
484 487 });
485 488
486 489 $targetRepo.on('change', function(e){
487 490 var repoName = $(this).val();
488 491 calculateContainerWidth();
489 492 $targetRef.select2('destroy');
490 493 $('#target_ref_loading').show();
491 494
492 495 $.ajax({
493 496 url: pyroutes.url('pullrequest_repo_refs',
494 497 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
495 498 data: {},
496 499 dataType: 'json',
497 500 type: 'GET',
498 501 success: function(data) {
499 502 $('#target_ref_loading').hide();
500 503 targetRepoChanged(data);
501 504 loadRepoRefDiffPreview();
502 505 },
503 506 error: function(data, textStatus, errorThrown) {
504 507 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
505 508 }
506 509 })
507 510
508 511 });
509 512
510 513 $pullRequestForm.on('submit', function(e){
511 514 // Flush changes into textarea
512 515 codeMirrorInstance.save();
513 516 prButtonLock(true, null, 'all');
514 517 });
515 518
516 519 prButtonLock(true, "${_('Please select source and target')}", 'all');
517 520
518 521 // auto-load on init, the target refs select2
519 522 calculateContainerWidth();
520 523 targetRepoChanged(defaultTargetRepoData);
521 524
522 525 $('#pullrequest_title').on('keyup', function(e){
523 526 $(this).removeClass('autogenerated-title');
524 527 });
525 528
526 529 % if c.default_source_ref:
527 530 // in case we have a pre-selected value, use it now
528 531 $sourceRef.select2('val', '${c.default_source_ref}');
529 532 // diff preview load
530 533 loadRepoRefDiffPreview();
531 534 // default reviewers
532 535 reviewersController.loadDefaultReviewers(
533 536 sourceRepo(), sourceRef(), targetRepo(), targetRef());
534 537 % endif
535 538
536 539 ReviewerAutoComplete('#user');
537 540 });
538 541 </script>
539 542
540 543 </%def>
General Comments 0
You need to be logged in to leave comments. Login now