##// END OF EJS Templates
release: Merge default into stable for release preparation
milka -
r4566:091769de merge stable
parent child Browse files
Show More
@@ -0,0 +1,89 b''
1 |RCE| 4.23.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-11-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Comments: introduced new draft comments.
14
15 * drafts are private to author
16 * not triggering any notifications
17 * sidebar doesn't display draft comments
18 * They are just placeholders for longer review.
19
20 - Comments: when channelstream is enabled, comments are pushed live, so there's no
21 need to refresh page to see other participant comments.
22 New comments are marker in the sidebar.
23
24 - Comments: multiple changes on comments navigation/display logic.
25
26 * toggle icon is smarter, open/hide windows according to actions. E.g commenting opens threads
27 * toggle are mor explicit
28 * possible to hide/show only single threads using the toggle icon.
29 * new UI for showing thread comments
30
31 - Reviewers: new logic for author/commit-author rules.
32 It's not possible to define if author or commit author should be excluded, or always included in a review.
33 - Reviewers: no reviewers would now allow a PR to be merged, unless review rules require some.
34 Use case is that pr can be created without review needed, maybe just for sharing, or CI checks
35 - Pull requests: save permanently the state if sorting columns for pull-request grids.
36 - Commit ranges: enable combined diff compare directly from range selector.
37
38
39 General
40 ^^^^^^^
41
42 - Authentication: enable custom names for auth plugins. It's possible to name the authentication
43 buttons now for SAML plugins.
44 - Login: optimized UI for login/register/password reset windows.
45 - Repo mapper: make it more resilient to errors, it's better it executes and skip certain
46 repositories, rather then crash whole mapper.
47 - Markdown: improved styling, and fixed nl2br extensions to only do br on new elements not inline.
48 - Pull requests: show pr version in the my-account and repo pr listing grids.
49 - Archives: allowing to obtain archives without the commit short id in the name for
50 better automation of obtained artifacts.
51 New url flag called `?=with_hash=1` controls this
52 - Error document: update info about stored exception retrieval.
53 - Range diff: enable hovercards for commits in range-diff.
54
55
56 Security
57 ^^^^^^^^
58
59
60
61 Performance
62 ^^^^^^^^^^^
63
64 - Improved logic of repo archive, now it's much faster to run archiver as VCSServer
65 communication was removed, and job is delegated to VCSServer itself.
66 - Improved VCSServer startup times.
67 - Notifications: skip double rendering just to generate email title/desc.
68 We'll re-use those now for better performance of creating notifications.
69 - App: improve logging, and remove DB calls on app startup.
70
71
72 Fixes
73 ^^^^^
74
75 - Login/register: fixed header width problem on mobile devices
76 - Exception tracker: don't fail on empty request in context of celery app for example.
77 - Exceptions: improved reporting of unhandled vcsserver exceptions.
78 - Sidebar: fixed refresh of TODOs url.
79 - Remap-rescan: fixes #5636 initial rescan problem.
80 - API: fixed SVN raw diff export. The API method was inconsistent, and used different logic.
81 Now it shares the same code as raw-diff from web-ui.
82
83
84 Upgrade notes
85 ^^^^^^^^^^^^^
86
87 - Scheduled feature release.
88 Please note that now the reviewers logic changed a bit, it's possible to create a pull request
89 Without any reviewers initially, and such pull request doesn't need to have an approval for merging.
@@ -0,0 +1,53 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.ChangesetComment.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('draft', Boolean(), nullable=True)
32 batch_op.add_column(new_column)
33
34 _set_default_as_non_draft(op, meta.Session)
35
36
37 def downgrade(migrate_engine):
38 meta = MetaData()
39 meta.bind = migrate_engine
40
41
42 def fixups(models, _SESSION):
43 pass
44
45
46 def _set_default_as_non_draft(op, session):
47 params = {'draft': False}
48 query = text(
49 'UPDATE changeset_comments SET draft = :draft'
50 ).bindparams(**params)
51 op.execute(query)
52 session().commit()
53
@@ -0,0 +1,78 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRule.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31
32 new_column = Column('pr_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
33 batch_op.add_column(new_column)
34
35 new_column = Column('commit_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
36 batch_op.add_column(new_column)
37
38 _migrate_review_flags_to_new_cols(op, meta.Session)
39
40
41 def downgrade(migrate_engine):
42 meta = MetaData()
43 meta.bind = migrate_engine
44
45
46 def fixups(models, _SESSION):
47 pass
48
49
50 def _migrate_review_flags_to_new_cols(op, session):
51
52 # set defaults for pr_author
53 query = text(
54 'UPDATE repo_review_rules SET pr_author = :val'
55 ).bindparams(val='no_rule')
56 op.execute(query)
57
58 # set defaults for commit_author
59 query = text(
60 'UPDATE repo_review_rules SET commit_author = :val'
61 ).bindparams(val='no_rule')
62 op.execute(query)
63
64 session().commit()
65
66 # now change the flags to forbid based on
67 # forbid_author_to_review, forbid_commit_author_to_review
68 query = text(
69 'UPDATE repo_review_rules SET pr_author = :val WHERE forbid_author_to_review = TRUE'
70 ).bindparams(val='forbid_pr_author')
71 op.execute(query)
72
73 query = text(
74 'UPDATE repo_review_rules SET commit_author = :val WHERE forbid_commit_author_to_review = TRUE'
75 ).bindparams(val='forbid_commit_author')
76 op.execute(query)
77
78 session().commit()
@@ -1,5 +1,5 b''
1 [bumpversion]
1 [bumpversion]
2 current_version = 4.22.0
2 current_version = 4.23.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:rhodecode/VERSION]
5 [bumpversion:file:rhodecode/VERSION]
@@ -5,25 +5,20 b' done = false'
5 done = true
5 done = true
6
6
7 [task:rc_tools_pinned]
7 [task:rc_tools_pinned]
8 done = true
9
8
10 [task:fixes_on_stable]
9 [task:fixes_on_stable]
11 done = true
12
10
13 [task:pip2nix_generated]
11 [task:pip2nix_generated]
14 done = true
15
12
16 [task:changelog_updated]
13 [task:changelog_updated]
17 done = true
18
14
19 [task:generate_api_docs]
15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21
18
22 [release]
19 [release]
23 state = prepared
20 state = in_progress
24 version = 4.22.0
21 version = 4.23.0
25
26 [task:updated_translation]
27
22
28 [task:generate_js_routes]
23 [task:generate_js_routes]
29
24
@@ -9,6 +9,7 b' Release Notes'
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.23.0.rst
12 release-notes-4.22.0.rst
13 release-notes-4.22.0.rst
13 release-notes-4.21.0.rst
14 release-notes-4.21.0.rst
14 release-notes-4.20.1.rst
15 release-notes-4.20.1.rst
@@ -1883,7 +1883,7 b' self: super: {'
1883 };
1883 };
1884 };
1884 };
1885 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1885 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1886 name = "rhodecode-enterprise-ce-4.22.0";
1886 name = "rhodecode-enterprise-ce-4.23.0";
1887 buildInputs = [
1887 buildInputs = [
1888 self."pytest"
1888 self."pytest"
1889 self."py"
1889 self."py"
@@ -1,1 +1,1 b''
1 4.22.0 No newline at end of file
1 4.23.0 No newline at end of file
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 110 # defines current db version for migrations
51 __dbversion__ = 112 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
@@ -351,7 +351,10 b' def get_pull_request_or_error(pullreques'
351 return pull_request
351 return pull_request
352
352
353
353
354 def build_commit_data(commit, detail_level):
354 def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
355 commit2 = commit
356 commit1 = commit.first_parent
357
355 parsed_diff = []
358 parsed_diff = []
356 if detail_level == 'extended':
359 if detail_level == 'extended':
357 for f_path in commit.added_paths:
360 for f_path in commit.added_paths:
@@ -362,8 +365,11 b' def build_commit_data(commit, detail_lev'
362 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
365 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
363
366
364 elif detail_level == 'full':
367 elif detail_level == 'full':
365 from rhodecode.lib.diffs import DiffProcessor
368 from rhodecode.lib import diffs
366 diff_processor = DiffProcessor(commit.diff())
369
370 _diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
371 diff_processor = diffs.DiffProcessor(_diff, format='newdiff', show_full_diff=True)
372
367 for dp in diff_processor.prepare():
373 for dp in diff_processor.prepare():
368 del dp['stats']['ops']
374 del dp['stats']['ops']
369 _stats = dp['stats']
375 _stats = dp['stats']
@@ -317,17 +317,18 b' def get_repo_changeset(request, apiuser,'
317 'ret_type must be one of %s' % (
317 'ret_type must be one of %s' % (
318 ','.join(_changes_details_types)))
318 ','.join(_changes_details_types)))
319
319
320 vcs_repo = repo.scm_instance()
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 'status', '_commit', '_file_paths']
322 'status', '_commit', '_file_paths']
322
323
323 try:
324 try:
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 except TypeError as e:
326 except TypeError as e:
326 raise JSONRPCError(safe_str(e))
327 raise JSONRPCError(safe_str(e))
327 _cs_json = cs.__json__()
328 _cs_json = commit.__json__()
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
329 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
329 if changes_details == 'full':
330 if changes_details == 'full':
330 _cs_json['refs'] = cs._get_refs()
331 _cs_json['refs'] = commit._get_refs()
331 return _cs_json
332 return _cs_json
332
333
333
334
@@ -398,7 +399,7 b' def get_repo_changesets(request, apiuser'
398 if cnt >= limit != -1:
399 if cnt >= limit != -1:
399 break
400 break
400 _cs_json = commit.__json__()
401 _cs_json = commit.__json__()
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
402 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
402 if changes_details == 'full':
403 if changes_details == 'full':
403 _cs_json['refs'] = {
404 _cs_json['refs'] = {
404 'branches': [commit.branch],
405 'branches': [commit.branch],
@@ -36,7 +36,7 b' from rhodecode.authentication.plugins im'
36 from rhodecode.events import trigger
36 from rhodecode.events import trigger
37 from rhodecode.model.db import true, UserNotice
37 from rhodecode.model.db import true, UserNotice
38
38
39 from rhodecode.lib import audit_logger, rc_cache
39 from rhodecode.lib import audit_logger, rc_cache, auth
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
@@ -295,6 +295,10 b' class UsersView(UserAppView):'
295 c.allowed_extern_types = [
295 c.allowed_extern_types = [
296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
297 ]
297 ]
298 perms = req.registry.settings.get('available_permissions')
299 if not perms:
300 # inject info about available permissions
301 auth.set_available_permissions(req.registry.settings)
298
302
299 c.available_permissions = req.registry.settings['available_permissions']
303 c.available_permissions = req.registry.settings['available_permissions']
300 PermissionModel().set_global_permission_choices(
304 PermissionModel().set_global_permission_choices(
@@ -252,7 +252,7 b' But please check this code'
252 var comment = $('#comment-'+commentId);
252 var comment = $('#comment-'+commentId);
253 var commentData = comment.data();
253 var commentData = comment.data();
254 if (commentData.commentInline) {
254 if (commentData.commentInline) {
255 this.createComment(comment, commentId)
255 this.createComment(comment, f_path, line_no, commentId)
256 } else {
256 } else {
257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
258 }
258 }
@@ -702,7 +702,9 b' class MyAccountView(BaseAppView, DataGri'
702 **valid_data)
702 **valid_data)
703 if old_email != valid_data['email']:
703 if old_email != valid_data['email']:
704 old = UserEmailMap.query() \
704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
705 .filter(UserEmailMap.user == c.user)\
706 .filter(UserEmailMap.email == valid_data['email'])\
707 .first()
706 old.email = old_email
708 old.email = old_email
707 h.flash(_('Your account was updated successfully'), category='success')
709 h.flash(_('Your account was updated successfully'), category='success')
708 Session().commit()
710 Session().commit()
@@ -718,6 +720,7 b' class MyAccountView(BaseAppView, DataGri'
718 def _get_pull_requests_list(self, statuses):
720 def _get_pull_requests_list(self, statuses):
719 draw, start, limit = self._extract_chunk(self.request)
721 draw, start, limit = self._extract_chunk(self.request)
720 search_q, order_by, order_dir = self._extract_ordering(self.request)
722 search_q, order_by, order_dir = self._extract_ordering(self.request)
723
721 _render = self.request.get_partial_renderer(
724 _render = self.request.get_partial_renderer(
722 'rhodecode:templates/data_table/_dt_elements.mako')
725 'rhodecode:templates/data_table/_dt_elements.mako')
723
726
@@ -735,7 +738,7 b' class MyAccountView(BaseAppView, DataGri'
735 for pr in pull_requests:
738 for pr in pull_requests:
736 repo_id = pr.target_repo_id
739 repo_id = pr.target_repo_id
737 comments_count = comments_model.get_all_comments(
740 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, count_only=True)
741 repo_id, pull_request=pr, include_drafts=False, count_only=True)
739 owned = pr.user_id == self._rhodecode_user.user_id
742 owned = pr.user_id == self._rhodecode_user.user_id
740
743
741 data.append({
744 data.append({
@@ -751,7 +754,8 b' class MyAccountView(BaseAppView, DataGri'
751 'title': _render('pullrequest_title', pr.title, pr.description),
754 'title': _render('pullrequest_title', pr.title, pr.description),
752 'description': h.escape(pr.description),
755 'description': h.escape(pr.description),
753 'updated_on': _render('pullrequest_updated_on',
756 'updated_on': _render('pullrequest_updated_on',
754 h.datetime_to_time(pr.updated_on)),
757 h.datetime_to_time(pr.updated_on),
758 pr.versions_count),
755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
759 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 'created_on': _render('pullrequest_updated_on',
760 'created_on': _render('pullrequest_updated_on',
757 h.datetime_to_time(pr.created_on)),
761 h.datetime_to_time(pr.created_on)),
@@ -355,6 +355,11 b' def includeme(config):'
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 repo_route=True)
356 repo_route=True)
357
357
358 config.add_route(
359 name='pullrequest_drafts',
360 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
361 repo_route=True)
362
358 # Artifacts, (EE feature)
363 # Artifacts, (EE feature)
359 config.add_route(
364 config.add_route(
360 name='repo_artifacts_list',
365 name='repo_artifacts_list',
@@ -608,23 +608,23 b' class TestPullrequestsView(object):'
608 pull_request.source_repo, pull_request=pull_request)
608 pull_request.source_repo, pull_request=pull_request)
609 assert status == ChangesetStatus.STATUS_REJECTED
609 assert status == ChangesetStatus.STATUS_REJECTED
610
610
611 comment_id = response.json.get('comment_id', None)
611 for comment_id in response.json.keys():
612 test_text = 'test'
612 test_text = 'test'
613 response = self.app.post(
613 response = self.app.post(
614 route_path(
614 route_path(
615 'pullrequest_comment_edit',
615 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
616 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
617 pull_request_id=pull_request_id,
618 comment_id=comment_id,
618 comment_id=comment_id,
619 ),
619 ),
620 extra_environ=xhr_header,
620 extra_environ=xhr_header,
621 params={
621 params={
622 'csrf_token': csrf_token,
622 'csrf_token': csrf_token,
623 'text': test_text,
623 'text': test_text,
624 },
624 },
625 status=403,
625 status=403,
626 )
626 )
627 assert response.status_int == 403
627 assert response.status_int == 403
628
628
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 pull_request = pr_util.create_pull_request()
630 pull_request = pr_util.create_pull_request()
@@ -644,27 +644,27 b' class TestPullrequestsView(object):'
644 )
644 )
645 assert response.json
645 assert response.json
646
646
647 comment_id = response.json.get('comment_id', None)
647 for comment_id in response.json.keys():
648 assert comment_id
648 assert comment_id
649 test_text = 'test'
649 test_text = 'test'
650 self.app.post(
650 self.app.post(
651 route_path(
651 route_path(
652 'pullrequest_comment_edit',
652 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
653 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
654 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
655 comment_id=comment_id,
656 ),
656 ),
657 extra_environ=xhr_header,
657 extra_environ=xhr_header,
658 params={
658 params={
659 'csrf_token': csrf_token,
659 'csrf_token': csrf_token,
660 'text': test_text,
660 'text': test_text,
661 'version': '0',
661 'version': '0',
662 },
662 },
663
663
664 )
664 )
665 text_form_db = ChangesetComment.query().filter(
665 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
666 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
667 assert test_text == text_form_db
668
668
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 pull_request = pr_util.create_pull_request()
670 pull_request = pr_util.create_pull_request()
@@ -684,26 +684,25 b' class TestPullrequestsView(object):'
684 )
684 )
685 assert response.json
685 assert response.json
686
686
687 comment_id = response.json.get('comment_id', None)
687 for comment_id in response.json.keys():
688 assert comment_id
688 test_text = 'init'
689 test_text = 'init'
689 response = self.app.post(
690 response = self.app.post(
690 route_path(
691 route_path(
691 'pullrequest_comment_edit',
692 'pullrequest_comment_edit',
692 repo_name=target_scm_name,
693 repo_name=target_scm_name,
693 pull_request_id=pull_request.pull_request_id,
694 pull_request_id=pull_request.pull_request_id,
694 comment_id=comment_id,
695 comment_id=comment_id,
695 ),
696 ),
696 extra_environ=xhr_header,
697 extra_environ=xhr_header,
697 params={
698 params={
698 'csrf_token': csrf_token,
699 'csrf_token': csrf_token,
699 'text': test_text,
700 'text': test_text,
700 'version': '0',
701 'version': '0',
701 },
702 },
702 status=404,
703 status=404,
704
703
705 )
704 )
706 assert response.status_int == 404
705 assert response.status_int == 404
707
706
708 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
707 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
709 pull_request = pr_util.create_pull_request()
708 pull_request = pr_util.create_pull_request()
@@ -722,48 +721,46 b' class TestPullrequestsView(object):'
722 extra_environ=xhr_header,
721 extra_environ=xhr_header,
723 )
722 )
724 assert response.json
723 assert response.json
725 comment_id = response.json.get('comment_id', None)
724 for comment_id in response.json.keys():
726 assert comment_id
725 test_text = 'test'
727
726 self.app.post(
728 test_text = 'test'
727 route_path(
729 self.app.post(
728 'pullrequest_comment_edit',
730 route_path(
729 repo_name=target_scm_name,
731 'pullrequest_comment_edit',
730 pull_request_id=pull_request.pull_request_id,
732 repo_name=target_scm_name,
731 comment_id=comment_id,
733 pull_request_id=pull_request.pull_request_id,
732 ),
734 comment_id=comment_id,
733 extra_environ=xhr_header,
735 ),
734 params={
736 extra_environ=xhr_header,
735 'csrf_token': csrf_token,
737 params={
736 'text': test_text,
738 'csrf_token': csrf_token,
737 'version': '0',
739 'text': test_text,
738 },
740 'version': '0',
741 },
742
739
743 )
740 )
744 test_text_v2 = 'test_v2'
741 test_text_v2 = 'test_v2'
745 response = self.app.post(
742 response = self.app.post(
746 route_path(
743 route_path(
747 'pullrequest_comment_edit',
744 'pullrequest_comment_edit',
748 repo_name=target_scm_name,
745 repo_name=target_scm_name,
749 pull_request_id=pull_request.pull_request_id,
746 pull_request_id=pull_request.pull_request_id,
750 comment_id=comment_id,
747 comment_id=comment_id,
751 ),
748 ),
752 extra_environ=xhr_header,
749 extra_environ=xhr_header,
753 params={
750 params={
754 'csrf_token': csrf_token,
751 'csrf_token': csrf_token,
755 'text': test_text_v2,
752 'text': test_text_v2,
756 'version': '0',
753 'version': '0',
757 },
754 },
758 status=409,
755 status=409,
759 )
756 )
760 assert response.status_int == 409
757 assert response.status_int == 409
761
758
762 text_form_db = ChangesetComment.query().filter(
759 text_form_db = ChangesetComment.query().filter(
763 ChangesetComment.comment_id == comment_id).first().text
760 ChangesetComment.comment_id == comment_id).first().text
764
761
765 assert test_text == text_form_db
762 assert test_text == text_form_db
766 assert test_text_v2 != text_form_db
763 assert test_text_v2 != text_form_db
767
764
768 def test_comment_and_comment_edit_permissions_forbidden(
765 def test_comment_and_comment_edit_permissions_forbidden(
769 self, autologin_regular_user, user_regular, user_admin, pr_util,
766 self, autologin_regular_user, user_regular, user_admin, pr_util,
@@ -24,7 +24,8 b' from rhodecode.model.pull_request import'
24 from rhodecode.model.db import PullRequestReviewers
24 from rhodecode.model.db import PullRequestReviewers
25 # V3 - Reviewers, with default rules data
25 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
27 # v5 - pr_author/commit_author include/exclude logic
28 REVIEWER_API_VERSION = 'V5'
28
29
29
30
30 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
31 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
@@ -88,6 +89,7 b' def get_default_reviewers_data(current_u'
88 'reviewers': json_reviewers,
89 'reviewers': json_reviewers,
89 'rules': {},
90 'rules': {},
90 'rules_data': {},
91 'rules_data': {},
92 'rules_humanized': [],
91 }
93 }
92
94
93
95
@@ -77,6 +77,7 b' class RepoCommitsView(RepoAppView):'
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
80
81
81 # fetch global flags of ignore ws or context lines
82 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
83 diff_context = get_diff_context(self.request)
@@ -117,6 +118,19 b' class RepoCommitsView(RepoAppView):'
117 raise HTTPNotFound()
118 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
119 single_commit = len(c.commit_ranges) == 1
119
120
121 if redirect_to_combined and not single_commit:
122 source_ref = getattr(c.commit_ranges[0].parents[0]
123 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
124 target_ref = c.commit_ranges[-1].raw_id
125 next_url = h.route_path(
126 'repo_compare',
127 repo_name=c.repo_name,
128 source_ref_type='rev',
129 source_ref=source_ref,
130 target_ref_type='rev',
131 target_ref=target_ref)
132 raise HTTPFound(next_url)
133
120 c.changes = OrderedDict()
134 c.changes = OrderedDict()
121 c.lines_added = 0
135 c.lines_added = 0
122 c.lines_deleted = 0
136 c.lines_deleted = 0
@@ -366,6 +380,121 b' class RepoCommitsView(RepoAppView):'
366 commit_id = self.request.matchdict['commit_id']
380 commit_id = self.request.matchdict['commit_id']
367 return self._commit(commit_id, method='download')
381 return self._commit(commit_id, method='download')
368
382
383 def _commit_comments_create(self, commit_id, comments):
384 _ = self.request.translate
385 data = {}
386 if not comments:
387 return
388
389 commit = self.db_repo.get_commit(commit_id)
390
391 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
392 for entry in comments:
393 c = self.load_default_context()
394 comment_type = entry['comment_type']
395 text = entry['text']
396 status = entry['status']
397 is_draft = str2bool(entry['is_draft'])
398 resolves_comment_id = entry['resolves_comment_id']
399 f_path = entry['f_path']
400 line_no = entry['line']
401 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
402
403 if status:
404 text = text or (_('Status change %(transition_icon)s %(status)s')
405 % {'transition_icon': '>',
406 'status': ChangesetStatus.get_status_lbl(status)})
407
408 comment = CommentsModel().create(
409 text=text,
410 repo=self.db_repo.repo_id,
411 user=self._rhodecode_db_user.user_id,
412 commit_id=commit_id,
413 f_path=f_path,
414 line_no=line_no,
415 status_change=(ChangesetStatus.get_status_lbl(status)
416 if status else None),
417 status_change_type=status,
418 comment_type=comment_type,
419 is_draft=is_draft,
420 resolves_comment_id=resolves_comment_id,
421 auth_user=self._rhodecode_user,
422 send_email=not is_draft, # skip notification for draft comments
423 )
424 is_inline = comment.is_inline
425
426 # get status if set !
427 if status:
428 # `dont_allow_on_closed_pull_request = True` means
429 # if latest status was from pull request and it's closed
430 # disallow changing status !
431
432 try:
433 ChangesetStatusModel().set_status(
434 self.db_repo.repo_id,
435 status,
436 self._rhodecode_db_user.user_id,
437 comment,
438 revision=commit_id,
439 dont_allow_on_closed_pull_request=True
440 )
441 except StatusChangeOnClosedPullRequestError:
442 msg = _('Changing the status of a commit associated with '
443 'a closed pull request is not allowed')
444 log.exception(msg)
445 h.flash(msg, category='warning')
446 raise HTTPFound(h.route_path(
447 'repo_commit', repo_name=self.db_repo_name,
448 commit_id=commit_id))
449
450 Session().flush()
451 # this is somehow required to get access to some relationship
452 # loaded on comment
453 Session().refresh(comment)
454
455 # skip notifications for drafts
456 if not is_draft:
457 CommentsModel().trigger_commit_comment_hook(
458 self.db_repo, self._rhodecode_user, 'create',
459 data={'comment': comment, 'commit': commit})
460
461 comment_id = comment.comment_id
462 data[comment_id] = {
463 'target_id': target_elem_id
464 }
465 Session().flush()
466
467 c.co = comment
468 c.at_version_num = 0
469 c.is_new = True
470 rendered_comment = render(
471 'rhodecode:templates/changeset/changeset_comment_block.mako',
472 self._get_template_context(c), self.request)
473
474 data[comment_id].update(comment.get_dict())
475 data[comment_id].update({'rendered_text': rendered_comment})
476
477 # finalize, commit and redirect
478 Session().commit()
479
480 # skip channelstream for draft comments
481 if not all_drafts:
482 comment_broadcast_channel = channelstream.comment_channel(
483 self.db_repo_name, commit_obj=commit)
484
485 comment_data = data
486 posted_comment_type = 'inline' if is_inline else 'general'
487 if len(data) == 1:
488 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
489 else:
490 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
491
492 channelstream.comment_channelstream_push(
493 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
494 comment_data=comment_data)
495
496 return data
497
369 @LoginRequired()
498 @LoginRequired()
370 @NotAnonymous()
499 @NotAnonymous()
371 @HasRepoPermissionAnyDecorator(
500 @HasRepoPermissionAnyDecorator(
@@ -378,17 +507,6 b' class RepoCommitsView(RepoAppView):'
378 _ = self.request.translate
507 _ = self.request.translate
379 commit_id = self.request.matchdict['commit_id']
508 commit_id = self.request.matchdict['commit_id']
380
509
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
383 text = self.request.POST.get('text')
384 comment_type = self.request.POST.get('comment_type')
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
391
392 multi_commit_ids = []
510 multi_commit_ids = []
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
511 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
512 if _commit_id not in ['', None, EmptyCommit.raw_id]:
@@ -397,81 +515,23 b' class RepoCommitsView(RepoAppView):'
397
515
398 commit_ids = multi_commit_ids or [commit_id]
516 commit_ids = multi_commit_ids or [commit_id]
399
517
400 comment = None
518 data = []
519 # Multiple comments for each passed commit id
401 for current_id in filter(None, commit_ids):
520 for current_id in filter(None, commit_ids):
402 comment = CommentsModel().create(
521 comment_data = {
403 text=text,
522 'comment_type': self.request.POST.get('comment_type'),
404 repo=self.db_repo.repo_id,
523 'text': self.request.POST.get('text'),
405 user=self._rhodecode_db_user.user_id,
524 'status': self.request.POST.get('changeset_status', None),
406 commit_id=current_id,
525 'is_draft': self.request.POST.get('draft'),
407 f_path=self.request.POST.get('f_path'),
526 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
408 line_no=self.request.POST.get('line'),
527 'close_pull_request': self.request.POST.get('close_pull_request'),
409 status_change=(ChangesetStatus.get_status_lbl(status)
528 'f_path': self.request.POST.get('f_path'),
410 if status else None),
529 'line': self.request.POST.get('line'),
411 status_change_type=status,
530 }
412 comment_type=comment_type,
531 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
413 resolves_comment_id=resolves_comment_id,
532 data.append(comment)
414 auth_user=self._rhodecode_user
415 )
416 is_inline = comment.is_inline
417
418 # get status if set !
419 if status:
420 # if latest status was from pull request and it's closed
421 # disallow changing status !
422 # dont_allow_on_closed_pull_request = True !
423
533
424 try:
534 return data if len(data) > 1 else data[0]
425 ChangesetStatusModel().set_status(
426 self.db_repo.repo_id,
427 status,
428 self._rhodecode_db_user.user_id,
429 comment,
430 revision=current_id,
431 dont_allow_on_closed_pull_request=True
432 )
433 except StatusChangeOnClosedPullRequestError:
434 msg = _('Changing the status of a commit associated with '
435 'a closed pull request is not allowed')
436 log.exception(msg)
437 h.flash(msg, category='warning')
438 raise HTTPFound(h.route_path(
439 'repo_commit', repo_name=self.db_repo_name,
440 commit_id=current_id))
441
442 commit = self.db_repo.get_commit(current_id)
443 CommentsModel().trigger_commit_comment_hook(
444 self.db_repo, self._rhodecode_user, 'create',
445 data={'comment': comment, 'commit': commit})
446
447 # finalize, commit and redirect
448 Session().commit()
449
450 data = {
451 'target_id': h.safeid(h.safe_unicode(
452 self.request.POST.get('f_path'))),
453 }
454 if comment:
455 c.co = comment
456 c.at_version_num = 0
457 rendered_comment = render(
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 self._get_template_context(c), self.request)
460
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
463
464 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
466
467 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
473
474 return data
475
535
476 @LoginRequired()
536 @LoginRequired()
477 @NotAnonymous()
537 @NotAnonymous()
@@ -665,6 +725,7 b' class RepoCommitsView(RepoAppView):'
665 def repo_commit_comment_edit(self):
725 def repo_commit_comment_edit(self):
666 self.load_default_context()
726 self.load_default_context()
667
727
728 commit_id = self.request.matchdict['commit_id']
668 comment_id = self.request.matchdict['comment_id']
729 comment_id = self.request.matchdict['comment_id']
669 comment = ChangesetComment.get_or_404(comment_id)
730 comment = ChangesetComment.get_or_404(comment_id)
670
731
@@ -717,11 +778,11 b' class RepoCommitsView(RepoAppView):'
717 if not comment_history:
778 if not comment_history:
718 raise HTTPNotFound()
779 raise HTTPNotFound()
719
780
720 commit_id = self.request.matchdict['commit_id']
781 if not comment.draft:
721 commit = self.db_repo.get_commit(commit_id)
782 commit = self.db_repo.get_commit(commit_id)
722 CommentsModel().trigger_commit_comment_hook(
783 CommentsModel().trigger_commit_comment_hook(
723 self.db_repo, self._rhodecode_user, 'edit',
784 self.db_repo, self._rhodecode_user, 'edit',
724 data={'comment': comment, 'commit': commit})
785 data={'comment': comment, 'commit': commit})
725
786
726 Session().commit()
787 Session().commit()
727 return {
788 return {
@@ -325,6 +325,21 b' class RepoFilesView(RepoAppView):'
325
325
326 return lf_enabled
326 return lf_enabled
327
327
328 def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha=''):
329 # original backward compat name of archive
330 clean_name = safe_str(db_repo_name.replace('/', '_'))
331
332 # e.g vcsserver.zip
333 # e.g vcsserver-abcdefgh.zip
334 # e.g vcsserver-abcdefgh-defghijk.zip
335 archive_name = '{}{}{}{}{}'.format(
336 clean_name,
337 '-sub' if subrepos else '',
338 commit_sha,
339 '-{}'.format(path_sha) if path_sha else '',
340 ext)
341 return archive_name
342
328 @LoginRequired()
343 @LoginRequired()
329 @HasRepoPermissionAnyDecorator(
344 @HasRepoPermissionAnyDecorator(
330 'repository.read', 'repository.write', 'repository.admin')
345 'repository.read', 'repository.write', 'repository.admin')
@@ -339,6 +354,7 b' class RepoFilesView(RepoAppView):'
339 default_at_path = '/'
354 default_at_path = '/'
340 fname = self.request.matchdict['fname']
355 fname = self.request.matchdict['fname']
341 subrepos = self.request.GET.get('subrepos') == 'true'
356 subrepos = self.request.GET.get('subrepos') == 'true'
357 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
342 at_path = self.request.GET.get('at_path') or default_at_path
358 at_path = self.request.GET.get('at_path') or default_at_path
343
359
344 if not self.db_repo.enable_downloads:
360 if not self.db_repo.enable_downloads:
@@ -364,30 +380,30 b' class RepoFilesView(RepoAppView):'
364 except Exception:
380 except Exception:
365 return Response(_('No node at path {} for this repository').format(at_path))
381 return Response(_('No node at path {} for this repository').format(at_path))
366
382
367 path_sha = sha1(at_path)[:8]
383 # path sha is part of subdir
368
384 path_sha = ''
369 # original backward compat name of archive
385 if at_path != default_at_path:
370 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
386 path_sha = sha1(at_path)[:8]
371 short_sha = safe_str(commit.short_id)
387 short_sha = '-{}'.format(safe_str(commit.short_id))
388 # used for cache etc
389 archive_name = self._get_archive_name(
390 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
391 path_sha=path_sha)
372
392
373 if at_path == default_at_path:
393 if not with_hash:
374 archive_name = '{}-{}{}{}'.format(
394 short_sha = ''
375 clean_name,
395 path_sha = ''
376 '-sub' if subrepos else '',
396
377 short_sha,
397 # what end client gets served
378 ext)
398 response_archive_name = self._get_archive_name(
379 # custom path and new name
399 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
380 else:
400 path_sha=path_sha)
381 archive_name = '{}-{}{}-{}{}'.format(
401 # remove extension from our archive directory name
382 clean_name,
402 archive_dir_name = response_archive_name[:-len(ext)]
383 '-sub' if subrepos else '',
384 short_sha,
385 path_sha,
386 ext)
387
403
388 use_cached_archive = False
404 use_cached_archive = False
389 archive_cache_enabled = CONFIG.get(
405 archive_cache_dir = CONFIG.get('archive_cache_dir')
390 'archive_cache_dir') and not self.request.GET.get('no_cache')
406 archive_cache_enabled = archive_cache_dir and not self.request.GET.get('no_cache')
391 cached_archive_path = None
407 cached_archive_path = None
392
408
393 if archive_cache_enabled:
409 if archive_cache_enabled:
@@ -403,12 +419,14 b' class RepoFilesView(RepoAppView):'
403 else:
419 else:
404 log.debug('Archive %s is not yet cached', archive_name)
420 log.debug('Archive %s is not yet cached', archive_name)
405
421
422 # generate new archive, as previous was not found in the cache
406 if not use_cached_archive:
423 if not use_cached_archive:
407 # generate new archive
424 _dir = os.path.abspath(archive_cache_dir) if archive_cache_dir else None
408 fd, archive = tempfile.mkstemp()
425 fd, archive = tempfile.mkstemp(dir=_dir)
409 log.debug('Creating new temp archive in %s', archive)
426 log.debug('Creating new temp archive in %s', archive)
410 try:
427 try:
411 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
428 commit.archive_repo(archive, archive_dir_name=archive_dir_name,
429 kind=fileformat, subrepos=subrepos,
412 archive_at_path=at_path)
430 archive_at_path=at_path)
413 except ImproperArchiveTypeError:
431 except ImproperArchiveTypeError:
414 return _('Unknown archive type')
432 return _('Unknown archive type')
@@ -445,8 +463,7 b' class RepoFilesView(RepoAppView):'
445 yield data
463 yield data
446
464
447 response = Response(app_iter=get_chunked_archive(archive))
465 response = Response(app_iter=get_chunked_archive(archive))
448 response.content_disposition = str(
466 response.content_disposition = str('attachment; filename=%s' % response_archive_name)
449 'attachment; filename=%s' % archive_name)
450 response.content_type = str(content_type)
467 response.content_type = str(content_type)
451
468
452 return response
469 return response
@@ -47,7 +47,7 b' from rhodecode.lib.vcs.exceptions import'
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
@@ -107,7 +107,8 b' class RepoPullRequestsView(RepoAppView, '
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111
112
112 data.append({
113 data.append({
113 'name': _render('pullrequest_name',
114 'name': _render('pullrequest_name',
@@ -120,7 +121,8 b' class RepoPullRequestsView(RepoAppView, '
120 'title': _render('pullrequest_title', pr.title, pr.description),
121 'title': _render('pullrequest_title', pr.title, pr.description),
121 'description': h.escape(pr.description),
122 'description': h.escape(pr.description),
122 'updated_on': _render('pullrequest_updated_on',
123 'updated_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.updated_on)),
124 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 'created_on': _render('pullrequest_updated_on',
127 'created_on': _render('pullrequest_updated_on',
126 h.datetime_to_time(pr.created_on)),
128 h.datetime_to_time(pr.created_on)),
@@ -268,12 +270,14 b' class RepoPullRequestsView(RepoAppView, '
268
270
269 return diffset
271 return diffset
270
272
271 def register_comments_vars(self, c, pull_request, versions):
273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
274 comments_model = CommentsModel()
273
275
274 # GENERAL COMMENTS with versions #
276 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
277 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
278 q = q.order_by(ChangesetComment.comment_id.asc())
279 if not include_drafts:
280 q = q.filter(ChangesetComment.draft == false())
277 general_comments = q
281 general_comments = q
278
282
279 # pick comments we want to render at current version
283 # pick comments we want to render at current version
@@ -283,6 +287,8 b' class RepoPullRequestsView(RepoAppView, '
283 # INLINE COMMENTS with versions #
287 # INLINE COMMENTS with versions #
284 q = comments_model._all_inline_comments_of_pull_request(pull_request)
288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
285 q = q.order_by(ChangesetComment.comment_id.asc())
289 q = q.order_by(ChangesetComment.comment_id.asc())
290 if not include_drafts:
291 q = q.filter(ChangesetComment.draft == false())
286 inline_comments = q
292 inline_comments = q
287
293
288 c.inline_versions = comments_model.aggregate_comments(
294 c.inline_versions = comments_model.aggregate_comments(
@@ -422,16 +428,12 b' class RepoPullRequestsView(RepoAppView, '
422 c.allowed_to_close = c.allowed_to_merge and not pr_closed
428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
423
429
424 c.forbid_adding_reviewers = False
430 c.forbid_adding_reviewers = False
425 c.forbid_author_to_review = False
426 c.forbid_commit_author_to_review = False
427
431
428 if pull_request_latest.reviewer_data and \
432 if pull_request_latest.reviewer_data and \
429 'rules' in pull_request_latest.reviewer_data:
433 'rules' in pull_request_latest.reviewer_data:
430 rules = pull_request_latest.reviewer_data['rules'] or {}
434 rules = pull_request_latest.reviewer_data['rules'] or {}
431 try:
435 try:
432 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
433 c.forbid_author_to_review = rules.get('forbid_author_to_review')
434 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
435 except Exception:
437 except Exception:
436 pass
438 pass
437
439
@@ -499,6 +501,11 b' class RepoPullRequestsView(RepoAppView, '
499 c.resolved_comments = CommentsModel() \
501 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
502 .get_pull_request_resolved_todos(pull_request_latest)
501
503
504 # Drafts
505 c.draft_comments = CommentsModel().get_pull_request_drafts(
506 self._rhodecode_db_user.user_id,
507 pull_request_latest)
508
502 # if we use version, then do not show later comments
509 # if we use version, then do not show later comments
503 # than current version
510 # than current version
504 display_inline_comments = collections.defaultdict(
511 display_inline_comments = collections.defaultdict(
@@ -979,8 +986,9 b' class RepoPullRequestsView(RepoAppView, '
979 }
986 }
980 return data
987 return data
981
988
982 def _get_existing_ids(self, post_data):
989 @classmethod
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
990 def get_comment_ids(cls, post_data):
991 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
984
992
985 @LoginRequired()
993 @LoginRequired()
986 @NotAnonymous()
994 @NotAnonymous()
@@ -1015,10 +1023,10 b' class RepoPullRequestsView(RepoAppView, '
1015 if at_version and at_version != PullRequest.LATEST_VER
1023 if at_version and at_version != PullRequest.LATEST_VER
1016 else None)
1024 else None)
1017
1025
1018 self.register_comments_vars(c, pull_request_latest, versions)
1026 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 all_comments = c.inline_comments_flat + c.comments
1027 all_comments = c.inline_comments_flat + c.comments
1020
1028
1021 existing_ids = self._get_existing_ids(self.request.POST)
1029 existing_ids = self.get_comment_ids(self.request.POST)
1022 return _render('comments_table', all_comments, len(all_comments),
1030 return _render('comments_table', all_comments, len(all_comments),
1023 existing_ids=existing_ids)
1031 existing_ids=existing_ids)
1024
1032
@@ -1055,12 +1063,12 b' class RepoPullRequestsView(RepoAppView, '
1055 else None)
1063 else None)
1056
1064
1057 c.unresolved_comments = CommentsModel() \
1065 c.unresolved_comments = CommentsModel() \
1058 .get_pull_request_unresolved_todos(pull_request)
1066 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1059 c.resolved_comments = CommentsModel() \
1067 c.resolved_comments = CommentsModel() \
1060 .get_pull_request_resolved_todos(pull_request)
1068 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1061
1069
1062 all_comments = c.unresolved_comments + c.resolved_comments
1070 all_comments = c.unresolved_comments + c.resolved_comments
1063 existing_ids = self._get_existing_ids(self.request.POST)
1071 existing_ids = self.get_comment_ids(self.request.POST)
1064 return _render('comments_table', all_comments, len(c.unresolved_comments),
1072 return _render('comments_table', all_comments, len(c.unresolved_comments),
1065 todo_comments=True, existing_ids=existing_ids)
1073 todo_comments=True, existing_ids=existing_ids)
1066
1074
@@ -1068,6 +1076,48 b' class RepoPullRequestsView(RepoAppView, '
1068 @NotAnonymous()
1076 @NotAnonymous()
1069 @HasRepoPermissionAnyDecorator(
1077 @HasRepoPermissionAnyDecorator(
1070 'repository.read', 'repository.write', 'repository.admin')
1078 'repository.read', 'repository.write', 'repository.admin')
1079 @view_config(
1080 route_name='pullrequest_drafts', request_method='POST',
1081 renderer='string_html', xhr=True)
1082 def pullrequest_drafts(self):
1083 self.load_default_context()
1084
1085 pull_request = PullRequest.get_or_404(
1086 self.request.matchdict['pull_request_id'])
1087 pull_request_id = pull_request.pull_request_id
1088 version = self.request.GET.get('version')
1089
1090 _render = self.request.get_partial_renderer(
1091 'rhodecode:templates/base/sidebar.mako')
1092 c = _render.get_call_context()
1093
1094 (pull_request_latest,
1095 pull_request_at_ver,
1096 pull_request_display_obj,
1097 at_version) = PullRequestModel().get_pr_version(
1098 pull_request_id, version=version)
1099 versions = pull_request_display_obj.versions()
1100 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1101 c.versions = versions + [latest_ver]
1102
1103 c.at_version = at_version
1104 c.at_version_num = (at_version
1105 if at_version and at_version != PullRequest.LATEST_VER
1106 else None)
1107
1108 c.draft_comments = CommentsModel() \
1109 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1110
1111 all_comments = c.draft_comments
1112
1113 existing_ids = self.get_comment_ids(self.request.POST)
1114 return _render('comments_table', all_comments, len(all_comments),
1115 existing_ids=existing_ids, draft_comments=True)
1116
1117 @LoginRequired()
1118 @NotAnonymous()
1119 @HasRepoPermissionAnyDecorator(
1120 'repository.read', 'repository.write', 'repository.admin')
1071 @CSRFRequired()
1121 @CSRFRequired()
1072 @view_config(
1122 @view_config(
1073 route_name='pullrequest_create', request_method='POST',
1123 route_name='pullrequest_create', request_method='POST',
@@ -1514,6 +1564,152 b' class RepoPullRequestsView(RepoAppView, '
1514 self._rhodecode_user)
1564 self._rhodecode_user)
1515 raise HTTPNotFound()
1565 raise HTTPNotFound()
1516
1566
1567 def _pull_request_comments_create(self, pull_request, comments):
1568 _ = self.request.translate
1569 data = {}
1570 if not comments:
1571 return
1572 pull_request_id = pull_request.pull_request_id
1573
1574 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1575
1576 for entry in comments:
1577 c = self.load_default_context()
1578 comment_type = entry['comment_type']
1579 text = entry['text']
1580 status = entry['status']
1581 is_draft = str2bool(entry['is_draft'])
1582 resolves_comment_id = entry['resolves_comment_id']
1583 close_pull_request = entry['close_pull_request']
1584 f_path = entry['f_path']
1585 line_no = entry['line']
1586 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1587
1588 # the logic here should work like following, if we submit close
1589 # pr comment, use `close_pull_request_with_comment` function
1590 # else handle regular comment logic
1591
1592 if close_pull_request:
1593 # only owner or admin or person with write permissions
1594 allowed_to_close = PullRequestModel().check_user_update(
1595 pull_request, self._rhodecode_user)
1596 if not allowed_to_close:
1597 log.debug('comment: forbidden because not allowed to close '
1598 'pull request %s', pull_request_id)
1599 raise HTTPForbidden()
1600
1601 # This also triggers `review_status_change`
1602 comment, status = PullRequestModel().close_pull_request_with_comment(
1603 pull_request, self._rhodecode_user, self.db_repo, message=text,
1604 auth_user=self._rhodecode_user)
1605 Session().flush()
1606 is_inline = comment.is_inline
1607
1608 PullRequestModel().trigger_pull_request_hook(
1609 pull_request, self._rhodecode_user, 'comment',
1610 data={'comment': comment})
1611
1612 else:
1613 # regular comment case, could be inline, or one with status.
1614 # for that one we check also permissions
1615 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1616 allowed_to_change_status = PullRequestModel().check_user_change_status(
1617 pull_request, self._rhodecode_user) and not is_draft
1618
1619 if status and allowed_to_change_status:
1620 message = (_('Status change %(transition_icon)s %(status)s')
1621 % {'transition_icon': '>',
1622 'status': ChangesetStatus.get_status_lbl(status)})
1623 text = text or message
1624
1625 comment = CommentsModel().create(
1626 text=text,
1627 repo=self.db_repo.repo_id,
1628 user=self._rhodecode_user.user_id,
1629 pull_request=pull_request,
1630 f_path=f_path,
1631 line_no=line_no,
1632 status_change=(ChangesetStatus.get_status_lbl(status)
1633 if status and allowed_to_change_status else None),
1634 status_change_type=(status
1635 if status and allowed_to_change_status else None),
1636 comment_type=comment_type,
1637 is_draft=is_draft,
1638 resolves_comment_id=resolves_comment_id,
1639 auth_user=self._rhodecode_user,
1640 send_email=not is_draft, # skip notification for draft comments
1641 )
1642 is_inline = comment.is_inline
1643
1644 if allowed_to_change_status:
1645 # calculate old status before we change it
1646 old_calculated_status = pull_request.calculated_review_status()
1647
1648 # get status if set !
1649 if status:
1650 ChangesetStatusModel().set_status(
1651 self.db_repo.repo_id,
1652 status,
1653 self._rhodecode_user.user_id,
1654 comment,
1655 pull_request=pull_request
1656 )
1657
1658 Session().flush()
1659 # this is somehow required to get access to some relationship
1660 # loaded on comment
1661 Session().refresh(comment)
1662
1663 # skip notifications for drafts
1664 if not is_draft:
1665 PullRequestModel().trigger_pull_request_hook(
1666 pull_request, self._rhodecode_user, 'comment',
1667 data={'comment': comment})
1668
1669 # we now calculate the status of pull request, and based on that
1670 # calculation we set the commits status
1671 calculated_status = pull_request.calculated_review_status()
1672 if old_calculated_status != calculated_status:
1673 PullRequestModel().trigger_pull_request_hook(
1674 pull_request, self._rhodecode_user, 'review_status_change',
1675 data={'status': calculated_status})
1676
1677 comment_id = comment.comment_id
1678 data[comment_id] = {
1679 'target_id': target_elem_id
1680 }
1681 Session().flush()
1682
1683 c.co = comment
1684 c.at_version_num = None
1685 c.is_new = True
1686 rendered_comment = render(
1687 'rhodecode:templates/changeset/changeset_comment_block.mako',
1688 self._get_template_context(c), self.request)
1689
1690 data[comment_id].update(comment.get_dict())
1691 data[comment_id].update({'rendered_text': rendered_comment})
1692
1693 Session().commit()
1694
1695 # skip channelstream for draft comments
1696 if not all_drafts:
1697 comment_broadcast_channel = channelstream.comment_channel(
1698 self.db_repo_name, pull_request_obj=pull_request)
1699
1700 comment_data = data
1701 posted_comment_type = 'inline' if is_inline else 'general'
1702 if len(data) == 1:
1703 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1704 else:
1705 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1706
1707 channelstream.comment_channelstream_push(
1708 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1709 comment_data=comment_data)
1710
1711 return data
1712
1517 @LoginRequired()
1713 @LoginRequired()
1518 @NotAnonymous()
1714 @NotAnonymous()
1519 @HasRepoPermissionAnyDecorator(
1715 @HasRepoPermissionAnyDecorator(
@@ -1525,9 +1721,7 b' class RepoPullRequestsView(RepoAppView, '
1525 def pull_request_comment_create(self):
1721 def pull_request_comment_create(self):
1526 _ = self.request.translate
1722 _ = self.request.translate
1527
1723
1528 pull_request = PullRequest.get_or_404(
1724 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1529 self.request.matchdict['pull_request_id'])
1530 pull_request_id = pull_request.pull_request_id
1531
1725
1532 if pull_request.is_closed():
1726 if pull_request.is_closed():
1533 log.debug('comment: forbidden because pull request is closed')
1727 log.debug('comment: forbidden because pull request is closed')
@@ -1539,124 +1733,17 b' class RepoPullRequestsView(RepoAppView, '
1539 log.debug('comment: forbidden because pull request is from forbidden repo')
1733 log.debug('comment: forbidden because pull request is from forbidden repo')
1540 raise HTTPForbidden()
1734 raise HTTPForbidden()
1541
1735
1542 c = self.load_default_context()
1736 comment_data = {
1543
1737 'comment_type': self.request.POST.get('comment_type'),
1544 status = self.request.POST.get('changeset_status', None)
1738 'text': self.request.POST.get('text'),
1545 text = self.request.POST.get('text')
1739 'status': self.request.POST.get('changeset_status', None),
1546 comment_type = self.request.POST.get('comment_type')
1740 'is_draft': self.request.POST.get('draft'),
1547 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1741 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1548 close_pull_request = self.request.POST.get('close_pull_request')
1742 'close_pull_request': self.request.POST.get('close_pull_request'),
1549
1743 'f_path': self.request.POST.get('f_path'),
1550 # the logic here should work like following, if we submit close
1744 'line': self.request.POST.get('line'),
1551 # pr comment, use `close_pull_request_with_comment` function
1552 # else handle regular comment logic
1553
1554 if close_pull_request:
1555 # only owner or admin or person with write permissions
1556 allowed_to_close = PullRequestModel().check_user_update(
1557 pull_request, self._rhodecode_user)
1558 if not allowed_to_close:
1559 log.debug('comment: forbidden because not allowed to close '
1560 'pull request %s', pull_request_id)
1561 raise HTTPForbidden()
1562
1563 # This also triggers `review_status_change`
1564 comment, status = PullRequestModel().close_pull_request_with_comment(
1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1566 auth_user=self._rhodecode_user)
1567 Session().flush()
1568 is_inline = comment.is_inline
1569
1570 PullRequestModel().trigger_pull_request_hook(
1571 pull_request, self._rhodecode_user, 'comment',
1572 data={'comment': comment})
1573
1574 else:
1575 # regular comment case, could be inline, or one with status.
1576 # for that one we check also permissions
1577
1578 allowed_to_change_status = PullRequestModel().check_user_change_status(
1579 pull_request, self._rhodecode_user)
1580
1581 if status and allowed_to_change_status:
1582 message = (_('Status change %(transition_icon)s %(status)s')
1583 % {'transition_icon': '>',
1584 'status': ChangesetStatus.get_status_lbl(status)})
1585 text = text or message
1586
1587 comment = CommentsModel().create(
1588 text=text,
1589 repo=self.db_repo.repo_id,
1590 user=self._rhodecode_user.user_id,
1591 pull_request=pull_request,
1592 f_path=self.request.POST.get('f_path'),
1593 line_no=self.request.POST.get('line'),
1594 status_change=(ChangesetStatus.get_status_lbl(status)
1595 if status and allowed_to_change_status else None),
1596 status_change_type=(status
1597 if status and allowed_to_change_status else None),
1598 comment_type=comment_type,
1599 resolves_comment_id=resolves_comment_id,
1600 auth_user=self._rhodecode_user
1601 )
1602 is_inline = comment.is_inline
1603
1604 if allowed_to_change_status:
1605 # calculate old status before we change it
1606 old_calculated_status = pull_request.calculated_review_status()
1607
1608 # get status if set !
1609 if status:
1610 ChangesetStatusModel().set_status(
1611 self.db_repo.repo_id,
1612 status,
1613 self._rhodecode_user.user_id,
1614 comment,
1615 pull_request=pull_request
1616 )
1617
1618 Session().flush()
1619 # this is somehow required to get access to some relationship
1620 # loaded on comment
1621 Session().refresh(comment)
1622
1623 PullRequestModel().trigger_pull_request_hook(
1624 pull_request, self._rhodecode_user, 'comment',
1625 data={'comment': comment})
1626
1627 # we now calculate the status of pull request, and based on that
1628 # calculation we set the commits status
1629 calculated_status = pull_request.calculated_review_status()
1630 if old_calculated_status != calculated_status:
1631 PullRequestModel().trigger_pull_request_hook(
1632 pull_request, self._rhodecode_user, 'review_status_change',
1633 data={'status': calculated_status})
1634
1635 Session().commit()
1636
1637 data = {
1638 'target_id': h.safeid(h.safe_unicode(
1639 self.request.POST.get('f_path'))),
1640 }
1745 }
1641 if comment:
1746 data = self._pull_request_comments_create(pull_request, [comment_data])
1642 c.co = comment
1643 c.at_version_num = None
1644 rendered_comment = render(
1645 'rhodecode:templates/changeset/changeset_comment_block.mako',
1646 self._get_template_context(c), self.request)
1647
1648 data.update(comment.get_dict())
1649 data.update({'rendered_text': rendered_comment})
1650
1651 comment_broadcast_channel = channelstream.comment_channel(
1652 self.db_repo_name, pull_request_obj=pull_request)
1653
1654 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1656 channelstream.comment_channelstream_push(
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1658 _('posted a new {} comment').format(comment_type),
1659 comment_data=comment_data)
1660
1747
1661 return data
1748 return data
1662
1749
@@ -1741,11 +1828,6 b' class RepoPullRequestsView(RepoAppView, '
1741 log.debug('comment: forbidden because pull request is closed')
1828 log.debug('comment: forbidden because pull request is closed')
1742 raise HTTPForbidden()
1829 raise HTTPForbidden()
1743
1830
1744 if not comment:
1745 log.debug('Comment with id:%s not found, skipping', comment_id)
1746 # comment already deleted in another call probably
1747 return True
1748
1749 if comment.pull_request.is_closed():
1831 if comment.pull_request.is_closed():
1750 # don't allow deleting comments on closed pull request
1832 # don't allow deleting comments on closed pull request
1751 raise HTTPForbidden()
1833 raise HTTPForbidden()
@@ -1796,10 +1878,10 b' class RepoPullRequestsView(RepoAppView, '
1796 raise HTTPNotFound()
1878 raise HTTPNotFound()
1797
1879
1798 Session().commit()
1880 Session().commit()
1799
1881 if not comment.draft:
1800 PullRequestModel().trigger_pull_request_hook(
1882 PullRequestModel().trigger_pull_request_hook(
1801 pull_request, self._rhodecode_user, 'comment_edit',
1883 pull_request, self._rhodecode_user, 'comment_edit',
1802 data={'comment': comment})
1884 data={'comment': comment})
1803
1885
1804 return {
1886 return {
1805 'comment_history_id': comment_history.comment_history_id,
1887 'comment_history_id': comment_history.comment_history_id,
@@ -215,9 +215,10 b' class RhodeCodeAuthPluginBase(object):'
215 """
215 """
216 return self._plugin_id
216 return self._plugin_id
217
217
218 def get_display_name(self):
218 def get_display_name(self, load_from_settings=False):
219 """
219 """
220 Returns a translation string for displaying purposes.
220 Returns a translation string for displaying purposes.
221 if load_from_settings is set, plugin settings can override the display name
221 """
222 """
222 raise NotImplementedError('Not implemented in base class')
223 raise NotImplementedError('Not implemented in base class')
223
224
@@ -213,7 +213,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
213 def get_settings_schema(self):
213 def get_settings_schema(self):
214 return CrowdSettingsSchema()
214 return CrowdSettingsSchema()
215
215
216 def get_display_name(self):
216 def get_display_name(self, load_from_settings=False):
217 return _('CROWD')
217 return _('CROWD')
218
218
219 @classmethod
219 @classmethod
@@ -95,7 +95,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
95 route_name='auth_home',
95 route_name='auth_home',
96 context=HeadersAuthnResource)
96 context=HeadersAuthnResource)
97
97
98 def get_display_name(self):
98 def get_display_name(self, load_from_settings=False):
99 return _('Headers')
99 return _('Headers')
100
100
101 def get_settings_schema(self):
101 def get_settings_schema(self):
@@ -89,7 +89,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
89 def get_settings_schema(self):
89 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
90 return JasigCasSettingsSchema()
91
91
92 def get_display_name(self):
92 def get_display_name(self, load_from_settings=False):
93 return _('Jasig-CAS')
93 return _('Jasig-CAS')
94
94
95 @hybrid_property
95 @hybrid_property
@@ -421,7 +421,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
421 def get_settings_schema(self):
421 def get_settings_schema(self):
422 return LdapSettingsSchema()
422 return LdapSettingsSchema()
423
423
424 def get_display_name(self):
424 def get_display_name(self, load_from_settings=False):
425 return _('LDAP')
425 return _('LDAP')
426
426
427 @classmethod
427 @classmethod
@@ -95,7 +95,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
95 route_name='auth_home',
95 route_name='auth_home',
96 context=PamAuthnResource)
96 context=PamAuthnResource)
97
97
98 def get_display_name(self):
98 def get_display_name(self, load_from_settings=False):
99 return _('PAM')
99 return _('PAM')
100
100
101 @classmethod
101 @classmethod
@@ -75,7 +75,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
75 def get_settings_schema(self):
75 def get_settings_schema(self):
76 return RhodeCodeSettingsSchema()
76 return RhodeCodeSettingsSchema()
77
77
78 def get_display_name(self):
78 def get_display_name(self, load_from_settings=False):
79 return _('RhodeCode Internal')
79 return _('RhodeCode Internal')
80
80
81 @classmethod
81 @classmethod
@@ -73,7 +73,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
73 def get_settings_schema(self):
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
74 return RhodeCodeSettingsSchema()
75
75
76 def get_display_name(self):
76 def get_display_name(self, load_from_settings=False):
77 return _('Rhodecode Token')
77 return _('Rhodecode Token')
78
78
79 @classmethod
79 @classmethod
@@ -53,7 +53,7 b' from rhodecode.lib.utils2 import aslist '
53 from rhodecode.lib.exc_tracking import store_exception
53 from rhodecode.lib.exc_tracking import store_exception
54 from rhodecode.subscribers import (
54 from rhodecode.subscribers import (
55 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
56 write_metadata_if_needed, write_usage_data)
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
@@ -310,8 +310,6 b' def includeme(config):'
310
310
311 # Add subscribers.
311 # Add subscribers.
312 if load_all:
312 if load_all:
313 config.add_subscriber(inject_app_settings,
314 pyramid.events.ApplicationCreated)
315 config.add_subscriber(scan_repositories_if_enabled,
313 config.add_subscriber(scan_repositories_if_enabled,
316 pyramid.events.ApplicationCreated)
314 pyramid.events.ApplicationCreated)
317 config.add_subscriber(write_metadata_if_needed,
315 config.add_subscriber(write_metadata_if_needed,
@@ -67,7 +67,7 b' markdown_tags = ['
67
67
68 markdown_attrs = {
68 markdown_attrs = {
69 "*": ["class", "style", "align"],
69 "*": ["class", "style", "align"],
70 "img": ["src", "alt", "title"],
70 "img": ["src", "alt", "title", "width", "height", "hspace", "align"],
71 "a": ["href", "alt", "title", "name", "data-hovercard-alt", "data-hovercard-url"],
71 "a": ["href", "alt", "title", "name", "data-hovercard-alt", "data-hovercard-url"],
72 "abbr": ["title"],
72 "abbr": ["title"],
73 "acronym": ["title"],
73 "acronym": ["title"],
@@ -339,13 +339,12 b' def comment_channelstream_push(request, '
339
339
340 comment_data = kwargs.pop('comment_data', {})
340 comment_data = kwargs.pop('comment_data', {})
341 user_data = kwargs.pop('user_data', {})
341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
342 comment_id = comment_data.keys()[0] if comment_data else ''
343
343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
344 message = '<strong>{}</strong> {} #{}'.format(
345 user.username,
345 user.username,
346 msg,
346 msg,
347 comment_id,
347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 )
348 )
350
349
351 message_obj = {
350 message_obj = {
@@ -1148,7 +1148,7 b' class DiffLimitExceeded(Exception):'
1148
1148
1149 # NOTE(marcink): if diffs.mako change, probably this
1149 # NOTE(marcink): if diffs.mako change, probably this
1150 # needs a bump to next version
1150 # needs a bump to next version
1151 CURRENT_DIFF_VERSION = 'v4'
1151 CURRENT_DIFF_VERSION = 'v5'
1152
1152
1153
1153
1154 def _cleanup_cache_file(cached_diff_file):
1154 def _cleanup_cache_file(cached_diff_file):
@@ -110,7 +110,7 b' def _store_exception(exc_id, exc_type_na'
110
110
111 mail_server = app.CONFIG.get('smtp_server') or None
111 mail_server = app.CONFIG.get('smtp_server') or None
112 send_email = send_email and mail_server
112 send_email = send_email and mail_server
113 if send_email:
113 if send_email and request:
114 try:
114 try:
115 send_exc_email(request, exc_id, exc_type_name)
115 send_exc_email(request, exc_id, exc_type_name)
116 except Exception:
116 except Exception:
@@ -18,17 +18,85 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import markdown
22 import markdown
23 import xml.etree.ElementTree as etree
22
24
23 from markdown.extensions import Extension
25 from markdown.extensions import Extension
24 from markdown.extensions.fenced_code import FencedCodeExtension
26 from markdown.extensions.fenced_code import FencedCodeExtension
25 from markdown.extensions.smart_strong import SmartEmphasisExtension
27 from markdown.extensions.smart_strong import SmartEmphasisExtension
26 from markdown.extensions.tables import TableExtension
28 from markdown.extensions.tables import TableExtension
27 from markdown.extensions.nl2br import Nl2BrExtension
29 from markdown.inlinepatterns import Pattern
28
30
29 import gfm
31 import gfm
30
32
31
33
34 class InlineProcessor(Pattern):
35 """
36 Base class that inline patterns subclass.
37 This is the newer style inline processor that uses a more
38 efficient and flexible search approach.
39 """
40
41 def __init__(self, pattern, md=None):
42 """
43 Create an instant of an inline pattern.
44 Keyword arguments:
45 * pattern: A regular expression that matches a pattern
46 """
47 self.pattern = pattern
48 self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
49
50 # Api for Markdown to pass safe_mode into instance
51 self.safe_mode = False
52 self.md = md
53
54 def handleMatch(self, m, data):
55 """Return a ElementTree element from the given match and the
56 start and end index of the matched text.
57 If `start` and/or `end` are returned as `None`, it will be
58 assumed that the processor did not find a valid region of text.
59 Subclasses should override this method.
60 Keyword arguments:
61 * m: A re match object containing a match of the pattern.
62 * data: The buffer current under analysis
63 Returns:
64 * el: The ElementTree element, text or None.
65 * start: The start of the region that has been matched or None.
66 * end: The end of the region that has been matched or None.
67 """
68 pass # pragma: no cover
69
70
71 class SimpleTagInlineProcessor(InlineProcessor):
72 """
73 Return element of type `tag` with a text attribute of group(2)
74 of a Pattern.
75 """
76 def __init__(self, pattern, tag):
77 InlineProcessor.__init__(self, pattern)
78 self.tag = tag
79
80 def handleMatch(self, m, data): # pragma: no cover
81 el = etree.Element(self.tag)
82 el.text = m.group(2)
83 return el, m.start(0), m.end(0)
84
85
86 class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
87 """ Return an element of type `tag` with no children. """
88 def handleMatch(self, m, data):
89 return etree.Element(self.tag), m.start(0), m.end(0)
90
91
92 class Nl2BrExtension(Extension):
93 BR_RE = r'\n'
94
95 def extendMarkdown(self, md, md_globals):
96 br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
97 md.inlinePatterns.add('nl', br_tag, '_end')
98
99
32 class GithubFlavoredMarkdownExtension(Extension):
100 class GithubFlavoredMarkdownExtension(Extension):
33 """
101 """
34 An extension that is as compatible as possible with GitHub-flavored
102 An extension that is as compatible as possible with GitHub-flavored
@@ -51,6 +119,7 b' class GithubFlavoredMarkdownExtension(Ex'
51
119
52 def extendMarkdown(self, md, md_globals):
120 def extendMarkdown(self, md, md_globals):
53 # Built-in extensions
121 # Built-in extensions
122 Nl2BrExtension().extendMarkdown(md, md_globals)
54 FencedCodeExtension().extendMarkdown(md, md_globals)
123 FencedCodeExtension().extendMarkdown(md, md_globals)
55 SmartEmphasisExtension().extendMarkdown(md, md_globals)
124 SmartEmphasisExtension().extendMarkdown(md, md_globals)
56 TableExtension().extendMarkdown(md, md_globals)
125 TableExtension().extendMarkdown(md, md_globals)
@@ -68,7 +137,6 b' class GithubFlavoredMarkdownExtension(Ex'
68 gfm.TaskListExtension([
137 gfm.TaskListExtension([
69 ('list_attrs', {'class': 'checkbox'})
138 ('list_attrs', {'class': 'checkbox'})
70 ]).extendMarkdown(md, md_globals)
139 ]).extendMarkdown(md, md_globals)
71 Nl2BrExtension().extendMarkdown(md, md_globals)
72
140
73
141
74 # Global Vars
142 # Global Vars
@@ -74,7 +74,11 b' def configure_dogpile_cache(settings):'
74
74
75 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
75 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
76 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
76 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
77 log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
77 if log.isEnabledFor(logging.DEBUG):
78 region_args = dict(backend=new_region.actual_backend.__class__,
79 region_invalidator=new_region.region_invalidator.__class__)
80 log.debug('dogpile: registering a new region `%s` %s', region_name, region_args)
81
78 region_meta.dogpile_cache_regions[region_name] = new_region
82 region_meta.dogpile_cache_regions[region_name] = new_region
79
83
80
84
@@ -915,8 +915,9 b' class BaseCommit(object):'
915 list of parent commits
915 list of parent commits
916
916
917 """
917 """
918 repository = None
919 branch = None
918
920
919 branch = None
920 """
921 """
921 Depending on the backend this should be set to the branch name of the
922 Depending on the backend this should be set to the branch name of the
922 commit. Backends not supporting branches on commits should leave this
923 commit. Backends not supporting branches on commits should leave this
@@ -1192,13 +1193,14 b' class BaseCommit(object):'
1192 return None
1193 return None
1193
1194
1194 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1195 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1195 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1196 archive_dir_name=None, write_metadata=False, mtime=None,
1197 archive_at_path='/'):
1196 """
1198 """
1197 Creates an archive containing the contents of the repository.
1199 Creates an archive containing the contents of the repository.
1198
1200
1199 :param archive_dest_path: path to the file which to create the archive.
1201 :param archive_dest_path: path to the file which to create the archive.
1200 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1202 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1201 :param prefix: name of root directory in archive.
1203 :param archive_dir_name: name of root directory in archive.
1202 Default is repository name and commit's short_id joined with dash:
1204 Default is repository name and commit's short_id joined with dash:
1203 ``"{repo_name}-{short_id}"``.
1205 ``"{repo_name}-{short_id}"``.
1204 :param write_metadata: write a metadata file into archive.
1206 :param write_metadata: write a metadata file into archive.
@@ -1214,43 +1216,26 b' class BaseCommit(object):'
1214 'Archive kind (%s) not supported use one of %s' %
1216 'Archive kind (%s) not supported use one of %s' %
1215 (kind, allowed_kinds))
1217 (kind, allowed_kinds))
1216
1218
1217 prefix = self._validate_archive_prefix(prefix)
1219 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1218
1219 mtime = mtime is not None or time.mktime(self.date.timetuple())
1220 mtime = mtime is not None or time.mktime(self.date.timetuple())
1220
1221 commit_id = self.raw_id
1221 file_info = []
1222 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1223 for _r, _d, files in cur_rev.walk(archive_at_path):
1224 for f in files:
1225 f_path = os.path.join(prefix, f.path)
1226 file_info.append(
1227 (f_path, f.mode, f.is_link(), f.raw_bytes))
1228
1222
1229 if write_metadata:
1223 return self.repository._remote.archive_repo(
1230 metadata = [
1224 archive_dest_path, kind, mtime, archive_at_path,
1231 ('repo_name', self.repository.name),
1225 archive_dir_name, commit_id)
1232 ('commit_id', self.raw_id),
1233 ('mtime', mtime),
1234 ('branch', self.branch),
1235 ('tags', ','.join(self.tags)),
1236 ]
1237 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1238 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1239
1226
1240 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1227 def _validate_archive_prefix(self, archive_dir_name):
1241
1228 if archive_dir_name is None:
1242 def _validate_archive_prefix(self, prefix):
1229 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1243 if prefix is None:
1244 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1245 repo_name=safe_str(self.repository.name),
1230 repo_name=safe_str(self.repository.name),
1246 short_id=self.short_id)
1231 short_id=self.short_id)
1247 elif not isinstance(prefix, str):
1232 elif not isinstance(archive_dir_name, str):
1248 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1233 raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
1249 elif prefix.startswith('/'):
1234 elif archive_dir_name.startswith('/'):
1250 raise VCSError("Prefix cannot start with leading slash")
1235 raise VCSError("Prefix cannot start with leading slash")
1251 elif prefix.strip() == '':
1236 elif archive_dir_name.strip() == '':
1252 raise VCSError("Prefix cannot be empty")
1237 raise VCSError("Prefix cannot be empty")
1253 return prefix
1238 return archive_dir_name
1254
1239
1255 @LazyProperty
1240 @LazyProperty
1256 def root(self):
1241 def root(self):
@@ -214,16 +214,19 b' def map_vcs_exceptions(func):'
214 # to translate them to the proper exception class in the vcs
214 # to translate them to the proper exception class in the vcs
215 # client layer.
215 # client layer.
216 kind = getattr(e, '_vcs_kind', None)
216 kind = getattr(e, '_vcs_kind', None)
217 exc_name = getattr(e, '_vcs_server_org_exc_name', None)
217
218
218 if kind:
219 if kind:
219 if any(e.args):
220 if any(e.args):
220 args = e.args
221 args = [a for a in e.args]
222 args[0] = '{}:'.format(exc_name) # prefix first arg with org exc name
221 else:
223 else:
222 args = [__traceback_info__ or 'unhandledException']
224 args = [__traceback_info__ or '{}: UnhandledException'.format(exc_name)]
223 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
225 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
224 # for other than unhandled errors also log the traceback
226 # for other than unhandled errors also log the traceback
225 # can be useful for debugging
227 # can be useful for debugging
226 log.error(__traceback_info__)
228 log.error(__traceback_info__)
229
227 raise _EXCEPTION_MAP[kind](*args)
230 raise _EXCEPTION_MAP[kind](*args)
228 else:
231 else:
229 raise
232 raise
@@ -37,6 +37,7 b' from rhodecode.lib.exceptions import Com'
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 false, true,
40 ChangesetComment,
41 ChangesetComment,
41 User,
42 User,
42 Notification,
43 Notification,
@@ -160,7 +161,7 b' class CommentsModel(BaseModel):'
160
161
161 return todos
162 return todos
162
163
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164
165
165 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
@@ -168,6 +169,9 b' class CommentsModel(BaseModel):'
168 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
170
171
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
174
171 if not show_outdated:
175 if not show_outdated:
172 todos = todos.filter(
176 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
@@ -177,7 +181,7 b' class CommentsModel(BaseModel):'
177
181
178 return todos
182 return todos
179
183
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
181
185
182 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
@@ -185,6 +189,9 b' class CommentsModel(BaseModel):'
185 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
187
191
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
194
188 if not show_outdated:
195 if not show_outdated:
189 todos = todos.filter(
196 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
@@ -194,7 +201,14 b' class CommentsModel(BaseModel):'
194
201
195 return todos
202 return todos
196
203
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
204 def get_pull_request_drafts(self, user_id, pull_request):
205 drafts = Session().query(ChangesetComment) \
206 .filter(ChangesetComment.pull_request == pull_request) \
207 .filter(ChangesetComment.user_id == user_id) \
208 .filter(ChangesetComment.draft == true())
209 return drafts.all()
210
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
198
212
199 todos = Session().query(ChangesetComment) \
213 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
214 .filter(ChangesetComment.revision == commit_id) \
@@ -202,6 +216,9 b' class CommentsModel(BaseModel):'
202 .filter(ChangesetComment.comment_type
216 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
217 == ChangesetComment.COMMENT_TYPE_TODO)
204
218
219 if not include_drafts:
220 todos = todos.filter(ChangesetComment.draft == false())
221
205 if not show_outdated:
222 if not show_outdated:
206 todos = todos.filter(
223 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
@@ -211,7 +228,7 b' class CommentsModel(BaseModel):'
211
228
212 return todos
229 return todos
213
230
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
215
232
216 todos = Session().query(ChangesetComment) \
233 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
234 .filter(ChangesetComment.revision == commit_id) \
@@ -219,6 +236,9 b' class CommentsModel(BaseModel):'
219 .filter(ChangesetComment.comment_type
236 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
237 == ChangesetComment.COMMENT_TYPE_TODO)
221
238
239 if not include_drafts:
240 todos = todos.filter(ChangesetComment.draft == false())
241
222 if not show_outdated:
242 if not show_outdated:
223 todos = todos.filter(
243 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
244 coalesce(ChangesetComment.display_state, '') !=
@@ -228,11 +248,15 b' class CommentsModel(BaseModel):'
228
248
229 return todos
249 return todos
230
250
231 def get_commit_inline_comments(self, commit_id):
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
232 inline_comments = Session().query(ChangesetComment) \
252 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
253 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
254 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
255 .filter(ChangesetComment.revision == commit_id)
256
257 if not include_drafts:
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
259
236 inline_comments = inline_comments.all()
260 inline_comments = inline_comments.all()
237 return inline_comments
261 return inline_comments
238
262
@@ -245,7 +269,7 b' class CommentsModel(BaseModel):'
245
269
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
271 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
272 status_change_type=None, comment_type=None, is_draft=False,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
274 renderer=None, auth_user=None, extra_recipients=None):
251 """
275 """
@@ -262,6 +286,7 b' class CommentsModel(BaseModel):'
262 :param line_no:
286 :param line_no:
263 :param status_change: Label for status change
287 :param status_change: Label for status change
264 :param comment_type: Type of comment
288 :param comment_type: Type of comment
289 :param is_draft: is comment a draft only
265 :param resolves_comment_id: id of comment which this one will resolve
290 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
291 :param status_change_type: type of status change
267 :param closing_pr:
292 :param closing_pr:
@@ -288,6 +313,7 b' class CommentsModel(BaseModel):'
288 validated_kwargs = schema.deserialize(dict(
313 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
314 comment_body=text,
290 comment_type=comment_type,
315 comment_type=comment_type,
316 is_draft=is_draft,
291 comment_file=f_path,
317 comment_file=f_path,
292 comment_line=line_no,
318 comment_line=line_no,
293 renderer_type=renderer,
319 renderer_type=renderer,
@@ -296,6 +322,7 b' class CommentsModel(BaseModel):'
296 repo=repo.repo_id,
322 repo=repo.repo_id,
297 user=user.user_id,
323 user=user.user_id,
298 ))
324 ))
325 is_draft = validated_kwargs['is_draft']
299
326
300 comment = ChangesetComment()
327 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
328 comment.renderer = validated_kwargs['renderer_type']
@@ -303,6 +330,7 b' class CommentsModel(BaseModel):'
303 comment.f_path = validated_kwargs['comment_file']
330 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
331 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
332 comment.comment_type = validated_kwargs['comment_type']
333 comment.draft = is_draft
306
334
307 comment.repo = repo
335 comment.repo = repo
308 comment.author = user
336 comment.author = user
@@ -438,9 +466,6 b' class CommentsModel(BaseModel):'
438
466
439 if send_email:
467 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
468 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
444
469
445 mention_recipients = set(
470 mention_recipients = set(
446 self._extract_mentions(text)).difference(recipients)
471 self._extract_mentions(text)).difference(recipients)
@@ -448,8 +473,8 b' class CommentsModel(BaseModel):'
448 # create notification objects, and emails
473 # create notification objects, and emails
449 NotificationModel().create(
474 NotificationModel().create(
450 created_by=user,
475 created_by=user,
451 notification_subject=subject,
476 notification_subject='', # Filled in based on the notification_type
452 notification_body=body_plaintext,
477 notification_body='', # Filled in based on the notification_type
453 notification_type=notification_type,
478 notification_type=notification_type,
454 recipients=recipients,
479 recipients=recipients,
455 mention_recipients=mention_recipients,
480 mention_recipients=mention_recipients,
@@ -462,10 +487,11 b' class CommentsModel(BaseModel):'
462 else:
487 else:
463 action = 'repo.commit.comment.create'
488 action = 'repo.commit.comment.create'
464
489
465 comment_data = comment.get_api_data()
490 if not is_draft:
491 comment_data = comment.get_api_data()
466
492
467 self._log_audit_action(
493 self._log_audit_action(
468 action, {'data': comment_data}, auth_user, comment)
494 action, {'data': comment_data}, auth_user, comment)
469
495
470 return comment
496 return comment
471
497
@@ -541,7 +567,8 b' class CommentsModel(BaseModel):'
541
567
542 return comment
568 return comment
543
569
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
570 def get_all_comments(self, repo_id, revision=None, pull_request=None,
571 include_drafts=True, count_only=False):
545 q = ChangesetComment.query()\
572 q = ChangesetComment.query()\
546 .filter(ChangesetComment.repo_id == repo_id)
573 .filter(ChangesetComment.repo_id == repo_id)
547 if revision:
574 if revision:
@@ -551,6 +578,8 b' class CommentsModel(BaseModel):'
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
578 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
552 else:
579 else:
553 raise Exception('Please specify commit or pull_request')
580 raise Exception('Please specify commit or pull_request')
581 if not include_drafts:
582 q = q.filter(ChangesetComment.draft == false())
554 q = q.order_by(ChangesetComment.created_on)
583 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
584 if count_only:
556 return q.count()
585 return q.count()
@@ -697,7 +726,8 b' class CommentsModel(BaseModel):'
697 path=comment.f_path, diff_line=diff_line)
726 path=comment.f_path, diff_line=diff_line)
698 except (diffs.LineNotInDiffException,
727 except (diffs.LineNotInDiffException,
699 diffs.FileNotInDiffException):
728 diffs.FileNotInDiffException):
700 comment.display_state = ChangesetComment.COMMENT_OUTDATED
729 if not comment.draft:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
701 return
731 return
702
732
703 if old_context == new_context:
733 if old_context == new_context:
@@ -707,14 +737,15 b' class CommentsModel(BaseModel):'
707 new_diff_lines = new_diff_proc.find_context(
737 new_diff_lines = new_diff_proc.find_context(
708 path=comment.f_path, context=old_context,
738 path=comment.f_path, context=old_context,
709 offset=self.DIFF_CONTEXT_BEFORE)
739 offset=self.DIFF_CONTEXT_BEFORE)
710 if not new_diff_lines:
740 if not new_diff_lines and not comment.draft:
711 comment.display_state = ChangesetComment.COMMENT_OUTDATED
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
712 else:
742 else:
713 new_diff_line = self._choose_closest_diff_line(
743 new_diff_line = self._choose_closest_diff_line(
714 diff_line, new_diff_lines)
744 diff_line, new_diff_lines)
715 comment.line_no = _diff_to_comment_line_number(new_diff_line)
745 comment.line_no = _diff_to_comment_line_number(new_diff_line)
716 else:
746 else:
717 comment.display_state = ChangesetComment.COMMENT_OUTDATED
747 if not comment.draft:
748 comment.display_state = ChangesetComment.COMMENT_OUTDATED
718
749
719 def _should_relocate_diff_line(self, diff_line):
750 def _should_relocate_diff_line(self, diff_line):
720 """
751 """
@@ -3767,6 +3767,7 b' class ChangesetComment(Base, BaseModel):'
3767 renderer = Column('renderer', Unicode(64), nullable=True)
3767 renderer = Column('renderer', Unicode(64), nullable=True)
3768 display_state = Column('display_state', Unicode(128), nullable=True)
3768 display_state = Column('display_state', Unicode(128), nullable=True)
3769 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3769 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3770 draft = Column('draft', Boolean(), nullable=True, default=False)
3770
3771
3771 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3772 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3772 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3773 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
@@ -5057,8 +5058,14 b' class RepoReviewRule(Base, BaseModel):'
5057 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5058 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5058
5059
5059 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5060 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5060 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5061
5061 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5062 # Legacy fields, just for backward compat
5063 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5064 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5065
5066 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5067 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5068
5062 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5069 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5063
5070
5064 rule_users = relationship('RepoReviewRuleUser')
5071 rule_users = relationship('RepoReviewRuleUser')
@@ -5094,6 +5101,22 b' class RepoReviewRule(Base, BaseModel):'
5094 self._validate_pattern(value)
5101 self._validate_pattern(value)
5095 self._file_pattern = value or '*'
5102 self._file_pattern = value or '*'
5096
5103
5104 @hybrid_property
5105 def forbid_pr_author_to_review(self):
5106 return self.pr_author == 'forbid_pr_author'
5107
5108 @hybrid_property
5109 def include_pr_author_to_review(self):
5110 return self.pr_author == 'include_pr_author'
5111
5112 @hybrid_property
5113 def forbid_commit_author_to_review(self):
5114 return self.commit_author == 'forbid_commit_author'
5115
5116 @hybrid_property
5117 def include_commit_author_to_review(self):
5118 return self.commit_author == 'include_commit_author'
5119
5097 def matches(self, source_branch, target_branch, files_changed):
5120 def matches(self, source_branch, target_branch, files_changed):
5098 """
5121 """
5099 Check if this review rule matches a branch/files in a pull request
5122 Check if this review rule matches a branch/files in a pull request
@@ -55,7 +55,7 b' class NotificationModel(BaseModel):'
55 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
56
56
57 def create(
57 def create(
58 self, created_by, notification_subject, notification_body,
58 self, created_by, notification_subject='', notification_body='',
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 mention_recipients=None, with_email=True, email_kwargs=None):
60 mention_recipients=None, with_email=True, email_kwargs=None):
61 """
61 """
@@ -64,11 +64,12 b' class NotificationModel(BaseModel):'
64
64
65 :param created_by: int, str or User instance. User who created this
65 :param created_by: int, str or User instance. User who created this
66 notification
66 notification
67 :param notification_subject: subject of notification itself
67 :param notification_subject: subject of notification itself,
68 it will be generated automatically from notification_type if not specified
68 :param notification_body: body of notification text
69 :param notification_body: body of notification text
70 it will be generated automatically from notification_type if not specified
69 :param notification_type: type of notification, based on that we
71 :param notification_type: type of notification, based on that we
70 pick templates
72 pick templates
71
72 :param recipients: list of int, str or User objects, when None
73 :param recipients: list of int, str or User objects, when None
73 is given send to all admins
74 is given send to all admins
74 :param mention_recipients: list of int, str or User objects,
75 :param mention_recipients: list of int, str or User objects,
@@ -82,14 +83,19 b' class NotificationModel(BaseModel):'
82 if recipients and not getattr(recipients, '__iter__', False):
83 if recipients and not getattr(recipients, '__iter__', False):
83 raise Exception('recipients must be an iterable object')
84 raise Exception('recipients must be an iterable object')
84
85
86 if not (notification_subject and notification_body) and not notification_type:
87 raise ValueError('notification_subject, and notification_body '
88 'cannot be empty when notification_type is not specified')
89
85 created_by_obj = self._get_user(created_by)
90 created_by_obj = self._get_user(created_by)
91
92 if not created_by_obj:
93 raise Exception('unknown user %s' % created_by)
94
86 # default MAIN body if not given
95 # default MAIN body if not given
87 email_kwargs = email_kwargs or {'body': notification_body}
96 email_kwargs = email_kwargs or {'body': notification_body}
88 mention_recipients = mention_recipients or set()
97 mention_recipients = mention_recipients or set()
89
98
90 if not created_by_obj:
91 raise Exception('unknown user %s' % created_by)
92
93 if recipients is None:
99 if recipients is None:
94 # recipients is None means to all admins
100 # recipients is None means to all admins
95 recipients_objs = User.query().filter(User.admin == true()).all()
101 recipients_objs = User.query().filter(User.admin == true()).all()
@@ -113,6 +119,15 b' class NotificationModel(BaseModel):'
113 # add mentioned users into recipients
119 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
120 final_recipients = set(recipients_objs).union(mention_recipients)
115
121
122 (subject, email_body, email_body_plaintext) = \
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
124
125 if not notification_subject:
126 notification_subject = subject
127
128 if not notification_body:
129 notification_body = email_body_plaintext
130
116 notification = Notification.create(
131 notification = Notification.create(
117 created_by=created_by_obj, subject=notification_subject,
132 created_by=created_by_obj, subject=notification_subject,
118 body=notification_body, recipients=final_recipients,
133 body=notification_body, recipients=final_recipients,
@@ -578,7 +578,7 b' class PermissionModel(BaseModel):'
578 return user_group_write_permissions
578 return user_group_write_permissions
579
579
580 def trigger_permission_flush(self, affected_user_ids=None):
580 def trigger_permission_flush(self, affected_user_ids=None):
581 affected_user_ids or User.get_all_user_ids()
581 affected_user_ids = affected_user_ids or User.get_all_user_ids()
582 events.trigger(events.UserPermissionsChange(affected_user_ids))
582 events.trigger(events.UserPermissionsChange(affected_user_ids))
583
583
584 def flush_user_permission_caches(self, changes, affected_user_ids=None):
584 def flush_user_permission_caches(self, changes, affected_user_ids=None):
@@ -1502,15 +1502,11 b' class PullRequestModel(BaseModel):'
1502 'user_role': role
1502 'user_role': role
1503 }
1503 }
1504
1504
1505 # pre-generate the subject for notification itself
1506 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1507 notification_type, **kwargs)
1508
1509 # create notification objects, and emails
1505 # create notification objects, and emails
1510 NotificationModel().create(
1506 NotificationModel().create(
1511 created_by=current_rhodecode_user,
1507 created_by=current_rhodecode_user,
1512 notification_subject=subject,
1508 notification_subject='', # Filled in based on the notification_type
1513 notification_body=body_plaintext,
1509 notification_body='', # Filled in based on the notification_type
1514 notification_type=notification_type,
1510 notification_type=notification_type,
1515 recipients=recipients,
1511 recipients=recipients,
1516 email_kwargs=kwargs,
1512 email_kwargs=kwargs,
@@ -1579,14 +1575,11 b' class PullRequestModel(BaseModel):'
1579 'thread_ids': [pr_url],
1575 'thread_ids': [pr_url],
1580 }
1576 }
1581
1577
1582 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1583 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1584
1585 # create notification objects, and emails
1578 # create notification objects, and emails
1586 NotificationModel().create(
1579 NotificationModel().create(
1587 created_by=updating_user,
1580 created_by=updating_user,
1588 notification_subject=subject,
1581 notification_subject='', # Filled in based on the notification_type
1589 notification_body=body_plaintext,
1582 notification_body='', # Filled in based on the notification_type
1590 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1583 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1591 recipients=recipients,
1584 recipients=recipients,
1592 email_kwargs=email_kwargs,
1585 email_kwargs=email_kwargs,
@@ -2067,6 +2060,8 b' class MergeCheck(object):'
2067 self.error_details = OrderedDict()
2060 self.error_details = OrderedDict()
2068 self.source_commit = AttributeDict()
2061 self.source_commit = AttributeDict()
2069 self.target_commit = AttributeDict()
2062 self.target_commit = AttributeDict()
2063 self.reviewers_count = 0
2064 self.observers_count = 0
2070
2065
2071 def __repr__(self):
2066 def __repr__(self):
2072 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2067 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
@@ -2128,11 +2123,12 b' class MergeCheck(object):'
2128 # review status, must be always present
2123 # review status, must be always present
2129 review_status = pull_request.calculated_review_status()
2124 review_status = pull_request.calculated_review_status()
2130 merge_check.review_status = review_status
2125 merge_check.review_status = review_status
2126 merge_check.reviewers_count = pull_request.reviewers_count
2127 merge_check.observers_count = pull_request.observers_count
2131
2128
2132 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2129 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2133 if not status_approved:
2130 if not status_approved and merge_check.reviewers_count:
2134 log.debug("MergeCheck: cannot merge, approval is pending.")
2131 log.debug("MergeCheck: cannot merge, approval is pending.")
2135
2136 msg = _('Pull request reviewer approval is pending.')
2132 msg = _('Pull request reviewer approval is pending.')
2137
2133
2138 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2134 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
@@ -231,6 +231,10 b' class ScmModel(BaseModel):'
231 with_wire={"cache": False})
231 with_wire={"cache": False})
232 except OSError:
232 except OSError:
233 continue
233 continue
234 except RepositoryError:
235 log.exception('Failed to create a repo')
236 continue
237
234 log.debug('found %s paths with repositories', len(repos))
238 log.debug('found %s paths with repositories', len(repos))
235 return repos
239 return repos
236
240
@@ -425,15 +425,12 b' class UserModel(BaseModel):'
425 'date': datetime.datetime.now()
425 'date': datetime.datetime.now()
426 }
426 }
427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
428 # pre-generate the subject for notification itself
429 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
430 notification_type, **kwargs)
431
428
432 # create notification objects, and emails
429 # create notification objects, and emails
433 NotificationModel().create(
430 NotificationModel().create(
434 created_by=new_user,
431 created_by=new_user,
435 notification_subject=subject,
432 notification_subject='', # Filled in based on the notification_type
436 notification_body=body_plaintext,
433 notification_body='', # Filled in based on the notification_type
437 notification_type=notification_type,
434 notification_type=notification_type,
438 recipients=None, # all admins
435 recipients=None, # all admins
439 email_kwargs=kwargs,
436 email_kwargs=kwargs,
@@ -60,7 +60,7 b' class CommentSchema(colander.MappingSche'
60 colander.String(),
60 colander.String(),
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
62 missing=ChangesetComment.COMMENT_TYPE_NOTE)
62 missing=ChangesetComment.COMMENT_TYPE_NOTE)
63
63 is_draft = colander.SchemaNode(colander.Boolean(),missing=False)
64 comment_file = colander.SchemaNode(colander.String(), missing=None)
64 comment_file = colander.SchemaNode(colander.String(), missing=None)
65 comment_line = colander.SchemaNode(colander.String(), missing=None)
65 comment_line = colander.SchemaNode(colander.String(), missing=None)
66 status_change = colander.SchemaNode(
66 status_change = colander.SchemaNode(
@@ -162,7 +162,6 b' input[type="button"] {'
162 }
162 }
163 }
163 }
164
164
165 .btn-warning,
166 .btn-danger,
165 .btn-danger,
167 .revoke_perm,
166 .revoke_perm,
168 .btn-x,
167 .btn-x,
@@ -196,6 +195,36 b' input[type="button"] {'
196 }
195 }
197 }
196 }
198
197
198 .btn-warning {
199 .border ( @border-thickness, @alert3 );
200 background-color: white;
201 color: @alert3;
202
203 a {
204 color: @alert3;
205 }
206
207 &:hover,
208 &.active {
209 .border ( @border-thickness, @alert3 );
210 color: white;
211 background-color: @alert3;
212
213 a {
214 color: white;
215 }
216 }
217
218 i {
219 display:none;
220 }
221
222 &:disabled {
223 background-color: white;
224 color: @alert3;
225 }
226 }
227
199 .btn-approved-status {
228 .btn-approved-status {
200 .border ( @border-thickness, @alert1 );
229 .border ( @border-thickness, @alert1 );
201 background-color: white;
230 background-color: white;
@@ -264,7 +293,6 b' input[type="button"] {'
264 margin-left: -1px;
293 margin-left: -1px;
265 padding-left: 2px;
294 padding-left: 2px;
266 padding-right: 2px;
295 padding-right: 2px;
267 border-left: 1px solid @grey3;
268 }
296 }
269 }
297 }
270
298
@@ -342,7 +370,7 b' input[type="button"] {'
342 color: @alert2;
370 color: @alert2;
343
371
344 &:hover {
372 &:hover {
345 color: darken(@alert2,30%);
373 color: darken(@alert2, 30%);
346 }
374 }
347
375
348 &:disabled {
376 &:disabled {
@@ -402,6 +430,37 b' input[type="button"] {'
402 }
430 }
403
431
404
432
433 input[type="submit"].btn-warning {
434 &:extend(.btn-warning);
435
436 &:focus {
437 outline: 0;
438 }
439
440 &:hover {
441 &:extend(.btn-warning:hover);
442 }
443
444 &.btn-link {
445 &:extend(.btn-link);
446 color: @alert3;
447
448 &:disabled {
449 color: @alert3;
450 background-color: transparent;
451 }
452 }
453
454 &:disabled {
455 .border ( @border-thickness-buttons, @alert3 );
456 background-color: white;
457 color: @alert3;
458 opacity: 0.5;
459 }
460 }
461
462
463
405 // TODO: johbo: Form button tweaks, check if we can use the classes instead
464 // TODO: johbo: Form button tweaks, check if we can use the classes instead
406 input[type="submit"] {
465 input[type="submit"] {
407 &:extend(.btn-primary);
466 &:extend(.btn-primary);
@@ -1002,7 +1002,7 b' input.filediff-collapse-state {'
1002 .nav-chunk {
1002 .nav-chunk {
1003 position: absolute;
1003 position: absolute;
1004 right: 20px;
1004 right: 20px;
1005 margin-top: -17px;
1005 margin-top: -15px;
1006 }
1006 }
1007
1007
1008 .nav-chunk.selected {
1008 .nav-chunk.selected {
@@ -4,7 +4,7 b''
4
4
5
5
6 // Comments
6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 1.0;
8
8
9 .comments {
9 .comments {
10 width: 100%;
10 width: 100%;
@@ -61,28 +61,37 b' tr.inline-comments div {'
61 visibility: hidden;
61 visibility: hidden;
62 }
62 }
63
63
64 .comment-draft {
65 float: left;
66 margin-right: 10px;
67 font-weight: 400;
68 color: @color-draft;
69 }
70
71 .comment-new {
72 float: left;
73 margin-right: 10px;
74 font-weight: 400;
75 color: @color-new;
76 }
77
64 .comment-label {
78 .comment-label {
65 float: left;
79 float: left;
66
80
67 padding: 0.4em 0.4em;
81 padding: 0 8px 0 0;
68 margin: 2px 4px 0px 0px;
69 display: inline-block;
70 min-height: 0;
82 min-height: 0;
71
83
72 text-align: center;
84 text-align: center;
73 font-size: 10px;
85 font-size: 10px;
74 line-height: .8em;
75
86
76 font-family: @text-italic;
87 font-family: @text-italic;
77 font-style: italic;
88 font-style: italic;
78 background: #fff none;
89 background: #fff none;
79 color: @grey3;
90 color: @grey3;
80 border: 1px solid @grey4;
81 white-space: nowrap;
91 white-space: nowrap;
82
92
83 text-transform: uppercase;
93 text-transform: uppercase;
84 min-width: 50px;
94 min-width: 50px;
85 border-radius: 4px;
86
95
87 &.todo {
96 &.todo {
88 color: @color5;
97 color: @color5;
@@ -270,64 +279,165 b' tr.inline-comments div {'
270 .comment-outdated {
279 .comment-outdated {
271 opacity: @comment-outdated-opacity;
280 opacity: @comment-outdated-opacity;
272 }
281 }
282
283 .comment-outdated-label {
284 color: @grey3;
285 padding-right: 4px;
286 }
273 }
287 }
274
288
275 .inline-comments {
289 .inline-comments {
276 border-radius: @border-radius;
290
277 .comment {
291 .comment {
278 margin: 0;
292 margin: 0;
279 border-radius: @border-radius;
280 }
293 }
294
281 .comment-outdated {
295 .comment-outdated {
282 opacity: @comment-outdated-opacity;
296 opacity: @comment-outdated-opacity;
283 }
297 }
284
298
299 .comment-outdated-label {
300 color: @grey3;
301 padding-right: 4px;
302 }
303
285 .comment-inline {
304 .comment-inline {
305
306 &:first-child {
307 margin: 4px 4px 0 4px;
308 border-top: 1px solid @grey5;
309 border-bottom: 0 solid @grey5;
310 border-left: 1px solid @grey5;
311 border-right: 1px solid @grey5;
312 .border-radius-top(4px);
313 }
314
315 &:only-child {
316 margin: 4px 4px 0 4px;
317 border-top: 1px solid @grey5;
318 border-bottom: 0 solid @grey5;
319 border-left: 1px solid @grey5;
320 border-right: 1px solid @grey5;
321 .border-radius-top(4px);
322 }
323
286 background: white;
324 background: white;
287 padding: @comment-padding @comment-padding;
325 padding: @comment-padding @comment-padding;
288 border: @comment-padding solid @grey6;
326 margin: 0 4px 0 4px;
327 border-top: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
329 border-left: 1px solid @grey5;
330 border-right: 1px solid @grey5;
289
331
290 .text {
332 .text {
291 border: none;
333 border: none;
292 }
334 }
335
293 .meta {
336 .meta {
294 border-bottom: 1px solid @grey6;
337 border-bottom: 1px solid @grey6;
295 margin: -5px 0px;
338 margin: -5px 0px;
296 line-height: 24px;
339 line-height: 24px;
297 }
340 }
341
298 }
342 }
299 .comment-selected {
343 .comment-selected {
300 border-left: 6px solid @comment-highlight-color;
344 border-left: 6px solid @comment-highlight-color;
301 }
345 }
346
347 .comment-inline-form-open {
348 display: block !important;
349 }
350
302 .comment-inline-form {
351 .comment-inline-form {
303 padding: @comment-padding;
304 display: none;
352 display: none;
305 }
353 }
306 .cb-comment-add-button {
354
307 margin: @comment-padding;
355 .comment-inline-form-edit {
356 padding: 0;
357 margin: 0px 4px 2px 4px;
358 }
359
360 .reply-thread-container {
361 display: table;
362 width: 100%;
363 padding: 0px 4px 4px 4px;
364 }
365
366 .reply-thread-container-wrapper {
367 margin: 0 4px 4px 4px;
368 border-top: 0 solid @grey5;
369 border-bottom: 1px solid @grey5;
370 border-left: 1px solid @grey5;
371 border-right: 1px solid @grey5;
372 .border-radius-bottom(4px);
373 }
374
375 .reply-thread-gravatar {
376 display: table-cell;
377 width: 24px;
378 height: 24px;
379 padding-top: 10px;
380 padding-left: 10px;
381 background-color: #eeeeee;
382 vertical-align: top;
308 }
383 }
309 /* hide add comment button when form is open */
384
385 .reply-thread-reply-button {
386 display: table-cell;
387 width: 100%;
388 height: 33px;
389 padding: 3px 8px;
390 margin-left: 8px;
391 background-color: #eeeeee;
392 }
393
394 .reply-thread-reply-button .cb-comment-add-button {
395 border-radius: 4px;
396 width: 100%;
397 padding: 6px 2px;
398 text-align: left;
399 cursor: text;
400 color: @grey3;
401 }
402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 background-color: white;
404 color: @grey2;
405 }
406
407 .reply-thread-last {
408 display: table-cell;
409 width: 10px;
410 }
411
412 /* Hide reply box when it's a first element,
413 can happen when drafts are saved but not shown to specific user,
414 or there are outdated comments hidden
415 */
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 display: none;
418 }
419
420 .reply-thread-container-wrapper.comment-outdated {
421 display: none
422 }
423
424 /* hide add comment button when form is open */
310 .comment-inline-form-open ~ .cb-comment-add-button {
425 .comment-inline-form-open ~ .cb-comment-add-button {
311 display: none;
426 display: none;
312 }
427 }
313 .comment-inline-form-open {
428
314 display: block;
315 }
316 /* hide add comment button when form but no comments */
317 .comment-inline-form:first-child + .cb-comment-add-button {
318 display: none;
319 }
320 /* hide add comment button when no comments or form */
321 .cb-comment-add-button:first-child {
322 display: none;
323 }
324 /* hide add comment button when only comment is being deleted */
429 /* hide add comment button when only comment is being deleted */
325 .comment-deleting:first-child + .cb-comment-add-button {
430 .comment-deleting:first-child + .cb-comment-add-button {
326 display: none;
431 display: none;
327 }
432 }
433
434 /* hide add comment button when form but no comments */
435 .comment-inline-form:first-child + .cb-comment-add-button {
436 display: none;
437 }
438
328 }
439 }
329
440
330
331 .show-outdated-comments {
441 .show-outdated-comments {
332 display: inline;
442 display: inline;
333 color: @rcblue;
443 color: @rcblue;
@@ -380,23 +490,36 b' form.comment-form {'
380 }
490 }
381
491
382 .comment-footer {
492 .comment-footer {
383 position: relative;
493 display: table;
384 width: 100%;
494 width: 100%;
385 min-height: 42px;
495 height: 42px;
386
496
387 .status_box,
497 .comment-status-box,
388 .cancel-button {
498 .cancel-button {
389 float: left;
390 display: inline-block;
499 display: inline-block;
391 }
500 }
392
501
393 .status_box {
502 .comment-status-box {
394 margin-left: 10px;
503 margin-left: 10px;
395 }
504 }
396
505
397 .action-buttons {
506 .action-buttons {
398 float: left;
507 display: table-cell;
399 display: inline-block;
508 padding: 5px 0 5px 2px;
509 }
510
511 .toolbar-text {
512 height: 28px;
513 display: table-cell;
514 vertical-align: baseline;
515 font-size: 11px;
516 color: @grey4;
517 text-align: right;
518
519 a {
520 color: @grey4;
521 }
522
400 }
523 }
401
524
402 .action-buttons-extra {
525 .action-buttons-extra {
@@ -427,10 +550,10 b' form.comment-form {'
427 margin-right: 0;
550 margin-right: 0;
428 }
551 }
429
552
430 .comment-footer {
553 #save_general {
431 margin-bottom: 50px;
554 margin-left: -6px;
432 margin-top: 10px;
433 }
555 }
556
434 }
557 }
435
558
436
559
@@ -482,8 +605,8 b' form.comment-form {'
482 .injected_diff .comment-inline-form,
605 .injected_diff .comment-inline-form,
483 .comment-inline-form {
606 .comment-inline-form {
484 background-color: white;
607 background-color: white;
485 margin-top: 10px;
608 margin-top: 4px;
486 margin-bottom: 20px;
609 margin-bottom: 10px;
487 }
610 }
488
611
489 .inline-form {
612 .inline-form {
@@ -519,9 +642,6 b' form.comment-form {'
519 margin: 0px;
642 margin: 0px;
520 }
643 }
521
644
522 .comment-inline-form .comment-footer {
523 margin: 10px 0px 0px 0px;
524 }
525
645
526 .hide-inline-form-button {
646 .hide-inline-form-button {
527 margin-left: 5px;
647 margin-left: 5px;
@@ -547,6 +667,7 b' comment-area-text {'
547
667
548 .comment-area-header {
668 .comment-area-header {
549 height: 35px;
669 height: 35px;
670 border-bottom: 1px solid @grey5;
550 }
671 }
551
672
552 .comment-area-header .nav-links {
673 .comment-area-header .nav-links {
@@ -554,6 +675,7 b' comment-area-text {'
554 flex-flow: row wrap;
675 flex-flow: row wrap;
555 -webkit-flex-flow: row wrap;
676 -webkit-flex-flow: row wrap;
556 width: 100%;
677 width: 100%;
678 border: none;
557 }
679 }
558
680
559 .comment-area-footer {
681 .comment-area-footer {
@@ -622,14 +744,3 b' comment-area-text {'
622 border-bottom: 2px solid transparent;
744 border-bottom: 2px solid transparent;
623 }
745 }
624
746
625 .toolbar-text {
626 float: right;
627 font-size: 11px;
628 color: @grey4;
629 text-align: right;
630
631 a {
632 color: @grey4;
633 }
634 }
635
@@ -213,7 +213,6 b' div.markdown-block pre {'
213 div.markdown-block img {
213 div.markdown-block img {
214 border-style: none;
214 border-style: none;
215 background-color: #fff;
215 background-color: #fff;
216 padding-right: 20px;
217 max-width: 100%;
216 max-width: 100%;
218 }
217 }
219
218
@@ -274,6 +273,13 b' div.markdown-block #ws {'
274 background-color: @grey6;
273 background-color: @grey6;
275 }
274 }
276
275
276 div.markdown-block p {
277 margin-top: 0;
278 margin-bottom: 16px;
279 padding: 0;
280 line-height: unset;
281 }
282
277 div.markdown-block code,
283 div.markdown-block code,
278 div.markdown-block pre,
284 div.markdown-block pre,
279 div.markdown-block #ws,
285 div.markdown-block #ws,
@@ -2,7 +2,7 b''
2
2
3
3
4 .loginbox {
4 .loginbox {
5 max-width: 65%;
5 max-width: 960px;
6 margin: @pagepadding auto;
6 margin: @pagepadding auto;
7 font-family: @text-light;
7 font-family: @text-light;
8 border: @border-thickness solid @grey5;
8 border: @border-thickness solid @grey5;
@@ -22,13 +22,27 b''
22 float: none;
22 float: none;
23 }
23 }
24
24
25 .header {
25 .header-account {
26 min-height: 49px;
26 width: 100%;
27 width: 100%;
27 padding: 0 35px;
28 padding: 0 @header-padding;
28 box-sizing: border-box;
29 box-sizing: border-box;
30 position: relative;
31 vertical-align: bottom;
32
33 background-color: @grey1;
34 color: @grey5;
29
35
30 .title {
36 .title {
31 padding: 0;
37 padding: 0;
38 overflow: visible;
39 }
40
41 &:before,
42 &:after {
43 content: "";
44 clear: both;
45 width: 100%;
32 }
46 }
33 }
47 }
34
48
@@ -69,7 +83,7 b''
69 .sign-in-image {
83 .sign-in-image {
70 display: block;
84 display: block;
71 width: 65%;
85 width: 65%;
72 margin: 5% auto;
86 margin: 1% auto;
73 }
87 }
74
88
75 .sign-in-title {
89 .sign-in-title {
@@ -263,9 +263,6 b' input.inline[type="file"] {'
263 // HEADER
263 // HEADER
264 .header {
264 .header {
265
265
266 // TODO: johbo: Fix login pages, so that they work without a min-height
267 // for the header and then remove the min-height. I chose a smaller value
268 // intentionally here to avoid rendering issues in the main navigation.
269 min-height: 49px;
266 min-height: 49px;
270 min-width: 1024px;
267 min-width: 1024px;
271
268
@@ -1143,9 +1140,8 b' label {'
1143 margin-left: -15px;
1140 margin-left: -15px;
1144 }
1141 }
1145
1142
1146 #rev_range_container, #rev_range_clear, #rev_range_more {
1143 #rev_range_action {
1147 margin-top: -5px;
1144 margin-bottom: -8px;
1148 margin-bottom: -5px;
1149 }
1145 }
1150
1146
1151 #filter_changelog {
1147 #filter_changelog {
@@ -1591,9 +1587,9 b' table.integrations {'
1591 }
1587 }
1592 .pr-details-title {
1588 .pr-details-title {
1593 height: 20px;
1589 height: 20px;
1594 line-height: 20px;
1590 line-height: 16px;
1595
1591
1596 padding-bottom: 8px;
1592 padding-bottom: 4px;
1597 border-bottom: @border-thickness solid @grey5;
1593 border-bottom: @border-thickness solid @grey5;
1598
1594
1599 .action_button.disabled {
1595 .action_button.disabled {
@@ -3212,7 +3208,12 b' details:not([open]) > :not(summary) {'
3212
3208
3213 .sidebar-element {
3209 .sidebar-element {
3214 margin-top: 20px;
3210 margin-top: 20px;
3215 }
3211
3212 .icon-draft {
3213 color: @color-draft
3214 }
3215 }
3216
3216
3217
3217 .right-sidebar-collapsed-state {
3218 .right-sidebar-collapsed-state {
3218 display: flex;
3219 display: flex;
@@ -3235,5 +3236,4 b' details:not([open]) > :not(summary) {'
3235
3236
3236 .old-comments-marker td {
3237 .old-comments-marker td {
3237 padding-top: 15px;
3238 padding-top: 15px;
3238 border-bottom: 1px solid @grey5;
3239 }
3239 }
@@ -115,11 +115,9 b' div.readme_box pre {'
115 div.readme_box img {
115 div.readme_box img {
116 border-style: none;
116 border-style: none;
117 background-color: #fff;
117 background-color: #fff;
118 padding-right: 20px;
119 max-width: 100%;
118 max-width: 100%;
120 }
119 }
121
120
122
123 div.readme_box strong {
121 div.readme_box strong {
124 font-weight: 600;
122 font-weight: 600;
125 margin: 0;
123 margin: 0;
@@ -152,6 +150,13 b' div.readme_box a:visited {'
152 }
150 }
153 */
151 */
154
152
153 div.readme_box p {
154 margin-top: 0;
155 margin-bottom: 16px;
156 padding: 0;
157 line-height: unset;
158 }
159
155
160
156 div.readme_box button {
161 div.readme_box button {
157 font-size: @basefontsize;
162 font-size: @basefontsize;
@@ -47,6 +47,8 b''
47
47
48 // Highlight color for lines and colors
48 // Highlight color for lines and colors
49 @comment-highlight-color: #ffd887;
49 @comment-highlight-color: #ffd887;
50 @color-draft: darken(@alert3, 30%);
51 @color-new: darken(@alert1, 5%);
50
52
51 // FONTS
53 // FONTS
52 @basefontsize: 13px;
54 @basefontsize: 13px;
@@ -248,6 +248,7 b' function registerRCRoutes() {'
248 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
248 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
251 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
251 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
254 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
@@ -386,6 +387,8 b' function registerRCRoutes() {'
386 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
389 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
390 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
391 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
389 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
392 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
390 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
393 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
391 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
394 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
@@ -71,14 +71,20 b' export class RhodecodeApp extends Polyme'
71 if (elem) {
71 if (elem) {
72 elem.handleNotification(data);
72 elem.handleNotification(data);
73 }
73 }
74
75 }
74 }
76
75
77 handleComment(data) {
76 handleComment(data) {
78 if (data.message.comment_id) {
77
78 if (data.message.comment_data.length !== 0) {
79 if (window.refreshAllComments !== undefined) {
79 if (window.refreshAllComments !== undefined) {
80 refreshAllComments()
80 refreshAllComments()
81 }
81 }
82 var json_data = data.message.comment_data;
83
84 if (window.commentsController !== undefined) {
85
86 window.commentsController.attachComment(json_data)
87 }
82 }
88 }
83 }
89 }
84
90
@@ -704,3 +704,13 b' var storeUserSessionAttr = function (key'
704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
705 return false;
705 return false;
706 };
706 };
707
708
709 var getUserSessionAttr = function(key) {
710 var storeKey = templateContext.session_attrs;
711 var val = storeKey[key]
712 if (val !== undefined) {
713 return JSON.parse(val)
714 }
715 return null
716 }
@@ -349,7 +349,12 b' var initCommentBoxCodeMirror = function('
349 };
349 };
350
350
351 var submitForm = function(cm, pred) {
351 var submitForm = function(cm, pred) {
352 $(cm.display.input.textarea.form).submit();
352 $(cm.display.input.textarea.form).find('.submit-comment-action').click();
353 return CodeMirror.Pass;
354 };
355
356 var submitFormAsDraft = function(cm, pred) {
357 $(cm.display.input.textarea.form).find('.submit-draft-action').click();
353 return CodeMirror.Pass;
358 return CodeMirror.Pass;
354 };
359 };
355
360
@@ -475,9 +480,11 b' var initCommentBoxCodeMirror = function('
475 // submit form on Meta-Enter
480 // submit form on Meta-Enter
476 if (OSType === "mac") {
481 if (OSType === "mac") {
477 extraKeys["Cmd-Enter"] = submitForm;
482 extraKeys["Cmd-Enter"] = submitForm;
483 extraKeys["Shift-Cmd-Enter"] = submitFormAsDraft;
478 }
484 }
479 else {
485 else {
480 extraKeys["Ctrl-Enter"] = submitForm;
486 extraKeys["Ctrl-Enter"] = submitForm;
487 extraKeys["Shift-Ctrl-Enter"] = submitFormAsDraft;
481 }
488 }
482
489
483 if (triggerActions) {
490 if (triggerActions) {
@@ -124,16 +124,20 b' var _submitAjaxPOST = function(url, post'
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 this.submitButtonText = this.submitButton.val();
129 this.submitButtonText = this.submitButton.val();
129
130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
130
133
131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
132 {'repo_name': templateContext.repo_name,
135 {'repo_name': templateContext.repo_name,
133 'commit_id': templateContext.commit_data.commit_id});
136 'commit_id': templateContext.commit_data.commit_id});
134
137
135 if (edit){
138 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
137 $(this.commentType).prop('disabled', true);
141 $(this.commentType).prop('disabled', true);
138 $(this.commentType).addClass('disabled');
142 $(this.commentType).addClass('disabled');
139 var editInfo =
143 var editInfo =
@@ -215,10 +219,17 b' var _submitAjaxPOST = function(url, post'
215 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
216 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
217 };
221 };
222
218 this.getCommentType = function() {
223 this.getCommentType = function() {
219 return $(this.submitForm).find(this.commentType).val();
224 return $(this.submitForm).find(this.commentType).val();
220 };
225 };
221
226
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
230 return data
231 }
232
222 this.getResolvesId = function() {
233 this.getResolvesId = function() {
223 return $(this.submitForm).find(this.resolvesId).val() || null;
234 return $(this.submitForm).find(this.resolvesId).val() || null;
224 };
235 };
@@ -233,7 +244,9 b' var _submitAjaxPOST = function(url, post'
233 };
244 };
234
245
235 this.isAllowedToSubmit = function() {
246 this.isAllowedToSubmit = function() {
236 return !$(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
237 };
250 };
238
251
239 this.initStatusChangeSelector = function(){
252 this.initStatusChangeSelector = function(){
@@ -259,11 +272,13 b' var _submitAjaxPOST = function(url, post'
259 dropdownAutoWidth: true,
272 dropdownAutoWidth: true,
260 minimumResultsForSearch: -1
273 minimumResultsForSearch: -1
261 });
274 });
275
262 $(this.submitForm).find(this.statusChange).on('change', function() {
276 $(this.submitForm).find(this.statusChange).on('change', function() {
263 var status = self.getCommentStatus();
277 var status = self.getCommentStatus();
264
278
265 if (status && !self.isInline()) {
279 if (status && !self.isInline()) {
266 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
267 }
282 }
268
283
269 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
@@ -295,10 +310,10 b' var _submitAjaxPOST = function(url, post'
295 $(this.statusChange).select2('readonly', false);
310 $(this.statusChange).select2('readonly', false);
296 };
311 };
297
312
298 this.globalSubmitSuccessCallback = function(){
313 this.globalSubmitSuccessCallback = function(comment){
299 // default behaviour is to call GLOBAL hook, if it's registered.
314 // default behaviour is to call GLOBAL hook, if it's registered.
300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
301 commentFormGlobalSubmitSuccessCallback();
316 commentFormGlobalSubmitSuccessCallback(comment);
302 }
317 }
303 };
318 };
304
319
@@ -321,6 +336,7 b' var _submitAjaxPOST = function(url, post'
321 var text = self.cm.getValue();
336 var text = self.cm.getValue();
322 var status = self.getCommentStatus();
337 var status = self.getCommentStatus();
323 var commentType = self.getCommentType();
338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
324 var resolvesCommentId = self.getResolvesId();
340 var resolvesCommentId = self.getResolvesId();
325 var closePullRequest = self.getClosePr();
341 var closePullRequest = self.getClosePr();
326
342
@@ -348,12 +364,15 b' var _submitAjaxPOST = function(url, post'
348 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
349 }
365 }
350
366
351 var submitSuccessCallback = function(o) {
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
352 // reload page if we change status for single commit.
369 // reload page if we change status for single commit.
353 if (status && self.commitId) {
370 if (status && self.commitId) {
354 location.reload(true);
371 location.reload(true);
355 } else {
372 } else {
356 $('#injected_page_comments').append(o.rendered_text);
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 self.attachGeneralComment(json_data)
375
357 self.resetCommentFormState();
376 self.resetCommentFormState();
358 timeagoActivate();
377 timeagoActivate();
359 tooltipActivate();
378 tooltipActivate();
@@ -365,7 +384,7 b' var _submitAjaxPOST = function(url, post'
365 }
384 }
366
385
367 // run global callback on submit
386 // run global callback on submit
368 self.globalSubmitSuccessCallback();
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
369
388
370 };
389 };
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
@@ -409,10 +428,20 b' var _submitAjaxPOST = function(url, post'
409 }
428 }
410
429
411 $(this.submitButton).prop('disabled', submitState);
430 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
432
412 if (submitEvent) {
433 if (submitEvent) {
413 $(this.submitButton).val(_gettext('Submitting...'));
434 var isDraft = self.getDraftState();
435
436 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
440 }
441
414 } else {
442 } else {
415 $(this.submitButton).val(this.submitButtonText);
443 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
416 }
445 }
417
446
418 };
447 };
@@ -488,6 +517,7 b' var _submitAjaxPOST = function(url, post'
488 if (!allowedToSubmit){
517 if (!allowedToSubmit){
489 return false;
518 return false;
490 }
519 }
520
491 self.handleFormSubmit();
521 self.handleFormSubmit();
492 });
522 });
493
523
@@ -538,26 +568,6 b' var CommentsController = function() {'
538 var mainComment = '#text';
568 var mainComment = '#text';
539 var self = this;
569 var self = this;
540
570
541 this.cancelComment = function (node) {
542 var $node = $(node);
543 var edit = $(this).attr('edit');
544 if (edit) {
545 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
552 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
554 }
555 $comment.show();
556 }
557 $node.closest('.comment-inline-form').remove();
558 return false;
559 };
560
561 this.showVersion = function (comment_id, comment_history_id) {
571 this.showVersion = function (comment_id, comment_history_id) {
562
572
563 var historyViewUrl = pyroutes.url(
573 var historyViewUrl = pyroutes.url(
@@ -655,12 +665,51 b' var CommentsController = function() {'
655 return self.scrollToComment(node, -1, true);
665 return self.scrollToComment(node, -1, true);
656 };
666 };
657
667
668 this.cancelComment = function (node) {
669 var $node = $(node);
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
672
673 if (edit) {
674 var $general_comments = null;
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
683 $comment.show();
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
691 }
692
693 $node.closest('.comment-inline-form').remove();
694 return false;
695 };
696
658 this._deleteComment = function(node) {
697 this._deleteComment = function(node) {
659 var $node = $(node);
698 var $node = $(node);
660 var $td = $node.closest('td');
699 var $td = $node.closest('td');
661 var $comment = $node.closest('.comment');
700 var $comment = $node.closest('.comment');
662 var comment_id = $comment.attr('data-comment-id');
701 var comment_id = $($comment).data('commentId');
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
702 var isDraft = $($comment).data('commentDraft');
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
706
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
712
664 var postData = {
713 var postData = {
665 'csrf_token': CSRF_TOKEN
714 'csrf_token': CSRF_TOKEN
666 };
715 };
@@ -677,10 +726,14 b' var CommentsController = function() {'
677 updateSticky()
726 updateSticky()
678 }
727 }
679
728
680 if (window.refreshAllComments !== undefined) {
729 if (window.refreshAllComments !== undefined && !isDraft) {
681 // if we have this handler, run it, and refresh all comments boxes
730 // if we have this handler, run it, and refresh all comments boxes
682 refreshAllComments()
731 refreshAllComments()
683 }
732 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
736 }
684 return false;
737 return false;
685 };
738 };
686
739
@@ -695,8 +748,6 b' var CommentsController = function() {'
695 };
748 };
696 ajaxPOST(url, postData, success, failure);
749 ajaxPOST(url, postData, success, failure);
697
750
698
699
700 }
751 }
701
752
702 this.deleteComment = function(node) {
753 this.deleteComment = function(node) {
@@ -716,7 +767,59 b' var CommentsController = function() {'
716 })
767 })
717 };
768 };
718
769
770 this._finalizeDrafts = function(commentIds) {
771
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 var commitId = templateContext.commit_data.commit_id;
774
775 if (pullRequestId) {
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 } else if (commitId) {
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 }
780
781 // remove the drafts so we can lock them before submit.
782 $.each(commentIds, function(idx, val){
783 $('#comment-{0}'.format(val)).remove();
784 })
785
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787
788 var submitSuccessCallback = function(json_data) {
789 self.attachInlineComment(json_data);
790
791 if (window.refreshDraftComments !== undefined) {
792 // if we have this handler, run it, and refresh all comments boxes
793 refreshDraftComments()
794 }
795
796 return false;
797 };
798
799 ajaxPOST(url, postData, submitSuccessCallback)
800
801 }
802
803 this.finalizeDrafts = function(commentIds, callback) {
804
805 SwalNoAnimation.fire({
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 icon: 'warning',
808 showCancelButton: true,
809 confirmButtonText: _gettext('Yes'),
810
811 }).then(function(result) {
812 if (result.value) {
813 if (callback !== undefined) {
814 callback(result)
815 }
816 self._finalizeDrafts(commentIds);
817 }
818 })
819 };
820
719 this.toggleWideMode = function (node) {
821 this.toggleWideMode = function (node) {
822
720 if ($('#content').hasClass('wrapper')) {
823 if ($('#content').hasClass('wrapper')) {
721 $('#content').removeClass("wrapper");
824 $('#content').removeClass("wrapper");
722 $('#content').addClass("wide-mode-wrapper");
825 $('#content').addClass("wide-mode-wrapper");
@@ -731,16 +834,49 b' var CommentsController = function() {'
731
834
732 };
835 };
733
836
734 this.toggleComments = function(node, show) {
837 /**
838 * Turn off/on all comments in file diff
839 */
840 this.toggleDiffComments = function(node) {
841 // Find closes filediff container
735 var $filediff = $(node).closest('.filediff');
842 var $filediff = $(node).closest('.filediff');
843 if ($(node).hasClass('toggle-on')) {
844 var show = false;
845 } else if ($(node).hasClass('toggle-off')) {
846 var show = true;
847 }
848
849 // Toggle each individual comment block, so we can un-toggle single ones
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 self.toggleLineComments($(val), show)
852 })
853
854 // since we change the height of the diff container that has anchor points for upper
855 // sticky header, we need to tell it to re-calculate those
856 if (window.updateSticky !== undefined) {
857 // potentially our comments change the active window size, so we
858 // notify sticky elements
859 updateSticky()
860 }
861
862 return false;
863 }
864
865 this.toggleLineComments = function(node, show) {
866
867 var trElem = $(node).closest('tr')
868
736 if (show === true) {
869 if (show === true) {
737 $filediff.removeClass('hide-comments');
870 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
872 $(trElem).removeClass('hide-line-comments');
738 } else if (show === false) {
873 } else if (show === false) {
739 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
874 $(trElem).find('.comment-outdated').hide();
740 $filediff.addClass('hide-comments');
875 $(trElem).addClass('hide-line-comments');
741 } else {
876 } else {
742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
877 // mark outdated comments as visible before the toggle;
743 $filediff.toggleClass('hide-comments');
878 $(trElem).find('.comment-outdated').show();
879 $(trElem).toggleClass('hide-line-comments');
744 }
880 }
745
881
746 // since we change the height of the diff container that has anchor points for upper
882 // since we change the height of the diff container that has anchor points for upper
@@ -751,15 +887,6 b' var CommentsController = function() {'
751 updateSticky()
887 updateSticky()
752 }
888 }
753
889
754 return false;
755 };
756
757 this.toggleLineComments = function(node) {
758 self.toggleComments(node, true);
759 var $node = $(node);
760 // mark outdated comments as visible before the toggle;
761 $(node.closest('tr')).find('.comment-outdated').show();
762 $node.closest('tr').toggleClass('hide-line-comments');
763 };
890 };
764
891
765 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
@@ -913,63 +1040,58 b' var CommentsController = function() {'
913 return commentForm;
1040 return commentForm;
914 };
1041 };
915
1042
916 this.editComment = function(node) {
1043 this.editComment = function(node, line_no, f_path) {
1044 self.edit = true;
917 var $node = $(node);
1045 var $node = $(node);
1046 var $td = $node.closest('td');
1047
918 var $comment = $(node).closest('.comment');
1048 var $comment = $(node).closest('.comment');
919 var comment_id = $comment.attr('data-comment-id');
1049 var comment_id = $($comment).data('commentId');
920 var $form = null
1050 var isDraft = $($comment).data('commentDraft');
1051 var $editForm = null
921
1052
922 var $comments = $node.closest('div.inline-comments');
1053 var $comments = $node.closest('div.inline-comments');
923 var $general_comments = null;
1054 var $general_comments = null;
924 var lineno = null;
925
1055
926 if($comments.length){
1056 if($comments.length){
927 // inline comments setup
1057 // inline comments setup
928 $form = $comments.find('.comment-inline-form');
1058 $editForm = $comments.find('.comment-inline-form');
929 lineno = self.getLineNumber(node)
1059 line_no = self.getLineNumber(node)
930 }
1060 }
931 else{
1061 else{
932 // general comments setup
1062 // general comments setup
933 $comments = $('#comments');
1063 $comments = $('#comments');
934 $form = $comments.find('.comment-inline-form');
1064 $editForm = $comments.find('.comment-inline-form');
935 lineno = $comment[0].id
1065 line_no = $comment[0].id
936 $('#cb-comment-general-form-placeholder').hide();
1066 $('#cb-comment-general-form-placeholder').hide();
937 }
1067 }
938
1068
939 this.edit = true;
1069 if ($editForm.length === 0) {
940
1070
941 if (!$form.length) {
1071 // unhide all comments if they are hidden for a proper REPLY mode
942
943 var $filediff = $node.closest('.filediff');
1072 var $filediff = $node.closest('.filediff');
944 $filediff.removeClass('hide-comments');
1073 $filediff.removeClass('hide-comments');
945 var f_path = $filediff.attr('data-f-path');
946
947 // create a new HTML from template
948
1074
949 var tmpl = $('#cb-comment-inline-form-template').html();
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
950 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1076 if(f_path && line_no) {
951 $form = $(tmpl);
1077 $editForm.addClass('comment-inline-form-edit')
952 $comment.after($form)
1078 }
953
1079
954 var _form = $($form[0]).find('form');
1080 $comment.after($editForm)
1081
1082 var _form = $($editForm[0]).find('form');
955 var autocompleteActions = ['as_note',];
1083 var autocompleteActions = ['as_note',];
956 var commentForm = this.createCommentForm(
1084 var commentForm = this.createCommentForm(
957 _form, lineno, '', autocompleteActions, resolvesCommentId,
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
958 this.edit, comment_id);
1086 this.edit, comment_id);
959 var old_comment_text_binary = $comment.attr('data-comment-text');
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
960 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
961 commentForm.cm.setValue(old_comment_text);
1089 commentForm.cm.setValue(old_comment_text);
962 $comment.hide();
1090 $comment.hide();
1091 tooltipActivate();
963
1092
964 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1093 // set a CUSTOM submit handler for inline comment edit action.
965 form: _form,
1094 commentForm.setHandleFormSubmit(function(o) {
966 parent: $comments,
967 lineno: lineno,
968 f_path: f_path}
969 );
970
971 // set a CUSTOM submit handler for inline comments.
972 commentForm.setHandleFormSubmit(function(o) {
973 var text = commentForm.cm.getValue();
1095 var text = commentForm.cm.getValue();
974 var commentType = commentForm.getCommentType();
1096 var commentType = commentForm.getCommentType();
975
1097
@@ -1000,14 +1122,15 b' var CommentsController = function() {'
1000 var postData = {
1122 var postData = {
1001 'text': text,
1123 'text': text,
1002 'f_path': f_path,
1124 'f_path': f_path,
1003 'line': lineno,
1125 'line': line_no,
1004 'comment_type': commentType,
1126 'comment_type': commentType,
1127 'draft': isDraft,
1005 'version': version,
1128 'version': version,
1006 'csrf_token': CSRF_TOKEN
1129 'csrf_token': CSRF_TOKEN
1007 };
1130 };
1008
1131
1009 var submitSuccessCallback = function(json_data) {
1132 var submitSuccessCallback = function(json_data) {
1010 $form.remove();
1133 $editForm.remove();
1011 $comment.show();
1134 $comment.show();
1012 var postData = {
1135 var postData = {
1013 'text': text,
1136 'text': text,
@@ -1072,8 +1195,7 b' var CommentsController = function() {'
1072 'commit_id': templateContext.commit_data.commit_id});
1195 'commit_id': templateContext.commit_data.commit_id});
1073
1196
1074 _submitAjaxPOST(
1197 _submitAjaxPOST(
1075 previewUrl, postData, successRenderCommit,
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1076 failRenderCommit
1077 );
1199 );
1078
1200
1079 try {
1201 try {
@@ -1084,7 +1206,7 b' var CommentsController = function() {'
1084 $comments.find('.cb-comment-add-button').before(html);
1206 $comments.find('.cb-comment-add-button').before(html);
1085
1207
1086 // run global callback on submit
1208 // run global callback on submit
1087 commentForm.globalSubmitSuccessCallback();
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1088
1210
1089 } catch (e) {
1211 } catch (e) {
1090 console.error(e);
1212 console.error(e);
@@ -1101,10 +1223,14 b' var CommentsController = function() {'
1101 updateSticky()
1223 updateSticky()
1102 }
1224 }
1103
1225
1104 if (window.refreshAllComments !== undefined) {
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1105 // if we have this handler, run it, and refresh all comments boxes
1227 // if we have this handler, run it, and refresh all comments boxes
1106 refreshAllComments()
1228 refreshAllComments()
1107 }
1229 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1233 }
1108
1234
1109 commentForm.setActionButtonsDisabled(false);
1235 commentForm.setActionButtonsDisabled(false);
1110
1236
@@ -1129,66 +1255,122 b' var CommentsController = function() {'
1129 });
1255 });
1130 }
1256 }
1131
1257
1132 $form.addClass('comment-inline-form-open');
1258 $editForm.addClass('comment-inline-form-open');
1133 };
1259 };
1134
1260
1135 this.createComment = function(node, resolutionComment) {
1261 this.attachComment = function(json_data) {
1136 var resolvesCommentId = resolutionComment || null;
1262 var self = this;
1263 $.each(json_data, function(idx, val) {
1264 var json_data_elem = [val]
1265 var isInline = val.comment_f_path && val.comment_lineno
1266
1267 if (isInline) {
1268 self.attachInlineComment(json_data_elem)
1269 } else {
1270 self.attachGeneralComment(json_data_elem)
1271 }
1272 })
1273
1274 }
1275
1276 this.attachGeneralComment = function(json_data) {
1277 $.each(json_data, function(idx, val) {
1278 $('#injected_page_comments').append(val.rendered_text);
1279 })
1280 }
1281
1282 this.attachInlineComment = function(json_data) {
1283
1284 $.each(json_data, function (idx, val) {
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 var html = val.rendered_text;
1287 var $inlineComments = $('#' + val.target_id)
1288 .find(line_qry)
1289 .find('.inline-comments');
1290
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292
1293 if (lastComment.length === 0) {
1294 // first comment, we append simply
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 } else {
1297 $(lastComment).after(html)
1298 }
1299
1300 })
1301
1302 };
1303
1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 // create a new reply HTML form from template
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 return $(tmpl);
1309 }
1310
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 self.edit = false;
1137 var $node = $(node);
1313 var $node = $(node);
1138 var $td = $node.closest('td');
1314 var $td = $node.closest('td');
1139 var $form = $td.find('.comment-inline-form');
1315 var resolvesCommentId = resolutionComment || null;
1140 this.edit = false;
1141
1316
1142 if (!$form.length) {
1317 var $replyForm = $td.find('.comment-inline-form');
1143
1318
1144 var $filediff = $node.closest('.filediff');
1319 // if form isn't existing, we're generating a new one and injecting it.
1145 $filediff.removeClass('hide-comments');
1320 if ($replyForm.length === 0) {
1146 var f_path = $filediff.attr('data-f-path');
1321
1147 var lineno = self.getLineNumber(node);
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1148 // create a new HTML from template
1323 self.toggleLineComments($node, true);
1149 var tmpl = $('#cb-comment-inline-form-template').html();
1324
1150 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1151 $form = $(tmpl);
1152
1326
1153 var $comments = $td.find('.inline-comments');
1327 var $comments = $td.find('.inline-comments');
1154 if (!$comments.length) {
1328
1155 $comments = $(
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1156 $('#cb-comments-inline-container-template').html());
1330 if ($comments.length===0) {
1157 $td.append($comments);
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 $td.append($($reply_container).html());
1158 }
1335 }
1159
1336
1160 $td.find('.cb-comment-add-button').before($form);
1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 // set marker, that we have a open form
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 $replyWrapper.addClass('comment-form-active');
1161
1342
1162 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1343 var lastComment = $comments.find('.comment-inline').last();
1163 var _form = $($form[0]).find('form');
1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 $replyWrapper.show();
1346 }
1347
1348 var _form = $($replyForm[0]).find('form');
1164 var autocompleteActions = ['as_note', 'as_todo'];
1349 var autocompleteActions = ['as_note', 'as_todo'];
1165 var comment_id=null;
1350 var comment_id=null;
1166 var commentForm = this.createCommentForm(
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1167 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1352 var commentForm = self.createCommentForm(
1168
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1169 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1354 self.edit, comment_id);
1170 form: _form,
1171 parent: $td[0],
1172 lineno: lineno,
1173 f_path: f_path}
1174 );
1175
1355
1176 // set a CUSTOM submit handler for inline comments.
1356 // set a CUSTOM submit handler for inline comments.
1177 commentForm.setHandleFormSubmit(function(o) {
1357 commentForm.setHandleFormSubmit(function(o) {
1178 var text = commentForm.cm.getValue();
1358 var text = commentForm.cm.getValue();
1179 var commentType = commentForm.getCommentType();
1359 var commentType = commentForm.getCommentType();
1180 var resolvesCommentId = commentForm.getResolvesId();
1360 var resolvesCommentId = commentForm.getResolvesId();
1361 var isDraft = commentForm.getDraftState();
1181
1362
1182 if (text === "") {
1363 if (text === "") {
1183 return;
1364 return;
1184 }
1365 }
1185
1366
1186 if (lineno === undefined) {
1367 if (line_no === undefined) {
1187 alert('missing line !');
1368 alert('Error: unable to fetch line number for this inline comment !');
1188 return;
1369 return;
1189 }
1370 }
1371
1190 if (f_path === undefined) {
1372 if (f_path === undefined) {
1191 alert('missing file path !');
1373 alert('Error: unable to fetch file path for this inline comment !');
1192 return;
1374 return;
1193 }
1375 }
1194
1376
@@ -1199,66 +1381,79 b' var CommentsController = function() {'
1199 var postData = {
1381 var postData = {
1200 'text': text,
1382 'text': text,
1201 'f_path': f_path,
1383 'f_path': f_path,
1202 'line': lineno,
1384 'line': line_no,
1203 'comment_type': commentType,
1385 'comment_type': commentType,
1386 'draft': isDraft,
1204 'csrf_token': CSRF_TOKEN
1387 'csrf_token': CSRF_TOKEN
1205 };
1388 };
1206 if (resolvesCommentId){
1389 if (resolvesCommentId){
1207 postData['resolves_comment_id'] = resolvesCommentId;
1390 postData['resolves_comment_id'] = resolvesCommentId;
1208 }
1391 }
1209
1392
1393 // submitSuccess for inline commits
1210 var submitSuccessCallback = function(json_data) {
1394 var submitSuccessCallback = function(json_data) {
1211 $form.remove();
1395
1212 try {
1396 $replyForm.remove();
1213 var html = json_data.rendered_text;
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1214 var lineno = json_data.line_no;
1398
1215 var target_id = json_data.target_id;
1399 try {
1400
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 self.attachInlineComment(json_data)
1216
1403
1217 $comments.find('.cb-comment-add-button').before(html);
1404 //mark visually which comment was resolved
1405 if (resolvesCommentId) {
1406 commentForm.markCommentResolved(resolvesCommentId);
1407 }
1218
1408
1219 //mark visually which comment was resolved
1409 // run global callback on submit
1220 if (resolvesCommentId) {
1410 commentForm.globalSubmitSuccessCallback({
1221 commentForm.markCommentResolved(resolvesCommentId);
1411 draft: isDraft,
1412 comment_id: comment_id
1413 });
1414
1415 } catch (e) {
1416 console.error(e);
1222 }
1417 }
1223
1418
1224 // run global callback on submit
1225 commentForm.globalSubmitSuccessCallback();
1226
1227 } catch (e) {
1228 console.error(e);
1229 }
1230
1231 // re trigger the linkification of next/prev navigation
1232 linkifyComments($('.inline-comment-injected'));
1233 timeagoActivate();
1234 tooltipActivate();
1235
1236 if (window.updateSticky !== undefined) {
1419 if (window.updateSticky !== undefined) {
1237 // potentially our comments change the active window size, so we
1420 // potentially our comments change the active window size, so we
1238 // notify sticky elements
1421 // notify sticky elements
1239 updateSticky()
1422 updateSticky()
1240 }
1423 }
1241
1424
1242 if (window.refreshAllComments !== undefined) {
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1243 // if we have this handler, run it, and refresh all comments boxes
1426 // if we have this handler, run it, and refresh all comments boxes
1244 refreshAllComments()
1427 refreshAllComments()
1245 }
1428 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1432 }
1246
1433
1247 commentForm.setActionButtonsDisabled(false);
1434 commentForm.setActionButtonsDisabled(false);
1248
1435
1436 // re trigger the linkification of next/prev navigation
1437 linkifyComments($('.inline-comment-injected'));
1438 timeagoActivate();
1439 tooltipActivate();
1249 };
1440 };
1441
1250 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1251 var prefix = "Error while submitting comment.\n"
1443 var prefix = "Error while submitting comment.\n"
1252 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1253 ajaxErrorSwal(message);
1445 ajaxErrorSwal(message);
1254 commentForm.resetCommentFormState(text)
1446 commentForm.resetCommentFormState(text)
1255 };
1447 };
1448
1256 commentForm.submitAjaxPOST(
1449 commentForm.submitAjaxPOST(
1257 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1258 });
1451 });
1259 }
1452 }
1260
1453
1261 $form.addClass('comment-inline-form-open');
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 $replyForm.addClass('comment-inline-form-open');
1456 tooltipActivate();
1262 };
1457 };
1263
1458
1264 this.createResolutionComment = function(commentId){
1459 this.createResolutionComment = function(commentId){
@@ -1268,9 +1463,12 b' var CommentsController = function() {'
1268 var comment = $('#comment-'+commentId);
1463 var comment = $('#comment-'+commentId);
1269 var commentData = comment.data();
1464 var commentData = comment.data();
1270 if (commentData.commentInline) {
1465 if (commentData.commentInline) {
1271 this.createComment(comment, commentId)
1466 var f_path = commentData.fPath;
1467 var line_no = commentData.lineNo;
1468 //TODO check this if we need to give f_path/line_no
1469 this.createComment(comment, f_path, line_no, commentId)
1272 } else {
1470 } else {
1273 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1471 this.createGeneralComment('general', "$placeholder", commentId)
1274 }
1472 }
1275
1473
1276 return false;
1474 return false;
@@ -1296,3 +1494,8 b' var CommentsController = function() {'
1296 };
1494 };
1297
1495
1298 };
1496 };
1497
1498 window.commentHelp = function(renderer) {
1499 var funcData = {'renderer': renderer}
1500 return renderTemplate('commentHelpHovercard', funcData)
1501 } No newline at end of file
@@ -42,12 +42,22 b' window.toggleElement = function (elem, t'
42 var $elem = $(elem);
42 var $elem = $(elem);
43 var $target = $(target);
43 var $target = $(target);
44
44
45 if ($target.is(':visible') || $target.length === 0) {
45 if (target !== undefined) {
46 var show = $target.is(':visible') || $target.length === 0;
47 } else {
48 var show = $elem.hasClass('toggle-off')
49 }
50
51 if (show) {
46 $target.hide();
52 $target.hide();
47 $elem.html($elem.data('toggleOn'))
53 $elem.html($elem.data('toggleOn'))
54 $elem.addClass('toggle-on')
55 $elem.removeClass('toggle-off')
48 } else {
56 } else {
49 $target.show();
57 $target.show();
50 $elem.html($elem.data('toggleOff'))
58 $elem.html($elem.data('toggleOff'))
59 $elem.addClass('toggle-off')
60 $elem.removeClass('toggle-on')
51 }
61 }
52
62
53 return false
63 return false
@@ -182,83 +182,34 b' window.ReviewersController = function ()'
182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 // default rule, case for older repo that don't have any rules stored
183 // default rule, case for older repo that don't have any rules stored
184 self.$rulesList.append(
184 self.$rulesList.append(
185 self.addRule(
185 self.addRule(_gettext('All reviewers must vote.'))
186 _gettext('All reviewers must vote.'))
187 );
186 );
188 return self.forbidUsers
187 return self.forbidUsers
189 }
188 }
190
189
191 if (data.rules.voting !== undefined) {
190 if (data.rules.forbid_adding_reviewers) {
192 if (data.rules.voting < 0) {
191 $('#add_reviewer_input').remove();
193 self.$rulesList.append(
194 self.addRule(
195 _gettext('All individual reviewers must vote.'))
196 )
197 } else if (data.rules.voting === 1) {
198 self.$rulesList.append(
199 self.addRule(
200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
201 )
202
203 } else {
204 self.$rulesList.append(
205 self.addRule(
206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
207 )
208 }
209 }
192 }
210
193
211 if (data.rules.voting_groups !== undefined) {
194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
212 $.each(data.rules.voting_groups, function (index, rule_data) {
195 $.each(data.rules_data.forbidden_users, function(idx, val){
213 self.$rulesList.append(
196 self.forbidUsers.push(val)
214 self.addRule(rule_data.text)
197 })
215 )
216 });
217 }
218
219 if (data.rules.use_code_authors_for_review) {
220 self.$rulesList.append(
221 self.addRule(
222 _gettext('Reviewers picked from source code changes.'))
223 )
224 }
198 }
225
199
226 if (data.rules.forbid_adding_reviewers) {
200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
227 $('#add_reviewer_input').remove();
201 $.each(data.rules_humanized, function(idx, val) {
202 self.$rulesList.append(
203 self.addRule(val)
204 )
205 })
206 } else {
207 // we don't have any rules set, so we inform users about it
228 self.$rulesList.append(
208 self.$rulesList.append(
229 self.addRule(
209 self.addRule(_gettext('No additional review rules set.'))
230 _gettext('Adding new reviewers is forbidden.'))
231 )
232 }
233
234 if (data.rules.forbid_author_to_review) {
235 self.forbidUsers.push(data.rules_data.pr_author);
236 self.$rulesList.append(
237 self.addRule(
238 _gettext('Author is not allowed to be a reviewer.'))
239 )
210 )
240 }
211 }
241
212
242 if (data.rules.forbid_commit_author_to_review) {
243
244 if (data.rules_data.forbidden_users) {
245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
246 self.forbidUsers.push(member_data)
247 });
248 }
249
250 self.$rulesList.append(
251 self.addRule(
252 _gettext('Commit Authors are not allowed to be a reviewer.'))
253 )
254 }
255
256 // we don't have any rules set, so we inform users about it
257 if (self.enabledRules.length === 0) {
258 self.addRule(
259 _gettext('No review rules set.'))
260 }
261
262 return self.forbidUsers
213 return self.forbidUsers
263 };
214 };
264
215
@@ -1066,14 +1017,14 b' window.ReviewerPresenceController = func'
1066 this.handlePresence = function (data) {
1017 this.handlePresence = function (data) {
1067 if (data.type == 'presence' && data.channel === self.channel) {
1018 if (data.type == 'presence' && data.channel === self.channel) {
1068 this.storeUsers(data.users);
1019 this.storeUsers(data.users);
1069 this.render()
1020 this.render();
1070 }
1021 }
1071 };
1022 };
1072
1023
1073 this.handleChannelUpdate = function (data) {
1024 this.handleChannelUpdate = function (data) {
1074 if (data.channel === this.channel) {
1025 if (data.channel === this.channel) {
1075 this.storeUsers(data.state.users);
1026 this.storeUsers(data.state.users);
1076 this.render()
1027 this.render();
1077 }
1028 }
1078
1029
1079 };
1030 };
@@ -1085,6 +1036,30 b' window.ReviewerPresenceController = func'
1085
1036
1086 };
1037 };
1087
1038
1039 window.refreshCommentsSuccess = function(targetNode, counterNode, extraCallback) {
1040 var $targetElem = targetNode;
1041 var $counterElem = counterNode;
1042
1043 return function (data) {
1044 var newCount = $(data).data('counter');
1045 if (newCount !== undefined) {
1046 var callback = function () {
1047 $counterElem.animate({'opacity': 1.00}, 200)
1048 $counterElem.html(newCount);
1049 };
1050 $counterElem.animate({'opacity': 0.15}, 200, callback);
1051 }
1052
1053 $targetElem.css('opacity', 1);
1054 $targetElem.html(data);
1055 tooltipActivate();
1056
1057 if (extraCallback !== undefined) {
1058 extraCallback(data)
1059 }
1060 }
1061 }
1062
1088 window.refreshComments = function (version) {
1063 window.refreshComments = function (version) {
1089 version = version || templateContext.pull_request_data.pull_request_version || '';
1064 version = version || templateContext.pull_request_data.pull_request_version || '';
1090
1065
@@ -1109,23 +1084,8 b' window.refreshComments = function (versi'
1109
1084
1110 var $targetElem = $('.comments-content-table');
1085 var $targetElem = $('.comments-content-table');
1111 $targetElem.css('opacity', 0.3);
1086 $targetElem.css('opacity', 0.3);
1112
1087 var $counterElem = $('#comments-count');
1113 var success = function (data) {
1088 var success = refreshCommentsSuccess($targetElem, $counterElem);
1114 var $counterElem = $('#comments-count');
1115 var newCount = $(data).data('counter');
1116 if (newCount !== undefined) {
1117 var callback = function () {
1118 $counterElem.animate({'opacity': 1.00}, 200)
1119 $counterElem.html(newCount);
1120 };
1121 $counterElem.animate({'opacity': 0.15}, 200, callback);
1122 }
1123
1124 $targetElem.css('opacity', 1);
1125 $targetElem.html(data);
1126 tooltipActivate();
1127 }
1128
1129 ajaxPOST(loadUrl, data, success, null, {})
1089 ajaxPOST(loadUrl, data, success, null, {})
1130
1090
1131 }
1091 }
@@ -1139,7 +1099,7 b' window.refreshTODOs = function (version)'
1139 'repo_name': templateContext.repo_name,
1099 'repo_name': templateContext.repo_name,
1140 'version': version,
1100 'version': version,
1141 };
1101 };
1142 var loadUrl = pyroutes.url('pullrequest_comments', params);
1102 var loadUrl = pyroutes.url('pullrequest_todos', params);
1143 } // commit case
1103 } // commit case
1144 else {
1104 else {
1145 return
1105 return
@@ -1153,27 +1113,46 b' window.refreshTODOs = function (version)'
1153 var data = {"comments": currentIDs};
1113 var data = {"comments": currentIDs};
1154 var $targetElem = $('.todos-content-table');
1114 var $targetElem = $('.todos-content-table');
1155 $targetElem.css('opacity', 0.3);
1115 $targetElem.css('opacity', 0.3);
1156
1116 var $counterElem = $('#todos-count');
1157 var success = function (data) {
1117 var success = refreshCommentsSuccess($targetElem, $counterElem);
1158 var $counterElem = $('#todos-count')
1159 var newCount = $(data).data('counter');
1160 if (newCount !== undefined) {
1161 var callback = function () {
1162 $counterElem.animate({'opacity': 1.00}, 200)
1163 $counterElem.html(newCount);
1164 };
1165 $counterElem.animate({'opacity': 0.15}, 200, callback);
1166 }
1167
1168 $targetElem.css('opacity', 1);
1169 $targetElem.html(data);
1170 tooltipActivate();
1171 }
1172
1118
1173 ajaxPOST(loadUrl, data, success, null, {})
1119 ajaxPOST(loadUrl, data, success, null, {})
1174
1120
1175 }
1121 }
1176
1122
1123 window.refreshDraftComments = function () {
1124
1125 // Pull request case
1126 if (templateContext.pull_request_data.pull_request_id !== null) {
1127 var params = {
1128 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1129 'repo_name': templateContext.repo_name,
1130 };
1131 var loadUrl = pyroutes.url('pullrequest_drafts', params);
1132 } // commit case
1133 else {
1134 return
1135 }
1136
1137 var data = {};
1138
1139 var $targetElem = $('.drafts-content-table');
1140 $targetElem.css('opacity', 0.3);
1141 var $counterElem = $('#drafts-count');
1142 var extraCallback = function(data) {
1143 if ($(data).data('counter') == 0){
1144 $('#draftsTable').hide();
1145 } else {
1146 $('#draftsTable').show();
1147 }
1148 // uncheck on load the select all checkbox
1149 $('[name=select_all_drafts]').prop('checked', 0);
1150 }
1151 var success = refreshCommentsSuccess($targetElem, $counterElem, extraCallback);
1152
1153 ajaxPOST(loadUrl, data, success, null, {})
1154 };
1155
1177 window.refreshAllComments = function (version) {
1156 window.refreshAllComments = function (version) {
1178 version = version || templateContext.pull_request_data.pull_request_version || '';
1157 version = version || templateContext.pull_request_data.pull_request_version || '';
1179
1158
@@ -1,7 +1,5 b''
1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
3 /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
4 /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
5 /notifications - shows new event notifications
3 /notifications - shows new event notifications
6 /connection_controller/subscribe - subscribes user to new channels
4 /connection_controller/subscribe - subscribes user to new channels
7 /connection_controller/presence - receives presence change messages
5 /connection_controller/presence - receives presence change messages
@@ -104,12 +104,6 b' def add_request_user_context(event):'
104 request.environ['rc_req_id'] = req_id
104 request.environ['rc_req_id'] = req_id
105
105
106
106
107 def inject_app_settings(event):
108 settings = event.app.registry.settings
109 # inject info about available permissions
110 auth.set_available_permissions(settings)
111
112
113 def scan_repositories_if_enabled(event):
107 def scan_repositories_if_enabled(event):
114 """
108 """
115 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
109 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
@@ -46,6 +46,8 b''
46 $pullRequestListTable.DataTable({
46 $pullRequestListTable.DataTable({
47 processing: true,
47 processing: true,
48 serverSide: true,
48 serverSide: true,
49 stateSave: true,
50 stateDuration: -1,
49 ajax: {
51 ajax: {
50 "url": "${h.route_path('my_account_pullrequests_data')}",
52 "url": "${h.route_path('my_account_pullrequests_data')}",
51 "data": function (d) {
53 "data": function (d) {
@@ -119,6 +121,10 b''
119 if (data['owned']) {
121 if (data['owned']) {
120 $(row).addClass('owned');
122 $(row).addClass('owned');
121 }
123 }
124 },
125 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
122 }
128 }
123 });
129 });
124 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
@@ -129,6 +135,7 b''
129 $pullRequestListTable.css('opacity', 0.3);
135 $pullRequestListTable.css('opacity', 0.3);
130 });
136 });
131
137
138
132 // filter
139 // filter
133 $('#q_filter').on('keyup',
140 $('#q_filter').on('keyup',
134 $.debounce(250, function () {
141 $.debounce(250, function () {
@@ -1225,6 +1225,14 b''
1225 (function () {
1225 (function () {
1226 "use sctrict";
1226 "use sctrict";
1227
1227
1228 // details block auto-hide menu
1229 $(document).mouseup(function(e) {
1230 var container = $('.details-inline-block');
1231 if (!container.is(e.target) && container.has(e.target).length === 0) {
1232 $('.details-inline-block[open]').removeAttr('open')
1233 }
1234 });
1235
1228 var $sideBar = $('.right-sidebar');
1236 var $sideBar = $('.right-sidebar');
1229 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1237 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1230 var sidebarState = templateContext.session_attrs.sidebarState;
1238 var sidebarState = templateContext.session_attrs.sidebarState;
@@ -4,7 +4,7 b''
4 ## ${sidebar.comments_table()}
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
5 <%namespace name="base" file="/base/base.mako"/>
6
6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
7 <%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
8 <%
8 <%
9 if todo_comments:
9 if todo_comments:
10 cls_ = 'todos-content-table'
10 cls_ = 'todos-content-table'
@@ -15,10 +15,13 b''
15 # own comments first
15 # own comments first
16 user_id = 0
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 elif draft_comments:
19 cls_ = 'drafts-content-table'
20 def sorter(entry):
21 return '{}'.format(str(entry.comment_id).zfill(10000))
18 else:
22 else:
19 cls_ = 'comments-content-table'
23 cls_ = 'comments-content-table'
20 def sorter(entry):
24 def sorter(entry):
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
25 return '{}'.format(str(entry.comment_id).zfill(10000))
23
26
24 existing_ids = existing_ids or []
27 existing_ids = existing_ids or []
@@ -31,8 +34,12 b''
31 <%
34 <%
32 display = ''
35 display = ''
33 _cls = ''
36 _cls = ''
37 ## Extra precaution to not show drafts in the sidebar for todo/comments
38 if comment_obj.draft and not draft_comments:
39 continue
34 %>
40 %>
35
41
42
36 <%
43 <%
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 prev_comment_ver_index = 0
45 prev_comment_ver_index = 0
@@ -83,6 +90,11 b''
83 % endif
90 % endif
84
91
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
92 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
93 % if draft_comments:
94 <td style="width: 15px;">
95 ${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
96 </td>
97 % endif
86 <td class="td-todo-number">
98 <td class="td-todo-number">
87 <%
99 <%
88 version_info = ''
100 version_info = ''
@@ -24,8 +24,6 b''
24
24
25 <%def name="main()">
25 <%def name="main()">
26 <script type="text/javascript">
26 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
29 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
27 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
30 </script>
28 </script>
31
29
@@ -1,4 +1,4 b''
1 ## this is a dummy html file for partial rendering on server and sending
1 ## this is a dummy html file for partial rendering on server and sending
2 ## generated output via ajax after comment submit
2 ## generated output via ajax after comment submit
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=c.co.is_inline)}
4 ${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}
@@ -3,20 +3,25 b''
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6
7
7 <%!
8 <%!
8 from rhodecode.lib import html_filters
9 from rhodecode.lib import html_filters
9 %>
10 %>
10
11
11 <%namespace name="base" file="/base/base.mako"/>
12
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
13
14
14 <%
15 <%
15 from rhodecode.model.comment import CommentsModel
16 from rhodecode.model.comment import CommentsModel
16 comment_model = CommentsModel()
17 comment_model = CommentsModel()
18
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
21 visible_for_user = True
22 if comment.draft:
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
17 %>
24 %>
18 <% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
19 <% latest_ver = len(getattr(c, 'versions', [])) %>
20
25
21 % if inline:
26 % if inline:
22 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
@@ -24,6 +29,7 b''
24 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
25 % endif
30 % endif
26
31
32 % if visible_for_user:
27 <div class="comment
33 <div class="comment
28 ${'comment-inline' if inline else 'comment-general'}
34 ${'comment-inline' if inline else 'comment-general'}
29 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
@@ -31,14 +37,26 b''
31 line="${comment.line_no}"
37 line="${comment.line_no}"
32 data-comment-id="${comment.comment_id}"
38 data-comment-id="${comment.comment_id}"
33 data-comment-type="${comment.comment_type}"
39 data-comment-type="${comment.comment_type}"
40 data-comment-draft=${h.json.dumps(comment.draft)}
34 data-comment-renderer="${comment.renderer}"
41 data-comment-renderer="${comment.renderer}"
35 data-comment-text="${comment.text | html_filters.base64,n}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
36 data-comment-line-no="${comment.line_no}"
44 data-comment-line-no="${comment.line_no}"
37 data-comment-inline=${h.json.dumps(inline)}
45 data-comment-inline=${h.json.dumps(inline)}
38 style="${'display: none;' if outdated_at_ver else ''}">
46 style="${'display: none;' if outdated_at_ver else ''}">
39
47
40 <div class="meta">
48 <div class="meta">
41 <div class="comment-type-label">
49 <div class="comment-type-label">
50 % if comment.draft:
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
53 </div>
54 % elif is_new:
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
56 NEW
57 </div>
58 % endif
59
42 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
43
61
44 ## TODO COMMENT
62 ## TODO COMMENT
@@ -90,7 +108,7 b''
90
108
91 </div>
109 </div>
92 </div>
110 </div>
93
111 ## NOTE 0 and .. => because we disable it for now until UI ready
94 % if 0 and comment.status_change:
112 % if 0 and comment.status_change:
95 <div class="pull-left">
113 <div class="pull-left">
96 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
@@ -100,10 +118,12 b''
100 </span>
118 </span>
101 </div>
119 </div>
102 % endif
120 % endif
103
121 ## Since only author can see drafts, we don't show it
122 % if not comment.draft:
104 <div class="author ${'author-inline' if inline else 'author-general'}">
123 <div class="author ${'author-inline' if inline else 'author-general'}">
105 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
106 </div>
125 </div>
126 % endif
107
127
108 <div class="date">
128 <div class="date">
109 ${h.age_component(comment.modified_at, time_is_local=True)}
129 ${h.age_component(comment.modified_at, time_is_local=True)}
@@ -164,7 +184,7 b''
164 % if inline:
184 % if inline:
165 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
166 % if outdated_at_ver:
186 % if outdated_at_ver:
167 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
168 <code class="action-divider">|</code>
188 <code class="action-divider">|</code>
169 % elif comment_ver:
189 % elif comment_ver:
170 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
@@ -210,11 +230,17 b''
210 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
211 <div class="dropdown-divider"></div>
231 <div class="dropdown-divider"></div>
212 <div class="dropdown-item">
232 <div class="dropdown-item">
213 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
214 </div>
234 </div>
215 <div class="dropdown-item">
235 <div class="dropdown-item">
216 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
217 </div>
237 </div>
238 ## Only available in EE edition
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
240 <div class="dropdown-item">
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
242 </div>
243 % endif
218 %else:
244 %else:
219 <div class="dropdown-divider"></div>
245 <div class="dropdown-divider"></div>
220 <div class="dropdown-item">
246 <div class="dropdown-item">
@@ -252,6 +278,7 b''
252 </div>
278 </div>
253
279
254 </div>
280 </div>
281 % endif
255 </%def>
282 </%def>
256
283
257 ## generate main comments
284 ## generate main comments
@@ -298,10 +325,9 b''
298 ## inject form here
325 ## inject form here
299 </div>
326 </div>
300 <script type="text/javascript">
327 <script type="text/javascript">
301 var lineNo = 'general';
302 var resolvesCommentId = null;
328 var resolvesCommentId = null;
303 var generalCommentForm = Rhodecode.comments.createGeneralComment(
329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
304 lineNo, "${placeholder}", resolvesCommentId);
330 'general', "${placeholder}", resolvesCommentId);
305
331
306 // set custom success callback on rangeCommit
332 // set custom success callback on rangeCommit
307 % if is_compare:
333 % if is_compare:
@@ -311,6 +337,7 b''
311 var text = self.cm.getValue();
337 var text = self.cm.getValue();
312 var status = self.getCommentStatus();
338 var status = self.getCommentStatus();
313 var commentType = self.getCommentType();
339 var commentType = self.getCommentType();
340 var isDraft = self.getDraftState();
314
341
315 if (text === "" && !status) {
342 if (text === "" && !status) {
316 return;
343 return;
@@ -337,6 +364,7 b''
337 'text': text,
364 'text': text,
338 'changeset_status': status,
365 'changeset_status': status,
339 'comment_type': commentType,
366 'comment_type': commentType,
367 'draft': isDraft,
340 'commit_ids': commitIds,
368 'commit_ids': commitIds,
341 'csrf_token': CSRF_TOKEN
369 'csrf_token': CSRF_TOKEN
342 };
370 };
@@ -371,7 +399,7 b''
371
399
372 <div class="comment-area-write" style="display: block;">
400 <div class="comment-area-write" style="display: block;">
373 <div id="edit-container">
401 <div id="edit-container">
374 <div style="padding: 40px 0">
402 <div style="padding: 20px 0px 0px 0;">
375 ${_('You need to be logged in to leave comments.')}
403 ${_('You need to be logged in to leave comments.')}
376 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
377 </div>
405 </div>
@@ -430,7 +458,7 b''
430 </div>
458 </div>
431
459
432 <div class="comment-area-write" style="display: block;">
460 <div class="comment-area-write" style="display: block;">
433 <div id="edit-container_${lineno_id}">
461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
434 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
435 </div>
463 </div>
436 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
@@ -477,39 +505,50 b''
477 <div class="action-buttons-extra"></div>
505 <div class="action-buttons-extra"></div>
478 % endif
506 % endif
479
507
480 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
508 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
509
510 % if form_type == 'inline':
511 % if c.rhodecode_edition_id == 'EE':
512 ## Disable the button for CE, the "real" validation is in the backend code anyway
513 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
514 % else:
515 <input class="btn btn-warning comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
516 % endif
517 % endif
518
519 % if review_statuses:
520 <div class="comment-status-box">
521 <select id="change_status_${lineno_id}" name="changeset_status">
522 <option></option> ## Placeholder
523 % for status, lbl in review_statuses:
524 <option value="${status}" data-status="${status}">${lbl}</option>
525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
527 %endif
528 % endfor
529 </select>
530 </div>
531 % endif
481
532
482 ## inline for has a file, and line-number together with cancel hide button.
533 ## inline for has a file, and line-number together with cancel hide button.
483 % if form_type == 'inline':
534 % if form_type == 'inline':
484 <input type="hidden" name="f_path" value="{0}">
535 <input type="hidden" name="f_path" value="{0}">
485 <input type="hidden" name="line" value="${lineno_id}">
536 <input type="hidden" name="line" value="${lineno_id}">
486 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
537 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
487 ${_('Cancel')}
538 <i class="icon-cancel-circled2"></i>
488 </button>
539 </button>
489 % endif
540 % endif
490 </div>
541 </div>
491
542
492 % if review_statuses:
493 <div class="status_box">
494 <select id="change_status_${lineno_id}" name="changeset_status">
495 <option></option> ## Placeholder
496 % for status, lbl in review_statuses:
497 <option value="${status}" data-status="${status}">${lbl}</option>
498 %if is_pull_request and change_status and status in ('approved', 'rejected'):
499 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
500 %endif
501 % endfor
502 </select>
503 </div>
504 % endif
505
506 <div class="toolbar-text">
543 <div class="toolbar-text">
507 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
508 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
545 <span>${_('{} is supported.').format(renderer_url)|n}
509 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
546
510 ${_('and')}
547 <i class="icon-info-circled tooltip-hovercard"
511 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
548 data-hovercard-alt="ALT"
512 ${_('actions supported.')}
549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
551 </span>
513 </div>
552 </div>
514 </div>
553 </div>
515
554
@@ -104,7 +104,9 b''
104 %for commit in c.commit_ranges:
104 %for commit in c.commit_ranges:
105 ## commit range header for each individual diff
105 ## commit range header for each individual diff
106 <h3>
106 <h3>
107 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
107 <a class="tooltip-hovercard revision" data-hovercard-alt="Commit: ${commit.short_id}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=commit.raw_id)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
108 ${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
109 </a>
108 </h3>
110 </h3>
109
111
110 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
112 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
@@ -1,3 +1,4 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
3
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
@@ -74,24 +75,9 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
74
75
75 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="comment-inline-form ac">
77 <div class="comment-inline-form ac">
77
78 %if not c.rhodecode_user.is_default:
78 %if c.rhodecode_user.username != h.DEFAULT_USER:
79 ## render template for inline comments
79 ## render template for inline comments
80 ${commentblock.comment_form(form_type='inline')}
80 ${commentblock.comment_form(form_type='inline')}
81 %else:
82 ${h.form('', class_='inline-form comment-form-login', method='get')}
83 <div class="pull-left">
84 <div class="comment-help pull-right">
85 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
86 </div>
87 </div>
88 <div class="comment-button pull-right">
89 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
90 ${_('Cancel')}
91 </button>
92 </div>
93 <div class="clearfix"></div>
94 ${h.end_form()}
95 %endif
81 %endif
96 </div>
82 </div>
97 </div>
83 </div>
@@ -287,7 +273,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
287 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
288 <%
274 <%
289 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
290 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
291 %>
277 %>
292 <div class="filediff-collapse-indicator icon-"></div>
278 <div class="filediff-collapse-indicator icon-"></div>
293
279
@@ -327,7 +313,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
327 </label>
313 </label>
328
314
329 ${diff_menu(filediff, use_comments=use_comments)}
315 ${diff_menu(filediff, use_comments=use_comments)}
330 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
331
317
332 ## new/deleted/empty content case
318 ## new/deleted/empty content case
333 % if not filediff.hunks:
319 % if not filediff.hunks:
@@ -626,8 +612,10 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
626
612
627 % if use_comments:
613 % if use_comments:
628 |
614 |
629 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
630 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
631 </a>
619 </a>
632 % endif
620 % endif
633
621
@@ -637,23 +625,37 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
637 </%def>
625 </%def>
638
626
639
627
640 <%def name="inline_comments_container(comments, active_pattern_entries=None)">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
641
629
642 <div class="inline-comments">
630 <div class="inline-comments">
643 %for comment in comments:
631 %for comment in comments:
644 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
645 %endfor
633 %endfor
646 % if comments and comments[-1].outdated:
634
647 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
635 <%
648 ${_('Add another comment')}
636 extra_class = ''
649 </span>
637 extra_style = ''
650 % else:
638
651 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
639 if comments and comments[-1].outdated_at_version(c.at_version_num):
652 ${_('Add another comment')}
640 extra_class = ' comment-outdated'
653 </span>
641 extra_style = 'display: none;'
654 % endif
642
643 %>
655
644
645 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
646 <div class="reply-thread-container${extra_class}">
647 <div class="reply-thread-gravatar">
648 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
649 </div>
650 <div class="reply-thread-reply-button">
651 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
652 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
653 </div>
654 <div class="reply-thread-last"></div>
655 </div>
656 </div>
656 </div>
657 </div>
658
657 </%def>
659 </%def>
658
660
659 <%!
661 <%!
@@ -711,16 +713,19 b' def get_comments_for(diff_type, comments'
711 data-line-no="${line.original.lineno}"
713 data-line-no="${line.original.lineno}"
712 >
714 >
713
715
714 <% line_old_comments = None %>
716 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 %if line.original.get_comment_args:
717 %if line.original.get_comment_args:
716 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
718 <%
719 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
720 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
721 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
722 %>
717 %endif
723 %endif
718 %if line_old_comments:
724 %if line_old_comments_no_drafts:
719 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
720 % if has_outdated:
725 % if has_outdated:
721 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
726 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
722 % else:
727 % else:
723 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
728 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
724 % endif
729 % endif
725 %endif
730 %endif
726 </td>
731 </td>
@@ -734,16 +739,18 b' def get_comments_for(diff_type, comments'
734 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
739 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
735 %endif
740 %endif
736 </td>
741 </td>
742
743 <% line_no = 'o{}'.format(line.original.lineno) %>
737 <td class="cb-content ${action_class(line.original.action)}"
744 <td class="cb-content ${action_class(line.original.action)}"
738 data-line-no="o${line.original.lineno}"
745 data-line-no="${line_no}"
739 >
746 >
740 %if use_comments and line.original.lineno:
747 %if use_comments and line.original.lineno:
741 ${render_add_comment_button()}
748 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
742 %endif
749 %endif
743 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
750 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
744
751
745 %if use_comments and line.original.lineno and line_old_comments:
752 %if use_comments and line.original.lineno and line_old_comments:
746 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
753 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
747 %endif
754 %endif
748
755
749 </td>
756 </td>
@@ -752,18 +759,20 b' def get_comments_for(diff_type, comments'
752 >
759 >
753 <div>
760 <div>
754
761
762 <% line_new_comments, line_new_comments_no_drafts = None, None %>
755 %if line.modified.get_comment_args:
763 %if line.modified.get_comment_args:
756 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
764 <%
757 %else:
765 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
758 <% line_new_comments = None%>
766 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
767 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
768 %>
759 %endif
769 %endif
760 %if line_new_comments:
761
770
762 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
771 %if line_new_comments_no_drafts:
763 % if has_outdated:
772 % if has_outdated:
764 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
773 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
765 % else:
774 % else:
766 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
775 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
767 % endif
776 % endif
768 %endif
777 %endif
769 </div>
778 </div>
@@ -778,22 +787,25 b' def get_comments_for(diff_type, comments'
778 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
787 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
779 %endif
788 %endif
780 </td>
789 </td>
790
791 <% line_no = 'n{}'.format(line.modified.lineno) %>
781 <td class="cb-content ${action_class(line.modified.action)}"
792 <td class="cb-content ${action_class(line.modified.action)}"
782 data-line-no="n${line.modified.lineno}"
793 data-line-no="${line_no}"
783 >
794 >
784 %if use_comments and line.modified.lineno:
795 %if use_comments and line.modified.lineno:
785 ${render_add_comment_button()}
796 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
786 %endif
797 %endif
787 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
798 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
788 %if use_comments and line.modified.lineno and line_new_comments:
789 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
790 %endif
791 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
799 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
792 <div class="nav-chunk" style="visibility: hidden">
800 <div class="nav-chunk" style="visibility: hidden">
793 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
801 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
794 </div>
802 </div>
795 <% chunk_count +=1 %>
803 <% chunk_count +=1 %>
796 % endif
804 % endif
805 %if use_comments and line.modified.lineno and line_new_comments:
806 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
807 %endif
808
797 </td>
809 </td>
798 </tr>
810 </tr>
799 %endfor
811 %endfor
@@ -814,20 +826,22 b' def get_comments_for(diff_type, comments'
814 <td class="cb-data ${action_class(action)}">
826 <td class="cb-data ${action_class(action)}">
815 <div>
827 <div>
816
828
817 %if comments_args:
829 <% comments, comments_no_drafts = None, None %>
818 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
830 %if comments_args:
819 %else:
831 <%
820 <% comments = None %>
832 comments = get_comments_for('unified', inline_comments, *comments_args)
821 %endif
833 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
834 has_outdated = any([x.outdated for x in comments_no_drafts])
835 %>
836 %endif
822
837
823 % if comments:
838 % if comments_no_drafts:
824 <% has_outdated = any([x.outdated for x in comments]) %>
839 % if has_outdated:
825 % if has_outdated:
840 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
826 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
841 % else:
827 % else:
842 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
828 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
843 % endif
829 % endif
844 % endif
830 % endif
831 </div>
845 </div>
832 </td>
846 </td>
833 <td class="cb-lineno ${action_class(action)}"
847 <td class="cb-lineno ${action_class(action)}"
@@ -850,15 +864,16 b' def get_comments_for(diff_type, comments'
850 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
864 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
851 %endif
865 %endif
852 </td>
866 </td>
867 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
853 <td class="cb-content ${action_class(action)}"
868 <td class="cb-content ${action_class(action)}"
854 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
869 data-line-no="${line_no}"
855 >
870 >
856 %if use_comments:
871 %if use_comments:
857 ${render_add_comment_button()}
872 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
858 %endif
873 %endif
859 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
874 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
860 %if use_comments and comments:
875 %if use_comments and comments:
861 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
876 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
862 %endif
877 %endif
863 </td>
878 </td>
864 </tr>
879 </tr>
@@ -879,10 +894,12 b' def get_comments_for(diff_type, comments'
879 </%def>file changes
894 </%def>file changes
880
895
881
896
882 <%def name="render_add_comment_button()">
897 <%def name="render_add_comment_button(line_no='', f_path='')">
883 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
898 % if not c.rhodecode_user.is_default:
899 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
884 <span><i class="icon-comment"></i></span>
900 <span><i class="icon-comment"></i></span>
885 </button>
901 </button>
902 % endif
886 </%def>
903 </%def>
887
904
888 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
905 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
@@ -108,7 +108,26 b''
108 <i class="icon-cancel-circled2"></i>
108 <i class="icon-cancel-circled2"></i>
109 </div>
109 </div>
110 <div class="btn btn-sm disabled" disabled="disabled" id="rev_range_more" style="display:none;">${_('Select second commit')}</div>
110 <div class="btn btn-sm disabled" disabled="disabled" id="rev_range_more" style="display:none;">${_('Select second commit')}</div>
111 <a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
111
112 <div id="rev_range_action" class="btn-group btn-group-actions" style="display:none;">
113 <a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
114
115 <a class="btn btn-success btn-sm btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button">
116 <i class="icon-down"></i>
117 </a>
118
119 <div class="btn-action-switcher-container right-align">
120 <ul class="btn-action-switcher" role="menu" style="min-width: 220px; width: max-content">
121 <li>
122 ## JS fills the URL
123 <a id="rev_range_combined_url" class="btn btn-primary btn-sm" href="">
124 ${_('Show combined diff')}
125 </a>
126 </li>
127 </ul>
128 </div>
129 </div>
130
112 </th>
131 </th>
113
132
114 ## commit message expand arrow
133 ## commit message expand arrow
@@ -147,6 +166,9 b''
147 var $commitRangeMore = $('#rev_range_more');
166 var $commitRangeMore = $('#rev_range_more');
148 var $commitRangeContainer = $('#rev_range_container');
167 var $commitRangeContainer = $('#rev_range_container');
149 var $commitRangeClear = $('#rev_range_clear');
168 var $commitRangeClear = $('#rev_range_clear');
169 var $commitRangeAction = $('#rev_range_action');
170 var $commitRangeCombinedUrl = $('#rev_range_combined_url');
171 var $compareFork = $('#compare_fork_button');
150
172
151 var checkboxRangeSelector = function(e){
173 var checkboxRangeSelector = function(e){
152 var selectedCheckboxes = [];
174 var selectedCheckboxes = [];
@@ -169,9 +191,8 b''
169 }
191 }
170
192
171 if (selectedCheckboxes.length > 0) {
193 if (selectedCheckboxes.length > 0) {
172 $('#compare_fork_button').hide();
194 $compareFork.hide();
173 var commitStart = $(selectedCheckboxes[selectedCheckboxes.length-1]).data();
195 var commitStart = $(selectedCheckboxes[selectedCheckboxes.length-1]).data();
174
175 var revStart = commitStart.commitId;
196 var revStart = commitStart.commitId;
176
197
177 var commitEnd = $(selectedCheckboxes[0]).data();
198 var commitEnd = $(selectedCheckboxes[0]).data();
@@ -181,7 +202,9 b''
181 var lbl_end = '{0}'.format(commitEnd.commitIdx, commitEnd.shortId);
202 var lbl_end = '{0}'.format(commitEnd.commitIdx, commitEnd.shortId);
182
203
183 var url = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd});
204 var url = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd});
184 var link = _gettext('Show commit range {0} ... {1}').format(lbl_start, lbl_end);
205 var urlCombined = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd, 'redirect_combined': '1'});
206
207 var link = _gettext('Show commit range {0}<i class="icon-angle-right"></i>{1}').format(lbl_start, lbl_end);
185
208
186 if (selectedCheckboxes.length > 1) {
209 if (selectedCheckboxes.length > 1) {
187 $commitRangeClear.show();
210 $commitRangeClear.show();
@@ -192,9 +215,12 b''
192 .html(link)
215 .html(link)
193 .show();
216 .show();
194
217
218 $commitRangeCombinedUrl.attr('href', urlCombined);
219 $commitRangeAction.show();
195
220
196 } else {
221 } else {
197 $commitRangeContainer.hide();
222 $commitRangeContainer.hide();
223 $commitRangeAction.hide();
198 $commitRangeClear.show();
224 $commitRangeClear.show();
199 $commitRangeMore.show();
225 $commitRangeMore.show();
200 }
226 }
@@ -212,6 +238,7 b''
212 $commitRangeContainer.hide();
238 $commitRangeContainer.hide();
213 $commitRangeClear.hide();
239 $commitRangeClear.hide();
214 $commitRangeMore.hide();
240 $commitRangeMore.hide();
241 $commitRangeAction.hide();
215
242
216 %if c.branch_name:
243 %if c.branch_name:
217 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'branch':'${c.branch_name}'});
244 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'branch':'${c.branch_name}'});
@@ -220,7 +247,7 b''
220 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
247 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
221 open_new_pull_request.attr('href', _url);
248 open_new_pull_request.attr('href', _url);
222 %endif
249 %endif
223 $('#compare_fork_button').show();
250 $compareFork.show();
224 }
251 }
225 };
252 };
226
253
@@ -397,7 +397,10 b''
397 % endif
397 % endif
398 </%def>
398 </%def>
399
399
400 <%def name="pullrequest_updated_on(updated_on)">
400 <%def name="pullrequest_updated_on(updated_on, pr_version=None)">
401 % if pr_version:
402 <code>v${pr_version}</code>
403 % endif
401 ${h.age_component(h.time_to_utcdatetime(updated_on))}
404 ${h.age_component(h.time_to_utcdatetime(updated_on))}
402 </%def>
405 </%def>
403
406
@@ -456,7 +459,7 b''
456 </div>
459 </div>
457
460
458 <div class="markup-form-area-write" style="display: block;">
461 <div class="markup-form-area-write" style="display: block;">
459 <div id="edit-container_${form_id}">
462 <div id="edit-container_${form_id}" style="margin-top: -1px">
460 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
463 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
461 </div>
464 </div>
462 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
465 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
@@ -237,6 +237,24 b' if (show_disabled) {'
237
237
238 </script>
238 </script>
239
239
240 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
241
242 <div>
243 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
244 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
245 <br/>
246 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
247 <br/>
248 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
249 They are permanent until deleted, or converted to regular comments.<br/>
250 <br/>
251 <br/>
252 </div>
253
254 </script>
255
256
257
240 ##// END OF EJS Templates
258 ##// END OF EJS Templates
241 </div>
259 </div>
242
260
@@ -337,7 +337,6 b' text_monospace = "\'Menlo\', \'Liberation M'
337 div.markdown-block img {
337 div.markdown-block img {
338 border-style: none;
338 border-style: none;
339 background-color: #fff;
339 background-color: #fff;
340 padding-right: 20px;
341 max-width: 100%
340 max-width: 100%
342 }
341 }
343
342
@@ -395,6 +394,13 b' text_monospace = "\'Menlo\', \'Liberation M'
395 background-color: #eeeeee
394 background-color: #eeeeee
396 }
395 }
397
396
397 div.markdown-block p {
398 margin-top: 0;
399 margin-bottom: 16px;
400 padding: 0;
401 line-height: unset;
402 }
403
398 div.markdown-block code,
404 div.markdown-block code,
399 div.markdown-block pre,
405 div.markdown-block pre,
400 div.markdown-block #ws,
406 div.markdown-block #ws,
@@ -82,8 +82,8 b''
82 <p>
82 <p>
83 <strong>Exception ID: <code><a href="${c.exception_id_url}">${c.exception_id}</a></code> </strong> <br/>
83 <strong>Exception ID: <code><a href="${c.exception_id_url}">${c.exception_id}</a></code> </strong> <br/>
84
84
85 Super-admins can see detailed traceback information from this exception by checking the below Exception ID.<br/>
85 Super-admins can see details of the above error in the exception tracker found under
86 Please include the above link for further details of this exception.
86 <a href="${h.route_url('admin_settings_exception_tracker')}">admin > settings > exception tracker</a>.
87 </p>
87 </p>
88 </div>
88 </div>
89 % endif
89 % endif
@@ -29,7 +29,7 b''
29 </a>
29 </a>
30
30
31 <div class="btn-action-switcher-container right-align">
31 <div class="btn-action-switcher-container right-align">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
33 <li>
33 <li>
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
35 <i class="icon-upload"></i>
35 <i class="icon-upload"></i>
@@ -44,18 +44,41 b''
44 % endif
44 % endif
45
45
46 % if c.enable_downloads:
46 % if c.enable_downloads:
47 <% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
47 <%
48 <div class="btn btn-default new-file">
48 at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6])
49 % if c.f_path == '/':
49 if c.f_path == '/':
50 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
50 label = _('Full tree as {}')
51 ${_('Download full tree ZIP')}
51 _query = {'with_hash': '1'}
52 else:
53 label = _('This tree as {}')
54 _query = {'at_path':c.f_path, 'with_hash': '1'}
55 %>
56
57 <div class="btn-group btn-group-actions new-file">
58 <a class="archive_link btn btn-default" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname='{}{}'.format(c.commit.raw_id, '.zip'), _query=_query)}">
59 <i class="icon-download"></i>
60 ${label.format('.zip')}
52 </a>
61 </a>
53 % else:
62
54 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id), _query={'at_path':c.f_path})}">
63 <a class="tooltip btn btn-default btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
55 ${_('Download this tree ZIP')}
64 <i class="icon-down"></i>
56 </a>
65 </a>
57 % endif
66
58 </div>
67 <div class="btn-action-switcher-container left-align">
68 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
69 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
70 % if extension not in ['.zip']:
71 <li>
72 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname='{}{}'.format(c.commit.raw_id, extension), _query=_query)}">
73 <i class="icon-download"></i>
74 ${label.format(extension)}
75 </a>
76 </li>
77 % endif
78 % endfor
79 </ul>
80 </div>
81 </div>
59 % endif
82 % endif
60
83
61 <div class="files-quick-filter">
84 <div class="files-quick-filter">
@@ -7,10 +7,10 b''
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10 <style>body{background-color:#eeeeee;}</style>
10
11
11 <style>body{background-color:#eeeeee;}</style>
12 <div class="loginbox">
12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 <div id="header-inner" class="title">
14 <div id="header-inner" class="title">
15 <div id="logo">
15 <div id="logo">
16 <div class="logo-wrapper">
16 <div class="logo-wrapper">
@@ -28,12 +28,12 b''
28 <div class="loginwrapper">
28 <div class="loginwrapper">
29 <rhodecode-toast id="notifications"></rhodecode-toast>
29 <rhodecode-toast id="notifications"></rhodecode-toast>
30
30
31 <div class="left-column">
31 <div class="auth-image-wrapper">
32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
33 </div>
33 </div>
34
34
35 <%block name="above_login_button" />
35 <div id="login">
36 <div id="login" class="right-column">
36 <%block name="above_login_button" />
37 <!-- login -->
37 <!-- login -->
38 <div class="sign-in-title">
38 <div class="sign-in-title">
39 <h1>${_('Sign In using username/password')}</h1>
39 <h1>${_('Sign In using username/password')}</h1>
@@ -10,7 +10,7 b''
10 <style>body{background-color:#eeeeee;}</style>
10 <style>body{background-color:#eeeeee;}</style>
11
11
12 <div class="loginbox">
12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 <div id="header-inner" class="title">
14 <div id="header-inner" class="title">
15 <div id="logo">
15 <div id="logo">
16 <div class="logo-wrapper">
16 <div class="logo-wrapper">
@@ -27,7 +27,8 b''
27
27
28 <div class="loginwrapper">
28 <div class="loginwrapper">
29 <rhodecode-toast id="notifications"></rhodecode-toast>
29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 <div class="left-column">
30
31 <div class="auth-image-wrapper">
31 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 </div>
33 </div>
33
34
@@ -43,7 +44,7 b''
43 </p>
44 </p>
44 </div>
45 </div>
45 %else:
46 %else:
46 <div id="register" class="right-column">
47 <div id="register">
47 <!-- login -->
48 <!-- login -->
48 <div class="sign-in-title">
49 <div class="sign-in-title">
49 <h1>${_('Reset your Password')}</h1>
50 <h1>${_('Reset your Password')}</h1>
@@ -32,8 +32,6 b''
32 %>
32 %>
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 // TODO: marcink switch this to pyroutes
36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
39 </script>
37 </script>
@@ -552,6 +550,42 b''
552 ## CONTENT
550 ## CONTENT
553 <div class="sidebar-content">
551 <div class="sidebar-content">
554
552
553 ## Drafts
554 % if c.rhodecode_edition_id == 'EE':
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 <i class="icon-comment icon-draft"></i>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
559 </div>
560
561 <div class="right-sidebar-expanded-state pr-details-title">
562 <span style="padding-left: 2px">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
564 </span>
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
566 <i class="icon-comment icon-draft"></i>
567 ${_('Drafts')}
568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
570 </div>
571
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
573 % if c.draft_comments:
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 % else:
576 <table class="drafts-content-table">
577 <tr>
578 <td>
579 ${_('No TODOs yet')}
580 </td>
581 </tr>
582 </table>
583 % endif
584 </div>
585
586 </div>
587 % endif
588
555 ## RULES SUMMARY/RULES
589 ## RULES SUMMARY/RULES
556 <div class="sidebar-element clear-both">
590 <div class="sidebar-element clear-both">
557 <% vote_title = _ungettext(
591 <% vote_title = _ungettext(
@@ -678,7 +712,7 b''
678 % endif
712 % endif
679
713
680 ## TODOs
714 ## TODOs
681 <div class="sidebar-element clear-both">
715 <div id="todosTable" class="sidebar-element clear-both">
682 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
716 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
683 <i class="icon-flag-filled"></i>
717 <i class="icon-flag-filled"></i>
684 <span id="todos-count">${len(c.unresolved_comments)}</span>
718 <span id="todos-count">${len(c.unresolved_comments)}</span>
@@ -712,7 +746,7 b''
712 % if c.unresolved_comments + c.resolved_comments:
746 % if c.unresolved_comments + c.resolved_comments:
713 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
747 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
714 % else:
748 % else:
715 <table>
749 <table class="todos-content-table">
716 <tr>
750 <tr>
717 <td>
751 <td>
718 ${_('No TODOs yet')}
752 ${_('No TODOs yet')}
@@ -725,7 +759,7 b''
725 </div>
759 </div>
726
760
727 ## COMMENTS
761 ## COMMENTS
728 <div class="sidebar-element clear-both">
762 <div id="commentsTable" class="sidebar-element clear-both">
729 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
763 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
730 <i class="icon-comment" style="color: #949494"></i>
764 <i class="icon-comment" style="color: #949494"></i>
731 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
765 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
@@ -763,7 +797,7 b''
763 % if c.inline_comments_flat + c.comments:
797 % if c.inline_comments_flat + c.comments:
764 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
798 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
765 % else:
799 % else:
766 <table>
800 <table class="comments-content-table">
767 <tr>
801 <tr>
768 <td>
802 <td>
769 ${_('No Comments yet')}
803 ${_('No Comments yet')}
@@ -846,6 +880,7 b' versionController.init();'
846
880
847 reviewersController = new ReviewersController();
881 reviewersController = new ReviewersController();
848 commitsController = new CommitsController();
882 commitsController = new CommitsController();
883 commentsController = new CommentsController();
849
884
850 updateController = new UpdatePrController();
885 updateController = new UpdatePrController();
851
886
@@ -891,6 +926,23 b' window.setObserversData = ${c.pull_reque'
891 );
926 );
892 };
927 };
893
928
929 window.submitDrafts = function (event) {
930 var target = $(event.currentTarget);
931 var callback = function (result) {
932 target.removeAttr('onclick').html('saving...');
933 }
934 var draftIds = [];
935 $.each($('[name=submit_draft]:checked'), function (idx, val) {
936 draftIds.push(parseInt($(val).val()));
937 })
938 if (draftIds.length > 0) {
939 Rhodecode.comments.finalizeDrafts(draftIds, callback);
940 }
941 else {
942
943 }
944 }
945
894 window.closePullRequest = function (status) {
946 window.closePullRequest = function (status) {
895 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
947 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
896 return false;
948 return false;
@@ -980,9 +1032,11 b' window.setObserversData = ${c.pull_reque'
980 $(btns).each(fn_display);
1032 $(btns).each(fn_display);
981 });
1033 });
982
1034
983 // register submit callback on commentForm form to track TODOs
1035 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
984 window.commentFormGlobalSubmitSuccessCallback = function () {
1036 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
985 refreshMergeChecks();
1037 if (!comment.draft) {
1038 refreshMergeChecks();
1039 }
986 };
1040 };
987
1041
988 ReviewerAutoComplete('#user', reviewersController);
1042 ReviewerAutoComplete('#user', reviewersController);
@@ -994,7 +1048,8 b' window.setObserversData = ${c.pull_reque'
994
1048
995 var channel = '${c.pr_broadcast_channel}';
1049 var channel = '${c.pr_broadcast_channel}';
996 new ReviewerPresenceController(channel)
1050 new ReviewerPresenceController(channel)
997
1051 // register globally so inject comment logic can re-use it.
1052 window.commentsController = commentsController;
998 })
1053 })
999 </script>
1054 </script>
1000
1055
@@ -74,6 +74,8 b''
74 $pullRequestListTable.DataTable({
74 $pullRequestListTable.DataTable({
75 processing: true,
75 processing: true,
76 serverSide: true,
76 serverSide: true,
77 stateSave: true,
78 stateDuration: -1,
77 ajax: {
79 ajax: {
78 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
79 "data": function (d) {
81 "data": function (d) {
@@ -114,6 +116,10 b''
114 if (data['closed']) {
116 if (data['closed']) {
115 $(row).addClass('closed');
117 $(row).addClass('closed');
116 }
118 }
119 },
120 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
117 }
123 }
118 });
124 });
119
125
@@ -10,7 +10,7 b''
10 <style>body{background-color:#eeeeee;}</style>
10 <style>body{background-color:#eeeeee;}</style>
11
11
12 <div class="loginbox">
12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 <div id="header-inner" class="title">
14 <div id="header-inner" class="title">
15 <div id="logo">
15 <div id="logo">
16 <div class="logo-wrapper">
16 <div class="logo-wrapper">
@@ -27,11 +27,13 b''
27
27
28 <div class="loginwrapper">
28 <div class="loginwrapper">
29 <rhodecode-toast id="notifications"></rhodecode-toast>
29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 <div class="left-column">
30
31 <div class="auth-image-wrapper">
31 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 </div>
33 </div>
33 <%block name="above_register_button" />
34
34 <div id="register" class="right-column">
35 <div id="register">
36 <%block name="above_register_button" />
35 <!-- login -->
37 <!-- login -->
36 <div class="sign-in-title">
38 <div class="sign-in-title">
37 % if external_auth_provider:
39 % if external_auth_provider:
@@ -187,7 +187,7 b''
187 <div class="enabled pull-left" style="margin-right: 10px">
187 <div class="enabled pull-left" style="margin-right: 10px">
188
188
189 <div class="btn-group btn-group-actions">
189 <div class="btn-group btn-group-actions">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip')}">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip', _query={'with_hash': '1'})}">
191 <i class="icon-download"></i>
191 <i class="icon-download"></i>
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
193 ## replaced by some JS on select
193 ## replaced by some JS on select
@@ -198,12 +198,11 b''
198 </a>
198 </a>
199
199
200 <div class="btn-action-switcher-container left-align">
200 <div class="btn-action-switcher-container left-align">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
203 % if extension not in ['.zip']:
203 % if extension not in ['.zip']:
204 <li>
204 <li>
205
205 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension, _query={'with_hash': '1'})}">
206 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension)}">
207 <i class="icon-download"></i>
206 <i class="icon-download"></i>
208 ${c.rhodecode_db_repo.landing_ref_name+extension}
207 ${c.rhodecode_db_repo.landing_ref_name+extension}
209 </a>
208 </a>
@@ -49,23 +49,32 b' class TestArchives(BackendTestMixin):'
49 @classmethod
49 @classmethod
50 def _get_commits(cls):
50 def _get_commits(cls):
51 start_date = datetime.datetime(2010, 1, 1, 20)
51 start_date = datetime.datetime(2010, 1, 1, 20)
52 yield {
53 'message': 'Initial Commit',
54 'author': 'Joe Doe <joe.doe@example.com>',
55 'date': start_date + datetime.timedelta(hours=12),
56 'added': [
57 FileNode('executable_0o100755', '...', mode=0o100755),
58 FileNode('executable_0o100500', '...', mode=0o100500),
59 FileNode('not_executable', '...', mode=0o100644),
60 ],
61 }
52 for x in range(5):
62 for x in range(5):
53 yield {
63 yield {
54 'message': 'Commit %d' % x,
64 'message': 'Commit %d' % x,
55 'author': 'Joe Doe <joe.doe@example.com>',
65 'author': 'Joe Doe <joe.doe@example.com>',
56 'date': start_date + datetime.timedelta(hours=12 * x),
66 'date': start_date + datetime.timedelta(hours=12 * x),
57 'added': [
67 'added': [
58 FileNode(
68 FileNode('%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
59 '%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
60 ],
69 ],
61 }
70 }
62
71
63 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
72 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
64 def test_archive_tar(self, compressor):
73 def test_archive_tar(self, compressor):
65 self.tip.archive_repo(
74 self.tip.archive_repo(
66 self.temp_file, kind='t' + compressor, prefix='repo')
75 self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
67 out_dir = tempfile.mkdtemp()
76 out_dir = tempfile.mkdtemp()
68 out_file = tarfile.open(self.temp_file, 'r|' + compressor)
77 out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
69 out_file.extractall(out_dir)
78 out_file.extractall(out_dir)
70 out_file.close()
79 out_file.close()
71
80
@@ -77,8 +86,24 b' class TestArchives(BackendTestMixin):'
77
86
78 shutil.rmtree(out_dir)
87 shutil.rmtree(out_dir)
79
88
89 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
90 def test_archive_tar_symlink(self, compressor):
91 return False
92
93 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
94 def test_archive_tar_file_modes(self, compressor):
95 self.tip.archive_repo(
96 self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
97 out_dir = tempfile.mkdtemp()
98 out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
99 out_file.extractall(out_dir)
100 out_file.close()
101 dest = lambda inp: os.path.join(out_dir, 'repo/' + inp)
102
103 assert oct(os.stat(dest('not_executable')).st_mode) == '0100644'
104
80 def test_archive_zip(self):
105 def test_archive_zip(self):
81 self.tip.archive_repo(self.temp_file, kind='zip', prefix='repo')
106 self.tip.archive_repo(self.temp_file, kind='zip', archive_dir_name='repo')
82 out = zipfile.ZipFile(self.temp_file)
107 out = zipfile.ZipFile(self.temp_file)
83
108
84 for x in range(5):
109 for x in range(5):
@@ -91,10 +116,10 b' class TestArchives(BackendTestMixin):'
91
116
92 def test_archive_zip_with_metadata(self):
117 def test_archive_zip_with_metadata(self):
93 self.tip.archive_repo(self.temp_file, kind='zip',
118 self.tip.archive_repo(self.temp_file, kind='zip',
94 prefix='repo', write_metadata=True)
119 archive_dir_name='repo', write_metadata=True)
95
120
96 out = zipfile.ZipFile(self.temp_file)
121 out = zipfile.ZipFile(self.temp_file)
97 metafile = out.read('.archival.txt')
122 metafile = out.read('repo/.archival.txt')
98
123
99 raw_id = self.tip.raw_id
124 raw_id = self.tip.raw_id
100 assert 'commit_id:%s' % raw_id in metafile
125 assert 'commit_id:%s' % raw_id in metafile
General Comments 0
You need to be logged in to leave comments. Login now